File: munin-run

package info (click to toggle)
munin 2.0.76-5
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 7,064 kB
  • sloc: perl: 11,684; java: 1,924; sh: 1,632; makefile: 636; javascript: 365; python: 267
file content (548 lines) | stat: -rwxr-xr-x 17,790 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
#!/usr/bin/perl -T
# -*- perl -*-
#
# Copyright and license: see bottom of file
#

use strict;
use warnings;

# Trust PERL5LIB from environment
use lib map { /(.*)/ } split(/:/, ($ENV{PERL5LIB} || ''));

use English qw(-no_match_vars);
use File::Temp;
use Getopt::Long;

use Munin::Common::Defaults;
use Munin::Node::Config;
use Munin::Node::OS;
use Munin::Node::Service;

my $services;
my $servicedir;
my $conffile = "$Munin::Common::Defaults::MUNIN_CONFDIR/munin-node.conf";
my $DEBUG    = 0;
my $PIDEBUG  = 0;
my $paranoia = 0;
my $ignore_systemd_properties = 0;
# The following parameters of "systemd-run" require rather recent systemd versions:
#   --wait: 232 or later
#   --pipe: 235 or later
#   --collect: 236 or later

my $REQUIRED_SYSTEMD_VERSION = 236;

# See "man systemd.exec" for the list of all systemd properties.
# The following properties belong to the relevant sections "Capabilities",
# "Security", "Mandatory Access Control", "Process Properties",
# "Sandboxing" and "System Call Filtering".
# These properties will be imported from the specification of
# "munin-node.service" if systemd is enabled.
# See "--ignore-systemd-properties" for details.
# See "get_systemd_hardening_flags" for a few exceptions from the list below.
my @SYSTEMD_PROPERTY_IMPORT_PATTERNS = qw(
    AmbientCapabilities
    AppArmorProfile
    CapabilityBoundingSet
    DynamicUser
    Environment
    EnvironmentFile
    Group
    Limit\w+
    LockPersonality
    MemoryDenyWriteExecute
    MountFlags
    NetworkNamespacePath
    NoNewPrivileges
    PassEnvironment
    Private\w+
    Protect\w+
    Restrict\w+
    SecureBits
    SELinuxContext
    SmackProcessLabel
    SystemCallArchitectures
    SystemCallFilter
    TemporaryFileSystem
    UMask
    UnsetEnvironment
    User
    \w+Directory
    \w+Paths
);

# The following environment variables are assigned automatically by
# systemd-run (see "man systemd.exec").  We should not override them
# when calling "systemd-run".
# See "--ignore-systemd-properties" for details.
my %ENVIRONMENT_IGNORE_HASH = map { $_ => 1 } qw(
    PATH
    LANG
    USER
    HOME
    SHELL
    LOGNAME
    INVOCATION_ID
    XDG_RUNTIME_DIR
    RUNTIME_DIRECTORY
    STATE_DIRECTORY
    CACHE_DIRECTORY
    LOGS_DIRECTORY
    CONFIGURATION_DIRECTORY
    MAINPID
    MANAGERPID
    LISTEN_FDS
    LISTEN_PID
    LISTEN_FDNAMES
    NOTIFY_SOCKET
    WATCHDOG_PID
    WATCHDOG_USEC
    TERM
    JOURNAL_STREAM
    SERVICE_RESULT
    EXIT_CODE
    EXIT_STATUS
    PIDFILE
);

my $config = Munin::Node::Config->instance();


sub main
{
    # "Clean" environment to disable taint-checking on the environment. We _know_
    # that the environment is insecure, but we want to let admins shoot themselves
    # in the foot with it, if they want to.
    foreach my $key (keys %ENV) {
        $ENV{$key} =~ /^(.*)$/s;
        $ENV{$key} = $1;
    }

    $0 =~ /^(.*)$/;
    $0 = $1;

    my @original_argv = @ARGV;
    my ($plugin, @args) = parse_args();

    # Loads the settings from munin-node.conf.
    # Ensures that, where options can be set both in the config and in
    # @ARGV, the latter takes precedence.
    $paranoia = $config->{paranoia};

    my $config = Munin::Node::Config->instance();
    $config->parse_config_from_file($conffile);

    # Run directly or execute recursively via "systemd-run".
    if (($ignore_systemd_properties) || (! -d "/run/systemd/system")) {
        return execute_plugin($plugin, @args);
    } elsif (!check_systemd_run_permissions()) {
        print STDERR "# Skipping systemd properties simulation due to lack of permissions.\n" if $config->{DEBUG};
        return execute_plugin($plugin, @args);
    } else {
        my $systemd_version = get_systemd_version();
        if ((not defined $systemd_version) or ($systemd_version < $REQUIRED_SYSTEMD_VERSION)) {
            print STDERR "# Skipping systemd properties simulation due to required systemd version ($REQUIRED_SYSTEMD_VERSION)\n" if $config->{DEBUG};
            return execute_plugin($plugin, @args);
        } else {
            my @munin_node_hardening_flags;
            my $parse_flags_success = 0;
            eval {
                @munin_node_hardening_flags = get_systemd_hardening_flags();
                $parse_flags_success = 1;
            };
            if ($parse_flags_success) {
                return run_via_systemd(\@munin_node_hardening_flags,
                                       \@original_argv, $config->{DEBUG});
            } else {
                # Failed to retrieve systemd properties of munin-node service.
                # Probable causes: systemd is not installed/enabled or the
                # service unit does not exist.
                return execute_plugin($plugin, @args);
            }
        }
    }
}


