| 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 |
|
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__ |