Creating New Linux Commands

As a Pro you probably already have many scripts to help your day. This post is primarily about creating custom commands in Linux. Scripts can easily be turned into commands with options and help screens. #wol #script #tool #custom #commands #linux

Creating New Linux Commands
Photo by Fahrul Razi / Unsplash

WOL - part 2

As a Pro you probably already have Scripts to help perform your daily activities. ‌‌This post is primarily about creating custom commands in Linux. Scripts can easily be turned into Commands with options and help screens. WOL part 1

History of a script

This part is about a script and why it was created twenty years ago. Using the new technology - Wake on LAN and it's Magic Packets. And how it later turned into a common Command for our Operators and Engineers.

Many years ago there was a customer running very very remote machines that could not be allowed to boot up after a power or any other failure and the maintenance crew was not allowed to boot them up. ‌‌To make it even more complicated there was different sections that was to operate independently and the assigned operators could only boot the machines in his/hers section and only after a direct order from the section manager.‌‌ Interresting setup ...

I created a little script for the operators back at the MCC (Monitoring and Control Center) to do the remote booting. There was tons of red tape all over the operation and also this little script was several hundred lines with all reporting and a crude 2FA sign in added on top of it.

Second use case

Later we adopted this on our own Hotline machines for starting emergency assets or stopped assets. Each customer had its own file customer.wol.nodes.
We also utilized AoL (Alert on Lan) technologies for monitoring. This was when few systems had IPMI and still today some are without it or the license are considered to expensive.
‌‌Due to the fact that some customers had strict requirements about security clearance and/or certifications, meant they dictated who could work on their system. Hotline operator used their own set of customername.wol.nodes files.

The main script wolstart was in /usr/local/bin and used by wolstart -f customera.wol.nodes (start assets from the list of Customer A ). ‌‌We had a full Audit Trail on all operator activities conducted against customer servers and other assets and a manual logbook for calls and activities performed.

The Script

You can make the Script work in different ways in different situations. It's easy to tweak and change the script according to your needs. And later we turn it into a command for all users to use, no user worries of script integrity.

Implemented in this skeleton script

  • Data in only the code (the command can also use a file)
  • Ping of IP address only (the command can also use DNS name)
  • Only NIC Auto Detect (the command can set a NIC)

Not shown here (privileged info)

You can use any you like and use in your environment

  • 2FA, two factor authentication
  • The reporting functions

Not sown now but maybe in a later post:

  • Staggered WoL ( delays between the startups)
  • The ssh based tools :
    Shutdown, Reboot and update remote servers
    They use keys to perform the tasks
  • Or any of the ad-ons, like links to Remote Desktop Apps

Use of the Script

Get if from my GitHub edit the variable nodeData and any other parts you like. make it executable chmod +x wolstart.sh. Then run it by ./wolstart.sh.

wget https://raw.githubusercontent.com/nallej/MyJourney/main/scripts/wolstart.sh
💡
Remeber that downloading scripts and running them may be dangerous.Read any script before running it and edit it to work for you.

Syntax: wolstart -f <path/file name> [-i <nam of NIC] [-n] [-h|--help]-

Turning Scripts into Commands

Edit the Script with the Modules Helps, Copyright, Version and Get Options (see code listing below) and you can make it into a command.

You do not need to send scripts to all users - just install it ones in /usr/local/bin and all user can use it. Commands are also easy to include in the install.

We use the wolstart.sh script as an example

Clean up the script

  1. Remove any personalized data in the script, nodeData
  2. Add options module - making the script work as a command
  3. Add Help and Get Options
  4. Test it ./wolstart.sh with all it's combinations
  5. Set the file privileges as you prefer 755 is the norm, but some have 750 or 700
  6. Make it executable by chmod -x or chmod 755
    755 root can read and write it, all users can read and run it
  7. Make it into a command by sudo cp wolstart.sh /usr/local/bin/wolstart
    - for all users copy it to /usr/local/bin
    - for a user copy it to ~/<user>/local/.bin
  8. Add a file with WoL info see my example file on GitHub
  9. Test the command
  10. Inform the user/users of the new tool they got: wolstart

Get a ready to use file from my GitHub

wget https://raw.githubusercontent.com/nallej/MyJourney/main/scripts/wolstart

Usage

