samuelk1 has asked for the wisdom of the Perl Monks concerning the following question:

Hello Perl Monks

I've written a perl script that connects to a bunch of crusty old Cisco 877 routers and backs up their configuration over the WAN.

The script is below. The first part just parses the nagios status.dat file, which has the current public IP address of each router. Some of them have dynamic IP addresses and these are kept up to date through some passive checks in Nagios.

use warnings; use strict; open STATUS_DAT, "</usr/local/nagios4/var/status.dat"; my $hs = 0; my $r = 0; my $r_ip; my $r_name; my $NAME = 0; my $IP = 1; my @routers; sub write_config { my $config = shift; my $filename = shift; open CONFIG, ">$filename"; print CONFIG $config; print "Wrote $filename\n"; } while (<STATUS_DAT>) { $hs = 1, next if (/hoststatus/); $hs = 0, $r = 0, next if ($hs and /}/); next if (not $hs); $r = 1, $r_name = $+, next if (/host_name=(.*ROU)\n/); next if (not $r); $r_ip = $+, push @routers, [$r_name, $r_ip] if (/_PUBLICIP=[01];(. +*)\n/); } for my $router (@routers) { my $retries = 0; retry: my $output = `expect -f show_running_config.exp $router->[$IP]`; if ($? == 0) { write_config($output, "$router->[$NAME].cfg"); } else { if ($retries <= 3) { sleep 5; $retries++; goto retry; } else { print "Failed to get config for $router->[$NAME] at $route +r->[$IP]: $output\n"; } } }
The firmware on these routers does not handle SSH particularly well. Nagios is checking that the routers can be accessed using SSH over the WAN every 5 minutes. If you try and SSH into the router around 5-10 seconds after this check place, the connection is reset because the router is still tearing down the SSH session. The workaround in the script is to just try and connect in 5 seconds using a goto. I was wondering if there is a good way to rewrite this loop without goto? Or is goto OK in this case? I have had one idea, but the problem with it is it still waits 5 seconds even after the last retry has taken place before checking the next router:
for my $router (@routers) { my $retries = 0; while ($retries <= 3) { my $output = `expect -f show_running_config.exp $router->[$IP] +`; write_config($output, "$router->[$NAME].cfg"), last if ($? == +0); } continue { sleep 5; $retries++; } if ($retries > 3) { print "Failed to get config for $router->[$NAME] at $router->[ +$IP]: $output\n"; } }
Thanks in advance for any advice. Regards, Sam

Replies are listed 'Best First'.
Re: Wait and retry loop without goto
by aaron_baugher (Curate) on May 15, 2015 at 02:50 UTC

    A do-while loop should do the job without the goto:

    my $output; my $tries = 0; do { $output = `expect -f show_running_config.exp $router->[$IP]`; if($?){ # subshell returns error $tries++; # count a try sleep 5; # pause } } while( $? and $tries < 4 ); # loop again if error and more tries # was the last try unsuccessful? if($?){ # last try was an error, ran out of tries print "Unable to connect after $tries tries\n"; exit; } # go on to handle the response in $output

    Aaron B.
    Available for small or large Perl jobs and *nix system administration; see my home node.

      Thanks, I didn't think of that. Works great.
Re: Wait and retry loop without goto
by einhverfr (Friar) on May 15, 2015 at 06:20 UTC

    Not only can you use a while loop but you can also use goto in a better way. Perl has two distinct forms of goto. The first (as you are doing here) is a classic goto. Just a jump, all state preserved, messy. Don't do it.

    The second is a tail call form. If you goto &sub it leaves the current stack frame and re-enters with the existing values of @_ re-aliased. This means you have clean scope. This is very useful for retries. So in this case we could:

    for my $router (@routers) { get_config($router, 0); } .... sub get_config { my ($router, $retries) = @_; my $output = `expect -f show_running_config.exp $router->[$IP]`; die "Failed to get config for $router->[$NAME] at $router->[ +$IP]: $output\n" if $retries > 3 and $?; unless ($?) { sleep 5; $_[2] = $_[2] + 1; goto &get_config; return write_config($output, "$router->[$NAME].cfg"); }

    Here goto gives you an effective restart condition, but with a clean state (i.e. all locally scoped variables, such as $output get cleared on each retry. It is very nice in the sense it gives you a clean restart on the function without the extra stack overhead (though since you are only retrying 5 times, recursion may be another option).

      Hmmm. I guess this probably works well, but this form of goto is somewhat magical, I am not sure it should be advised to someone probably not understanding the details and, more broadly, for such a simple task, where a while loop or a simple recursive call would do as well.

      Je suis Charlie.
        I think it seems magical because it isn't really a goto. I see why Perl 6 is changing it to &sub.nextwith(). It would be nicer IMO if it had been given its own keyword like tailcall().
Re: Wait and retry loop without goto
by jellisii2 (Hermit) on May 15, 2015 at 12:01 UTC
    Untested
    # refactor each $router to have a 'success' key of 0; # running this way allows you to iterate through the # entire list of routers, dealing with failures only # after the entire stack has been processed. # assuming a sufficient number of targets, this should # sidestep your problem as I understand it. for (my $i = 0; $i++; $i < 3) { my $failures = 0; foreach my $router (@routers) { if ($router->{'success'} != 1) { my $output = `expect -f show_running_config.exp $router->[$IP]`; if (write_config($output, "$router->[$NAME].cfg") == 0) { $router->{'success'} = 1; $router->{'msg'} = "Output was $output"; } else { $failures++; $router->{'msg'} = "Failed to get config for $router->[$NAME] +at $router->[$IP]: $output\n"; if ($i == 2) { print $router->{'msg'}; } } } } if ($failures < 1) { last; } sleep 5; $i++; }
    -- Edit: removed unneeded $failures. -- Edit2: Thanks Laurent_R for proofreading.