LinuxQuestions.org

LinuxQuestions.org (/questions/)
-   Programming (https://www.linuxquestions.org/questions/programming-9/)
-   -   Bash to ash: Help converting a script? (https://www.linuxquestions.org/questions/programming-9/bash-to-ash-help-converting-a-script-901308/)

Mouse750 09-05-2011 07:33 PM

Bash to ash: Help converting a script?
 
Ok, the awesome members here at linuxquestions helped (understatement) me modify a pick a random card bash script into this beauty:

Code:

#!/bin/bash
# The goal of this script is randomly
# pick a vpn server for openwrt routers.

# Put ovpn files in this directory:
cd /etc/config/vpn_servers/

# List Server Names Here
servers=(
        Chicago_01.ovpn       
        Dallas_06.ovpn       
        LA_04.ovpn       
        LasVegas_03.ovpn       
        Phoenix_04.ovpn       
        SaltLakeCity_05.ovpn       
        Virginia_02.ovpn
)

# Count The Number of Servers.
count_the_servers=${#servers[*]}

# Main Part
# Begin Loop
while true; do

        killall openvpn
        sleep 5
               
        #Pick A Random Server
        openvpn --config "${servers[RANDOM%count_the_servers]}" &
                               
        #Pick new server in 1 Hour.
        sleep 3595
                                       
# End of Loop
done

The scripts works perfect, I couldn't have asked for more. You guys are great!!

Now I'd like to pay it forward and help others by sharing the script.

Problem is, bash in not normally installed on the target devices. I was able to expand my routers limited flash memory via usb and install Bash to make the script work but that's not going to work out so well for others.

However, the target device does have ash. (#!/bin/sh)

Can somebody please convert, or help convert this? Or is it not possible?

awk is installed by default, I can list any others if you ask...

Thank You.

Nominal Animal 09-06-2011 04:48 AM

First, you don't want to modify the script whenever something minor changes. Create /etc/config/vpn_servers.conf and set the important variables there, e.g.
Code:

# Settings for the OpenVPN rotation.

# Number of seconds to use each configuration.
INTERVAL=3595

# Number of seconds to delay after stopping OpenVPN.
DELAY=5

# Directory where the .ovpn files are located.
CONFIGDIR=/etc/config/vpn_servers/

The script itself is below. It is untested. If the configuration file above does not exist, it uses the values above as defaults. The script requires sh find od bc wc sed mktemp cat to be installed to work. The script scans the CONFIGDIR for .ovpn files each time, so you don't need to restart the script, or really do anything, when adding new .ovpn files. If you create or modify the configuration file above, then you do need to restart the script.
Code:

#!/bin/sh

# Config file setting INTERVAL, DELAY, CONFIGDIR variables
if [ -f /etc/config/vpn_servers.conf ]; then
        . /etc/config/vpn_servers.conf
fi

# Defaults
[ -n "$INTERVAL"  ] || INTERVAL=3595
[ -n "$DELAY"    ] || DELAY=5
[ -n "$CONFIGDIR" ] || CONFIGDIR=/etc/config/vpn_servers/

while [ 1 ]; do

        # Create a temporary file for config files
        TEMPLIST=$(mktemp) || exit 1

        # List all config files into the temporary file
        find "$CONFIGDIR" -maxdepth 1 -name '*.ovpn' '!' -name '*
*' > "$TEMPLIST"

        # Count the config files
        COUNT=$[ $(cat "$TEMPLIST" | wc -l) -0 ]
        [ $COUNT -gt 0 ] || exit 1

        # Generate a large random number
        INDEX=$(od -A n -N 4 -t u4 /dev/urandom) +1 )

        # Limit the random number to the config file range
        INDEX=$(echo '(' $INDEX '%' $COUNT ')' + 1 | bc)

        # Pick a config file at random
        CONFIG=$(sed -n -e "$INDEX p" "$TEMPLIST")

        # Remove temporary file
        rm -f "$TEMPLIST"

        # Check that the config file exists
        [ -f "$CONFIG" ] || continue

        # Restart OpenVPN using the new config file.
        killall openvpn
        sleep $DELAY
        ( openvpn --config "$CONFIG" & )
                               
        # Pick new server in 1 Hour.
        sleep $INTERVAL
done

If you have /usr/bin/logger installed, you might consider expanding the error handling by logging the reason the script exits to syslog.

The script could be rewritten to use awk instead of mktemp cat od bc . Alternatively, you could use just perl or python depending on which you have installed. It is also simple enough to be rewritten in e.g. plain C, if you need to minimize the dependencies.

Mouse750 09-06-2011 08:44 AM

Thank You for all the effort...

Not Installed:

od, bc, perl, or python.

Installed by default:

/usr/bin/logger
sh, find, wc, sed, mktemp, cat, and awk

Nominal Animal 09-06-2011 10:13 AM

Okay, here is the script rewritten to use awk. Untested.
Code:

#!/bin/sh

# Config file setting INTERVAL, DELAY, CONFIGDIR variables
if [ -f /etc/config/vpn_servers.conf ]; then
        . /etc/config/vpn_servers.conf
fi

# Defaults
[ -n "$INTERVAL"  ] || INTERVAL=3595
[ -n "$DELAY"    ] || DELAY=5
[ -n "$CONFIGDIR" ] || CONFIGDIR=/etc/config/vpn_servers/

while [ 1 ]; do

        # Use find to list all config files, and
        # awk to pick one file at random.
        CONFIG=$(find "$CONFIGDIR" -maxdepth 1 -name '*.ovpn' '!' -name '*
*' | awk '{ file[n++] = $0 } END { srand(); print file[int(n * rand())] }')

        # Check that the config file exists
        [ -f "$CONFIG" ] || continue

        # Restart OpenVPN using the new config file
        killall openvpn

        # Wait for DELAY seconds before starting openvpn
        sleep $DELAY
        ( openvpn --config "$CONFIG" & )
                               
        # Use this server for INTERVAL seconds
        sleep $INTERVAL
done


David the H. 09-06-2011 01:46 PM

Nominal Animal probably has the better solution, as he appears to understand exactly what you want to do. His suggestion of an external config file is good.

But as a scripting exercise, here's my previous script converted into posix-compatible syntax, with a minimal number of alterations. I used the info listed here:

http://mywiki.wooledge.org/Bashism

The biggest challenge is the lack of arrays, but by using the positional parameters instead, it actually requires few other changes:

Code:

#!/bin/sh

# The goal of this script is randomly
# pick a vpn server for openwrt routers.

# Put ovpn files in this directory:
cd /etc/config/vpn_servers/

# List Server Names Here.
set -- "Chicago_01.ovpn" \
      "Dallas_06.ovpn" \
      "LA_04.ovpn" \
      "LasVegas_03.ovpn" \
      "Phoenix_04.ovpn" \
      "SaltLakeCity_05.ovpn" \
      "Virginia_02.ovpn"

# Count The Number of Servers.
count_the_servers="$#"

# Main Part
# Begin Loop
while true; do

    killall openvpn
    sleep 5

    #Pick A Random Server
    random="$( awk 'BEGIN{ srand(); printf "%d", ( rand() * 10000 ) }' )"
    server="$(( random % count_the_servers +1 ))"
    eval server="\$$server"

    openvpn --config "$server" &

    #Pick new server in 1 Hour.
    sleep 3595

#End of Loop
done

But if all the .ovpn files sit inside the /etc/config/vpn_servers/ directory, then you can simply change the line above to...
Code:

set -- *.ovpn
...which will load them into the "array".

If I had realized this before, we could've also done the same in my original script too:
Code:

servers=( *.ovpn )
The rest of the script values can then also be moved to an external file and sourced, instead of hard-coded, as NA demonstrated.

Mouse750 09-06-2011 05:55 PM

I don't know what to say... You guys are too kind! A simple Thank You doesn't quite sum up my feelings.

Both scripts work out of the box, I'm sure that takes some skill. You guys did it blind.

Sorry for the late reply but I have been playing with both scripts to see "how" they work.

In David the H's I did change it to set -- *.ovpn as suggested. It's a nice change because it uncomplicates the script. I don't understand what the rand() * 10000 does. Why the 10000? Also I noticed you used eval and I remember you telling me in the past to try not using it.

And again, Nominal Animal version is very clean looking and looks very professional. Please forgive me but I do not understand the need for the sleep command variables. Also since you built the defaults into it, I didn't see the need to use an external conf?

Would this also be acceptable?:

Code:

#!/bin/sh

# Defaults
[ -n "$CONFIGDIR" ] || CONFIGDIR=/etc/config/vpn_servers/

while true; do

        # Use find to list all config files, and                                                                                                           
        # awk to pick one file at random.                                                                                                                   
        CONFIG=$(find "$CONFIGDIR" -maxdepth 1 -name '*.ovpn' '!' -name '*                                                                                 
        *' | awk '{ file[n++] = $0 } END { srand(); print file[int(n * rand())] }')                                                                         
                                                                                                                       
        # Restart OpenVPN using the new config file                                                                                                         
        killall openvpn                                                                                                                                     

        # Wait 5 seconds before starting openvpn                                                                                                   
        sleep 5                                                                                                                                             
        ( openvpn --config "$CONFIG" & )                                                                                                                   

        # Use this server for 7200 seconds                                                                                                                 
        sleep 7200                                                                                                                                         
done
#

Or is it just more professional the way you originally intended? Sorry if it seems like I'm chopping up your hard work. I'm just trying to dissect it, so that I can learn from it.

David the H. 09-07-2011 04:51 PM

awk's rand function outputs numbers as a six-decimal-place floating-point between zero and one (i.e. "0.566305"). Multiplying the result by 10000 shifts it so there are four-digits in front of the decimal, which is then truncated into an integer with the %d (digit) printf option. You could use any multiplier 100 or greater, really.

Yes, I like to stress that eval is something that should generally be avoided whenever possible, and in most cases there are better solutions available. But it's not always bad; you just need to know when and how to use it properly. The main concern is to make sure that it only evaluates lines with known values in it; otherwise you're at risk of executing malicious code.

http://mywiki.wooledge.org/BashFAQ/048

In this case, there's no risk because the script ensures that the variable to be evaluated only contains a nice safe number. But there really isn't much choice anyway, as there's no other reasonable way to indirectly reference the needed parameter in a posix shell.

I think NA's point with the separate config file is that the script itself should hold only the processing code while things that are user-specified should be set as inputs to the script in some fashion, and in principle I agree. It allows you to change the settings as necessary without messing with the script itself. "Defaults" are of course just fallbacks that the script will use if it can't import the configured input values for some reason (e.g. the file becomes corrupted). It's generally a good idea to code for such contingencies.

But if you're really confident the settings will never change, then I say go ahead and simply hard-code them.

Nominal Animal 09-08-2011 01:32 PM

Quote:

Originally Posted by David the H. (Post 4464279)
awk's rand function outputs numbers as a six-decimal-place floating-point between zero and one (i.e. "0.566305"). Multiplying the result by 10000 shifts it so there are four-digits in front of the decimal, which is then truncated into an integer with the %d (digit) printf option. You could use any multiplier 100 or greater, really.

Right. In general, you can use int(rand() * N) to produce a random integer between 0 and N-1 inclusive, up to at least a million. The value will be always less than N. Remember to call srand() first though, to set a new random seed based on the system time, or you will get the same sequence (depending on awk variant and version).

Quote:

Originally Posted by David the H. (Post 4464279)
I think NA's point with the separate config file is that the script itself should hold only the processing code while things that are user-specified should be set as inputs to the script in some fashion, and in principle I agree.

Exactly. For something as simple as this script it is not necessary, but it is an important principle.

(As an aside, I think two different example solutions are always better than just one. It gives the reader multiple viewpoints to the problem, and may eventually lead to a better solution than either one alone. I think it is a very good thing you showed a different approach, David the H. Thank you.)

I know of several largeish Linux clusters that use a script to maintain firewall settings, with the settings contained in the script itself. Roughly once a year somebody manages to lock up a cluster by mis-editing the script. I did write a replacement script that uses separate config files, and cancels the changes unless the user expressly and interactively confirms the changes, but it was rejected: apparently the rare firewall lock-up is less of a disruption than changing something that 'works'.

Which is kind of my point: Learn the way to do things efficiently in the long term. You never know which script ends up being used for the next decade or so. Separating configuration from the script is one of the ways. It lowers the probability of typos due to changes. For simple variables, sourcing a config file (like in my example above) works well, and you can use
Code:

sh -c '. path/to/config.file >/dev/null 2>/dev/null' || echo "Error in config file!"
to check the syntax in the configuration file. (The above command does it in a specific separate shell, so current shell is irrelevant, and unaffected by sourcing the config file.)

If you have the settings within the script, the only way you can test it is by running it. If you were to expand your script to a full service, you could do the check before reload -- like Apache does. Then, when an administrator modifies the configuration and issues sudo service this-service reload , the configuration is checked first. If there is a problem in the configuration, the service is not taken offline; the administrator only gets a warning that there is a b0rk in the config, so no changes are done. (It still surprises me how most admins still use restart instead of reload -- and then do a headdesk when their buggy changes stop the service from working, and get an angry call from their boss and/or client. As you can see from my examples above, avoiding stuff like that does not require that much more effort.)

There is a secondary reason why I wrote the example script the way I did. I suspect you will eventually need to change the script from being a standalone script running always, to either a script run regularly via cron, or to a full-blown service. The former is trivial; just omit the loop and the sleep $INTERVAL bit. For the latter, I'd personally write a small C program instead, mainly to keep resource use minimal. The logic would follow the script very closely, though.

(The typical reason for moving to cron or a service script is ease of maintenance, for example through a web-based interface. Standalone scripts tend to be a pain to manage automatically -- each one needing their own management stuff --, while cron scripts and services follow very simple rules, and are therefore much easier to manage: you only need one interface to manage all possible cron scripts, and one interface to manage all service scripts, regardless of the script contents.)


All times are GMT -5. The time now is 11:13 AM.