Syntax: wolstart -f <path/file name> [-i <nam of NIC] [-n] [-h|--help]

If you only have one NIC wolstart -f my.nodes -n. ‌‌NIC AutoDetect will detect the NIC, no need for the -i option.

If you have multiple NICs wolstart -i enp2s0 -f my.nodes -i enp2s0.

Remember to set the NISs to accept Wake on Land in the BIOS.

Giraffe head
Photo by Chris Leipelt / Unsplash-

The Modules

These are the actual Modules in the Command version

Well, all bash scripts has the Bash Shebang.
The #! is called shebang and is used to tell the operating system which interpreter to use to parse the rest of the file,
followed by the path to the Bash interpreter.

#!/bin/bash 

Data Section Arrays and variables

For the command it's recommended to delete the variable nodeData.
And use a file with the info. See the sample file on my GitHub

# Data Section Arrays and variables ==========================================# 
macArray=(); ipArray=(); nameArray=(); statusArray=();startArray=() byn=0; byf=0; nic=0; err=0; wolFile=""; nodes=0 # Tally on nodes 
# Local list of nodes (not needed in a command) 
nodeData="NAS-1,192.0.2.40,00:11:22:33:44:55 Pve-1,192.0.2.41,00:11:22:33:44:55 
Pve-2,192.0.2.42,00:11:22:33:44:55 
Pve-3,192.0.2.43,00:11:22:33:44:55 
Pve-4,192.0.2.44,00:11:22:33:44:55 
Pve-5,192.0.2.45,00:11:22:33:44:55 
Pve-6,192.0.2.46,00:11:22:33:44:55 
Pve-7,192.0.2.47,00:11:22:33:44:55 
Pve-8,192.0.2.48,00:11:22:33:44:55 
Pve-9,192.0.2.49,00:11:22:33:44:55"

Generic Functions

Generic Functions are Functions I frequently used in my Scripts. These I copy into a new script as is, hardly any editing needed.

Semi generic declaration that makes a script acting more like a command.
This is the output from Get Options Module ( wolstart -h ).

helps

helps(){ # Function to show help
    clear
    echo "Syntax: wolstart [-f <file name>] [-i <NIC>] [-n] [-h|--help] -c -v"
    echo "Options"
    echo "  - i the name of your NIC to use: eth0"
    echo "      use if NIC AutoDetect don't work (more than 1 NIC)"
    echo "  - f use a file for data <path>/file: wolstart -f ~/my.wol.nodes"
    echo "      use if you have no local data (nodeData)"
    echo "  - n use ping by name instead of IP"
    echo "  - h Show this help"
    echo "  - c Copyright statement"
    echo "  - v Version statement"
    echo "  To use internal data: wolstarts [-n]"
    echo ""
    }

version

version() { # Function to show version info
    echo ""
    echo "wolstart.sh Wake on Lan for Servers"
    echo "Part of the MyJourney project @ homelab.casaursus.net (based on a 20 year old script of mine)"
    echo ""
    echo "Created by Nalle Juslén 27.8.2020, version 1.1 1.12.2021"
    echo "  v.2.0 4.1.2022, v. 2.1 9.3.2022, v. 2.2 29.8.2022"
    echo "  v.3.0 1.9.2023, v. 3.1 6.9.2023"
    echo ""
    }
copyright(){ # Function to show Copyright info
    echo ""
    echo "Copyright (C) 2023 Free Software Foundation, Inc."
    echo "License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>"
    echo ""
    echo "This is free software; you are free to change and redistribute it."
    echo "There is NO WARRANTY, to the extent permitted by law."
    echo ""
    }

Other Generic Functions

Many of my scripts use color and needs evelated privilidges

useColor

This way its easy to add color to the output. Link to my post

useColors() { # Function to define colors ------------------------------------#
    # color code   color as bold
    red=$'\e[31m'; redb=$'\e[1;31m' # call red with $red and bold as $redb
    grn=$'\e[32m'; grnb=$'\e[1;32m' # call as green $grn as bold green $grnb
    yel=$'\e[33m'; yelb=$'\e[1;33m' # call as yellow $yel as bold yellow $yelb
    blu=$'\e[34m'; blub=$'\e[1;34m' # call as blue $blu as bold blue $blub
    mag=$'\e[35m'; magb=$'\e[1;35m' # call as magenta $mag as bold magenta $magb
    cyn=$'\e[36m'; cynb=$'\e[1;36m' # call as cyan $cyn as cyan bold $cynb
    end=$'\e[0m'
    }

