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
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
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
- Remove any personalized data in the script,
nodeData
- Add options module - making the script work as a command
- Add Help and Get Options
- Test it
./wolstart.sh
with all it's combinations - Set the file privileges as you prefer 755 is the norm, but some have 750 or 700
- Make it executable by
chmod -x
orchmod 755
755 root can read and write it, all users can read and run it - 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
- Add a file with WoL info see my example file on GitHub
- Test the command
- 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.
The Modules
These are the actual Modules in the Command version
Header
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.
Help Copyright and Version section
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
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]