#!/usr/bin/perl -w
###############################################################################
# $Id: SysV.pm 1798488 2017-06-12 16:38:02Z arkurth $
###############################################################################
# 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::SysV.pm

=head1 DESCRIPTION

 This module provides VCL support for the SysV-style Linux init daemon used in
 distributions such as:
    Red Hat Enterprise Linux 5.x, 6.x
    CentOS 5.x, 6.x

=cut

###############################################################################
package VCL::Module::OS::Linux::init::SysV;

# 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       : 50
 Description : Determines the order in which Linux init daemon modules are used
               if an OS supports multiple init daemons. Lower values are used
               first. SysV has a higher value than other init modules because it
               is older than other, newer init daemons. The newer init daemon
               modules should be tried first.

=cut

our $INIT_DAEMON_ORDER = 50;

=head2 @REQUIRED_COMMANDS

 Data type   : array
 Values      : chkconfig, service
 Description : List of commands used within this module to configure and control
               SysV services. This module will not be used if any of these
               commands are unavailable on the computer.

=cut

our @REQUIRED_COMMANDS = ('chkconfig', 'service');

###############################################################################

=head1 OBJECT METHODS

=cut

#//////////////////////////////////////////////////////////////////////////////

=head2 get_service_names

 Parameters  : none
 Returns     : array
 Description : Calls 'chkconfig --list' to retrieve the list of services
               controlled by SysV 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 = "chkconfig --list";
	my ($exit_status, $output) = $self->os->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to list SysV services on $computer_node_name");
		return;
	}
	
	# Format out chkconfig --list output lines:
	# Note: This output shows SysV services only and does not include native
	#    systemd services. SysV configuration data might be overridden by native
	#    ...
	# sshd            0:off   1:off   2:on    3:on    4:on    5:on    6:off
	my %service_name_hash;
	for my $line (@$output) {
		my ($service_name) = $line =~ /^([^\s\t]+)[\s\t]+\d/;
		$service_name_hash{$service_name} = 1 if $service_name;
	}
	my @service_names = sort(keys %service_name_hash);
	notify($ERRORS{'DEBUG'}, 0, "retrieved SysV service names from $computer_node_name: " . join(", ", @service_names));
	return @service_names;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 enable_service

 Parameters  : $service_name
 Returns     : boolean
 Description : Calls 'chkconfig <$service_name> on' to configure the service to
               start automatically. Does not start the service.

