Skip to content
Snippets Groups Projects
Commit 26d78303 authored by Hahn Axel (hahn)'s avatar Hahn Axel (hahn)
Browse files

initial commit

parents
Branches
No related tags found
No related merge requests found
config/userconfig.sh
config/iml/commands.txt
config/iml/serverlist.json
config/commands.txt
logs/returncodes.log
# MULTI REMOTE EXECUTION WITH SSH
A shellscript written in Bash to execute a single command on one or many target hosts via ssh.
A target host can define a jumphost if it is not accessible directly.
The script is designed to run in interactive mode but supports command line parameters for scripting too.
## Installation
Go to your directory where you place scripts and clone it
With read access - via https:
```txt
cd ~/scripts
git clone https://git-repo.iml.unibe.ch/admins/multi-remote-execution.git``
```
As developer - to submit changes into git via ssh
```txt
cd ~/scripts
git clone git@git-repo.iml.unibe.ch:admins/multi-remote-execution.git
```
## First start
Just start `multi_ssh.sh` - it will initialize a user config file with the defaults.
Without a parameter you reach the interactive mode.
To see the parameters start `multi_ssh.sh -h`
```txt
_______________________________________________________________________________
MULTI REMOTE EXECUTION WITH SSH ______
________________________________________________________________________/ v0.14
SYNTAX:
multi_ssh.sh [OPTIONS] [SERVERLIST]
OPTIONS:
-c|--command [COMMAMD]
set a command for remote execution. A 2nd value
for the new command is optional. Without param
you reach an interactive mode.
Save config and exit.
-h|--help show this help and exit
-l|--list list servers
-t|--tag [TAGNAME(s)]
set a tag to filter serverlist.
To set multiple tags quote it as a single string
and seperate them wih a space. Without param
you reach an interactive mode.
Save config and exit.
PARAMETERS:
The serverlist is a combination of servers that should
execute the command [hostname -f].
It can be the number of a server or a
A special item is ALL - it executes the command on all servers
Without a parameter you reach the interactive mode.
EXAMPLES:
multi_ssh.sh -c - set a new command. Enter it on the prompt
multi_ssh.sh -c "sudo reboot" - set a given command
multi_ssh.sh -t "mon live" - set filter tags "mon" and "live"
multi_ssh.sh -l - list servers that match all tags (AND condition)
multi_ssh.sh 1 2 5 - run the command on 1st, 2nd and 5th server
multi_ssh.sh monitor - run the command on the first server that matches 'monitor'
```
## File structure
```txt
├── color.class.sh needed file with functions for coloring
├── config
│ ├── commands.txt list of pre defined commands
│ ├── serverlist.json Json file with servers and tags
│ └── userconfig.sh local userspecific config
├── logs
│ └── returncodes.log log of executed commands
├── multi_ssh.sh << main script to run
└── README.md this file
```
#!/bin/bash
# ======================================================================
#
# COLORS
#
# a few shell functions for colored output
#
# ----------------------------------------------------------------------
# License: GPL 3.0
# Source: <https://github.com/axelhahn/bash_colorfunctions>
# Docs: <https://www.axel-hahn.de/docs/bash_colorfunctions/>
# ----------------------------------------------------------------------
# 2023-08-09 ahahn 0.1 initial lines
# 2023-08-09 ahahn 0.2 hide output of regex test with grep
# 2023-08-13 ahahn 0.3 introduce of color presets with foreground and background
# 2023-08-13 ahahn 0.4 list presets, debug, count of colors
# 2023-08-13 ahahn 0.5 support of RGB hex code
# 2023-08-14 ahahn 0.6 fix setting fg and bg as RGB hex code
# 2023-08-14 ahahn 0.7 remove color.ansi; respect NO_COLOR=1
# 2023-08-16 ahahn 0.8 add function color.preset
# ======================================================================
_VERSION=0.8
typeset -i COLOR_DEBUG; COLOR_DEBUG=0
# ----------------------------------------------------------------------
# CONSTANTS
# ----------------------------------------------------------------------
declare -A BGCOLOR_CODE
declare -A COLOR_CODE
# background colors
BGCOLOR_CODE[black]="40"
BGCOLOR_CODE[red]="41"
BGCOLOR_CODE[green]="42"
BGCOLOR_CODE[brown]="43"
BGCOLOR_CODE[blue]="44"
BGCOLOR_CODE[purple]="45"
BGCOLOR_CODE[cyan]="46"
BGCOLOR_CODE[lightgray]="47"
BGCOLOR_CODE[darkgray]="1;40"
BGCOLOR_CODE[lightred]="1;41"
BGCOLOR_CODE[lightgreen]="1;42"
BGCOLOR_CODE[yellow]="1;43"
BGCOLOR_CODE[lightblue]="1;44"
BGCOLOR_CODE[lightpurple]="1;45"
BGCOLOR_CODE[lightcyan]="1;46"
BGCOLOR_CODE[white]="1;47"
# foreground colors
COLOR_CODE[black]="30"
COLOR_CODE[red]="31"
COLOR_CODE[green]="32"
COLOR_CODE[brown]="33"
COLOR_CODE[blue]="34"
COLOR_CODE[purple]="35"
COLOR_CODE[cyan]="36"
COLOR_CODE[lightgray]="37"
COLOR_CODE[darkgray]="1;30"
COLOR_CODE[lightred]="1;31"
COLOR_CODE[lightgreen]="1;32"
COLOR_CODE[yellow]="1;33"
COLOR_CODE[lightblue]="1;34"
COLOR_CODE[lightpurple]="1;35"
COLOR_CODE[lightcyan]="1;36"
COLOR_CODE[white]="1;37"
# custom presets as array of foreground and background color
#
# +--- the label is part of the variable
# |
# v
# COLOR_PRESET_error=("white" "red")
# COLOR_PRESET_ok=("white" "green")
# ----------------------------------------------------------------------
# PRIVATE FUNCTIONS
# ----------------------------------------------------------------------
# write debug output - if debugging is enabled
# Its output is written to STDERR
# param string text to show
function color.__wd(){
test "$COLOR_DEBUG" = "1" && >&2 echo "DEBUG: $*"
}
# test, if given value is a known color name
# param string colorname to test
function color.__iscolorname(){
test -n "${COLOR_CODE[$1]}" && return 0
return 1
}
# test, if given value is a value 0..7
# param string color to test
function color.__iscolorcode(){
test "$1" = "0" && return 0
test "$1" = "1" && return 0
test "$1" = "2" && return 0
test "$1" = "3" && return 0
test "$1" = "4" && return 0
test "$1" = "5" && return 0
test "$1" = "6" && return 0
test "$1" = "7" && return 0
return 1
}
# test, if given value is an ansi code
# param string color to test
function color.__iscolorvalue(){
if grep -E "^([01];|)[34][0-7]$" >/dev/null <<< "$1" ; then
return 0
fi
return 1
}
# test, if given value is an rgb hexcode eg. #80a0f0
# param string color to test
function color.__isrgbhex(){
if grep -iE "^#[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]$" >/dev/null <<< "$1" ; then
return 0
fi
return 1
}
# convert rgb hex code eg. #80a0f0 to 3 decimal values
# output us a string with space separated values for red, green, blue
# param string color as "#RRGGBB" to convert
function color.__getrgb(){
local _r
local _g
local _b
if color.__isrgbhex "$1"; then
_r=$( cut -c 2,3 <<< "$1" )
_g=$( cut -c 4,5 <<< "$1" )
_b=$( cut -c 6,7 <<< "$1" )
echo "$((16#$_r)) $((16#$_g)) $((16#$_b))"
fi
}
# test, if given value is a color that can be one of
# - colorname
# - value 0..7
# - ansi code
# param string color to test
function color.__isacolor(){
if color.__iscolorname "$1"; then return 0; fi
if color.__iscolorcode "$1"; then return 0; fi
if color.__iscolorvalue "$1"; then return 0; fi
if color.__isrgbhex "$1"; then return 0; fi
color.__wd "$FUNCNAME is acolor: $1 --> No"
return 1
}
# test, if given value is an existing preset
# param string color to test
function color.__isapreset(){
local _colorset
eval "_colorset=\$COLOR_PRESET_${1}"
test -n "$_colorset" && return 0
return 1
}
# respect NO_COLOR=1
# return 1 if colors are allowed to be used.
function color.__usecolor(){
test "$NO_COLOR" = "1" && return 1
return 0
}
# set foreground or background
# param string color as
# - basic color 0..7 OR
# - color name eg. "black" OR
# - a valid color value eg. "1;30" OR
# - a hex code eg. "#10404f"
# param integer what to set; '3' for for foreground or '4' for background colors
function color.__fgorbg(){
local _color="$1"
local _prefix="$2"
color.__wd "$FUNCNAME $1 $2"
if color.__iscolorname "${_color}"; then
color.__wd "yep, ${_color} is a color name."
test "$_prefix" = "3" && color.set "${COLOR_CODE[${_color}]}"
test "$_prefix" = "4" && color.set "${BGCOLOR_CODE[${_color}]}"
else
if color.__iscolorcode "${_color}"; then
color.__wd "yep, ${_color} is a color code."
else
if color.__iscolorvalue "${_color}"; then
color.__wd "yep, ${_color} is a color value."
color.set "${_color}"
else
if color.__isrgbhex "${_color}"; then
local _r
local _g
local _b
read -r _r _g _b <<< $( color.__getrgb "${_color}" )
color.set "${_prefix}8;2;$_r;$_g;$_b"
else
>&2 echo "ERROR: color '${_color}' is not a name nor a value between 0..7 nor a valid color value nor RGB."
fi
fi
fi
fi
}
# ----------------------------------------------------------------------
# FUNCTIONS :: helpers
# ----------------------------------------------------------------------
# get count of colors in the current terminal
function color.count(){
tput colors
}
# enable debug flag
function color.debugon(){
COLOR_DEBUG=1
color.__wd "$FUNCNAME - debugging is enabled now"
}
# disable debug flag
function color.debugoff(){
color.__wd "$FUNCNAME - disabling debugging now"
COLOR_DEBUG=0
}
# show debugging status
function color.debugstatus(){
echo -n "INFO: color.debug - debugging is "
if [ $COLOR_DEBUG -eq 0 ]; then
echo "DISABLED"
else
echo "ENABLED"
fi
}
# show help
function color.help(){
local _self; _self='[path]/color.class.sh'
color.reset
local _debug=$COLOR_DEBUG
COLOR_DEBUG=0
echo "_______________________________________________________________________________"
echo
color.echo "red" " ### ### # ### ####"
color.echo "yellow" " # # # # # # # #"
color.echo "white" " # # # # # # ####"
color.echo "yellow" " # # # # # # # #"
color.echo "red" " ### ### ##### ### # #"
echo "_________________________________________________________________________/ v$_VERSION"
echo
sed "s#^ ##g" << EOH
HELP:
'color' is a class like component to simplify the handling of ansi colors and keeps
the color settings readable. A set NO_COLOR=1 will be respected.
Author: Axel Hahn
License: GNU GPL 3.0
Source: <https://github.com/axelhahn/bash_colorfunctions>
Docs: <https://www.axel-hahn.de/docs/bash_colorfunctions/>
FUNCTIONS:
---------- Information:
color.help this help
color.list show a table with valid color names
color.presets show a table with defined custom presets
color.count get count of colors in the current terminal
color.debugon enable debugging
color.debugoff disable debugging
color.debugstatus show debugstatus
---------- Colored output:
color.bg COLOR (COLOR2)
set a background color; a 2nd parameter is optional to set
a foreground color too
color.fg COLOR (COLOR2)
set a foreground color; a 2nd parameter is optional to set
a background color too
color.preset PRESET
Apply the color set of foreground and background of a given
preset name.
color.echo COLOR|PRESET (COLOR2) TEXT
write a colored text with carriage return and reset colors
The 1st param must be a COLOR(code/ name) for the
foreground or a label of a preset.
The 2nd CAN be a color for the background, but can be
skipped.
Everything behind is text for the output.
color.print COLOR|PRESET (COLOR2) TEXT
see color.echo - the same but without carriage return.
color.reset reset colors
color.set RAWCOLOR (RAWCOLOR2 (... RAWCOLOR_N))
set ansi colors; it can handle multiple color values
---------- Other:
color.blink start blinking text
color.bold start bold text
color.invert start inverted text
color.underline start underline text
VALUES:
COLOR a color; it can be...
- a color keyword, eg black, blue, red, ... for all
known values run 'color.list'
- a value 0..7 to set basic colors 30..37 (or 40..47)
- an ansi color value eg. "30" or "1;42"
- RGB hexcode with '#' as prefix followed by 2 digit
hexcode for red, green and blue eg. "#10404f"
(like css rgb color codes)
PRESET Name of a custom preset; see DEFINE PRESETS below.
RAWCOLOR an ansi color value eg. "30" (black foreground) or
"1;42" (lightgreen background)
DEFINE PRESETS:
A shortcut for a combination of foreground + background color. The label
is part of a bash variable with the prefix 'COLOR_PRESET_'.
The value is a bash array with 2 colors for foreground and background.
See the value description for COLOR above.
SYNTAX:
COLOR_PRESET_<LABEL>=(<FOREGROUND> <BACKGROUND>)
To see all defined presets use 'color.presets'
EXAMPLES:
First you need to source the file $_self.
. $_self
(1)
Show output of the command 'ls -l' in blue
color.fg "blue"
ls -l
color.reset
(2)
show a red error message
color.echo "red" "ERROR: Something went wrong."
(3)
Use a custom preset:
COLOR_PRESET_error=("white" "red")
color.echo "error" "ERROR: Something went wrong."
This defines a preset named "error". "white" is a colorname
for the foreground color, "red" ist the background.
EOH
if [ -n "$NO_COLOR" ]; then
echo -n "INFO: NO_COLOR=$NO_COLOR was set. The coloring functionality is "
if ! color.__usecolor; then
echo "DISBALED."
else
echo "ENABLED (must be 1 to disable)."
fi
echo
else
echo "INFO: NO_COLOR will be respected - but it is not set."
fi
COLOR_DEBUG=$_debug
}
# a little helper: show colors and the color codes
function color.list(){
color.reset
local _debug=$COLOR_DEBUG
COLOR_DEBUG=0
echo
echo "List of colors:"
echo
echo "--------------------------------------------------"
echo "color | foreground | background"
echo "--------------------------------------------------"
for i in "${!COLOR_CODE[@]}"
do
printf "%-15s %4s " $i ${COLOR_CODE[$i]}
color.set "${COLOR_CODE[$i]}"
color.set "40"
printf " Test "
color.set "1;47"
color.set "${COLOR_CODE[$i]}"
printf " Test "
color.reset
printf " %5s " ${BGCOLOR_CODE[$i]}
color.set ${BGCOLOR_CODE[$i]}
printf " Test "
color.reset
echo
done | sort
color.reset
echo "--------------------------------------------------"
echo
COLOR_DEBUG=$_debug
}
# little helper: sow defined presets and its preview
function color.presets(){
local _label
local _value
local _colorvar
local _fg
local _bg
color.reset
local _debug=$COLOR_DEBUG
COLOR_DEBUG=0
if ! set | grep "^COLOR_PRESET_.*=(" >/dev/null; then
echo "INFO: No preset was defined yet."
echo "To set one define shell variables with an array of 2 colors:"
echo " COLOR_PRESET_<LABEL>=(<FOREGROUND> <BACKGROUND>)"
echo "For more help call 'color.help' or see the docs."
else
echo
echo "List of presets:"
echo
echo "---------------------------------------------------------------------"
echo "label | foreground | background | example"
echo "---------------------------------------------------------------------"
set | grep "^COLOR_PRESET_.*=(" | while read -r line
do
_label=$( cut -f 1 -d '=' <<< "$line" | cut -f 3- -d '_')
_example=$( color.print "$_label" "example for peset '$_label'" )
_colorvar="COLOR_PRESET_${_label}"
eval "_fg=\${$_colorvar[0]}"
eval "_bg=\${$_colorvar[1]}"
printf "%-10s | %-12s | %-12s | %-50s\n" "$_label" "${_fg}" "${_bg}" "$_example"
done
echo "---------------------------------------------------------------------"
echo
fi
COLOR_DEBUG=$_debug
}
# ----------------------------------------------------------------------
# FUNCTIONS :: set color
# ----------------------------------------------------------------------
# set background color
# param string backround color 0..7 OR color name eg "black" or a valid color value eg "1;30"
# param string optional: foreground color
function color.bg(){
color.__wd "$FUNCNAME $1"
color.__fgorbg "$1" 4
test -n "$2" && color.fg "$2"
}
# get a color of a preset
# param string name of preset
# param integer array index; 0= foreground; 1= background
function color.__getpresetcolor(){
local _label=$1
local _index=$2
local _colorvar
_colorvar="COLOR_PRESET_${_label}"
eval "echo \${$_colorvar[$_index]}"
}
# set foreground color
# param string foreground color 0..7 OR color name eg "black" or a valid color value eg "1;30"
# param string optional: background color
function color.fg(){
color.__wd "$FUNCNAME $1"
color.__fgorbg "$1" 3
test -n "$2" && color.bg "$2"
}
# set colors of a preset
# param string label of a preet
function color.preset(){
if color.__isapreset "$1"; then
local _colorvar
local _colfg=$( color.__getpresetcolor "$1" 0)
local _colbg=$( color.__getpresetcolor "$1" 1)
color.reset
test -n "$_colfg" && color.__fgorbg "$_colfg" 3
test -n "$_colbg" && color.__fgorbg "$_colbg" 4
else
>&2 echo "ERROR: this value is not a valid preset: $1. See 'color.presets' to see current presets."
fi
}
# ----------------------------------------------------------------------
# reset all colors to terminal default
function color.reset(){
color.__wd "$FUNCNAME"
color.set "0"
}
# start bold text
function color.bold(){
color.__wd "$FUNCNAME"
color.set "1"
}
# start underline text
function color.underline(){
color.__wd "$FUNCNAME"
color.set "4"
}
# start blinking text
function color.blink(){
color.__wd "$FUNCNAME"
color.set "5"
}
# start inverted text
function color.invert(){
color.__wd "$FUNCNAME"
color.set "7"
}
# ----------------------------------------------------------------------
# write ansicode to set color combination
# param string color 1 as ansi value
# param string color N as ansi value
function color.set(){
local _out=
if color.__usecolor; then
for mycolor in $*
do
color.__wd "$FUNCNAME: processing color value '${mycolor}'"
_out+="${mycolor}"
done
color.__wd "$FUNCNAME: output is '\e[${_out}m'"
printf "\e[${_out}m"
else
color.__wd "$FUNCNAME: skipping - coloring is disabled."
fi
}
# ----------------------------------------------------------------------
# FUNCTIONS :: print
# ----------------------------------------------------------------------
# show a colored text WITH carriage return
# param string foreground color as code / name / value
# param string optional: background color as code / name / value
# param string text to print
function color.echo(){
color.__wd "$FUNCNAME $*"
local _param1="$1"
local _param2="$2"
shift 1
shift 1
color.print "$_param1" "$_param2" "$*"
echo
}
# show a colored text without carriage return
# param string foreground color as code / name / value or preset
# param string optional: background color as code / name / value
# param string text to print
function color.print(){
color.__wd "$FUNCNAME $*"
if color.__isacolor "$1"; then
if color.__isacolor "$2"; then
color.fg "$1" "$2"
shift 1
shift 1
else
color.fg "$1"
shift 1
fi
elif color.__isapreset "$1"; then
color.preset "$1"
shift 1
else
>&2 echo -n "ERROR: Wrong color values detected. Command was: colors.print $*"
fi
echo -n "$*"
color.reset
}
# ======================================================================
# --- simple commands (for all profiles)
hostname -f
sleep 3
# --- reboot a system
sudo reboot
# --- some more profile specific commands
{
# ------------------------------------------------------------------
# maintenance windows
# ------------------------------------------------------------------
"maintenance":[
{"label":"mon", "description":"Monthly maintenance window on Monday"},
{"label":"tue", "description":"Monthly maintenance window on Tuesday"}
],
# ------------------------------------------------------------------
# S E R V E R C O N F I G
# ------------------------------------------------------------------
"servers":[
{"host":"server-01.example.com", "tags":["live", "mon", "apt"]},
{"host":"server-02.example.com", "tags":["live", "mon", "yum"]},
{"host":"server-03.example.com", "tags":["live", "tue", "apt"], "bastion": "jumphost.example.com" }
]
}
\ No newline at end of file
## Introduction
Multi SSH shellscript written in Bash to execute a single command on one or many target hosts via ssh.
A target host can define a jumphost if it is not accessible directly.
The script is designed to run in interactive mode but supports a few command line parameters for scripting too.
In a server configuration file you can put your servers. Each server can have
* multiple tags, eg. for OS/ for hardware os VM/ for application/ for maintenance group
* optional: a jumphost (if it is not accessible directly).
## Requirements
* Bash
* GNU tools
* OpenSSH client
The script was written on Linux.
It was not tested on other OS - but it "should" run. On MS Windows you can use WSL or Cygwin / MSYS or similiar compiled binaries.
Mac OS ensure to have GNU variants of tools like sed, grep, ... in the PATH.
## Get the code
Go to your directory where you place scripts and clone it
With read access - via https:
```txt
cd ~/scripts
git clone https://git-repo.iml.unibe.ch/admins/multi-remote-execution.git``
```
As developer - to submit changes into git via ssh
```txt
cd ~/scripts
git clone git@git-repo.iml.unibe.ch:admins/multi-remote-execution.git
```
## File structure
These files are needed to run:
```txt
├── color.class.sh needed file with functions for coloring
├── config
│ ├── commands.txt list of pre defined commands (*)
│ ├── serverlist.json Json file with servers and tags (*)
│ └── userconfig.sh local userspecific config
├── logs
│ └── returncodes.log log of executed commands (will be created)
└── multi_ssh.sh << main script
```
(*) This file comes with a .dist file.
## Create configuration
In the config directory copy 2 *.dist files to the same filename without .dist:
```txt
cd dist
cp commands.txt.dist commands.txt
cp serverlist.json.dist serverlist.json
```
## Configuration
In the subfolder ``./config/`` are these files
* config/commands.txt
* config/serverlist.json
* config/userconfig.sh
### File commands.txt
This is a textfile with pre defined commands you want to use more often. The file is just plain text. Structure its content like you want.
**Example**:
```txt
# --- simple things / testing
hostname -f
sleep 20
# --- reboot a system
sudo reboot
```
### File serverlist.json
This is the example configuration in the dist file:
```txt
{
# ------------------------------------------------------------------
# maintenance windows
# ------------------------------------------------------------------
"maintenance":[
{"label":"mon", "description":"Monthly maintenance window on Monday"},
{"label":"tue", "description":"Monthly maintenance window on Tuesday"}
],
# ------------------------------------------------------------------
# S E R V E R C O N F I G
# ------------------------------------------------------------------
"servers":[
{"host":"server-01.example.com", "tags":["live","mon","apt"]},
{"host":"server-02.example.com", "tags":["live","mon","apt"]},
{"host":"server-03.example.com", "tags":["live","tue","apt"], "bastion": "jumphost.example.com" }
]
}
```
#### Commants
The serverlist.json is nearly JSON: next to plain JSON you can start a comment with ``#`` character that sets a comment up to the end of line.
The ``#`` cannot be used inside you JSON data.
#### maintenance
This is a group to describe tags for a maintenance window.
**Keys**:
* label: {string} name of the tag
* description: {string} description tag
#### servers
In the subkey "servers" are array values for each server you want to use as ssh target.
**Keys**:
* host: {string}
* tags: {array}
* bastion: {string} optional jumphost if a target is not reachable directly
### File userconfig.sh
This file wil be created on the first run.
It contains user inputs and some customizations.
The file is plain bash shell script and will be sourced by multi_ssh.
The file will be written if you set a tag or a command in multi_ssh. All other variables you need to confure in the file directly.
```bash
#!/bin/bash
# CONFIG for multi_ssh.sh - saved Do 19 Okt 2023 12:23:23 CEST
# admin user to connecto to other hosts
export sshuser=admin
# terminal program to open per connection
export terminal=gnome-terminal
# filter hosts, set command
export taglist="mon"
# command to execute on target host(s)
export remotecommand="hostname -f"
# colors
export COLOR_PRESET_cmd=( blue )
export COLOR_PRESET_bastion=( purple )
export COLOR_PRESET_lastexec=( green )
export COLOR_PRESET_terminal=( brown )
export COLOR_PRESET_prompt=( green )
export COLOR_PRESET_output=( darkgray )
export COLOR_PRESET_ok=( green )
export COLOR_PRESET_error=( red )
```
**Variables**:
* COLOR_PRESET_(...): {array} foreground and background color for a few presets.
* remotecommand: {string} saved remote command
* sshuser: {string} a user to be used for ssh connection. If a connection is via bastion host this user is used for bastion and for target.
* taglist: {string} savd list of tags seperated by space. The active servers are these that match all given tags (AND condition).
* terminal: {string} command to start a terminal. In it the ssh command will be executed. If a command will be executed you get a single terminal per target. The terminal must know the param "-e" to execute a command. Known as compatible: gnome-terminal (default), konsole (KDE), xterm
## First start
Just start `multi_ssh.sh` - it will initialize a user config file with the defaults.
Without a parameter you reach the interactive mode.
To see the parameters start `multi_ssh.sh -h`
```txt
_______________________________________________________________________________
MULTI REMOTE EXECUTION WITH SSH ______
________________________________________________________________________/ v0.14
SYNTAX:
multi_ssh.sh [OPTIONS] [SERVERLIST]
OPTIONS:
-c|--command [COMMAMD]
set a command for remote execution. A 2nd value
for the new command is optional. Without param
you reach an interactive mode.
Save config and exit.
-h|--help show this help and exit
-l|--list list servers
-t|--tag [TAGNAME(s)]
set a tag to filter serverlist.
To set multiple tags quote it as a single string
and seperate them wih a space. Without param
you reach an interactive mode.
Save config and exit.
PARAMETERS:
The serverlist is a combination of servers that should
execute the command [hostname -f].
It can be the number of a server or a
A special item is ALL - it executes the command on all servers
Without a parameter you reach the interactive mode.
EXAMPLES:
multi_ssh.sh -c - set a new command. Enter it on the prompt
multi_ssh.sh -c "sudo reboot" - set a given command
multi_ssh.sh -t "mon live" - set filter tags "mon" and "live"
multi_ssh.sh -l - list servers that match all tags (AND condition)
multi_ssh.sh 1 2 5 - run the command on 1st, 2nd and 5th server
multi_ssh.sh monitor - run the command on the first server that matches 'monitor'
```
<html>
<div class="hero">
<h2>Multi SSH</h2>
Send a command via ssh to one or multiple servers
</div>
</html>
TODO
Free software and Open Source from University of Bern :: IML - Institute of Medical Education
📄 Source: TODO \
📜 License: GNU GPL 3.0 \
📗 Docs: TODO
{
"title": "Multi SSH",
"author": "Axel Hahn",
"tagline": "Send a command via ssh to one or multiple servers",
"ignore": {
"files": ["30_PHP-client/Plugins/Checks/_skeleton.md"],
"folders": ["99_Not_Ready"]
},
"html": {
"auto_toc": true,
"auto_landing": false,
"date_modified": false,
"jump_buttons": true,
"edit_on_github_": "iml-it/__PROJECT__/tree/master/docs",
"edit_on_": {
"name": "Gitlab",
"basepath": "https://git-repo.iml.unibe.ch/iml-open-source/__PROJECT__/tree/master/docs"
},
"links": {
"Git Repo": "__GITURL__",
"IML Opensource": "https://os-docs.iml.unibe.ch/"
},
"theme": "daux-blue",
"search": true
}
}
/*
override css elements of daux.io blue theme
version 2023-10-09
*/
:root {
/* Axels Overrides */
--color-text: #234;
--link-color: #822;
--brand-color: var(--color-text);
--brand-background: var(--body-background);
--hr-color: none;
--search-field-background: none;
--search-field-border-color: none;
--sidebar-background: var(--body-background);
--sidebar-border-color: none;
--sidebar-link-active-background: #e8f4f6;
--sidebar-link-active-background: #eee;
/* Axels custom values */
--axel_bg-toc: var(--body-background);
--axel_bg-toc-head: #f8f8f8;
--axel_brand-background: none;
--axel_brand-pre-background: rgb(255, 0, 51);
;
--axel_brand-pre-background-hover: rgb(255, 0, 51);
;
--axel_h1_header: none;
--axel_h1: #111;
--axel_h1-bg: none;
--axel_h1-bottom: 3px solid none;
--axel_h2: #222;
--axel_h2-bg: none;
--axel_h2-bottom: 0px solid #467;
--axel_h2-hero-bottom: 2px solid #912;
--axel_h3: #333;
--axel_h3-bottom: 0px solid #ddd;
--axel_h4: #444;
--axel_hero_bg: #f8f8f8;
--axel_img-border: 2px dashed #ccc;
--axel_nav-bg: #fcfcfc;
--axel_nav-buttomborder: #ddd;
--axel_pre-background: #f8f8f8;
--axel-th-background: #e0e4e8;
--axel-article-nav-border-top: 0px dotted #ddd;
}
.dark {
/* Axels Overrides */
--color-text: #c0c0c0;
--link-color: #c66;
--brand-color: var(--color-text);
--brand-background: var(--body-background);
--hr-color: none;
--code-tag-background-color_: #bcc;
--search-field-background: none;
--search-field-border-color: none;
--sidebar-background: var(--body-background);
--sidebar-border-color: none;
--sidebar-link-active-background: #333;
/* Axels custom values */
--axel_bg-toc: var(--body-background);
--axel_bg-toc-head: #333;
--axel_brand-background: none;
--axel_brand-pre-background: rgb(255, 0, 51);
;
--axel_brand-pre-background-hover: rgb(255, 0, 51);
;
--axel_h1_header: none;
--axel_h1: #578;
--axel_h1-bg: none;
--axel_h1-bottom: none;
--axel_h2: #467;
--axel_h2-bg: none;
--axel_h2-bottom: 0px solid #256;
--axel_h2-hero-bottom: 2px solid #712;
--axel_h3: #589;
--axel_h3-bottom: 0px solid #333;
--axel_hero_bg: #242424;
--axel_img-border: 2px dashed #555;
--axel_nav-bg: #242424;
--axel_nav-buttomborder: #555;
--axel_pre-background: #bcc;
--axel-th-background: #203038;
--axel-article-nav-border-top: 0px dotted #234;
}
/* ---------- left side ---------- */
a.Brand::before {
background: var(--axel_brand-pre-background);
color: #fff;
font-family: arial;
font-weight: bold;
padding: 0.5em 0.3em;
content: 'IML';
margin-right: 0.4em;
float: left;
}
a.Brand:hover::before {
background: var(--axel_brand-pre-background-hover);
}
a.Brand {
background: var(--axel_brand-background);
font-size: 200%;
height: 4em;
}
/* ---------- page header: breadcrumb ---------- */
.Page__header {
border: none;
}
.Page__header a {
color: var(--axel_h1_header);
}
.Page__header h1 {
font-size: 1.3em;
}
/* ---------- page content ---------- */
.s-content {
padding-top: 1em;
}
.s-content h1 {
background: var(--axel_h1-bg);
color: var(--axel_h1);
font-size: 200%;
font-weight: bold;
margin-bottom: 2em;
margin-top: 2em;
border-bottom: var(--axel_h1-bottom);
}
.s-content h2 {
background: var(--axel_h2-bg);
color: var(--axel_h2);
font-size: 190%;
font-weight: bold;
margin-top: 4em;
border-bottom: var(--axel_h2-bottom);
}
h1:first-of-type {
margin-top: 0em;
}
h2:first-of-type {
margin-top: 0em;
}
img{
border: var(--axel_img-border);
border-radius: 1.5em;
padding: 0.7em;
}
.s-content h3 {
background: var(--axel_h3-bg);
color: var(--axel_h3);
font-size: 150%;
font-weight: bold;
margin-top: 3em;
border-bottom: var(--axel_h3-bottom);
}
.s-content > h4 {
color: var(--axel_h4);
font-size: 135%;
font-weight: bold;
margin: 2em 0;
}
.s-content .TableOfContentsContainer h4 {
margin: 1em 0;
font-size: 100%;
text-align: center;
background-color: rgba(0, 0, 0, 0.05);
padding: 0.3em;
}
ul.TableOfContents a{
color: var(--color-text);
}
.s-content pre {
background: var(--axel_pre-background);
}
/* FIX smaller fnt size in tables */
.s-content table {
font-size: 1em;
}
.s-content table th {
background: var(--axel-th-background);
}
.s-content h3 code {
border: none;
background: none;
}
article nav {
border-top: var(--axel-article-nav-border-top);
margin: 8em 0 5em;
}
.Pager li>a {
padding: 1em 2em;
}
/* ---------- classes ---------- */
.required {
color: #a42;
}
.optional {
color: #888;
}
div.hero {
background: var(--axel_hero_bg);
border-radius: 2em;
padding: 5em 2em;
text-align: center;
margin-bottom: 1.5em;
}
div.hero h2 {
color: var(--color-text);
background: none;
border-bottom: var(--axel_h2-hero-bottom);
font-size: 300%;
margin: 0 0 2em;
}
/* ---------- TOC ---------- */
@media(min-width:1700px) {
.TableOfContentsContainer {
position: fixed;
right: 2em;
top: 1em;
}
}
.TableOfContentsContainer {
background-color: var(--axel_bg-toc);
padding: 0.5em;
}
.s-content .TableOfContentsContainer h4 {
background-color: var(--axel_bg-toc-head);
border-top-left-radius: 1em;
font-size: 1.1em;
margin: 0;
padding: 0;
}
.TableOfContentsContainer__content {
border-width: 1px;
font-size: 0.5em;
}
ul.TableOfContents ul {
list-style-type: none;
padding-left: 1em;
}
/* ----- Icons on links --- */
.EditOn a::before{
content: '✏️ ';
}
.Links a[href^="https://github.com/"]::before {
content: '🌐 ';
}
.Links a[href^="https://git-repo.iml.unibe.ch/"]::before {
content: '🌐 ';
}
.Links a[href^="https://os-docs.iml.unibe.ch"]::before {
content: '📗 ';
}
#!/bin/bash
# ======================================================================
#
# MULTI REMOTE EXECUTION
#
# ----------------------------------------------------------------------
# 2023-03-13 <axel.hahn@unibe.ch> WIP
# 2023-03-24 0.3 <axel.hahn@unibe.ch> show last execution
# 2023-04-17 0.4 <axel.hahn@unibe.ch> add ssh user; fix display of last execution
# 2023-06-05 0.5 <axel.hahn@unibe.ch> fix missing -e param
# 2023-09-07 0.6 <axel.hahn@unibe.ch> execute each command in a single terminal
# 2023-09-08 0.7 <axel.hahn@unibe.ch> show if a command is running; use color.class.sh
# 2023-09-08 0.8 <axel.hahn@unibe.ch> fix ERROR: color 'output' is not a name nor a value (...)
# 2023-10-06 0.9 <axel.hahn@unibe.ch> better matching of hostname in processes and log
# 2023-10-13 0.10 <axel.hahn@unibe.ch> change log format; simplify command string
# 2023-10-16 0.11 <axel.hahn@unibe.ch> change date format; serverlist timestamp hides date of current day
# 2023-10-16 0.12 <axel.hahn@unibe.ch> fix last success value for long hostnames
# 2023-10-18 0.13 <axel.hahn@unibe.ch> rewrite: config handled by json; filtering by tags
# 2023-10-18 0.14 <axel.hahn@unibe.ch> automatically init of userconfig; check terminal
# 2023-10-19 0.15 <axel.hahn@unibe.ch> Fix: Show running terminal in server list
# 2024-05-15 0.16 <axel.hahn@unibe.ch> add profiles; rename internal variables
# ======================================================================
_version=0.16
_self=$( basename $0 )
cd "$( dirname $0 )"
# file definitions
MRE_CONFIGFILE=config/userconfig.sh
# profile (folder below ./config/)
MRE_PROFILE=default
MRE_JSONFILE=
MRE_LOGFILE=logs/returncodes.log
MRE_SSHUSER=ladmin
MRE_COMMAND="hostname -f"
MRE_TERMINAL=gnome-terminal
MRE_TAGS=
# default colors
COLOR_PRESET_cmd=("blue")
COLOR_PRESET_bastion=("purple")
COLOR_PRESET_lastexec=("green")
COLOR_PRESET_terminal=("brown")
COLOR_PRESET_prompt=("green")
COLOR_PRESET_output=("darkgray")
COLOR_PRESET_ok=("green")
COLOR_PRESET_error=("red")
. color.class.sh
. "${MRE_CONFIGFILE}" 2>/dev/null
USAGE="
SYNTAX:
$_self [OPTIONS] [SERVERLIST]
OPTIONS:
-c|--command [COMMAMD]
set a command for remote execution. A 2nd value
for the new command is optional. Without param
you reach an interactive mode.
Save config and exit.
-h|--help show this help and exit
-l|--list list servers
-p|--profile [PROFILE]
set a profile for network environment
-t|--tag [TAGNAME(s)]
set a tag to filter serverlist.
To set multiple tags quote it as a single string
and seperate them wih a space. Without param
you reach an interactive mode.
Save config and exit.
PARAMETERS:
The serverlist is a combination of servers that should
execute the command [$MRE_COMMAND].
It can be the number of a server or a
A special item is ALL - it executes the command on all servers
Without a parameter you reach the interactive mode.
EXAMPLES:
$_self -c - set a new command. Enter it on the prompt
$_self -c \"sudo reboot\" - set a given command
$_self -t \"mon live\" - set filter tags \"mon\" and \"live\"
$_self -l - list servers that match all tags (AND condition)
$_self 1 2 5 - run the command on 1st, 2nd and 5th server
$_self monitor - run the command on the first server that matches 'monitor'
"
# ----------------------------------------------------------------------
# FUNCTIONS
# ----------------------------------------------------------------------
# ---------- PROFILE
# set a profile; switches to interactive mode if no param is given
# param string optional: new profile
function _setProfile(){
local new="$1"
if [ -z "$new" ]; then
find config -maxdepth 1 -type d | sed "s#^config[/]*##g"
echo
echo -n "Current: "
color.echo "cmd" "$MRE_PROFILE"
echo "Enter a profile name"
color.print "prompt" "> "
read -r new
fi
if [ -n "$new" ]; then
MRE_PROFILE=$new
_save
_getProfile
else
echo "Doing nothing."
fi
}
# check current profilename; if correct then set MRE_JSONFILE that
# points to a json with the serverlist
function _getProfile(){
if [ -z "$MRE_PROFILE" ]; then
echo "WARNING: profile is empty - set it to [default]"
MRE_PROFILE=default
fi
if [ ! -d "config/${MRE_PROFILE}/" ]; then
echo "ERROR: profile [${MRE_PROFILE}] is invalid - ./config/${MRE_PROFILE}/ does not exist"
exit 1
fi
if [ ! -f "config/${MRE_PROFILE}/serverlist.json" ]; then
echo
echo "WELCOME!"
echo "We need a file <serverlist.json> to start."
echo
echo "Create a file config/${MRE_PROFILE}/serverlist.json ... OR"
echo "You can create a new profile directory ./config/<PROFILE>/ and for a"
echo "seperate network with custom servers and commands."
echo "HINT: Copy the template config/default/serverlist.json.dist to start."
echo
echo "If you want to switch to another profile use $(basename $0 ) -p "
echo
exit 1
fi
MRE_JSONFILE=config/${MRE_PROFILE}/serverlist.json
}
# ---------- COMMANDS
# show a list of stored commands
function _getCommandlist(){
echo "---- List default commands :: ./config/commands.txt"
echo
color.preset "output"
cat "./config/commands.txt"
color.reset
if [ -f "./config/${MRE_PROFILE}/commands.txt" ]; then
echo
echo "---- Profile specific commands :: ./config/${MRE_PROFILE}/commands.txt"
echo
color.preset "output"
cat "./config/${MRE_PROFILE}/commands.txt"
color.reset
fi
}
# set a command; switches to interactive mode if no param is given
# param string optional: new command
function _setCommand(){
local newcommand="$1"
if [ -z "$newcommand" ]; then
_getCommandlist
echo
echo -n "Current: "
color.echo "cmd" "$MRE_COMMAND"
echo "Enter a command to execute (from given list or a new one)"
color.print "prompt" "> "
read -r newcommand
fi
if [ -n "$newcommand" ]; then
MRE_COMMAND=$newcommand
_save
else
echo "Doing nothing."
fi
}
# ---------- Json related: maintenance, servers, tags
# get json data; this functions removes comments befind "#"
# global: $MRE_JSONFILE
function _getJSON(){
cat $MRE_JSONFILE | sed "s,#.*$,,g"
}
# get a list of maintenance tags
# param1 string value of matching tag 1
# paramN string value of matching tag N
function _getMaintenances(){
_getJSON | jq ".maintenance[] .label "
}
# get a list of server entries matching the given tags
# Remark: a tag with spaces will be splittet
# param1 string value of matching tag 1
# paramN string value of matching tag N
function _getServerJson(){
local _cmd=".servers[]"
for mytag in $*
do
_cmd+=' | select(.tags[] == "'$mytag'")'
done
_getJSON | jq "${_cmd} | { host: .host, bastion: .bastion, tags: .tags } "
}
# get a single host record with all its properties as JSON
# param string hostname
function _getHostData(){
local _host="$1"
_getServerJson "$MRE_TAGS" | jq ". | select ( .host == \"$_host\" ) "
}
# get a property from a host item.
# the given hostname must match a host that matches given tags
# param string hostname
# param string property key; one of host|bastion|tags
function _getHostProperty(){
local _host="$1"
local _property="$2"
_getHostData "$myserver" | jq ".${_property}" | tr -d '"' | sed "s#^null\$##"
}
# show a list of all tags on screen
function _gettags(){
local _spacer=" "
echo "-- all current tags in hosts:"
color.preset output
_getJSON | jq ".servers[] | .tags " | grep '"' | tr -d ',"' | sort -u | sed "s#^ *##g" | sed "s#^#${_spacer}#g"
color.reset
echo "-- maintenance tags:"
color.preset output
_getMaintenances | sed "s#^#${_spacer}#g" | tr -d '"'
color.reset
}
# set a command; switches to interactive mode if no param is given
# param string optional: new command
function _setTags(){
local newtags="$1"
if [ -z "$newtags" ]; then
_gettags
echo
echo -n "Current: "
color.echo "cmd" "$MRE_TAGS"
echo "Enter tags to filter hosts; seprate mulitple tags with space (from given list or a new one)"
color.print "prompt" "> "
read -r newtags
fi
if [ -n "$newtags" ]; then
MRE_TAGS="$newtags"
_save
else
echo "Doing nothing."
fi
}
# ---------- save
# save params into user config
# gobal MRE_CONFIGFILE string filename of user config file
function _save(){
cat << EOC > "${MRE_CONFIGFILE}"
#!/bin/bash
# CONFIG for $_self - saved $(date)
# profile
export MRE_PROFILE=$MRE_PROFILE
# admin user to connecto to other hosts
export MRE_SSHUSER=$MRE_SSHUSER
# terminal program to open per connection
export MRE_TERMINAL=$MRE_TERMINAL
# filter hosts, set command
export MRE_TAGS="$MRE_TAGS"
# command to execute on target host(s)
export MRE_COMMAND="$MRE_COMMAND"
# colors
export COLOR_PRESET_cmd=( ${COLOR_PRESET_cmd[*]} )
export COLOR_PRESET_bastion=( ${COLOR_PRESET_bastion[*]} )
export COLOR_PRESET_lastexec=( ${COLOR_PRESET_lastexec[*]} )
export COLOR_PRESET_terminal=( ${COLOR_PRESET_terminal[*]} )
export COLOR_PRESET_prompt=( ${COLOR_PRESET_prompt[*]} )
export COLOR_PRESET_output=( ${COLOR_PRESET_output[*]} )
export COLOR_PRESET_ok=( ${COLOR_PRESET_ok[*]} )
export COLOR_PRESET_error=( ${COLOR_PRESET_error[*]} )
EOC
echo "saved:"
ls -l "${MRE_CONFIGFILE}"
}
# ---------- Execute commands
# get my sub processes in open terminal windows
function _getTerminals(){
ps -ef | grep ssh | grep "$MRE_COMMAND"
}
# get my sub processes in open terminal windows
# param string process list
function _isOpen(){
local _processes="${1}"
local _srv="${2}"
local _bst="${3}"
local _openterm
local _running
_running=
# for checking the bastion itself: remove processes of ssh tunnels
test -z "$_bst" && _processes=$( grep -v 'ssh -W %h:%p' <<< "$_processes" )
# gnome-terminal -e ... does not appear in process list
# _openterm=$( grep "@$_srv" <<< "$_processes" | grep "$MRE_TERMINAL")
_openterm=$( grep "@$_srv" <<< "$_processes")
if [ -n "$_openterm" ]; then
_openterm="TERM"
_running=$( grep "$_srv" <<< "$_processes" | grep -v "$MRE_TERMINAL" | grep -v "bash -c")
test -n "$_running" && _openterm+=" >> RUNNING"
# test -n "$_running" || _openterm+=" <done>"
echo "$(color.preset terminal )$_openterm$(color.reset)"
fi
}
# helper: get a numbered list of servers to connect to
function showServers(){
local _openterm
local _running
local _processes
_processes=$( _getTerminals )
# --- show bastion and last successful execution
echo "---- List of servers - $( date )"
echo
color.underline
echo " Server Bastion (20 chars) last success Running"
color.reset
_getServerJson "$MRE_TAGS" | jq -c | while read -r line
do
local _srv=$( echo "$line" | jq ".host" | tr -d '"')
local _bst=$( echo "$line" | jq ".bastion" | tr -d '"' | cut -c 1-20 | sed "s#^null\$##g")
if [ -f "$MRE_LOGFILE" ]; then
_lastexec=$( cat "$MRE_LOGFILE" | grep "|host=$_srv|rc=0|" | grep -F "$MRE_COMMAND" | tail -1 | cut -f 1 -d "|" | sed "s#$( date +%Y-%m-%d )##g")
else
_lastexec=
fi
_openterm=$( _isOpen "$_processes" "$_srv" "$_bst")
_srv=$( echo "$_srv" | cut -c 1-40 )
printf "%-40s %-20s %19s %s\n" "${_srv}" "${_bst}" "${_lastexec}" "${_openterm}"
done | nl
}
# helper: get the first matching server based on param which can be a
# number or part of a fqdn
# param integer|string number or part of servername to search
function getServerByNumber(){
local _searchtext="$1"
# generate a list with number and hostname
local _servers; _servers=$( _getServerJson "$MRE_TAGS" | jq ".host" -c | tr -d '"' | nl | awk '{ print $1 " " $2 }' )
(
# match a given number first ... and then in hostnames
grep "^${_searchtext} " <<< "$_servers" \
|| grep "${_searchtext}" <<< "$_servers"
) | head -1 | cut -f 2 -d " "
}
# run the command on a target system
# param integer|string number or part of servername to search
function run(){
local myserver="$1"
# --- deny multiple instances
_processes=$( _getTerminals )
_openterm=$( _isOpen "$_processes" "$myserver")
if [ -n "$_openterm" ]; then
# echo
# grep "$myserver" <<< "$_processes" | grep "$MRE_TERMINAL"
echo
echo "ABORT: $myserver has an open terminal already: $_openterm"
echo
sleep 0.5
return 1
fi
# --- create command line
bastion=$( _getHostProperty "$1" "bastion" )
remoteCmd=
remoteCmd+="ssh "
test -n "$bastion" && remoteCmd+='-o ProxyCommand="ssh -W %h:%p -q '${MRE_SSHUSER}'@'$bastion'" '
remoteCmd+="${MRE_SSHUSER}@${myserver} "
remoteCmd+=' "'$MRE_COMMAND'"'
command="
echo \$( date ) START $remoteCmd ...
echo _______________________________________________________________________________
echo
$remoteCmd
rc=\$?
echo _______________________________________________________________________________
echo \$( date +\"%Y-%m-%d %H:%M:%S\" )\|host=$myserver\|rc=\$rc\|command=$remoteCmd >> $MRE_LOGFILE
echo \$( date ) END $remoteCmd
test \$rc -eq 0 && ( echo -en \"\e[32m>>> OK >>> \" )
test \$rc -eq 0 || ( echo -en \"\e[31m>>> ERROR: rc =\$rc >>> \" )
echo -en \"\e[34m Press RETURN to exit\"
read
"
# --- open terminal
color.preset "output"
set -vx
$MRE_TERMINAL -e " bash -c '$command' " &
set +vx
color.reset
}
# loop over list of servers and run the command
# param string list of servers to handle (seperated by space)
function looper(){
for mysrv in $*
do
myserver=$( getServerByNumber "$mysrv")
run "$myserver"
shift
done
}
# ----------------------------------------------------------------------
# MAIN
# ----------------------------------------------------------------------
cat <<EOH
_______________________________________________________________________________
MULTI REMOTE EXECUTION WITH SSH ______
________________________________________________________________________/ v$_version
EOH
test ! -f "${MRE_CONFIGFILE}" && ( echo ; echo "INIT: Creating user config..."; _save )
test ! -f "./config/commands.txt" && ( echo ; echo "INIT: Creating default commands ./config/commands.txt ..."; cp "./config/commands.txt.dist" "./config/commands.txt" )
# --------- param check
if echo "$*" | grep "\-h" >/dev/null; then
echo "$USAGE"
exit 0
fi
# --------- check terminal program
if ! which $MRE_TERMINAL >/dev/null 2>&1; then
color.echo "error" "ERROR: the terminal [$MRE_TERMINAL] was not found."
echo
echo "In the [${MRE_CONFIGFILE}] set a line:"
color.echo "output" "MRE_TERMINAL=[your-terminal-program-here]"
echo
echo "Ensure that it supports the parameter -e to execute a command."
echo "These tools are known to be OK: konsole, gnome-terminal, xterm"
echo
exit 1
fi
if ! which jq >/dev/null 2>&1; then
color.echo "error" "ERROR: the JSON parser [jq] was not found."
echo
exit 1
fi
# --------- param check II
serverlist=
while [[ "$#" -gt 0 ]]; do case $1 in
-c|--command) _setCommand "$2"; exit 0;;
-h|--help) echo "$USAGE"; exit 0;;
-l|--list) showServers ; echo; exit 0;;
-t|--tag) _setTags "$2"; exit 0;;
-p|--profile) _setProfile "$2"; exit 0;;
*) if echo "$1" | grep "^-" >/dev/null; then
color.echo "error" "ERROR: Unknown parameter: $1"; echo "${USAGE}"; exit 1;
else
serverlist+="$1 "; shift
fi
;;
esac; done
# --------- Check JSON
_getProfile
if ! _getJSON | jq >/dev/null; then
color.echo "error" "ERROR: The JSON data seem to be corrupt - fix $MRE_JSONFILE"
exit 2
fi
if _getServerJson 2>& 1| grep -F "error (at"; then
color.echo "error" "ERROR: The JSON data seem to be corrupt - fix $MRE_JSONFILE"
exit 2
fi
if echo "$serverlist" | grep "ALL" >/dev/null; then
echo "INFO: Keyword 'ALL' detected ... looping over all servers"
serverlist=$( _getServerlist | cut -f 1 -d " " )
fi
if [ -n "$serverlist" ]; then
# execute the current command on given server(s)
looper "$serverlist"
echo "DONE"
exit 0
fi
# --------- interactive mode
echo
while true; do
showServers
echo
echo "Special commands:"
echo -n " ':c' change command "; color.echo "cmd" "$MRE_COMMAND"
echo -n " ':t' change tags "; color.echo "cmd" "$MRE_TAGS"
echo -n " ':p' change profile "; color.echo "cmd" "$MRE_PROFILE"
echo "Enter numbers or parts of servernames. Use space as divider."
echo "Just press return to update server infos amd running status."
color.print "prompt" "> "
# read -t 60 -r srvnumber
read -r srvnumber
if [ "$srvnumber" = ":c" ]; then
_setCommand
elif [ "$srvnumber" = ":t" ]; then
_setTags
elif [ "$srvnumber" = ":p" ]; then
_setProfile
else
# execute the current command on given server(s)
looper "$srvnumber"
fi
echo
done
# ----------------------------------------------------------------------
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment