Current section
Files
Jump to
Current section
Files
priv/scripts/bsd_install_script.sh
#!/bin/sh
set -eu
#
# The script that does the actual install on the remote host.
# This script is self contained and runs on the target host machine.
#
# This script is typically called by calling bsd_install.sh from the remote host.
#
# You can run this script directly on the target host:
#
# ## Example on target host
#
# bsd_install_script.sh --postgres-init
#
# add this to usage docs
# bsd_install_script \
# --output-format=[ansi|json] \
# --quiet \
# --pkg git \
# --path /usr/local/lib/erlang27/bin \
# --dot-path some/additional/path \
# --service postgresql \
# --postgres-init \
# --postgres-db-us_utf8_full mydb1 \
# --postgres-db-c_mixed_utf8 mydb2 \
# Default output format and quiet mode
OUTPUT_FORMAT="ansi"
QUIET="false"
ELIXIR_VERSION=""
COMMANDS="" # Use a string to store commands
CUSTOM_PATHS=""
# Define colors
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
WHITE='\033[0;37m'
RESET='\033[0m' # No Color
## todo: add logging
LOG_FILE="/var/log/bsd_install.log"
log() {
STATUS="$1"
MESSAGE="$2"
case $STATUS in
success)
COLOR=$GREEN
LABEL="[SUCCESS]"
;;
error)
COLOR=$RED
LABEL="[ERROR]"
;;
esac
doas printf "$(date '+%Y-%m-%d %H:%M:%S') $LABEL $MESSAGE\n" | tee -a "$LOG_FILE"
}
#
# Function: puts status message
#
# Description:
#
# Outputs messages based on state: success, error, debug, info, warn.
#
puts() {
STATUS="$1"
MESSAGE="$2"
case $STATUS in
success)
COLOR=$GREEN
LABEL="[SUCCESS]"
;;
error)
COLOR=$RED
LABEL="[ERROR]"
;;
debug)
COLOR=$YELLOW
LABEL="[DEBUG]"
;;
info)
COLOR=$YELLOW
LABEL="[INFO]"
;;
warn)
COLOR=$YELLOW
LABEL="[WARN]"
;;
*)
COLOR=$WHITE
LABEL=""
;;
esac
case "$OUTPUT_FORMAT" in
ansi)
printf "${COLOR}${LABEL} ${MESSAGE}${RESET}\n"
;;
json)
# Escape quotes and backslashes in message
ESCAPED_MESSAGE=$(echo "$MESSAGE" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g')
# Append JSON object to JSON_MESSAGES
if [ -z "$JSON_MESSAGES" ]; then
JSON_MESSAGES="{\"status\":\"$STATUS\",\"message\":\"$ESCAPED_MESSAGE\"}"
else
JSON_MESSAGES="${JSON_MESSAGES}, {\"status\":\"$STATUS\",\"message\":\"$ESCAPED_MESSAGE\"}"
fi
;;
*)
printf "${RED}[ERROR]${RESET} Unknown output format: ${OUTPUT_FORMAT}\n"
exit 1
;;
esac
}
exit_message() {
# Output JSON_MESSAGES if in JSON format
if [ "$OUTPUT_FORMAT" = "json" ]; then
echo "[${JSON_MESSAGES}]"
fi
}
#
# Parse command-line arguments
#
while [ $# -gt 0 ]; do
case "$1" in
--output-format=*)
OUTPUT_FORMAT="${1#*=}"
;;
--quiet)
QUIET="true"
;;
--pkg)
shift
if [ -n "$1" ]; then
COMMANDS="$COMMANDS pkg:$1"
else
echo "${RED}[ERROR]${RESET} --pkg requires an argument"
exit 1
fi
;;
--service)
shift
if [ -n "$1" ]; then
COMMANDS="$COMMANDS service:$1"
else
echo "${RED}[ERROR]${RESET} --service requires an argument$"
exit 1
fi
;;
--elixir)
shift
if [ -n "$1" ]; then
ELIXIR_VERSION="$1"
COMMANDS="$COMMANDS elixir"
else
echo "${RED}[ERROR]${RESET} --elixir requires a version argument"
exit 1
fi
;;
--postgres-init)
# no arguments to shift
COMMANDS="$COMMANDS postgres-init"
;;
--postgres-db-c_mixed_utf8)
shift
if [ -n "$1" ]; then
COMMANDS="$COMMANDS postgres-db-c_mixed_utf8:$1"
else
echo "${RED}[ERROR]${RESET} --postgres-db-c_mixed_utf8 requires a database name argument"
exit 1
fi
;;
--postgres-db-c_utf8_full)
shift
if [ -n "$1" ]; then
COMMANDS="$COMMANDS postgres-db-c_utf8_full:$1"
else
echo "${RED}[ERROR]${RESET} --postgres-db-c_utf8_full requires a database name argument"
exit 1
fi
;;
--postgres-db-us_utf8_full)
shift
if [ -n "$1" ]; then
COMMANDS="$COMMANDS postgres-db-us_utf8_full:$1"
else
echo "${RED}[ERROR]${RESET} --postgres-db-us_utf8_full requires a database name argument"
exit 1
fi
;;
--path)
shift
if [ -n "$1" ]; then
CUSTOM_PATHS="$1:$CUSTOM_PATHS"
else
echo "${RED}[ERROR]${RESET} --path requires an argument"
exit 1
fi
;;
--dot-path)
shift
if [ -n "$1" ]; then
echo "$1" >".path"
else
echo "${RED}[ERROR]${RESET} --dot-path requires an argument"
exit 1
fi
;;
*)
# Unknown option
echo "Unknown option: '$1'"
exit 1
;;
esac
shift
done
# Update PATH with custom paths
if [ -n "$CUSTOM_PATHS" ]; then
PATH="$CUSTOM_PATHS$PATH"
export PATH
fi
# Initialize JSON_MESSAGES
JSON_MESSAGES=""
#
# Function: run_cmd
#
# Description:
#
# Run commands with optional quiet mode
#
run_cmd() {
if [ "$QUIET" = "true" ]; then
"$@" >/dev/null 2>&1
else
"$@"
fi
return $?
}
# Function to check if Elixir is installed
is_elixir_installed() {
if command -v elixir >/dev/null 2>&1; then
INSTALLED_VERSION=$(elixir --version | grep 'Elixir' | awk '{print $2}')
if [ "$INSTALLED_VERSION" = "$ELIXIR_VERSION" ]; then
return 0 # Elixir is installed and version matches
else
return 1 # Elixir is installed but version differs
fi
else
return 1 # Elixir is not installed
fi
}
#
# Function to install Elixir
#
# Description:
#
# Install Elixir from source
#
install_elixir() {
puts "info" "Installing Elixir v$ELIXIR_VERSION..."
WORKDIR=${WORKDIR:-$(mktemp -d -t elixir)}
if [ ! -d "$WORKDIR" ]; then
if ! run_cmd doas mkdir -p "$WORKDIR"; then
puts "error" "Failed to create directory $WORKDIR."
return 1
fi
fi
cd "$WORKDIR" || {
puts "error" "Failed to enter directory $WORKDIR."
return 1
}
# Clone the Elixir repository
if [ ! -d "elixir" ]; then
if ! run_cmd git clone https://github.com/elixir-lang/elixir.git; then
puts "error" "Failed to clone Elixir repository."
return 1
fi
else
puts "info" "Elixir repository already cloned."
fi
cd elixir || {
puts "error" "Failed to enter elixir directory."
return 1
}
# Fetch tags and checkout the desired version
if ! run_cmd git fetch --tags && run_cmd git checkout "v$ELIXIR_VERSION"; then
puts "error" "Failed to checkout Elixir version v$ELIXIR_VERSION."
cd ..
return 1
fi
# Compile Elixir
if ! run_cmd gmake; then
puts "error" "Failed to build Elixir."
cd ..
return 1
fi
# Install Elixir
if ! run_cmd doas gmake install; then
puts "error" "Failed to install Elixir."
cd ..
return 1
fi
cd ..
puts "success" "Elixir v$ELIXIR_VERSION installed successfully."
return 0
}
#
# Function: ip_address
#
# Description:
#
# Get the IP address of the current host.
#
ip_address() {
ifconfig | awk '/inet / && $2 != "127.0.0.1" {print $2}'
}
#
# Function: generate_credentials
#
# Description:
#
# Generate username and password
#
generate_credentials() {
username=$(uuidgen)
password=$(openssl rand -hex 24)
}
#
# Function: echo_credentials
#
# Description:
#
# Echo the username, password, and database name.
#
# Usage:
#
# echo_credentials $database_name
#
echo_credentials() {
puts "success" "Username: $username"
puts "success" "Password: $password"
puts "success" "Database: $DB"
puts "warn" "Record these credentials in a secure location."
puts "warn" "You will not be able to retrieve the password later."
}
#
# Function: init_postgres
#
# Description:
#
# Initializes Postgres server
#
init_postgres() {
POSTGRES_DB_PATH=/var/db/postgres
# Get the current major version of the installed PostgreSQL
PG_VERSION=$(pkg info postgresql* | grep -Eo 'postgresql[0-9]+-client' | sed -E 's/postgresql([0-9]+)-client/\1/')
POSTGRES_CONF_FILE="${POSTGRES_DB_PATH}/data${PG_VERSION}/postgresql.conf"
# Configure Sysrc
if run_cmd doas sysrc postgresql_enable="YES"; then
puts "success" "[INFO] Enabled postgresql service."
else
puts "error" "[ERROR] Failed to enable postgresql in /etc/rc.conf."
exit_message
exit 1
fi
# Run initdb if the data directory doesn't exist
# Check if ${POSTGRES_DB_PATH}/data$vsn directory exists
if [ ! -d "${POSTGRES_DB_PATH}/data${PG_VERSION}" ]; then
puts "info" "${POSTGRES_DB_PATH}/data${PG_VERSION} not found, initializing database..."
doas service postgresql initdb
else
puts "info" "${POSTGRES_DB_PATH}/data${PG_VERSION} already exists."
return 0
fi
server_ip=$(ip_address)
# Set the permissions
hba="""
# TYPE DATABASE USER ADDRESS METHOD
local all postgres peer
host all postgres ${server_ip}/24 md5
host replication replicator ${server_ip}/24 md5
### Database access rules
"""
# Set the listen_address in postgresql.conf
doas -u postgres sh -c "sed -i '' \"s/#listen_addresses = 'localhost'/listen_addresses = 'localhost, $server_ip'/\" ${POSTGRES_CONF_FILE}"
cat <<EOL | doas -u postgres tee -a "$POSTGRES_CONF_FILE" >/dev/null
log_min_messages = notice
log_min_error_statement = notice
log_checkpoints = on
log_lock_waits = on
#log_timezone = 'UTC'
log_connections = on
log_disconnections = on
log_duration = on
log_statement = 'all'
EOL
# Uncomment the log_timezone line if it exists
doas -u postgres sed -i '' 's/^#\(log_timezone =.*\)/\1/' "$POSTGRES_CONF_FILE"
puts "info" "Updated $POSTGRES_CONF_FILE with PostgreSQL logging settings."
doas -u postgres sh -c "echo \"$hba\" > ${POSTGRES_DB_PATH}/data${PG_VERSION}/pg_hba.conf"
doas service postgresql status || doas service postgresql start >/dev/null
configure_postgres_logging
}
#
# Function: configure_postgres_logging
#
# Description:
#
# Configure PostgreSQL logging
#
configure_postgres_logging() {
postgres_log_file="/var/log/postgresql.log"
syslog_conf_file="/etc/syslog.conf"
# Return if /var/log/postgresql.log exists
if [ -d $postgres_log_file ]; then
puts "info" "$postgres_log_file already exists."
return 0
else
doas touch $postgres_log_file
doas chown postgres:wheel $postgres_log_file
fi
# Step 1: Create Syslog Configuration for PostgreSQL
if [ ! -d /etc/syslog.d ]; then
echo "Directory /etc/syslog.d does not exist. Creating it..."
doas mkdir -p /etc/syslog.d
fi
echo "*.* /var/log/postgresql.log" | doas tee /etc/syslog.d/postgres.conf >/dev/null
puts "info" "Created /etc/syslog.d/postgres.conf with PostgreSQL logging configuration."
# Step 2: Modify Syslog Configuration
if grep -q "postgresql\.log" "$syslog_conf_file"; then
echo "[INFO] PostgreSQL log file already exists. Skipping changes."
else
if [ -f "$syslog_conf_file" ]; then
# Uncomment the relevant line if it is commented
doas sed -i '' 's/^#\(*.notice;authpriv.none;kern.debug;lpr.info;mail.crit;news.err\)/\1/' "$syslog_conf_file"
# Update the line to add local0.none
doas sed -i '' 's/\*.notice;authpriv.none;kern.debug;lpr.info;mail.crit;news.err/\*.notice;authpriv.none;kern.debug;lpr.info;mail.crit;news.err;local0.none/' "$syslog_conf_file"
# Add the PostgreSQL logging line
echo "local0.* /var/log/postgresql.log" | doas tee -a "$syslog_conf_file" >/dev/null
echo "[INFO] Updated $syslog_conf_file with PostgreSQL logging configuration."
else
puts "error" "$syslog_conf_file does not exist."
return 1
fi
fi
# Step 4: Restart Services
doas service postgresql reload
puts "info" "Reloaded PostgreSQL service."
doas service syslogd restart
puts "info" "Restarted syslogd service."
}
#
# Function: create_db
#
# Description:
#
# Create a new user, password and database. User supplied database name.
#
create_db() {
POSTGRES_DB_PATH=/var/db/postgres
# Get the current major version of the installed PostgreSQL
PG_VERSION=$(pkg info postgresql* | grep -Eo 'postgresql[0-9]+-client' | sed -E 's/postgresql([0-9]+)-client/\1/')
POSTGRES_HBA_FILE="${POSTGRES_DB_PATH}/data${PG_VERSION}/pg_hba.conf"
# Ensure the server is running
doas service postgresql status || doas service postgresql start >/dev/null
generate_credentials
create_user_sql="CREATE USER \"${username}\" WITH PASSWORD '${password}';"
if run_cmd doas -u postgres psql -c "$create_user_sql"; then
puts "success" "User ${username} created successfully."
else
puts "error" "Failed to create user '${username}'i."
exit_message
exit 1
fi
# | Collate | Ctype
# c_mixed_utf8 | C | C.UTF-8
# c_utf8_full | C.UTF-8 | C.UTF-8
# us_utf8_full | en_US.UTF-8 | en_US.UTF-8
create_db_sql="CREATE DATABASE \"${DB}\" OWNER \"${username}\" ${ENCODING};"
if run_cmd doas -u postgres psql -c "$create_db_sql"; then
puts "success" "Database $DB created successfully."
else
puts "error" "Failed to create database $DB. See /var/log/postgresql.log for more information."
doas tail -n 5 /var/log/postgresql.log >&2
exit_message
exit 1
fi
server_ip=$(ip_address)
hba="host ${DB} ${username} ${server_ip}/24 md5"
if run_cmd doas -u postgres sh -c "echo \"$hba\" >> ${POSTGRES_HBA_FILE}"; then
puts "success" "Added user ${username}@${DB} to pg_hba.conf."
else
puts "error" "Failed to add host ${server_ip} to pg_hba.conf."
exit_message
exit 1
fi
if run_cmd doas service postgresql reload; then
puts "success" "Postgres reloaded successfully."
else
puts "error" "Failed to reload Postgres."
exit_message
exit 1
fi
echo_credentials
}
# Process commands in the order they were provided
for CMD in $COMMANDS; do
case "$CMD" in
pkg:*)
APP="${CMD#pkg:}"
# Check if package is installed
if pkg info "$APP" >/dev/null 2>&1; then
puts "info" "$APP is already installed."
else
# Try to install the package
if run_cmd doas pkg install -y "$APP"; then
puts "info" "$APP installed successfully."
else
puts "error" "[ERROR] Failed to install $APP."
exit_message
exit 1
fi
fi
;;
service:*)
SERVICE="${CMD#service:}"
# Enable and start the service
if run_cmd doas sysrc "${SERVICE}_enable=YES"; then
puts "info" "$SERVICE service enabled."
else
puts "error" "Failed to enable $SERVICE service."
fi
# Check if the service is already running
run_cmd doas service "$SERVICE" status
SERVICE_STATUS=$?
if [ $SERVICE_STATUS -eq 0 ]; then
puts "info" "$SERVICE service is already running."
else
# Attempt to start the service
if run_cmd doas service "$SERVICE" start; then
puts "success" "[INFO] $SERVICE service started."
else
puts "error" "[ERROR] Failed to start $SERVICE service."
fi
fi
;;
elixir)
# Handle Elixir installation
if [ -z "$ELIXIR_VERSION" ]; then
puts "error" "--elixir option requires a version."
exit_message
exit 1
fi
if is_elixir_installed; then
puts "info" "Elixir v$ELIXIR_VERSION is already installed."
else
if install_elixir; then
puts "info" "Elixir v$ELIXIR_VERSION installed successfully."
else
puts "error" "Failed to install Elixir v$ELIXIR_VERSION."
exit_message
exit 1
fi
fi
;;
postgres-init)
# Handle Postgres configuration
init_postgres
;;
postgres-db-c_mixed_utf8:*)
DB="${CMD#postgres-db-c_mixed_utf8:}"
ENCODING=""
create_db
;;
postgres-db-c_utf8_full:*)
DB="${CMD#postgres-db-c_utf8_full:}"
ENCODING="ENCODING 'UTF8' LC_COLLATE = 'C.UTF-8' LC_CTYPE = 'C.UTF-8' TEMPLATE template0"
create_db
;;
postgres-db-us_utf8_full:*)
DB="${CMD#postgres-db-us_utf8_full:}"
ENCODING="ENCODING 'UTF8' LC_COLLATE = 'en_US.UTF-8' LC_CTYPE = 'en_US.UTF-8' TEMPLATE template0"
create_db
;;
*)
puts "error" "Unknown command: $CMD"
exit 1
;;
esac
done
exit_message