#!/bin/bash # Copyright (c) Dwarf Technologies s.r.o. 2024-2025 fixname="dt_rmm_cmd" sname="dwarfg" lname="Dwarfguard" call_prefix="${sname}_" version="1.6" debug="/tmp/mydebug" cmdstrings=("help" "status" "stop" "start" "restart" "reload" "stats_proc" "stats_app" "logview" "loggrep" "logtail" "logexport" "backup" "restore" "grafint" "deplist" "depstatus" "trafcnt" "user" "inst_pyext") cmdqhelp=( "[command] ... list all commands with quickhelp or provide longer help on one command" "... show runtime status of $lname service (${sname}d)" "... stops $lname service" "... starts $lname service" "... restarts $lname service" "... reloads dwarflib configuration" "... shows systemd-gathered information (if under systemd)" "... shows app stats like # of devices, license, last restart" "... opens daemon log in ${EDITOR:-view}" "[level] ... displays logs messages. CRIT|ERR|*WARN*|NOTE" "[level] ... continually shows logs messages. CRIT|ERR|*WARN*|NOTE" "[fname] ... create log archive. may add suffix: fname[.tgz]" "... run backup NOW (downtime!) via ${sname}_upgrade.sh" "... interactive mode: ${sname}_upgrade.sh (backup+restore)" "... enable grafana integration and push (re-push) dashboard" "... show all $lname deployments found on server" "... show service status for each deployment" "... count traffic for device/monitoring group over period of time. Call with -h for help." "[new_username] [password] ... add UI user. Both username and password must be non-empty." "[cache_path] ... install or upgrade Python extension via PIP - SSHWifty and PureSNMP library") c_status=1 c_stop=2 c_start=3 c_restart=4 c_reload=5 c_stats_proc=6 c_stats_app=7 c_logview=8 c_loggrep=9 c_logtail=10 c_logexport=11 c_backup=12 c_restore=13 c_grafint=14 c_deplist=15 c_depstatus=16 c_trafcnt=17 c_user=18 c_inst_pyext=19 declare -a depnums declare -a depnames declare -a depnotes curl_json_out= curl_http_res=0 gr_table_accesslist="grafana_table_accesslist.txt" gr_datasource_sql_uid="grafana_datasource_sql_uid.txt" gr_datasource_influx_uid="grafana_datasource_influx_uid.txt" gr_dashboard_sql_uid="grafana_dashboard_sql_uid.txt" gr_dashboard_influx_uid="grafana_dashboard_influx_uid.txt" gr_dashboard_sql_template="grafana_dashboard_sql_template.json" gr_dashboard_influx_template="grafana_dashboard_influx_template.json" gr_tmpdashb="/tmp/dwarfg_grafana_dashboard.txt" gr_tmpapaconf="/tmp/dwarfg_tmpapaconf" list_commands() { mute="" [ $# -gt 0 ] && mute="." [ -z "$mute" ] && echo -e "Supported commands: (call \"${sname}_command\" or \"$fixname command\")\n" for i in "${!cmdstrings[@]}"; do if [ -n "$mute" ] ; then echo "${cmdstrings[$i]}" else echo -e "${cmdstrings[$i]} ${cmdqhelp[$i]}" fi done echo } help() { [[ $# -eq 1 && "$1" = "mute" ]] && { list_commands "."; return 0; } echo -e "\n$lname command-line interface version $version" local pat="$1" if [ "$1" = "help" ] ; then pat="$2" fi case "$pat" in "${cmdstrings[$c_stop]}"|"${cmdstrings[$c_start]}"|"${cmdstrings[$c_restart]}") echo -e "\nstop|start|restart ... controls app runtime status via ${sname}_ctl.sh" echo -e "\t$lname may be integrated via systemd. CTL script uses systemd for actions then." ;; "${cmdstrings[$c_status]}"|"${cmdstrings[$c_stats_proc]}") echo -e "\nstatus ... displays process status using ${sname}_ctl.sh script" echo -e "stats_proc ... displays systemd-gathered information (if integrated in systemd)" ;; "${cmdstrings[$c_reload]}") echo -e "\nreload ... reloads dwarflib configuration (usually dwarflib_cfg.txt)" ;; "${cmdstrings[$c_stats_app]}") echo -e "\nstats_app ... display app status - # of devices, license state, last restart etc." ;; "${cmdstrings[$c_logview]}"|"${cmdstrings[$c_loggrep]}"|"${cmdstrings[$c_logtail]}"|"${cmdstrings[$c_logexport]}") echo -e "\nlogview ... opens app log in ${EDITOR:-view}" echo -e "loggrep [] ... shows and more serious logs. Note that if you have log throttling enabled (standard on production) and troubleshooting, you want to disable it first." echo -e "\t can be any of CRIT|ERR|WARN|NOTE|INFO. Default is WARN." echo -e "logtail [] ... like loggrep, but continuous (stop with CTRL-C)." echo -e "logexport [filename] ... calls grab_logs.sh script to produce log archive" echo -e "\tIf filename given, archive name is filename[.tgz] - suffix added if missing" ;; "${cmdstrings[$c_backup]}"|"${cmdstrings[$c_restore]}") echo -e "\nbackup ... performs backup NOW (incurs downtime!) via ${sname}_upgrade.sh" echo -e "restore ... calls interactive mode of ${sname}_upgrade.sh (backup+restore)" ;; "${cmdstrings[$c_grafint]}") echo -e "\ngrafint ... trigger grafana installation and configuration (if not installed)" echo -e " ... it also does push the default dashboard into grafana" echo -e " ... usable to re-push the dashboard if user messes the dashboard up" ;; "${cmdstrings[$c_deplist]}") echo -e "\ndeplist ... shows all ${sname} deployments found on server" ;; "${cmdstrings[$c_depstatus]}") echo -e "\ndepstatus ... shows status of each of the ${sname} deployements on server" ;; "${cmdstrings[$c_trafcnt]}") print_traffic_help ;; "${cmdstrings[$c_user]}") echo -e "\n${cmdstrings[$c_user]} [new_username] [password] ... adds *inactive* UI user with role ADMIN." echo -e " ... both username and password must be non-empty" echo -e " ... user can be activated via UI by the admin user (role SUPER_ADMIN)" ;; "${cmdstrings[$c_inst_pyext]}") echo -e "\n${cmdstrings[$c_inst_pyext]} [--download] [cache_path] ... install/upgrade or download following Python-based SW:" echo -e " SSHWifty ... to allow web-terminal based access to SSH tunnel." echo -e " PureSNMP library ... to enable SNMP Gateway to run." echo -e " --download [cache_path] ... download all reuqired packages to directory cache_path so that you can copy it to another server and install them on your production server. Use dirname without spaces." echo -e " cache_path ... install/upgrade all packages from the directory cache_path. User dirname without spaces." echo -e " You can also download the packages manually using 'pip3 install --download ', Repeat that process if you find any missing dependecies. Use dirname without spaces." ;; *) echo -e "\nsyntax1: $fixname [command options...] [--dn ] [--dd ]" echo -e "syntax2: ${call_prefix} [command options...] [--dn ] [--dd ]" echo -e "\tcommand options are noted below in the command list" echo -e "\t--dn number ... select deployment by its deployment (not order!) number" echo -e "\t--dd string ... select deployment by its deployment name\n" list_commands ;; esac } f_cmdlist() { list_commands "" } f_status() { echo -n "$1: " "/opt/$1/${sname}_ctl.sh" --status } f_stop() { f_status "$1" "/opt/$1/${sname}_ctl.sh" --stop [ -z "$2" ] && f_status "$1" } f_start() { [ -z "$2" ] && f_status "$1" "/opt/$1/${sname}_ctl.sh" --start f_status "$1" } f_restart() { f_stop "$1" "restart" f_start "$1" "restart" } f_reload() { "/opt/$1/${sname}_ctl.sh" --reload-cfg } f_stats_proc() { systemctl status "$depname" } f_stats_app() { lictxt="" dbname="$1" dbname="${dbname//[.-]/_}" devices=$(mysql "$dbname" -Nse "SELECT COUNT(devid) FROM devices") prodname="$(grep "^NAME" "/opt/$1/base_defs" | sed "s/.*=\"\\([^\"]*\\)\"/\\1/")" suplink="$(mysql "$dbname" -Nse "SELECT strval from product_conf where name='SupportLink'")" myoname="$(pwd)" cd "/opt/$1" || return 1 if out=$("./licman" -c) ; then lictxt="License is valid. Use \"cd /opt/$1 && ./licman -d\" to see license dump." else lictxt="ERROR: licensing problem. Use \"cd /opt/$1 && ./licman -c\" to know more." fi cd "$myoname" || return 1 "/opt/$1/${sname}d" -v || { echo -e "\nERROR: \"/opt/$1/${sname}d\" -v returned error. Your installation is CORRUPTED. Restore from backup.\n" >&2 exit 1 } echo -n "$prodname status via CTL script: " "/opt/$1/${sname}_ctl.sh" --status dpid=$("/opt/$1/${sname}_ctl.sh" --pid) seats=$(echo "$out" | grep "# of device seats" | sed "s/.*: //") echo -n "$prodname is licensed for use by: " echo "$out" | grep "^Licensee:" | sed "s/.*: //" echo "$lictxt" echo "Devices registered/seats licensed: $devices/$seats" echo -n "License valid till " echo "$out" | grep "Valid till" | sed "s/.*: //" echo "Your support link (empty if self-support): $suplink" echo "Basic product variables values:" grep -E "^VERSION|^DOMAIN|^EXTERNURL|^SERVID|^SERV_TUNSSH_PORT|^USE_SSL|^DWARFG_PORT|^LISTENER_THREADS" "/opt/$1/base_defs" | sed "s/^/\t/" echo "Overrides defined in /opt/$1/${sname}.ini:" grep -E "^SERV_TUNSSH_PORT|^LISTENER_THREADS" "/opt/$1/${sname}.ini" | sed "s/^/\t/" [ -n "$dpid" ] && { echo "Daemon threads breakdown:" ps -p "$dpid" -T -o pid,rss,comm,cmd,pcpu,stime,etimes,times } echo "Service status via systemd:" sstat=$(systemctl status "$1" 2>&1) echo "$sstat" } f_logview() { editor=${EDITOR:-view} "$editor" "/srv/$1/logs/log_$sname.txt" } f_loggrep() { local cmd="cat" [ -n "$3" ] && cmd="tail -Fc +1" case "$2" in "CRIT") pat="^[^/]*/CRIT|INIT" ;; "ERR") pat="^[^/]*/ERR|^[^/]*/CRIT|INIT" ;; "NOTE") pat="^[^/]*/NOTE|^[^/]*/WARN|^[^/]*/ERR|^[^/]*/CRIT|INIT" ;; "INFO") pat="^[^/]*/INFO|^[^/]*/NOTE|^[^/]*/WARN|^[^/]*/ERR|^[^/]*/CRIT|INIT" ;; *) pat="^[^/]*/WARN|^[^/]*/ERR|^[^/]*/CRIT|INIT" ;; esac logf="/srv/$1/logs/log_${sname}.txt" [ ! -s "$logf" ] && echo "Logfile \"$logf\" not found or empty!" >&2 && return 1 $cmd "$logf" | grep -E "$pat" } f_logtail() { f_loggrep "$@" "string" } f_logexport() { file="$("/opt/$1/grab_logs.sh" | tail -1)" if [[ -z "$file" || ! -s "$file" ]] ; then echo "Failed to produce logs. Try running /opt/$1/grab_logs.sh manually." >&2 else if [ -n "$2" ] ; then if echo "$2" | grep -q "\.tgz$" ; then target_file="$2" else target_file="$2.tgz" fi cp "$file" "$target_file" echo "Copied $file to $target_file" else echo "Output filepath is: $file" fi fi } f_backup() { "/opt/$1/${sname}_upgrade.sh" backup } f_restore() { "/opt/$1/${sname}_upgrade.sh" } f_user() { [ $# -ne 3 ] && { echo "ERROR: Both username and password must be provided." >&2; return 1; } [ -z "$2" ] && { echo "ERROR: Username must be provided and non-empty." >&2; return 1; } [ -z "$3" ] && { echo "ERROR: Password must be provided and non-empty." >&2; return 1; } . "/opt/$1/base_defs" || { echo "ERROR: Unable to source base deployment defs." >&2; return 1; } opwd="$(pwd)" cd "$GUIDIR" || return 1 sudo -u "$WWWUSER" "/usr/bin/php" bin/console user:create "$2" "$3" ROLE_ADMIN -a false cd "$opwd" } f_deplist() { local ctr=0 if [ ${#depnums[@]} -eq 0 ] ; then echo "No $lname deployments found!" >&2 else if [[ $# -eq 2 && "$2" = "--getfirst" ]]; then echo "${depnames[0]}" else echo "Found ${#depnums[@]} deployments. Order and dep numbers and domains:" for i in "${!depnums[@]}" ; do ctr=$((ctr+1)) echo -en "$ctr. \t${depnums[$i]}: ${depnames[$i]}" if [ -n "${depnotes[$i]}" ] ; then echo -n "... ${depnotes[$i]}" fi if [[ $# -eq 1 && "$1" = "showstatus" ]] ; then echo -en "\n\t\tStatus: " "/opt/${depnames[$i]}/${sname}_ctl.sh" --status else echo fi done fi fi } f_depstatus() { f_deplist "showstatus" } f_json_patmatch() { [ $# -eq 2 ] || return 1 pattern="$1" json="$2" echo "$json" | tr ',' '\n' | grep "\"$pattern\":" | head -1 | sed "s/.*\"$pattern\"[ ]*:[ ]*\"\([^\"]*\).*/\1/" } debug_grapi() { [ -n "$debug" ] && echo "Grafana API: $*" >>$debug return 0 } f_grafapi() { debug_grapi "Grafapi call: $*" [ $# -ne 5 ] && { echo "grafana api call and parse function - need exactly 5 parameters" >&2 return 1 } request="$1" json_in="$2" pattern="$3" curladd="$4" http_ok="$5" [ -z "$http_ok" ] && http_ok=0 curl_json_out= api_url="http://admin:$GRADMPASS@localhost:3000/$request" semi_fail="" if [ -n "$json_in" ] ; then if (echo "$json_in" | grep "^file:" >/dev/null) ; then realfile="$(echo "$json_in" | sed "s/^file://")" [ -f "$realfile" ] || { echo "JSON file to post ($realfile) is missing." >&2 return 2 } debug_grapi "curl --no-progress-meter --json \"@$realfile\" -w \"\nHTTP_CODE=%{http_code}\n\" $curladd \"$api_url\"" my_out=$(curl --no-progress-meter --json "@$realfile" -w "\nHTTP_CODE=%{http_code}\n" $curladd "$api_url") || { echo "Grafana API call failed to connect to Grafana (API URL: \"$api_url\")" >&2 return 3 } else debug_grapi "\$(echo -e \"$json_in\" | curl --no-progress-meter --json @- -w \"\nHTTP_CODE=%{http_code}\n\" $curladd \"$api_url\"" my_out=$(echo -e "$json_in" | curl --no-progress-meter --json @- -w "\nHTTP_CODE=%{http_code}\n" $curladd "$api_url") || { echo "Grafana API call failed to connect to Grafana (API URL: \"$api_url\")" >&2 return 4 } fi else debug_grapi "curl --no-progress-meter $curladd -w \"\nHTTP_CODE=%{http_code}\n\" \"$api_url\"" my_out=$(curl --no-progress-meter $curladd -w "\nHTTP_CODE=%{http_code}\n" "$api_url") || { echo "Grafana API call failed to connect to Grafana (API URL: \"$api_url\")" >&2 return 5 } fi curl_http_res=$(echo -e "$my_out" | tail -1 | sed "s/.*HTTP_CODE=\([0-9]*\)$/\1/") debug_grapi "curl_http_res is '$curl_http_res'" if [[ -z "$curl_http_res" || "200" != "$curl_http_res" ]] ; then [ "$curl_http_res" = "$http_ok" ] || { echo -e "Grafana API call failed with \"$curl_http_res\"" >&2 json_err_message=$(f_json_patmatch "message" "$my_out") && { if [ -n "$json_err_message" ] ; then echo -e "Error message was: \"$json_err_message\"" >&2 fi } return 6 } semi_fail="yes" fi curl_json_out=$(echo "$my_out" | sed \$d) curl_json_res= if [[ -z "$semi_fail" && -n "$pattern" ]] ; then curl_json_res=$(f_json_patmatch "$pattern" "$curl_json_out") || echo "Error when matching pattern in returned json." >&2 [ -n "$curl_json_res" ] || { echo "Grafana API call failed to return expected pattern." >&2 return 7 } fi return 0 } f_getuid_from_file() { [[ $# -eq 1 && -n "$1" ]] || return 1 [ -f "$1" ] && { candy=$(cat "$1") if [[ -n "$candy" && "$candy" = "${candy//[^-0-9a-z]}" ]] ; then echo "$candy" else echo 0 echo "NOTE: no or garbled number in $1" >&2 fi } return } python_venv_swinst() { cmd="$1" packages="webssh puresnmp" shift extra="$*" [ -z "$PYTHON_VENV" ] && { echo "Python virtual environment is not defined!" >&2 return 1 } echo -n "Entering python virtual environment at $PYTHON_VENV..." source "$PYTHON_VENV/bin/activate" || { echo "FAILURE"; return 1; } echo " success"; for li in $packages; do case "$cmd" in "download") echo -n "Downloading $li..." pip3 download $extra "$li" || { echo "FAILURE"; return 1; } ;; "install") if pip3 list --short 2>/dev/null | grep "$li"; then echo -n "Upgrading $li..." pip3 upgrade $extra "$li" || { echo "FAILURE"; return 1; } echo " success"; else echo -n "Installing $li..." pip3 install $extra "$li" || { echo "FAILURE"; return 1; } echo " success"; fi ;; *) echo "Install command $cmd not understood" >&2 return 1 esac done } f_inst_pyext() { extra= MYID=$(id -u) cmd="install" [ "$MYID" -eq 0 ] || { echo "Python extra components installation requires root priviledges." >&2 return 1 } if [ $# -ge 2 ]; then if [ "$2" = "--download" ] ; then cmd="download" if [ $# -ge 3 ] ; then extra="-d \"$3\"" else extra="-d ${sname}_download" fi elif [ -d "$2" ]; then extra="--no-index --find-links $2" else echo "Provided cache directory ($2) is not a directory." return 1 fi fi varname="DWARFG_UI_PYEXT_AVAIL" . "/opt/$1/base_defs" || { echo "Failed to read basic definitions from the deployment"; return 1; } . "/opt/$1/deploy_funcs.sh" || { echo "Failed to read deploy functions"; return 1; } grep "$varname" "$GUIDIR/.env" >/dev/null || { echo "GUI variable managing Python extension SW ($varname) is missing in Environment file - your $lname deployment is DAMAGED!" >&2 return 1 } status=$(grep "$varname" "$GUIDIR/.env" | head -1 | grep "^$varname='[^']*'$" | sed "s/.*'\([^']*\)'/\1/") PVSWI=$(declare -f python_venv_swinst) export PYTHON_VENV sudo -E -H -u "$APPUSER" bash -c "$PVSWI; python_venv_swinst $cmd $extra" || { echo "Failed to dwonload/install/update additional Python SW components." >&2 return 1 } # if successful update env if it was empty [[ -n "$status" || "download" = "$cmd" ]] || { sed -i "s#^$varname=.*#$varname='Installed'#" "$GUIDIR/.env" if (run_snmpgw_svc) ; then echo "$lname SNMP Gateway has been enabled and started." else echo "Failed to enable+start $lname SNMP Gateway." fi } } f_graf_new_datasource() { [ 10 -ne $# ] && echo "Incorrect number of parameteres to ${FUNCNAME[0]}">&2 && return 1 curl_json_res= f_grafapi "$1" "$2" "$3" "$4" "$5" [ "$curl_http_res" = "$5" ] && { # datasource exists already (with the same name) - get the uid by datasource name curl_json_res="" f_grafapi "$6" "$7" "$8" "$9" "${10}" } [ -z "$curl_json_res" ] && echo "Failed to get datasource uid (is empty)" >&2 && return 1 [ "$curl_json_res" != "${curl_json_res//[^0-9a-z]}" ] && echo "Invalid datasource UID returned from Grafana ($curl_json_res)" && return 1 echo "$curl_json_res" return 0 } f_graf_push_dashboard() { # parameters: # 1 - first datasource text to be replaced in template # 2 - first datasource uid # 3 - second datasource text to be replaced in template # 4 - second datasource uid # 5 - dashboard template file name # 6 - dashboard textual name (inside template) # 7 - filename to export the resulting dashboard UID to # output - should output the UID (or it could be re-read from the file ???) sed "s/$1/$2/g" "$DIR/$5" | sed "s/$3/$4/g" >"$gr_tmpdashb" || { echo "Unable to write to $gr_tmpdashb" >&2 && return 1; } curl_json_res= f_grafapi "api/dashboards/db" "file:$gr_tmpdashb" "uid" "" 412 || { echo "Unable to add new Grafana dashboard ($?), integration failed." >&2 && return 1; } [ "$curl_http_res" = "412" ] && { echo "Conflicting dashboard detected. Attempting to detect uid and remove." >&2 f_grafapi "api/search?type=dash-db" "" "" "" "" || { echo "Failed to get dashboard list from Grafana. Remove the '$6' dashboard manually and retry the integration." >&2 && return 1; } target_uid="" readarray -t dashbs_array < <(jq -c '.[]' <<< "$curl_json_out") for item in "${dashbs_array[@]}" ; do json_type=$(jq --raw-output '.type' <<< "$item") json_title=$(jq --raw-output '.title' <<< "$item") json_uid=$(jq --raw-output '.uid' <<< "$item") [[ "$json_type" = "dash-db" && "$json_title" = "$6" ]] && { target_uid=$json_uid } done [ -z "$target_uid" ] && { echo "Unable to detect dashboard uid. Please remove the '$6' dashboard manually and retry the integration." >&2 && return 1; } f_grafapi "api/dashboards/uid/$target_uid" "" "" "-X DELETE" "" || { echo "Unable to remove '$6' with uid '$target_uid'. Please remove manually and retry integration." >&2 && return 1; } f_grafapi "api/dashboards/db" "file:$gr_tmpdashb" "uid" "" "" || { echo "Unable to add new Grafana dashboard ($6), integration failed." >&2 && return 1; } } [ -n "$curl_json_res" ] || { echo "Failed to get dashboard uid (is empty). Integration failed." >&2 && return 1 ; } [ "$curl_json_res" != "${curl_json_res//[^-0-9a-z]}" ] && { echo "Invalid dashboard UID returned from Grafana ($curl_json_res). Integration failed." >&2 return 1 } dbuid=$curl_json_res echo "$dbuid" >"$DIR/$7" } f_grafint() { DIR="/opt/$1" grconf="/etc/grafana/grafana.ini" dbname="$1" gruser="dwarf" GRDBPASS=${GRDBPASS:-} GRADMPASS=${GRADMPASS:-} . "$DIR/base_defs" || return 1 . "$DIR/deploy_funcs.sh" || return 1 read_defs || return 1 "$DIR"/machine_install.sh grafana || return 1 dbname="${dbname//[.-]/_}" mydomain="$DOMAIN" [ -z "$mydomain" ] && { echo "Domain is empty, using localhost domain for Grafana... " mydomain="localhost" } echo "Waiting for grafana to finish background setup... (30 seconds)" && sleep 30 if [ -z "$GRADMPASS" ] ; then new_gradmpass || return 1 read_defs || return 1 echo "Setting grafana admin password to \"$GRADMPASS\"" grafana-cli admin reset-admin-password "$GRADMPASS" || return 1 else echo "Grafana admin password left at \"$GRADMPASS\"" fi if [ -z "$GRDBPASS" ] ; then new_grdbpass || return 1 read_defs || return 1 echo "Setting grafana DB user password to \"$GRDBPASS\"" else echo "Grafana DB user password left at \"$GRDBPASS\"" fi [[ -z "$GRDBPASS" || -z "$GRADMPASS" ]] && return 1 echo "Adding grafana user $gruser with password dgpwd ..." f_grafapi "api/admin/users" "{ \"name\":\"$gruser\", \"login\":\"$gruser\", \"password\": \"dgpwd\" }" "" "" 412 [ "$curl_http_res" = "412" ] && echo " ... user exists already." sed -i "s#^DWARFG_UI_GRAFANA_USER=.*#DWARFG_UI_GRAFANA_USER='$gruser'#" "$GUIDIR/.env" || { echo "Failed to replace Grafana user in GUI config file ($GUIDIR/.env)" >&2 return 1 } sed -i "s#^DWARFG_UI_GRAFANA_PASS=.*#DWARFG_UI_GRAFANA_PASS='dgpwd'#" "$GUIDIR/.env" || { echo "Failed to replace Grafana password in GUI config file ($GUIDIR/.env)" >&2 return 1 } echo "Adding DB user for Grafana ..." mysql "$dbname" <&2 return 1 } [ -f "$DIR/$gr_table_accesslist" ] || { echo "Deployment is missing grafana table list." >&2 return 1 } echo "Allowing Grafana DB user to access selected tables ..." for table in $(<"$DIR"/grafana_table_accesslist.txt); do echo "GRANT SELECT ON $dbname.${table} TO 'dg_grafana'@'localhost';" done | mysql "$dbname" dsuid_sql=$(f_getuid_from_file "$DIR/$gr_datasource_sql_uid") [ -n "$dsuid_sql" ] && echo "Using grafana SQL datasource UID: $dsuid_sql. If operation fails, you can delete $DIR/$gr_datasource_sql_uid and retry - new datasource will be defined." [ -z "$dsuid_sql" ] && { echo "Creating new SQL datasource ..." if dsuid_sql=$(f_graf_new_datasource "api/datasources" "{ \"name\":\"${dbname}_sql\", \"access\":\"proxy\", \"type\":\"mysql\", \"host\":\"localhost:3306\", \"database\":\"$dbname\", \"user\":\"dg_grafana\", \"secureJsonData\": { \"password\": \"$GRDBPASS\" } }" "uid" "" 409 "api/datasources/name/${dbname}_sql" "" "uid" "" "") ; then echo "Saving new datasource into $DIR/$gr_datasource_sql_uid ..." echo "$dsuid_sql" >"$DIR/$gr_datasource_sql_uid" else echo "Failed to create SQL datasource..." return 1 fi } dsuid_influx=$(f_getuid_from_file "$DIR/$gr_datasource_influx_uid") [ -n "$dsuid_influx" ] && echo "Using grafana InfluxDB datasource UID: $dsuid_influx. If operation fails, you can delete $DIR/$gr_datasource_influx_uid and retry - new datasource will be defined." [ -z "$dsuid_influx" ] && { echo "Creating new InfluxDB datasource ..." if dsuid_influx=$(f_graf_new_datasource "api/datasources" "{ \"name\":\"${dbname}_influx\", \"access\":\"proxy\", \"type\":\"influxdb\", \"url\":\"http://localhost:8086\", \"jsonData\": { \"dbName\": \"${dbname}\", \"pdcInjected\": false }, \"user\":\"\" }" "uid" "" 409 "api/datasources/name/${dbname}_influx" "" "uid" "" "") ; then echo "Saving new datasource into $DIR/$gr_datasource_influx_uid ..." echo "$dsuid_influx" >"$DIR/$gr_datasource_influx_uid" else echo "Failed to create InfluxDB datasource..." return 1 fi } dbuid_sql=$(f_getuid_from_file "$DIR/$gr_dashboard_sql_uid") [ -n "$dbuid_sql" ] && { echo "Deleting grafana SQL dashboard UID: $dbuid_sql ..." f_grafapi "api/dashboards/uid/$dbuid_sql" "" "" "-X DELETE" "" } echo "Pushing SQL dashboard into Grafana ..." if f_graf_push_dashboard "DATASOURCEUID_SQL" "$dsuid_sql" "DATASOURCEUID_INFLUX" "$dsuid_influx" "$gr_dashboard_sql_template" "Traffic counter" "$gr_dashboard_sql_uid"; then dbuid_sql=$(f_getuid_from_file "$DIR/$gr_dashboard_sql_uid") else echo "Unable to push dashboard into Grafana." >&2 return 1 fi dbuid_influx=$(f_getuid_from_file "$DIR/$gr_dashboard_influx_uid") [ -n "$dbuid_influx" ] && { echo "Deleting grafana InfluxDB dashboard UID: $dbuid_influx ..." f_grafapi "api/dashboards/uid/$dbuid_influx" "" "" "-X DELETE" "" } echo "Pushing InfluxDB dashboard into Grafana ..." if f_graf_push_dashboard "DATASOURCEUID_SQL" "$dsuid_sql" "DATASOURCEUID_INFLUX" "$dsuid_influx" "$gr_dashboard_influx_template" "Traffic counter" "$gr_dashboard_influx_uid"; then dbuid_influx=$(f_getuid_from_file "$DIR/$gr_dashboard_influx_uid") else echo "Unable to push dashboard into Grafana." >&2 return 1 fi randlink="$(get_randpass 10)" [[ -z "$randlink" || ! -f "$BINDIR/$DWARFG_APACONF" ]] && { echo "Unable to configure Apache proxy to grafana for $dbname" >&2 return 1 } echo "Updating Apache2 webserver config so that it serves Grafana under a sub-URL ..." # Apache config: # 0. copy the config to temporary file # 1. remove old grafana proxy lines if there cat "$BINDIR/$DWARFG_APACONF" | grep -v "^[^#]*ProxyPassMatch.*grafana" | grep -v "^[^#]*ProxyPassReverse.*grafana" >"$gr_tmpapaconf" # 2. grab the relevant template lines (2) line_prox="$(grep "#ProxyPassMatch.*grafana" $gr_tmpapaconf | head -1)" line_rev="$(grep "#ProxyPassReverse.*grafana" $gr_tmpapaconf | head -1)" [[ -z "$line_prox" || -z "$line_rev" ]] && { echo "Apache config file misses template grafana lines as comments, unable to generate Apache grafana link." >&2 rm "$gr_tmpapaconf" return 1 } line_prox="$(echo "$line_prox" | sed "s/#ProxyPassMatch/ProxyPassMatch/" | sed "s/GHASH/$randlink/")" line_rev="$(echo "$line_rev" | sed "s/#ProxyPassReverse/ProxyPassReverse/" | sed "s/GHASH/$randlink/")" # 3. add the substituded lines before the template lines sed -i "\;#ProxyPassMatch.*grafana;i $line_prox" "$gr_tmpapaconf" #" sed -i "\;#ProxyPassReverse.*grafana;i $line_rev" "$gr_tmpapaconf" #" # 4. move the resulting config over the original one mv "$gr_tmpapaconf" "$BINDIR/$DWARFG_APACONF" || { echo "Failed to move the new Apache2 config in place ($gr_tmpapaconf -> $BINDIR/$DWARFG_APACONF)" >&2 return 1 } # replace the two variables in place echo "Updating GUI configuration file with Grafana URL (sql: $dbuid_sql; influx: $dbuid_influx) ..." sed -i "s#^DWARFG_UI_GRAFANA_REDIR=.*#DWARFG_UI_GRAFANA_REDIR='grafana_$randlink/d/$dbuid_sql'#" "$GUIDIR/.env" || { echo "Failed to replace Grafana SQL dashboard URL in GUI config file ($GUIDIR/.env)" >&2 return 1 } sed -i "s#^DWARFG_UI_GRAFANA_REDIR_INFLUX=.*#DWARFG_UI_GRAFANA_REDIR_INFLUX='grafana_$randlink/d/$dbuid_influx'#" "$GUIDIR/.env" || { echo "Failed to replace Grafana InfluxDB dashboard URL in GUI config file ($GUIDIR/.env)" >&2 return 1 } echo "Updating Grafana server configuration with currect URL ..." # Grafana configuration [ ! -f "$grconf" ] && { echo "Cannot locate Grafana configuration file ($grconf)." >&2 return 1 } target_cfgline="root_url = https://$mydomain/grafana_$randlink/" if grep "^root_url *=" $grconf >/dev/null; then sed -i "0,/^root_url *=/s#^root_url *=.*#${target_cfgline}#" "$grconf" || { echo "Failed to update Grafana configuration file ($grconf)." >&2 return 1 } else if grep "^;root_url *=" $grconf >/dev/null; then sed -i "0,/^;root_url *=/s#^;root_url *=.*#${target_cfgline}#" "$grconf" || { echo "Failed to update Grafana configuration file [2] ($grconf)." >&2 return 1 } else echo "Failed to locate proper place in grafana config file to put the root_url directive into." >&2 return 1 fi fi echo "Restarting Grafana and Apache2 for the changes to take effect ..." systemctl restart grafana-server systemctl reload apache2 } count_traffic() { [ $# -eq 8 ] || return 1 local dbname="$1" local groupid="$2" local devid="$3" local traftype="$4" local trafperiod="$5" local time_start="$6" local time_end="$7" local csv="$8" local mysqlopt="" if [ -n "$groupid" ] ; then [ "$groupid" != "${groupid//[^0-9]}" ] && { echo "Monitoring group ID ('$groupid') is not a number." >&2 return 1 } selection="n.id_device IN (SELECT id_device FROM monitoring_group_device_map WHERE mongrp_id = $groupid)" else [[ -z "$devid" || "$devid" != "${devid//[^0-9]}" || 0 -gt $devid || 65535 -lt $devid ]] && { echo "Device ID is either empty, or ivalid (is '$devid', must be number between 0 and 65535)." >&2 return 1 } selection="n.id_device = $devid" fi [[ "$traftype" != "${traftype//[^0-9]}" || $traftype -gt 2 || $traftype -lt 0 ]] && { echo "Unsupported traffic type - must be between 0 and 2 (inclusive, is: '$traftype', '${traftype//[^0-9]}')" >&2 return 1 } [[ "$trafperiod" != "${trafperiod//[^0-9]}" || $trafperiod -gt 2 || $trafperiod -lt 0 ]] && { echo "Unsupported traffic period - must be between 0 and 2 (inclusive, is: '$trafperiod')" >&2 return 1 } [ -n "$csv" ] && mysqlopt="--batch --raw" sql_query_base_1="sum(t.rx_bytes) AS Received, sum(t.tx_bytes) AS Transmitted FROM device_netdevs n JOIN" sql_query_base_2="t ON n.id = t.netdev where t.time >= '$time_start' AND t.time <= '$time_end' AND $selection" # following based on trfaperiod - daily, monthly, totals sql_head=("SELECT t.time AS Day," "SELECT t.time AS Month," "SELECT") sql_table=("netdevs_daily_traffic" "netdevs_monthly_traffic" "netdevs_daily_traffic") sql_groupby=("GROUP BY t.time" "GROUP BY t.time" "") # now based on traftype - all, cellular, non-cellular sql_traftype=("" "AND n.is_cellular = 1" "AND n.is_cellular = 0") sql_preface=("All traffic" "Cellullar Traffic" "Non-cellullar traffic") sql="${sql_head[$trafperiod]} $sql_query_base_1 ${sql_table[$trafperiod]} $sql_query_base_2 ${sql_traftype[$traftype]} ${sql_groupby[$trafperiod]}" echo "${sql_preface[$traftype]}" if [ -n "$csv" ] ; then mysql "$dbname" $mysqlopt -e "$sql" | tr '\t' ',' else mysql "$dbname" $mysqlopt -e "$sql" fi return 0 } print_traffic_help() { echo "$0 syntax and options:" echo " $0 [-a]|-c|-n -g|-d [-D]|-M|-T -s -e [-C]" echo " -a ... count All traffic types together (default)" echo " -c ... count only Cellular traffic" echo " -n ... count only Non-cellular traffic" echo " -g ... count traffic for monitoring Group. (-g \"All devices\")" echo " -d ... count traffic for Device. (-d ABAB)" echo " -D ... display Daily counters (default)" echo " -M ... display Monthly totals over a time period" echo " -T ... display Total traffic for a time period" echo " -s ... specify start date YYYY-MM-DD (-s 2024-01-01)" echo " -e ... specify end date YYYY-MM-DD (-s 2024-02-01)" echo " -C ... produce CSV file. You may want to redirect to a file. (>filename.csv)" echo echo " Examples:" echo " $0 -g \"All devices\" -s 2024-10-01 -e 2024-10-31" echo " ... Show daily totals for all devices during October" echo " $0 -c -d ABAB -M -s 2024-01-01 -e 2024-12-31 -C" echo " ... Show CSV with monthly cellular traffic over year 2024 for device ABAB" echo } check_date() { [ $# -eq 1 ] || return 1 [[ "$1" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || return 1 date "+%F" -d "$1" >/dev/null 2>&1 || return 1 } f_trafcnt() { local devid local groupid local csv local traftype=0 # 0 ... all traffic, 1 ... cellular, 2 ... non-cellular local trafperiod=0 # 0 ... daily, 1 ... monthly, 2 ... totals dbname="$1" dbname="${dbname//[.-]/_}" shift [[ $# -lt 1 || "$1" = "-h" || "$1" = "--help" || $# -lt 2 ]] && { print_traffic_help [[ "$1" = "-h" || "$1" = "--help" ]] && return 0 return 1 } while [ $# -gt 0 ] ; do case $1 in -a) traftype=0 shift ;; -c) traftype=1 shift ;; -n) traftype=2 shift ;; -g) gid="$2" shift 2 ;; -d) devid="$2" shift 2 ;; -T) trafperiod=2 shift ;; -M) trafperiod=1 shift ;; -D) trafperiod=0 shift ;; -C) csv="yes" shift ;; -s) date_start="$2" shift 2 ;; -e) date_end="$2" shift 2 ;; *) echo "Unknown argument ($1)" return 1 esac done # check either gid or devid is set if [[ -z "$gid" && -z "$devid" ]] || [[ -n "$gid" && -n "$devid" ]] ; then echo "Exactly one of monitoring group name (-g \"name\") or device name (-d \"name\") must be provided." >&2 return 1 fi # check both start and end dates are defined and valid [[ -z "$date_start" || -z "$date_end" ]] && { echo "You must provide both start date (-s) and end date (-e)" return 1 } check_date "$date_start" || { echo "Invalid start date provided. Use YYYY-MM-DD format." >&2 return 1 } check_date "$date_end" || { echo "Invalid end date provided. Use YYYY-MM-DD format." >&2 return 1 } numid= # check if monitoring group / device exists and get its numeric id [ -n "$gid" ] && numid=$(mysql "$dbname" -Nse "SELECT id FROM monitoring_groups WHERE name = '$gid'"); [ -n "$devid" ] && numid=$(mysql "$dbname" -Nse "SELECT id_device FROM devices WHERE devid = '$devid'"); [[ -z "$numid" || "$numid" != "${numid//[^0-9]}" ]] && { echo "Unable to fetch nemuric ID for group or device name. Mis-typed group or device name?" >&2 return 1 } numeric_gid= numeric_devid= if [ -n "$gid" ] ; then numeric_gid=$numid else numeric_devid=$numid fi count_traffic "$dbname" "$numeric_gid" "$numeric_devid" "$traftype" "$trafperiod" "$date_start" "$date_end" "$csv" } func="" [[ $# -eq 1 && "$1" = "lscmd" ]] && { help mute; exit 0; } # first try parsing the function name from $0... cmdmatch=$(basename "$0") if [ "$cmdmatch" = "$call_prefix${cmdstrings[0]}" ] ; then # if help requested, call immediately with no additional parsing help "$@" exit 0 fi for i in $(seq 1 $((${#cmdstrings[@]}-1))); do if [ "$cmdmatch" = "$call_prefix${cmdstrings[$i]}" ] ; then if [[ $# -eq 1 && "$1" = "help" ]] ; then # help ==> help help "${cmdstrings[$i]}" exit 0 fi func="${cmdstrings[$i]}" break fi done # if not found, then try getting it from $1 [[ -z "$func" && $# -ge 1 ]] && for i in $(seq 1 $((${#cmdstrings[@]}-1))); do if [ "$1" = "${cmdstrings[$i]}" ] ; then shift # throw out the first parameter as that was the function requested func="${cmdstrings[$i]}" break fi done if [ -n "$func" ] ; then depname= declare -a cmdpars shopt -s nullglob for i in "/opt/cache_$sname/${sname}_deployments/"* ; do j=$(basename "$i") num="${j%%_*}" name="${j#*_}" addon="" [ ! -d "/opt/$name" ] && addon="ERROR: failed to find the deployment directory (/opt/$name)!" depnums+=("$num") depnames+=("$name") depnotes+=("$addon") done if [ ${#depnums[@]} -eq 0 ] ; then echo "No $lname deployments found, bailing out!" >&2 exit 1 elif [ ${#depnums[@]} -eq 1 ] ; then depname="${depnames[0]}" elif [[ "$func" != "deplist" && "$func" != "depstatus" && "$func" != "help" ]] ; then # iterate over rest of parameters, see if there is selection by num or name pardet="" byname="" token="" for arg in "$@" ; do if [ -z "$pardet" ] ; then if [ "$arg" = "--dn" ] ; then pardet="yes" elif [ "$arg" = "--dd" ] ; then pardet="yes" byname="yes" else cmdpars+=("$arg") fi elif [ -z "$token" ] ; then token="$arg" [[ -n "$byname" && "${token:0:${#call_prefix}}" != "$call_prefix" ]] && { token="$call_prefix$token" echo -e "\tNOTE: switching to deployment name $token ..." } else cmdpars+=("$arg") fi done # check if we can select based on parameters if [ -n "$pardet" ] ; then if [ -n "$byname" ] ; then for i in "${!depnames[@]}" ; do [ "$token" = "${depnames[$i]}" ] && depname="${depnames[$i]}" && break done else for i in "${!depnums[@]}" ; do [ "$token" = "${depnums[$i]}" ] && depname="${depnames[$i]}" && break done fi [ -z "$depname" ] && { echo "Deployment \"$token\" does not exist!" >&2; exit 1; } fi # if no selection is done by now, allow interactive selection if [ -z "$pardet" ] ; then f_deplist echo "Select deployment by its order number (first number on line):" read -r resp if [[ -n "$resp" && "$resp" = "${resp//[^0-9]}" && "$resp" -gt 0 && "$resp" -le ${#depnums[@]} ]] ; then depname="${depnames[$((resp-1))]}" else echo "Invalid deployment selection, bailing out." >&2 exit 1 fi fi [ -z "$depname" ] && { echo "No deployment name!" >&2; exit 1; } fi f_"$func" "$depname" "$@" else help "$@" fi