• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

munin-monitoring / munin / 9141217035

18 May 2024 04:31PM UTC coverage: 70.067% (+0.05%) from 70.022%
9141217035

Pull #1558

github

web-flow
Merge a5c90f9eb into 7ff6a866f
Pull Request #1558: Static generation ci

2 of 2 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

1580 of 2255 relevant lines covered (70.07%)

11240.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.81
/lib/Munin/Master/UpdateWorker.pm
1
package Munin::Master::UpdateWorker;
2
use base qw(Munin::Master::Worker);
32✔
3

4

5
use warnings;
32✔
6
use strict;
32✔
7

8
use Carp;
32✔
9
use English qw(-no_match_vars);
32✔
10
use Munin::Common::Logger;
32✔
11

12
use File::Basename;
32✔
13
use File::Path;
32✔
14
use File::Spec;
32✔
15
use IO::Socket::INET;
32✔
16
use Munin::Master::Config;
32✔
17
use Munin::Master::Node;
32✔
18
use Munin::Master::Utils;
32✔
19
use RRDs;
32✔
20
use Data::Dumper;
32✔
21
use Scalar::Util qw(weaken);
32✔
22

23
use List::Util qw(max shuffle);
32✔
24

25
my $config = Munin::Master::Config->instance()->{config};
26

27
# Flags that have RRD autotuning enabled.
28
my $rrd_tune_flags = {
29
        type => '--data-source-type',
30
        max => '--maximum',
31
        min => '--minimum',
32
};
33

34
sub new {
35
    my ($class, $host, $worker) = @_;
250✔
36

37
    my $self = $class->SUPER::new($host->get_full_path);
250✔
38
    $self->{host} = $host;
250✔
39

40
    # node addresses are optional, defaulting to node name
41
    # More infos in #972 & D:592213
42
    $host->{address} = _get_default_address($host) unless defined $host->{address};
250✔
43

44
    $self->{node} = Munin::Master::Node->new($host->{address},
45
                                             $host->{port},
46
                                             $host->{host_name},
47
                                             $host);
250✔
48
    # $worker already has a ref to $self, so avoid mem leak
49
    $self->{worker} = $worker;
250✔
50
    weaken($self->{worker});
250✔
51

52
    DEBUG "created $self";
250✔
53

54
    return $self;
250✔
55
}
56

57

58
sub do_work {
59
    my ($self) = @_;
25✔
60

61
    my $update_time = Time::HiRes::time;
25✔
62
    my $host = $self->{host}{host_name};
25✔
63
    my $group = $self->{host}{group};
25✔
64
    my $path = $self->{host}->get_full_path;
25✔
65
    $path =~ s{[:;]}{-}g;
25✔
66

67
    # Parameters are space-separated from the main address
68
    my ($url, $params) = split(/ +/, $self->{host}{address}, 2);
25✔
69
    my $uri = new URI($url);
25✔
70

71
    # If the scheme is not defined, it's a plain host.
72
    # Prefix it with munin:// to be able to parse it like others
73
    $uri = new URI("munin://" . $url) unless $uri->scheme;
25✔
74

75
    my $nodedesignation;
25✔
76
    if ($uri->scheme eq "ssh" || $uri->scheme eq "cmd" || $uri->scheme eq "unix") {
25✔
77
        $nodedesignation = $host . " (" . $self->{host}{address} . ")";
×
78
    } else {
79
        $nodedesignation = $host . " (" . $self->{host}{address} . ":" . $self->{host}{port} . ")";
25✔
80
    }
81

82
    local $0 = "$0 [$nodedesignation]";
25✔
83

84
    # No need to lock for the node. We'll use per plugin locking, and it will be
85
    # handled directly in SQL. This will enable node-pushed updates.
86

87
    my %all_service_configs = (
25✔
88
                data_source => {},
89
                global => {},
90
        );
91

92
        # Connecting to carbon isn't maintained with the new SQL metadata, so removing it.
93
        # We should provide a generic way to hook into the data updating part
94
        WARN "Connecting to a Carbon Server isn't supported anymore." if $config->{carbon_server};
25✔
95

96
        # Having a local handle looks easier
97
        my $node = $self->{node};
25✔
98

99
    INFO "[INFO] starting work in $$ for $nodedesignation.\n";
25✔
100
    my $done = $node->do_in_session(sub {
101

102
        # A I/O timeout results in a violent exit.  Catch and handle.
103
        eval {
25✔
104
                # Create the group path
105
                my $grp_id = $self->_db_mkgrp($group);
25✔
106

107
                # Fetch the node name
108
                my $node_name = $self->{node_name} || $self->{host}->{host_name};
25✔
109

110
                # Create the node
111
                my $node_id = $self->_db_node($grp_id, $node_name);
25✔
112
                $self->{node_id} = $node_id;
25✔
113

114
                my @node_capabilities = $node->negotiate_capabilities();
25✔
115

116

117
                $self->{dbh}->begin_work() if $self->{dbh}->{AutoCommit};
25✔
118

119
                my $dbh = $self->{dbh};
25✔
120

121
                # Handle spoolfetch, one call to retrieve everything
122
                if (grep /^spool$/, @node_capabilities) {
25✔
123
                        my $spoolfetch_last_timestamp = $self->get_spoolfetch_timestamp();
15✔
124
                        local $0 = "$0 s($spoolfetch_last_timestamp)";
15✔
125

126
                        # We do inject the update handling, in order to have on-the-fly
127
                        # updates, as we don't want to slurp the whole spoolfetched output
128
                        # and process it later. It will surely timeout, and use a truckload
129
                        # of RSS.
130
                        my $timestamp = $node->spoolfetch($spoolfetch_last_timestamp, sub {
131
                                my ($plugin, $now, $data, $last_timestamp, $update_rate_ptr) = @_;
10,950✔
132
                                INFO "spoolfetch config ($plugin, $now)";
10,950✔
133
                                local $0 = "$0 t($now) c($plugin)";
10,950✔
134
                                $self->uw_handle_config( @_ );
10,950✔
135
                        } );
15✔
136

137
                        # update the timestamp if we spoolfetched something
138
                        $self->set_spoolfetch_timestamp($timestamp) if $timestamp;
15✔
139

140
                        # Note that spoolfetching hosts is always a success. BY DESIGN.
141
                        # Since, if we cannot connect, or whatever else, it is NOT an issue.
142

143
                        # No need to do more than that on this node
144
                        goto NODE_END;
15✔
145
        }
146

147
        # Note: A multigraph plugin can present multiple services.
148
        my @plugins = $node->list_plugins();
10✔
149

150
        # We are not spoolfetching, so we should protect ourselves against
151
        # plugin redef. Note that we should declare 2 different HASHREF,
152
        # otherwise it is _shared_ which isn't what we want.
153
        $self->{__SEEN_PLUGINS__} = {};
10✔
154

155
        # Shuffle @plugins to avoid always having the same ordering
156
        # XXX - It might be best to preorder them on the TIMETAKEN ASC
157
        #       in order that statisticall fast plugins are done first to increase
158
        #       the global throughtput
159
        @plugins = shuffle(@plugins);
10✔
160

161
        for my $plugin (@plugins) {
10✔
162
                DEBUG "[DEBUG] for my $plugin (@plugins)";
300✔
163
                if (defined $config->{limit_services} && %{$config->{limit_services}}) {
300✔
164
                    next unless $config->{limit_services}{$plugin};
×
165
                }
166

167
                DEBUG "[DEBUG] config $plugin";
300✔
168

169
                local $0 = "$0 c($plugin)";
300✔
170
                my $update_rate = "300"; # Default
300✔
171
                my $last_timestamp = $node->fetch_service_config($plugin, sub { $self->uw_handle_config( @_, \$update_rate); });
300✔
172

173
                # Ignoring if $last_timestamp is undef, as we don't have config
174
                if (! defined ($last_timestamp)) {
300✔
175
                        INFO "[INFO] $plugin did emit no proper config, ignoring";
×
176
                        next;
×
177
                }
178

179
                if ($update_rate ne "300") {
300✔
180
                        INFO "[INFO] $plugin did change update_rate to $update_rate";
300✔
181
                }
182

183
                # Done with this plugin on dirty config (we already have a timestamp for data)
184
                # --> Note that dirtyconfig plugin are always polled every run,
185
                #     as we don't have a way to know yet.
186
                next if ($last_timestamp);
300✔
187

188

189
                my $now = time;
300✔
190
                my $is_fresh_enough = $self->is_fresh_enough($update_rate, $last_timestamp, $now);
300✔
191

192
                next if ($is_fresh_enough);
300✔
193

194
                DEBUG "[DEBUG] fetch $plugin";
300✔
195
                local $0 = "$0 f($plugin)";
300✔
196

197
                $last_timestamp = $node->fetch_service_data($plugin,
198
                        sub {
199
                                # First argument is the plugin name to be overridden when multigraphing
200
                                my $plugin_name = shift;
300✔
201

202
                                $self->uw_handle_fetch($plugin_name, $now, $update_rate, @_);
300✔
203
                        }
204
                );
300✔
205
            } # for @plugins
206
NODE_END:
207
            # Send "quit" to node
208
            $node->quit();
25✔
209

210
            # We want to commit to avoid leaking transactions
211
            $dbh->commit() unless $dbh->{AutoCommit};
25✔
212
        }; # eval
213

214
        # kill the remaining process if needed
215
        # (useful if we spawned an helper, as for cmd:// or ssh://)
216
        # XXX - investigate why this leaks here. It should be handled directly by Node.pm
217
        my $node_pid = $node->{pid};
25✔
218
        if ($node_pid && kill(0, $node_pid)) {
25✔
219
                INFO "[INFO] Killing subprocess $node_pid";
×
220
                kill 'KILL', $node_pid; # Using SIGKILL, since normal termination didn't happen
×
221
        }
222

223
        if ($@ =~ m/^NO_SPOOLFETCH_DATA /) {
25✔
224
            INFO "[INFO] No spoofetch data for $nodedesignation";
×
225
            return;
×
226
        } elsif ($@) {
227
            ERROR "[ERROR] Error in node communication with $nodedesignation: "
×
228
                .$@;
229
            return;
×
230
        }
231

232
FETCH_OK:
233
        # Everything went smoothly.
234
        DEBUG "[DEBUG] Everything went smoothly.";
25✔
235
        return 1;
25✔
236

237
    }); # do_in_session
25✔
238

239
    # This handles failure in do_in_session,
240
    return 0 if ! $done || ! $done->{exit_value};
25✔
241

242
    return 1;
25✔
243
}
244

