1package Munin::Node::Service;
2
3use warnings;
4use strict;
5
6use English qw(-no_match_vars);
7use Carp;
8
9use Munin::Node::Config;
10use Munin::Node::OS;
11use Munin::Node::Logger;
12
13use Munin::Common::Defaults;
14
15my $config = Munin::Node::Config->instance();
16
17
18sub new
19{
20    my ($class, %args) = @_;
21
22    # Set defaults
23    $args{servicedir} ||= "$Munin::Common::Defaults::MUNIN_CONFDIR/plugins";
24
25    $args{defuser}  = getpwnam $Munin::Common::Defaults::MUNIN_PLUGINUSER unless defined($args{defuser});
26    $args{defgroup} = getgrnam $Munin::Common::Defaults::MUNIN_GROUP unless defined($args{defgroup});
27
28    $args{timeout}  ||= 60; # Default transaction timeout : 1 min
29    $args{pidebug}  ||= 0;
30
31    die "Fatal error. Bailing out.\n"
32        unless (Munin::Node::OS->check_perms_if_paranoid($args{servicedir}));
33
34    return bless \%args, $class;
35}
36
37
38sub is_a_runnable_service
39{
40    my ($self, $file) = @_;
41
42    return unless -f "$self->{servicedir}/$file" && -x _;
43
44    # FIX isn't it enough to check that the file is executable and not
45    # in 'ignores'? Can hidden files and config files be
46    # unintentionally executable? What does config files do in the
47    # service directory? Shouldn't we complain if there is junk in the
48    # service directory?
49    return if $file =~ m/^\./;      # Hidden files
50    return if $file =~ m/\.conf$/;  # Config files
51
52    return if $file !~ m/^[-\w\@.:]+$/;  # Skip if any weird chars
53
54    foreach my $regex (@{$config->{ignores}}) {
55        return if $file =~ /$regex/;
56    }
57
58    return 1;
59}
60
61
62sub list
63{
64    my ($self) = @_;
65    opendir my $dir, $self->{servicedir}
66        or die "Unable to open $self->{servicedir}: $!";
67    return grep { $self->is_a_runnable_service($_) } readdir $dir;
68}
69
70
71# FIXME: unexpected things are likely to happen if this isn't called before
72# running plugins.  it should be done automatically the first time a service is
73# run.
74sub prepare_plugin_environment
75{
76    my ($self, @plugins) = @_;
77
78    Munin::Common::Defaults->export_to_environment();
79
80    $config->{fqdn} ||= Munin::Node::OS->get_fq_hostname();
81
82    # Export some variables plugins might be interested in
83    $ENV{MUNIN_DEBUG} = $self->{pidebug};
84    $ENV{FQDN}        = $config->{fqdn};
85
86    # munin-node will override this with the IP of the connecting master
87    $ENV{MUNIN_MASTER_IP} = '';
88
89    # Tell plugins about supported capabilities
90    $ENV{MUNIN_CAP_MULTIGRAPH} = 1;
91
92    # Some locales use "," as decimal separator. This can mess up a lot
93    # of plugins.
94    $ENV{LC_ALL} = 'C';
95
96    # LC_ALL should be enough, but some plugins don't follow specs (#1014)
97    $ENV{LANG} = 'C';
98
99    # Force UTF-8 encoding for stdout in Python3. This is only relevant for
100    # Python3 before 3.7 (which will use UTF-8 anyway, if possible).
101    # This override allows python3-based plugins as well as any indrectly
102    # executed python3-based commands to output UTF-8 characters.
103    $ENV{PYTHONIOENCODING} = 'utf8:replace';
104
105    # PATH should be *very* sane by default. Can be overridden via
106    # config file if needed (Closes #863 and #1128).
107    $ENV{PATH} = '/usr/sbin:/usr/bin:/sbin:/bin';
108
109    if ($config->{sconffile}) {
110        # only used by munin-run
111        $config->parse_plugin_config_file($config->{sconffile});
112    }
113    else {
114        $config->process_plugin_configuration_files();
115    }
116    $config->apply_wildcards(@plugins);
117
118    return;
119}
120
121
122sub export_service_environment {
123    my ($self, $service) = @_;
124    print STDERR "# Setting up environment\n" if $config->{DEBUG};
125
126    # We append the USER to the MUNIN_PLUGSTATE, to avoid CVE-2012-3512
127    my $uid = $self->_resolve_uid($service);
128    my $user = getpwuid($uid);
129    $ENV{MUNIN_PLUGSTATE} = "$Munin::Common::Defaults::MUNIN_PLUGSTATE/$user";
130
131    # Provide a consistent default state-file.
132    $ENV{MUNIN_STATEFILE} = "$ENV{MUNIN_PLUGSTATE}/$service-$ENV{MUNIN_MASTER_IP}";
133
134    my $env = $config->{sconf}{$service}{env} or return;
135
136    while (my ($k, $v) = each %$env) {
137        print STDERR "# Environment $k = $v\n" if $config->{DEBUG};
138        $ENV{$k} = $v;
139    }
140}
141
142
143# Resolves the uid the service should be run as.  If it cannot be resolved, an
144# exception will be thrown.
145sub _resolve_uid
146{
147    my ($self, $service) = @_;
148
149    my $user = $config->{sconf}{$service}{user};
150
151    # Need to test for defined, since a user might be specified with UID = 0
152    my $service_user = defined $user ? $user : $self->{defuser};
153
154    my $u = Munin::Node::OS->get_uid($service_user);
155    croak "User '$service_user' required for '$service' does not exist."
156        unless defined $u;
157
158    return $u;
159}
160
161
162# resolves the GIDs (real and effective) the service should be run as.
163# http://munin-monitoring.org/wiki/plugin-conf.d
164sub _resolve_gids
165{
166    my ($self, $service) = @_;
167
168    my $group_list = $config->{sconf}{$service}{group};
169
170    my $default_gid = $self->{defgroup};
171
172    my @groups;
173
174    foreach my $group (@{$group_list||[]}) {
175        my $is_optional = ($group =~ m{\A \( ([^)]+) \) \z}xms);
176        $group = $1 if $is_optional;
177
178        my $gid = Munin::Node::OS->get_gid($group);
179
180        croak "Group '$group' required for '$service' does not exist"
181            unless defined $gid || $is_optional;
182
183        if (!defined $gid && $is_optional) {
184            carp "DEBUG: Skipping OPTIONAL nonexisting group '$group'"
185                if $config->{DEBUG};
186            next;
187        }
188        push @groups, $gid;
189    }
190
191    # Support running with more than one group in effect. See documentation on
192    # $EFFECTIVE_GROUP_ID in the perlvar(1) manual page.  Need to specify the
193    # primary group twice: once for setegid(2), and once for setgroups(2).
194    if (scalar(@groups) != 0) {
195        return ($groups[0], join ' ', $groups[0], @groups);
196    }
197    return ($default_gid, join ' ', ($default_gid) x 2);
198}
199
200
201sub change_real_and_effective_user_and_group
202{
203    my ($self, $service) = @_;
204
205    my $root_uid = 0;
206    my $root_gid = 0;
207
208    my $sconf = $config->{sconf}{$service};
209
210    if ($REAL_USER_ID == $root_uid) {
211        # Resolve UIDs now, as they are not resolved when the config was read.
212        my $uid = $self->_resolve_uid($service);
213
214        # Ditto for groups
215        my ($rgid, $egids) = $self->_resolve_gids($service);
216
217        eval {
218            if ($Munin::Common::Defaults::MUNIN_HASSETR) {
219                print STDERR "# Setting /rgid/ruid/ to /$rgid/$uid/\n"
220                    if $config->{DEBUG};
221                Munin::Node::OS->set_real_group_id($rgid) unless $rgid == $root_gid;
222                Munin::Node::OS->set_real_user_id($uid)   unless $uid  == $root_uid;
223            }
224
225            print STDERR "# Setting /egid/euid/ to /$egids/$uid/\n"
226                if $config->{DEBUG};
227            Munin::Node::OS->set_effective_group_id($egids) unless $rgid == $root_gid;
228            Munin::Node::OS->set_effective_user_id($uid)    unless $uid  == $root_uid;
229        };
230
231        if ($EVAL_ERROR) {
232            logger("# FATAL: Plugin '$service' Can't drop privileges: $EVAL_ERROR.");
233            exit 1;
234        }
235    }
236    elsif (defined $sconf->{user} or defined $sconf->{groups}) {
237        print "# Warning: Root privileges are required to change user/group.  "
238            . "The plugin may not behave as expected.\n";
239    }
240
241    return;
242}
243
244
245sub exec_service
246{
247    my ($self, $service, $arg) = @_;
248
249    # XXX - Create the statedir for the user
250    my $uid = $self->_resolve_uid($service);
251    Munin::Node::OS->mkdir_subdir("$Munin::Common::Defaults::MUNIN_PLUGSTATE", $uid);
252
253    $self->change_real_and_effective_user_and_group($service);
254
255    unless (Munin::Node::OS->check_perms_if_paranoid("$self->{servicedir}/$service")) {
256        logger ("Error: unsafe permissions on $service. Bailing out.");
257        exit 2;
258    }
259
260    $self->export_service_environment($service);
261
262    Munin::Node::OS::set_umask();
263
264    my @command = grep defined, _service_command($self->{servicedir}, $service, $arg);
265    print STDERR "# About to run '", join (' ', @command), "'\n"
266        if $config->{DEBUG};
267
268    exec @command;
269}
270
271
272# Returns the command for the service and (optional) argument, expanding '%c'
273# as the original command (see 'command' directive in
274# <http://munin-monitoring.org/wiki/plugin-conf.d>).
275sub _service_command
276{
277    my ($dir, $service, $argument) = @_;
278
279    my @run;
280    my $sconf = $config->{sconf};
281
282    if ($sconf->{$service}{command}) {
283        for my $t (@{ $sconf->{$service}{command} }) {
284            if ($t eq '%c') {
285                push @run, ("$dir/$service", $argument);
286            } else {
287                push @run, ($t);
288            }
289        }
290    }
291    else {
292        @run = ("$dir/$service", $argument);
293    }
294
295    return @run;
296}
297
298
299sub fork_service
300{
301    my ($self, $service, $arg) = @_;
302
303    my $timeout = $config->{sconf}{$service}{timeout}
304               || $self->{timeout};
305
306    my $run_service = sub {
307        $self->exec_service($service, $arg);
308        # shouldn't be reached
309        print STDERR "# ERROR: Failed to exec.\n";
310        exit 42;
311    };
312
313    return Munin::Node::OS->run_as_child($timeout, $run_service);
314}
315
316
3171;
318
319__END__
320
321
322=head1 NAME
323
324Munin::Node::Service - Methods related to handling of Munin services
325
326
327=head1 SYNOPSIS
328
329 my $services = Munin::Node::Service->new(timeout => 30);
330 $services->prepare_plugin_environment;
331 if ($services->is_a_runnable_service($file_name)) {
332    $services->fork_service($file_name);
333 }
334
335=head1 METHODS
336
337=over
338
339=item B<new>
340
341 my $services = Munin::Node::Service->new(%args);
342
343Constructor.  All arguments are optional.  Valid arguments are:
344
345=over 8
346
347=item C<servicedir>
348
349The directory that will be searched for services.
350
351=item C<defuser>, C<defgroup>
352
353The default uid and gid that services will run as.  Service-specific user and
354group directives (as set by the service configuration files) will override
355this.
356
357=item C<timeout>
358
359The default timeout for services.  Services taking longer than this to run will
360be killed.  Service-specific timeouts will (as set in the service configuration
361files) will override this value.
362
363=back
364
365=item B<is_a_runnable_service>
366
367 my $bool = $services->is_a_runnable_service($file_name);
368
369Runs miscellaneous tests on $file_name in the service directory, to try and
370establish whether it is a runnable service.
371
372=item B<list>
373
374  my @services = $services->list;
375
376Returns a list of all the runnable services in the directory.
377
378=item B<prepare_plugin_environment>
379
380 $services->prepare_plugin_environment(@services);
381
382Carries out various tasks that plugins require before being run, such as
383loading service configurations and exporting common environment variables.
384
385=item B<export_service_environment>
386
387 $services->export_service_enviromnent($service);
388
389Exports all the environment variables specific to service $service.
390
391=item B<change_real_and_effective_user_and_group>
392
393 $service->change_real_and_effective_user_and_group($service);
394
395Changes the current process' effective group and user IDs to those specified in
396the configuration, or the default user or group otherwise.  Also changes the
397real group and user IDs if the operating system supports it.
398
399On failure, causes the process to exit.
400
401=item B<exec_service>
402
403 $service->exec_service($service, [$argument]);
404
405Replaces the current process with an instance of service $service in
406$directory, running with the correct environment and privileges.
407
408This function never returns.  The process will exit(2) if the service to be run
409failed the paranoia check.
410
411=item B<fork_service>
412
413 $result = $service->fork_service($service, [$argument]);
414
415Identical to exec_service(), except it runs the service in a subprocess.  If
416the service takes longer than the timeout, it will be terminated.
417
418Returns a hash reference containing (among other things) the service's output
419and exit value.  (See documentation for run_as_child() in
420L<Munin::Node::Service> for a comprehensive description.)
421
422=back
423
424=cut
425
426# vim: sw=4 : ts=4 : expandtab
427