sub check_systemd_run_permissions {
    # verify whether systemd-run can be exected (e.g. unprivileged users cannot execute it)
    return system("systemd-run --collect --pipe --quiet --wait -- true </dev/null >/dev/null 2>&1") == 0;
}


# Retrieve the locally configured hardening flags for the "munin-node" systemd
# service.
# The result of the function is a list of strings like the following:
#    ProtectHome=yes
sub get_systemd_hardening_flags {
    # retrieve all active properties except for soft (runtime) limits
    my @munin_service_properties = grep !/^Limit\w+Soft=/, `systemctl show munin-node 2>/dev/null`;
    die "no systemd enabled or failed to retrieve unit properties" unless ($CHILD_ERROR >> 8 == 0);
    my $flag_name_regex = '^((?:' . join("|", @SYSTEMD_PROPERTY_IMPORT_PATTERNS) . ')=.*)$';
    my @flag_list;
    foreach my $property_definition (@munin_service_properties) {
        # The effect of files referenced in "DropInPaths" (e.g. files overriding the properties of
        # a service) is already applied to the output of "systemd-show".
        # Thus we can safely ignore this property (which is not accepted by "systemd-run" anyway).
        next if ($property_definition =~ /^DropInPaths=/);
        # "systemd show" does not output the EnvironmentFile property in a readable format.
        # See https://github.com/systemd/systemd/issues/14723.
        next if ($property_definition =~ /^EnvironmentFiles?=(.*) \(ignore_errors=(yes|no)\)$/);
        push @flag_list, $1 if $property_definition =~ /$flag_name_regex/;
    }
    return @flag_list;
}


# "man systemd.exec" describes the quoting rules for EnvironmentFile.
# We apply the following steps:
#     1) escape all double quotes with a backslash
#     2) surround the value with double quotes
# This combination ensures that even line breaks are properly parsed by
# systemd-run.
sub quote_for_systemd_environment_file {
    my ($key, $value) = @_;
    # escape double quotes
    $value =~ s/"/\\"/;
    return $key . '="' . $value . "\"\n";
}


sub get_systemd_version {
    my @version_output = `systemd-run --version 2>/dev/null`;
    foreach my $line (@version_output) {
        if ($line =~ /^systemd(?:-run)?\s+(\d+).*$/) {
            return int($1);
        }
    }
    return;
}