=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 = "chkconfig $service_name on";
	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(/(error reading information|No such file)/i, @$output)) {
		# Output if the service doesn't exist: 'error reading information on service httpdx: No such file or directory'
		notify($ERRORS{'WARNING'}, 0, "'$service_name' service does not exist on $computer_node_name");
		return;
	}
	elsif (grep(/(failed|warn|error)/i, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to enable '$service_name' service on $computer_node_name, exit status: $exit_status, output:\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 'chkconfig <$service_name> off' to prevent the service from
               starting automatically. Does not stop the service.

=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();
	
	# Disable the service
	my $command = "chkconfig $service_name off";
	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(/(error reading information|No such file)/i, @$output)) {
		# Output if the service doesn't exist: 'error reading information on service httpdx: No such file or directory'
		notify($ERRORS{'WARNING'}, 0, "'$service_name' service does not exist on $computer_node_name");
		return;
	}
	elsif (grep(/(failed|warn|error)/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 service_enabled

 Parameters  : $service_name
 Returns     : boolean
 Description : Calls 'chkconfig --list <$service_name>' to determine if a
               service is enabled.

=cut

sub service_enabled {
	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 = "chkconfig --list $service_name";
	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 enabled on $computer_node_name: $command");
		return;
	}
	elsif (grep(/(error reading information|No such file)/i, @$output)) {
		# Output if the service does not exist: 'error reading information on service httpdx: No such file or directory'
		notify($ERRORS{'WARNING'}, 0, "'$service_name' service does not exist on $computer_node_name");
		return;
	}
	elsif (grep(/^$service_name\s+.*3:on/i, @$output)) {
		# Output if the service is enabled: '<service name>    0:off   1:off   2:on    3:on    4:on    5:on    6:off'
		notify($ERRORS{'DEBUG'}, 0, "'$service_name' service is enabled on $computer_node_name");
		return 1;
	}
	elsif (grep(/^$service_name\s+.*3:off/i, @$output)) {
		# Output if the service is disabled: '<service name>    0:off   1:off   2:off   3:off   4:off   5:off   6:off'
		notify($ERRORS{'DEBUG'}, 0, "'$service_name' service is not enabled on $computer_node_name");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to determine if '$service_name' service is enabled on $computer_node_name, exit status: $exit_status, output:\n" . join("\n", @$output));
		return;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 service_running

 Parameters  : $service_name
 Returns     : boolean
 Description : Calls 'service <$service_name> status' to determine if a
               service is running.

=cut

sub service_running {
	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 = "service $service_name status";
	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: $command");
		return;
	}
	elsif (grep(/(error reading information|No such file)/i, @$output)) {
		# Output if the service does not exist: 'error reading information on service httpdx: No such file or directory'
		notify($ERRORS{'WARNING'}, 0, "'$service_name' service does not exist on $computer_node_name");
		return;
	}
	elsif (grep(/(is running)/i, @$output)) {
		# Output if the service is running: '<service name> is running'
		notify($ERRORS{'DEBUG'}, 0, "'$service_name' service is running on $computer_node_name, output:\n" . join("\n", @$output));
		return 1;
	}
	elsif (grep(/(is not running|no\s.*process)/i, @$output)) {
		# Output if the service is not running: '<service name> is not running'
		notify($ERRORS{'DEBUG'}, 0, "'$service_name' service is not running on $computer_node_name, output:\n" . join("\n", @$output));
		return 0;
	}
	elsif ($exit_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "unable to determine if '$service_name' service is running on $computer_node_name based on output but exit status of $command is $exit_status, assuming service is running, output:\n" . join("\n", @$output));
		return 1;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "unable to determine if '$service_name' service is running on $computer_node_name based on output but exit status of $command is $exit_status, assuming service is NOT running, output:\n" . join("\n", @$output));
		return 0;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 add_service

 Parameters  : $service_name
 Returns     : boolean
 Description : Calls 'chkconfig --add <$service_name>' to add the service
               specified by the argument. The service file must already reside
               in /etc/rc.d/init.d/.

=cut

sub add_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;
	}
	
	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();
	
	# Add the service
	my $command = "chkconfig --add $service_name";
	my ($exit_status, $output) = $self->os->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to add '$service_name' service on $computer_node_name");
		return;
	}
	elsif (grep(/(error|No such file)/i, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to add '$service_name' service on $computer_node_name, exit status: $exit_status, command: '$command', output:\n" . join("\n", @$output));
		return;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "added '$service_name' service on $computer_node_name");
		return 1;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 delete_service

 Parameters  : $service_name
 Returns     : boolean
 Description : Calls 'chkconfig --del <$service_name>' to delete the service
               specified by the argument. Deletes the service file from
               /etc/rc.d/init.d/.

=cut

sub delete_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;
	}
	
	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();
	
	# Delete the service
	my $command = "chkconfig --del $service_name";
	my ($exit_status, $output) = $self->os->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to delete '$service_name' service on $computer_node_name");
		return;
	}
	elsif (grep(/(error reading information|No such file)/i, @$output)) {
		# Output if the service doesn't exist: 'error reading information on service xxx: No such file or directory'
		notify($ERRORS{'DEBUG'}, 0, "'$service_name' service does not exist on $computer_node_name");
	}
	elsif ($exit_status ne '0') {
		notify($ERRORS{'WARNING'}, 0, "failed to delete '$service_name' service on $computer_node_name, exit status: $exit_status, command: '$command', output:\n" . join("\n", @$output));
		return;
	}
	
	# Delete the service configuration file
	my $service_file_path = "/etc/rc.d/init.d/$service_name";
	if (!$self->os->delete_file($service_file_path)) {
		return;
	}
	
	notify($ERRORS{'DEBUG'}, 0, "deleted '$service_name' service on $computer_node_name");
	return 1;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 start_service

 Parameters  : $service_name
 Returns     : boolean
 Description : Calls 'service <$service_name> start' to start the service.

=cut