spinner

I use spinners in scripts to indicate a longer period of inactivity due to background tasks running. Usage: choose one array and set the delay.

spinner() { # Function to display an animated spinner Choose a array -------------------#
    local array1=("◐" "◓" "◑" "◒")
    local array2=("░" "▒" "▓" "█")
    local array3=("╔" "╗" "╝" "╚")
    local array4=("┌" "┐" "┘" "└")
    local array5=("▄" "█" "▀" "█")
    local array6=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
    local array7=("܀" " " "܀")
    local array8=(" " "٠" " " "٠" " " "܀" "܀")
    local array9=("🌑" "🌒" "🌓" "🌔" "🌕" "🌖" "🌘") #running moon

    local delays=0.3 # Delay between each character 
    tput civis       # Hide cursor
    while :; do
        for character in "${array7[@]}"; do # Which Array to Use
            printf "%s" "$character"
            sleep "$delays"
            printf "\b"  # Move the cursor back
        done
    done
    }

Generic call of the Spinner

# Run function and/or code using a spinner. Start it - run code -Stop it -----#
    #start the Spinner
    spinner &
    spinner_pid=$! 
            Run some code here      # Run Code section
    kill "$spinner_pid"             # Terminate Spinner
    wait "$spinner_pid" 2>/dev/null
    tput cnorm                      # Retun of the Cursor
#-----------------------------------------------------------------------------#

setroot

If part of the script or the whole script needs elevated privileges, this section will check and set if not root already. ‌‌Just do wolstart instead of sudo wolstart and the command will ask for sudo when needed.

setroot() { # Function I am root ---------------------------------------------#
    if [[ "$EUID" -ne 0 ]]; then  # 0 = I am root
        sudo -k
        if sudo true ; then       # Correct password
            return
          else
            echo "${redb} Wrong password! Execution halted"
            exit                  #exit if 3 times wrong
        fi
    fi
    }

Local Functions

These Functions are Local to this Script or Command.
Using Functions make the Main program easier to understand and edit.

initDataFile

Initiation of the arrays used from a file -f <path/file>

initDataFile() { # Function read and split the file with csv_data into the arrays  
    i=1
    while IFS=',' read -r nameArray ipArray macArray; do
        nameArray+=("$nameArray"); ipArray+=("$ipArray"); macArray+=("$macArray")
        (( nodes++ )) 
    done < "${wolFile}" # Read from file /dir/file
    }

initDataLocal

Initialization of the arrays used from a local variable nodeData

initDataLocal() { # Function read local data and split the csv_data into the arrays  
    i=1
    while IFS=',' read -r nameArray ipArray macArray; do
        nameArray+=("$nameArray"); ipArray+=("$ipArray"); macArray+=("$macArray")
        (( nodes++ ))
    done <<< "$nodeData" # Read local data
    }

‌testping

Ping for a server by IP or DNS name. Retries can be set by the RETRY variable.

testPing () { # Function to test by ping if a Server is down -----------------#
    RETRY=1
        ping -I $myNIC -c $RETRY $1 > /dev/null 2>&1 # ping with no output
        status=$?
    return $status
    }

downServers

Find servers in the list that are off line.

downServers() { # Fuction checking for down servers --------------------------#
    i=1
    while [ $i -le $nodes ]
    do
        statusArray[$i]=''
        if [ $byn == true ]; then testPing ${nameArray[$i]}; else testPing ${ipArray[$i]}; fi    
        statusArray[$i]=$? 
        if [[ ${statusArray[$i]} == 0 ]]; then # display status
            echo -e "\b  ${nameArray[$i]}\t ${grn}✔  running  ${end}"
          else 
            echo -e "\b  ${nameArray[$i]}\t ${red}✘  off line ${end}"
        fi
        (( i++ ))
    done
    }

askStart

Ask if a server marked as down to be rebooted or not

