Initial code commit; still a work in progress
authorBrian Flowers <git-admn@bsflowers.net>
Tue, 24 May 2016 01:23:30 +0000 (21:23 -0400)
committerBrian Flowers <git-admn@bsflowers.net>
Tue, 24 May 2016 01:23:30 +0000 (21:23 -0400)
README [new file with mode: 0644]
bdsm.conf [new file with mode: 0644]
bdsm.d/df.generic [new file with mode: 0644]
bdsm.d/template.generic [new file with mode: 0644]
bdsm.d/vmstat.generic [new file with mode: 0755]
bdsm.sh [new file with mode: 0755]

diff --git a/README b/README
new file mode 100644 (file)
index 0000000..b5b07b6
--- /dev/null
+++ b/README
@@ -0,0 +1,29 @@
+bdsm
+A Slightly Cyberpunk project
+[git|www].slightlycyberpunk.com
+
+bdsm is the Bash Daemon for System Monitoring, a truly agentless system monitoring and administration tool for Linux and Unix systems. 
+
+0) PREREQUISITES
+
+a) A GNU system
+b) bash
+c) vmstat
+
+1) INSTALLATION INSTRUCTIONS
+
+Create a new user named bdsm and clone the repository into this user's home directory.
+
+2) CONFIGURATION
+
+Login as the bdsm user
+
+Configure your infrastructure with:
+~/bdsm.sh configure
+
+Then deploy the required components to all downstream hosts with:
+~/bdsm.sh deploy
+
+3) USAGE
+
+Execute ~/bdsm.sh help for usage information
\ No newline at end of file
diff --git a/bdsm.conf b/bdsm.conf
new file mode 100644 (file)
index 0000000..f7ecb56
--- /dev/null
+++ b/bdsm.conf
@@ -0,0 +1,4 @@
+generic:bdsm
+{
+  vmstat(5);
+}
diff --git a/bdsm.d/df.generic b/bdsm.d/df.generic
new file mode 100644 (file)
index 0000000..09648de
--- /dev/null
@@ -0,0 +1,139 @@
+#!/bin/bash
+################################################################################
+#
+# df.generic
+# Bash Daemon for System Monitoring
+# Generic df plugin for monitoring disk utilization
+#
+#
+#
+#
+###############################################################################
+
+# TRAP UNKNOWN ERRORS
+###############################################################################
+trap failure SIGTERM SIGINT SIGFPE
+
+failure()
+{
+        error "Something has gone SERIOUSLY wrong! (Received signal $?)"
+        return 255
+}
+
+error()
+{
+  printf "\e[41m" 1>&2
+  printf "ERROR: $1" 1>&2
+  printf "\e[0m\n" 1>&2
+}
+
+# VARIABLES
+###############################################################################
+SCRIPT=$0
+HOSTNAME=$2
+DELAY=$3
+LABEL=$4
+PATH=$5
+OUTPATH=/home/bdsm/out.fifo
+
+# MAIN FUNCTIONS
+###############################################################################
+help()
+{
+  echo "Generic df disk utilization monitoring"
+  echo "USAGE: $0 [FUNCTION] [HOSTNAME] [DELAY]"
+  echo "FUNCTION -- start/stop/status/help"
+  echo "HOSTNAME -- the hostname to monitor"
+  echo "::DELAY  -- seconds between measurements"
+  echo "::LABEL  -- label for this drive"
+  echo "::DRIVE  -- filesystem path to monitor"
+  exit 0
+}
+
+start()
+{
+  running=`ssh $HOSTNAME "cat /home/bdsm/.df.pid | \
+                          xargs ps -T | \
+                          grep -v PID | \
+                          wc -l"` 2>/dev/null
+
+  if [ $running -eq 0 ] || [ $running -gt 5 ]; then
+    # Check if df is installed
+    installed=`ssh -q $HOSTNAME "df > /dev/null 2>&1; echo $?"`
+    if [ ! $installed ]; then
+      echo "" > $OUTPATH
+      return 1
+    fi
+
+    ssh -q $HOSTNAME <<EOF
+    while [ 1 ]; do
+      df=`df --output=pcent / | tail -1 | sed 's/[^0-9]//g'`
+      ts=`date '+%s'`
+      echo "$ts|$HOSTNAME|DISK-$LABEL-USED|$df" >> $OUTPATH
+      sleep $DELAY
+    done &
+EOF
+    ssh -q $HOSTNAME "ps aux | \
+                  grep bdsm | \
+                  grep df | \
+                  grep -v grep | \
+                  awk '{print \$2}' \
+                  > /home/bdsm/.df.pid &"
+  else
+    echo "Already Running"
+  fi
+}
+
+stop()
+{
+    ssh $HOSTNAME "kill `cat /home/bdsm/.df.pid`"
+    ssh $HOSTNAME "rm /home/bdsm/.df.pid"
+}
+
+status()
+{
+  running=`ssh $HOSTNAME "cat /home/bdsm/.df.pid | \
+                            xargs ps -T | \
+                            grep -v PID | \
+                            wc -l"`
+
+  if [ $running -eq 0 ]; then
+    echo "OFF"
+  else
+    echo "ON"
+  fi
+}
+
+# UTILITY FUNCTIONS
+###############################################################################
+
+formatConfig()
+{
+  # Reformat config file to be more machine-readable
+  # Removes whitespace and adds semicolons after property values
+  cat /home/bdsm/bdsm.conf | tr '\n' ' ' | sed 's/}/}\
+  /g' | sed 's/^;*//g' | sed "s/[[:space:]]//g"| \
+  sed 's/;\([{}]\)/\1/g' | sed 's/\([{}]\);/\1/g' \
+  > /home/bdsm/.bdsm.conf.tmp
+}
+
+# SCRIPT START
+###############################################################################
+
+# Get a list of configured hosts
+hosts=$(cat /home/bdsm/.bdsm.conf.tmp | cut -d'{' -f1)
+
+if [ -z $1 ]; then
+  help
+elif [ $1 == "start" ]; then
+  formatConfig
+  start
+elif [ $1 == "stop" ]; then
+  formatConfig
+  stop
+elif [ $1 == "status" ]; then
+  formatConfig
+  status
+elif [ $1 == "help" ]; then
+  help
+fi
diff --git a/bdsm.d/template.generic b/bdsm.d/template.generic
new file mode 100644 (file)
index 0000000..e1fb372
--- /dev/null
@@ -0,0 +1,109 @@
+#!/bin/bash
+################################################################################
+#
+# template.generic
+# Bash Daemon for System Monitoring
+# Generic plugin template
+#
+###############################################################################
+
+# TRAP UNKNOWN ERRORS
+###############################################################################
+trap failure SIGTERM SIGINT SIGFPE
+
+failure()
+{
+        error "Something has gone SERIOUSLY wrong! (Received signal $?)"
+        return 255
+}
+
+error()
+{
+  printf "\e[41m" 1>&2
+  printf "ERROR: $1" 1>&2
+  printf "\e[0m\n" 1>&2
+}
+
+# VARIABLES
+###############################################################################
+SCRIPT=$0
+HOSTNAME=$2
+
+# FIFO pipe to be used for output
+OUTPATH=/home/bdsm/out.fifo
+
+# Optional Parameters
+PARAM1=$3
+PARAM2=$4
+
+# MAIN FUNCTIONS
+###############################################################################
+help()
+{
+  # Print usage information
+  # Formatting is important here, as this is processed by bdsm.sh!
+  # First two parameters should be function and hostname
+  # Other parameters are optional and will be stored in bdsm.conf
+  # Any parameters to be stored in BSDM.conf should output as:
+  # ::[PARAM NAME] -- [PARAM DESCRIPTION]
+  # Parameters NOT stored in bsfc.conf must NOT start with ::!
+  echo "PLUGIN DESCRIPTION GOES HERE"
+  echo "USAGE: $0 [FUNCTION] [HOSTNAME] [PARAMETER 1] [PARAMETER 2]"
+  echo "FUNCTION -- start, stop, status or help"
+  echo "HOSTNAME -- host to run the monitor on"
+  echo "::PARAM1 -- description of the first parameter"
+  echo "::PARAM2 -- description of the second parameter"
+  exit 0
+}
+
+start()
+{
+  # Start the monitor and write output to $OUTPATH
+  # Output should be pipe-delimited
+  # One line per attribute
+  # Format:
+  #   $TIMESTAMP|$HOST|$ATTRIBUTE|$VALUE
+  #
+  # TIMESTAMP format is date +%s
+  # HOSTNAME is a global variable
+  # ATTRIBUTE is a string describing what is being measure
+  #   ex. "CPU-SY" for system CPU utilization
+  # VALUE is the value measured (ie, percentage)
+  #   : Don't include units -- ie, 59 not 59%
+  #   : UP/DOWN/??? for service up/down checks
+  #   : If those don't make sense, use whatever does, but keep it concise!
+}
+
+stop()
+{
+  # Stop the monitor
+}
+
+status()
+{
+  # Check if the monitor is running and echo ON or OFF
+}
+
+# SCRIPT START
+###############################################################################
+
+# Reformat config file to be more machine-readable
+# Removes whitespace and adds semicolons after property values
+cat /home/bdsm/bdsm.conf | sed "s/[[:space:]]//g" | tr '\n' ' ' | sed 's/}/}\
+  /g' | sed 's/^;*//g' | sed 's/;\([{}]\)/\1/g' | sed 's/\([{}]\);/\1/g' \
+  > /home/bdsm/.bdsm.conf.tmp
+    
+# Get a list of configured hosts
+hosts=$(cat /home/bdsm/.bdsm.conf.tmp | cut -d'{' -f1)
+
+if [ -z $1 ]; then
+  help
+elif [ $1 == "start" ]; then
+  start
+elif [ $1 == "stop" ]; then
+  stop
+elif [ $1 == "status" ]; then
+  status
+elif [ $1 == "help" ]; then
+  help
+fi
diff --git a/bdsm.d/vmstat.generic b/bdsm.d/vmstat.generic
new file mode 100755 (executable)
index 0000000..e34a079
--- /dev/null
@@ -0,0 +1,172 @@
+#!/bin/bash
+################################################################################
+#
+# vmstat.generic
+# Bash Daemon for System Monitoring
+# Generic vmstat plugin
+#
+#
+#
+#
+###############################################################################
+
+# TRAP UNKNOWN ERRORS
+###############################################################################
+trap failure SIGTERM SIGINT SIGFPE
+
+failure()
+{
+        error "Something has gone SERIOUSLY wrong! (Received signal $?)"
+        return 255
+}
+
+error()
+{
+  printf "\e[41m" 1>&2
+  printf "ERROR: $1" 1>&2
+  printf "\e[0m\n" 1>&2
+}
+
+# VARIABLES
+###############################################################################
+SCRIPT=$0
+HOSTNAME=$2
+DELAY=$3
+OUTPATH=/home/bdsm/out.fifo
+
+# MAIN FUNCTIONS
+###############################################################################
+help()
+{
+  echo "Generic vmstat monitoring"
+  echo "USAGE: $0 [FUNCTION] [HOSTNAME] [DELAY]"
+  echo "FUNCTION -- start/stop/status/help"
+  echo "HOSTNAME -- the hostname to monitor"
+  echo "::DELAY  -- seconds between measurements"
+  exit 0
+}
+
+start()
+{
+  running=`ssh $HOSTNAME "cat /home/bdsm/.vmstat.pid | \
+                          xargs ps -T | \
+                          grep -v PID | \
+                          wc -l"` 2>/dev/null
+
+  if [ $running -eq 0 ] || [ $running -gt 5 ]; then
+    # Check if vmstat is installed
+    installed=`ssh -q $HOSTNAME "vmstat > /dev/null 2>&1; echo $?"`
+    if [ ! $installed ]; then
+      echo "" > $OUTPATH
+      return 1
+    fi
+
+    # Check what options vmstat supports
+    if [ `ssh -q $HOSTNAME "vmstat -n >/dev/null 2>&1; echo $?"` ]; then
+      vmopt="-n"
+    else
+      vmopt=""
+    fi
+
+    # Check what options awk supports
+    if [ ! -z `ssh $HOSTNAME "echo | awk -W interactive '{print 1}' 2>&1 >/dev/null"` ]; then
+      awkopt=""
+    else
+      awkopt="-W interactive"
+    fi
+
+    ssh -q $HOSTNAME <<EOF
+    vmstat $vmopt $DELAY | awk $awkopt -v host=$HOSTNAME 'BEGIN {
+      OFS="|"
+    }
+    /us/ {
+      uscol=99
+      for(i=1; i<NF; i++)
+      {
+        if(\$i == "us")
+          uscol=i
+        if(\$i == "sy" && i > uscol)
+          sycol=i
+        if(\$i == "wa" && i > uscol)
+          wacol=i
+        if(\$i == "id" && i > uscol)
+          idcol=i
+      }
+    }
+    !/[a-z-]/ {
+      "date '+%s'" | getline ts
+      if(uscol != 99 && uscol != 0)
+        print ts,host,"CPU-US",\$uscol
+      if(sycol != 0)
+        print ts,host,"CPU-SY",\$sycol
+      if(wacol != 0)
+        print ts,host,"CPU-WA",\$wacol
+      if(idcol != 0)
+        print ts,host,"CPU-ID",\$idcol
+      close("date '+%s'")
+    }' >> $OUTPATH &
+EOF
+    ssh -q $HOSTNAME "ps aux | \
+                  grep bdsm | \
+                  grep vmstat | \
+                  grep -v grep | \
+                  awk '{print \$2}' \
+                  > /home/bdsm/.vmstat.pid &"
+  else
+    echo "Already Running"
+  fi
+}
+
+stop()
+{
+    ssh $HOSTNAME "kill `cat /home/bdsm/.vmstat.pid`"
+    ssh $HOSTNAME "rm /home/bdsm/.vmstat.pid"
+}
+
+status()
+{
+  running=`ssh $HOSTNAME "cat /home/bdsm/.vmstat.pid | \
+                            xargs ps -T | \
+                            grep -v PID | \
+                            wc -l"`
+
+  if [ $running -eq 0 ]; then
+    echo "OFF"
+  else
+    echo "ON"
+  fi
+}
+
+# UTILITY FUNCTIONS
+###############################################################################
+
+formatConfig()
+{
+  # Reformat config file to be more machine-readable
+  # Removes whitespace and adds semicolons after property values
+  cat /home/bdsm/bdsm.conf | tr '\n' ' ' | sed 's/}/}\
+  /g' | sed 's/^;*//g' | sed "s/[[:space:]]//g"| \
+  sed 's/;\([{}]\)/\1/g' | sed 's/\([{}]\);/\1/g' \
+  > /home/bdsm/.bdsm.conf.tmp
+}
+
+# SCRIPT START
+###############################################################################
+
+# Get a list of configured hosts
+hosts=$(cat /home/bdsm/.bdsm.conf.tmp | cut -d'{' -f1)
+
+if [ -z $1 ]; then
+  help
+elif [ $1 == "start" ]; then
+  formatConfig
+  start
+elif [ $1 == "stop" ]; then
+  formatConfig
+  stop
+elif [ $1 == "status" ]; then
+  formatConfig
+  status
+elif [ $1 == "help" ]; then
+  help
+fi
diff --git a/bdsm.sh b/bdsm.sh
new file mode 100755 (executable)
index 0000000..c2b4622
--- /dev/null
+++ b/bdsm.sh
@@ -0,0 +1,425 @@
+#!/bin/bash
+################################################################################
+#
+# bdsm.sh
+# Bash Daemon for System Monitoring
+#
+# TODO:
+# 1) Fully shakeout configure
+# 2) Finish Deploy
+#  ) Plugins:
+# 3)  iostat
+# 4)  ram
+# 5)  disk utilization
+#
+###############################################################################
+
+# TRAP UNKNOWN ERRORS
+###############################################################################
+
+trap failure SIGFPE SIGHUP SIGABRT SIGALRM SIGQUIT
+trap term SIGTERM SIGINT
+
+term()
+{
+  echo
+  echo
+  error "BDSM TERMINATED."
+  exit 1
+}
+
+failure()
+{
+  echo
+  echo
+  error "Something has gone SERIOUSLY wrong! (Received signal $?)"
+  exit 255
+}
+
+# UTILITY FUNCTIONS
+###############################################################################
+error()
+{
+  printf "\e[41m" 1>&2
+  printf "ERROR: $1" 1>&2
+  printf "\e[0m\n" 1>&2
+}
+
+warn()
+{
+  printf "\e[30;43m" 1>&2
+  printf "WARNING: $1" 1>&2
+  printf "\e[0m\n" 1>&2
+
+}
+
+header()
+{
+  echo
+  printf "\e[44m" 1>&2
+  printf "      $1" 1>&2
+  printf "\e[0m\n" 1>&2
+}
+
+option()
+{
+  printf "\e[36m" 1>&2
+  printf " $1" 1>&2
+  printf "\e[0m\n" 1>&2
+}
+
+prompt()
+{
+  read -t .25 -n 1000 discard </dev/tty
+  printf "\e[30;46m" 1>&2
+  printf "\r$1" 1>&2
+  printf "\e[0;0m " 1>&2
+}
+
+info()
+{
+  printf "\e[33m"
+  printf "INFO: $1"
+  printf "\e[0m\n"
+}
+
+selector()
+{
+  prmpt=${1}
+  shift
+  
+  argarr=( "$@" )
+  i=0
+  while [ ! -z "$1" ]; do
+    option "$i. $1"
+    i=`expr $i + 1`
+    shift
+  done
+  
+  selection=-1
+  while [ "$selection" -lt 0 ] || \
+        [ "$selection" -gt $i ] || \
+        [ "`echo $selection | sed 's/[^0-9]//g'`" != "$selection" ]; do
+    prompt "$prmpt: "
+    read selection
+  done
+  
+  echo ${argarr[$selection]}
+}
+
+confirm()
+{
+  prmpt=${1}
+  
+  prompt "CONFIRM: $prmpt Y/[N]"
+  read selection
+  if [ "$selection" == "Y" ] || [ "$selection" == "y" ]; then
+    echo 0
+  else
+    echo 1
+  fi
+}
+
+# ACTION FUNCTIONS
+###############################################################################
+
+help()
+{
+  echo
+  echo "Bash Daemon for System Monitoring (bdsm.sh)"
+  echo "USAGE: $0 [start|stop|status|deploy|configure|help]"
+  echo
+  echo "If this is your first time executing $0, start with:"
+  echo "  $0 configure"
+  echo
+  echo "Once configuration is complete, you must deploy with:"
+  echo "  $0 deploy"
+  echo
+  echo "During deployment you may be asked for the root password"
+  echo " for these systems. This is required for configuring user"
+  echo " setup. To avoid this, create a user 'bdsm' on the remote"
+  echo " server and configure passwordless SSH access to this user."
+  echo " This user will need very limited permissions."
+  echo
+  echo " After deployment is completed, you can start, check, or stop:"
+  echo "  $0 start"
+  echo "  $0 status"
+  echo "  $0 stop"
+  echo
+  echo "Returns:"
+  echo " 0: Success!"
+  echo " 1: PEBKAC"
+  echo " 2: Could not connect to remote host"
+  echo " 255: You're on your own. Good luck!"
+}
+
+start()
+{
+  i=0
+  while [ $i -lt ${#hosts[@]} ]; do
+    type=`echo ${hosts[$i]} | cut -d':' -f1`
+    host=`echo ${hosts[$i]} | cut -d':' -f2-`
+    info "Starting $type monitors on $host..."
+    functions=`cat /home/bdsm/.bdsm.conf.tmp | grep "$type:$host" | sed 's/.*{\(.*\)}.*/\1/'`
+        
+    j=0
+    echo $functions | tr ';' '\n' | sed 's/[(),]/ /g' | while read line; do
+      command=`echo $line | cut -d' ' -f1`
+      args="`echo $line | cut -d' ' -f2-`"
+      if [ -f /home/bdsm/bdsm.d/$command.$type ]; then
+        /home/bdsm/bdsm.d/$command.$type start $host $args
+      elif [ -f /home/bdsm/bdsm.d/$command.$type ]; then
+        /home/bdsm/bdsm.d/$command.generic start $host $args
+      else
+        error "Could not find plugin for $command on $type:$host!"
+      fi
+    done
+    
+    ssh $host "cat /home/bdsm/out.fifo" > /home/bdsm/in.fifo &
+    echo $! > /home/bdsm/pids/$host.in
+    
+    i=`expr $i + 1`
+  done
+}
+
+stop()
+{
+  i=0
+  while [ $i -lt ${#hosts[@]} ]; do
+    type=`echo ${hosts[$i]} | cut -d':' -f1`
+    host=`echo ${hosts[$i]} | cut -d':' -f2-`
+    info "Stopping $type monitors on $host..."
+    functions=`cat /home/bdsm/.bdsm.conf.tmp | grep "$type:$host" | sed 's/.*{\(.*\)}.*/\1/'`
+
+    j=0
+    echo $functions | tr ';' '\n' | sed 's/[(),]/ /g' | while read line; do
+      command=`echo $line | cut -d' ' -f1`
+      args="`echo $line | cut -d' ' -f2-`"
+      if [ -f /home/bdsm/bdsm.d/$command.$type ]; then
+        /home/bdsm/bdsm.d/$command.$type stop $host $args
+      elif [ -f /home/bdsm/bdsm.d/$command.$type ]; then
+        /home/bdsm/bdsm.d/$command.generic stop $host $args
+      else
+        error "Could not find plugin for $command on $type:$host!"
+      fi
+    done
+
+    kill `cat /home/bdsm/pids/$host.in` 2>/dev/null
+    rm /home/bdsm/pids/$host.in
+
+    i=`expr $i + 1`
+  done
+}
+
+status()
+{
+  i=0
+  while [ $i -lt ${#hosts[@]} ]; do
+    type=`echo ${hosts[$i]} | cut -d':' -f1`
+    host=`echo ${hosts[$i]} | cut -d':' -f2-`
+    info "Stopping $type monitors on $host..."
+    functions=`cat /home/bdsm/.bdsm.conf.tmp | grep "$type:$host" | sed 's/.*{\(.*\)}.*/\1/'`
+        
+    j=0
+    echo $functions | tr ';' '\n' | sed 's/[(),]/ /g' | while read line; do
+      command=`echo $line | cut -d' ' -f1`
+      prefix="$type:$host|$command|"
+      if [ -f /home/bdsm/bdsm.d/$command.$type ]; then
+        /home/bdsm/bdsm.d/$command.$type status $host | sed "s/^/$prefix/g"
+      elif [ -f /home/bdsm/bdsm.d/$command.$type ]; then
+        /home/bdsm/bdsm.d/$command.generic status $host | sed "s/^/$prefix/g"
+      else
+        error "Could not find plugin for $command on $type:$host!"
+      fi
+    done
+    
+    pid=`cat /home/bdsm/pids/$host.in`
+    if [ ! -f /home/bdsm/pids/$host.in ]; then
+      echo "$type:$host:MONITOR|???"
+    elif [ `ps -p $pid | wc -l` -ge 2 ]; then
+      echo "$type:$host:MONITOR|UP"
+    else
+      echo "$type:$host:MONITOR|DOWN"
+    fi
+    
+    i=`expr $i + 1`
+  done
+}
+
+configure()
+{
+  configured=0
+  if [ -f /home/bdsm/bdsm.conf ]; then
+    prompt "Configuration already exists! [K]eep, [U]pdate, or [D]elete?"
+    read selection
+    if [ $selection == "K" ]; then
+      info "Proceeding with existing configuration..."
+      configured=1
+    elif [ $selection == "U" ]; then
+      info "Updating existing configuration..."
+    elif [ $selection == "D" ]; then
+      info "Purging existing configuration..."
+      rm /home/bdsm/bdsm.conf
+      touch /home/bdsm/bdsm.conf
+    else
+      error "Invalid selection."
+      return 1
+    fi
+  else
+    touch /home/bdsm/bdsm.conf
+  fi
+  
+  while [ $configured -ne 1 ]; do
+    # Prompt which host to configure
+    header "Select a host to configure:"
+    
+    host=`selector "Select a host" $hosts "New..." "Done"`
+    if [ "$host" == "New..." ]; then
+      host=""
+    elif [ "$host" == "Done" ]; then
+      configured=1
+      break
+    else
+      type=`echo $host | cut -d':' -f1`
+      host=`echo $host | cut -d':' -f2-`
+    fi
+    while [ -z $host ]; do
+      header "Configure a new host"
+      
+      types=$(ls /home/bdsm/bdsm.d/ | cut -d'.' -f2 | sort -u)
+      type=`selector "Host type" $types`
+      
+      prompt "Hostname: "
+      read host
+      
+      if [ `confirm "Confirm creation of $type:$host?"` -eq 0 ]; then
+        echo "$host{}" >> /home/bdsm/.bdsm.conf.tmp
+      else
+        host=""
+      fi
+    done
+    
+    header "Configuring $host"
+    availServices=$(ls /home/bdsm/bdsm.d | cut -d'.' -f1 | sort -u)
+    services=$(cat /home/bdsm/.bdsm.conf.tmp | grep ":$host{" | \
+      sed 's/.*{\(.*\)}/\1/g' | tr ';' ' ')
+    service=`selector "Select a service to configure" $services "Add a service..." "Done"`
+    
+    if [ "$service" == "Add a service..." ]; then
+      header "Add a new service"
+      service=`selector "Service type: " $availServices`
+    elif [ "$service" == "Done" ]; then
+      continue
+    fi
+    
+    header "Configuring $service"
+    if [ `confirm "Disable $service?"` -eq 0 ]; then
+      # TODO
+      # This may need fixed
+      # TODO
+      cat /home/bdsm/.bdsm.conf.tmp | \
+        sed 's/\(.*:$host{.*\)$service;\(.*\)/\1\2/g' \
+        > /home/bdsm/.bdsm.conf.tmp2
+      mv /home/bdsm/.bdsm.conf.tmp2 /home/bdsm/.bdsm.conf.tmp
+    else
+      header "Reconfiguring $service"
+      svc=`echo $service | sed 's/(.*)$//g'`
+      if [ -f /home/bdsm/bdsm.d/$svc.$type ]; then
+        serviceFile="$svc.$type"
+      elif [ -f /home/bdsm/bdsm.d/$svc.generic ]; then
+        serviceFile="$svc.generic"
+      else
+        error "$service plugin not found!"
+      fi
+     
+      max=`echo $params | wc -l`
+      newservice=`echo $service | cut -d'(' -f1`"("
+      i=1
+      while [ $i -le $max ]; do
+        param="`/home/bdsm/bdsm.d/$serviceFile help | grep "^::" | sed -n ${i}p`"
+        paramName="`echo ${param} | sed 's/^::\(.*\) *--.*/\1/g'`"
+        paramDesc="`echo ${param} | sed 's/^.*--\(.*\)/\1/g'`"
+        prompt "$paramName: "
+        read param
+        newservice=${newservice}${param}","
+        i=`expr $i + 1`
+      done
+      newservice=`echo $newservice | sed 's/,$/)/g'`
+    fi
+    
+    # TODO
+    # Validate removal/replacement of old service
+    # TODO
+    curHost="`cat /home/bdsm/.bdsm.conf.tmp | grep ":$host{"`"
+    curHost=`echo $curHost | sed "s/$service;//g" | sed "s/}\$/$newservice;}/g"`
+    cat /home/bdsm/.bdsm.conf.tmp | \
+      grep -v ":$host{" \
+      > /home/bdsm/.bdsm.conf.tmp2
+    echo $curHost >> /home/bdsm/.bdsm.conf.tmp2
+    mv /home/bdsm/.bdsm.conf.tmp2 /home/bdsm/.bdsm.conf.tmp
+    # Reload the hosts list
+    hosts=$(cat /home/bdsm/.bdsm.conf.tmp | cut -d'{' -f1)
+  done
+  
+  # Convert config back to a more readable format
+  cat /home/bdsm/.bdsm.conf.tmp | sed 's/{/\
+    {\
+    /g' | sed 's/;/;\
+    /g' | sed 's/}/}\
+    /g' > /home/bdsm/bdsm.conf
+  rm /home/bdsm/.bdsm.conf.tmp
+  
+  info "Configuration complete!"
+  info "Don't forget to DEPLOY these changes!"
+  exit 0
+}
+
+deploy()
+{
+  i=0
+  while [ $i -lt ${#hosts[@]} ]; do
+    type=`echo ${hosts[$i]} | cut -d':' -f1`
+    host=`echo ${hosts[$i]} | cut -d':' -f2-`
+    info "Deploying on $host..."
+    
+    ssh -q bdsm@$host
+    ret=$?
+    if [ ! $ret ]; then
+      info "Passwordless SSH not configured. Creating bdsm user as root..."
+      ssh root@$host "useradd --home-dir /home/bdsm bdsm"
+      
+      info "Logging in as bdsm user to transfer SSH key..."
+      cat /home/bdsm/.ssh/id_rsa.pub | ssh bdsm@$host 'cat >> ~/.ssh/authorized_keys'
+    fi
+    
+    ssh bdsm@$host "mkfifo /home/bdsm/out.fifo"
+  done
+}
+
+# SCRIPT START
+###############################################################################
+
+# Reformat config file to be more machine-readable
+# Removes whitespace and adds semicolons after property values
+cat /home/bdsm/bdsm.conf | tr '\n' ' ' | sed "s/[[:space:]]//g" | sed 's/}/}\
+/g' | sed 's/^;*//g' | sed 's/\([{}]\);/\1/g' \
+> /home/bdsm/.bdsm.conf.tmp
+    
+# Get a list of configured hosts
+hosts=$(cat /home/bdsm/.bdsm.conf.tmp | cut -d'{' -f1)
+
+if [ -z $1 ]; then
+  help
+elif [ $1 == "start" ]; then
+  start
+elif [ $1 == "stop" ]; then
+  stop
+elif [ $1 == "status" ]; then
+  status
+elif [ $1 == "configure" ]; then
+  configure
+elif [ $1 == "deploy" ]; then
+  deploy
+else
+  help
+fi
\ No newline at end of file