sub start_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;
	}
	
	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 = "service $service_name start";
	my ($exit_status, $output) = $self->os->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to start '$service_name' service on $computer_node_name");
		return;
	}
	elsif (grep(/(error reading information|No such file)/i, @$output)) {
		# Output if the service doesn't exist: 'error reading information on service xxx: No such file or directory'
		notify($ERRORS{'DEBUG'}, 0, "'$service_name' service does not exist on $computer_node_name");
	}
	elsif (grep(/Starting $service_name:.*FAIL/i, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to start '$service_name' service on $computer_node_name, exit status: $exit_status, command: '$command', 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 'service <$service_name> stop' to start the service.

=cut

sub stop_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;
	}
	
	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 = "service $service_name status ; service $service_name stop";
	my ($exit_status, $output) = $self->os->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to stop '$service_name' service on $computer_node_name");
		return;
	}
	elsif (grep(/(error reading information|No such file)/i, @$output)) {
		# Output if the service doesn't exist: 'error reading information on service xxx: No such file or directory'
		notify($ERRORS{'DEBUG'}, 0, "'$service_name' service does not exist on $computer_node_name");
		return 1;
	}
	elsif (grep(/is stopped/i, @$output)) {
		notify($ERRORS{'DEBUG'}, 0, "'$service_name' service is already stopped on $computer_node_name");
		return 1;
	}
	elsif (grep(/Stopping $service_name:.*FAIL/i, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to stop '$service_name' service on $computer_node_name, exit status: $exit_status, command: '$command', output:\n" . join("\n", @$output));
		return;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "stopped '$service_name' service on $computer_node_name, output:\n" . join("\n", @$output));
		return 1;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 restart_service

 Parameters  : $service_name
 Returns     : boolean
 Description : Calls 'service <$service_name> restart' to start the service.

=cut

sub restart_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;
	}
	
	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 = "service $service_name restart";
	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");
		return;
	}
	elsif (grep(/(error reading information|No such file)/i, @$output)) {
		# Output if the service doesn't exist: 'error reading information on service xxx: No such file or directory'
		notify($ERRORS{'DEBUG'}, 0, "'$service_name' service does not exist on $computer_node_name");
	}
	elsif (grep(/Starting $service_name:.*FAIL/i, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to restart '$service_name' service on $computer_node_name, exit status: $exit_status, command: '$command', output:\n" . join("\n", @$output));
		return;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "restarted '$service_name' service on $computer_node_name, output:\n" . join("\n", @$output));
		return 1;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 add_ext_sshd_service

 Parameters  : none
 Returns     : boolean
 Description : Adds the ext_sshd service to the computer. Generates and
               configures /etc/rc.d/init.d/ext_sshd based off of the existing
               /etc/rc.d/init.d/sshd file. Adds the ext_sshd service and
               configures it to start automatically.

=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();
	
	my $sshd_service_file_path          = '/etc/rc.d/init.d/sshd';
	my $sshd_service_file_path_original = '/etc/rc.d/init.d/sshd_original';
	my $ext_sshd_service_file_path      = '/etc/rc.d/init.d/ext_sshd';
	my $ext_sshd_config_file_path       = '/etc/ssh/external_sshd_config';
	
	# Get the contents of the sshd service startup file already on the computer
	my @sshd_service_file_lines = $self->os->get_file_contents($sshd_service_file_path);
	if (!@sshd_service_file_lines) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve contents of $sshd_service_file_path from $computer_node_name");
		return;
	}
	
	my $sshd_service_file_contents_original = join("\n", @sshd_service_file_lines);
	my $sshd_service_file_contents_updated = $sshd_service_file_contents_original;
	my $ext_sshd_service_file_contents = $sshd_service_file_contents_original;
	
	# Replace: OpenSSH --> externalOpenSSH
	$ext_sshd_service_file_contents =~ s|( OpenSSH)| external$1|g;
	
	# Replace: openssh-daemon --> external-openssh-daemon
	$ext_sshd_service_file_contents =~ s| (openssh-daemon)| external-$1|g;
	
	# Replace: sshd --> ext_sshd, exceptions:
	# /bin/sshd
	# /sshd_config
	# Note: pattern in look-behind assertion (?<!) must all be same length
	$ext_sshd_service_file_contents =~ s*(?<!(bin|pty)/)sshd(?!_config)*ext_sshd*g;
	
	# Replace: sshd_config --> external_sshd_config
	$ext_sshd_service_file_contents =~ s|(?:ext_)?(sshd_config)|external_$1|g;
	
	# Add config file path argument to '$SSHD $OPTIONS'
	$ext_sshd_service_file_contents =~ s|(\$SSHD)\s+(\$OPTIONS)|$1 -f $ext_sshd_config_file_path $2|g;
	
	# Replace:
	#    'pidfileofproc $SSHD' --> 'pidfileofproc $prog'
	#    'killproc $SSHD' --> 'killproc $prog'
	#    'status $SSHD' --> 'status $prog'
	$ext_sshd_service_file_contents =~ s/(pidfileofproc|killproc|status)\s+\$SSHD/$1 \$prog/g;
	
	# Update the sshd file as well or else 'service sshd status' will always report sshd is running if ext_sshd is running
	# The status line has to be: 'status -p $PID_FILE openssh-daemon'
	$sshd_service_file_contents_updated =~ s/(status)\s+.*/$1 -p \$PID_FILE openssh-daemon/g;
	
	# Check if any changes were made to the original sshd file
	if ($sshd_service_file_contents_updated ne $sshd_service_file_contents_original) {
		# Save a copy of the original sshd file if the backup doesn't already exist
		if (!$self->os->file_exists($sshd_service_file_path_original)) {
			$self->os->copy_file($sshd_service_file_path, $sshd_service_file_path_original);
		}
		
		if (!$self->os->create_text_file($sshd_service_file_path, $sshd_service_file_contents_updated)) {
			notify($ERRORS{'WARNING'}, 0, "failed to update sshd service file on $computer_node_name: $sshd_service_file_path");
		}
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "sshd service file on $computer_node_name does not need to be updated");
	}
	
	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, '755')) {
		notify($ERRORS{'WARNING'}, 0, "failed to set permissions on ext_sshd service file to 755 on $computer_node_name: $ext_sshd_service_file_path");
		return;
	}
	
	# Add the service
	return unless $self->add_service('ext_sshd');
	
	return $self->enable_service('ext_sshd');
}

#//////////////////////////////////////////////////////////////////////////////

1;
__END__

=head1 SEE ALSO

L<http://cwiki.apache.org/VCL/>

=cut
