#!/usr/bin/env bash # This is my Linux server configuration utility (sconfig.sh) # Read more about it here: https://git.socialnerds.org/david/sconfig # ***** CONFIG ***** # general information NAME="sconfig" TITLE="Server Configuration" DESCRIPTION="The Linux server configuration utility" VERSION="0.2" AUTHOR="david@socialnerds.org" WEBSITE="https://git.socialnerds.org/david/sconfig" REPO="$WEBSITE.git" LICENSE="https://git.socialnerds.org/david/sconfig/raw/branch/main/LICENSE" # default settings CONFIGFILE="/etc/sconfig/sconfig.conf" DEBUG=0 LOGFILE="" INTERACTIVE=2 declare -A CONFIG CONFIG=([debug_enabled]=$DEBUG [logfile]="$LOGFILE" [btrfs_enabled]= [btrfs_subvols]="/" [btrfs_versions]=30 [miab_enabled]= [miab_host]="box.example.com" [miab_user]="admin@example.com" [miab_password]="verysecret!=+%password" [healthchecks_enabled]= [healthchecks_baseurl]="" [healthchecks_id]="") declare -A PACKAGES PACKAGES=([ubuntu]="curl less vim" [arch]="curl less vim" [debian]="curl less vim") # external library files and command prerequsites #LIBRARIES=("library.sh" "ui.sh") DEPENDENCIES=("sort" "stat" "date" "whoami" "id" "tput" "chmod" "chown" "dirname" "basename" "seq") # ***** FUNCTION DEFINITIONS ***** # execute upon Ctrl-C detection function bashtrap { if [ $INTERACTIVE -eq 1 ]; then header echo "Detected Ctrl-C"; echo "Dropping to shell"; sleep 1 clear; exit 2 else library_log warn "Ctrl-C detected, exiting" exit 2 fi } # return 1/false if not executed as root function amiroot { if [ "$(whoami)" != "root" ]; then return 1 fi } # check if stdout is a TTY and return 1 if it is not # for detecting if executed by cron (or user) function amitty { if [ ! -t 1 ]; then return 1 fi } # general text output method # usage: output function library_log { if [ "$1" != "debug" -o $DEBUG -eq 1 ]; then echo "[$1] ${@:2}" if [ "$LOGFILE" -a -w "$LOGFILE" ]; then echo "[$(date +%Y%m%d%H%M%S)][$1] ${@:2}" >> "$LOGFILE" fi fi } # very simple configfile parser # it fills a global variable with key=value pairs from a configfile # usage: config_read function config_read { if [ "$1" -a -r "$1" ]; then local FILE="$1" while read -r LINE || [ -n "$LINE" ]; do local FILTER='^[0-9a-zA-Z]+[_-]?[0-9a-zA-Z]+=.*$' if [[ "$LINE" =~ $FILTER ]]; then CONFIG[${LINE%%=*}]=${LINE#*=} fi done < $FILE else return 1 fi } # very simple configfile writer # it (over)writes a configfile with key=value pairs from a global variable # usage: config_write function config_write { if [ "$1" ]; then local FILE="$1" for KEY in ${!CONFIG[@]}; do echo "$KEY=${CONFIG[$KEY]}" done | sort > $FILE else return 1 fi } # get the linux distribution id from /etc/os-release # only systems with this file present are supported function get_distro { local RELEASE_FILE="/etc/os-release" if [ -r $RELEASE_FILE ]; then while read -r LINE || [ -n "$LINE" ]; do local FILTER='^ID=' if [[ "$LINE" =~ $FILTER ]]; then echo "${LINE#*=}"; break fi done < $RELEASE_FILE fi } function set_fs_permissions { #TODO: check if $1 is a valid mask #TODO: check if is ownership is wrong if [ $# -eq 2 ]; then if [ -f "$2" -o -d "$2" ]; then local PERM="$1" local FILE="$2" local STAT=$(stat -c "%a" "$FILE") if [ $PERM -ne $STAT ]; then library_log debug "Fixing wrong file permissions [$FILE: $STAT -> $PERM]" chown $USERID:$GROUPID "$FILE" chmod $PERM "$FILE" fi fi else return 1 fi } # retrieve path to this script #function get_scriptpath { # local DIRNAME="$(dirname $0)" # if [ -L $0 ]; then # local DIRLINK="$(dirname $(readlink $0))" # if [ $DIRLINK == "." ]; then # local SCRIPTPATH="$DIRNAME" # else # local SCRIPTPATH="$DIRNAME/$DIRLINK" # fi # else # local SCRIPTPATH="$DIRNAME" # fi # if [[ $SCRIPTPATH =~ ^/.* ]]; then # echo "$SCRIPTPATH" # else # #TODO: redirect stdout and stderr for cd to /dev/null # cd $SCRIPTPATH; echo "$PWD" # fi #} #function main { # for MODULE in ${MODULES[@]}; do # library_log debug "Module found ($MODULE)" # done # library_log info "This is an informational message"; sleep 2 #} # print the usage message function usage { echo; echo "Usage: $(basename $0) " echo ""; echo "OPTIONS:" echo " -h Show help message" echo " -i Show script information (Version, etc.)" echo " -c Use custom configuration file" echo " -l Use custom log file" echo " -n Force non-interactive mode" echo " -d|-v Enable verbose output"; echo } # print general information function info { echo; echo "$NAME - $DESCRIPTION"; echo echo "Version: $VERSION" echo "Author: $AUTHOR" echo "Website: $WEBSITE" echo "License: $LICENSE"; echo } #print header header() { clear local cols=$(tput cols); local char="=" if [ $cols -gt 59 ]; then local i=0; while [ $i != $((cols)) ]; do local chars="$chars$char"; i=$((i+1)); done echo $chars; echo "$TITLE"; echo -e "$chars\n" else echo "error: terminal not wide enough (it must be at least 60 characters wide)"; exit 1 fi } #print content content() { local lines=$(tput lines) if [ $lines -gt 14 ]; then for i in $(seq 1 $#); do echo "${!i}" done local l=5; while [ $l != $((lines-$#)) ]; do echo ""; l=$((l+1)); done else clear echo "error: terminal not high enough (it must be at least 15 lines high)"; exit 1 fi } #get user input prompt() { local input="" case $1 in password) read -s -p "Enter [sudo] password: " input; echo $input ;; string) read -p "Enter string: " input; echo $input ;; *) read -n 1 -p "Enter number to select an option: " input if [[ $input =~ ^[1-9]+$ ]]; then echo $input; fi ;; esac } # get sudo #makemeasandwich() #{ # while true; do # if $(sudo -n true 2>/dev/null); then # return 0 # else # header # content "With great power comes great responsibility!" # password=$(prompt password) # echo "$password" | sudo -S -v 2>/dev/null # if [ $? -ne 0 ]; then # header # echo "Wrong password. Try again."; sleep 1 # continue # fi # fi # done #} # ask for confirmation function areyousure { echo "i am sure" } # update package repositories and install required packages function update_package_repos { case "$(get_distro)" in debian|ubuntu|raspbian) library_log info "Updating package repositories" apt-get update > /dev/null if [ $? -eq 0 ]; then library_log debug "Success!" else library_log error "An error occured while updating package repositories" exit 1 fi ;; arch) library_log info "Updating package repositories" pacman -Sy > /dev/null if [ $? -eq 0 ]; then library_log debug "Success!" else library_log error "An error occured while updating package repositories" exit 1 fi ;; *) library_log error "Could not determine your operating system" exit 1 ;; esac } # ***** START ***** # bashtrap initialization trap bashtrap INT # require root if ! amiroot; then library_log error "You need to run this script with elevated privileges" exit 1 fi # checking for the availibility of ${DEPENDENCIES[@]} #TODO: rename to PREREQISITES MISSING_DEPENDENCIES=() for DEPENDENCY in ${DEPENDENCIES[@]}; do if ! $(command -v $DEPENDENCY &> /dev/null); then MISSING_DEPENDENCIES+=("$DEPENDENCY") fi done if [ $MISSING_DEPENDENCIES ]; then library_log error "Dependencies not found [${MISSING_DEPENDENCIES[@]}]" exit 1 fi # option handler while getopts "c:l:ndvih" ARG; do case $ARG in c) CONFIGFILE="$OPTARG" library_log debug "Custom config file set by command line option [-$ARG $OPTARG]" ;; l) LOGFILE="$OPTARG" library_log debug "Custom log file set by command line option [-$ARG $OPTARG]" ;; n) INTERACTIVE=0 library_log debug "Forcing non-interactive mode by command line option [-$ARG]" ;; d|v) if [ ! $DEBUG -eq 1 ]; then DEBUG=1 library_log debug "Debug mode enabled by command line option [-$ARG]" fi ;; i) info; exit 0 ;; h) usage; exit 0 ;; *) usage; exit 1 ;; esac done # loading config values (from $CONFIGFILE) if ! config_read "$CONFIGFILE"; then library_log warning "Could not read config file, skipping [$CONFIGFILE]" fi # setting runtime variables USERID=$(id -u) GROUPID=$(id -g) DISTRO=$(get_distro) #SCRIPTPATH="$(get_scriptpath)" #MODULES=($(ls $SCRIPTPATH/modules/*.sh)) if [ ! $DEBUG -eq 1 ]; then DEBUG=${CONFIG[debug_enabled]} fi if [ -z $LOGFILE ]; then LOGFILE=${CONFIG[logfile]} fi if [ ! $INTERACTIVE -eq 0 ]; then if amitty; then INTERACTIVE=1 fi fi # create logfile if missing if [ "$LOGFILE" -a ! -w "$LOGFILE" ]; then if [ ! -d $(dirname "$LOGFILE") ]; then mkdir -p $(dirname "$LOGFILE") fi touch "$LOGFILE" fi # create configfile if missing if [ ! -w $CONFIGFILE ]; then if [ ! -d $(dirname $CONFIGFILE) ]; then mkdir -p $(dirname $CONFIGFILE) fi # write config values (to $CONFIGFILE) library_log info "Creating new config file [$CONFIGFILE]" if ! config_write "$CONFIGFILE"; then library_log warning "Could not write to config file [$CONFIGFILE]" fi fi # change file permissions if wrong #TODO: check return code set_fs_permissions 600 "$CONFIGFILE" set_fs_permissions 644 "$LOGFILE" # loading libraries #for LIBRARY in ${LIBRARIES[@]}; do # if [ -r $SCRIPTPATH/$LIBRARY ]; then # source $SCRIPTPATH/$LIBRARY # if [ $DEBUG -eq 1 ]; then # echo "[debug] Library loaded [$LIBRARY]" # fi # else # echo "[error] Library not found [$LIBRARY]" # exit 1 # fi #done # log start of main script library_log debug "***** START *****" if [ $INTERACTIVE -eq 1 ]; then # run text ui while true; do # Main menu header content "1) System status" \ "2) Configuration" \ "3) Maintenance" \ "" \ "4) Reboot" \ "5) Shutdown" \ "6) Exit to shell" case $(prompt) in 1) header echo "Not yet implemented"; sleep 1 ;; 2) while true; do # Configuration submenu header content "1) Users" \ "2) Install admin utilities" \ "" \ "*) Return to main menu" case $(prompt) in 1) header echo "Not yet implemented"; sleep 1 ;; 2) header echo "Not yet implemented"; sleep 1 ;; *) break ;; esac done ;; 3) while true; do # Maintenance submenu header content "1) System updates" \ "2) Backup" \ "3) Update this script" \ "" \ "*) Return to main menu" case $(prompt) in 1) #update system packages makemeasandwich if [ $? -eq 0 ]; then clear; header; sudo apt update && sudo apt dist-upgrade -y && sudo apt autoremove -y && sudo apt autoclean -y clear; header; echo "System is up-to-date"; sleep 1 fi ;; 2) header echo "Not yet implemented"; sleep 1 ;; 3) # Update this script submenu header #TODO: check for new version if false; then #this should run the check content "A new version of sconfig is available." \ "Do you want to update?" \ "" \ "1) Yes" \ "2) No" if [ $(prompt) -eq 1 ]; then #run self update header echo "Update not yet implemented"; sleep 1 fi else header echo "You are already up-to-date."; sleep 1 fi ;; *) break ;; esac done ;; 4) # Reboot submenu header content "1) Yes, i really want to reboot." \ "*) Get me out of here!" case $(prompt) in 1) #reboot server header echo "Rebooting server"; sleep 1 shutdown -r now ;; *) continue ;; esac ;; 5) # Shutdown submenu header content "1) Yes, i really want to shutdown." \ "*) Get me out of here!" case $(prompt) in 1) #shutdown server header echo "Shutting down server"; sleep 1 shutdown -h now ;; *) continue ;; esac ;; 6) header echo "Dropping to shell"; sleep 1 clear; break ;; esac done else # run scheduled tasks library_log info "Running scheduled tasks" update_package_repos fi # ***** END ***** library_log debug "***** END *****" exit 0