I need to write a perl script that executes a command at a specified time.
use net::ssh::expect to login to a router
read the time from the router's clock ("show clock" command displays the time.)
At the 17:30:00 execute a command.
I tried writing script for it but it doesn't work. Any suggestions please ?
use strict;
use warnings;
use autodie;
use feature qw/say/;
use Net::SSH::Expect;
my $Time;
my $ssh = Net::SSH::Expect->new(
host => "ip",
password => 'pwd',
user => 'user name',
raw_pty => 1,
);
my $login_output = $ssh->login();
while(1) {
$Time = localtime();
if( $Time == 17:30:00 ) {
my $cmd = $ssh->exec("cmd");
print($cmd);
} else {
print" Failed to execute the cmd \n";
}
}
Several things here:
First, use Time::Piece. It's now included in Perl.
use Time::Piece;
for (;;) { # I prefer using "for" for infinite loops
my $time = localtime; # localtime creates a Time::Piece object
# I could also simply look at $time
if ( $time->hms eq "17:30:00" ) {
my $cmd $ssh->exec("cmd");
print "$cmd\n";
}
else {
print "Didn't execute command\n";
}
}
Second, you shouldn't use a loop like this because you're going to be tying up a process just looping over and over again. You can try sleeping until the correct time:
use strict;
use warnings;
use feature qw(say);
use Time::Piece;
my $time_zone = "-0500"; # Or whatever your offset from GMT
my $current_time = local time;
my $run_time = Time::Piece(
$current_time->mdy . " 17:30:00 $time_zone", # Time you want to run including M/D/Y
"%m-%d-%Y %H:%M:%S %z"); # Format of timestamp
sleep $run_time - $current_time;
$ssh->("cmd");
...
What I did here was calculate the difference between the time you want to run your command and the time you want to execute the command. Only issue if I run this script after 5:30pm local time. In that case, I may have to check for the next day.
Or, even better, if you're on Unix, look up the crontab and use that. The crontab will allow you to specify exactly when a particular command should be executed, and you don't have to worry about calculating it in your program. Simply create an entry in the crontab table:
30 17 * * * my_script.pl
The 30 and 17 say you want to run your script everyday at 5:30pm. The other asterisks are for day of the month, the month, and the day of the week. For example, you only want to run your program on weekdays:
30 17 * * 1-5 my_script.pl # Sunday is 0, Mon is 1...
Windows has a similar method called the Schedule Control Panel where you can setup jobs that run at particular times. You might have to use perl my_scipt.pl, so Windows knows to use the Perl interpreter for executing your program.
I highly recommend using the crontab route. It's efficient, guaranteed to work, allows you to concentrate on your program an not finagling when to execute your program. Plus, it's flexible, everyone knows about it, and no one will kill your task while it sits there and waits for 5:30pm.
localtime converts a Unix timestamp (seconds since epoch, which is about 1.4 billion now) to a list of values. The time function conveniently provides that timestamp. From perldoc -f localtime:
Converts a time as returned by the time function to a 9-element
list with the time analyzed for the local time zone. Typically
used as follows:
# 0 1 2 3 4 5 6 7 8
($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
localtime(time);
For your time comparison you could do:
$Time = join ':', (localtime(time))[2, 1, 0];
if ($Time eq '17:30:00') {
...
}
Since Perl allows the postcircumfix [...] operator to index into lists just like it does with arrays, we can use it to remove the slice of the (localtime(time)) list that contains hours, minutes, and seconds, join them with colons, and assign the resulting string to $Time.
Note that because $Time now holds a string, you should compare it to '17:30:00' rather than the bareword 17:30:00, which isn't a valid numeric form and should result in a compilation error. And since we're comparing strings instead of numbers, we use the eq operator. == forces numeric context on its operands, and since 17:30:00 isn't a valid number, Perl will treat it as 0 and warn you with
Argument "foo" isn't numeric in numeric eq (==) at ....
Related
Let's say I just ran for 10 km and I now have 10 1km split times in the form of MM::SS. I want a simple way to add up an arbitrary number of the split times and average them. For instance, maybe I want to see how much faster (or slower) the last 5 km were when compared with the first 5 km.
I can do this myself by parsing the times, dividing them into seconds and then converting them back to MM::SS. The math isn't hard, but I was wondering if something on CPAN already does this in a simple way. My first attempt was using DateTime, but it doesn't convert from seconds to minutes, because of leap seconds.
To be clear, I don't care about leap seconds in this context, but I'm curious as to whether a library exists. As an example, here is what I have tried.
#!/usr/bin/env perl
use strict;
use warnings;
use feature qw( say );
use DateTime::Format::Duration;
my $formatter = DateTime::Format::Duration->new( pattern => '%M:%S' );
my #splits = #ARGV;
my $split_number = #splits;
my $total = $formatter->parse_duration( shift #splits );
foreach my $split (#splits) {
$total->add_duration(
$formatter->parse_duration($split) );
}
say 'Total time: ' . join ':', $total->minutes, $total->seconds;
$total->multiply( 1 / $split_number );
say 'Average time: ' . join ':', $total->minutes, $total->seconds;
say 'Using DateTime::Format::Duration: ' . $formatter->format_duration( $total );
And a sample run:
$ perl script/add-splits.pl 1:30 2:30
Total time: 3:60
Average time: 1.5:30
Using DateTime::Format::Duration: 01:30
So, you can see that the duration object itself, gives me a correct answer, but not something that a human wants to decipher.
DateTime::Format::Duration tries to be helpful, but tosses out 30 seconds in the process.
I'm not looking for the raw code to do this. I am interested in whether this already exists on CPAN.
The problem with finding this exact functionality on CPAN is that you want to manipulate strings that are time intervals. Most modules are concerned with the context of such strings, working with them as date and time. So it's hard to find something that simply adds mm:ss format. Since this quest is rather specific, and simple to write, why not wrap it in your own package?
Having said that, see whether the snippet below fits what you are looking for.
This is a simple solution with the core module Time::Piece. It does go to seconds to do the math, but it can be wrapped in a few subs that are then also easily extended for other calculations.
use warnings 'all';
use strict;
use feature 'say';
use Time::Piece;
use POSIX 'strftime';
use List::Util 'sum';
my #times = #ARGV;
my $fmt = '%M:%S';
my $tot_sec = sum map { Time::Piece->strptime($_, $fmt)->epoch } #times;
my $ave_sec = sprintf("%.0f", $tot_sec/#times); # round the average
my ($tot, $ave) = map { strftime $fmt, gmtime $_ } ($tot_sec, $ave_sec);
say "Total time: $tot";
say "Average time: $ave";
For manip_times.pl 2:30 1:30 this prints
Total time: 04:00
Average time: 02:00
We use strptime from Time::Piece to get the object, and then its epoch method returns seconds, which are added and averaged. This is converted back to mm:ss using strftime from POSIX. The Time::Piece also has strftime but to use it we'd have to have an object.
Note that Time::Piece does subtract its objects directly, $t1 - $t2, but it cannot add them. One can add an integer (seconds) to an object though, $t1 + 120. Also see the core Time::Seconds, a part of the distribution.
Comments on the method used in the question
The DateTime::Duration objects that are used cannot convert between different units
See the How DateTime Math Works section of the DateTime.pm documentation for more details. The short course: One cannot in general convert between seconds, minutes, days, and months, so this class will never do so.
From a bit further down the page, we see what conversions can be done and the related ones are only "hours <=> minutes" and "seconds <=> nanoseconds". The reasons have to do with leap seconds, DST, and such. Thus the calculation has to produce results such as 1.5 minutes.
The Class::Date also looks suitable for these particular requirements.
Here is a version using the DateTime object. Since it doesn't handle date parsing, we have to split the strings ourselves...
add_splits.pl
#!/usr/bin/env perl
use strict;
use warnings;
use feature qw( say );
use DateTime;
my #splits = #ARGV;
my $split_number = #splits;
my $fmt = "%M:%S";
my $total = DateTime->from_epoch( epoch => 0 );
foreach my $split (#splits) {
my ($split_min, $split_sec) = split /:/, $split;
$total->add( minutes => $split_min, seconds => $split_sec );
}
say 'Total time: ' . $total->strftime($fmt);
my $avg_seconds = $total->epoch / $split_number;
my $avg = DateTime->from_epoch( epoch => 0 )->add( seconds => $avg_seconds );
say 'Average time: ' . $avg->strftime($fmt);
Output
$ perl add_splits.pl 1:30 2:30
Total time: 04:00
Average time: 02:00
I am writing a small tool to parse some application logs for to collect data that is going to be used as the inputs for Zabbix monitoring. I am just wanting to keep data from the logs that are within the past two hours.
The format of the logs is pretty simple, the fields are separated by white space and the first three fields are used to determine the time when the logging was written.
Here is an example of the first three fields of a log line:
Jan 5 13:42:07
What I set out to do was to utilize one of my favorite modules, DateTime. Where I convert the above into a DateTime object and then compare that object to another DateTime object when the utility would be invoked.
Everything was fine an dandy and working nicely until I actually set the utility against the a portion of the logs it would actually be parsing -- only a couple gigabytes in size. The test run was being done on a kitchen invoked Ubuntu virtual box instance on my laptop, so the resources are -- as expected -- rather limited. The script would halt with the words 'Killed' displayed.
Looking into /var/log/messages I would see log lines describing the process being killed due to resource issues.
When I invoked the process again, and then switching to another screen instance to watch top, I noticed that the memory percentage would grow, that swap space would being to be consumed all until the script would again stop with the 'Killed' message.
When I would rerun the script with the DateTime portion commented out, the script would execute as expected.
In the script I have a subroutine which would be called to create a DateTime object based upon the information found in the first three fields of the log line. I have tried where I create the object at the beginning of the subroutine then undef it prior to returning a value at the end of the subroutine, I have tried it where I create a global object ( using our ) and then use the DateTime set_* methods to modify what I thought would be a single object's values.
I have read that perl does not clean up hash memory so that it can be reused by the program--I feel that this is the base of the issue that I am running into.
At this point, I am feel the need to get input of others and that is the reason for this post. All comments and criticisms would be appreciated.
This utility was running on Perl v5.14.2.
This code produces the memory leak:
#!/usr/bin/perl -w
use strict;
use DateTime;
my $month = 1;
my $day = 6;
my $hour = 20;
my $minute = 30;
my $second = 00;
for (my $count = 0; $count <= 25_000_000; $count++) {
my $epoch = &get_epoch( $month, $day, $hour, $minute, $second );
}
sub get_epoch {
my $mon = shift;
my $day = shift;
my $hour = shift;
my $min = shift;
my $sec = shift;
my $temp_dt = DateTime->new(
year => 2015,
month => $mon,
day => $day,
hour => $hour,
minute => $min,
second => $sec,
nanosecond => 500_000_000,
time_zone => 'UTC',
);
return( $temp_dt->epoch );
}
This is a bug in Params::Validate 1.15 and will be fixed very soon.
I have this Perl script where I need to monitor the execution time of DBI calls.
In Europe (France), I have no problem: 2 seconds execution time is reported 2 seconds.
This same script running on a computer in Singapore reports 30 minutes and 2 seconds.
Why ?
use strict;
use Time::Format qw(%time);
use Time::HiRes qw(gettimeofday);
my $time_start = gettimeofday();
sleep 2; # some action goes here
my $stat_perf = gettimeofday() - $time_start;
print STDOUT $time{'mm:ss.mmm', $stat_perf} . " \n";
The output in France is
00:02.000
The same script running in Singapore yields:
30:02.001
Why ?
According to this documentation, the gettimeofday function returns seconds or microseconds since the unix epoch, which is 1/1/1970 UTC. Because it is in UTC, it is not affected by time zones at all.
Also, in your original code you are just using gettimeofday, which is going to be returning timestamps from now, not from 1970. But in your suggested answer, for some reason, you have hard-set the timestamp, which won't help you do much.
Yes, there is history to just about every time zone, including Singapore. You can see it in the TZDB here. But you are incorrect about it being +8:30 at the epoch. It was actually +7:30. You can verify also on this site. But it doesn't matter anyway because like I said, gettimeofday works strictly in UTC.
I think the problem is in how you are interpreting the results. You have as your last line:
print STDOUT $time{'mm:ss.mmm', $stat_perf} . " \n";
But $stat_perf is the elapsed duration of time, not a value that you can treat as a timestamp. You probably shouldn't be passing it to $time, since that will use the local time zone and be expecting a full timestamp.
Also, you may want to use tv_interval, as shown in the examples.
Update
I searched through the CPAN archives and I'm sure somewhere there is a module for formatting an elapsed duration of time, but I can't seem to find it. Anyway, it's not too hard to write this on your own. Here, this should work:
my $min = $stat_perf / 60;
my $sec = ($stat_perf * 1000 % 60000) / 1000;
my $elapsed = sprintf("%02u:%06.3f", $min, $sec);
print STDOUT $elapsed . "\n";
The anser is ...
Singapore is now 08h00 offset from UTC. In 1970, it was offset by 08h30. Asking for the conversion of a few seconds into a string will get us to 1970, not today's date, and timezone.
By requesting
print STDOUT $time{'mm:ss.mmm', 2} . " \n";
the system adjusts to 1970 (epoch) timezone offset.
In order to get a correct result in Singapore, we must shift to after 1982, when Singapore made its last timezone change.
print STDOUT $time{'mm:ss.mmm', 2 + 1356994800} . " \n";
as
UNIX_TIMESTAMP('2013-01-01 00:00:00') = 1356994800
We are only concerned by the time of day portion of the date, so this does it.
Check with
zdump -v Asia/Singapore
This is the trick.
Here is a script that emulates $time{} in converting a real number into a string representing the mm:ss sexagesimal conversion of its integer part, concatenated with the decimal remainder formatted as microseconds.
As this is going to be part of a library, there are protections set to avoid invoking it with bad arguments.
I hope I didn't miss something.
use strict;
use Time::Format qw(%time);
# ----------------------------------------------------------
# A substitute to $time{} as we have issues with TZ offsets at epoch days in some part of the World
# A real to sexagesimal converter
# Format will be set to match $time{'mm:ss.mmm', $stat_perf};
sub microTime {
return '' unless (my ($intertime) = #_);
return '' unless (ref ($intertime) eq '');
return '' unless (sprintf("%s", $intertime) =~ m/^(?:[\d]+)(?:\.(?:[\d]+))?$/);
my $intNum = int($intertime);
"a" =~ /a/; # Resets regex buffers
sprintf ("%.03f", $intertime - $intNum) =~ m,\.([\d]+),;
my $intDec = $1; # It's always defined
my $intUnder = $intNum % 3600;
my $intMin = int($intUnder / 60);
my $intSec = $intUnder % 60;
return sprintf ("%02d:%02d.%03d", $intMin, $intSec, $intDec);
}
my $stat_perf;
$stat_perf = 345.987;
$stat_perf = 345;
$stat_perf = 3945.987;
$stat_perf = 0;
$stat_perf = 3945.918733;
print STDOUT sprintf (" >> %s\n", µTime ($stat_perf));
print STDOUT sprintf (" == %s\n", $time{'mm:ss.mmm', $stat_perf});
There are some programs/scripts that need to be run at specific times in a timezone different from the system timezone.
A la crontab in Perl, but one that honors a timezone and DST rules in a region different from that in which the system is configured.
Here is the use case : I will create an excel sheet with the time in PT in column B and the corresponding program/Perl script to run in column C.
Nothing specific about this information being in a Excel sheet - could be plain text file/"crontab" entry too.
A Perl script will read in the data from the excel sheet and run/spawn those scripts at the correct time.
The thing to keep at mind is that the Perl script should run correctly regardless of what timezone the system that it is running on is.
Regardless of whether the script is running on a Box in NY or IL or CA, it should spawn the scripts at the time mentioned in the file entries as per the Pacific Standard Time with DST at mind.
It is very important, as I said before, of it being aware, "automagically" ( without me doing any explicit programmming ) of the latest DST rules for the PT region.
What would you suggest?
Maybe I can visit some website that shows current time in that region and scan the time value from it, and run the scripts when it's the correct time?
Any such Perl screen scraper friendly site?
Or maybe I can use some smart Perl module, like Schedule::Cron
For the record, a large number of good suggestions came by at http://www.perlmonks.org/index.pl?node_id=772934, however, they, in typical at/cron fashion, work as per the system configured timezone.
In general, if you care about timezones, represent times internally in some universal format and convert times for display purposes only.
Applying this to your problem, write a crontab whose times are expressed in GMT. On each worker machine, convert to local time and install the crontab.
Front matter:
#! /usr/bin/perl
use warnings;
use strict;
use feature qw/ switch /;
use Time::Local qw/ timegm /;
For the conversions this program supports, use today's date and substitute the time from the current cronjob. Return the adjusted hour and day-of-week offset:
sub gmtoday {
my($gmmin,$gmhr,$gmmday,$gmmon,$gmwday) = #_;
my #gmtime = gmtime $^T;
my(undef,undef,$hour,$mday,$mon,$year,$wday) = #gmtime;
my #args = (
0, # sec
$gmmin eq "*" ? "0" : $gmmin,
$gmhr,
$mday,
$mon,
$year,
);
my($lhour,$lwday) = (localtime timegm #args)[2,6];
($lhour, $lwday - $wday);
}
Take the five-field time specification from the current cronjob and convert it from GMT to local time. Note that a fully general implementation would support 32 (i.e., 2 ** 5) cases.
sub localcron {
my($gmmin,$gmhr,$gmmday,$gmmon,$gmwday) = #_;
given ("$gmmin,$gmhr,$gmmday,$gmmon,$gmwday") {
# trivial case: no adjustment necessary
when (/^\d+,\*,\*,\*,\*$/) {
return ($gmmin,$gmhr,$gmmday,$gmmon,$gmwday);
}
# hour and maybe minute
when (/^(\d+|\*),\d+,\*,\*,\*$/) {
my($lhour) = gmtoday #_;
return ($gmmin,$lhour,$gmmday,$gmmon,$gmwday);
}
# day of week, hour, and maybe minute
when (/^(\d+|\*),\d+,\*,\*,\d+$/) {
my($lhour,$wdoff) = gmtoday #_;
return ($gmmin,$lhour,$gmmday,$gmmon,$gmwday+$wdoff);
}
default {
warn "$0: unhandled case: $gmmin $gmhr $gmmday $gmmon $gmwday";
return;
}
}
}
Finally, the main loop reads each line from the input and generates the appropriate output. Note that we do not destroy unhandled times: they instead appear in the output as comments.
while (<>) {
if (/^\s*(?:#.*)?$/) {
print;
next;
}
chomp;
my #gmcron = split " ", $_, 6;
my $cmd = pop #gmcron;
my #localcron = localcron #gmcron;
if (#localcron) {
print join(" " => #localcron), "\t", $cmd, "\n"
}
else {
print "# ", $_, "\n";
}
}
For this sorta-crontab
33 * * * * minute only
0 0 * * * minute and hour
0 10 * * 1 minute, hour, and wday (same day)
0 2 * * 1 minute, hour, and wday (cross day)
the output is the following when run in the US Central timezone:
33 * * * * minute only
0 18 * * * minute and hour
0 4 * * 1 minute, hour, and wday (same day)
0 20 * * 0 minute, hour, and wday (cross day)
In the schedule, store the number of seconds from the epoch when each run should occur rather than a date/time string.
Expanding a little:
#!/usr/bin/perl
use strict; use warnings;
use DateTime;
my $dt = DateTime->new(
year => 2010,
month => 3,
day => 14,
hour => 2,
minute => 0,
second => 0,
time_zone => 'America/Chicago',
);
print $dt->epoch, "\n";
gives me
Invalid local time for date in time zone: America/Chicago
because 2:00 am on March 14, 2010 is when the switch occurs. On the other hand, using hour => 3, I get: 1268553600. Now, in New York, I use:
C:\Temp> perl -e "print scalar localtime 1268553600"
Sun Mar 14 04:00:00 2010
So, the solution seems to be to avoid scheduling these events during non-existent times in your local time zone. This does not require elaborate logic: Just wrap the DateTime constructor call in an eval and deal with the exceptional time.
While I certainly think that there are likely "cleaner" solutions, would the following work?
set the cron to run the scripts several hours ahead of the possible range of times you actually want the script to run
handle the timezone detection in the script and have it sleep for the appropriate amount of time
Again, I know this is kinda kludgey but I thought I would put it out there.
Use the DateTime module to calculate times.
So if your setup says to run a script at 2:30 am every day, you will need logic to:
Try to create a DateTime object for 2:30am in timezone America\Los_Angeles.
If no object add 5 minutes to the time and try again. Give up after 2 hours offset.
Once you have a DateTime object, you can do comparisons with DateTime->now or extract an epoch time from your object and compare that with the results of time.
Note that I chose 2:30 am, since that time won't exist at least 1 day a year. That's why you need to have a loop that adds an offset.
The log4j logs I have contain timestamps in the following format:
2009-05-10 00:48:41,905
I need to convert it in perl to millseconds since epoch, which in this case would be 124189673005, using the following gawk function. How do I do it in perl?
I have little or no experience in perl, so appreciate if someone can post an entire script that does this
function log4jTimeStampToMillis(log4jts) {
# log4jts is of the form 2009-03-02 20:04:13,474
# extract milliseconds that is after the command
split(log4jts, tsparts, ",");
millis = tsparts[2];
# remove - : from tsstr
tsstr = tsparts[1];
gsub("[-:]", " ", tsstr);
seconds = mktime(tsstr);
print log4jts;
return seconds * 1000 + millis;
}
Though I almost always tell people to go use one of the many excellent modules from the CPAN for this, most of them do have one major drawback - speed. If you're parsing a large number of log files in real-time, that can sometimes be an issue. In those cases, rolling your own can often be a more suitable solution, but there are many pitfalls and nuances that must be considered and handled properly. Hence the preference for using a known-correct, proven, reliable module written by somebody else. :)
However, before I even considered my advice above, I looked at your code and had converted it to perl in my head... therefore, here is a more-or-less direct conversion of your gawk code into perl. I've tried to write it as simply as possible, so as to highlight some of the more delicate parts of dealing with dates and times in perl by hand.
# import the mktime function from the (standard) POSIX module
use POSIX qw( mktime );
sub log4jTimeStampToMillis {
my ($log4jts, $dst) = #_;
# extract the millisecond field
my ($tsstr, $millis) = split( ',', $log4jts );
# extract values to pass to mktime()
my #mktime_args = reverse split( '[-: ]', $tsstr );
# munge values for posix compatibility (ugh)
$mktime_args[3] -= 1;
$mktime_args[4] -= 1;
$mktime_args[5] -= 1900;
# print Dumper \#mktime_args; ## DEBUG
# convert, make sure to account for daylight savings
my $seconds = mktime( #mktime_args, 0, 0, $dst );
# return that time as milliseconds since the epoch
return $seconds * 1000 + $millis;
}
One important difference between my code and yours - my log4jTimeStampToMillis subroutine takes two parameters:
the log timestamp string
whether or not that timestamp is using daylight savings time ( 1 for true, 0 for false )
Of course, you could just add code to detect if that time falls in DST or not and adjust automatically, but I was trying to keep it simple. :)
NOTE: If you uncomment the line marked DEBUG, make sure to add "use Data::Dumper;" before that line in your program so it will work.
Here's an example of how you could test that subroutine:
my $milliseconds = log4jTimeStampToMillis( "2009-05-10 00:48:41,905", 1 );
my $seconds = int( $milliseconds / 1000 );
my $local = scalar localtime( $seconds );
print "ms: $milliseconds\n"; # ms: 1241844521905
print "sec: $seconds\n"; # sec: 1241844521
print "local: $local\n"; # local: Sat May 9 00:48:41 2009
You should take advantage of the great DateTime package, specifically use DateTime::Format::Strptime:
use DateTime;
use DateTime::Format::Strptime;
sub log4jTimeStampToMillis {
my $log4jts=shift(#_);
#see package docs for how the pattern parameter works
my $formatter= new DateTime::Format::Strptime(pattern => '%Y-%m-%d %T,%3N');
my $dayObj = $formatter->parse_datetime($log4jts);
return $dayObj->epoch()*1000+$dayObj->millisecond();
}
print log4jTimeStampToMillis('2009-05-10 10:48:41,905')."\n";
#prints my local version of the TS: 1241952521905
This saves you the pain of figuring out DST yourself (although you'll have to pass your server's TZ to Strptime via the time_zone parameter). It also saves you from dealing with leap everything if it becomes relevant (and I'm sure it will).
Haven't used it, but you might want to check out Time::ParseDate.
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS");
Date time = dateFormat.parse(log4jts);
long millis = time.getTime();