# Recursively execute this script ("munin-run") via "systemd-run".
# This allows to apply the hardening properties defined in "munin-node.service".
# Thus the behavior of "munin-run" should be the same as the behavior of
# munin-node service itself.  This is less surprising for users.
sub run_via_systemd {
    my ($systemd_properties_ref, $original_argv_ref, $debug_enabled) = @_;
    my @call_args;
    push @call_args, "systemd-run";
    # discard the transient service even in case of errors
    push @call_args, "--collect";
    # use our stdin/stdout/stderr for the created process
    push @call_args, "--pipe";
    push @call_args, "--quiet";
    # wait for the end of the command execution
    push @call_args, "--wait";
    # Preserve the environment (e.g. manual plugin configuration applied by
    # the user).
    # We use systemd-run's property "EnvironmentFile" for transferring the
    # environment of the current process to the new process.  The following
    # simpler approaches ("properties") are not suitable:
    #    * Environment: would expose the private environment of the calling
    #      user to all other users (via the commandline).
    #    * PassEnvironment: the variables are only transferred from the
    #      system manager (PID 1) instead of the calling process.
    # This approach causes a problem
    my $environment_file = File::Temp->new();
    # The order of systemd-run's environment variable processing may cause
    # problems, if "munin-node.service" specifies "Environment" properties,
    # which exist in the caller's environment.  Such variables (being written
    # to the temporary EnvironmentFile) take precedence over the ones defined
    # in "munin-node.service".  There does not seem to be a clean generic
    # workaround for this issue.
    foreach my $key (keys %ENV) {
        next if exists($ENVIRONMENT_IGNORE_HASH{$key});
        print $environment_file quote_for_systemd_environment_file($key, $ENV{$key});
    }
    push @call_args, "--property";
    push @call_args, "EnvironmentFile=" . $environment_file->filename;
    # enable the hardening flags of the munin-node service
    foreach my $key_value (@$systemd_properties_ref) {
        push @call_args, "--property";
        push @call_args, $key_value;
    }
    push @call_args, "--";
    # append the untainted name/path of "munin-run" itself
    $0 =~ /^(.*)$/s;
    push @call_args, $1;
    push @call_args, "--ignore-systemd-properties";
    foreach my $arg (@$original_argv_ref) {
        # untaint our arguments
        $arg =~ /^(.*)$/s;
        push @call_args, $1;
    }
    if ($debug_enabled) {
        print STDERR ("# Running 'munin-run' via 'systemd-run' with systemd "
                      . "properties based on 'munin-node.service'.\n");
        my $command_printable = "";
        foreach my $token (@call_args) {
            $command_printable .= " " if ($command_printable);
            if ($token =~ /\s/) {
                $command_printable .= "'$token'";
            } else {
                $command_printable .= "$token";
            }
        }
        print STDERR "# Command invocation: $command_printable\n";
    }
    # We need to use "system" instead of "exec in order to remove the EnvironmentFile
    # afterwards.  This is indirectly handled by the object cleanup from File::Temp.
    my $result = system(@call_args);
    if ($result == -1) {
        die "Failed to execute the 'systemd-run' wrapper. Maybe try '--ignore-systemd-properties'.";
    } else {
        my $exitcode = $result >> 8;
        if ($exitcode != 0) {
            # Sadly problems with "systemd-run" are only visible in the log (no error output).
            print STDERR ("Warning: the execution of 'munin-run' via 'systemd-run' returned an "
                          . "error. This may either be caused by a problem with the plugin to be "
                          . "executed or a failure of the 'systemd-run' wrapper. Details of the "
                          . "latter can be found via 'journalctl'.\n");
        }
        return $exitcode;
    }
}


sub execute_plugin {
    my ($plugin, @args) = @_;

    $services = Munin::Node::Service->new(
        servicedir => $servicedir,
        defuser    => $config->{defuser},
        defgroup   => $config->{defgroup},
        pidebug    => $PIDEBUG,
    );

    $config->reinitialize({
        %$config,
        paranoia   => $paranoia,
    });

    unless ($services->is_a_runnable_service($plugin)) {
        print STDERR "# Unknown service '$plugin'\n";
        exit 1;
    }

    $services->prepare_plugin_environment($plugin);

    # no need for a timeout -- the user can kill this process any
    # time they want.
    $services->exec_service($plugin, @args);

    # Never reached, but just in case...
    print STDERR "# FATAL: Failed to exec.\n";
    exit 42;
}


