File Coverage

blib/lib/RRD/Simple.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             ############################################################
2             #
3             # $Id: Simple.pm 1100 2008-01-24 17:39:35Z nicolaw $
4             # RRD::Simple - Simple interface to create and store data in RRD files
5             #
6             # Copyright 2005,2006,2007,2008 Nicola Worthington
7             #
8             # Licensed under the Apache License, Version 2.0 (the "License");
9             # you may not use this file except in compliance with the License.
10             # You may obtain a copy of the License at
11             #
12             # http://www.apache.org/licenses/LICENSE-2.0
13             #
14             # Unless required by applicable law or agreed to in writing, software
15             # distributed under the License is distributed on an "AS IS" BASIS,
16             # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17             # See the License for the specific language governing permissions and
18             # limitations under the License.
19             #
20             ############################################################
21              
22             package RRD::Simple;
23             # vim:ts=8:sw=8:tw=78
24              
25 1     1   17660 use strict;
  1         3  
  1         46  
26             require Exporter;
27 1     1   406 use RRDs;
  0            
  0            
28             use POSIX qw(strftime); # Used for strftime in graph() method
29             use Carp qw(croak cluck confess carp);
30             use File::Spec qw(); # catfile catdir updir path rootdir tmpdir
31             use File::Basename qw(fileparse dirname basename);
32              
33             use vars qw($VERSION $DEBUG $DEFAULT_DSTYPE
34             @EXPORT @EXPORT_OK %EXPORT_TAGS @ISA);
35              
36             $VERSION = '1.44' || sprintf('%d', q$Revision: 1100 $ =~ /(\d+)/g);
37              
38             @ISA = qw(Exporter);
39             @EXPORT = qw();
40             @EXPORT_OK = qw(create update last_update graph info rename_source
41             add_source sources retention_period last_values
42             heartbeat);
43             # delete_source minimum maximum
44             %EXPORT_TAGS = (all => \@EXPORT_OK);
45              
46             $DEBUG ||= $ENV{DEBUG} ? 1 : 0;
47             $DEFAULT_DSTYPE ||= exists $ENV{DEFAULT_DSTYPE}
48             ? $ENV{DEFAULT_DSTYPE} : 'GAUGE';
49              
50             my $objstore = {};
51              
52              
53              
54             #
55             # Methods
56             #
57              
58             # Create a new object
59             sub new {
60             TRACE(">>> new()");
61             ref(my $class = shift) && croak 'Class name required';
62             croak 'Odd number of elements passed when even was expected' if @_ % 2;
63              
64             # Conjure up an invisible object
65             my $self = bless \(my $dummy), $class;
66             $objstore->{_refaddr($self)} = {@_};
67             my $stor = $objstore->{_refaddr($self)};
68             #my $self = { @_ };
69              
70             # - Added "file" support in 1.42 - see sub _guess_filename.
71             # - Added "on_missing_ds"/"on_missing_source" support in 1.44
72             # - Added "tmpdir" support in 1.44
73             my @validkeys = qw(rrdtool cf default_dstype default_dst tmpdir
74             file on_missing_ds on_missing_source);
75             my $validkeys = join('|', @validkeys);
76              
77             cluck('Unrecognised parameters passed: '.
78             join(', ',grep(!/^$validkeys$/,keys %{$stor})))
79             if (grep(!/^$validkeys$/,keys %{$stor}) && $^W);
80              
81             $stor->{rrdtool} = _find_binary(exists $stor->{rrdtool} ?
82             $stor->{rrdtool} : 'rrdtool');
83              
84             # Check that "default_dstype" isn't complete rubbish (validation from v1.44+)
85             # GAUGE | COUNTER | DERIVE | ABSOLUTE | COMPUTE
86             # http://oss.oetiker.ch/rrdtool/doc/rrdcreate.en.html
87             $stor->{default_dstype} ||= $stor->{default_dst};
88             croak "Invalid value passed in parameter default_dstype; '$stor->{default_dstype}'"
89             if defined $stor->{default_dstype}
90             && $stor->{default_dstype} !~ /^(GAUGE|COUNTER|DERIVE|ABSOLUTE|COMPUTE|[A-Z]{1,10})$/i;
91              
92             # Check that "on_missing_ds" isn't complete rubbish.
93             # Added "on_missing_ds"/"on_missing_source" support in 1.44
94             $stor->{on_missing_ds} ||= $stor->{on_missing_source};
95             if (defined $stor->{on_missing_ds}) {
96             $stor->{on_missing_ds} = lc($stor->{on_missing_ds});
97             croak "Invalid value passed in parameter on_missing_ds; '$stor->{on_missing_ds}'"
98             if $stor->{on_missing_ds} !~ /^\s*(add|ignore|die|croak)\s*$/i;
99             }
100             $stor->{on_missing_ds} ||= 'add'; # default to add
101              
102             #$stor->{cf} ||= [ qw(AVERAGE MIN MAX LAST) ];
103             # By default, now only create RRAs for AVERAGE and MAX, like
104             # mrtg v2.13.2. This is to save disk space and processing time
105             # during updates etc.
106             $stor->{cf} ||= [ qw(AVERAGE MAX) ];
107             $stor->{cf} = [ $stor->{cf} ] if !ref($stor->{cf});
108              
109             DUMP($class,$self);
110             DUMP('$stor',$stor);
111             return $self;
112             }
113              
114              
115             # Create a new RRD file
116             sub create {
117             TRACE(">>> create()");
118             my $self = shift;
119             unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) {
120             unshift @_, $self unless $self eq __PACKAGE__;
121             $self = new __PACKAGE__;
122             }
123              
124             my $stor = $objstore->{_refaddr($self)};
125              
126             #
127             #
128             #
129              
130             # Grab or guess the filename
131             my $rrdfile = $stor->{file};
132              
133             # Odd number of values and first is not a valid scheme
134             # then the first value is likely an RRD file name.
135             if (@_ % 2 && !_valid_scheme($_[0])) {
136             $rrdfile = shift;
137              
138             # Even number of values and the second value is a valid
139             # scheme then the first value is likely an RRD file name.
140             } elsif (!(@_ % 2) && _valid_scheme($_[1])) {
141             $rrdfile = shift;
142              
143             # If we still don't have an RRD file name then try and
144             # guess what it is
145             } elsif (!defined $rrdfile) {
146             $rrdfile = _guess_filename($stor);
147             }
148              
149             #
150             #
151             #
152              
153             # Barf if the rrd file already exists
154             croak "RRD file '$rrdfile' already exists" if -f $rrdfile;
155             TRACE("Using filename: $rrdfile");
156              
157             # We've been given a scheme specifier
158             # Until v1.32 'year' was the default. As of v1.33 'mrtg'
159             # is the new default scheme.
160             #my $scheme = 'year';
161             my $scheme = 'mrtg';
162             if (@_ % 2 && _valid_scheme($_[0])) {
163             $scheme = _valid_scheme($_[0]);
164             shift @_;
165             }
166             TRACE("Using scheme: $scheme");
167              
168             croak 'Odd number of elements passed when even was expected' if @_ % 2;
169             my %ds = @_;
170             DUMP('%ds',\%ds);
171              
172             my $rrdDef = _rrd_def($scheme);
173             my @def = ('-b', time - _seconds_in($scheme,120));
174             push @def, '-s', ($rrdDef->{step} || 300);
175              
176             # Add data sources
177             for my $ds (sort keys %ds) {
178             $ds =~ s/[^a-zA-Z0-9_-]//g;
179             push @def, sprintf('DS:%s:%s:%s:%s:%s',
180             substr($ds,0,19),
181             uc($ds{$ds}),
182             ($rrdDef->{heartbeat} || 600),
183             'U','U'
184             );
185             }
186              
187             # Add RRA definitions
188             my %cf;
189             for my $cf (@{$stor->{cf}}) {
190             $cf{$cf} = $rrdDef->{rra};
191             }
192             for my $cf (sort keys %cf) {
193             for my $rra (@{$cf{$cf}}) {
194             push @def, sprintf('RRA:%s:%s:%s:%s',
195             $cf, 0.5, $rra->{step}, $rra->{rows}
196             );
197             }
198             }
199              
200             DUMP('@def',\@def);
201              
202             # Pass to RRDs for execution
203             my @rtn = RRDs::create($rrdfile, @def);
204             my $error = RRDs::error();
205             croak($error) if $error;
206             DUMP('RRDs::info',RRDs::info($rrdfile));
207             return wantarray ? @rtn : \@rtn;
208             }
209              
210              
211             # Update an RRD file with some data values
212             sub update {
213             TRACE(">>> update()");
214             my $self = shift;
215             unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) {
216             unshift @_, $self unless $self eq __PACKAGE__;
217             $self = new __PACKAGE__;
218             }
219              
220             my $stor = $objstore->{_refaddr($self)};
221              
222             #
223             #
224             #
225              
226             # Grab or guess the filename
227             my $rrdfile = $stor->{file};
228              
229             # Odd number of values and first is does not look
230             # like a recent unix time stamp then the first value
231             # is likely to be an RRD file name.
232             if (@_ % 2 && $_[0] !~ /^[1-9][0-9]{8,10}$/i) {
233             $rrdfile = shift;
234              
235             # Even number of values and the second value looks like
236             # a recent unix time stamp then the first value is
237             # likely to be an RRD file name.
238             } elsif (!(@_ % 2) && $_[1] =~ /^[1-9][0-9]{8,10}$/i) {
239             $rrdfile = shift;
240              
241             # If we still don't have an RRD file name then try and
242             # guess what it is
243             } elsif (!defined $rrdfile) {
244             $rrdfile = _guess_filename($stor);
245             }
246              
247             #
248             #
249             #
250              
251             # We've been given an update timestamp
252             my $time = time();
253             if (@_ % 2 && $_[0] =~ /^([1-9][0-9]{8,10})$/i) {
254             $time = $1;
255             shift @_;
256             }
257             TRACE("Using update time: $time");
258              
259             # Try to automatically create it
260             unless (-f $rrdfile) {
261             my $default_dstype = defined $stor->{default_dstype} ? $stor->{default_dstype} : $DEFAULT_DSTYPE;
262             cluck("RRD file '$rrdfile' does not exist; attempting to create it ",
263             "using default DS type of '$default_dstype'") if $^W;
264             my @args;
265             for (my $i = 0; $i < @_; $i++) {
266             push @args, ($_[$i],$default_dstype) unless $i % 2;
267             }
268             $self->create($rrdfile,@args);
269             }
270              
271             croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile;
272             TRACE("Using filename: $rrdfile");
273              
274             croak 'Odd number of elements passed when even was expected' if @_ % 2;
275              
276             my %ds;
277             while (my $ds = shift(@_)) {
278             $ds =~ s/[^a-zA-Z0-9_-]//g;
279             $ds = substr($ds,0,19);
280             $ds{$ds} = shift(@_);
281             $ds{$ds} = 'U' if !defined($ds{$ds});
282             }
283             DUMP('%ds',\%ds);
284              
285             # Validate the data source names as we add them
286             my @sources = $self->sources($rrdfile);
287             for my $ds (sort keys %ds) {
288             # Check the data source names
289             if (!grep(/^$ds$/,@sources)) {
290             TRACE("Supplied data source '$ds' does not exist in pre-existing ".
291             "RRD data source list: ". join(', ',@sources));
292              
293             # If someone got the case wrong, remind and correct them
294             if (grep(/^$ds$/i,@sources)) {
295             cluck("Data source '$ds' does not exist; automatically ",
296             "correcting it to '",(grep(/^$ds$/i,@sources))[0],
297             "' instead") if $^W;
298             $ds{(grep(/^$ds$/i,@sources))[0]} = $ds{$ds};
299             delete $ds{$ds};
300              
301             # If it's not just a case sensitivity typo and the data source
302             # name really doesn't exist in this RRD file at all, regardless
303             # of case, then ...
304             } else {
305             # Ignore the offending missing data source name
306             if ($stor->{on_missing_ds} eq 'ignore') {
307             TRACE("on_missing_ds = ignore; ignoring data supplied for missing data source '$ds'");
308              
309             # Fall on our bum and die horribly if requested to do so
310             } elsif ($stor->{on_missing_ds} eq 'die' || $stor->{on_missing_ds} eq 'croak') {
311             croak "Supplied data source '$ds' does not exist in RRD file '$rrdfile'";
312              
313             # Default behaviour is to automatically add the new data source
314             # to the RRD file in order to preserve the existing default
315             # functionality of RRD::Simple
316             } else {
317             TRACE("on_missing_ds = add (or not set at all/default); ".
318             "automatically adding new data source '$ds'");
319              
320             # Otherwise add any missing or new data sources on the fly
321             # Decide what DS type and heartbeat to use
322             my $info = RRDs::info($rrdfile);
323             my $error = RRDs::error();
324             croak($error) if $error;
325              
326             my %dsTypes;
327             for my $key (grep(/^ds\[.+?\]\.type$/,keys %{$info})) {
328             $dsTypes{$info->{$key}}++;
329             }
330             DUMP('%dsTypes',\%dsTypes);
331             my $dstype = (sort { $dsTypes{$b} <=> $dsTypes{$a} }
332             keys %dsTypes)[0];
333             TRACE("\$dstype = $dstype");
334              
335             $self->add_source($rrdfile,$ds,$dstype);
336             }
337             }
338             }
339             }
340              
341             # Build the def
342             my @def = ('--template');
343             push @def, join(':',sort keys %ds);
344             push @def, join(':',$time,map { $ds{$_} } sort keys %ds);
345             DUMP('@def',\@def);
346              
347             # Pass to RRDs to execute the update
348             my @rtn = RRDs::update($rrdfile, @def);
349             my $error = RRDs::error();
350             croak($error) if $error;
351             return wantarray ? @rtn : \@rtn;
352             }
353              
354              
355             # Get the last time an RRD was updates
356             sub last_update { __PACKAGE__->last(@_); }
357             sub last {
358             TRACE(">>> last()");
359             my $self = shift;
360             unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) {
361             unshift @_, $self unless $self eq __PACKAGE__;
362             $self = new __PACKAGE__;
363             }
364              
365             my $stor = $objstore->{_refaddr($self)};
366             my $rrdfile = shift || _guess_filename($stor);
367             croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile;
368             TRACE("Using filename: $rrdfile");
369              
370             my $last = RRDs::last($rrdfile);
371             my $error = RRDs::error();
372             croak($error) if $error;
373             return $last;
374             }
375              
376              
377             # Get a list of data sources from an RRD file
378             sub sources {
379             TRACE(">>> sources()");
380             my $self = shift;
381             unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) {
382             unshift @_, $self unless $self eq __PACKAGE__;
383             $self = new __PACKAGE__;
384             }
385              
386             my $stor = $objstore->{_refaddr($self)};
387             my $rrdfile = shift || _guess_filename($stor);
388             croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile;
389             TRACE("Using filename: $rrdfile");
390              
391             my $info = RRDs::info($rrdfile);
392             my $error = RRDs::error();
393             croak($error) if $error;
394              
395             my @ds;
396             foreach (keys %{$info}) {
397             if (/^ds\[(.+)?\]\.type$/) {
398             push @ds, $1;
399             }
400             }
401             return wantarray ? @ds : \@ds;
402             }
403              
404              
405             # Add a new data source to an RRD file
406             sub add_source {
407             TRACE(">>> add_source()");
408             my $self = shift;
409             unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) {
410             unshift @_, $self unless $self eq __PACKAGE__;
411             $self = new __PACKAGE__;
412             }
413              
414             # Grab or guess the filename
415             my $stor = $objstore->{_refaddr($self)};
416             my $rrdfile = @_ % 2 ? shift : _guess_filename($stor);
417             unless (-f $rrdfile) {
418             cluck("RRD file '$rrdfile' does not exist; attempting to create it")
419             if $^W;
420             return $self->create($rrdfile,@_);
421             }
422             croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile;
423             TRACE("Using filename: $rrdfile");
424              
425             # Check that we will understand this RRD file version first
426             my $info = $self->info($rrdfile);
427             # croak "Unable to add a new data source to $rrdfile; ",
428             # "RRD version $info->{rrd_version} is too new"
429             # if ($info->{rrd_version}+1-1) > 1;
430              
431             my ($ds,$dstype) = @_;
432             TRACE("\$ds = $ds");
433             TRACE("\$dstype = $dstype");
434              
435             my $rrdfileBackup = "$rrdfile.bak";
436             confess "$rrdfileBackup already exists; please investigate"
437             if -e $rrdfileBackup;
438              
439             # Decide what heartbeat to use
440             my $heartbeat = $info->{ds}->{(sort {
441             $info->{ds}->{$b}->{minimal_heartbeat} <=>
442             $info->{ds}->{$b}->{minimal_heartbeat}
443             } keys %{$info->{ds}})[0]}->{minimal_heartbeat};
444             TRACE("\$heartbeat = $heartbeat");
445              
446             # Make a list of expected sources after the addition
447             my $TgtSources = join(',',sort(($self->sources($rrdfile),$ds)));
448              
449             # Add the data source
450             my $new_rrdfile = '';
451             eval {
452             $new_rrdfile = _modify_source(
453             $rrdfile,$stor,$ds,
454             'add',$dstype,$heartbeat,
455             );
456             };
457              
458             # Barf if the eval{} got upset
459             if ($@) {
460             croak "Failed to add new data source '$ds' to RRD file '$rrdfile': $@";
461             }
462              
463             # Barf of the new RRD file doesn't exist
464             unless (-f $new_rrdfile) {
465             croak "Failed to add new data source '$ds' to RRD file '$rrdfile': ",
466             "new RRD file '$new_rrdfile' does not exist";
467             }
468              
469             # Barf is the new data source isn't in our new RRD file
470             unless ($TgtSources eq join(',',sort($self->sources($new_rrdfile)))) {
471             croak "Failed to add new data source '$ds' to RRD file '$rrdfile': ",
472             "new RRD file '$new_rrdfile' does not contain expected data ",
473             "source names";
474             }
475              
476             # Try and move the new RRD file in to place over the existing one
477             # and then remove the backup RRD file if sucessfull
478             if (File::Copy::move($rrdfile,$rrdfileBackup) &&
479             File::Copy::move($new_rrdfile,$rrdfile)) {
480             unless (unlink($rrdfileBackup)) {
481             cluck("Failed to remove back RRD file '$rrdfileBackup': $!")
482             if $^W;
483             }
484             } else {
485             croak "Failed to move new RRD file in to place: $!";
486             }
487             }
488              
489              
490             # Make a number of graphs for an RRD file
491             sub graph {
492             TRACE(">>> graph()");
493             my $self = shift;
494             unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) {
495             unshift @_, $self unless $self eq __PACKAGE__;
496             $self = new __PACKAGE__;
497             }
498              
499             # Grab or guess the filename
500             my $stor = $objstore->{_refaddr($self)};
501             my $rrdfile = @_ % 2 ? shift : _guess_filename($stor);
502              
503             # How much data do we have to graph?
504             my $period = $self->retention_period($rrdfile);
505              
506             # Check at RRA CFs are available and graph the best one
507             my $info = $self->info($rrdfile);
508             my $cf = 'AVERAGE';
509             for my $rra (@{$info->{rra}}) {
510             if ($rra->{cf} eq 'AVERAGE') {
511             $cf = 'AVERAGE'; last;
512             } elsif ($rra->{cf} eq 'MAX') {
513             $cf = 'MAX';
514             } elsif ($rra->{cf} eq 'MIN' && $cf ne 'MAX') {
515             $cf = 'MIN';
516             } elsif ($cf ne 'MAX' && $cf ne 'MIN') {
517             $cf = $rra->{cf};
518             }
519             }
520             TRACE("graph() - \$cf = $cf");
521              
522             # Create graphs which we have enough data to populate
523             # Version 1.39 - Change the return from an array to a hash (semi backward compatible)
524             # my @rtn;
525             my %rtn;
526              
527             ##
528             ## TODO
529             ## 1.45 Only generate hour, 6hour and 12hour graphs if the
530             ### data resolution (stepping) is fine enough (sub minute)
531             ##
532              
533             #i my @graph_periods = qw(hour 6hour 12hour day week month year 3years);
534             my @graph_periods;
535             my %param = @_;
536             if (defined $param{'periods'}) {
537             my %map = qw(daily day weekly week monthly month annual year 3years 3years);
538             for my $period (_convert_to_array($param{'periods'})) {
539             $period = lc($period);
540             if (_valid_scheme($period)) {
541             push @graph_periods, $period;
542             } elsif (_valid_scheme($map{$period})) {
543             push @graph_periods, $map{$period};
544             } else {
545             croak "Invalid period value passed in parameter periods; '$period'";
546             }
547             }
548             }
549             push @graph_periods, qw(day week month year 3years) unless @graph_periods;
550              
551             for my $type (@graph_periods) {
552             next if $period < _seconds_in($type);
553             TRACE("graph() - \$type = $type");
554             # push @rtn, [ ($self->_create_graph($rrdfile, $type, $cf, @_)) ];
555             $rtn{_alt_graph_name($type)} = [ ($self->_create_graph($rrdfile, $type, $cf, @_)) ];
556             }
557              
558             # return @rtn;
559             return wantarray ? %rtn : \%rtn;
560             }
561              
562              
563             # Rename an existing data source
564             sub rename_source {
565             TRACE(">>> rename_source()");
566             my $self = shift;
567             unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) {
568             unshift @_, $self unless $self eq __PACKAGE__;
569             $self = new __PACKAGE__;
570             }
571              
572             # Grab or guess the filename
573             my $stor = $objstore->{_refaddr($self)};
574             my $rrdfile = @_ % 2 ? shift : _guess_filename($stor);
575             croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile;
576             TRACE("Using filename: $rrdfile");
577              
578             my ($old,$new) = @_;
579             croak "No old data source name specified" unless defined $old && length($old);
580             croak "No new data source name specified" unless defined $new && length($new);
581             croak "Data source '$old' does not exist in RRD file '$rrdfile'"
582             unless grep($_ eq $old, $self->sources($rrdfile));
583              
584             my @rtn = RRDs::tune($rrdfile,'-r',"$old:$new");
585             my $error = RRDs::error();
586             croak($error) if $error;
587             return wantarray ? @rtn : \@rtn;
588             }
589              
590              
591             # Get or set a data source heartbeat
592             sub heartbeat {
593             TRACE(">>> heartbeat()");
594             my $self = shift;
595             unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) {
596             unshift @_, $self unless $self eq __PACKAGE__;
597             $self = new __PACKAGE__;
598             }
599              
600             # Grab or guess the filename
601             my $stor = $objstore->{_refaddr($self)};
602             my $rrdfile = @_ >= 3 ? shift :
603             _isLegalDsName($_[0]) && $_[1] =~ /^[0-9]+$/ ?
604             _guess_filename($stor) : shift;
605             croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile;
606             TRACE("Using filename: $rrdfile");
607              
608             # Explode if we get no data source name
609             my ($ds,$new_heartbeat) = @_;
610             croak "No data source name was specified" unless defined $ds && length($ds);
611              
612             # Check the data source name exists
613             my $info = $self->info($rrdfile);
614             my $heartbeat = $info->{ds}->{$ds}->{minimal_heartbeat};
615             croak "Data source '$ds' does not exist in RRD file '$rrdfile'"
616             unless defined $heartbeat && $heartbeat;
617              
618             if (!defined $new_heartbeat) {
619             return wantarray ? ($heartbeat) : $heartbeat;
620             }
621              
622             my @rtn = !defined $new_heartbeat ? ($heartbeat) : ();
623             # Redefine the data source heartbeat
624             if (defined $new_heartbeat) {
625             croak "New minimal heartbeat '$new_heartbeat' is not a valid positive integer"
626             unless $new_heartbeat =~ /^[1-9][0-9]*$/;
627             my @rtn = RRDs::tune($rrdfile,'-h',"$ds:$new_heartbeat");
628             my $error = RRDs::error();
629             croak($error) if $error;
630             }
631              
632             return wantarray ? @rtn : \@rtn;
633             }
634              
635              
636             # Fetch data point information from an RRD file
637             sub fetch {
638             TRACE(">>> fetch()");
639             my $self = shift;
640             unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) {
641             unshift @_, $self unless $self eq __PACKAGE__;
642             $self = new __PACKAGE__;
643             }
644              
645             # Grab or guess the filename
646             my $stor = $objstore->{_refaddr($self)};
647             my $rrdfile = @_ % 2 ? shift : _guess_filename($stor);
648              
649             }
650              
651              
652             # Fetch the last values inserted in to an RRD file
653             sub last_values {
654             TRACE(">>> last_values()");
655             my $self = shift;
656             unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) {
657             unshift @_, $self unless $self eq __PACKAGE__;
658             $self = new __PACKAGE__;
659             }
660              
661             # Grab or guess the filename
662             my $stor = $objstore->{_refaddr($self)};
663             my $rrdfile = @_ % 2 ? shift : _guess_filename($stor);
664              
665             # When was the RRD last updated?
666             my $lastUpdated = $self->last($rrdfile);
667              
668             # Is there a LAST RRA?
669             my $info = $self->info($rrdfile);
670             my $hasLastRRA = 0;
671             for my $rra (@{$info->{rra}}) {
672             $hasLastRRA++ if $rra->{cf} eq 'LAST';
673             }
674             return if !$hasLastRRA;
675              
676             # What's the largest heartbeat in the RRD file data sources?
677             my $largestHeartbeat = 1;
678             for (map { $info->{ds}->{$_}->{'minimal_heartbeat'} } keys(%{$info->{ds}})) {
679             $largestHeartbeat = $_ if $_ > $largestHeartbeat;
680             }
681              
682             my @def = ('LAST',
683             '-s', $lastUpdated - ($largestHeartbeat * 2),
684             '-e', $lastUpdated
685             );
686              
687             # Pass to RRDs to execute
688             my ($time,$heartbeat,$ds,$data) = RRDs::fetch($rrdfile, @def);
689             my $error = RRDs::error();
690             croak($error) if $error;
691              
692             # Put it in to a nice easy format
693             my %rtn = ();
694             for my $rec (reverse @{$data}) {
695             for (my $i = 0; $i < @{$rec}; $i++) {
696             if (defined $rec->[$i] && !exists($rtn{$ds->[$i]})) {
697             $rtn{$ds->[$i]} = $rec->[$i];
698             }
699             }
700             }
701              
702             # Well, I'll be buggered if the LAST CF does what you'd think
703             # it's meant to do. If anybody can give me some decent documentation
704             # on what the LAST CF does, and/or how to get the last value put
705             # in to an RRD, then I'll admit that this method exists and export
706             # it too.
707              
708             return wantarray ? %rtn : \%rtn;
709             }
710              
711              
712             # Return how long this RRD retains data for
713             sub retention_period {
714             TRACE(">>> retention_period()");
715             my $self = shift;
716             unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) {
717             unshift @_, $self unless $self eq __PACKAGE__;
718             $self = new __PACKAGE__;
719             }
720              
721             my $info = $self->info(@_);
722             return if !defined($info);
723              
724             my $duration = $info->{step};
725             for my $rra (@{$info->{rra}}) {
726             my $secs = ($rra->{pdp_per_row} * $info->{step}) * $rra->{rows};
727             $duration = $secs if $secs > $duration;
728             }
729              
730             return wantarray ? ($duration) : $duration;
731             }
732              
733              
734             # Fetch information about an RRD file
735             sub info {
736             TRACE(">>> info()");
737             my $self = shift;
738             unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) {
739             unshift @_, $self unless $self eq __PACKAGE__;
740             $self = new __PACKAGE__;
741             }
742              
743             # Grab or guess the filename
744             my $stor = $objstore->{_refaddr($self)};
745             my $rrdfile = @_ % 2 ? shift : _guess_filename($stor);
746              
747             my $info = RRDs::info($rrdfile);
748             my $error = RRDs::error();
749             croak($error) if $error;
750             DUMP('$info',$info);
751              
752             my $rtn;
753             for my $key (sort(keys(%{$info}))) {
754             if ($key =~ /^rra\[(\d+)\]\.([a-z_]+)/) {
755             $rtn->{rra}->[$1]->{$2} = $info->{$key};
756             } elsif (my (@dsKey) = $key =~ /^ds\[([[A-Za-z0-9\_]+)?\]\.([a-z_]+)/) {
757             $rtn->{ds}->{$1}->{$2} = $info->{$key};
758             } elsif ($key !~ /\[[\d_a-z]+\]/i) {
759             $rtn->{$key} = $info->{$key};
760             }
761             }
762              
763             # Return the information
764             DUMP('$rtn',$rtn);
765             return $rtn;
766             }
767              
768              
769             # Convert a string or an array reference to an array
770             sub _convert_to_array {
771             return unless defined $_[0];
772             if (!ref $_[0]) {
773             $_[0] =~ /^\s+|\s+$/g;
774             return split(/(?:\s+|\s*,\s*)/,$_[0]);
775             } elsif (ref($_[0]) eq 'ARRAY') {
776             return @{$_[0]};
777             }
778             return;
779             }
780              
781              
782             # Make a single graph image
783             sub _create_graph {
784             TRACE(">>> _create_graph()");
785             my $self = shift;
786             my $rrdfile = shift;
787             my $type = _valid_scheme(shift) || 'day';
788             my $cf = shift || 'AVERAGE';
789              
790             my $command_regex = qr/^([VC]?DEF|G?PRINT|COMMENT|[HV]RULE\d*|LINE\d*|AREA|TICK|SHIFT|STACK):.+/;
791             $command_regex = qr/^([VC]?DEF|G?PRINT|COMMENT|[HV]RULE\d*|LINE\d*|AREA|TICK|SHIFT|STACK|TEXTALIGN):.+/
792             if $RRDs::VERSION >= 1.3; # http://oss.oetiker.ch/rrdtool-trac/wiki/RRDtool13
793              
794             my %param;
795             my @command_param;
796             while (my $k = shift) {
797             if ($k =~ /$command_regex/) {
798             push @command_param, $k;
799             shift;
800             } else {
801             $k =~ s/_/-/g;
802             $param{lc($k)} = shift;
803             }
804             }
805              
806             # If we get this custom parameter then it would have already
807             # been dealt with by the calling graph() method so we should
808             # ditch it right here and now!
809             delete $param{'periods'};
810              
811             # Specify some default values
812             $param{'end'} ||= $self->last($rrdfile) || time();
813             $param{'imgformat'} ||= 'PNG'; # RRDs >1.3 now support PDF, SVG and EPS
814             # $param{'alt-autoscale'} ||= '';
815             # $param{'alt-y-grid'} ||= '';
816              
817             # Define what to call the image
818             my $basename = defined $param{'basename'} &&
819             $param{'basename'} =~ /^[0-9a-z_\.-]+$/i ?
820             $param{'basename'} :
821             (fileparse($rrdfile,'\.[^\.]+'))[0];
822             delete $param{'basename'};
823              
824             # Define where to write the image
825             my $image = sprintf('%s-%s.%s',$basename,
826             _alt_graph_name($type), lc($param{'imgformat'}));
827             if ($param{'destination'}) {
828             $image = File::Spec->catfile($param{'destination'},$image);
829             }
830             delete $param{'destination'};
831              
832             # Specify timestamps- new for version 1.41
833             my $timestamp = !defined $param{'timestamp'} ||
834             $param{'timestamp'} !~ /^(graph|rrd|both|none)$/i
835             ? 'graph'
836             : lc($param{'timestamp'});
837             delete $param{'timestamp'};
838              
839             # Specify extended legend - new for version 1.35
840             my $extended_legend = defined $param{'extended-legend'} &&
841             $param{'extended-legend'} ? 1 : 0;
842             delete $param{'extended-legend'};
843              
844             # Define how thick the graph lines should be
845             my $line_thickness = defined $param{'line-thickness'} &&
846             $param{'line-thickness'} =~ /^[123]$/ ?
847             $param{'line-thickness'} : 1;
848             delete $param{'line-thickness'};
849              
850             # Colours is an alias to colors
851             if (exists $param{'source-colours'} && !exists $param{'source-colors'}) {
852             $param{'source-colors'} = $param{'source-colours'};
853             delete $param{'source-colours'};
854             }
855              
856             # Allow source line colors to be set
857             my @source_colors = ();
858             my %source_colors = ();
859             if (defined $param{'source-colors'}) {
860             #if (ref($param{'source-colors'}) eq 'ARRAY') {
861             # @source_colors = @{$param{'source-colors'}};
862             if (ref($param{'source-colors'}) eq 'HASH') {
863             %source_colors = %{$param{'source-colors'}};
864             } else {
865             @source_colors = _convert_to_array($param{'source-colors'});
866             }
867             }
868             delete $param{'source-colors'};
869              
870             # Define which data sources we should plot
871             my @rrd_sources = $self->sources($rrdfile);
872             my @ds = !exists $param{'sources'}
873             ? @rrd_sources
874             #: defined $param{'sources'} && ref($param{'sources'}) eq 'ARRAY'
875             #? @{$param{'sources'}}
876             : defined $param{'sources'}
877             ? _convert_to_array($param{'sources'})
878             : ();
879              
880             # Allow source legend source_labels to be set
881             my %source_labels = ();
882             if (defined $param{'source-labels'}) {
883             if (ref($param{'source-labels'}) eq 'HASH') {
884             %source_labels = %{$param{'source-labels'}};
885             } elsif (ref($param{'source-labels'}) eq 'ARRAY') {
886             if (defined $param{'sources'} && ref($param{'sources'}) eq 'ARRAY') {
887             for (my $i = 0; $i < @{$param{'source-labels'}}; $i++) {
888             $source_labels{$ds[$i]} = $param{'source-labels'}->[$i]
889             if defined $ds[$i];
890             }
891             } elsif ($^W) {
892             carp "source_labels may only be an array if sources is also ".
893             "an specified and valid array";
894             }
895             }
896             }
897             delete $param{'source-labels'};
898              
899             # Allow source legend source_drawtypes to be set
900             # ... "oops" ... yes, this is quite obviously
901             # copy and paste code from the chunk above. I'm
902             # sorry. I'll rationalise it some other day if
903             # it's necessary.
904             my %source_drawtypes = ();
905             if (defined $param{'source-drawtypes'}) {
906             if (ref($param{'source-drawtypes'}) eq 'HASH') {
907             %source_drawtypes = %{$param{'source-drawtypes'}};
908             } elsif (ref($param{'source-drawtypes'}) eq 'ARRAY') {
909             if (defined $param{'sources'} && ref($param{'sources'}) eq 'ARRAY') {
910             for (my $i = 0; $i < @{$param{'source-drawtypes'}}; $i++) {
911             $source_drawtypes{$ds[$i]} = $param{'source-drawtypes'}->[$i]
912             if defined $ds[$i];
913             }
914             } elsif ($^W) {
915             carp "source_drawtypes may only be an array if sources is ".
916             "also an specified and valid array"
917             }
918             }
919              
920             # Validate the values we have and set default thickness
921             while (my ($k,$v) = each %source_drawtypes) {
922             if ($v !~ /^(LINE[1-9]?|STACK|AREA)$/) {
923             delete $source_drawtypes{$k};
924             carp "source_drawtypes may be LINE, LINEn, AREA or STACK ".
925             "only; value '$v' is not valid" if $^W;
926             }
927             $source_drawtypes{$k} = uc($v);
928             $source_drawtypes{$k} .= $line_thickness if $v eq 'LINE';
929             }
930             }
931             delete $param{'source-drawtypes'};
932             delete $param{'sources'};
933              
934             # Specify a default start time
935             $param{'start'} ||= $param{'end'} - _seconds_in($type,115);
936              
937             # Suffix the title with the period information
938             $param{'title'} ||= basename($rrdfile);
939             $param{'title'} .= ' - [Hourly Graph]' if $type eq 'hour';
940             $param{'title'} .= ' - [6 Hour Graph]' if $type eq '6hour' || $type eq 'quarterday';
941             $param{'title'} .= ' - [12 Hour Graph]' if $type eq '12hour' || $type eq 'halfday';
942             $param{'title'} .= ' - [Daily Graph]' if $type eq 'day';
943             $param{'title'} .= ' - [Weekly Graph]' if $type eq 'week';
944             $param{'title'} .= ' - [Monthly Graph]' if $type eq 'month';
945             $param{'title'} .= ' - [Annual Graph]' if $type eq 'year';
946             $param{'title'} .= ' - [3 Year Graph]' if $type eq '3years';
947              
948             # Convert our parameters in to an RRDs friendly defenition
949             my @def;
950             while (my ($k,$v) = each %param) {
951             if (length($k) == 1) { # Short single character options
952             $k = '-'.uc($k);
953             } else { # Long options
954             $k = "--$k";
955             }
956             for my $v ((ref($v) eq 'ARRAY' ? @{$v} : ($v))) {
957             if (!defined $v || !length($v)) {
958             push @def, $k;
959             } else {
960             push @def, "$k=$v";
961             }
962             }
963             }
964              
965             # Populate a cycling tied scalar for line colors
966             @source_colors = qw(
967             FF0000 00FF00 0000FF 00FFFF FF00FF FFFF00 000000
968             990000 009900 000099 009999 990099 999900 999999
969             552222 225522 222255 225555 552255 555522 555555
970             ) unless @source_colors > 0;
971             # Pre 1.35 colours
972             # FF0000 00FF00 0000FF FFFF00 00FFFF FF00FF 000000
973             # 550000 005500 000055 555500 005555 550055 555555
974             # AA0000 00AA00 0000AA AAAA00 00AAAA AA00AA AAAAAA
975             tie my $colour, 'RRD::Simple::_Colour', \@source_colors;
976              
977             my $fmt = '%s:%s#%s:%s%s';
978             my $longest_label = 1;
979             if ($extended_legend) {
980             for my $ds (@ds) {
981             my $len = length( defined $source_labels{$ds} ?
982             $source_labels{$ds} : $ds );
983             $longest_label = $len if $len > $longest_label;
984             }
985             $fmt = "%s:%s#%s:%-${longest_label}s%s";
986             }
987              
988              
989              
990             ##
991             ##
992             ##
993              
994             # Create the @cmd
995             my @cmd = ($image,@def);
996              
997             # Add the data sources definitions to @cmd
998             for my $ds (@rrd_sources) {
999             # Add the data source definition
1000             push @cmd, sprintf('DEF:%s=%s:%s:%s',$ds,$rrdfile,$ds,$cf);
1001             }
1002              
1003             # Add the data source draw commands to the grap/@cmd
1004             for my $ds (@ds) {
1005             # Stack operates differently in RRD 1.2 or higher
1006             my $drawtype = defined $source_drawtypes{$ds} ? $source_drawtypes{$ds}
1007             : "LINE$line_thickness";
1008             my $stack = '';
1009             if ($RRDs::VERSION >= 1.2 && $drawtype eq 'STACK') {
1010             $drawtype = 'AREA';
1011             $stack = ':STACK';
1012             }
1013              
1014             # Draw the line (and add to the legend)
1015             push @cmd, sprintf($fmt,
1016             $drawtype,
1017             $ds,
1018             (defined $source_colors{$ds} ? $source_colors{$ds} : $colour),
1019             (defined $source_labels{$ds} ? $source_labels{$ds} : $ds),
1020             $stack
1021             );
1022              
1023             # New for version 1.39
1024             # Return the min,max,last information in the graph() return @rtn
1025             if ($RRDs::VERSION >= 1.2) {
1026             push @cmd, sprintf('VDEF:%sMIN=%s,MINIMUM',$ds,$ds);
1027             push @cmd, sprintf('VDEF:%sMAX=%s,MAXIMUM',$ds,$ds);
1028             push @cmd, sprintf('VDEF:%sLAST=%s,LAST',$ds,$ds);
1029             # Don't automatically add this unless we have to
1030             # push @cmd, sprintf('VDEF:%sAVERAGE=%s,AVERAGE',$ds,$ds);
1031             push @cmd, sprintf('PRINT:%sMIN:%s min %%1.2lf',$ds,$ds);
1032             push @cmd, sprintf('PRINT:%sMAX:%s max %%1.2lf',$ds,$ds);
1033             push @cmd, sprintf('PRINT:%sLAST:%s last %%1.2lf',$ds,$ds);
1034             } else {
1035             push @cmd, sprintf('PRINT:%s:MIN:%s min %%1.2lf',$ds,$ds);
1036             push @cmd, sprintf('PRINT:%s:MAX:%s max %%1.2lf',$ds,$ds);
1037             push @cmd, sprintf('PRINT:%s:LAST:%s last %%1.2lf',$ds,$ds);
1038             }
1039              
1040             # New for version 1.35
1041             if ($extended_legend) {
1042             if ($RRDs::VERSION >= 1.2) {
1043             # Moved the VDEFs to the block of code above which is
1044             # always run, regardless of the extended legend
1045             push @cmd, sprintf('GPRINT:%sMIN: min\:%%10.2lf\g',$ds);
1046             push @cmd, sprintf('GPRINT:%sMAX: max\:%%10.2lf\g',$ds);
1047             push @cmd, sprintf('GPRINT:%sLAST: last\:%%10.2lf\l',$ds);
1048             } else {
1049             push @cmd, sprintf('GPRINT:%s:MIN: min\:%%10.2lf\g',$ds);
1050             push @cmd, sprintf('GPRINT:%s:MAX: max\:%%10.2lf\g',$ds);
1051             push @cmd, sprintf('GPRINT:%s:LAST: last\:%%10.2lf\l',$ds);
1052             }
1053             }
1054             }
1055              
1056              
1057              
1058              
1059              
1060              
1061             # Push the post command defs on to the stack
1062             push @cmd, @command_param;
1063              
1064             # Add a comment stating when the graph was last updated
1065             if ($timestamp ne 'none') {
1066             #push @cmd, ('COMMENT:\s','COMMENT:\s','COMMENT:\s');
1067             push @cmd, ('COMMENT:\s','COMMENT:\s');
1068             push @cmd, 'COMMENT:\s' unless $extended_legend || !@ds;
1069             my $timefmt = '%a %d/%b/%Y %T %Z';
1070              
1071             if ($timestamp eq 'rrd' || $timestamp eq 'both') {
1072             my $time = sprintf('RRD last updated: %s\r',
1073             strftime($timefmt,localtime((stat($rrdfile))[9]))
1074             );
1075             $time =~ s/:/\\:/g if $RRDs::VERSION >= 1.2; # Only escape for 1.2
1076             push @cmd, "COMMENT:$time";
1077             }
1078              
1079             if ($timestamp eq 'graph' || $timestamp eq 'both') {
1080             my $time = sprintf('Graph last updated: %s\r',
1081             strftime($timefmt,localtime(time))
1082             );
1083             $time =~ s/:/\\:/g if $RRDs::VERSION >= 1.2; # Only escape for 1.2
1084             push @cmd, "COMMENT:$time";
1085             }
1086             }
1087              
1088             DUMP('@cmd',\@cmd);
1089              
1090             # Generate the graph
1091             my @rtn = RRDs::graph(@cmd);
1092             my $error = RRDs::error();
1093             croak($error) if $error;
1094             return ($image,@rtn);
1095             }
1096              
1097              
1098              
1099              
1100             #
1101             # Private subroutines
1102             #
1103              
1104             no warnings 'redefine';
1105             sub UNIVERSAL::a_sub_not_likely_to_be_here { ref($_[0]) }
1106             use warnings 'redefine';
1107              
1108              
1109             sub _blessed ($) {
1110             local($@, $SIG{__DIE__}, $SIG{__WARN__});
1111             return length(ref($_[0]))
1112             ? eval { $_[0]->a_sub_not_likely_to_be_here }
1113             : undef
1114             }
1115              
1116              
1117             sub _refaddr($) {
1118             my $pkg = ref($_[0]) or return undef;
1119             if (_blessed($_[0])) {
1120             bless $_[0], 'Scalar::Util::Fake';
1121             } else {
1122             $pkg = undef;
1123             }
1124             "$_[0]" =~ /0x(\w+)/;
1125             my $i = do { local $^W; hex $1 };
1126             bless $_[0], $pkg if defined $pkg;
1127             return $i;
1128             }
1129              
1130              
1131             sub _isLegalDsName {
1132             #rrdtool-1.0.49/src/rrd_format.h:#define DS_NAM_FMT "%19[a-zA-Z0-9_-]"
1133             #rrdtool-1.2.11/src/rrd_format.h:#define DS_NAM_FMT "%19[a-zA-Z0-9_-]"
1134              
1135             ##
1136             ## TODO
1137             ## 1.45 - Double check this with the latest 1.3 version of RRDtool
1138             ## to see if it has changed or not
1139             ##
1140              
1141             return $_[0] =~ /^[a-zA-Z0-9_-]{1,19}$/;
1142             }
1143              
1144              
1145             sub _rrd_def {
1146             croak('Pardon?!') if ref $_[0];
1147             my $type = _valid_scheme(shift);
1148              
1149             # This is calculated the same way as mrtg v2.13.2
1150             if ($type eq 'mrtg') {
1151             my $step = 5; # 5 minutes
1152             return {
1153             step => $step * 60,
1154             heartbeat => $step * 60 * 2,
1155             rra => [(
1156             { step => 1, rows => int(4000 / $step) }, # 800
1157             { step => int( 30 / $step), rows => 800 }, # if $step < 30
1158             { step => int( 120 / $step), rows => 800 },
1159             { step => int(1440 / $step), rows => 800 },
1160             )],
1161             };
1162             }
1163              
1164             ##
1165             ## TODO
1166             ## 1.45 Add higher resolution for hour, 6hour and 12 hour
1167             ##
1168              
1169             my $step = 1; # 1 minute highest resolution
1170             my $rra = {
1171             step => $step * 60,
1172             heartbeat => $step * 60 * 2,
1173             rra => [(
1174             # Actual $step resolution (for 1.25 days retention)
1175             { step => 1, rows => int( _minutes_in('day',125) / $step) },
1176             )],
1177             };
1178              
1179             if ($type =~ /^(week|month|year|3years)$/i) {
1180             push @{$rra->{rra}}, {
1181             step => int( 30 / $step),
1182             rows => int( _minutes_in('week',125) / int(30/$step) )
1183             }; # 30 minute average
1184              
1185             push @{$rra->{rra}}, {
1186             step => int( 120 / $step),
1187             rows => int( _minutes_in($type eq 'week' ? 'week' : 'month',125)
1188             / int(120/$step) )
1189             }; # 2 hour average
1190             }
1191              
1192             if ($type =~ /^(year|3years)$/i) {
1193             push @{$rra->{rra}}, {
1194             step => int(1440 / $step),
1195             rows => int( _minutes_in($type,125) / int(1440/$step) )
1196             }; # 1 day average
1197             }
1198              
1199             return $rra;
1200             }
1201              
1202              
1203             sub _odd {
1204             return $_[0] % 2;
1205             }
1206              
1207              
1208             sub _even {
1209             return !($_[0] % 2);
1210             }
1211              
1212              
1213             sub _valid_scheme {
1214             TRACE(">>> _valid_scheme()");
1215             croak('Pardon?!') if ref $_[0];
1216             #if ($_[0] =~ /^(day|week|month|year|3years|mrtg)$/i) {
1217             if ($_[0] =~ /^((?:6|12)?hour|(?:half)?day|week|month|year|3years|mrtg)$/i) {
1218             TRACE("'".lc($1)."' is a valid scheme.");
1219             return lc($1);
1220             }
1221             TRACE("'@_' is not a valid scheme.");
1222             return undef;
1223             }
1224              
1225              
1226             sub _hours_in { return int((_seconds_in(@_)/60)/60); }
1227             sub _minutes_in { return int(_seconds_in(@_)/60); }
1228             sub _seconds_in {
1229             croak('Pardon?!') if ref $_[0];
1230             my $str = lc(shift);
1231             my $scale = shift || 100;
1232              
1233             return undef if !defined(_valid_scheme($str));
1234              
1235             my %time = (
1236             # New for version 1.44 of RRD::Simple by
1237             # popular request
1238             'hour' => 60 * 60,
1239             '6hour' => 60 * 60 * 6,
1240             'quarterday' => 60 * 60 * 6,
1241             '12hour' => 60 * 60 * 12,
1242             'halfday' => 60 * 60 * 12,
1243              
1244             'day' => 60 * 60 * 24,
1245             'week' => 60 * 60 * 24 * 7,
1246             'month' => 60 * 60 * 24 * 31,
1247             'year' => 60 * 60 * 24 * 365,
1248             '3years' => 60 * 60 * 24 * 365 * 3,
1249             'mrtg' => ( int(( 1440 / 5 )) * 800 ) * 60, # mrtg v2.13.2
1250             );
1251              
1252             my $rtn = $time{$str} * ($scale / 100);
1253             return $rtn;
1254             }
1255              
1256              
1257             sub _alt_graph_name {
1258             croak('Pardon?!') if ref $_[0];
1259             my $type = _valid_scheme(shift);
1260             return unless defined $type;
1261              
1262             # New for version 1.44 of RRD::Simple by popular request
1263             return 'hourly' if $type eq 'hour';
1264             return '6hourly' if $type eq '6hour' || $type eq 'quarterday';
1265             return '12hourly' if $type eq '12hour' || $type eq 'halfday';
1266              
1267             return 'daily' if $type eq 'day';
1268             return 'weekly' if $type eq 'week';
1269             return 'monthly' if $type eq 'month';
1270             return 'annual' if $type eq 'year';
1271             return '3years' if $type eq '3years';
1272             return $type;
1273             }
1274              
1275              
1276             ##
1277             ## TODO
1278             ## 1.45 - Check to see if there is now native support in RRDtool to
1279             ## add, remove or change existing sources - and if there is
1280             ## make this code only run for onler versions that do not have
1281             ## native support.
1282             ##
1283              
1284             sub _modify_source {
1285             croak('Pardon?!') if ref $_[0];
1286             my ($rrdfile,$stor,$ds,$action,$dstype,$heartbeat) = @_;
1287             my $rrdtool = $stor->{rrdtool};
1288             $rrdtool = '' unless defined $rrdtool;
1289              
1290             # Decide what action we should take
1291             if ($action !~ /^(add|del)$/) {
1292             my $caller = (caller(1))[3];
1293             $action = $caller =~ /\badd\b/i ? 'add' :
1294             $caller =~ /\bdel(ete)?\b/i ? 'del' : undef;
1295             }
1296             croak "Unknown or no action passed to method _modify_source()"
1297             unless defined $action && $action =~ /^(add|del)$/;
1298              
1299             require File::Copy;
1300             require File::Temp;
1301              
1302             # Generate an XML dump of the RRD file
1303             # - Added "tmpdir" support in 1.44
1304             my $tmpdir = defined $stor->{tmpdir} ? $stor->{tmpdir} : File::Spec->tmpdir();
1305             my ($tempXmlFileFH,$tempXmlFile) = File::Temp::tempfile(
1306             DIR => $tmpdir,
1307             TEMPLATE => 'rrdXXXXX',
1308             SUFFIX => '.tmp',
1309             );
1310              
1311             # Check that we managed to get a sane temporary filename
1312             croak "File::Temp::tempfile() failed to return a temporary filename"
1313             unless defined $tempXmlFile;
1314             TRACE("_modify_source(): \$tempXmlFile = $tempXmlFile");
1315              
1316             # Try the internal perl way first (portable)
1317             eval {
1318             # Patch to rrd_dump.c emailed to Tobi and developers
1319             # list by nicolaw/heds on 2006/01/08
1320             if ($RRDs::VERSION >= 1.2013) {
1321             my @rtn = RRDs::dump($rrdfile,$tempXmlFile);
1322             my $error = RRDs::error();
1323             croak($error) if $error;
1324             }
1325             };
1326              
1327             # Do it the old fashioned way
1328             if ($@ || !-f $tempXmlFile || (stat($tempXmlFile))[7] < 200) {
1329             croak "rrdtool binary '$rrdtool' does not exist or is not executable"
1330             if !defined $rrdtool || !-f $rrdtool || !-x $rrdtool;
1331             _safe_exec(sprintf('%s dump %s > %s',$rrdtool,$rrdfile,$tempXmlFile));
1332             }
1333              
1334             # Read in the new temporary XML dump file
1335             open(IN, "<$tempXmlFile") || croak "Unable to open '$tempXmlFile': $!";
1336              
1337             # Open XML output file
1338             # my $tempImportXmlFile = File::Temp::tmpnam();
1339             # - Added "tmpdir" support in 1.44
1340             my ($tempImportXmlFileFH,$tempImportXmlFile) = File::Temp::tempfile(
1341             DIR => $tmpdir,
1342             TEMPLATE => 'rrdXXXXX',
1343             SUFFIX => '.tmp',
1344             );
1345             open(OUT, ">$tempImportXmlFile")
1346             || croak "Unable to open '$tempImportXmlFile': $!";
1347              
1348             # Create a marker hash ref to store temporary state
1349             my $marker = {
1350             currentDSIndex => 0,
1351             deleteDSIndex => undef,
1352             addedNewDS => 0,
1353             parse => 0,
1354             version => 1,
1355             };
1356              
1357             # Parse the input XML file
1358             while (local $_ = ) {
1359             chomp;
1360              
1361             # Find out what index number the existing DS definition is in
1362             if ($action eq 'del' && /\s*(\S+)\s*<\/name>/) {
1363             $marker->{deleteIndex} = $marker->{currentDSIndex} if $1 eq $ds;
1364             $marker->{currentDSIndex}++;
1365             }
1366              
1367             # Add the DS definition
1368             if ($action eq 'add' && !$marker->{addedNewDS} && //) {
1369             print OUT <
1370            
1371             $ds
1372             $dstype
1373             $heartbeat
1374             0.0000000000e+00
1375             NaN
1376              
1377            
1378             UNKN
1379             0.0000000000e+00
1380             0
1381            
1382              
1383             EndDS
1384             $marker->{addedNewDS} = 1;
1385             }
1386              
1387             # Insert DS under CDP_PREP entity
1388             if ($action eq 'add' && /<\/cdp_prep>/) {
1389             # Version 0003 RRD from rrdtool 1.2x
1390             if ($marker->{version} >= 3) {
1391             print OUT " \n";
1392             print OUT " 0.0000000000e+00 \n";
1393             print OUT " 0.0000000000e+00 \n";
1394             print OUT " NaN \n";
1395             print OUT " 0 \n";
1396             print OUT " \n";
1397              
1398             # Version 0001 RRD from rrdtool 1.0x
1399             } else {
1400             print OUT " NaN 0 \n";
1401             }
1402             }
1403              
1404             # Look for the end of an RRA
1405             if (/<\/database>/) {
1406             $marker->{parse} = 0;
1407              
1408             # Find the dumped RRD version (must take from the XML, not the RRD)
1409             } elsif (/\s*([0-9\.]+)\s*<\/version>/) {
1410             $marker->{version} = ($1 + 1 - 1);
1411             }
1412              
1413             # Add the extra " NaN " under the RRAs. Just print normal lines
1414             if ($marker->{parse} == 1) {
1415             if ($_ =~ /^(.+ .+)(<\/row>.*)/) {
1416             print OUT $1;
1417             print OUT " NaN " if $action eq 'add';
1418             print OUT $2;
1419             print OUT "\n";
1420             }
1421             } else {
1422             print OUT "$_\n";
1423             }
1424              
1425             # Look for the start of an RRA
1426             if (//) {
1427             $marker->{parse} = 1;
1428             }
1429             }
1430              
1431             # Close the files
1432             close(IN) || croak "Unable to close '$tempXmlFile': $!";
1433             close(OUT) || croak "Unable to close '$tempImportXmlFile': $!";
1434              
1435             # Import the new output file in to the old RRD filename
1436             my $new_rrdfile = File::Temp::tmpnam();
1437             TRACE("_modify_source(): \$new_rrdfile = $new_rrdfile");
1438              
1439             # Try the internal perl way first (portable)
1440             eval {
1441             if ($RRDs::VERSION >= 1.0049) {
1442             my @rtn = RRDs::restore($tempImportXmlFile,$new_rrdfile);
1443             my $error = RRDs::error();
1444             croak($error) if $error;
1445             }
1446             };
1447              
1448             # Do it the old fashioned way
1449             if ($@ || !-f $new_rrdfile || (stat($new_rrdfile))[7] < 200) {
1450             croak "rrdtool binary '$rrdtool' does not exist or is not executable"
1451             unless (-f $rrdtool && -x $rrdtool);
1452             my $cmd = sprintf('%s restore %s %s',$rrdtool,$tempImportXmlFile,$new_rrdfile);
1453             my $rtn = _safe_exec($cmd);
1454              
1455             # At least check the file is created
1456             unless (-f $new_rrdfile) {
1457             _nuke_tmp($tempXmlFile,$tempImportXmlFile);
1458             croak "Command '$cmd' failed to create the new RRD file '$new_rrdfile': $rtn";
1459             }
1460             }
1461              
1462             # Remove the temporary files
1463             _nuke_tmp($tempXmlFile,$tempImportXmlFile);
1464             sub _nuke_tmp {
1465             for (@_) {
1466             unlink($_) ||
1467             carp("Unable to unlink temporary file '$_': $!");
1468             }
1469             }
1470              
1471             # Return the new RRD filename
1472             return wantarray ? ($new_rrdfile) : $new_rrdfile;
1473             }
1474              
1475              
1476             ##
1477             ## TODO
1478             ## 1.45 - Improve this _safe_exec function to see if it can be made
1479             ## more robust and use any better CPAN modules if that happen
1480             ## to already be installed on the users system (don't add any
1481             ## new module dependancies though)
1482             ##
1483              
1484             sub _safe_exec {
1485             croak('Pardon?!') if ref $_[0];
1486             my $cmd = shift;
1487             if ($cmd =~ /^([\/\.\_\-a-zA-Z0-9 >]+)$/) {
1488             $cmd = $1;
1489             TRACE($cmd);
1490             system($cmd);
1491             if ($? == -1) {
1492             croak "Failed to execute command '$cmd': $!\n";
1493             } elsif ($? & 127) {
1494             croak(sprintf("While executing command '%s', child died ".
1495             "with signal %d, %s coredump\n", $cmd,
1496             ($? & 127), ($? & 128) ? 'with' : 'without'));
1497             }
1498             my $exit_value = $? >> 8;
1499             croak "Error caught from '$cmd'" if $exit_value != 0;
1500             return $exit_value;
1501             } else {
1502             croak "Unexpected potentially unsafe command will not be executed: $cmd";
1503             }
1504             }
1505              
1506              
1507             sub _find_binary {
1508             croak('Pardon?!') if ref $_[0];
1509             my $binary = shift || 'rrdtool';
1510             return $binary if -f $binary && -x $binary;
1511              
1512             my @paths = File::Spec->path();
1513             my $rrds_path = dirname($INC{'RRDs.pm'});
1514             push @paths, $rrds_path;
1515             push @paths, File::Spec->catdir($rrds_path,
1516             File::Spec->updir(),File::Spec->updir(),'bin');
1517              
1518             for my $path (@paths) {
1519             my $filename = File::Spec->catfile($path,$binary);
1520             return $filename if -f $filename && -x $filename;
1521             }
1522              
1523             my $path = File::Spec->catdir(File::Spec->rootdir(),'usr','local');
1524             if (opendir(DH,$path)) {
1525             my @dirs = sort { $b cmp $a } grep(/^rrdtool/,readdir(DH));
1526             closedir(DH) || carp "Unable to close file handle: $!";
1527             for my $dir (@dirs) {
1528             my $filename = File::Spec->catfile($path,$dir,'bin',$binary);
1529             return $filename if -f $filename && -x $filename;
1530             }
1531             }
1532             }
1533              
1534              
1535             sub _guess_filename {
1536             croak('Pardon?!') if !defined $_[0] || ref($_[0]) ne 'HASH';
1537             my $stor = shift;
1538             if (defined $stor->{file}) {
1539             TRACE("_guess_filename = \$stor->{file} = $stor->{file}");
1540             return $stor->{file};
1541             }
1542             my ($basename, $dirname, $extension) = fileparse($0, '\.[^\.]+');
1543             TRACE("_guess_filename = calculated = $dirname$basename.rrd");
1544             return "$dirname$basename.rrd";
1545             }
1546              
1547              
1548             sub DESTROY {
1549             my $self = shift;
1550             delete $objstore->{_refaddr($self)};
1551             }
1552              
1553              
1554             sub TRACE {
1555             return unless $DEBUG;
1556             carp(shift());
1557             }
1558              
1559              
1560             sub DUMP {
1561             return unless $DEBUG;
1562             eval {
1563             require Data::Dumper;
1564             $Data::Dumper::Indent = 2;
1565             $Data::Dumper::Terse = 1;
1566             carp(shift().': '.Data::Dumper::Dumper(shift()));
1567             }
1568             }
1569              
1570             BEGIN {
1571             eval "use RRDs";
1572             if ($@) {
1573             carp qq{
1574             +-----------------------------------------------------------------------------+
1575             | ERROR! -- Could not load RRDs.pm |
1576             | |
1577             | RRD::Simple requires RRDs.pm (a part of RRDtool) in order to function. You |
1578             | can download a copy of RRDtool from http://www.rrdtool.org. See the INSTALL |
1579             | document for more details. |
1580             +-----------------------------------------------------------------------------+
1581              
1582             } unless $ENV{AUTOMATED_TESTING};
1583             }
1584             }
1585              
1586              
1587             1;
1588              
1589              
1590             ###############################################################
1591             # This tie code is from Tie::Cycle
1592             # written by brian d foy,
1593              
1594             package RRD::Simple::_Colour;
1595              
1596             sub TIESCALAR {
1597             my ($class,$list_ref) = @_;
1598             my @shallow_copy = map { $_ } @$list_ref;
1599             return unless UNIVERSAL::isa( $list_ref, 'ARRAY' );
1600             my $self = [ 0, scalar @shallow_copy, \@shallow_copy ];
1601             bless $self, $class;
1602             }
1603              
1604             sub FETCH {
1605             my $self = shift;
1606             my $index = $$self[0]++;
1607             $$self[0] %= $self->[1];
1608             return $self->[2]->[ $index ];
1609             }
1610              
1611             sub STORE {
1612             my ($self,$list_ref) = @_;
1613             return unless ref $list_ref eq ref [];
1614             return unless @$list_ref > 1;
1615             $self = [ 0, scalar @$list_ref, $list_ref ];
1616             }
1617              
1618             1;
1619              
1620              
1621              
1622              
1623             =pod
1624              
1625             =head1 NAME
1626              
1627             RRD::Simple - Simple interface to create and store data in RRD files
1628              
1629             =head1 SYNOPSIS
1630              
1631             use strict;
1632             use RRD::Simple ();
1633            
1634             # Create an interface object
1635             my $rrd = RRD::Simple->new( file => "myfile.rrd" );
1636            
1637             # Create a new RRD file with 3 data sources called
1638             # bytesIn, bytesOut and faultsPerSec.
1639             $rrd->create(
1640             bytesIn => "GAUGE",
1641             bytesOut => "GAUGE",
1642             faultsPerSec => "COUNTER"
1643             );
1644            
1645             # Put some arbitary data values in the RRD file for the same
1646             # 3 data sources called bytesIn, bytesOut and faultsPerSec.
1647             $rrd->update(
1648             bytesIn => 10039,
1649             bytesOut => 389,
1650             faultsPerSec => 0.4
1651             );
1652            
1653             # Generate graphs:
1654             # /var/tmp/myfile-daily.png, /var/tmp/myfile-weekly.png
1655             # /var/tmp/myfile-monthly.png, /var/tmp/myfile-annual.png
1656             my %rtn = $rrd->graph(
1657             destination => "/var/tmp",
1658             title => "Network Interface eth0",
1659             vertical_label => "Bytes/Faults",
1660             interlaced => ""
1661             );
1662             printf("Created %s\n",join(", ",map { $rtn{$_}->[0] } keys %rtn));
1663              
1664             # Return information about an RRD file
1665             my $info = $rrd->info;
1666             require Data::Dumper;
1667             print Data::Dumper::Dumper($info);
1668              
1669             # Get unixtime of when RRD file was last updated
1670             my $lastUpdated = $rrd->last;
1671             print "myfile.rrd was last updated at " .
1672             scalar(localtime($lastUpdated)) . "\n";
1673            
1674             # Get list of data source names from an RRD file
1675             my @dsnames = $rrd->sources;
1676             print "Available data sources: " . join(", ", @dsnames) . "\n";
1677            
1678             # And for the ultimately lazy, you could create and update
1679             # an RRD in one go using a one-liner like this:
1680             perl -MRRD::Simple=:all -e"update(@ARGV)" myfile.rrd bytesIn 99999
1681              
1682             =head1 DESCRIPTION
1683              
1684             RRD::Simple provides a simple interface to RRDTool's RRDs module.
1685             This module does not currently offer a C method that is
1686             available in the RRDs module.
1687              
1688             It does however create RRD files with a sensible set of default RRA
1689             (Round Robin Archive) definitions, and can dynamically add new
1690             data source names to an existing RRD file.
1691              
1692             This module is ideal for quick and simple storage of data within an
1693             RRD file if you do not need to, nor want to, bother defining custom
1694             RRA definitions.
1695              
1696             =head1 METHODS
1697              
1698             =head2 new
1699              
1700             my $rrd = RRD::Simple->new(
1701             file => "myfile.rrd",
1702             rrdtool => "/usr/local/rrdtool-1.2.11/bin/rrdtool",
1703             tmpdir => "/var/tmp",
1704             cf => [ qw(AVERAGE MAX) ],
1705             default_dstype => "GAUGE",
1706             on_missing_ds => "add",
1707             );
1708              
1709             The C parameter is currently optional but will become mandatory in
1710             future releases, replacing the optional C<$rrdfile> parameters on subsequent
1711             methods. This parameter specifies the RRD filename to be used.
1712              
1713             The C parameter is optional. It specifically defines where the
1714             C binary can be found. If not specified, the module will search for
1715             the C binary in your path, an additional location relative to where
1716             the C module was loaded from, and in /usr/local/rrdtool*.
1717              
1718             The C parameter is option and is only used what automatically adding
1719             a new data source to an existing RRD file. By default any temporary files
1720             will be placed in your default system temp directory (typically /tmp on Linux,
1721             or whatever your TMPDIR environment variable is set to). This parameter can
1722             be used for force any temporary files to be created in a specific directory.
1723              
1724             The C binary is only used by the C method, and only
1725             under certain circumstances. The C method may also be called
1726             automatically by the C method, if data point values for a previously
1727             undefined data source are provided for insertion.
1728              
1729             The C parameter is optional, but when specified expects an array
1730             reference. The C parameter defines which consolidation functions are
1731             used in round robin archives (RRAs) when creating new RRD files. Valid
1732             values are AVERAGE, MIN, MAX and LAST. The default value is AVERAGE and
1733             MAX.
1734              
1735             The C parameter is optional. Specifying the default data
1736             source type (DST) through the new() method allows the DST to be localised
1737             to the $rrd object instance rather than be global to the RRD::Simple package.
1738             See L<$RRD::Simple::DEFAULT_DSTYPE>.
1739              
1740             The C parameter is optional and will default to "add" when
1741             not defined. This parameter will determine what will happen if you try
1742             to insert or update data for a data source name that does not exist in
1743             the RRD file. Valid values are "add", "ignore" and "die".
1744              
1745             =head2 create
1746              
1747             $rrd->create($rrdfile, $period,
1748             source_name => "TYPE",
1749             source_name => "TYPE",
1750             source_name => "TYPE"
1751             );
1752              
1753             This method will create a new RRD file on disk.
1754              
1755             C<$rrdfile> is optional and will default to using the RRD filename specified
1756             by the C constructor method, or C<$0.rrd>. (Script basename with the file
1757             extension of .rrd).
1758              
1759             C<$period> is optional and will default to C. Valid options are C,
1760             C<6hour>/C, C<12hour>/C, C, C, C,
1761             C, C<3years> and C. Specifying a data retention period value will
1762             change how long data will be retained for within the RRD file. The C
1763             scheme will try and mimic the data retention period used by MRTG v2.13.2
1764             (L.
1765              
1766             The C data retention period uses a data stepping resolution of 300
1767             seconds (5 minutes) and heartbeat of 600 seconds (10 minutes), whereas all the
1768             other data retention periods use a data stepping resolution of 60 seconds
1769             (1 minute) and heartbeat of 120 seconds (2 minutes).
1770              
1771             Each data source name should specify the data source type. Valid data source
1772             types (DSTs) are GAUGE, COUNTER, DERIVE and ABSOLUTE. See the section
1773             regrading DSTs at L
1774             for further information.
1775              
1776             RRD::Simple will croak and die if you try to create an RRD file that already
1777             exists.
1778              
1779             =head2 update
1780              
1781             $rrd->update($rrdfile, $unixtime,
1782             source_name => "VALUE",
1783             source_name => "VALUE",
1784             source_name => "VALUE"
1785             );
1786              
1787             This method will update an RRD file by inserting new data point values
1788             in to the RRD file.
1789              
1790             C<$rrdfile> is optional and will default to using the RRD filename specified
1791             by the C constructor method, or C<$0.rrd>. (Script basename with the file
1792             extension of .rrd).
1793              
1794             C<$unixtime> is optional and will default to C (the current unixtime).
1795             Specifying this value will determine the date and time that your data point
1796             values will be stored against in the RRD file.
1797              
1798             If you try to update a value for a data source that does not exist, it will
1799             automatically be added for you. The data source type will be set to whatever
1800             is contained in the C<$RRD::Simple::DEFAULT_DSTYPE> variable. (See the
1801             VARIABLES section below).
1802              
1803             If you explicitly do not want this to happen, then you should check that you
1804             are only updating pre-existing data source names using the C method.
1805             You can manually add new data sources to an RRD file by using the C
1806             method, which requires you to explicitly set the data source type.
1807              
1808             If you try to update an RRD file that does not exist, it will attept to create
1809             the RRD file for you using the same behaviour as described above. A warning
1810             message will be displayed indicating that the RRD file is being created for
1811             you if have perl warnings turned on.
1812              
1813             =head2 last
1814              
1815             my $unixtime = $rrd->last($rrdfile);
1816              
1817             This method returns the last (most recent) data point entry time in the RRD
1818             file in UNIX time (seconds since the epoch; Jan 1st 1970). This value should
1819             not be confused with the last modified time of the RRD file.
1820              
1821             C<$rrdfile> is optional and will default to using the RRD filename specified
1822             by the C constructor method, or C<$0.rrd>. (Script basename with the file
1823             extension of .rrd).
1824              
1825             =head2 sources
1826              
1827             my @sources = $rrd->sources($rrdfile);
1828              
1829             This method returns a list of all of the data source names contained within
1830             the RRD file.
1831              
1832             C<$rrdfile> is optional and will default to using the RRD filename specified
1833             by the C constructor method, or C<$0.rrd>. (Script basename with the file
1834             extension of .rrd).
1835              
1836             =head2 add_source
1837              
1838             $rrd->add_source($rrdfile,
1839             source_name => "TYPE"
1840             );
1841              
1842             You may add a new data source to an existing RRD file using this method. Only
1843             one data source name can be added at a time. You must also specify the data
1844             source type.
1845              
1846             C<$rrdfile> is optional and will default to using the RRD filename specified
1847             by the C constructor method, or C<$0.rrd>. (Script basename with the file
1848             extension of .rrd).
1849              
1850             This method can be called internally by the C method to automatically
1851             add missing data sources.
1852              
1853             =head2 rename_source
1854              
1855             $rrd->rename_source($rrdfile, "old_datasource", "new_datasource");
1856              
1857             You may rename a data source in an existing RRD file using this method.
1858              
1859             C<$rrdfile> is optional and will default to using the RRD filename specified
1860             by the C constructor method, or C<$0.rrd>. (Script basename with the file
1861             extension of .rrd).
1862              
1863             =head2 graph
1864              
1865             my %rtn = $rrd->graph($rrdfile,
1866             destination => "/path/to/write/graph/images",
1867             basename => "graph_basename",
1868             timestamp => "both", # graph, rrd, both or none
1869             periods => [ qw(week month) ], # omit to generate all graphs
1870             sources => [ qw(source_name1 source_name2 source_name3) ],
1871             source_colors => [ qw(ff0000 aa3333 000000) ],
1872             source_labels => [ ("My Source 1", "My Source Two", "Source 3") ],
1873             source_drawtypes => [ qw(LINE1 AREA LINE) ],
1874             line_thickness => 2,
1875             extended_legend => 1,
1876             rrd_graph_option => "value",
1877             rrd_graph_option => "value",
1878             rrd_graph_option => "value"
1879             );
1880              
1881             This method will render one or more graph images that show the data in the
1882             RRD file.
1883              
1884             The number of image files that are created depends on the retention period
1885             of the RRD file. Hourly, 6 hourly, 12 hourly, daily, weekly, monthly, annual
1886             and 3year graphs will be created if there is enough data in the RRD file to
1887             accomodate them.
1888              
1889             The image filenames will start with either the basename of the RRD
1890             file, or whatever is specified by the C parameter. The second part
1891             of the filename will be "-hourly", "-6hourly", "-12hourly", "-daily",
1892             "-weekly", "-monthly", "-annual" or "-3year" depending on the period that
1893             is being graphed.
1894              
1895             C<$rrdfile> is optional and will default to using the RRD filename specified
1896             by the C constructor method, or C<$0.rrd>. (Script basename with the file
1897             extension of .rrd).
1898              
1899             Graph options specific to RRD::Simple are:
1900              
1901             =over 4
1902              
1903             =item destination
1904              
1905             The C parameter is optional, and it will default to the same
1906             path location as that of the RRD file specified by C<$rrdfile>. Specifying
1907             this value will force the resulting graph images to be written to this path
1908             location. (The specified path must be a valid directory with the sufficient
1909             permissions to write the graph images).
1910              
1911             =item basename
1912              
1913             The C parameter is optional. This parameter specifies the basename
1914             of the graph image files that will be created. If not specified, it will
1915             default to the name of the RRD file. For example, if you specify a basename
1916             name of C, the following graph image files will be created in the
1917             C directory:
1918              
1919             mygraph-daily.png
1920             mygraph-weekly.png
1921             mygraph-monthly.png
1922             mygraph-annual.png
1923              
1924             The default file format is C, but this can be explicitly specified using
1925             the standard RRDs options. (See below).
1926              
1927             =item timestamp
1928              
1929             my %rtn = $rrd->graph($rrdfile,
1930             timestamp => "graph", # graph, rrd, both or none
1931             );
1932              
1933             The C parameter is optional, but will default to "graph". This
1934             parameter specifies which "last updated" timestamps should be added to the
1935             bottom right hand corner of the graph.
1936              
1937             Valid values are: "graph" - the timestamp of when the graph was last rendered
1938             will be used, "rrd" - the timestamp of when the RRD file was last updated will
1939             be used, "both" - both the timestamps of when the graph and RRD file were last
1940             updated will be used, "none" - no timestamp will be used.
1941              
1942             =item periods
1943              
1944             The C parameter is an optional list of periods that graphs should
1945             be generated for. If omitted, all possible graphs will be generated and not
1946             restricted to any specific subset. See the L method for a list of
1947             valid time periods.
1948              
1949             =item sources
1950              
1951             The C parameter is optional. This parameter should be an array of
1952             data source names that you want to be plotted. All data sources will be
1953             plotted by default.
1954              
1955             =item source_colors
1956              
1957             my %rtn = $rrd->graph($rrdfile,
1958             source_colors => [ qw(ff3333 ff00ff ffcc99) ],
1959             );
1960            
1961             %rtn = $rrd->graph($rrdfile,
1962             source_colors => { source_name1 => "ff3333",
1963             source_name2 => "ff00ff",
1964             source_name3 => "ffcc99", },
1965             );
1966              
1967             The C parameter is optional. This parameter should be an
1968             array or hash of hex triplet colors to be used for the plotted data source
1969             lines. A selection of vivid primary colors will be set by default.
1970              
1971             =item source_labels
1972              
1973             my %rtn = $rrd->graph($rrdfile,
1974             sources => [ qw(source_name1 source_name2 source_name3) ],
1975             source_labels => [ ("My Source 1","My Source Two","Source 3") ],
1976             );
1977            
1978             %rtn = $rrd->graph($rrdfile,
1979             source_labels => { source_name1 => "My Source 1",
1980             source_name2 => "My Source Two",
1981             source_name3 => "Source 3", },
1982             );
1983              
1984             The C parameter is optional. The parameter should be an
1985             array or hash of labels to be placed in the legend/key underneath the
1986             graph. An array can only be used if the C parameter is also
1987             specified, since the label index position in the array will directly
1988             relate to the data source index position in the C array.
1989              
1990             The data source names will be used in the legend/key by default if no
1991             C parameter is specified.
1992              
1993             =item source_drawtypes
1994              
1995             my %rtn = $rrd->graph($rrdfile,
1996             source_drawtypes => [ qw(LINE1 AREA LINE) ],
1997             );
1998            
1999             %rtn = $rrd->graph($rrdfile,
2000             source_colors => { source_name1 => "LINE1",
2001             source_name2 => "AREA",
2002             source_name3 => "LINE", },
2003             );
2004            
2005             %rtn = $rrd->graph($rrdfile,
2006             sources => [ qw(system user iowait idle) ]
2007             source_colors => [ qw(AREA STACK STACK STACK) ],
2008             );
2009              
2010             The C parameter is optional. This parameter should be an
2011             array or hash of drawing/plotting types to be used for the plotted data source
2012             lines. By default all data sources are drawn as lines (LINE), but data sources
2013             may also be drawn as filled areas (AREA). Valid values are, LINE, LINEI
2014             (where I represents the thickness of the line in pixels), AREA or STACK.
2015              
2016             =item line_thickness
2017              
2018             Specifies the thickness of the data lines drawn on the graphs for
2019             any data sources that have not had a specific line thickness already
2020             specified using the C option.
2021             Valid values are 1, 2 and 3 (pixels).
2022              
2023             =item extended_legend
2024              
2025             If set to boolean true, prints more detailed information in the graph legend
2026             by adding the minimum, maximum and last values recorded on the graph for each
2027             data source.
2028              
2029             =back
2030              
2031             Common RRD graph options are:
2032              
2033             =over 4
2034              
2035             =item title
2036              
2037             A horizontal string at the top of the graph.
2038              
2039             =item vertical_label
2040              
2041             A vertically placed string at the left hand side of the graph.
2042              
2043             =item width
2044              
2045             The width of the canvas (the part of the graph with the actual data
2046             and such). This defaults to 400 pixels.
2047              
2048             =item height
2049              
2050             The height of the canvas (the part of the graph with the actual data
2051             and such). This defaults to 100 pixels.
2052              
2053             =back
2054              
2055             For examples on how to best use the C method, refer to the example
2056             scripts that are bundled with this module in the examples/ directory. A
2057             complete list of parameters can be found at
2058             L.
2059              
2060             =head2 retention_period
2061              
2062             my $seconds = $rrd->retention_period($rrdfile);
2063              
2064             This method will return the maximum period of time (in seconds) that the RRD
2065             file will store data for.
2066              
2067             C<$rrdfile> is optional and will default to using the RRD filename specified
2068             by the C constructor method, or C<$0.rrd>. (Script basename with the file
2069             extension of .rrd).
2070              
2071             =head2 info
2072              
2073             my $info = $rrd->info($rrdfile);
2074              
2075             This method will return a complex data structure containing details about
2076             the RRD file, including RRA and data source information.
2077              
2078             C<$rrdfile> is optional and will default to using the RRD filename specified
2079             by the C constructor method, or C<$0.rrd>. (Script basename with the file
2080             extension of .rrd).
2081              
2082             =head2 heartbeat
2083              
2084             my $heartbeat = $rrd->heartbeat($rrdfile, "dsname");
2085             my @rtn = $rrd->heartbeat($rrdfile, "dsname", 600);
2086              
2087             This method will return the current heartbeat of a data source, or set a
2088             new heartbeat of a data source.
2089              
2090             C<$rrdfile> is optional and will default to using the RRD filename specified
2091             by the C constructor method, or C<$0.rrd>. (Script basename with the file
2092             extension of .rrd).
2093              
2094             =head1 VARIABLES
2095              
2096             =head2 $RRD::Simple::DEBUG
2097              
2098             Debug and trace information will be printed to STDERR if this variable
2099             is set to 1 (boolean true).
2100              
2101             This variable will take its value from C<$ENV{DEBUG}>, if it exists,
2102             otherwise it will default to 0 (boolean false). This is a normal package
2103             variable and may be safely modified at any time.
2104              
2105             =head2 $RRD::Simple::DEFAULT_DSTYPE
2106              
2107             This variable is used as the default data source type when creating or
2108             adding new data sources, when no other data source type is explicitly
2109             specified.
2110              
2111             This variable will take its value from C<$ENV{DEFAULT_DSTYPE}>, if it
2112             exists, otherwise it will default to C. This is a normal package
2113             variable and may be safely modified at any time.
2114              
2115             =head1 EXPORTS
2116              
2117             You can export the following functions if you do not wish to go through
2118             the extra effort of using the OO interface:
2119              
2120             create
2121             update
2122             last_update (synonym for the last() method)
2123             sources
2124             add_source
2125             rename_source
2126             graph
2127             retention_period
2128             info
2129             heartbeat
2130              
2131             The tag C is available to easily export everything:
2132              
2133             use RRD::Simple qw(:all);
2134              
2135             See the examples and unit tests in this distribution for more
2136             details.
2137              
2138             =head1 SEE ALSO
2139              
2140             L, L, L,
2141             L, examples/*.pl,
2142             L,
2143             L
2144              
2145             =head1 VERSION
2146              
2147             $Id: Simple.pm 1100 2008-01-24 17:39:35Z nicolaw $
2148              
2149             =head1 AUTHOR
2150              
2151             Nicola Worthington
2152              
2153             L
2154              
2155             If you like this software, why not show your appreciation by sending the
2156             author something nice from her
2157             L?
2158             ( http://www.amazon.co.uk/gp/registry/1VZXC59ESWYK0?sort=priority )
2159              
2160             =head1 COPYRIGHT
2161              
2162             Copyright 2005,2006,2007,2008 Nicola Worthington.
2163              
2164             This software is licensed under The Apache Software License, Version 2.0.
2165              
2166             L
2167              
2168             =cut
2169              
2170              
2171             __END__