245
sub _db_url {
246
        my ($self, $type, $id, $path, $p_type, $p_id) = @_;
11,315✔
247
        my $dbh = $self->{dbh};
11,315✔
248

249
        if ($p_type) {
11,315✔
250
                my $sth_g_url = $dbh->prepare_cached("SELECT path FROM url WHERE type = ? AND id = ?");
11,275✔
251
                $sth_g_url->execute($p_type, $p_id);
11,275✔
252
                my ($p_path) = $sth_g_url->fetchrow_array();
11,275✔
253
                $sth_g_url->finish();
11,275✔
254

255
                # prefix with the parent URL if provided
256
                $path = "$p_path/$path" if $p_path;
11,275✔
257
        }
258

259
        my $sth_u_url = $dbh->prepare_cached("UPDATE url SET path = ? WHERE type = ? AND id = ?");
11,315✔
260
        my $nb_rows_affected = $sth_u_url->execute($path, $type, $id);
11,315✔
261
        unless ($nb_rows_affected > 0) {
11,315✔
262
                my $sth_url = $dbh->prepare_cached('INSERT INTO url (type, id, path) VALUES (?, ?, ?)');
326✔
263
                $sth_url->execute($type, $id, $path);
326✔
264
        }
265
}
266

267
sub _db_mkgrp {
268
        my ($self, $group) = @_;
40✔
269
        my $dbh = $self->{dbh};
40✔
270

271
        DEBUG "group:".Dumper($group);
40✔
272

273
        # Recursively creates the group path
274
        my $p_id = 0; # XXX - 0 is a magic number that says NO_PARENT, as the == NULL doesn't work
40✔
275

276
        $p_id = $self->_db_mkgrp($group->{group}) if defined $group->{group};
40✔
277

278
        my $grp_name = $group->{group_name};
40✔
279

280
        # Create the group if needed
281
        my $sth_grp_id;
40✔
282
        if (defined $p_id) {
40✔
283
                $sth_grp_id = $dbh->prepare_cached("SELECT id FROM grp WHERE name = ? AND p_id = ?");
40✔
284
                $sth_grp_id->execute($grp_name, $p_id);
40✔
285
        } else {
286
                $sth_grp_id = $dbh->prepare_cached("SELECT id FROM grp WHERE name = ? AND p_id IS NULL");
×
287
                $sth_grp_id->execute($grp_name);
×
288
        }
289
        my ($grp_id) = $sth_grp_id->fetchrow_array();
40✔
290
        $sth_grp_id->finish();
40✔
291

292
        if (! defined $grp_id) {
40✔
293
                # Create the Group
294
                my $sth_grp = $dbh->prepare_cached('INSERT INTO grp (name, p_id, path) VALUES (?, ?, ?)');
16✔
295
                my $path = "";
16✔
296
                $sth_grp->execute($grp_name, $p_id, $path);
16✔
297
                $grp_id = _get_last_insert_id($dbh, "grp");
16✔
298
        } else {
299
                # Nothing to do, the grp doesn't need any updates anyway.
300
                # Removal of grp is *unsupported* yet.
301
        }
302

303
        $self->_db_url("group", $grp_id, $grp_name);
40✔
304

305
        return $grp_id;
40✔
306
}
307

308
# This should go in a generic DB.pm
309
sub _get_last_insert_id {
310
        my ($dbh, $tablename) = @_;
1,826✔
311
        return $dbh->last_insert_id("", "", $tablename, "");
1,826✔
312
}
313

