managementnode/lib/VCL/Module/OS/Linux/init/systemd.pm (376 lines of code) (raw):

#!/usr/bin/perl -w ############################################################################### # $Id$ ############################################################################### # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ############################################################################### =head1 NAME VCL::Module::OS::Linux::init::systemd.pm =head1 DESCRIPTION This module provides VCL support for the systemd Linux init daemon used in distributions such as: Fedora 15+ openSUSE 12.1+ =cut ############################################################################### package VCL::Module::OS::Linux::init::systemd; # Specify the lib path using FindBin use FindBin; use lib "$FindBin::Bin/../../../../.."; # Configure inheritance use base qw(VCL::Module::OS::Linux::init); # Specify the version of this module our $VERSION = '2.5.1'; # Specify the version of Perl to use use 5.008000; use strict; use warnings; use diagnostics; use VCL::utils; ############################################################################### =head1 CLASS VARIABLES =cut =head2 $INIT_DAEMON_ORDER Data type : integer Value : 20 Description : Determines the order in which Linux init daemon modules are used. Lower values are used first. =cut our $INIT_DAEMON_ORDER = 20; =head2 @REQUIRED_COMMANDS Data type : array Values : systemctl Description : List of commands used within this module to configure and control systemd services. This module will not be used if any of these commands are unavailable on the computer. =cut our @REQUIRED_COMMANDS = ('systemctl'); ############################################################################### =head1 OBJECT METHODS =cut #////////////////////////////////////////////////////////////////////////////// =head2 get_service_names Parameters : none Returns : array Description : Calls 'systemctl list-unit-files' to retrieve the list of services controlled by systemd on the computer. =cut sub get_service_names { my $self = shift; if (ref($self) !~ /linux/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $computer_node_name = $self->data->get_computer_node_name(); my $command = "systemctl --no-pager list-unit-files"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to retrieve systemd service names on $computer_node_name"); return; } # Format of systemctl list output lines: # ssyslog.target static # Add to hash then extract keys to remove duplicates my %service_name_hash; for my $line (@$output) { my ($service_name) = $line =~ /^(.+)\.service/; $service_name_hash{$service_name} = 1 if $service_name; } my @service_names = sort(keys %service_name_hash); notify($ERRORS{'DEBUG'}, 0, "retrieved systemd service names from $computer_node_name: " . join(", ", @service_names)); return @service_names; } #////////////////////////////////////////////////////////////////////////////// =head2 service_running Parameters : $service_name Returns : boolean Description : Calls 'systemctl is-active' to determines if a service is running. =cut sub service_running { my $self = shift; if (ref($self) !~ /linux/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $service_name = shift; if (!$service_name) { notify($ERRORS{'WARNING'}, 0, "service name argument was not supplied"); return; } my $computer_node_name = $self->data->get_computer_node_name(); my $command = "systemctl is-active $service_name.service"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to determine if $service_name service is running on $computer_node_name"); return; } # Output should either be 'active' or 'inactive if (grep(/inactive/, @$output)) { notify($ERRORS{'DEBUG'}, 0, "$service_name service is not running on $computer_node_name"); return 0; } elsif (grep(/active/, @$output)) { notify($ERRORS{'DEBUG'}, 0, "$service_name service is running on $computer_node_name"); return 1; } else { notify($ERRORS{'WARNING'}, 0, "failed to determine if $service_name service is running on $computer_node_name, output does not contain 'active' or 'inactive':\n" . join("\n", @$output)); return; } } #////////////////////////////////////////////////////////////////////////////// =head2 service_enabled Parameters : $service_name Returns : boolean Description : Calls 'systemctl is-enabled' to determines if a service is enabled. =cut sub service_enabled { my $self = shift; if (ref($self) !~ /linux/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $service_name = shift; if (!$service_name) { notify($ERRORS{'WARNING'}, 0, "service name argument was not supplied"); return; } my $computer_node_name = $self->data->get_computer_node_name(); my $command = "systemctl is-enabled $service_name.service"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to determine if $service_name service is running on $computer_node_name"); return; } # Output should either be 'enabled' or 'disabled if (grep(/disabled/, @$output)) { notify($ERRORS{'DEBUG'}, 0, "$service_name service is disabled on $computer_node_name"); return 0; } elsif (grep(/enabled/, @$output)) { notify($ERRORS{'DEBUG'}, 0, "$service_name service is enabled on $computer_node_name"); return 1; } else { notify($ERRORS{'WARNING'}, 0, "failed to determine if $service_name service is enabled on $computer_node_name, output does not contain 'enabled' or 'disabled':\n" . join("\n", @$output)); return; } } #////////////////////////////////////////////////////////////////////////////// =head2 enable_service Parameters : $service_name Returns : boolean Description : Calls 'systemctl enable' to enable the service specified by the argument. =cut sub enable_service { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my $service_name = shift; if (!$service_name) { notify($ERRORS{'WARNING'}, 0, "service name argument was not supplied"); return; } my $computer_node_name = $self->data->get_computer_node_name(); # Enable the service my $command = "systemctl --no-reload enable $service_name.service"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to enable '$service_name' service on $computer_node_name: $command"); return; } elsif (grep(/(Failed to issue method call|No such file)/i, @$output)) { # Output if the service doesn't exist: 'Failed to issue method call: No such file or directory' notify($ERRORS{'WARNING'}, 0, "unable to enable '$service_name' service because it does not exist on $computer_node_name"); return; } elsif ($exit_status ne 0 || grep(/(failed)/i, @$output)) { notify($ERRORS{'WARNING'}, 0, "failed to enable '$service_name' service on $computer_node_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); return; } else { notify($ERRORS{'DEBUG'}, 0, "enabled '$service_name' service on $computer_node_name"); } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 disable_service Parameters : $service_name Returns : boolean Description : Calls 'systemctl disable' to disable the service specified by the argument. =cut sub disable_service { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my $service_name = shift; if (!$service_name) { notify($ERRORS{'WARNING'}, 0, "service name argument was not supplied"); return; } my $computer_node_name = $self->data->get_computer_node_name(); my $command = "systemctl --no-reload disable $service_name.service"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to disable '$service_name' service on $computer_node_name: $command"); return; } elsif (grep(/(Failed to issue method call|No such file)/i, @$output)) { # Output if the service doesn't exist: 'Failed to issue method call: No such file or directory' notify($ERRORS{'WARNING'}, 0, "unable to disable '$service_name' service because it does not exist on $computer_node_name"); return; } elsif ($exit_status ne 0 || grep(/(failed)/i, @$output)) { notify($ERRORS{'WARNING'}, 0, "failed to disable '$service_name' service on $computer_node_name, exit status: $exit_status, output:\n" . join("\n", @$output)); return; } else { notify($ERRORS{'DEBUG'}, 0, "disabled '$service_name' service on $computer_node_name"); } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 delete_service Parameters : $service_name Returns : boolean Description : Disables the service and deletes the service file from /lib/systemd/system/. =cut sub delete_service { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my $service_name = shift; if (!$service_name) { notify($ERRORS{'WARNING'}, 0, "service name argument was not supplied"); return; } my $computer_node_name = $self->data->get_computer_node_name(); # Disable the service before deleting it $self->stop_service($service_name) || return; $self->disable_service($service_name) || return; # Delete the service configuration file my $service_file_path = "/lib/systemd/system/$service_name.service"; if (!$self->os->delete_file($service_file_path)) { return; } $self->_daemon_reload(); return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 start_service Parameters : $service_name Returns : boolean Description : Calls 'systemctl start' to start the service specified by the argument. =cut sub start_service { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my $service_name = shift; if (!$service_name) { notify($ERRORS{'WARNING'}, 0, "service name argument was not supplied"); return; } my $computer_node_name = $self->data->get_computer_node_name(); # start the service my $command = "systemctl start $service_name.service"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to start '$service_name' service on $computer_node_name: $command"); return; } elsif (grep(/(Failed to issue method call|No such file)/i, @$output)) { # Output if the service doesn't exist # Failed to issue method call: Unit httpdx.service failed to load: No such file or directory. notify($ERRORS{'WARNING'}, 0, "unable to start '$service_name' service because it does not exist on $computer_node_name"); return; } elsif ($exit_status ne 0 || grep(/(failed)/i, @$output)) { notify($ERRORS{'WARNING'}, 0, "failed to start '$service_name' service on $computer_node_name, exit status: $exit_status, output:\n" . join("\n", @$output)); return; } else { notify($ERRORS{'DEBUG'}, 0, "started '$service_name' service on $computer_node_name"); } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 stop_service Parameters : $service_name Returns : boolean Description : Calls 'systemctl stop' to stop the service specified by the argument. =cut sub stop_service { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my $service_name = shift; if (!$service_name) { notify($ERRORS{'WARNING'}, 0, "service name argument was not supplied"); return; } my $computer_node_name = $self->data->get_computer_node_name(); # stop the service my $command = "systemctl stop $service_name.service"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to stop '$service_name' service on $computer_node_name: $command"); return; } elsif (grep(/(Failed to issue method call|No such file)/i, @$output)) { # Output if the service doesn't exist # Failed to issue method call: Unit httpdx.service failed to load: No such file or directory. notify($ERRORS{'DEBUG'}, 0, "unable to stop '$service_name' service because it does not exist on $computer_node_name"); return 1; } elsif (grep(/(not loaded)/i, @$output)) { # Output if the service isn't loaded # Failed to stop ext_ssh.service: Unit ext_ssh.service not loaded. notify($ERRORS{'DEBUG'}, 0, "unable to stop '$service_name' service because it is not loaded $computer_node_name"); return 1; } elsif ($exit_status ne 0 || grep(/(failed)/i, @$output)) { notify($ERRORS{'WARNING'}, 0, "failed to stop '$service_name' service on $computer_node_name, exit status: $exit_status, output:\n" . join("\n", @$output)); return; } else { notify($ERRORS{'DEBUG'}, 0, "stopped '$service_name' service on $computer_node_name"); } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 restart_service Parameters : $service_name Returns : boolean Description : Calls 'systemctl restart' to restart the service specified by the argument. =cut sub restart_service { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my $service_name = shift; if (!$service_name) { notify($ERRORS{'WARNING'}, 0, "service name argument was not supplied"); return; } my $computer_node_name = $self->data->get_computer_node_name(); # Restart the service my $command = "systemctl restart $service_name.service"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to restart '$service_name' service on $computer_node_name: $command"); return; } elsif (grep(/(Failed to issue method call|No such file)/i, @$output)) { # Output if the service doesn't exist # Failed to issue method call: Unit httpdx.service failed to load: No such file or directory. notify($ERRORS{'WARNING'}, 0, "unable to restart '$service_name' service because it does not exist on $computer_node_name"); return; } elsif ($exit_status ne 0 || grep(/(failed)/i, @$output)) { notify($ERRORS{'WARNING'}, 0, "failed to restart '$service_name' service on $computer_node_name, exit status: $exit_status, output:\n" . join("\n", @$output)); return; } else { notify($ERRORS{'DEBUG'}, 0, "restarted '$service_name' service on $computer_node_name"); } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 add_ext_sshd_service Parameters : none Returns : boolean Description : Constructs the ext_sshd service configuration file: /lib/systemd/system/ext_sshd.service =cut sub add_ext_sshd_service { my $self = shift; if (ref($self) !~ /linux/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my $computer_node_name = $self->data->get_computer_node_name(); # Get the unit file path for the sshd service # Do not automatically assume it is /lib/systemd/system/sshd.service # https://issues.apache.org/jira/browse/VCL-989 my $sshd_service_file_path = $self->_get_service_unit_file_path('sshd'); if (!$sshd_service_file_path) { $sshd_service_file_path = '/lib/systemd/system/sshd.service'; } # Hard-code the ext_sshd file path (intentional) my $ext_sshd_service_file_path = '/lib/systemd/system/ext_sshd.service'; # Get the contents of the sshd service configuration file already on the computer my @sshd_service_file_contents = $self->os->get_file_contents($sshd_service_file_path); if (!@sshd_service_file_contents) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve contents of $sshd_service_file_path from $computer_node_name"); return; } my $ext_sshd_service_file_contents = join("\n", @sshd_service_file_contents); # Replace: OpenSSH --> External OpenSSH $ext_sshd_service_file_contents =~ s|(OpenSSH)|external $1|g; # Replace: sshd --> ext_sshd, exceptions: # /bin/sshd # /sshd_config $ext_sshd_service_file_contents =~ s|(?<!bin/)sshd(?!_config\|-keygen)|ext_sshd|g; # Remove ExecStart options variables # ExecStart=/usr/sbin/sshd -D $OPTIONS # ExecStart=/usr/sbin/sshd -D $SSHD_OPTS $ext_sshd_service_file_contents =~ s/^\s*(ExecStart=.+\S)\s+\$\S*OPT\S*(.*)$/$1$2/gm; # Remove explicit -f arguments from ExecStart line $ext_sshd_service_file_contents =~ s/^\s*(ExecStart=.+\S)\s+-f\s+\S+(.*)$/$1$2/gm; # Add -f argument to ExecStart line $ext_sshd_service_file_contents =~ s|^\s*(ExecStart=.+\S)\s*$|$1 -f /etc/ssh/external_sshd_config|gm; # Set EnvironmentFile to /dev/null, service won't start if the file doesn't exist $ext_sshd_service_file_contents =~ s|(EnvironmentFile)=.*|$1=/dev/null|g; # Remove Alias= line which may exist in ssh_config: # Alias=ext_sshd.service # Otherwise, this may occur when attempting to enable the service if the service is named the same as the alias: # Failed to execute operation: Too many levels of symbolic links $ext_sshd_service_file_contents =~ s/^\s*Alias=.*//gm; # Add explicit lines, remove first to avoid duplicates: $ext_sshd_service_file_contents =~ s/^\s*(Restart|RestartSec|StartLimitInterval)=.*\n?//gm; # Attempt to restart if the service dies $ext_sshd_service_file_contents =~ s/(\[Service\])/$1\nRestart=on-failure/gm; $ext_sshd_service_file_contents =~ s/(\[Service\])/$1\nRestartSec=3s/gm; # (VCL-1027) Add StartLimitInterval=0 under [Service] to prevent: # Job for ext_sshd.service failed because start of the service was attempted too often $ext_sshd_service_file_contents =~ s/(\[Service\])/$1\nStartLimitInterval=0/gm; notify($ERRORS{'DEBUG'}, 0, "$ext_sshd_service_file_path:\n$ext_sshd_service_file_contents"); if (!$self->os->create_text_file($ext_sshd_service_file_path, $ext_sshd_service_file_contents)) { notify($ERRORS{'WARNING'}, 0, "failed to create ext_sshd service file on $computer_node_name: $ext_sshd_service_file_path"); return; } if (!$self->os->set_file_permissions($ext_sshd_service_file_path, '644')) { notify($ERRORS{'WARNING'}, 0, "failed to set permissions of ext_sshd service file to 644 on $computer_node_name: $ext_sshd_service_file_path"); return; } $self->_daemon_reload(); return $self->enable_service('ext_sshd'); } #////////////////////////////////////////////////////////////////////////////// =head2 _daemon_reload Parameters : none Returns : boolean Description : Runs 'systemctl --system daemon-reload'. This is necessary when adding or deleting services or else systemctl will complain: Warning: Unit file changed on disk, 'systemctl --system daemon-reload' recommended. =cut sub _daemon_reload { my $self = shift; if (ref($self) !~ /linux/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my $computer_node_name = $self->data->get_computer_node_name(); my $command = "systemctl --system daemon-reload"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to reload systemd manager configuration on $computer_node_name: $command"); return; } elsif ($exit_status ne 0 || grep(/(failed)/i, @$output)) { notify($ERRORS{'WARNING'}, 0, "failed to reload systemd manager configuration on $computer_node_name, exit status: $exit_status, output:\n" . join("\n", @$output)); return; } else { notify($ERRORS{'DEBUG'}, 0, "reloaded systemd manager configuration on $computer_node_name"); } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 _get_service_unit_file_path Parameters : $service_name Returns : string Description : Determines the unit file for the service specified by the argument. This is needed because the file name is not always $service_name.service. This is the case when a service has alias names configured such as the ssh and sshd services on Ubuntu 16. The file path for the sshd service is ssh.service. =cut sub _get_service_unit_file_path { my $self = shift; if (ref($self) !~ /VCL::/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my $service_name = shift; if (!$service_name) { notify($ERRORS{'WARNING'}, 0, "service name argument was not supplied"); return; } my $computer_node_name = $self->data->get_computer_node_name(); my $command = "systemctl show $service_name.service --property=FragmentPath"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve unit file path for $service_name service on $computer_node_name: $command"); return; } elsif ($exit_status ne 0 || grep(/(failed)/i, @$output)) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve unit file path for $service_name service on $computer_node_name, exit status: $exit_status, output:\n" . join("\n", @$output)); return; } # Expected output: # FragmentPath=/lib/systemd/system/ssh.service my ($file_path_line) = grep(/FragmentPath=/, @$output); if (!$file_path_line) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve unit file path for $service_name service on $computer_node_name, output does not contain a 'FragmentPath=' line, output:\n" . join("\n", @$output)); return; } my ($file_path) = $file_path_line =~ /FragmentPath=(.+)\s*$/g; if (!$file_path) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve unit file path for $service_name service on $computer_node_name, failed to parse 'FragmentPath=' line, output:\n" . join("\n", @$output)); return; } notify($ERRORS{'DEBUG'}, 0, "retrieved unit file path for $service_name service on $computer_node_name: $file_path"); return $file_path } #////////////////////////////////////////////////////////////////////////////// 1; __END__ =head1 SEE ALSO L<http://cwiki.apache.org/VCL/> =cut