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