sub parse_args
{
    # Default configuration values
    my $sconfdir   = "$Munin::Common::Defaults::MUNIN_CONFDIR/plugin-conf.d";
    my $sconffile;

    my ($plugin, $arg, @extra_args);

    print_usage_and_exit() unless GetOptions(
            "config=s"     => \$conffile,
            "debug!"       => \$DEBUG,
            "pidebug!"     => \$PIDEBUG,
            "servicedir=s" => \$servicedir,
            "sconfdir=s"   => \$sconfdir,
            "sconffile=s"  => \$sconffile,
            "paranoia!"    => \$paranoia,
            "ignore-systemd-properties" => \$ignore_systemd_properties,
            "version"      => \&print_version_and_exit,
            "help"         => \&print_usage_and_exit,
    );

    print_usage_and_exit() unless $ARGV[0];

    # Detaint the plugin name
    ($plugin) = ($ARGV[0] =~ m/^([-\w.:]+)$/) or die "# ERROR: Invalid plugin name '$ARGV[0].\n";
    if ($ARGV[1]) {
        ($arg) = ($ARGV[1] =~ m/^(\w+)$/)
            or die "# ERROR: Invalid characters in argument '$ARGV[1]'.\n";
    }
    # all remaining arguments are handed over to the plugin without further checks
    @extra_args = map {m/^(.*)$/} @ARGV[2..$#ARGV];

    # Detaint service directory.  FIXME: do more strict detainting?
    if ($servicedir) {
        $servicedir =~ /(.*)/;
        $servicedir = $1;
    }

    # Update the config
    $config->reinitialize({
        %$config,

        sconfdir   => $sconfdir,
        conffile   => $conffile,
        sconffile  => $sconffile,
        DEBUG      => $DEBUG,
        paranoia   => $paranoia,
    });

    return ($plugin, $arg, @extra_args);
}


sub print_usage_and_exit
{
    require Pod::Usage;
    Pod::Usage::pod2usage(-verbose => 1);
}


sub print_version_and_exit
{
    require Pod::Usage;
    Pod::Usage::pod2usage(
        -verbose => 99,
        -sections => 'VERSION|COPYRIGHT',
    );
}


exit main() unless caller;


1;

__END__

=head1 NAME

munin-run - A program to run Munin plugins from the command line

=head1 SYNOPSIS

munin-run [options] <plugin> [ config | autoconf | snmpconf | suggest ]

=head1 DESCRIPTION

munin-run is a script to run Munin plugins from the command-line.
It's useful when debugging plugins, as they are run in the same conditions
as they are under munin-node.

=head1 OPTIONS

=over 5

=item B<< --config <configfile> >>

Use E<lt>fileE<gt> as configuration file. [@@CONFDIR@@/munin-node.conf]

=item B<< --servicedir <dir> >>

Use E<lt>dirE<gt> as plugin dir. [@@CONFDIR@@/plugins/]

=item B<< --sconfdir <dir> >>

Use E<lt>dirE<gt> as plugin configuration dir. [@@CONFDIR@@/plugin-conf.d/]

=item B<< --sconffile <file> >>

Use E<lt>fileE<gt> as plugin configuration. Overrides sconfdir.  [undefined]

=item B<--paranoia >

Only run plugins owned by root and check permissions.  [disabled]

=item B<--ignore-systemd-properties >

Do not try to detect and enforce the locally configured hardening flags of the
"munin-node" service unit. This detection is skipped, if systemd is not enabled.
The hardening flags may cause subtile surprises.
For example "ProtectHome=yes" prevents the "df" plugin from determining the
state of the "home" partition.  [disabled]

=item B<--help >

View this help message.

=item B<--debug >

Print debug messages.  Debug messages are sent to STDOUT and are
prefixed with "#" (this makes it easier for other parts of munin to
use munin-run and still have --debug on).  Only errors go to STDERR.

=item B<--pidebug >

Plugin debug.  Sets the environment variable MUNIN_DEBUG to 1 so
that plugins may enable debugging.  [disabled]

=item B<--version >

Show version information.

=back

=head1 NOTES FOR SYSTEMD USERS

The "munin-node" service is usually started by systemd via a
"munin-node.service" definition.  Some distributions enable hardening
settings in this service file in order to restrict the allowed set of
activities for the "munin-node" process.
This may cause surprising differences between the result of "munin-run"
and the real "munin-node" service.

A popular example of such a surprising restriction is "ProtectHome=yes"
combined with the "df" plugin.  The restriction silently prevents the
plugin from determining the status of mountpoints below /home.

"munin-run" tries to mimic this behavior of "munin-node" automatically.
Thus the execution of "munin-run df" should provide the same output as
"echo fetch df | nc localhost munin".

If you want to debug potential issues of systemd restrictions, then you
may want to use the parameters "--ignore-systemd-properties" and
"--debug".  Permanent overrides of systemd properties can be configured
locally via "systemctl edit munin-node".
See "man systemd.exec" for the documentation of systemd's properties.

=head1 FILES

    @@CONFDIR@@/munin-node.conf
    @@CONFDIR@@/plugins/*
    @@CONFDIR@@/plugin-conf.d/*
    @@STATEDIR@@/munin-node.pid
    @@LOGDIR@@/munin-node.log

=head1 VERSION

This is munin-run (munin-node) v@@VERSION@@

=head1 AUTHORS

Audun Ytterdal, Jimmy Olsen, Tore Anderson, Nicolai Langfeldt,
Lars Kruse.

=head1 COPYRIGHT

Copyright (C) 2002-2009 Audun Ytterdal, Jimmy Olsen, Tore Anderson,
Nicolai Langfeldt / Linpro AS.
Copyright (C) 2020 Lars Kruse

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

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

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

=cut

# vim: sw=4 : ts=4 : expandtab