askStart() { # Function Ask to start down Servers ----------------------------#
    tput setaf 3
    echo -e "\n$yel  \e[4mStart Servers not running              \e[0m"
    i=1
    while [ $i -le $nodes ]
    do
      if [[ ${statusArray[$i]} -ne 0 ]]; then
         read -rp "  Start node: ${nameArray[$i]} ${ipArray[$i]} [y/N] : " o 
         startArray[$i]=$o
      fi
      (( i++ ))
    done
    }

startServers

Reboot selected servers

startServers() { # Function for starting servers chosen to run ---------------#
    i=1; err=0
    while [ $i -le $nodes ]
    do
        if [[ ${startArray[$i]} == [yY] ]]; then
            echo -e "$yel \b  Booting up: $end\b ${nameArray[$i]} @ ${ipArray[$i]} MAC ${macArray[$i]}"; 
            sudo etherwake -i $myNIC ${macArray[$i]} 2>/dev/null
            if [ $? == 0 ]; then tput cuu1; echo -e "$grn✔$end"; else tput cuu1; err=1; echo -e "$red✘$end"; fi
            sleep .5; 
        fi
        (( i++ ))
    done
    echo -e "\n$grnb\bSelected Servers start to boot up, it will take several minutes.$end"
    if [ $err == 1 ]; then echo -e "$redb \bError$end$yel Servers with the $red✘$yel prefix faild the start command.$end"; fi
    exit
    }

Get Options Module

Get Options is a semi Generic Module. Initialization of the script by importing variables and setting status variables. See the Help, Copyright and Version Modules.

By getopts command it's easy to import user inputs to Commans or Scripts.
It reads the parts after the command and react to them. Syntag i: andd f: means its a two part input (reads i and the next as the second part) and hncv are the input.

while getopts ":i:hf:ncv" option; do # Get Options ---------------------------#
   case $option in
      i) nic=true; myNIC=$OPTARG;;
      f) byf=true; wolFile=$OPTARG;;
      n) byn=true; pingn="by name";;
      c) copyright; exit;;
      v) version; exit;; 
      h) helps; exit;;
      \?) echo "Error: Invalid option"; exit;;
   esac
done # -----------------------------------------------------------------------#

Main Script

This is the program part of the Script. Here goes the logic and functionality.
The heavy lifting is perfomed by the Functions.

# Main Script ================================================================#

useColors        # Use color codes
clear            # Clear the screan
if [ $nic == false ]; then myNIC=$( ls /sys/class/net | grep ^e); fi  #NIC AutoDetect

if [ $byf == false ]; then # Using local 
    echo -e "\n$yelb \bStart locally stored nodes$end"
    initDataLocal
  else # use a file
    echo -e "\n$yelb \bStart nodes in file:$end $wolFile $yel \busing$end $myNIC$yel"
    initDataFile
fi

tput setaf 3   # Set text to yellow foreground
echo -e "\nInitialaizing with ping $end$pingn$yel"
echo -e "\b  Servers now running out of the$end $nodes$yel nodes$end"
#tput sgr0 # set graphic rendition to default
echo -e "  \e[4mnode      status  \e[0m"

# Run function and/or code using a spinner. Start it - run code -Stop it -----#
    spinner &
    spinner_pid=$! # Run code with spinner running
            downServers 
    kill "$spinner_pid"; wait "$spinner_pid" 2>/dev/null; tput cnorm # Terminate spinner
#-----------------------------------------------------------------------------#

# Do you want to run the rest of the script
read -rp $'\n\e[1;36m  Do you like to continue [Yn]: \e[0m' continue 
    if [[ $continue == [nN] ]]; then 
        exit 
      else
        askStart # Function asking to start nodes not running 
        read -rp $'\n\e[1;36m  Start selected Servers [y/N] : \e[0m' ok # Ask for confirmation
            echo ""
            if [[ $ok == [yY] ]]; then
                setroot; tput cuu1; startServers # Function Start the choosen ones
              else
                echo -e "\n${redb} No Servers Started. $end${yel}Operators choise ${end}"
            fi
    fi
# End of script ==============================================================#

References

wol [1] ping [2] tput [3] getopts [4] aol [5]


  1. Wake on Lan Wikipedia, Arch WoL ↩︎

  2. The ping command man page ↩︎

  3. See tput in terminfo Linux man page Wikipedia tput Wikipedia ↩︎

  4. See getopts man page ↩︎

  5. Alert on Lan Wikipedia ↩︎