314
sub _db_node {
315
        my ($self, $grp_id, $node_name) = @_;
25✔
316
        my $dbh = $self->{dbh};
25✔
317

318
        DEBUG "_db_node($grp_id, $node_name)";
25✔
319

320
        my $sth_node_id = $dbh->prepare_cached("SELECT id FROM node WHERE grp_id = ? AND name = ?");
25✔
321
        $sth_node_id->execute($grp_id, $node_name);
25✔
322
        my ($node_id) = $sth_node_id->fetchrow_array();
25✔
323
        $sth_node_id->finish();
25✔
324

325
        if (! defined $node_id) {
25✔
326
                # Create the node
327
                my $sth_node = $dbh->prepare_cached('INSERT INTO node (grp_id, name, path) VALUES (?, ?, ?)');
10✔
328
                my $path = "";
10✔
329
                $sth_node->execute($grp_id, $node_name, $path);
10✔
330
                $node_id = _get_last_insert_id($dbh, "node");
10✔
331
        } else {
332
                # Nothing to do, the node doesn't need any updates anyway.
333
                # Removal of nodes is *unsupported* yet.
334
        }
335

336
        $self->_db_url("node", $node_id, $node_name, "group", $grp_id);
25✔
337

338
        DEBUG "_db_node() = $node_id";
25✔
339
        return $node_id;
25✔
340
}
341

