blob: fda195e5e244a92d946be735ac98550d8fb84405 [file] [log] [blame]
#!/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';
# 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