342
sub _db_service {
343
        my ($self, $plugin, $service_attr, $fields) = @_;
11,250✔
344
        my $dbh = $self->{dbh};
11,250✔
345
        my $node_id = $self->{node_id};
11,250✔
346

347
        DEBUG "_db_service($node_id, $plugin)";
11,250✔
348
        DEBUG "_db_service.service_attr:".Dumper($service_attr);
11,250✔
349
        DEBUG "_db_service.fields:".Dumper($fields);
11,250✔
350

351
        # Save the whole service config, and drop it.
352
        my $sth_service_id = $dbh->prepare_cached("SELECT id FROM service WHERE node_id = ? AND name = ?");
11,250✔
353
        $sth_service_id->execute($node_id, $plugin);
11,250✔
354
        my ($service_id) = $sth_service_id->fetchrow_array();
11,250✔
355
        $sth_service_id->finish();
11,250✔
356

357
        if (! defined $service_id) {
11,250✔
358
                # Doesn't exist yet, create it
359
                my $sth_service = $dbh->prepare_cached("INSERT INTO service (node_id, name) VALUES (?, ?)");
300✔
360
                $sth_service->execute($node_id, $plugin);
300✔
361
                $service_id = _get_last_insert_id($dbh, "service");
300✔
362
        }
363

364
        DEBUG "_db_service.service_id:$service_id";
11,250✔
365

366
        # Save the existing values
367
        my (%service_attrs_old, %fields_old);
11,250✔
368
        {
369
                my $sth_service_attrs = $dbh->prepare_cached("SELECT name, value FROM service_attr WHERE id = ?");
11,250✔
370
                $sth_service_attrs->execute($service_id);
11,250✔
371

372
                while (my ($_name, $_value) = $sth_service_attrs->fetchrow_array()) {
11,250✔
373
                        $service_attrs_old{$_name} = $_value;
33,000✔
374
                }
375
                $sth_service_attrs->finish();
11,250✔
376

377
                my $sth_fields_attr = $dbh->prepare_cached("SELECT ds.name as field, ds_attr.name as attr, ds_attr.value FROM ds
11,250✔
378
                        LEFT OUTER JOIN ds_attr ON ds.id = ds_attr.id WHERE ds.service_id = ?");
379
                $sth_fields_attr->execute($service_id);
11,250✔
380

381
                my %fields_old;
11,250✔
382
                while (my ($_field, $_name, $_value) = $sth_fields_attr->fetchrow_array()) {
11,250✔
383
                        $fields_old{$_field}{$_name} = $_value;
219,750✔
384
                }
385
                $sth_fields_attr->finish();
11,250✔
386
        }
387

388
        DEBUG "_db_service.%service_attrs_old:" . Dumper(\%service_attrs_old);
11,250✔
389
        DEBUG "_db_service.%fields_old:" . Dumper(\%fields_old);
11,250✔
390

391
        # Leave room for refresh
392
        # XXX - we might only update DB with diff.
393
        my $sth_service_attrs_del = $dbh->prepare_cached("DELETE FROM service_attr WHERE id = ?");
11,250✔
394
        $sth_service_attrs_del->execute($service_id);
11,250✔
395

396
        for my $attr (keys %$service_attr) {
11,250✔
397
                my $_service_value = $service_attr->{$attr};
34,050✔
398
                $self->_db_service_attr($service_id, $attr, $_service_value);
34,050✔
399
        }
400

401
        # Handle the service_category
402
        {
403
                my $category = $service_attr->{graph_category} || "other";
11,250✔
404

405
                # XXX - might only INSERT IT IF NOT PRESENT
406
                my $sth_service_cat_del = $dbh->prepare_cached("DELETE FROM service_categories WHERE id = ? and category = ?");
11,250✔
407
                $sth_service_cat_del->execute($service_id, $category);
11,250✔
408

409
                my $sth_service_cat = $dbh->prepare_cached("INSERT INTO service_categories (id, category) VALUES (?, ?)");
11,250✔
410
                $sth_service_cat->execute($service_id, $category);
11,250✔
411
        }
412

413
        # Handle the fields
414

415
        # Remove the ds_attr rows
416
        {
417
                my $sth_del_attr = $dbh->prepare_cached('DELETE FROM ds_attr WHERE id IN (SELECT id FROM ds WHERE service_id = ?)');
11,250✔
418
                $sth_del_attr->execute($service_id);
11,250✔
419
        }
420

421
        my %ds_ids;
11,250✔
422
        for my $field_name (keys %$fields) {
11,250✔
423
                my $_field_attrs = $fields->{$field_name};
56,250✔
424

425

426
                my $ds_id = $self->_db_ds_update($service_id, $field_name, $_field_attrs);
56,250✔
427
                $ds_ids{$field_name} = $ds_id;
56,250✔
428
        }
429

430
        # Purge the ds that have no attributes, as they are not relevant anymore
431
        {
432
                my $sth_del_ds = $dbh->prepare_cached('DELETE FROM ds WHERE service_id = ? AND NOT EXISTS (SELECT * FROM ds_attr WHERE ds_attr.id = ds.id)');
11,250✔
433
                $sth_del_ds->execute($service_id);
11,250✔
434
        }
435

436
        # Update the ordering of fields
437
        {
438
                my @graph_order = split(/ /, $service_attr->{graph_order});
11,250✔
439
                DEBUG "_db_service.graph_order: @graph_order";
11,250✔
440
                my $ordr = 0;
11,250✔
441
                for my $_name (@graph_order) {
11,250✔
442
                        my $sth_update_ordr = $dbh->prepare_cached("UPDATE ds SET ordr = ? WHERE ds.service_id = ? AND ds.name = ?");
56,250✔
443
                        $sth_update_ordr->execute($ordr, $service_id, $_name);
56,250✔
444
                        DEBUG "_db_service.update_order($ordr, $service_id, $_name)";
56,250✔
445
                        $ordr ++;
56,250✔
446
                }
447
        }
448

449
        $self->_db_url("service", $service_id, $plugin, "node", $node_id);
11,250✔
450

451
        DEBUG "_db_service() = $service_id";
11,250✔
452

453
        return ($service_id, \%service_attrs_old, \%fields_old, \%ds_ids);
11,250✔
454
}
455

456
sub _db_service_attr {
457
        my ($self, $service_id, $name, $value) = @_;
34,050✔
458
        my $dbh = $self->{dbh};
34,050✔
459

460
        DEBUG "_db_service_attr($service_id, $name, $value)";
34,050✔
461

462
        # Save the whole service config, and drop it.
463
        my $sth_service_attr = $dbh->prepare_cached("INSERT INTO service_attr (id, name, value) VALUES (?, ?, ?)");
34,050✔
464
        $sth_service_attr->execute($service_id, $name, $value);
34,050✔
465
}
466

467
sub _db_ds_update {
468
        my ($self, $service_id, $field_name, $attrs) = @_;
56,250✔
469
        my $dbh = $self->{dbh};
56,250✔
470

471
        DEBUG "_db_ds_update($service_id, $field_name, $attrs)";
56,250✔
472

473
        my $sth_id = $dbh->prepare_cached("SELECT id FROM ds WHERE service_id = ? AND name = ?");
56,250✔
474
        $sth_id->execute($service_id, $field_name);
56,250✔
475

476
        my ($ds_id) = $sth_id->fetchrow_array();
56,250✔
477
        $sth_id->finish();
56,250✔
478

479
        if (! defined $ds_id) {
56,250✔
480
                # Doesn't exist yet, create it
481
                my $sth_ds = $dbh->prepare_cached("INSERT INTO ds (service_id, name) VALUES (?, ?)");
1,500✔
482
                $sth_ds->execute($service_id, $field_name);
1,500✔
483
                $ds_id = _get_last_insert_id($dbh, "ds");
1,500✔
484
        }
485

486
        # Reinsert the other rows
487
        my $sth_ds_attr = $dbh->prepare_cached('INSERT INTO ds_attr (id, name, value) VALUES (?, ?, ?)');
56,250✔
488
        for my $field_attr (keys %$attrs) {
56,250✔
489
                my $_value = $attrs->{$field_attr};
114,000✔
490
                $sth_ds_attr->execute($ds_id, $field_attr, $_value);
114,000✔
491
        }
492

493
        return $ds_id;
56,250✔
494
}
495

496
sub _db_state_update {
497
        my ($self, $plugin, $field, $when, $value) = @_;
56,250✔
498
        my $dbh = $self->{dbh};
56,250✔
499
        my $node_id = $self->{node_id};
56,250✔
500

501
        DEBUG "_db_state_update($plugin, $field, $when, $value)";
56,250✔
502
        DEBUG "_db_state_update.node_id:$node_id";
56,250✔
503
        my $sth_ds = $dbh->prepare_cached("
56,250✔
504
                SELECT ds.id FROM ds
505
                JOIN service s ON ds.service_id = s.id AND s.node_id = ? AND s.name = ?
506
                WHERE ds.name = ?");
507
        $sth_ds->execute($node_id, $plugin, $field);
56,250✔
508
        my ($ds_id) = $sth_ds->fetchrow_array();
56,250✔
509
        DEBUG "_db_state_update.ds_id:$ds_id";
56,250✔
510
        WARN "ds_id($plugin, $field, $when, $value) is NULL, SELECT ds.id FROM ds
56,250✔
511
                        JOIN service s ON ds.service_id = s.id AND s.node_id = '$node_id' AND s.name = '$plugin'
512
                                        WHERE ds.name = '$field'" unless $ds_id;
513
        $sth_ds->finish();
56,250✔
514

515
        # Update the state with the new values
516
        my $sth_state_u = $dbh->prepare_cached("UPDATE state SET prev_epoch = last_epoch, prev_value = last_value, last_epoch = ?, last_value = ? WHERE id = ? AND type = ?");
56,250✔
517
        my $rows_u = $sth_state_u->execute($when, $value, $ds_id, "ds");
56,250✔
518
        if ($rows_u eq "0E0") {
56,250✔
519
                # No line exists yet. Create It.
520
                my $sth_state_i = $dbh->prepare_cached("INSERT INTO state (id, type) VALUES (?, ?)");
1,500✔
521
                $sth_state_i->execute($ds_id, "ds");
1,500✔
522
        }
523

524
        return $ds_id;
56,250✔
525
}
526

527

528
sub is_fresh_enough {
529
        my ($self, $update_rate, $last_timestamp, $now) = @_;
300✔
530

531
        DEBUG "is_fresh_enough($update_rate, $last_timestamp, $now)";
300✔
532

533
        my ($update_rate_in_sec, $is_update_aligned) = parse_update_rate($update_rate);
300✔
534

535
        DEBUG "update_rate_in_sec:$update_rate_in_sec";
300✔
536

537
        my $age = $now - $last_timestamp;
300✔
538

539
        DEBUG "now:$now, age:$age";
300✔
540

541
        my $is_fresh_enough = ($age < $update_rate_in_sec) ? 1 : 0;
300✔
542
        DEBUG "is_fresh_enough  $is_fresh_enough";
300✔
543

544
        return $is_fresh_enough;
300✔
545
}
546

547
sub get_spoolfetch_timestamp {
548
        my ($self) = @_;
15✔
549
        my $dbh = $self->{dbh};
15✔
550
        my $node_id = $self->{node_id};
15✔
551

552
        my $sth_spoolfetch = $dbh->prepare_cached("SELECT spoolepoch FROM node WHERE id = ?");
15✔
553
        $sth_spoolfetch->execute($node_id);
15✔
554
        my ($last_updated_value) = $sth_spoolfetch->fetchrow_array();
15✔
555
        $sth_spoolfetch->finish();
15✔
556

557
        # 0 if unset
558
        $last_updated_value = 0 unless $last_updated_value;
15✔
559

560
        DEBUG "[DEBUG] get_spoolfetch_timestamp($node_id) = $last_updated_value";
15✔
561
        return $last_updated_value;
15✔
562
}
563

564
sub set_spoolfetch_timestamp {
565
        my ($self, $timestamp) = @_;
15✔
566
        my $dbh = $self->{dbh};
15✔
567
        my $node_id = $self->{node_id};
15✔
568
        DEBUG "[DEBUG] set_spoolfetch_timestamp($node_id, $timestamp)";
15✔
569

570
        my $sth_spoolfetch = $dbh->prepare_cached("UPDATE node SET spoolepoch = ? WHERE id = ?");
15✔
571
        $sth_spoolfetch->execute($timestamp, $node_id);
15✔
572
        $sth_spoolfetch->finish();
15✔
573
}
574

575
sub parse_update_rate {
576
        my ($update_rate_config) = @_;
67,805✔
577

578
        my ($update_rate_in_sec, $is_update_aligned);
67,805✔
579
        if ($update_rate_config =~ m/(\d+[a-z]?)(?: (.*))?/) {
67,805✔
580
                $update_rate_in_sec = to_sec($1);
67,805✔
581
                $is_update_aligned = $2 && ($2 eq "aligned");
67,805✔
582
        } else {
583
                return (0, 0);
×
584
        }
585

586
        $is_update_aligned ||= 0;
67,805✔
587

588
        return ($update_rate_in_sec, $is_update_aligned);
67,805✔
589
}
590

591
sub round_to_granularity {
592
        my ($when, $granularity_in_sec) = @_;
403✔
593
        $when = time if ($when eq "N"); # N means "now"
403✔
594

595
        my $rounded_when = $when - ($when % $granularity_in_sec);
403✔
596
        return $rounded_when;
403✔
597
}
598

599

600
# For the uw_handle_* :
601
# The $data has been already sanitized :
602
# * chomp()
603
# * comments are removed
604
# * empty lines are removed
605

606
# This handles one config part.
607
# - It will automatically call uw_handle_fetch to handle dirty_config
608
# - In case of multigraph (or spoolfetch) the caller has to call this for every multigraph part
609
# - It handles empty $data, and just does nothing
610
#
611
# Returns the last updated timestamp
612
sub uw_handle_config {
613
        my ($self, $plugin, $now, $data, $last_timestamp, $update_rate_ptr) = @_;
11,250✔
614

615
        # Protect oneself against multiple, conflicting multigraphs
616
        if (defined $self->{__SEEN_PLUGINS__} && $self->{__SEEN_PLUGINS__}{$plugin} ++) {
11,250✔
617
                WARN "uw_handle_config: $plugin is already configured, skipping";
×
618
                return $last_timestamp;
×
619
        }
620

621
        # Begin transaction if not already in a transaction
622
        $self->{dbh}->begin_work() if $self->{dbh}->{AutoCommit};
11,250✔
623

624
        # Build FETCH data, just in case of dirty_config.
625
        my @fetch_data;
11,250✔
626

627
        # Parse the output to a simple HASH
628
        my %service_attr = ();
11,250✔
629
        my %fields = ();
11,250✔
630
        my @field_order = ();
11,250✔
631
        for my $line (@$data) {
11,250✔
632
                DEBUG "uw_handle_config: $line";
191,550✔
633
                # Barbaric regex to parse the output of the config
634
                # graph_title hymir : Sea giant ===> $arg1: "graph_title", $arg2: undef, $value: "hymir : Sea giant"
635
                next unless ($line =~ m{^([^\.\s]+)(?:\.(\S+))?\s+?(.+)$});
191,550✔
636
                my ($arg1, $arg2, $value) = ($1, $2, $3);
191,550✔
637

638
                if (! $arg2) {
191,550✔
639
                        # This is a service config line
640
                        $service_attr{$arg1} = $value;
22,800✔
641
                        next; # Handled
22,800✔
642
                }
643

644
                # Handle dirty_config
645
                if ($arg2 && $arg2 eq "value") {
168,750✔
646
                        push @fetch_data, $line;
54,750✔
647
                        next; # Handled
54,750✔
648
                }
649

650
                # Prevent plugins from trying to set rrd:* attrs
651
                if ($arg2 =~ /^rrd:/) {
114,000✔
652
                        WARN "Invalid line: $line";
×
653
                        next;
×
654
                }
655

656
                # Adding the $field if not seen before.
657
                if (!exists($fields{$arg1})) {
114,000✔
658
                        push @field_order, $arg1;
56,250✔
659
                }
660

661
                $fields{$arg1}{$arg2} = $value;
114,000✔
662
        }
663

664
        $$update_rate_ptr = $service_attr{"update_rate"} if $service_attr{"update_rate"};
11,250✔
665

666
        # Merging graph_order & field_order
667
        {
668
                my @graph_order = split(/ /, $service_attr{"graph_order"} || "");
11,250✔
669
                for my $field (@field_order) {
11,250✔
670
                        # filter out of _exact_ equality
671
                        push @graph_order, $field unless grep { $_ eq $field } @graph_order;
56,250✔
672
                }
673

674
                $service_attr{"graph_order"} = join(" ", @graph_order);
11,250✔
675
        }
676

677
        # Always provide a default graph_title
678
        $service_attr{graph_title} = $plugin unless defined $service_attr{graph_title};
11,250✔
679

680
        # Sync to database
681
        # Create/Update the service
682
        my ($service_id, $service_attrs_old, $fields_old, $ds_ids) = $self->_db_service($plugin, \%service_attr, \%fields);
11,250✔
683

684
        # Create the RRDs
685
        for my $ds_name (keys %fields) {
11,250✔
686
                # Merge attributes from ds & service
687
                my $ds_config = { %service_attr, %{$fields{$ds_name}} };
56,250✔
688
                my $ds_id = $ds_ids->{$ds_name};
56,250✔
689

690
                my $first_epoch = time - (12 * 3600); # XXX - we should be able to have some delay in the past for spoolfetched plugins
56,250✔
691
                my $rrd_file = $self->_create_rrd_file_if_needed($plugin, $ds_name, $ds_config, $first_epoch);
56,250✔
692

693
                # Update the RRD file
694
                # XXX - Should be handled in a stateful way, as now it is reconstructed every time
695
                my $dbh = $self->{dbh};
56,250✔
696
                my $sth_ds_attr = $dbh->prepare_cached('INSERT INTO ds_attr (id, name, value) VALUES (?, ?, ?)');
56,250✔
697
                $sth_ds_attr->execute($ds_id, "rrd:file", $rrd_file);
56,250✔
698
                $sth_ds_attr->execute($ds_id, "rrd:field", "42");
56,250✔
699
        }
700

701
        # timestamp == 0 means "Nothing was updated". We only count on the
702
        # "fetch" part to provide us good timestamp info, as the "config" part
703
        # doesn't contain any, specially in case we are spoolfetching.
704
        #
705
        # Also, the caller can override the $last_timestamp, to be called in a loop
706
        $last_timestamp = 0 unless defined $last_timestamp;
11,250✔
707

708
        # Delegate the FETCH part
709
        my $update_rate = $service_attr{"update_rate"} || 300;
11,250✔
710
        my $timestamp;
11,250✔
711
        $timestamp = $self->uw_handle_fetch($plugin, $now, $update_rate, \@fetch_data) if (@fetch_data);
11,250✔
712
        $last_timestamp = $timestamp if $timestamp && $timestamp > $last_timestamp;
11,250✔
713

714
        $self->{dbh}->commit() unless $self->{dbh}->{AutoCommit};
11,250✔
715
        return $last_timestamp;
11,250✔
716
}
717

718
# This handles one fetch part.
719
# Returns the last updated timestamp
720
sub uw_handle_fetch {
721
        my ($self, $plugin, $now, $update_rate, $data) = @_;
11,250✔
722

723
        # timestamp == 0 means "Nothing was updated"
724
        my $last_timestamp = 0;
11,250✔
725

726
        $self->{dbh}->begin_work() if $self->{dbh}->{AutoCommit};
11,250✔
727

728
        my ($update_rate_in_seconds, $is_update_aligned) = parse_update_rate($update_rate);
11,250✔
729

730
        # Process all the data in-order
731
        for my $line (@$data) {
11,250✔
732
                next if ($line =~ m/^#/); # Ignore lines with comments
56,250✔
733
                next unless ($line =~ m{\A ([^\.]+)(?:\.(\S+))? \s+ ([\S:]+) }xms);
56,250✔
734
                my ($field, $arg, $value) = ($1, $2, $3);
56,250✔
735
                $arg = "" unless defined $arg;
56,250✔
736
                if ($arg ne "value") {
56,250✔
737
                        WARN "got '$line' but it should not be part of a fetch reply as (arg:$arg), ignoring.";
×
738
                        next;
×
739
                }
740

741
                my $when = $now; # Default is NOW, unless specified
56,250✔
742
                my $when_is_now = 1;
56,250✔
743
                if ($value =~ /^(\d+):(.+)$/) {
56,250✔
744
                        $when = $1;
54,750✔
745
                        $when_is_now = 0;
54,750✔
746
                        $value = $2;
54,750✔
747
                }
748

749
                use Scalar::Util qw(looks_like_number);
32✔
750
                WARN "asked to parse '$line' and got value=$value" unless looks_like_number($value) || $value eq "U";
56,250✔
751

752
                # Always round the $when if plugin asks for. Rounding the plugin-provided
753
                # time is weird, but we are doing it to follow the "least surprise principle".
754
                $when = round_to_granularity($when, $update_rate_in_seconds) if $is_update_aligned;
56,250✔
755

756
                # Update last_timestamp if the current update is more recent
757
                $last_timestamp = $when if (!$when_is_now && $when > $last_timestamp);
56,250✔
758

759
                # Update all data-driven components: State, RRD, Graphite
760
                my $ds_id = $self->_db_state_update($plugin, $field, $when, $value);
56,250✔
761
                DEBUG "[DEBUG] ds_id($plugin, $field, $when, $value) = $ds_id";
56,250✔
762

763
                my ($rrd_file, $rrd_field);
56,250✔
764
                {
765
                        # XXX - Quite inefficient, but works
766
                        my $dbh = $self->{dbh};
56,250✔
767
                        my $sth_rrdinfos = $dbh->prepare_cached(
56,250✔
768
                                "SELECT name, value FROM ds_attr WHERE id = ? AND name in (
769
                                        'rrd:file',
770
                                        'rrd:field'
771
                                )"
772
                        );
773
                        $sth_rrdinfos->execute($ds_id);
56,250✔
774
                        while ( my @row = $sth_rrdinfos->fetchrow_array ) {
56,250✔
775
                                $rrd_file  = $row[1] if $row[0] eq "rrd:file";
112,500✔
776
                                $rrd_field = $row[1] if $row[0] eq "rrd:field";
112,500✔
777
                        }
778
                        $sth_rrdinfos->finish();
56,250✔
779
                }
780

781
                # This is a little convoluted but is needed as the API permits
782
                # vectorized updates
783
                my $ds_values = {
56,250✔
784
                        "value" => [ $value, ],
785
                        "when" => [ $when, ],
786
                };
787
                DEBUG "[DEBUG] self->_update_rrd_file($rrd_file, $field, $ds_values";
56,250✔
788
                $self->_update_rrd_file($rrd_file, $field, $ds_values);
56,250✔
789

790
        }
791

792
        $self->{dbh}->commit() unless $self->{dbh}->{AutoCommit};
11,250✔
793

794
        return $last_timestamp;
11,250✔
795
}
796

797
sub _get_rrd_data_source_with_defaults {
798
    my ($self, $data_source) = @_;
112,500✔
799

800
    # Copy it into a new hash, we don't want to alter the $data_source
801
    # and anything already defined should not be overridden by defaults
802
    my $ds_with_defaults = {
803
            type => 'GAUGE',
804
            min => 'U',
805
            max => 'U',
806

807
            update_rate => ($config->{update_rate} || 300),
808
            graph_data_size => ($config->{graph_data_size} || "normal"),
112,500✔
809
    };
810
    for my $key (keys %$data_source) {
112,500✔
811
            $ds_with_defaults->{$key} = $data_source->{$key};
568,500✔
812
    }
813

814
    return $ds_with_defaults;
112,500✔
815
}
816

817

818
sub _create_rrd_file_if_needed {
819
    my ($self, $service, $ds_name, $ds_config, $first_epoch) = @_;
56,250✔
820

821
    my $rrd_file = $self->_get_rrd_file_name($service, $ds_name, $ds_config);
56,250✔
822
    unless (-f $rrd_file) {
56,250✔
823
        $self->_create_rrd_file($rrd_file, $service, $ds_name, $ds_config, $first_epoch);
56,250✔
824
    }
825

826
    return $rrd_file;
56,250✔
827
}
828

829

830
sub _get_rrd_file_name {
831
    my ($self, $service, $ds_name, $ds_config) = @_;
56,250✔
832

833
    $ds_config = $self->_get_rrd_data_source_with_defaults($ds_config);
56,250✔
834
    my $type_id = lc(substr(($ds_config->{type}), 0, 1));
56,250✔
835

836
    my $path = $self->{host}->get_full_path;
56,250✔
837
    $path =~ s{[;:]}{/}g;
56,250✔
838

839
    # Multigraph/nested services will have . in the service name in this function.
840
    $service =~ s{\.}{-}g;
56,250✔
841

842
    my $file = sprintf("%s-%s-%s-%s.rrd",
56,250✔
843
                       $path,
844
                       $service,
845
                       $ds_name,
846
                       $type_id);
847

848
    DEBUG "[DEBUG] rrd filename: $file\n";
56,250✔
849

850
    return $file;
56,250✔
851
}
852

853

854
sub _create_rrd_file {
855
    my ($self, $rrd_file, $service, $ds_name, $ds_config, $first_epoch) = @_;
56,250✔
856

857
    INFO "[INFO] creating rrd-file for $service->$ds_name: '$rrd_file'";
56,250✔
858

859
    $rrd_file = File::Spec->catfile($config->{dbdir}, $rrd_file);
56,250✔
860

861
    munin_mkdir_p(dirname($rrd_file), oct(777));
56,250✔
862

863
    my @args;
56,250✔
864

865
    $ds_config = $self->_get_rrd_data_source_with_defaults($ds_config);
56,250✔
866
    my $resolution = $ds_config->{graph_data_size};
56,250✔
867
    my ($update_rate_in_sec, $is_update_aligned) = parse_update_rate($ds_config->{update_rate});
56,250✔
868
    if ($resolution eq 'normal') {
56,250✔
869
        $update_rate_in_sec = 300; # 'normal' means hard coded RRD $update_rate
50✔
870
        push (@args,
50✔
871
              "RRA:AVERAGE:0.5:1:576",   # resolution 5 minutes
872
              "RRA:MIN:0.5:1:576",
873
              "RRA:MAX:0.5:1:576",
874
              "RRA:AVERAGE:0.5:6:432",   # 9 days, resolution 30 minutes
875
              "RRA:MIN:0.5:6:432",
876
              "RRA:MAX:0.5:6:432",
877
              "RRA:AVERAGE:0.5:24:540",  # 45 days, resolution 2 hours
878
              "RRA:MIN:0.5:24:540",
879
              "RRA:MAX:0.5:24:540",
880
              "RRA:AVERAGE:0.5:288:450", # 450 days, resolution 1 day
881
              "RRA:MIN:0.5:288:450",
882
              "RRA:MAX:0.5:288:450");
883
    } elsif ($resolution eq 'huge') {
884
        $update_rate_in_sec = 300; # 'huge' means hard coded RRD $update_rate
50✔
885
        push (@args,
50✔
886
              "RRA:AVERAGE:0.5:1:115200",  # resolution 5 minutes, for 400 days
887
              "RRA:MIN:0.5:1:115200",
888
              "RRA:MAX:0.5:1:115200");
889
    } elsif ($resolution eq 'debug') {
890
        $update_rate_in_sec = 300; # 'debug' means hard coded RRD $update_rate
56,050✔
891
        push (@args,
56,050✔
892
              "RRA:AVERAGE:0.5:1:42",  # resolution 5 minutes, for 42 steps
893
              "RRA:MIN:0.5:1:42",
894
              "RRA:MAX:0.5:1:42");
895
    } elsif ($resolution =~ /^custom (.+)/) {
896
        # Parsing resolution to achieve computer format as defined on the RFC :
897
        # FULL_NB, MULTIPLIER_1 MULTIPLIER_1_NB, ... MULTIPLIER_NMULTIPLIER_N_NB
898
        my @resolutions_computer = parse_custom_resolution($1, $update_rate_in_sec);
100✔
899
        my @enlarged_resolutions = enlarge_custom_resolution(@resolutions_computer);
100✔
900
        foreach my $resolution_computer(@resolutions_computer) {
100✔
901
            my ($multiplier, $multiplier_nb) = @{$resolution_computer};
250✔
902
            push (@args,
250✔
903
                "RRA:AVERAGE:0.5:$multiplier:$multiplier_nb",
904
                "RRA:MIN:0.5:$multiplier:$multiplier_nb",
905
                "RRA:MAX:0.5:$multiplier:$multiplier_nb"
906
            );
907
        }
908
    }
909

910
    # Add the RRD::create prefix (filename & RRD params)
911
    my $heartbeat = $update_rate_in_sec * 2;
56,250✔
912
    $first_epoch -= $first_epoch % $update_rate_in_sec; # the RRD start should _always_ be aligned
56,250✔
913
    unshift (@args,
914
        $rrd_file,
915
        "--start", ($first_epoch - $update_rate_in_sec),
916
        "-s", $update_rate_in_sec,
917
        sprintf('DS:42:%s:%s:%s:%s',
918
                $ds_config->{type}, $heartbeat, $ds_config->{min}, $ds_config->{max}),
56,250✔
919
    );
920

921
    INFO "[INFO] RRDs::create @args";
56,250✔
922
    RRDs::create @args unless $ENV{NO_UPDATE_RRD};
56,250✔
923
    if (my $ERROR = RRDs::error) {
56,250✔
924
        ERROR "[ERROR] Unable to create '$rrd_file': $ERROR";
×
925
    }
926
}
927

928
sub enlarge_custom_resolution {
929
        my @enlarged_resolutions;
100✔
930
        foreach my $resolution_computer(@_) {
100✔
931
            my ($multiplier, $multiplier_nb) = @{$resolution_computer};
250✔
932
            # Always add 10% to the RRA size, as specified in
933
            # http://munin-monitoring.org/wiki/format-graph_data_size
934
            $multiplier_nb += int ($multiplier_nb / 10) || 1;
250✔
935

936
            push @enlarged_resolutions, [
250✔
937
                $multiplier,
938
                $multiplier_nb,
939
            ];
940
        }
941

942
        return @enlarged_resolutions;
100✔
943
}
944

945
sub parse_custom_resolution {
946
        my @elems = split(',\s*', shift);
104✔
947
        my $update_rate = shift;
104✔
948

949
        DEBUG "[DEBUG] update_rate: $update_rate";
104✔
950

951
        my @computer_format;
104✔
952

953
        # First element is always the full resolution
954
        my $full_res = shift @elems;
104✔
955
        if ($full_res =~ m/^\d+$/) {
104✔
956
                # Only numeric, computer format
957
                unshift @elems, "1 $full_res";
102✔
958
        } else {
959
                # Human readable. Adding $update_rate in front of
960
                unshift @elems, "$update_rate for $full_res";
2✔
961
        }
962

963
        foreach my $elem (@elems) {
104✔
964
                if ($elem =~ m/(\d+) (\d+)/) {
307✔
965
                        # nothing to do, already in computer format
966
                        push @computer_format, [$1, $2];
203✔
967
                } elsif ($elem =~ m/(\w+) for (\w+)/) {
968
                        my $nb_sec = to_sec($1);
104✔
969
                        my $for_sec = to_sec($2);
104✔
970

971
                        my $multiplier = int ($nb_sec / $update_rate);
104✔
972
                        my $multiplier_nb = int ($for_sec / $nb_sec);
104✔
973

974
                        # Log & ignore if having a 0
975
                        unless ($multiplier && $multiplier_nb) {
104✔
976
                                ERROR "$elem"
50✔
977
                                        . " -> nb_sec:$nb_sec, for_sec:$for_sec"
978
                                        . " -> multiplier:$multiplier, multiplier_nb:$multiplier_nb";
979
                                next;
50✔
980
                        }
981

982
                        DEBUG "[DEBUG] $elem"
54✔
983
                                . " -> nb_sec:$nb_sec, for_sec:$for_sec"
984
                                . " -> multiplier:$multiplier, multiplier_nb:$multiplier_nb"
985
                        ;
986
                        push @computer_format, [$multiplier, $multiplier_nb];
54✔
987
                }
988
        }
989

990
        return @computer_format;
104✔
991
}
992

993
# return the number of seconds
994
# for the human readable format
995
# s : second,  m : minute, h : hour
996
# d : day, w : week, t : month, y : year
997
sub to_sec {
998
        my $secs_table = {
68,013✔
999
                "s" => 1,
1000
                "m" => 60,
1001
                "h" => 60 * 60,
1002
                "d" => 60 * 60 * 24,
1003
                "w" => 60 * 60 * 24 * 7,
1004
                "t" => 60 * 60 * 24 * 31, # a month always has 31 days
1005
                "y" => 60 * 60 * 24 * 365, # a year always has 365 days
1006
        };
1007

1008
        my ($target) = @_;
68,013✔
1009
        if ($target =~ m/(\d+)([smhdwty])/i) {
68,013✔
1010
                return $1 * $secs_table->{$2};
110✔
1011
        } else {
1012
                # no recognised unit, return the int value as seconds
1013
                return int $target;
67,903✔
1014
        }
1015
}
1016

1017
sub _update_rrd_file {
1018
        my ($self, $rrd_file, $ds_name, $ds_values) = @_;
56,250✔
1019

1020
        $rrd_file = File::Spec->catfile($config->{dbdir}, $rrd_file);
56,250✔
1021

1022
        my $values = $ds_values->{value};
56,250✔
1023

1024
        # Some kind of mismatch between fetch and config can cause this.
1025
        return unless defined($values);
56,250✔
1026

1027
        if ($config->{"rrdcached_socket"}) {
56,250✔
1028
                if (! -e $config->{"rrdcached_socket"} || ! -w $config->{"rrdcached_socket"}) {
×
1029
                        WARN "[WARN] RRDCached feature ignored: rrdcached socket not writable";
×
1030
                } elsif($RRDs::VERSION < 1.3){
1031
                        WARN "[WARN] RRDCached feature ignored: perl RRDs lib version must be at least 1.3. Version found: " . $RRDs::VERSION;
×
1032
                } else {
1033
                        # Using the RRDCACHED_ADDRESS environment variable, as
1034
                        # it is way less intrusive than the command line args.
1035
                        $ENV{RRDCACHED_ADDRESS} = $config->{"rrdcached_socket"};
×
1036
                }
1037
        }
1038

1039
        my @update_rrd_data;
56,250✔
1040

1041
        my ($current_updated_timestamp, $current_updated_value);
56,250✔
1042
        for (my $i = 0; $i < scalar @$values; $i++) {
56,250✔
1043
                my $value = $values->[$i];
56,250✔
1044
                my $when = $ds_values->{when}[$i];
56,250✔
1045

1046
                # Ignore values that are not in monotonic increasing timestamp for the RRD.
1047
                # Otherwise it will reject the whole update
1048
                next if ($current_updated_timestamp && $when <= $current_updated_timestamp);
56,250✔
1049

1050
                # RRDtool does not like scientific format so we convert it.
1051
                $value = convert_to_float($value);
56,250✔
1052

1053
                # Schedule for addition
1054
                push @update_rrd_data, "$when:$value";
56,250✔
1055

1056
                $current_updated_timestamp = $when;
56,250✔
1057
                $current_updated_value = $value;
56,250✔
1058
        }
1059

1060
        DEBUG "[DEBUG] Updating $rrd_file with @update_rrd_data";
56,250✔
1061
        if ($ENV{RRDCACHED_ADDRESS} && (scalar @update_rrd_data > 32) ) {
56,250✔
1062
                # RRDCACHED only takes about 4K worth of commands. If the commands is
1063
                # too large, we have to break it in smaller calls.
1064
                #
1065
                # Note that 32 is just an arbitrary chosen number. It might be tweaked.
1066
                #
1067
                # For simplicity we only call it with 1 update each time, as RRDCACHED
1068
                # will buffer for us as suggested on the rrd mailing-list.
1069
                # https://lists.oetiker.ch/pipermail/rrd-users/2011-October/018196.html
1070
                for my $update_rrd_data (@update_rrd_data) {
×
1071
                        DEBUG "RRDs::update($rrd_file, $update_rrd_data)";
×
1072
                        RRDs::update($rrd_file, $update_rrd_data) unless $ENV{NO_UPDATE_RRD};
×
1073
                        # Break on error.
1074
                        last if RRDs::error;
×
1075
                }
1076
        } else {
1077
                # normal vector-update the RRD
1078
                DEBUG "RRDs::update($rrd_file, @update_rrd_data)";
56,250✔
1079
                RRDs::update($rrd_file, @update_rrd_data) unless $ENV{NO_UPDATE_RRD};
56,250✔
1080
        }
1081

1082
        if (my $ERROR = RRDs::error) {
56,250✔
1083
                #confess Dumper @_;
1084
                ERROR "[ERROR] In RRD: Error updating $rrd_file: $ERROR";
×
1085
        }
1086

1087
        return $current_updated_timestamp;
56,250✔
1088
}
1089

1090
sub convert_to_float
1091
{
1092
        my $value = shift;
56,250✔
1093

1094
        # Only convert if it looks like scientific format
1095
        return $value unless ($value =~ /\d[Ee]([+-]?\d+)$/);
56,250✔
1096

1097
        my $magnitude = $1;
5✔
1098
        if ($magnitude < 0) {
5✔
1099
                # Preserve at least 4 significant digits
1100
                $magnitude = abs($magnitude) + 4;
5✔
1101
                $value = sprintf("%.*f", $magnitude, $value);
5✔
1102
        } else {
UNCOV
1103
                $value = sprintf("%.4f", $value);
×
1104
        }
1105

1106
        return $value
5✔
1107
}
1108

1109
sub _get_default_address
1110
{
1111
        my ($host) = @_;
54✔
1112

1113
        # As suggested by madduck in D:592213
1114
        #
1115
        # Might I suggest that the address parameter became optional and that
1116
        # in its absence, the node's name is treated as a FQDN?
1117
        #
1118
        # If the node is specified with a group name, then one could use the
1119
        # following heuristics : $node, $group.$node
1120
        #
1121
        # relative names might well work but should be tried last
1122

1123
        my $host_name = $host->{host_name};
54✔
1124
        my $group_name = $host->{group}->{group_name};
54✔
1125
        if ($host_name =~ m/\./ && _does_resolve($host_name)) {
54✔
1126
                return $host_name;
27✔
1127
        }
1128

1129
        if ($group_name =~ m/\./ && _does_resolve("$group_name.$host_name")) {
27✔
1130
                return "$group_name.$host_name";
×
1131
        }
1132

1133
        # Note that we do NOT care if relative names resolves or not, as it is
1134
        # our LAST chance anyway
1135
        return $host_name;
27✔
1136
}
1137

1138
sub _does_resolve
1139
{
1140
        my ($name) = @_;
27✔
1141

1142
        use Socket;
32✔
1143

1144
        # evaluates to "True" if it resolves
1145
        return gethostbyname($name);
27✔
1146
}
1147

1148

1149
1;
1150

1151

1152
__END__
1153

1154
=encoding utf8
1155

1156
=head1 NAME
1157

1158
Munin::Master::UpdateWorker - FIX
1159

1160
=head1 SYNOPSIS
1161

1162
FIX
1163

1164
=head1 METHODS
1165

1166
=over
1167

1168
=item B<new>
1169

1170
FIX
1171

1172
=item B<do_work>
1173

1174
FIX
1175

1176
=back
1177

1178
=head1 COPYING
1179

1180
  Copyright (C) 2010-2018 Steve Schnepp
1181
  Copyright (C) 2009-2013 Nicolai Langfeldt
1182
  Copyright (C) 2009 Kjell-Magne Øierud
1183
  Copyright (C) 2002-2009 Jimmy Olsen
1184

1185
  This program is free software; you can redistribute it and/or modify
1186
  it under the terms of the GNU General Public License as published by
1187
  the Free Software Foundation; version 2 dated June, 1991.
1188

1189
  This program is distributed in the hope that it will be useful,
1190
  but WITHOUT ANY WARRANTY; without even the implied warranty of
1191
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
1192
  GNU General Public License for more details.
1193

1194
  You should have received a copy of the GNU General Public License along
1195
  with this program; if not, write to the Free Software Foundation, Inc.,
1196
  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
1197

1198

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc