#!/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.pm - VCL module to support Linux operating systems

=head1 SYNOPSIS

 Needs to be written

=head1 DESCRIPTION

 This module provides VCL support for Linux operating systems.

=cut

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

# Specify the lib path using FindBin
use FindBin;
use lib "$FindBin::Bin/../../..";

# Configure inheritance
use base qw(VCL::Module::OS);

# 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;
no warnings 'redefine';

use VCL::utils;
use English qw(-no_match_vars);
use Net::Netmask;
use File::Basename;
use File::Temp qw(tempfile mktemp);

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

=head1 CLASS VARIABLES

=cut

=head2 $SOURCE_CONFIGURATION_DIRECTORY

 Data type   : String
 Description : Location on the management node of the files specific to this OS
               module which are needed to configure the loaded OS on a computer.
               This is normally the directory under 'tools' named after this OS
               module.
               
               Example:
               /usr/local/vcl/tools/Linux

=cut

our $SOURCE_CONFIGURATION_DIRECTORY = "$TOOLS/Linux";

=head2 $NODE_CONFIGURATION_DIRECTORY

 Data type   : String
 Description : Location on computer loaded with a VCL image where configuration
               files and scripts reside.

=cut

our $NODE_CONFIGURATION_DIRECTORY = '/root/VCL';

=head2 $CAPTURE_DELETE_FILE_PATHS

 Data type   : Array
 Description : List of files to be deleted during the image capture process.

=cut

our $CAPTURE_DELETE_FILE_PATHS = [
	'/root/.ssh/id_rsa',
	'/root/.ssh/id_rsa.pub',
	'/root/*-v*.xml',
	'/etc/sysconfig/iptables*old*',
	'/etc/sysconfig/iptables_pre*',
	'/etc/udev/rules.d/70-persistent-net.rules',
	'/tmp/*',
	'/var/log/*.0*',
	'/var/log/*.1*',
	'/var/log/*-20*',
	'/var/log/*.gz',
	'/var/log/*.old',
];

=head2 $CAPTURE_CLEAR_FILE_PATHS

 Data type   : Array
 Description : List of files to be cleared during the image capture process.

=cut

our $CAPTURE_CLEAR_FILE_PATHS = [
	'/etc/hostname',
	'/var/log/audit/audit.log',
	'/var/log/auth.log',
	'/var/log/boot.log',
	'/var/log/kern.log',
	'/var/log/lastlog',
	'/var/log/maillog',
	'/var/log/messages',
	'/var/log/secure',
	'/var/log/syslog',
	'/var/log/udev',
	'/var/log/ufw.log',
	'/var/log/wtmp',
];

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

=head2 get_node_configuration_directory

 Parameters  : none
 Returns     : string
 Description : Retrieves the $NODE_CONFIGURATION_DIRECTORY variable value for
               the OS. This is the path on the computer's hard drive where image
               configuration files and scripts are copied.

=cut

sub get_node_configuration_directory {
	return $NODE_CONFIGURATION_DIRECTORY;
}

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

=head1 OBJECT METHODS

=cut

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

=head2 get_init_modules

 Parameters  : none
 Returns     : array of Linux init module references
 Description : Determines the Linux init daemon being used by the computer
               (SysV, systemd, etc.) and creates an object. The default is SysV
               if no other modules in the lib/VCL/Module/OS/Linux/init directory
               match the init daemon on the computer. The init module is mainly
               used to control services on the computer.

=cut

sub get_init_modules {
	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;
	}

	return @{$self->{init_modules}} if $self->{init_modules};
	
	notify($ERRORS{'DEBUG'}, 0, "beginning Linux init daemon module initialization");
	
	my $computer_node_name = $self->data->get_computer_node_name();
	
	# Get the absolute path of the init module directory
	my $init_directory_path = "$FindBin::Bin/../lib/VCL/Module/OS/Linux/init";
	notify($ERRORS{'DEBUG'}, 0, "Linux init module directory path: $init_directory_path");
	
	# Get a list of all *.pm files in the init module directory
	my @init_module_paths = $self->mn_os->find_files($init_directory_path, '*.pm');
	
	# Attempt to create an initialize an object for each init module
	my %init_module_hash;
	INIT_MODULE: for my $init_module_path (@init_module_paths) {
		my $init_name = fileparse($init_module_path, qr/\.pm$/i);
		my $init_perl_package = "VCL::Module::OS::Linux::init::$init_name";
		
		# Attempt to load the init module
		notify($ERRORS{'DEBUG'}, 0, "attempting to load $init_name init module: $init_perl_package");
		eval "use $init_perl_package";
		if ($EVAL_ERROR || $@) {
			notify($ERRORS{'CRITICAL'}, 0, "failed to load $init_name init module: $init_perl_package, error: $EVAL_ERROR");
			next INIT_MODULE;
		}
		
		# Attempt to create an init module object
		# The 'new' constructor will automatically call the module's initialize subroutine
		# initialize will check the computer to determine if it contains the corresponding Linux init daemon installed
		# If not installed, the constructor will return false
		my $init;
		eval { $init = ($init_perl_package)->new({
					data_structure => $self->data,
					os => $self,
					mn_os => $self->mn_os,
					init_modules => $self->{init_modules},
		}) };
		if ($init) {
			my @required_commands = eval "@" . $init_perl_package . "::REQUIRED_COMMANDS";
			if ($EVAL_ERROR) {
				notify($ERRORS{'CRITICAL'}, 0, "\@REQUIRED_COMMANDS variable is not defined in the $init_perl_package Linux init daemon module");
				next INIT_MODULE;
			}
			if (@required_commands) {
				for my $command (@required_commands) {
					if (!$self->command_exists($command)) {
						next INIT_MODULE;
					}
				}
			}
			
			my @prohibited_commands = eval "@" . $init_perl_package . "::PROHIBITED_COMMANDS";
			if (@prohibited_commands) {
				for my $command (@prohibited_commands) {
					if ($self->command_exists($command)) {
						notify($ERRORS{'DEBUG'}, 0, "ignoring $init_perl_package Linux init daemon module, '$command' command exists on $computer_node_name");
						next INIT_MODULE;
					}
				}
			}
			
			
			# init object successfully created, retrieve the module's $INIT_DAEMON_ORDER variable
			# An OS may have/support multiple Linux init daemons, services may be registered under different init daemons
			# In some cases, need to try multple init modules to control a service
			# This $INIT_DAEMON_ORDER integer determines the order in which the modules are tried
			my $init_daemon_order = eval '$' . $init_perl_package . '::INIT_DAEMON_ORDER';
			if ($EVAL_ERROR) {
				notify($ERRORS{'CRITICAL'}, 0, "\$INIT_DAEMON_ORDER variable is not defined in the $init_perl_package Linux init daemon module");
				next INIT_MODULE;
			}
			elsif ($init_module_hash{$init_daemon_order}) {
				notify($ERRORS{'CRITICAL'}, 0, "multiple Linux init daemon modules are configured to use \$INIT_DAEMON_ORDER=$init_daemon_order: " . ref($init_module_hash{$init_daemon_order}) . ", " . ref($init) . ", the value of this variable must be unique");
				next INIT_MODULE;
			}
			else {
				notify($ERRORS{'DEBUG'}, 0, "$init_name init object created and initialized to control $computer_node_name, order: $init_daemon_order");
				$init_module_hash{$init_daemon_order} = $init;
			}
		}
		elsif ($EVAL_ERROR) {
			notify($ERRORS{'WARNING'}, 0, "$init_perl_package init object could not be created, error:\n$EVAL_ERROR");
		}
		else {
			notify($ERRORS{'DEBUG'}, 0, "$init_name init object could not be initialized to control $computer_node_name");
		}
	}
	
	# Make sure at least 1 init module object was successfully initialized
	if (!%init_module_hash) {
		notify($ERRORS{'WARNING'}, 0, "failed to create Linux init daemon module");
		return;
	}
	
	# Construct an array of init module objects from highest to lowest $INIT_DAEMON_ORDER
	$self->{init_modules} = [];
	my $init_module_order_string;
	for my $init_daemon_order (sort {$a <=> $b} keys %init_module_hash) {
		push @{$self->{init_modules}}, $init_module_hash{$init_daemon_order};
		$init_module_order_string .= "$init_daemon_order: " . ref($init_module_hash{$init_daemon_order}) . "\n";
	}
	notify($ERRORS{'DEBUG'}, 0, "constructed array containing init module objects which may be used to control $computer_node_name:\n$init_module_order_string");
	return @{$self->{init_modules}};
}

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

=head2 firewall

 Parameters  : none
 Returns     : Linux firewall module reference
 Description : Determines the Linux firewall module to use and creates an
               object.

=cut

sub firewall {
	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;
	}

	return $self->{firewall} if $self->{firewall};
	
	notify($ERRORS{'DEBUG'}, 0, "beginning Linux firewall daemon module initialization");
	
	my $computer_node_name = $self->data->get_computer_node_name();
	
	# Get the absolute path of the init module directory
	my $firewall_directory_path = "$FindBin::Bin/../lib/VCL/Module/OS/Linux/firewall";
	notify($ERRORS{'DEBUG'}, 0, "Linux firewall module directory path: $firewall_directory_path");
	
	# Get a list of all *.pm files in the firewall module directory
	my @firewall_module_paths = $self->mn_os->find_files($firewall_directory_path, '*.pm');
	
	# Attempt to create an initialize an object for each firewall module
	my %firewall_module_hash;
	FIREWALL_MODULE: for my $firewall_module_path (@firewall_module_paths) {
		my $firewall_name = fileparse($firewall_module_path, qr/\.pm$/i);
		my $firewall_perl_package = "VCL::Module::OS::Linux::firewall::$firewall_name";
		
		# Attempt to load the module
		eval "use $firewall_perl_package";
		if ($EVAL_ERROR) {
			notify($ERRORS{'WARNING'}, 0, "$firewall_perl_package module could not be loaded, error:\n" . $EVAL_ERROR);
			return;
		}
		notify($ERRORS{'DEBUG'}, 0, "$firewall_perl_package module loaded");
		
		# Attempt to create the object
		my $firewall_object;
		eval {
			$firewall_object = ($firewall_perl_package)->new({
				data_structure => $self->data,
				os => $self,
				mn_os => $self->mn_os,
			})
		};
		
		if ($EVAL_ERROR) {
			notify($ERRORS{'WARNING'}, 0, "failed to create $firewall_perl_package object, error: $EVAL_ERROR");
		}
		elsif (!$firewall_object) {
			notify($ERRORS{'DEBUG'}, 0, "$firewall_perl_package object could not be initialized");
		}
		else {
			$self->{firewall} = $firewall_object;
			my $linux_address = sprintf('%x', $self);
			my $firewall_object_address = sprintf('%x', $firewall_object);
			my $self_firewall_address = sprintf('%x', $self->{firewall});
			notify($ERRORS{'DEBUG'}, 0, "$firewall_perl_package object created for $computer_node_name, Linux object address: $linux_address, firewall object address: $firewall_object_address, \$self->{firewall} address: $self_firewall_address");
			return $firewall_object;
		}
	}
	
	notify($ERRORS{'DEBUG'}, 0, "unable to initialize suitable specific firewall module, returning generic VCL::Module::OS::Linux::firewall object");
	return bless {}, 'VCL::Module::OS::Linux::firewall';
}

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

=head2 pre_capture

 Parameters  : none
 Returns     : boolean
 Description :

=cut

sub pre_capture {
	my $self = shift;
	my $args = 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;
	}
	
	# Check if end_state argument was passed
	if (defined $args->{end_state}) {
		$self->{end_state} = $args->{end_state};
	}
	else {
		$self->{end_state} = 'off';
	}
	
	my $computer_node_name = $self->data->get_computer_node_name();
	
	# Call OS::pre_capture to perform the pre-capture tasks common to all OS's
	if (!$self->SUPER::pre_capture($args)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute parent class pre_capture() subroutine");
		return;
	}
	
	notify($ERRORS{'OK'}, 0, "beginning Linux-specific image capture preparation tasks");
	
	if (!$self->generate_exclude_list_sample()) {
		notify($ERRORS{'DEBUG'}, 0, "could not create /root/.vclcontrol/vcl_exclude_list.sample");
	}
	
	# Force user off computer
	if (!$self->logoff_user()) {
		notify($ERRORS{'WARNING'}, 0, "unable to log user off $computer_node_name");
	}
	
	# Attempt to unmount NFS shares configured for the management node (Site Configuration > NFS Mounts)
	$self->unmount_nfs_shares() || return;
	$self->remove_matching_fstab_lines('Added by VCL');
	
	# Remove user accounts
	if ($self->delete_user_accounts()) {
		notify($ERRORS{'OK'}, 0, "deleted user accounts added by VCL from $computer_node_name");
	}
	
	# Attempt to set the root password to a known value
	# This is useful for troubleshooting image problems
	$self->set_password("root", $WINDOWS_ROOT_PASSWORD);
	
	# Prevent the "Text Mode Setup Utility" - "Choose a Tool" screen from appearing
	if ($self->service_exists('firstboot')) {
		$self->disable_service('firstboot');
	}
	
	if (!$self->configure_default_sshd()) {
		return;
	}
	
	if (!$self->configure_rc_local()) {
		return;
	}
	
	if ($self->can('firewall') && $self->firewall->can('process_pre_capture')) {
		$self->firewall->process_pre_capture() || return;
	}
	
	if (!$self->clean_known_files()) {
		notify($ERRORS{'WARNING'}, 0, "unable to clean known files");
	}
	
	# Configure the private and public interfaces to use DHCP
	my $private_interface_name = $self->get_private_interface_name();
	my $public_interface_name = $self->get_public_interface_name();
	
	if (!$self->enable_dhcp($private_interface_name)) {
		notify($ERRORS{'WARNING'}, 0, "failed to enable DHCP on the private interface");
		return;
	}
	if (!$self->enable_dhcp($public_interface_name)) {
		notify($ERRORS{'WARNING'}, 0, "failed to enable DHCP on the public interface");
		return;
	}
	
	# Delete route files if they exist for either the private or public interface
	$self->delete_file("/etc/sysconfig/network-scripts/route-$private_interface_name");
	$self->delete_file("/etc/sysconfig/network-scripts/route-$public_interface_name");
	
	# Remove computer/reservation specific lines from network file
	$self->remove_lines_from_file('/etc/sysconfig/network', 'HOSTNAME');
	$self->remove_lines_from_file('/etc/sysconfig/network', 'GATEWAY');
	
	# Shut the computer down
	if ($self->{end_state} =~ /off/i) {
		notify($ERRORS{'DEBUG'}, 0, "shutting down $computer_node_name, provisioning module specified end state: $self->{end_state}");
		if (!$self->shutdown()) {
			notify($ERRORS{'WARNING'}, 0, "failed to shut down $computer_node_name");
			return;
		}
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "$computer_node_name not shut down, provisioning module specified end state: $self->{end_state}");
	}
	
	notify($ERRORS{'OK'}, 0, "Linux pre-capture steps complete");
	return 1;
}

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

=head2 post_load

 Parameters  : none
 Returns     : boolean
 Description :

=cut

sub post_load {
	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 $image_name            = $self->data->get_image_name();
	my $computer_node_name    = $self->data->get_computer_node_name();
	my $image_os_install_type = $self->data->get_image_os_install_type();
	
	notify($ERRORS{'OK'}, 0, "beginning Linux post_load tasks, image: $image_name, computer: $computer_node_name");

	# Wait for computer to respond to SSH
	if (!$self->wait_for_response(5, 600, 5)) {
		notify($ERRORS{'WARNING'}, 0, "$computer_node_name never responded to SSH");
		return;
	}
	
	# Attempt to generate ifcfg-eth* files and start any interfaces which the file does not exist
	$self->activate_interfaces();
	
	# Configure the firewall to allow SSH traffic only from the management node
	if ($self->can('firewall') && $self->firewall->can('process_post_load')) {
		$self->firewall->process_post_load() || return;
	}
	
	# Make sure the public IP address assigned to the computer matches the database
	if (!$self->update_public_ip_address()) {
		notify($ERRORS{'WARNING'}, 0, "failed to update public IP address");
		return;
	}
	
	# Configure sshd to only listen on the private interface and add ext_sshd service listening on the public interface
	# This locks down sshd so that it isn't listening on the public interface -- ext_sshd isn't started yet
	if (!$self->configure_ext_sshd()) {
		notify($ERRORS{'WARNING'}, 0, "failed to configure ext_sshd on $computer_node_name");
		return 0;
	}
	
	# Remove commands from rc.local added by previous versions of VCL
	$self->configure_rc_local();
	
	# Kickstart installations likely won't have currentimage.txt, generate it
	if ($image_os_install_type eq "kickstart") {
		notify($ERRORS{'OK'}, 0, "detected kickstart install on $computer_node_name, writing current_image.txt");
		if (!$self->create_currentimage_txt()) {
			notify($ERRORS{'WARNING'}, 0, "failed to create currentimage.txt on $computer_node_name");
		}
	}
	
	# Update time and ntpservers
	if (!$self->synchronize_time()) {
		notify($ERRORS{'WARNING'}, 0, "unable to synchroinze date and time on $computer_node_name");
	}
	
	# Change password
	if (!$self->set_password("root")) {
		notify($ERRORS{'OK'}, 0, "failed to edit root password on $computer_node_name");
	}
	
	# Clear ssh idenity keys from /root/.ssh
	if (!$self->clear_private_keys()) {
		notify($ERRORS{'WARNING'}, 0, "failed to clear known identity keys");
	}
	
	# Update computer hostname if imagemeta.sethostname is not set to 0
	my $set_hostname = $self->data->get_imagemeta_sethostname(0);
	if (defined($set_hostname) && $set_hostname =~ /0/) {
		notify($ERRORS{'DEBUG'}, 0, "not setting computer hostname, imagemeta.sethostname = $set_hostname");
	}
	else {
		$self->update_public_hostname();
	}
	
	# Run the vcl_post_load script if it exists in the image
	my @post_load_script_paths = ('/usr/local/vcl/vcl_post_load', '/etc/init.d/vcl_post_load');	

	foreach my $script_path (@post_load_script_paths) {
		notify($ERRORS{'DEBUG'}, 0, "script_path $script_path");
		if ($self->file_exists($script_path)) {
			my $result = $self->run_script($script_path, '1', '300', '1');
			if (!defined($result)) {
				notify($ERRORS{'WARNING'}, 0, "error occurred running $script_path");
			}
			elsif ($result == 0) {
				notify($ERRORS{'DEBUG'}, 0, "$script_path does not exist in image: $image_name");
			}
			else {
				notify($ERRORS{'DEBUG'}, 0, "ran $script_path");
			}
		}
	}
	
	return $self->SUPER::post_load();
}

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

=head2 post_reserve

 Parameters  : none
 Returns     : boolean
 Description :

=cut

sub post_reserve {
	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 $reservation_id = $self->data->get_reservation_id();
	my $image_name = $self->data->get_image_name();
	my $computer_short_name = $self->data->get_computer_short_name();
	
	
	# User supplied data
	#check if variable is set
	#get variable from variable table related to server reservation id ‘userdata|<reservation id>’
	# write contents to local temp file /tmp/resrvationid_post_reserve_userdata
	# scp tmpfile to ‘/root/.vclcontrol/post_reserve_userdata’
	# assumes the image has the call in vcl_post_reserve to import/read the user data file
	my $variable_name = "userdata|$reservation_id"; 
	my $variable_data;
	my $target_location = "/root/.vclcontrol/post_reserve_userdata";
	if ($self->data->is_variable_set($variable_name)) {
		$variable_data = get_variable($variable_name);
		
		#write to local temp file
		my $tmpfile = "/tmp/$reservation_id" ."_post_reserve_userdata";
		if (open(TMP, ">$tmpfile")) {
			print TMP $variable_data;
			close(TMP);

			if ($self->copy_file_to($tmpfile, $target_location)) {
				notify($ERRORS{'DEBUG'}, 0, "copied $tmpfile to $target_location on $computer_short_name");	
			}
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to open $tmpfile for writing userdata");
		}
		#Clean variable from variable table
		if (delete_variable($variable_name)) {
			notify($ERRORS{'DEBUG'}, 0, "deleted variable_name $variable_name from variable table");
		}
	}
	
	# Check if script exists
	my @post_reserve_script_paths = ('/usr/local/vcl/vcl_post_reserve', '/etc/init.d/vcl_post_reserve');
	foreach my $script_path (@post_reserve_script_paths) {
		if ($self->file_exists($script_path)) {
			# If post_reserve script exists, assume it does user or reservation-specific actions
			# If the user never connects and the reservation times out, there's no way to revert these actions in order to clean the computer for another user
			# Tag the image as tainted so it is reloaded
			$self->set_tainted_status('post-reserve scripts residing in the image executed');
			
			# Run the vcl_post_reserve script if it exists in the image
			my $result = $self->run_script($script_path, '1', '300', '1');
			if (!defined($result)) {
				notify($ERRORS{'WARNING'}, 0, "error occurred running $script_path");
			}
			elsif ($result == 0) {
				notify($ERRORS{'DEBUG'}, 0, "$script_path does not exist in image: $image_name");
			}
			else {
				notify($ERRORS{'DEBUG'}, 0, "ran $script_path");
			}
		}
	}
	
	return $self->SUPER::post_reserve();
}

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

=head2 post_reservation

 Parameters  : none
 Returns     : boolean
 Description : Checks for and runs vcl_post_reservation script at the end of a
               reservation.

=cut

sub post_reservation {
	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 $script_path = '/usr/local/vcl/vcl_post_reservation';
	
	# Check if script exists
	if ($self->file_exists($script_path)) {
		# Run the vcl_post_reserve script if it exists in the image
		$self->run_script($script_path, '1', '300', '1');
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "script does NOT exist: $script_path");
	}
	
	return $self->SUPER::post_reservation();
}

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

=head2 update_public_hostname

 Parameters  : none
 Returns     : boolean
 Description : Retrieves the public IP address being used on the Linux computer.
               Determines the hostname the IP address resolves to. Sets the
               hostname on the Linux computer.

=cut

sub update_public_hostname {
	my $self = shift;
	unless (ref($self) && $self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine can only be called as a VCL::Module module object method");
		return;
	}
	
	my $computer_node_name = $self->data->get_computer_node_name();
	
	my $public_hostname = shift;
	if (!$public_hostname) {
		# Get the IP address of the public adapter
		my $public_ip_address = $self->get_public_ip_address();
		if (!$public_ip_address) {
			notify($ERRORS{'WARNING'}, 0, "hostname cannot be set, unable to determine public IP address");
			return;
		}
		notify($ERRORS{'DEBUG'}, 0, "retrieved public IP address of $computer_node_name: $public_ip_address");
		
		# Get the hostname for the public IP address
		$public_hostname = ip_address_to_hostname($public_ip_address) || $computer_node_name;
	}
	
	my $error_occurred = 0;
	
	# Check if hostname file exists and update if necessary
	my $hostname_file_path = '/etc/hostname';
	if ($self->file_exists($hostname_file_path)) {
		if ($self->create_text_file($hostname_file_path, $public_hostname)) {
			notify($ERRORS{'DEBUG'}, 0, "updated $hostname_file_path on $computer_node_name with hostname '$public_hostname'");
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to update $hostname_file_path on $computer_node_name with '$public_hostname'");
			$error_occurred = 1;
		}
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "$hostname_file_path not updated on $computer_node_name because the file does not exist");
	}
	
	
	# Check if network file exists and update if necessary
	my $network_file_path = '/etc/sysconfig/network';
	if ($self->file_exists($network_file_path)) {
		my $sed_command = "sed -i -e \"/^HOSTNAME=/d\" $network_file_path; echo \"HOSTNAME=$public_hostname\" >> $network_file_path";
		my ($sed_exit_status, $sed_output) = $self->execute($sed_command);
		if (!defined($sed_output)) {
			notify($ERRORS{'WARNING'}, 0, "failed to execute command to update hostname in $network_file_path on $computer_node_name");
			return;
		}
		elsif ($sed_exit_status != 0) {
			notify($ERRORS{'WARNING'}, 0, "failed to update hostname in $network_file_path on $computer_node_name, exit status: $sed_exit_status, output:\n" . join("\n", @$sed_output));
			$error_occurred = 1;
		}
		else {
			notify($ERRORS{'OK'}, 0, "updated hostname in $network_file_path on $computer_node_name to '$public_hostname'");
		}
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "$network_file_path not updated on $computer_node_name because the file does not exist");
	}
	
	# Check if hostnamectl exists, this is provided by systemd on CentOS/RHEL 7+
	if ($self->command_exists('hostnamectl')) {
		my $hostnamectl_command = "hostnamectl set-hostname $public_hostname";
		my ($hostnamectl_exit_status, $hostnamectl_output) = $self->execute($hostnamectl_command);
		if (!defined($hostnamectl_output)) {
			notify($ERRORS{'WARNING'}, 0, "failed to execute command to set hostname using hostnamectl command on $computer_node_name to $public_hostname");
			return;
		}
		elsif ($hostnamectl_exit_status != 0) {
			notify($ERRORS{'WARNING'}, 0, "failed to set hostname using hostnamectl command on $computer_node_name to $public_hostname, exit status: $hostnamectl_exit_status, command: '$hostnamectl_command', output:\n" . join("\n", @$hostnamectl_output));
			$error_occurred = 1;
		}
		else {
			notify($ERRORS{'OK'}, 0, "set hostname using hostnamectl command on $computer_node_name to $public_hostname");
		}
	}
	else {
		my $hostname_command = "hostname $public_hostname";
		my ($hostname_exit_status, $hostname_output) = $self->execute($hostname_command);
		if (!defined($hostname_output)) {
			notify($ERRORS{'WARNING'}, 0, "failed to execute command to set hostname using hostname command on $computer_node_name to $public_hostname");
			return;
		}
		elsif ($hostname_exit_status != 0) {
			notify($ERRORS{'WARNING'}, 0, "failed to set hostname using hostname command on $computer_node_name to $public_hostname, exit status: $hostname_exit_status, command: '$hostname_command', output:\n" . join("\n", @$hostname_output));
			$error_occurred = 1;
		}
		else {
			notify($ERRORS{'OK'}, 0, "set hostname using hostname command on $computer_node_name to $public_hostname");
		}
	}
	
	return !$error_occurred;
}

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

=head2 clear_private_keys

 Parameters  :
 Returns     :
 Description :

=cut

sub clear_private_keys {
	my $self = shift;
	unless (ref($self) && $self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine can only be called as a VCL::Module module object method");
		return;
	}
	
	notify($ERRORS{'DEBUG'}, 0, "perparing to clear known identity keys");
	my $management_node_keys = $self->data->get_management_node_keys();
	my $computer_short_name  = $self->data->get_computer_short_name();
	my $computer_node_name   = $self->data->get_computer_node_name();
	
	# Clear ssh idenity keys from /root/.ssh
	my $clear_private_keys = "/bin/rm -f /root/.ssh/id_rsa /root/.ssh/id_rsa.pub";
	if ($self->execute($clear_private_keys)) {
		notify($ERRORS{'DEBUG'}, 0, "cleared any id_rsa keys from /root/.ssh");
		return 1;
	}
	else {
		notify($ERRORS{'CRITICAL'}, 0, "failed to clear any id_rsa keys from /root/.ssh");
		return 0;
	}
}

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

=head2 set_static_public_address

 Parameters  : none
 Returns     : boolean
 Description : Configures the public interface with a static IP address.

=cut

sub set_static_public_address {
	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_name = $self->data->get_computer_short_name();
	my $ip_configuration = $self->data->get_management_node_public_ip_configuration();
	my $public_ip_address = $self->data->get_computer_public_ip_address();
	my $subnet_mask = $self->data->get_management_node_public_subnet_mask();
	my @dns_servers = $self->data->get_management_node_public_dns_servers();
	
	# TODO: Get this out of here. OS modules shouldn't have to figure this out. $self->data should always return correct value.
	my $server_request_fixed_ip = $self->data->get_server_request_fixed_ip();
	if ($server_request_fixed_ip) {
		$public_ip_address = $server_request_fixed_ip;
		$subnet_mask = $self->data->get_server_request_netmask();
		@dns_servers = $self->data->get_server_request_dns_servers();
	}
	
	# Make sure public IP configuration is static or this is a server request
	if ($ip_configuration !~ /static/i && !$server_request_fixed_ip) {
		notify($ERRORS{'WARNING'}, 0, "management node IP configuration is $ip_configuration, static public IP address can only be set if the IP configuration is static or if a fixed IP was requested");
		return;
	}
	elsif (!$public_ip_address) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve public IP address to assign to $computer_name");
		return;
	}
	elsif (!$subnet_mask) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve public subnet mask to assign to $computer_name");
		return;
	}
	
	# Determine the public interface name
	my $public_interface_name = $self->get_public_interface_name();
	if (!$public_interface_name) {
		notify($ERRORS{'WARNING'}, 0, "unable to set static public IP address, public interface name could not be determined");
		return;
	}

	# Get the current public IP address being used by the computer
	# Use cached data if available (0), ignore errors (1)
	my $current_public_ip_address = $self->get_public_ip_address(0, 1);
	if ($current_public_ip_address && $current_public_ip_address eq $public_ip_address) {
		notify($ERRORS{'DEBUG'}, 0, "static public IP address does not need to be set, $computer_name is already configured to use $current_public_ip_address");
	}
	else {
		if ($current_public_ip_address) {
			notify($ERRORS{'DEBUG'}, 0, "static public IP address needs to be set, public IP address currently being used by $computer_name $current_public_ip_address does NOT match correct public IP address: $public_ip_address");
		}
		else {
			notify($ERRORS{'DEBUG'}, 0, "static public IP address needs to be set, unable to determine public IP address currently in use on $computer_name");
		}
		
		
		# Try to ping address to make sure it's available
		# FIXME  -- need to add other tests for checking ip_address is or is not available.
		if (_pingnode($public_ip_address)) {
			notify($ERRORS{'CRITICAL'}, 0, "ip_address $public_ip_address is pingable, can not assign to $computer_name ");
			return;
		}
		
		notify($ERRORS{'DEBUG'}, 0, "attempting to set static public IP address on $computer_name:\n" .
			"interface: $public_interface_name\n" .
			"IP address: $public_ip_address\n" .
			"subnet mask: $subnet_mask"
		);
		
		my $ifcfg_parameters = {
			bootproto => 'static',
			ipaddr => $public_ip_address,
			netmask => $subnet_mask,
		};
		
		if (!$self->generate_ifcfg_file($public_interface_name, $ifcfg_parameters)) {
			notify($ERRORS{'WARNING'}, 0, "failed to set static public IP address on $computer_name, ifcfg file could not be created");
			return;
		}
		
		# Restart the interface
		if (!$self->restart_network_interface($public_interface_name)) {
			notify($ERRORS{'WARNING'}, 0, "failed to restart public interface $public_interface_name on $computer_name");
			return;
		}
	}
	
	# Set default gateway
	if (!$self->set_static_default_gateway()) {
		notify($ERRORS{'WARNING'}, 0, "failed to set static public IP address on $computer_name, default gateway could not be set");
		return;
	}
	
	# Update resolv.conf if DNS server address is configured for the management node
	if (@dns_servers) {
		if (!$self->update_resolv_conf(@dns_servers)) {
			notify($ERRORS{'WARNING'}, 0, "failed to set static public IP address on $computer_name, DNS servers could not be configured");
			return;
		}
	}
	
	# Delete cached network configuration info - forces next call to get_network_configuration to retrieve changed network info from computer
	delete $self->{network_configuration};
	
	notify($ERRORS{'OK'}, 0, "set static public IP address on $computer_name");
	return 1;
}

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

=head2 start_network_interface

 Parameters  : $interface_name
 Returns     : boolean
 Description : Calls ifup on the network interface.

=cut

sub start_network_interface {
	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 $interface_name = shift;
	if (!$interface_name) {
		notify($ERRORS{'WARNING'}, 0, "unable to start network interface, interface name argument was not supplied");
		return;
	}
	
	my $computer_name = $self->data->get_computer_short_name();
	
	notify($ERRORS{'DEBUG'}, 0, "attempting to start network interface $interface_name on $computer_name");
	
	my $command = "/sbin/ifup $interface_name";
	my ($exit_status, $output) = $self->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to start $interface_name interface on $computer_name");
		return;
	}
	elsif (grep(/already configured/i, @$output)) {
		notify($ERRORS{'DEBUG'}, 0, "$interface_name interface on $computer_name is already started, output:\n" . join("\n", @$output));
	}
	elsif ($exit_status == 0 || grep(/done/i, @$output)) {
		notify($ERRORS{'DEBUG'}, 0, "started $interface_name interface on $computer_name, " . (@$output ? "output:\n" . join("\n", @$output) : 'no output'));
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to start $interface_name interface on $computer_name, exit status: $exit_status, command: '$command', output:\n" . join("\n", @$output));
		return;
	}
	
	return 1;
}

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

=head2 stop_network_interface

 Parameters  : $interface_name
 Returns     : boolean
 Description : Calls ifdown on the network interface.

=cut

sub stop_network_interface {
	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 $interface_name = shift;
	if (!$interface_name) {
		notify($ERRORS{'WARNING'}, 0, "unable to stop network interface, interface name argument was not supplied");
		return;
	}
	
	my $computer_name = $self->data->get_computer_short_name();
	
	notify($ERRORS{'DEBUG'}, 0, "attempting to stop network interface $interface_name on $computer_name");
	
	my $command = "/sbin/ifdown $interface_name";
	my ($exit_status, $output) = $self->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to stop $interface_name interface on $computer_name");
		return;
	}
	elsif (grep(/not configured/i, @$output)) {
		notify($ERRORS{'DEBUG'}, 0, "$interface_name interface on $computer_name is already stopped, output:\n" . join("\n", @$output));
		return 1;
	}
	elsif ($exit_status) {
		notify($ERRORS{'WARNING'}, 0, "failed to stop $interface_name interface on $computer_name, exit status: $exit_status, command: '$command', output:\n" . join("\n", @$output));
		return;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "stopped $interface_name interface on $computer_name, output:\n" . join("\n", @$output));
	}
	
	return 1;
}

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

=head2 restart_network_interface

 Parameters  : $interface_name
 Returns     : boolean
 Description : Calls ifdown and then ifup on the network interface.

=cut

sub restart_network_interface {
	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 $interface_name = shift;
	if (!$interface_name) {
		notify($ERRORS{'WARNING'}, 0, "unable to restart network interface, interface name argument was not supplied");
		return;
	}
	
	my $computer_name = $self->data->get_computer_short_name();
	
	notify($ERRORS{'DEBUG'}, 0, "attempting to restart network interface $interface_name on $computer_name");
	
	my $command = "/sbin/ifdown $interface_name ; /sbin/ifup $interface_name";
	my ($exit_status, $output) = $self->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to restart $interface_name interface on $computer_name");
		return;
	}
	elsif ($exit_status) {
		notify($ERRORS{'WARNING'}, 0, "failed to restart $interface_name interface on $computer_name, exit status: $exit_status, command: '$command', output:\n" . join("\n", @$output));
		return;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "restarted $interface_name interface on $computer_name, output:\n" . join("\n", @$output));
	}
	
	return 1;
}

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

=head2 delete_default_gateway

 Parameters  : none
 Returns     : boolean
 Description : Deletes the existing default gateway from the routing table.

=cut

sub delete_default_gateway {
	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_name = $self->data->get_computer_short_name();
	
	my $command = "/sbin/route del default";
	my ($exit_status, $output) = $self->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to delete default gateway on $computer_name: $command");
		return;
	}
	elsif (grep(/No such process/i, @$output)) {
		notify($ERRORS{'DEBUG'}, 0, "default gateway not set on $computer_name");
	}
	elsif ($exit_status ne '0') {
		notify($ERRORS{'WARNING'}, 0, "failed to delete default gateway on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
		return 0;
	}
	else {
		notify($ERRORS{'OK'}, 0, "deleted default gateway on $computer_name");
	}
	
	return 1;
}

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

=head2 set_static_default_gateway

 Parameters  : none
 Returns     : boolean
 Description : Sets the default route.

=cut

sub set_static_default_gateway {
	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_name = $self->data->get_computer_short_name();
	
	my $default_gateway = $self->get_correct_default_gateway();
	if (!$default_gateway) {
		notify($ERRORS{'WARNING'}, 0, "unable to set static default gateway on $computer_name, correct default gateway IP address could not be determined");
		return;
	}
	
	my $current_default_gateway = $self->get_public_default_gateway();
	if ($current_default_gateway && $current_default_gateway eq $default_gateway) {
		notify($ERRORS{'OK'}, 0, "default gateway on $computer_name is already set to $current_default_gateway");
		return 1;
	}
	
	my $interface_name = $self->get_public_interface_name();
	if (!$interface_name) {
		notify($ERRORS{'WARNING'}, 0, "unable to set static default gateway on $computer_name, public interface name could not be determined");
		return;
	}
	
	# Delete existing default gateway or else error will occur: SIOCADDRT: File exists
	$self->delete_default_gateway();
	
	my $command = "/sbin/route add default gw $default_gateway metric 0 $interface_name";
	my ($exit_status, $output) = $self->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to set default gateway on $computer_name: $command");
		return;
	}
	elsif ($exit_status ne '0') {
		notify($ERRORS{'WARNING'}, 0, "failed to set default gateway on $computer_name to $default_gateway, interface: $interface_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
		return 0;
	}
	
	# Create a route file so default route persists across reboots
	my $route_file_path = "/etc/sysconfig/network-scripts/route-$interface_name";
	# For testing:
	#$self->delete_file($route_file_path);
	my $route_file_contents = "default via $default_gateway dev $interface_name";
	$self->create_text_file($route_file_path, $route_file_contents);
	
	# Adding a route-* file does not prevent computer from obtaining a default route via DHCP
	# Add a 'DEFROUTE=no' line to the ifcfg-<interface> file
	my $interface_file = "/etc/sysconfig/network-scripts/ifcfg-$interface_name";
	# For testing:
	#$self->remove_lines_from_file($interface_file, 'DEFROUTE');
	if ($self->file_exists($interface_file)) {
		$self->set_config_file_parameter($interface_file, 'DEFROUTE', '=', 'no');
	}
	
	# Note: leave for future reference, this doesn't seem to work on CentOS/RHEL 7
	# Add a 'GATEWAY=' line to /etc/sysconfig/network
	#my $network_file = "/etc/sysconfig/network";
	# For testing: $self->remove_lines_from_file($network_file, 'GATEWAY');
	#$self->set_config_file_parameter($network_file, 'GATEWAY', '=', $default_gateway);
	
	notify($ERRORS{'OK'}, 0, "set default gateway on $computer_name to $default_gateway, interface: $interface_name");
	return 1;
}

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

=head2 logoff_user

 Parameters  :
 Returns     :
 Description :

=cut

sub logoff_user {
	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;
	}
	
	# Make sure the user login ID was passed
	my $user_login_id = shift || $self->data->get_user_login_id();
	if (!$user_login_id) {
		notify($ERRORS{'WARNING'}, 0, "user could not be determined");
		return 0;
	}
	
	# Make sure the user login ID was passed
	my $computer_node_name = shift || $self->data->get_computer_node_name();
	if (!$computer_node_name) {
		notify($ERRORS{'WARNING'}, 0, "computer node name could not be determined");
		return 0;
	}
	
	my $logoff_cmd = "pkill -KILL -u $user_login_id";
	my ($exit_status, $output) = $self->execute($logoff_cmd);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to log off $user_login_id from $computer_node_name");
		return;
	}
	elsif (grep(/invalid user name/i, @$output)) {
		notify($ERRORS{'DEBUG'}, 0, "user $user_login_id does not exist on $computer_node_name");
		return 1;
	}
	elsif ($exit_status ne '0' && $exit_status ne '1') {
		# pkill will exit with status = 1 if one or more processes were killed, and 1 if no processes matched
		notify($ERRORS{'WARNING'}, 0, "error occurred attempting to log off $user_login_id from $computer_node_name, exit status: $exit_status, output:\n" . join("\n", @$output));
		return;
	}
	else {
		notify($ERRORS{'OK'}, 0, "logged off $user_login_id from $computer_node_name");
	}
	
	return 1;
}

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

=head2 reserve

 Parameters  : none
 Returns     : boolean
 Description : Performs the steps necessary to reserve a computer for a user.
               A "vcl" user group is added.

=cut

sub reserve {
	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;
	}
	
	notify($ERRORS{'OK'}, 0, "beginning Linux reserve tasks");
	
	# Add a local vcl user group if it doesn't already exist
	# Do this before OS.pm::reserve calls add_user_accounts
	$self->add_vcl_usergroup();
	
	# Configure sshd to only listen on the private interface and add ext_sshd service listening on the public interface
	# This needs to be done after update_public_ip_address is called from OS.pm::reserve
	$self->configure_ext_sshd() || return;
	
	# Call OS.pm's reserve subroutine
	$self->SUPER::reserve() || return;
	
	# Attempt to mount NFS shares configured for the management node (Site Configuration > NFS Mounts)
	$self->mount_nfs_shares();
	
	notify($ERRORS{'OK'}, 0, "Linux reserve tasks complete");
	return 1;
} ## end sub reserve

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

=head2 grant_access

 Parameters  : none
 Returns     : boolean
 Description : adds username to external_sshd_config and and starts sshd with
               custom config

=cut

sub grant_access {
	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();
	
	if ($self->can('firewall') && $self->firewall->can('process_reserved')) {
		if (!$self->firewall->process_reserved()) {
			notify($ERRORS{'WARNING'}, 0, "failed to grant access to $computer_node_name, firewall configuration failed");
			return;
		}
	}
	
	# Process the connection methods, allow firewall access from any address
	if ($self->process_connect_methods("", 1)) {
		notify($ERRORS{'DEBUG'}, 0, "granted access to $computer_node_name by processing the connection methods");
		return 1;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to grant access to $computer_node_name by processing the connection methods");
		return;
	}
} ## end sub grant_access

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

=head2 synchronize_time

 Parameters  : none
 Returns     : boolean
 Description : 

=cut

sub synchronize_time {
	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 $management_node_hostname = $self->data->get_management_node_hostname();
	
	my $variable_name = "timesource|$management_node_hostname";
	my $variable_name_global = "timesource|global";
	
	my $time_source_variable;
	if (is_variable_set($variable_name)) {
		$time_source_variable = get_variable($variable_name);
		notify($ERRORS{'DEBUG'}, 0, "retrieved time source variable '$variable_name': $time_source_variable");
	}
	elsif (is_variable_set($variable_name_global)) {
		$time_source_variable = get_variable($variable_name_global);
		notify($ERRORS{'DEBUG'}, 0, "retrieved global time source variable '$variable_name_global': $time_source_variable");
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "unable to sync time, neither '$variable_name' or '$variable_name_global' time source variable is set in database");
		return;
	}
	
	# Split the time source variable into an array
	my @time_sources = split(/[,; ]+/, $time_source_variable);
	
	# Assemble the rdate command
	# Ubuntu doesn't accept multiple servers in a single command
	my $rdate_command;
	for my $time_source (@time_sources) {
		$rdate_command .= "rdate -t 3 -s $time_source || ";
	}
	$rdate_command =~ s/[ \|]+$//g;
	my ($rdate_exit_status, $rdate_output) = $self->execute($rdate_command, 0, 180);
	if (!defined($rdate_output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute rdate command to synchronize time on $computer_node_name");
		return;
	}
	elsif (grep(/not found/i, @$rdate_output)) {
		notify($ERRORS{'DEBUG'}, 0, "unable to synchronize time on $computer_node_name, rdate is not installed");
	}
	elsif ($rdate_exit_status > 0) {
		notify($ERRORS{'WARNING'}, 0, "failed to synchronize time on $computer_node_name using rdate, exit status: $rdate_exit_status, command:\n$rdate_command\noutput:\n" . join("\n", @$rdate_output));
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "synchronized time on $computer_node_name using rdate");
	}
	
	# Check if the ntpd service exists before attempting to configure it
	if (!$self->service_exists('ntpd')) {
		notify($ERRORS{'DEBUG'}, 0, "skipping ntpd configuration, ntpd service does not exist");
		return 1;
	}
	
	# Update ntpservers file
	my $ntpservers_contents = join("\n", @time_sources);
	if (!$self->create_text_file('/etc/ntp/ntpservers', $ntpservers_contents)) {
		return;
	}
	
	return $self->restart_service('ntpd');
}

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

=head2 set_password

 Parameters  : $username, $password (optional)
 Returns     : boolean
 Description : Sets password for the account specified by the username argument.
               If no password argument is supplied, a random password is
               generated.

=cut

sub set_password {
	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 $username = shift;
	my $password  = shift;
	
	if (!$username) {
		notify($ERRORS{'WARNING'}, 0, "username argument was not provided");
		return;
	}
	
	if (!$password) {
		$password = getpw(15);
	}
	
	my $command = "echo -e '";
	$command .= qq[$password];
	$command .= "' \| /usr/bin/passwd -f $username --stdin";
	my ($exit_status, $output) = $self->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to set password for $username");
		return;
	}
	elsif (grep(/(unknown user|warning|error)/i, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to change password for $username to '$password', command: '$command', output:\n" . join("\n", @$output));
		return;
	}
	else {
		notify($ERRORS{'OK'}, 0, "changed password for $username to '$password', output:\n" . join("\n", @$output));
		return 1;
	}
}

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

=head2 sanitize

 Parameters  :
 Returns     :
 Description :

=cut

sub sanitize {
	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();
	
	# Make sure user is not connected
	if ($self->is_connected()) {
		notify($ERRORS{'WARNING'}, 0, "unable to sanitize $computer_node_name, user is connected");
		return 0;
	}
	
	if ($self->can('firewall') && $self->firewall->can('process_sanitize')) {
		$self->firewall->process_sanitize() || return;
	}
	
	# Call process_connect_methods with the overwrite flag to remove firewall exceptions
	$self->process_connect_methods() || return;
	
	# Attempt to unmount NFS shares configured for the management node (Site Configuration > NFS Mounts)
	$self->unmount_nfs_shares() || return;
	$self->remove_matching_fstab_lines('Added by VCL');

	# Delete all user associated with the reservation
	$self->delete_user_accounts() || return;
	
	# Make sure ext_sshd is stopped
	$self->stop_external_sshd() || return;
	
	notify($ERRORS{'OK'}, 0, "$computer_node_name has been sanitized");
	return 1;
} ## end sub sanitize

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

=head2 add_vcl_usergroup

 Parameters  : 
 Returns     : 1
 Description : step to add a user group to avoid group errors from useradd cmd 

=cut

sub add_vcl_usergroup {
	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();
	
	if ($self->execute("groupadd vcl")) {
		notify($ERRORS{'DEBUG'}, 0, "successfully added the vcl user group to $computer_node_name");
	}
	
	return 1;
}

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

=head2 is_connected

 Parameters  : none
 Returns     : boolean, undefined if error occurred
 Description : Checks if a connection on port 22 is established to the
               computer's public IP address.

=cut

sub is_connected {
	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 $computer_public_ip_address = $self->data->get_computer_public_ip_address();
	if (!$computer_public_ip_address) {
		notify($ERRORS{'WARNING'}, 0, "unable to determine if connection exists to $computer_node_name, public IP address could not be determined");
		return;
	}

	my $command = "netstat -an | grep ESTABLISHED";
	my ($exit_status, $output) = $self->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command on $computer_node_name: $command");
		return;
	}
	
	if (grep(/(Warning|Connection refused)/i, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "unable to determine if connection exists to $computer_public_ip_address on $computer_node_name, output:\n" . join("\n", @$output));
		return;
	}
	elsif (my ($line) = grep(/tcp\s+([0-9]*)\s+([0-9]*)\s($computer_public_ip_address:22)\s+([.0-9]*):([0-9]*)(.*)(ESTABLISHED)/, @$output)) {
		notify($ERRORS{'DEBUG'}, 0, "connection exists to $computer_public_ip_address on $computer_node_name:\n$line");
		return 1;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "connection does not exist to $computer_public_ip_address on $computer_node_name");
		return 0;
	}
} ## end sub is_connected

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

=head2 run_script

 Parameters  : script path
 Returns     : boolean
 Description : Checks if script exists on the Linux node and attempts to run it.

=cut

sub run_script {
	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;
	}
	
	# Get the script path argument
	my $script_path = shift;
	if (!$script_path) {
		notify($ERRORS{'WARNING'}, 0, "script path argument was not specified");
		return;
	}
	my $display_output  = shift || 0;
	my $timeout_seconds = shift || 60;
	my $max_attempts    = shift || 3;
	
	# Check if script exists
	if ($self->file_exists($script_path)) {
		notify($ERRORS{'DEBUG'}, 0, "script exists: $script_path");
	}
	else {
		notify($ERRORS{'OK'}, 0, "script does NOT exist: $script_path");
		return 0;
	}
	
	# Determine the script name
	my ($script_name) = $script_path =~ /\/([^\/]+)$/;
	notify($ERRORS{'DEBUG'}, 0, "script name: $script_name");
	
	# Get the node configuration directory, make sure it exists, create if necessary
	my $node_log_directory = $self->get_node_configuration_directory() . '/Logs';
	if (!$self->create_directory($node_log_directory)) {
		notify($ERRORS{'WARNING'}, 0, "failed to create node log file directory: $node_log_directory");
		return;
	}
	
	my $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();
	
	# Assemble the log file path
	my $log_file_path = $node_log_directory . "/$script_name.log";
	notify($ERRORS{'DEBUG'}, 0, "script log file path: $log_file_path");
	
	# Assemble the command
	my $command = "chmod +rx \"$script_path\" && \"$script_path\" >> \"$log_file_path\" 2>&1";
	
	# Execute the command
	my ($exit_status, $output) = $self->execute($command, $display_output, $timeout_seconds, $max_attempts);
	if (defined($exit_status) && $exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "executed $script_path, exit status: $exit_status");
	}
	elsif (defined($exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "$script_path returned a non-zero exit status: $exit_status, command: '$command'");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to execute $script_path");
		return;
	}
	
	return 1;
}

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

=head2 file_exists

 Parameters  : $file_path, $display_output (optional)
 Returns     : boolean
 Description : Checks if a file or directory exists on the Linux computer.

=cut

sub file_exists {
	my $self = shift;
	if (ref($self) !~ /module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	# Get the path from the subroutine arguments and make sure it was passed
	my $file_path = shift;
	if (!$file_path) {
		notify($ERRORS{'WARNING'}, 0, "path argument was not specified");
		return 0;
	}
	
	my $display_output = shift;
	if (!defined($display_output)) {
		$display_output = 1;
	}
	
	# Remove any quotes from the beginning and end of the path
	$file_path = normalize_file_path($file_path);
	
	# Escape all spaces in the path
	my $escaped_path = escape_file_path($file_path);
	
	my $computer_short_name = $self->data->get_computer_short_name();
	
	# Check if the file or directory exists
	# Do not enclose the path in quotes or else wildcards won't work
	my $command = "stat $escaped_path";
	my ($exit_status, $output) = $self->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'DEBUG'}, 0, "failed to run command to determine if file or directory exists on $computer_short_name:\npath: '$file_path'\ncommand: '$command'");
		return 0;
	}
	elsif (grep(/no such file/i, @$output)) {
		notify($ERRORS{'DEBUG'}, 0, "file or directory does not exist on $computer_short_name: '$file_path'") if $display_output;
		return 0;
	}
	elsif (grep(/stat: /i, @$output)) {
		notify($ERRORS{'DEBUG'}, 0, "failed to determine if file or directory exists on $computer_short_name:\npath: '$file_path'\ncommand: '$command'\nexit status: $exit_status, output:\n" . join("\n", @$output));
		return 0;
	}
	
	# Count the lines beginning with "Size:" and ending with "file", "directory", or "link" to determine how many files and/or directories were found
	my $files_found       = grep(/^\s*Size:.*file$/i,      @$output);
	my $directories_found = grep(/^\s*Size:.*directory$/i, @$output);
	my $links_found       = grep(/^\s*Size:.*link$/i,      @$output);
	
	if ($files_found || $directories_found || $links_found) {
		notify($ERRORS{'DEBUG'}, 0, "'$file_path' exists on $computer_short_name, files: $files_found, directories: $directories_found, links: $links_found") if $display_output;
		return 1;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "unexpected output returned while attempting to determine if file or directory exists on $computer_short_name: '$file_path'\ncommand: '$command'\nexit status: $exit_status, output:\n" . join("\n", @$output));
		return 0;
	}
}

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

=head2 delete_file

 Parameters  : $path
 Returns     : boolean
 Description : Deletes files or directories on the Linux computer.

=cut

sub delete_file {
	my $self = shift;
	if (ref($self) !~ /module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the path argument
	my $path = shift;
	if (!$path) {
		notify($ERRORS{'WARNING'}, 0, "path argument were not specified");
		return;
	}
	
	# Remove any quotes from the beginning and end of the path
	$path = normalize_file_path($path);
	
	# Escape all spaces in the path
	my $escaped_path = escape_file_path($path);
	
	my $computer_short_name = $self->data->get_computer_short_name();
	
	# Delete the file
	my $command = "rm -rfv $escaped_path";
	my ($exit_status, $output) = $self->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run command to delete file or directory on $computer_short_name:\npath: '$path'\ncommand: '$command'");
		return;
	}
	elsif (grep(/(cannot access|no such file)/i, @$output)) {
		notify($ERRORS{'OK'}, 0, "file or directory not deleted because it does not exist on $computer_short_name: $path");
	}
	elsif (grep(/rm: /i, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "error occurred attempting to delete file or directory on $computer_short_name: '$path':\ncommand: '$command'\nexit status: $exit_status\noutput:\n" . join("\n", @$output));
	}
	else {
		notify($ERRORS{'OK'}, 0, "deleted '$path' on $computer_short_name");
	}
	
	# Make sure the path does not exist
	my $file_exists = $self->file_exists($path, 0);
	if (!defined($file_exists)) {
		notify($ERRORS{'WARNING'}, 0, "failed to confirm file doesn't exist on $computer_short_name: '$path'");
		return;
	}
	elsif ($file_exists) {
		notify($ERRORS{'WARNING'}, 0, "file was not deleted, it still exists on $computer_short_name: '$path'");
		return;
	}
	else {
		#notify($ERRORS{'DEBUG'}, 0, "confirmed file does not exist on $computer_short_name: '$path'");
		return 1;
	}
}

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

=head2 clear_file

 Parameters  : $file_path
 Returns     : boolean
 Description : Clears a file on the computer via 'cat /dev/null'. If the file
               doesn't exist it is not created and true is returned.

=cut

sub clear_file {
	my $self = shift;
	if (ref($self) !~ /module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the path argument
	my $file_path = shift;
	if (!$file_path) {
		notify($ERRORS{'WARNING'}, 0, "file path argument was not specified");
		return;
	}
	
	my $computer_short_name = $self->data->get_computer_short_name();
	
	# Check if the file exists
	if (!$self->file_exists($file_path, 0)) {
		notify($ERRORS{'DEBUG'}, 0, "file not cleared on $computer_short_name because it doesn't exist: $file_path");
		return 1;
	}
	
	# Remove any quotes from the beginning and end of the path
	$file_path = normalize_file_path($file_path);
	
	# Escape all spaces in the path
	my $escaped_file_path = escape_file_path($file_path);
	
	# Clear the file
	my $command = "cat /dev/null > $escaped_file_path";
	my ($exit_status, $output) = $self->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to clear file on $computer_short_name: '$file_path'");
		return;
	}
	elsif ($exit_status ne 0) {
		notify($ERRORS{'WARNING'}, 0, "error occurred attempting to clear file on $computer_short_name: '$file_path', exit status: $exit_status, command: '$command', output:\n" . join("\n", @$output));
		return;
	}
	else {
		notify($ERRORS{'OK'}, 0, "cleared file on $computer_short_name: '$file_path'");
	}
	
	return 1;
}

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

=head2 create_directory

 Parameters  : $directory_path
 Returns     : boolean
 Description : Creates a directory on the Linux computer as indicated by the
               $directory_path argument.

=cut

sub create_directory {
	my $self = shift;
	if (ref($self) !~ /module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the directory path argument
	my $directory_path = shift;
	if (!$directory_path) {
		notify($ERRORS{'WARNING'}, 0, "directory path argument was not supplied");
		return;
	}
	
	# Remove any quotes from the beginning and end of the path
	$directory_path = normalize_file_path($directory_path);
	
	# If ~ is passed as the directory path, skip directory creation attempt
	# The command will create a /root/~ directory since the path is enclosed in quotes
	return 1 if $directory_path eq '~';
	
	my $computer_short_name = $self->data->get_computer_short_name();
	
	# Attempt to create the directory
	my $command = "ls -d --color=never \"$directory_path\" 2>/dev/null || (mkdir -p \"$directory_path\" 2>&1 && ls -d --color=never \"$directory_path\")";
	my ($exit_status, $output) = $self->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run command to create directory on $computer_short_name:\npath: '$directory_path'\ncommand: '$command'");
		return;
	}
	elsif (grep(/mkdir:/i, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "error occurred attempting to create directory on $computer_short_name: '$directory_path':\ncommand: '$command'\nexit status: $exit_status\noutput:\n" . join("\n", @$output));
		return;
	}
	elsif (grep(/^\s*$directory_path\s*$/, @$output)) {
		if (grep(/ls:/, @$output)) {
			notify($ERRORS{'OK'}, 0, "directory created on $computer_short_name: '$directory_path'");
		}
		else {
			#notify($ERRORS{'OK'}, 0, "directory already exists on $computer_short_name: '$directory_path'");
		}
		return 1;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unexpected output returned from command to create directory on $computer_short_name: '$directory_path':\ncommand: '$command'\nexit status: $exit_status\noutput:\n" . join("\n", @$output) . "\nlast line:\n" . string_to_ascii(@$output[-1]));
		return;
	}
}

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

=head2 move_file

 Parameters  : $source_path, $destination_path
 Returns     : boolean
 Description : Moves or renames a file on a Linux computer.

=cut

sub move_file {
	my $self = shift;
	if (ref($self) !~ /module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the path arguments
	my $source_path      = shift;
	my $destination_path = shift;
	if (!$source_path || !$destination_path) {
		notify($ERRORS{'WARNING'}, 0, "source and destination path arguments were not specified");
		return;
	}
	
	# Remove any quotes from the beginning and end of the path
	$source_path      = normalize_file_path($source_path);
	$destination_path = normalize_file_path($destination_path);
	
	# Escape all spaces in the path
	my $escaped_source_path      = escape_file_path($source_path);
	my $escaped_destination_path = escape_file_path($destination_path);
	
	my $computer_short_name = $self->data->get_computer_short_name();
	
	# Execute the command to move the file
	my $command = "mv -f $escaped_source_path $escaped_destination_path";
	my ($exit_status, $output) = $self->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run command to move file on $computer_short_name:\nsource path: '$source_path'\ndestination path: '$destination_path'\ncommand: '$command'");
		return;
	}
	elsif (grep(/^mv: /i, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to move file on $computer_short_name:\nsource path: '$source_path'\ndestination path: '$destination_path'\ncommand: '$command'\noutput:\n" . join("\n", @$output));
		return;
	}
	else {
		notify($ERRORS{'OK'}, 0, "moved file on $computer_short_name:\n'$source_path' --> '$destination_path'");
		return 1;
	}
}

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

=head2 get_available_space

 Parameters  : $path
 Returns     : If successful: integer
               If failed: undefined
 Description : Returns the bytes available in the path specified by the
               argument. 0 is returned if no space is available. Undefined is
               returned if an error occurred.

=cut

sub get_available_space {
	my $self = shift;
	if (ref($self) !~ /module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the path argument
	my $path = shift;
	if (!$path) {
		notify($ERRORS{'WARNING'}, 0, "path argument was not specified");
		return;
	}
	
	my $computer_short_name = $self->data->get_computer_short_name();
	
	# Run stat -f specifying the path as an argument
	# Don't use df because you can't specify a path under ESX and parsing would be difficult
	my $command = "stat -f \"$path\"";
	my ($exit_status, $output) = $self->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to determine available space on $computer_short_name, command: $command");
		return;
	}
	elsif (grep(/^stat: /i, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "error occurred running command to determine available space on $computer_short_name\ncommand: $command\noutput:\n" . join("\n", @$output));
		return;
	}
	
	# Create an output string from the array of lines for easier regex parsing
	my $output_string = join("\n", @$output);
	
	# Extract the block size value
	# Search case sensitive for 'Block size:' because the line may also contain "Fundamental block size:"
	# Some versions of Linux may not display a "Size:" value instead of "Block size:"
	# Blocks: Total: 8720776    Free: 8288943    Available: 7845951    Size: 4096
	my ($block_size) = $output_string =~ /(?:Block size|Size): (\d+)/;
	if (!$block_size) {
		notify($ERRORS{'WARNING'}, 0, "unable to locate 'Block size:' or 'Size:' value in stat output:\ncommand: $command\noutput:\n" . join("\n", @$output));
		return;
	}
	
	# Extract the blocks free value
	my ($blocks_available) = $output_string =~ /Blocks:[^\n]*Available: (\d+)/;
	if (!defined($blocks_available)) {
		notify($ERRORS{'WARNING'}, 0, "unable to locate blocks available value in stat output:\ncommand: $command\noutput:\n" . join("\n", @$output));
		return;
	}
	
	# Calculate the bytes available
	my $bytes_available = ($block_size * $blocks_available);
	my $mb_available    = format_number(($bytes_available / 1024 / 1024), 2);
	my $gb_available    = format_number(($bytes_available / 1024 / 1024 / 1024), 1);
	
	notify($ERRORS{'DEBUG'}, 0, "space available on volume on $computer_short_name containing '$path': " . get_file_size_info_string($bytes_available));
	return $bytes_available;
}

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

=head2 get_total_space

 Parameters  : $path
 Returns     : If successful: integer
               If failed: undefined
 Description : Returns the total size in bytes of the volume where the path
               resides specified by the argument. Undefined is returned if an
               error occurred.

=cut

sub get_total_space {
	my $self = shift;
	if (ref($self) !~ /module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the path argument
	my $path = shift;
	if (!$path) {
		notify($ERRORS{'WARNING'}, 0, "path argument was not specified");
		return;
	}
	
	my $computer_short_name = $self->data->get_computer_short_name();
	
	# Run stat -f specifying the path as an argument
	# Don't use df because you can't specify a path under ESX and parsing would be difficult
	my $command = "stat -f \"$path\"";
	my ($exit_status, $output) = $self->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to determine available space on $computer_short_name, command: $command");
		return;
	}
	elsif (grep(/^stat: /i, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "error occurred running command to determine available space on $computer_short_name\ncommand: $command\noutput:\n" . join("\n", @$output));
		return;
	}
	
	# Create an output string from the array of lines for easier regex parsing
	my $output_string = join("\n", @$output);
	
	# Extract the block size value
	# Search case sensitive for 'Block size:' because the line may also contain "Fundamental block size:"
	# Some versions of Linux may not display a "Size:" value instead of "Block size:"
	# Blocks: Total: 8720776    Free: 8288943    Available: 7845951    Size: 4096
	my ($block_size) = $output_string =~ /(?:Block size|Size): (\d+)/;
	if (!$block_size) {
		notify($ERRORS{'WARNING'}, 0, "unable to locate 'Block size:' or 'Size:' value in stat output:\ncommand: $command\noutput:\n" . join("\n", @$output));
		return;
	}
	
	# Extract the blocks total value
	my ($blocks_total) = $output_string =~ /Blocks:[^\n]*Total: (\d+)/;
	if (!defined($blocks_total)) {
		notify($ERRORS{'WARNING'}, 0, "unable to locate blocks total value in stat output:\ncommand: $command\noutput:\n" . join("\n", @$output));
		return;
	}
	
	# Calculate the bytes free
	my $bytes_total = ($block_size * $blocks_total);
	my $mb_total    = format_number(($bytes_total / 1024 / 1024), 2);
	my $gb_total    = format_number(($bytes_total / 1024 / 1024 / 1024), 1);
	
	notify($ERRORS{'DEBUG'}, 0, "total size of volume on $computer_short_name containing '$path': " . get_file_size_info_string($bytes_total));
	return $bytes_total;
}

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

=head2 copy_file_from

 Parameters  : $source_file_path, $destination_file_path
 Returns     : boolean
 Description : Copies file(s) from the Linux computer to the management node.

=cut

sub copy_file_from {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the source and destination arguments
	my ($source_file_path, $destination_file_path) = @_;
	if (!$source_file_path || !$destination_file_path) {
		notify($ERRORS{'WARNING'}, 0, "source and destination file path arguments were not specified");
		return;
	}
	
	# Get the computer name
	my $computer_node_name = $self->data->get_computer_node_name() || return;
	
	# Get the destination parent directory path and create the directory on the management node
	my $destination_directory_path = parent_directory_path($destination_file_path);
	if (!$destination_directory_path) {
		notify($ERRORS{'WARNING'}, 0, "unable to determine destination parent directory path: $destination_file_path");
		return;
	}
	create_management_node_directory($destination_directory_path) || return;
	
	# Get the identity keys used by the management node
	my $management_node_keys = $self->data->get_management_node_keys() || '';
	
	# Run the SCP command
	if (run_scp_command("$computer_node_name:\"$source_file_path\"", $destination_file_path, $management_node_keys)) {
		notify($ERRORS{'DEBUG'}, 0, "copied file from $computer_node_name to management node: $computer_node_name:'$source_file_path' --> '$destination_file_path'");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to copy file from $computer_node_name to management node: $computer_node_name:'$source_file_path' --> '$destination_file_path'");
		return;
	}
	
	return 1;
}

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

=head2 get_file_size

 Parameters  : @file_paths
 Returns     : integer or array
 Description : Determines the size of the file specified by the file path
               argument in bytes. The file path argument may be a directory or
               contain wildcards. Directories are processed recursively.
               
               When called in sclar context, the actual bytes used on the disk by the file
               is returned. This correlates to the size reported by the `du`
               command. This value is not the same as what is reported by the `ls`
               command. This is important when determining the size of
               compressed files or thinly-provisioned virtual disk images.
               
               When called in array context, 3 values are returned:
               [0] bytes used (`du` size)
               [1] bytes reserved (`ls` size)
               [2] file count

=cut

sub get_file_size {
	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;
	}
	
	my $calling_sub = (caller(1))[3] || '';
	
	# Get the path argument
	my @file_paths = @_;
	if (!@file_paths) {
		notify($ERRORS{'WARNING'}, 0, "file paths argument was not specified");
		return;
	}
	
	# Get the computer name
	my $computer_node_name = $self->data->get_computer_node_name() || return;
	
	my $file_count           = 0;
	my $total_bytes_reserved = 0;
	my $total_bytes_used     = 0;
	
	for my $file_path (@file_paths) {
		# Normalize the file path
		$file_path = normalize_file_path($file_path);
		
		# Escape all spaces in the path
		my $escaped_file_path = escape_file_path($file_path);
		
		# Run stat rather than du because du is not available on VMware ESX
		# -L     Dereference links
		# %F     File type
		# %n     File name
		# %b     Number of blocks allocated (see %B)
		# %B     The size in bytes of each block reported by %b
		# %s     Total size, in bytes
		
		my $command = 'stat -L -c "%F:%n:%s:%b:%B" ' . $escaped_file_path;
		my ($exit_status, $output) = $self->execute($command);
		if (!defined($output)) {
			notify($ERRORS{'WARNING'}, 0, "failed to run command to determine file size on $computer_node_name: $file_path\ncommand: '$command'");
			return;
		}
		elsif (grep(/no such file/i, @$output)) {
			if ($calling_sub !~ /get_file_size/) {
				notify($ERRORS{'DEBUG'}, 0, "unable to determine size of file on $computer_node_name because it does not exist: $file_path\ncommand: '$command'");
			}
			return;
		}
		elsif (grep(/^stat:/i, @$output)) {
			notify($ERRORS{'WARNING'}, 0, "error occurred attempting to determine file size on $computer_node_name: $file_path\ncommand: $command\noutput:\n" . join("\n", @$output));
			return;
		}
		
		# Loop through the stat output lines
		for my $line (@$output) {
			# Take the stat output line apart
			my ($type, $path, $file_bytes, $file_blocks, $block_size) = split(/:/, $line);
			if (!defined($type) || !defined($file_bytes) || !defined($file_blocks) || !defined($block_size) || !defined($path)) {
				notify($ERRORS{'WARNING'}, 0, "unexpected output returned from stat, line: $line\ncommand: $command\noutput:\n" . join("\n", @$output));
				return;
			}
			
			# Add the size to the total if the type is file
			if ($type =~ /file/) {
				$file_count++;
				
				my $file_bytes_allocated = ($file_blocks * $block_size);
				
				$total_bytes_used     += $file_bytes_allocated;
				$total_bytes_reserved += $file_bytes;
			}
			elsif ($type =~ /directory/) {
				$path =~ s/[\\\/\*]+$//g;
				#notify($ERRORS{'DEBUG'}, 0, "recursively retrieving size of files under directory: '$path'");
				my ($subdirectory_bytes_allocated, $subdirectory_bytes_used, $subdirectory_file_count) = $self->get_file_size("$path/*");
				
				# Values will be null if there are no files under the subdirectory
				if (!defined($subdirectory_bytes_allocated)) {
					next;
				}
				
				$file_count           += $subdirectory_file_count;
				$total_bytes_reserved += $subdirectory_bytes_used;
				$total_bytes_used     += $subdirectory_bytes_allocated;
			}
		}
	}
	
	if ($calling_sub !~ /get_file_size/) {
		notify($ERRORS{'DEBUG'}, 0, "size of " . join(", ", @file_paths) . " on $computer_node_name:\n" .
		"file count: $file_count\n" .
		"reserved: " . get_file_size_info_string($total_bytes_reserved) . "\n" .
		"used: " . get_file_size_info_string($total_bytes_used));
	}
	
	if (wantarray) {
		return ($total_bytes_used, $total_bytes_reserved, $file_count);
	}
	else {
		return $total_bytes_used;
	}
}

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

=head2 set_file_permissions

 Parameters  : $file_path, $chmod_mode, $recursive (optional)
 Returns     : boolean
 Description : Calls chmod to set the file permissions on the Linux computer.
               The $chmod_mode argument may be any valid chmod mode (+rw, 0755,
               etc). The $recursive argument is optional. The default is false.

=cut

sub set_file_permissions {
	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;
	}
	
	# Get the arguments
	my $path = shift;
	if (!defined($path)) {
		notify($ERRORS{'WARNING'}, 0, "path argument was not specified");
		return;
	}
	
	# Escape the file path in case it contains spaces
	$path = escape_file_path($path);
	
	my $chmod_mode = shift;
	if (!defined($chmod_mode)) {
		notify($ERRORS{'WARNING'}, 0, "chmod mode argument was not specified");
		return;
	}
	
	my $recursive        = shift;
	my $recursive_string = '';
	$recursive_string = "recursively " if $recursive;
	
	# Get the computer short and hostname
	my $computer_node_name = $self->data->get_computer_node_name();
	
	# Run the chmod command
	my $command = "chmod ";
	$command .= "-R " if $recursive;
	$command .= "$chmod_mode $path";
	
	my ($exit_status, $output) = $self->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run command to " . $recursive_string . "set file permissions on $computer_node_name: '$command'");
		return;
	}
	elsif (grep(/No such file or directory/i, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to " . $recursive_string . "set permissions of '$path' to '$chmod_mode' on $computer_node_name because the file does not exist, command: '$command', output:\n" . join("\n", @$output));
		return;
	}
	elsif (grep(/^chmod:/i, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "error occurred attempting to " . $recursive_string . "set permissions of '$path' to '$chmod_mode' on $computer_node_name, command: '$command'\noutput:\n" . join("\n", @$output));
		return;
	}
	else {
		notify($ERRORS{'OK'}, 0, $recursive_string . "set permissions of '$path' to '$chmod_mode' on $computer_node_name");
		return 1;
	}
}

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

=head2 set_file_owner

 Parameters  : $file_path, $owner, $group, $recursive (optional)
 Returns     : boolean
 Description : Calls chown to set the owner of a file or directory.

=cut

sub set_file_owner {
	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;
	}
	
	# Get the arguments
	my $path = shift;
	if (!defined($path)) {
		notify($ERRORS{'WARNING'}, 0, "path argument was not specified");
		return;
	}
	
	# Escape the file path in case it contains spaces
	$path = escape_file_path($path);
	
	my $owner = shift;
	if (!defined($owner)) {
		notify($ERRORS{'WARNING'}, 0, "owner argument was not specified");
		return;
	}
	
	my $group = shift;
	$owner .= ":$group" if $group;
	
	my $recursive = shift;
	$recursive = 1 if !defined($recursive);
	
	my $recursive_string = '';
	$recursive_string = "recursively " if $recursive;
	
	# Get the computer short and hostname
	my $computer_node_name = $self->data->get_computer_node_name();
	
	# Run the chown command
	my $command = "chown ";
	$command .= "-R " if $recursive;
	$command .= "$owner $path";
	
	my ($exit_status, $output) = $self->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run command to " . $recursive_string . "set file owner on $computer_node_name: '$command'");
		return;
	}
	elsif (grep(/No such file or directory/i, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to " . $recursive_string . "set owner of '$path' to '$owner' on $computer_node_name because the file does not exist, command: '$command', output:\n" . join("\n", @$output));
		return;
	}
	elsif (grep(/^chown:/i, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "error occurred attempting to " . $recursive_string . "set owner of '$path' to '$owner' on $computer_node_name, command: '$command'\noutput:\n" . join("\n", @$output));
		return;
	}
	else {
		notify($ERRORS{'OK'}, 0, $recursive_string . "set owner of '$path' to '$owner' on $computer_node_name");
		return 1;
	}
}

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

=head2 activate_interfaces

 Parameters  : none
 Returns     : true
 Description : Finds all networking interfaces with an active link. Checks if an
               ifcfg-eth* file exists for the interface. An ifcfg-eth* file is
               generated if it does not exist using DHCP and the interface is
               brought up. This is useful if additional interfaces are added by
               the provisioning module when an image is loaded.

=cut

sub activate_interfaces {
	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;
	}
	
	# Run 'ip link' to find all interfaces with links
	my $command = "ip link";
	notify($ERRORS{'DEBUG'}, 0, "attempting to find network interfaces with an active link");
	my ($exit_status, $output) = $self->execute($command, 1);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run command to find network interfaces with an active link:\n$command");
		return;
	}
	
	# Extract the interface names from the 'ip link' output
	my @interface_names = grep {/^\d+:\s+(eth\d+)/; $_ = $1} @$output;
	notify($ERRORS{'DEBUG'}, 0, "found interface names:\n" . join("\n", @interface_names));
	
	# Find existing ifcfg-eth* files
	my $ifcfg_directory = '/etc/sysconfig/network-scripts';
	my @ifcfg_paths = $self->find_files($ifcfg_directory, 'ifcfg-eth*');
	notify($ERRORS{'DEBUG'}, 0, "found existing ifcfg-eth* files:\n" . join("\n", @ifcfg_paths));
	
	# Loop through the linked interfaces
	for my $interface_name (@interface_names) {
		my $ifcfg_path = "$ifcfg_directory/ifcfg-$interface_name";
		
		# Check if an ifcfg-eth* file already exists for the interface
		if (grep(/$ifcfg_path/, @ifcfg_paths)) {
			notify($ERRORS{'DEBUG'}, 0, "ifcfg file already exists for $interface_name");
			next;
		}
		
		notify($ERRORS{'DEBUG'}, 0, "ifcfg file does not exist for $interface_name");
		
		# Assemble the contents of the ifcfg-eth* file for the interface
		my $ifcfg_contents = <<EOF;
DEVICE=$interface_name
BOOTPROTO=dhcp
STARTMODE=onboot
ONBOOT=yes
EOF
		
		# Create the ifcfg file
		if (!$self->create_text_file($ifcfg_path, $ifcfg_contents)) {
			notify($ERRORS{'WARNING'}, 0, "failed to create $ifcfg_path for interface: $interface_name");
			return;
		}
		
		$self->start_network_interface($interface_name);
	}
	
	return 1;
}

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

=head2 get_network_configuration

 Parameters  : $no_cache (optional)
 Returns     : hash reference
 Description : Retrieves the network configuration on the Linux computer and
               constructs a hash. The hash reference returned is formatted as
               follows:
               {
                 "eth0" => {
                   "broadcast_address" => "10.25.15.255",
                   "ip_address" => {
                     "10.25.10.194" => "255.255.240.0"
                   },
                   "name" => "eth0",
                   "physical_address" => "00:50:56:23:00:bc"
                 },
                 "eth1" => {
                   "name" => "eth1",
                   "physical_address" => "00:50:56:23:00:bd"
                 },
                 "lo" => {
                   "name" => "lo"
                 },
                 "xbr1" => {
                   "bridge" => {
                     "bridge_id" => "8000.0050562300bd",
                     "interfaces" => [
                       "eth1"
                     ],
                     "stp_enabled" => "8000.0050562300bd"
                   },
                   "broadcast_address" => "192.168.53.255",
                   "default_gateway" => "192.168.53.254",
                   "ip_address" => {
                     "152.46.18.135" => "255.255.248.0"
                   },
                   "name" => "xbr1",
                   "physical_address" => "00:50:56:23:00:bd"
                 }
               }

=cut

sub get_network_configuration {
	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;
	}
	
	my $no_cache = shift || 0;
	notify($ERRORS{'DEBUG'}, 0, "attempting to retrieve network configuration, no cache: $no_cache");
	
	# Delete previously retrieved data if $no_cache was specified
	if ($no_cache) {
		delete $self->{network_configuration};
	}
	elsif ($self->{network_configuration}) {
		return $self->{network_configuration}
	}
	
	# Run ipconfig
	my $ifconfig_command = "/sbin/ifconfig -a";
	my ($ifconfig_exit_status, $ifconfig_output) = $self->execute($ifconfig_command);
	if (!defined($ifconfig_output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run command to retrieve network configuration: $ifconfig_command");
		return;
	}
	#notify($ERRORS{'DEBUG'}, 0, "ifconfig output:\n" . join("\n", @$ifconfig_output));
	
	# Loop through the ifconfig output lines
	my $network_configuration;
	my $interface_name;
	for my $ifconfig_line (@$ifconfig_output) {
		# Extract the interface name from the Link line:
		# eth2      Link encap:Ethernet  HWaddr 00:0C:29:78:77:AB
		#eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
		#if ($ifconfig_line =~ /^([^\s]+).*Link/) {
		if ($ifconfig_line =~ /^([^\s:]+).*(Link|flags)/) {
			$interface_name = $1;
			$network_configuration->{$interface_name}{name} = $interface_name;
		}
		
		# Skip to the next line if the interface name has not been determined yet
		next if !$interface_name;
		
		# Parse the HWaddr line:
		# eth2      Link encap:Ethernet  HWaddr 00:0C:29:78:77:AB
		#if ($ifconfig_line =~ /HWaddr\s+([\w:]+)/) {
		if ($ifconfig_line =~ /(ether|HWaddr)\s+([\w:]+)/) {
			$network_configuration->{$interface_name}{physical_address} = lc($2);
		}
		
		# Parse the IP address line:
		# inet addr:10.10.4.35  Bcast:10.10.15.255  Mask:255.255.240.0
		if ($ifconfig_line =~ /inet addr:([\d\.]+)\s+Bcast:([\d\.]+)\s+Mask:([\d\.]+)/) {
			$network_configuration->{$interface_name}{ip_address}{$1} = $3;
			$network_configuration->{$interface_name}{broadcast_address} = $2;
		}
      
		# inet 10.25.14.3  netmask 255.255.240.0  broadcast 10.25.15.255
      if ($ifconfig_line =~ /inet\s+([\d\.]+)\s+netmask\s+([\d\.]+)\s+broadcast\s+([\d\.]+)/) {
			$network_configuration->{$interface_name}{ip_address}{$1} = $2;
			$network_configuration->{$interface_name}{broadcast_address} = $3;
		}
	}
	
	
	# Run route
	my $route_command = "/sbin/route -n";
	my ($route_exit_status, $route_output) = $self->execute($route_command);
	if (!defined($route_output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run command to retrieve routing configuration: $route_command");
		return;
	}
	
	# Loop through the route output lines
	for my $route_line (@$route_output) {
		my ($default_gateway, $interface_name) = $route_line =~ /^0\.0\.0\.0\s+([\d\.]+).*\s([^\s]+)$/g;
		
		if (!defined($interface_name) || !defined($default_gateway)) {
			#notify($ERRORS{'DEBUG'}, 0, "route output line does not contain a default gateway: '$route_line'");
		}
		elsif (!defined($network_configuration->{$interface_name})) {
			notify($ERRORS{'WARNING'}, 0, "found default gateway for '$interface_name' interface but the network configuration for '$interface_name' was not previously retrieved, route output:\n" . join("\n", @$route_output) . "\nnetwork configuation:\n" . format_data($network_configuration));
		}
		elsif (defined($network_configuration->{$interface_name}{default_gateway}) && $default_gateway ne $network_configuration->{$interface_name}{default_gateway}) {
			notify($ERRORS{'WARNING'}, 0, "multiple default gateways are configured for '$interface_name' interface, route output:\n" . join("\n", @$route_output));
		}
		else {
			$network_configuration->{$interface_name}{default_gateway} = $default_gateway;
			notify($ERRORS{'DEBUG'}, 0, "found default route configured for '$interface_name' interface: $default_gateway");
		}
	}
	
	# Check if bridge is configured
	my $network_bridge_info = $self->get_network_bridge_info();
	for my $bridge_name (keys %$network_bridge_info) {
		# Add bridge info under 'bridge' key for the bridge
		if (defined($network_configuration->{$bridge_name})) {
			$network_configuration->{$bridge_name}{bridge} = $network_bridge_info->{$bridge_name};
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "'$bridge_name' bridge was not found in 'ifconfig' output:" .
				"ifconfig output:\n" . join("\n", @$ifconfig_output) . "\n" .
				"network bridge info:\n" . format_data($network_bridge_info)
			);
		}
		
		# Add name of bridge to 'master' key for the physical interface
		for my $bridge_interface_name (@{$network_bridge_info->{$bridge_name}{interfaces}}) {
			if (defined($network_configuration->{$bridge_interface_name})) {
				$network_configuration->{$bridge_interface_name}{master} = $bridge_name;
			}
			else {
				notify($ERRORS{'WARNING'}, 0, "'$bridge_name' bridge contains '$bridge_interface_name' interface but '$bridge_interface_name' was not found in 'ifconfig' output:\n" .
					"ifconfig output:\n" . join("\n", @$ifconfig_output) . "\n" .
					"network bridge info:\n" . format_data($network_bridge_info)
				);
			}
		}
	}
	
	$self->{network_configuration} = $network_configuration;
	#can produce large output, if you need to monitor the configuration setting uncomment the below output statement
	notify($ERRORS{'DEBUG'}, 0, "retrieved network configuration:\n" . format_data($self->{network_configuration}));
	return $self->{network_configuration};
}

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

=head2 reboot

 Parameters  : none
 Returns     : boolean
 Description : Attempts to gracefully reboot the computer by executing
               'shutdown -r now' command. Attempts to detect reboot began and
               completed. If this fails or if the computer is not responding to
               SSH, the provisioning module will attempt to forcefully perform a
               hard reset of the computer.

=cut

sub reboot {
	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();
	
	notify($ERRORS{'DEBUG'}, 0, "rebooting $computer_node_name and waiting for SSH to become active");
	
	my $reboot_start_time = time();
	
	# Check if computer responds to ssh before preparing for reboot
	if ($self->wait_for_ssh(0)) {
		my $reboot_command = '/sbin/shutdown -r now &';
		notify($ERRORS{'DEBUG'}, 0, "attempting to gracefully reboot $computer_node_name by executing '$reboot_command'");
		my ($reboot_exit_status, $reboot_output) = $self->execute(
			{
				command => $reboot_command,
				timeout => 30,
				max_attempts => 1,
				display_output => 0,
			}
		);
		
		if ($self->wait_for_reboot()) {
			my $reboot_duration = (time() - $reboot_start_time);
			notify($ERRORS{'OK'}, 0, "gracefully rebooted $computer_node_name, took $reboot_duration seconds");
			return 1;
		}
		else {
			notify($ERRORS{'DEBUG'}, 0, "did not detect $computer_node_name rebooting after executing '$reboot_command', attempting hard reset using the provisioning module");
		}
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "$computer_node_name is not responding to SSH, graceful reboot cannot be performed, attempting hard reset using the provisioning module");
	}
	
	$self->provisioner->power_reset() || return;
	if ($self->wait_for_reboot()) {
		my $reboot_duration = (time() - $reboot_start_time);
		notify($ERRORS{'OK'}, 0, "hard reset of $computer_node_name complete, took $reboot_duration seconds");
		return 1;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "$computer_node_name may not have rebooted, did not detect reboot after attempting hard reset using the provisioning module");
		return;
	}
}

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

=head2 shutdown

 Parameters  : none
 Returns     : boolean
 Description : Attempts to gracefully shut down the computer by executing the
               shutdown command. Waits for provisioning module to report that
               the computer is off. If this fails or if the computer is not
               responding to SSH, the provisioning module will attempt to
               forcefully power off the computer.

=cut

sub shutdown {
	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();
	
	# Check if computer responds to ssh before preparing for shut down
	if ($self->wait_for_ssh(0)) {
		my $shutdown_command = '/sbin/shutdown -h now &';
		notify($ERRORS{'DEBUG'}, 0, "attempting to gracefully shut down $computer_node_name by executing '$shutdown_command'");
		my ($exit_status, $output) = $self->execute(
			{
				command => $shutdown_command,
				timeout => 30,
				max_attempts => 1,
				display_output => 0,
			}
		);
		
		if ($self->provisioner->wait_for_power_off(300, 10)) {
			notify($ERRORS{'OK'}, 0, "gracefully shut down $computer_node_name by executing the OS's shutdown command");
			return 1;
		}
		else {
			notify($ERRORS{'DEBUG'}, 0, "$computer_node_name is still on after executing shutdown command, attempting to power off the computer using the provisioning module");
		}
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "$computer_node_name is NOT responding to SSH, attempting to power off the computer using the provisioning module");
	}
	
	$self->provisioner->power_off() || return;
	if ($self->provisioner->wait_for_power_off(300, 10)) {
		notify($ERRORS{'OK'}, 0, "forcefully powered off $computer_node_name using the provisioning module");
		return 1;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to shut down $computer_node_name, computer is still on after attempting to power off the computer using the provisioning module");
		return;
	}
}

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

=head2 hibernate

 Parameters  : none
 Returns     : boolean
 Description : Hibernates the computer.

=cut

sub hibernate {
	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 = 'echo disk > /sys/power/state &';
	my ($exit_status, $output) = $self->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to hibernate $computer_node_name");
		return;
	}
	elsif ($exit_status eq 0) {
		notify($ERRORS{'OK'}, 0, "executed command to hibernate $computer_node_name: $command" . (scalar(@$output) ? "\noutput:\n" . join("\n", @$output) : ''));
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to hibernate $computer_node_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
		return;
	}
	
	# Wait for computer to power off
	my $power_off = $self->provisioner->wait_for_power_off(300, 5);
	if (!defined($power_off)) {
		# wait_for_power_off result will be undefined if the provisioning module doesn't implement a power_status subroutine
		notify($ERRORS{'OK'}, 0, "unable to determine power status of $computer_node_name from provisioning module, sleeping 1 minute to allow computer time to hibernate");
		sleep 60;
		return 1;
	}
	elsif (!$power_off) {
		notify($ERRORS{'WARNING'}, 0, "$computer_node_name never powered off after executing hibernate command: $command");
		return;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "$computer_node_name powered off after executing hibernate command");
		return 1;
	}
}

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

=head2 create_user

 Parameters  : $argument_hash_ref
 Returns     : boolean
 Description : Creates a user on the computer. The argument hash reference
               should be constructed as follows:
					{
						username => $username,
						password => $password, (optional)
						root_access => $root_access,
						uid => $uid, (optional)
						ssh_public_keys => $ssh_public_keys, (optional)
					});

=cut

sub create_user {
	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 $user_parameters = shift;
	if (!$user_parameters) {
		notify($ERRORS{'WARNING'}, 0, "unable to create user, user parameters argument was not provided");
		return;
	}
	elsif (!ref($user_parameters) || ref($user_parameters) ne 'HASH') {
		notify($ERRORS{'WARNING'}, 0, "unable to create user, argument provided is not a hash reference");
		return;
	}
	
	my $username = $user_parameters->{username};
	if (!defined($username)) {
		notify($ERRORS{'WARNING'}, 0, "failed to create user on $computer_node_name, argument hash does not contain a 'username' key:\n" . format_data($user_parameters));
		return;
	}
	
	my $root_access = $user_parameters->{root_access};
	if (!defined($root_access)) {
		notify($ERRORS{'WARNING'}, 0, "failed to create user on $computer_node_name, argument hash does not contain a 'root_access' key:\n" . format_data($user_parameters));
		return;
	}
	
	my $password = $user_parameters->{password};
	my $uid = $user_parameters->{uid};
	my $ssh_public_keys = $user_parameters->{ssh_public_keys};
	
	# If user account does not already exist - create it, then
	# -- Set password if using local authentication
	# -- update sudoers file if root access allowed
	# -- process connect_methods_access

	if (!$self->user_exists($username)) {
	
		notify($ERRORS{'DEBUG'}, 0, "creating user on $computer_node_name:\n" .
			"username: $username\n" .
			"password: " . (defined($password) ? $password : '<not set>') . "\n" .
			"UID: " . ($uid ? $uid : '<not set>') . "\n" .
			"root access: " . ($root_access ? 'yes' : 'no') . "\n" .
			"SSH public keys: " . (defined($ssh_public_keys) ? $ssh_public_keys : '<not set>')
		);
		
		my $home_directory_root = "/home";
		my $home_directory_path = "$home_directory_root/$username";
		my $home_directory_on_local_disk = $self->is_file_on_local_disk($home_directory_root);
		if ($home_directory_on_local_disk) {
			my $useradd_command = "/usr/sbin/useradd -s /bin/bash -m -d /home/$username -g vcl";
			$useradd_command .= " -u $uid" if ($uid);
			$useradd_command .= " $username";
			
			my ($useradd_exit_status, $useradd_output) = $self->execute($useradd_command);
			if (!defined($useradd_output)) {
				notify($ERRORS{'WARNING'}, 0, "failed to execute command to add user '$username' to $computer_node_name: '$useradd_command'");
				return;
			}
			elsif (grep(/^useradd: /, @$useradd_output)) {
				notify($ERRORS{'WARNING'}, 0, "warning detected on add user '$username' to $computer_node_name\ncommand: '$useradd_command'\noutput:\n" . join("\n", @$useradd_output));
			}
			else {
				notify($ERRORS{'OK'}, 0, "added user '$username' to $computer_node_name, output:" . (scalar(@$useradd_output) ? "\n" . join("\n", @$useradd_output) : ' <none>'));
			}
		}
		else {
			notify($ERRORS{'OK'}, 0, "$home_directory_path is NOT on local disk, skipping useradd attempt");	
		}
	}
	
	# Set the password
	if ($password) {
		# Set password
		if (!$self->set_password($username, $password)) {
			notify($ERRORS{'CRITICAL'}, 0, "failed to set password of user '$username' on $computer_node_name");
			return;
		}
	}

	# Process connect_methods
	if ($self->can("grant_connect_method_access")) {
		if (!$self->grant_connect_method_access({
			username => $username,
			uid => $uid,
			ssh_public_keys => $ssh_public_keys,
			})) {
			notify($ERRORS{'WARNING'}, 0, "failed to process grant_connect_method_access for $username");
		}
	}
	
	# Add user to sudoers if necessary
	if ($root_access) {
		if (!$self->grant_administrative_access($username)) {
			notify($ERRORS{'WARNING'}, 0, "failed to process grant_administrative_access for $username");
			return;
		}
	}
	else {
		# Make sure user does not have root access
		$self->revoke_administrative_access($username);
	}
	
	return 1;
} ## end sub create_user

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

=head2 grant_administrative_access

 Parameters  : $username
 Returns     : boolean
 Description : Adds the user to the sudoers file.

=cut

sub grant_administrative_access {
	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 $username = shift;
	if (!defined($username)) {
		notify($ERRORS{'WARNING'}, 0, "username argument was not supplied");
		return;
	}
	
	my $timestamp = makedatestring();
	
	my $sudoers_file_path = '/etc/sudoers';
	
	my @existing_lines = $self->get_file_contents($sudoers_file_path);
	my @matching_lines;
	for my $line (@existing_lines) {
		if ($line =~ /^\s*$username\s/) {
			push @matching_lines, $line;
		}
	}
	if (@matching_lines) {
		notify($ERRORS{'DEBUG'}, 0, "$username was previously added to $sudoers_file_path:\n" . join("\n", @matching_lines));
		return 1;
	}
	
	my $sudoers_line = "$username ALL= NOPASSWD: ALL\t# Added by VCL, ($timestamp)";
	if ($self->append_text_file($sudoers_file_path, $sudoers_line)) {
		notify($ERRORS{'DEBUG'}, 0, "appended line to $sudoers_file_path: '$sudoers_line'");
		return 1;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to append line to $sudoers_file_path: '$sudoers_line'");
		return 0;
	}
}

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

=head2 revoke_administrative_access

 Parameters  : $username
 Returns     : boolean
 Description : Removes all entries from the sudoers file for the user.

=cut

sub revoke_administrative_access {
	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 $username = shift;
	if (!defined($username)) {
		notify($ERRORS{'WARNING'}, 0, "username argument was not supplied");
		return;
	}
	
	my $sudoers_file_path = '/etc/sudoers';

	# Remove lines from sudoers
	if (defined($self->remove_lines_from_file($sudoers_file_path, "^[\\s#]*$username\\s"))) {
		return 1;
	}
	else {
		return;
	}
}

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

=head2 delete_user

 Parameters  : $username
 Returns     :
 Description :

=cut

sub delete_user {
	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;
	}
	
	# Make sure the user login ID was passed
	my $username = shift;
	$username = $self->data->get_user_login_id() if (!$username);
	if (!$username) {
		notify($ERRORS{'WARNING'}, 0, "user could not be determined");
		return;
	}
	
	my $computer_node_name = $self->data->get_computer_node_name();
	
	# Make sure the user exists
	if (!$self->user_exists($username)) {
		notify($ERRORS{'DEBUG'}, 0, "user NOT deleted from $computer_node_name because it does not exist: $username");
		
		# Make sure user does not exist in sudoers
		$self->revoke_administrative_access($username);
		
		return 1;
	}
	
	# Check if the user is logged in
	if ($self->user_logged_in($username)) {
		if (!$self->logoff_user($username)) {
			notify($ERRORS{'WARNING'}, 0, "failed to delete user $username from $computer_node_name, user appears to be logged in but could NOT be logged off");
			return;
		}
	}
	
	# Determine if home directory is on a local device or network share
	my $home_directory_path = "/home/$username";
	my $home_directory_on_local_disk = $self->is_file_on_local_disk($home_directory_path);
	
	# Assemble the userdel command
	my $userdel_command = "/usr/sbin/userdel";
	
	my $delete_home_directory = 0;
	
	if ($home_directory_on_local_disk) {
		$delete_home_directory = 1;
		
		# Fetch exclude_list
		my @exclude_list = $self->get_exclude_list();
		if ((grep(/\/home\/$username/, @exclude_list))) {
			notify($ERRORS{'DEBUG'}, 0, "home directory will NOT be deleted: $home_directory_path");
			$delete_home_directory = 0;
		}
		else {
			# Make sure no NFS shares are mounted under home directory
			my @nfs_mount_strings = $self->get_nfs_mount_strings();
			for my $nfs_mount_string (@nfs_mount_strings) {
				my ($nfs_remote_host, $nfs_remote_path, $nfs_local_path) = $nfs_mount_string =~
					/
						^
						([^:]+)		# Remote hostname or IP address
						:
						(\/.+)		# Remote path
						\s+
						(\/.+)		# Local path
						\s+
						nfs\d*		# ' nfs ' or ' nfs4 '
						\s+
					/gx;
				
				if ($nfs_local_path) {
					if ($nfs_local_path =~ /^$home_directory_path/) {
						notify($ERRORS{'WARNING'}, 0, "home directory will NOT be deleted, NFS share is mounted under it\n" .
							"NFS mount string    : $nfs_mount_string\n" .
							"home directory path : $home_directory_path\n" .
							"local mount path    : $nfs_local_path"
						);
						$delete_home_directory = 0;
						last;
					}
					else {
						notify($ERRORS{'DEBUG'}, 0, "NFS share is NOT mounted under home directory\n" .
							"NFS mount string    : $nfs_mount_string\n" .
							"home directory path : $home_directory_path\n" .
							"local mount path    : $nfs_local_path"
						);
					}
				}
				else {
					notify($ERRORS{'WARNING'}, 0, "home directory will NOT be deleted: $home_directory_path, failed to parse NFS mount string: $nfs_mount_string");
					$delete_home_directory = 0;
					last;
				}
			}
		}
	}
	
	if ($delete_home_directory) {
		notify($ERRORS{'DEBUG'}, 0, "home directory will be deleted: $home_directory_path");
		$userdel_command .= ' -r';
	}
	$userdel_command .= " $username";
	
	# Call userdel to delete the user
	my ($userdel_exit_status, $userdel_output) = $self->execute($userdel_command);
	if (!defined($userdel_output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to delete user from $computer_node_name: $username");
		return;
	}
	elsif (grep(/does not exist/i, @$userdel_output)) {
		notify($ERRORS{'DEBUG'}, 0, "user '$username' NOT deleted from $computer_node_name because it does not exist");
	}
	elsif (grep(/not found/i, @$userdel_output)) {
		notify($ERRORS{'DEBUG'}, 0, "userdel warning '$username' $computer_node_name :\n" . join("\n", @$userdel_output));
	}
	elsif (grep(/userdel: /i, @$userdel_output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to delete user '$username' from $computer_node_name, command: '$userdel_command', exit status: $userdel_exit_status, output:\n" . join("\n", @$userdel_output));
		return;
	}
	else {
		notify($ERRORS{'OK'}, 0, "deleted user '$username' from $computer_node_name");
	}
	
	# Call groupdel to delete the user's group
	my $groupdel_command = "/usr/sbin/groupdel $username";
	my ($groupdel_exit_status, $groupdel_output) = $self->execute($groupdel_command);
	if (!defined($groupdel_output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to delete group from $computer_node_name: $username");
		return;
	}
	elsif (grep(/does not exist/i, @$groupdel_output)) {
		notify($ERRORS{'DEBUG'}, 0, "group '$username' NOT deleted from $computer_node_name because it does not exist");
	}
	elsif (grep(/groupdel: /i, @$groupdel_output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to delete group '$username' from $computer_node_name, command: '$groupdel_command', output:\n" . join("\n", @$groupdel_output));
	}
	else {
		notify($ERRORS{'OK'}, 0, "deleted group '$username' from $computer_node_name");
	}
	
	# Remove username from AllowUsers lines in ssh/external_sshd_config
	my $external_sshd_config_file_path = '/etc/ssh/external_sshd_config';
	my @original_lines = $self->get_file_contents($external_sshd_config_file_path);
	my @modified_lines;
	my $new_file_contents;
	for my $line (@original_lines) {
		if ($line =~ /AllowUsers.*\s$username(\s|$)/) {
			push @modified_lines, $line;
			$line =~ s/\s*$username//g;
			# If user was only username listed on line, don't add empty AllowUsers line back to file
			if ($line !~ /AllowUsers\s+\w/) {
				next;
			}
		}
		$new_file_contents .= "$line\n";
	}
	if (@modified_lines) {
		notify($ERRORS{'OK'}, 0, "removing or modifying AllowUsers lines in $external_sshd_config_file_path:\n" . join("\n", @modified_lines));
		$self->create_text_file($external_sshd_config_file_path, $new_file_contents) || return;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "no AllowUsers lines were found in $external_sshd_config_file_path containing '$username'");
	}
	
	# Remove lines from sudoers
	$self->revoke_administrative_access($username);
	
	return 1;
} ## end sub delete_user

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

=head2 is_file_on_local_disk

 Parameters  : $file_path
 Returns     : boolean
 Description : Determines if the file or directory is located on a local disk or
               network share.

=cut

sub is_file_on_local_disk {
	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 $file_path = shift;
	if (!$file_path) {
		notify($ERRORS{'WARNING'}, 0, "file path argument was not specified");
		return;
	}
	
	my $computer_name = $self->data->get_computer_short_name();
	
	# Run df to determine if file is on a local device or network share
	my $df_command = "df -T -P $file_path";
	my ($df_exit_status, $df_output) = $self->execute($df_command);
	if (!defined($df_output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to determine if file is on a local disk");
		return;
	}
	elsif (grep(/(no such file|no file system)/i, @$df_output)) {
		notify($ERRORS{'DEBUG'}, 0, "file does NOT exist on $computer_name: $file_path");
		return;
	}
	elsif (grep(m|/dev/|i, @$df_output) && !grep(/ (nfs|afs) /i, @$df_output)) {
		notify($ERRORS{'DEBUG'}, 0, "file is on a local disk: $file_path, output:\n" . join("\n", @$df_output));
		return 1;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "file is NOT on a local disk: $file_path, output:\n" . join("\n", @$df_output));
		return 0;
	}
}

#//////////////////////////////////////////////////////////////////////////////
=head2 enable_dhcp

 Parameters  : $interface_name
 Returns     : boolean
 Description : Configures the ifcfg-* file for the specified interface to use
               DHCP.

=cut

sub enable_dhcp {
	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;
	}
	
	my $computer_name = $self->data->get_computer_node_name();
	
	my $interface_name = shift;
	if (!$interface_name) {
		notify($ERRORS{'WARNING'}, 0, "interface name argument was not supplied");
		return;
	}
	
	# Delete existing static route file for the interface if one exists
	$self->delete_file("/etc/sysconfig/network-scripts/route-$interface_name");
	
	my $ifcfg_file_info = $self->get_ifcfg_file_info($interface_name) || {};
	
	my $calling_subroutine = get_calling_subroutine();
	if ($calling_subroutine !~ /enable_dhcp/) {
		# Check if interface is configured as a bridge
		my @bridge_interface_names;
		if ($ifcfg_file_info->{bridge}) {
			# ifcfg file contains something like: BRIDGE=br1
			push @bridge_interface_names, $ifcfg_file_info->{bridge};
		}
		elsif ($ifcfg_file_info->{type} && $ifcfg_file_info->{type} =~ /Bridge/i) {
			# ifcfg file contains something like: TYPE=Bridge
			# For ifcfg-br* files, the name of the physical interface usually isn't listed in the file
			# Get the network bridge info
			my $network_bridge_info = $self->get_network_bridge_info();
			if (defined($network_bridge_info) && defined($network_bridge_info->{$interface_name})) {
				@bridge_interface_names = @{$network_bridge_info->{$interface_name}{interfaces}};
			}
		}
		for my $bridge_interface_name (@bridge_interface_names) {
			# Make sure the bridge isn't the same name as the interface being checked to avoid recurive loop
			next if ($bridge_interface_name eq $interface_name);
			
			notify($ERRORS{'DEBUG'}, 0, "$interface_name is bridged, attempting to enable DHCP on bridge interface: $bridge_interface_name");
			$self->enable_dhcp($bridge_interface_name) || return;
		}
	}
	
	return $self->generate_ifcfg_file($interface_name, { 'bootproto' => 'dhcp' });
}

#//////////////////////////////////////////////////////////////////////////////
=head2 generate_ifcfg_file

 Parameters  : $interface_name, $parameters
 Returns     : boolean
 Description : Creates an interface configuration file in
               /etc/sysconfig/network-scripts. The parameters argument contains
               key value pairs and must contain a 'bootproto' key. The key names
               must be completely lowercase for consistency. The resulting file
               will contain uppercase parameter names.

=cut

sub generate_ifcfg_file {
	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;
	}
	
	my ($interface_name, $parameters_argument) = @_;
	if (!$interface_name) {
		notify($ERRORS{'WARNING'}, 0, "interface name argument was not supplied");
		return;
	}
	elsif (!$parameters_argument) {
		notify($ERRORS{'WARNING'}, 0, "parameters argument was not supplied");
		return;
	}
	elsif (!ref($parameters_argument) || ref($parameters_argument) ne 'HASH') {
		notify($ERRORS{'WARNING'}, 0, "parameters argument is not a hash reference:\n" . format_data($parameters_argument));
		return;
	}
	elsif (!$parameters_argument->{bootproto}) {
		notify($ERRORS{'WARNING'}, 0, "parameters argument must contain a 'bootproto' key:\n" . format_data($parameters_argument));
		return;
	}
	
	my $computer_name = $self->data->get_computer_node_name();
	
	my $ifcfg_directory_path = "/etc/sysconfig/network-scripts";
	my $ifcfg_file_name = "ifcfg-$interface_name";
	my $ifcfg_file_path = "$ifcfg_directory_path/$ifcfg_file_name";
	
	if ($self->file_exists($ifcfg_file_path)) {
		my $timestamp = POSIX::strftime("%Y-%m-%d_%H-%M-%S\n", localtime);
		my $ifcfg_backup_file_path = "/tmp/$ifcfg_file_name.$timestamp";
		$self->copy_file($ifcfg_file_path, $ifcfg_backup_file_path);
	}
	
	my $ifcfg_file_info = $self->get_ifcfg_file_info($interface_name) || {};
	
	# Remove parameters which are specific to a particular network or computer
	my @remove_parameter_patterns = (
		'addr',
		'broadcast',
		'dns',
		'gateway',
		'hostname',
		'metric',
		'netmask',
		'network',
		'prefix',
		'uuid',
	);
	for my $remove_pattern (@remove_parameter_patterns) {
		my @matching_properties = grep { $_ =~ /.*$remove_pattern.*/ } sort keys %$ifcfg_file_info;
		if (@matching_properties) {
			notify($ERRORS{'DEBUG'}, 0, "removing parameters from ifcfg-$interface_name file matching pattern '$remove_pattern': " . join(', ', @matching_properties));
			map { delete $ifcfg_file_info->{$_} } @matching_properties;
		}
	}
	
	# Add/overwrite required parameters to file contents
	my $common_parameters = {
		'device' => $interface_name,
		'name' => $interface_name,
		'onboot' => 'yes',
	};
	for my $parameter (keys %$common_parameters) {
		my $value = $common_parameters->{$parameter};
		$ifcfg_file_info->{$parameter} = $value;
	}
	
	# Add/overwrite parameters specified by argument to file contents
	for my $parameter (keys %$parameters_argument) {
		my $value = $parameters_argument->{$parameter};
		$ifcfg_file_info->{$parameter} = $value;
	}
	
	# Convert the parameter/value hash to a string
	my $updated_ifcfg_contents;
	for my $parameter (sort keys %$ifcfg_file_info) {
		my $value = $ifcfg_file_info->{$parameter};
		$updated_ifcfg_contents .= uc($parameter) . "=$value\n";
	}
	
	# Create the text file
	notify($ERRORS{'DEBUG'}, 0, "attempting to generate file on $computer_name: $ifcfg_file_path, contents:\n$updated_ifcfg_contents");
	return $self->create_text_file($ifcfg_file_path, $updated_ifcfg_contents);
}

#//////////////////////////////////////////////////////////////////////////////
=head2 get_ifcfg_file_info

 Parameters  : $interface_name
 Returns     : hash reference
 Description : Parses the file:
               /etc/sysconfig/network-scripts/ifcfg-<interface name>
               
               A hash is constructed such as:
               {
                 "bootproto" => "dhcp",
                 "device" => "eth0",
                 "onboot" => "yes"
               }
               
               The hash key names are guaranteed to be lowercase.

=cut

sub get_ifcfg_file_info {
	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;
	}
	
	my $interface_name = shift;
	if (!$interface_name) {
		notify($ERRORS{'WARNING'}, 0, "interface name argument was not supplied");
		return;
	}
	
	my $ifcfg_file_path = "/etc/sysconfig/network-scripts/ifcfg-$interface_name";
	
	my $info = {};
	my @lines = $self->get_file_contents($ifcfg_file_path);
	for my $line (@lines) {
		next if $line =~ /^\s*#/;
		my ($property, $value) = $line =~ /^\s*([^=]+)\s*=\s*(.*)\s*$/g;
		if (defined($property)) {
			$info->{lc($property)} = $value;
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to parse line from $ifcfg_file_path: '$line'");
		}
	}
	
	notify($ERRORS{'DEBUG'}, 0, "parsed $ifcfg_file_path:\n" . format_data($info));
	return $info;
}

#//////////////////////////////////////////////////////////////////////////////
=head2 get_network_bridge_info

 Parameters  : none
 Returns     : hash reference
 Description : Executes 'brctl show' and parses the output. A hash is
               constructed:
               {
                  "br0" => {
                    "bridge_id" => "8000.00505623001c",
                    "bridge_name" => "br0",
                    "interfaces" => [
                      "eth0",
                    ],
                    "stp_enabled" => "no"
                  },
                  "xbr1" => {
                    "bridge_id" => "8000.00505623001d",
                    "bridge_name" => "xbr1",
                    "interfaces" => [
                      "eth1",
                      "vnet1"
                    ],
                    "stp_enabled" => "no"
                  }
               }

=cut

sub get_network_bridge_info {
	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;
	}
	
	# This only gets cached if the brctl command does not exist
	if (defined($self->{network_bridge_info})) {
		return $self->{network_bridge_info};
	}
	
	my $computer_name = $self->data->get_computer_short_name();
	
	# It's possible that a bridge will have multiple interfaces:
	# [root@bn19-183 network-scripts]# brctl show
	# bridge name     bridge id               STP enabled     interfaces
	# br1             8000.000c29494c97       no              eth1
	#                                                         eth2
	
	# It's possible to have no interfaces listed:
	# bridge name     bridge id               STP enabled     interfaces
	# xbr1            8000.000000000000       no
	
	my $command = "brctl show";
	my ($exit_status, $output) = $self->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command on $computer_name: $command");
		return;
	}
	elsif ($exit_status == 127 || grep(/command not found/i, @$output)) {
		notify($ERRORS{'DEBUG'}, 0, "network bridge configuration does not exist on $computer_name, brctl is not installed");
		# Cache an empty hash reference so this command isn't needlessly run multiple times
		$self->{network_bridge_info} = {};
		return $self->{network_bridge_info};
	}
	elsif ($exit_status > 0) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve network bridge configuration from $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
		return;
	}
	
	my $network_bridge_info = {};
	my $current_bridge_name;
	for my $line (@$output) {
		# Ignore blank and heading lines
		next if ($line !~ /\w/ || $line =~ /(bridge name)/);
		
		my ($bridge_name, $bridge_id, $stp_enabled, $interface_name) = $line =~ /
			^
			([^\s]+)
			\s+
			([^\s]+)
			\s+
			([^\s]+)
			\s*
			([^\s]*)
			$
		/gx;
		
		if (defined($bridge_name)) {
			$current_bridge_name = $bridge_name;
		}
		elsif (defined($current_bridge_name)) {
			# Bridge name not found in line but current bridge name was previously determined
			# Check if line only contains an interface name:
			($interface_name) = $line =~ /^\s+([^\s]+)$/gx;
			if (!defined($interface_name)) {
				notify($ERRORS{'DEBUG'}, 0, "ignoring line, neither bridge name nor interface name were not found\n" .
					"line: '$line'\n" .
					"output:\n" . join("\n", @$output)
				);
				next;
			}
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "ignoring line, it does not contain the bridge name and bridge name was not previously determined\n" .
				"line: '$line'\n" .
				"output:\n" . join("\n", @$output)
			);
			next;
		}
		
		$network_bridge_info->{$current_bridge_name}{bridge_id} = $bridge_id if defined($bridge_id);
		$network_bridge_info->{$current_bridge_name}{stp_enabled} = $stp_enabled if defined($stp_enabled);
		
		# Guarantee 'interfaces' key exists
		if (!defined($network_bridge_info->{$current_bridge_name}{interfaces})) {
			$network_bridge_info->{$current_bridge_name}{interfaces} = [];
		}
		push @{$network_bridge_info->{$current_bridge_name}{interfaces}}, $interface_name if $interface_name;
	}
	
	notify($ERRORS{'OK'}, 0, "retrieved network bridge configuration from $computer_name:" . format_data($network_bridge_info));
	return $network_bridge_info;
}

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

=head2 _delete_cached_service_info

 Parameters  : none
 Returns     : true
 Description :

=cut

sub _delete_cached_service_info {
	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;
	}
	
	if (defined($self->{service_init_module})) {
		delete $self->{service_init_module};
		notify($ERRORS{'DEBUG'}, 0, "deleted cached service init module info stored in \$self->{service_init_module}");
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "cached service init module info is NOT stored in \$self->{service_init_module}");
	}
	
	return 1;
}
#//////////////////////////////////////////////////////////////////////////////

=head2 service_exists

 Parameters  : $service_name, $no_cache (optional)
 Returns     : If called in scalar/boolean context: boolean
               If called in array context: array
 Description : Checks if the service exists on the computer. The return value
               differs depending on if this subroutine was called in
               scalar/boolean or array context.
               
               Scalar/boolean context returns either '0' or '1':
               if ($self->service_exists('xxx'))
               
               Array context returns an array with a single, integer element.
               The value of this integer is the index of the init module
               returned by get_init_modules which controls the service. This is
               done so the calling subroutine doesn't need to perform the same
               steps to determine which init module to use when controlling
               services. The value of the array element may be 0, meaning the
               service exists and is controlled by the first init module
               returned by get_init_modules. Therefore, be sure to check if the
               return value is defined and not whether it is true/false when
               called in array context.
               
               my ($init_module_index) = $self->service_exists('xxx');
               
               if (defined($init_module_index))... means service exists,
               $init_module_index may be 0 or another positive integer.
               
               if ($init_module_index)... WRONG! This will evaluate to false if
               the service does not exist or if it does exist and the first init
               module controls it.

=cut

sub service_exists {
	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, $no_cache) = @_;
	if (!$service_name) {
		notify($ERRORS{'WARNING'}, 0, "service name was not passed as an argument");
		return;
	}
	
	if ($no_cache) {
		$self->_delete_cached_service_info();
	}
	
	my $computer_node_name = $self->data->get_computer_node_name();
	
	if (!defined($self->{service_init_module}{$service_name})) {
		my @init_modules = $self->get_init_modules();
		for (my $init_module_index = 0; $init_module_index < scalar(@init_modules); $init_module_index++) {
			my $init_module = $init_modules[$init_module_index];
			my ($init_module_name) = ref($init_module) =~ /([^:]+)$/;
			my @service_names = $init_module->get_service_names();;
			for my $service_name (@service_names) {
				$self->{service_init_module}{$service_name} = {
					init_module_index => $init_module_index,
					init_module_name => $init_module_name,
				};
			}
		}
	}
	
	# Initialize an empty hash reference if the service name was not found to
	# prevent another full retrieval if this is called again for the same service
	if (!defined($self->{service_init_module}{$service_name})) {
		$self->{service_init_module}{$service_name} = {};
	}
	
	my $init_module_index = $self->{service_init_module}{$service_name}{init_module_index};
	my $init_module_name = $self->{service_init_module}{$service_name}{init_module_name};
	if (defined($init_module_index)) {
		notify($ERRORS{'DEBUG'}, 0, "'$service_name' exists, contolled by $init_module_name init module ($init_module_index)");
		return (wantarray) ? ($init_module_index) : 1;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "'$service_name' does NOT exist");
		return (wantarray) ? () : 0;
	}
}

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

=head2 is_service_enabled

 Parameters  : $service_name
 Returns     : boolean
 Description : Determines if a service is enabled on the computer.

=cut

sub is_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 was not passed as an argument");
		return;
	}
	
	my $computer_node_name = $self->data->get_computer_node_name();
	
	my ($init_module_index) = $self->service_exists($service_name);
	if (!defined($init_module_index)) {
		notify($ERRORS{'WARNING'}, 0, "unable to determine if '$service_name' service is enabled, it does not exist on $computer_node_name");
		return;
	}
	
	my $init_module = ($self->get_init_modules())[$init_module_index];
	if (!$init_module->can('service_enabled')) {
		notify($ERRORS{'WARNING'}, 0, "unable to determine if '$service_name' service is enabled on $computer_node_name, " . ref($init_module) . " module does not implement a 'service_running' subroutine");
		return;
	}
	return $init_module->service_enabled($service_name);
}


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

=head2 is_service_running

 Parameters  : $service_name
 Returns     : boolean
 Description : Determines if a service is running on the computer.

=cut

sub is_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 was not passed as an argument");
		return;
	}
	
	my $computer_node_name = $self->data->get_computer_node_name();
	
	my ($init_module_index) = $self->service_exists($service_name);
	if (!defined($init_module_index)) {
		notify($ERRORS{'WARNING'}, 0, "unable to determine if '$service_name' service is running, it does not exist on $computer_node_name");
		return;
	}
	
	my $init_module = ($self->get_init_modules())[$init_module_index];
	if (!$init_module->can('service_running')) {
		notify($ERRORS{'WARNING'}, 0, "unable to determine if '$service_name' service is running on $computer_node_name, " . ref($init_module) . " module does not implement a 'service_running' subroutine");
		return;
	}
	return $init_module->service_running($service_name);
}

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

=head2 enable_service

 Parameters  : $service_name
 Returns     : boolean
 Description : Enables a service on the computer.

=cut

sub enable_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 was not passed as an argument");
		return;
	}
	
	my $computer_node_name = $self->data->get_computer_node_name();
	
	my ($init_module_index) = $self->service_exists($service_name);
	if (!defined($init_module_index)) {
		notify($ERRORS{'WARNING'}, 0, "unable to enable '$service_name' service, it does not exist on $computer_node_name");
		return;
	}
	
	my $init_module = ($self->get_init_modules())[$init_module_index];
	if (!$init_module->can('enable_service')) {
		notify($ERRORS{'WARNING'}, 0, "unable to enable '$service_name' service on $computer_node_name, " . ref($init_module) . " module does not implement an 'enable_service' subroutine");
		return;
	}
	return $init_module->enable_service($service_name);
}

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

=head2 disable_service

 Parameters  : $service_name
 Returns     : boolean
 Description : disables a service on the computer.

=cut

sub disable_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 was not passed as an argument");
		return;
	}
	
	my $computer_node_name = $self->data->get_computer_node_name();
	
	my ($init_module_index) = $self->service_exists($service_name);
	if (!defined($init_module_index)) {
		notify($ERRORS{'WARNING'}, 0, "unable to disable '$service_name' service, it does not exist on $computer_node_name");
		return;
	}
	
	my $init_module = ($self->get_init_modules())[$init_module_index];
	if (!$init_module->can('disable_service')) {
		notify($ERRORS{'WARNING'}, 0, "unable to disable '$service_name' service on $computer_node_name, " . ref($init_module) . " module does not implement an 'disable_service' subroutine");
		return;
	}
	return $init_module->disable_service($service_name);
}

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

=head2 start_service

 Parameters  : $service_name
 Returns     : boolean
 Description : 

=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 was not passed as an argument");
		return;
	}
	
	my $computer_node_name = $self->data->get_computer_node_name();
	
	my ($init_module_index) = $self->service_exists($service_name);
	if (!defined($init_module_index)) {
		notify($ERRORS{'WARNING'}, 0, "unable to start '$service_name' service because it does not exist on $computer_node_name");
		return;
	}
	
	my $init_module = ($self->get_init_modules())[$init_module_index];
	return $init_module->start_service($service_name);
}

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

=head2 stop_service

 Parameters  : $service_name
 Returns     : boolean
 Description : 

=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 was not passed as an argument");
		return;
	}
	
	my $computer_node_name = $self->data->get_computer_node_name();
	
	my ($init_module_index) = $self->service_exists($service_name);
	if (!defined($init_module_index)) {
		notify($ERRORS{'DEBUG'}, 0, "unable to stop '$service_name' service because it does not exist on $computer_node_name");
		return 1;
	}
	
	my $init_module = ($self->get_init_modules())[$init_module_index];
	return $init_module->stop_service($service_name);
}

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

=head2 restart_service

 Parameters  : $service_name
 Returns     : boolean
 Description : 

=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 was not passed as an argument");
		return;
	}
	
	my $computer_node_name = $self->data->get_computer_node_name();
	
	my ($init_module_index) = $self->service_exists($service_name);
	if (!defined($init_module_index)) {
		notify($ERRORS{'WARNING'}, 0, "unable to restart '$service_name' service because it does not exist on $computer_node_name");
		return;
	}
	
	my $init_module = ($self->get_init_modules())[$init_module_index];
	return $init_module->restart_service($service_name);
}

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

=head2 delete_service

 Parameters  : $service_name
 Returns     : boolean
 Description : 

=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 was not passed as an argument");
		return;
	}
	
	my $computer_node_name = $self->data->get_computer_node_name();
	
	my ($init_module_index) = $self->service_exists($service_name, 1);
	if (!defined($init_module_index)) {
		notify($ERRORS{'DEBUG'}, 0, "unable to delete '$service_name' service because it does not exist on $computer_node_name");
		return 1;
	}
	
	my $init_module = ($self->get_init_modules())[$init_module_index];
	if ($init_module->delete_service($service_name)) {
		$self->_delete_cached_service_info();
	}
	else {
		return;
	}
	
	return 1;
}

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

=head2 check_connection_on_port

 Parameters  : $port
 Returns     : boolean (1=connected, 0=not connected, NULL=error)
 Description : Checks if a connection exists on the port specified.

=cut

sub check_connection_on_port {
	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 $remote_ip                   = $self->data->get_reservation_remote_ip();
	my $computer_public_ip_address  = $self->get_public_ip_address();
	
	my $port = shift;
	if (!$port) {
		notify($ERRORS{'WARNING'}, 0, "port variable was not passed as an argument");
		return;
	}
	
	my $port_connection_info = $self->get_port_connection_info();
	
	for my $protocol (keys %$port_connection_info) {
		if (!defined($port_connection_info->{$protocol}{$port})) {
			next;
		}
		
		for my $connection (@{$port_connection_info->{$protocol}{$port}}) {
			my $connection_local_ip = $connection->{local_ip};
			my $connection_remote_ip = $connection->{remote_ip};
			
			if (defined($computer_public_ip_address) && $connection_local_ip ne $computer_public_ip_address) {
				notify($ERRORS{'DEBUG'}, 0, "ignoring connection to $computer_node_name, not connected to public IP address ($computer_public_ip_address): $connection_remote_ip --> $connection_local_ip:$port ($protocol)");
				next;
			}
			
			if ($connection_remote_ip eq $remote_ip) {
				notify($ERRORS{'DEBUG'}, 0, "connection to $computer_node_name detected from reservation remote IP: $connection_remote_ip --> $connection_local_ip:$port ($protocol)");
				return 1;
			}
			
			# Connection is not from reservation remote IP address, check if user is logged in
			if ($self->user_logged_in()) {
				notify($ERRORS{'DEBUG'}, 0, "connection to $computer_node_name detected from different remote IP address than current reservation remote IP ($remote_ip): $connection_remote_ip --> $connection_local_ip:$port ($protocol), updating reservation remote IP to $connection_remote_ip");
				$self->data->set_reservation_remote_ip($connection_remote_ip);
				return 1;
			}
			
			notify($ERRORS{'DEBUG'}, 0, "ignoring connection to $computer_node_name, user is not logged in and remote IP address does not match current reservation remote IP ($remote_ip): $connection_remote_ip --> $connection_local_ip:$port ($protocol)");
		}
	}
	
	notify($ERRORS{'DEBUG'}, 0, "connection to $computer_node_name NOT detected on port $port");
	return 0;
}

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

=head2 get_cpu_core_count

 Parameters  : none
 Returns     : integer
 Description : Retrieves the quantitiy of CPU cores the computer has.

=cut

sub get_cpu_core_count {
	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;
	}
	
	my $computer_node_name = $self->data->get_computer_node_name();
	
	my $command = "cat /proc/cpuinfo";
	my ($exit_status, $output) = $self->execute($command);
	
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve CPU info from $computer_node_name");
		return;
	}
	
	# Get the number of 'processor :' lines and the 'cpu cores :' and 'siblings :' values from the cpuinfo output
	my $processor_count = scalar(grep(/^processor\s*:/, @$output));
	if (!$processor_count) {
		notify($ERRORS{'WARNING'}, 0, "unable to determine $computer_node_name CPU core count, output does not contain any 'processor :' lines:\n" . join("\n", @$output));
		return;
	}
	my ($cpu_cores) = map {$_ =~ /cpu cores\s*:\s*(\d+)/} @$output;
	$cpu_cores = 1 unless $cpu_cores;
	
	my ($siblings) = map {$_ =~ /siblings\s*:\s*(\d+)/} @$output;
	$siblings = 1 unless $siblings;
	
	# The actual CPU core count can be determined by the equation:
	my $cpu_core_count = ($processor_count * $cpu_cores / $siblings);
	
	# If hyperthreading is enabled, siblings will be greater than CPU cores
	# If hyperthreading is not enabled, they will be equal
	my $hyperthreading_enabled = ($siblings > $cpu_cores) ? 'yes' : 'no';
	
	notify($ERRORS{'DEBUG'}, 0, "retrieved $computer_node_name CPU core count: $cpu_core_count
		cpuinfo 'processor' line count: $processor_count
		cpuinfo 'cpu cores': $cpu_cores
		cpuinfo 'siblings': $siblings
		hyperthreading enabled: $hyperthreading_enabled"
	);
	
	return $cpu_core_count;
}

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

=head2 get_cpu_speed

 Parameters  : none
 Returns     : integer
 Description : Retrieves the speed of the computer's CPUs in MHz.

=cut

sub get_cpu_speed {
	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;
	}
	
	my $computer_node_name = $self->data->get_computer_node_name();
	
	my $command = "cat /proc/cpuinfo";
	my ($exit_status, $output) = $self->execute($command);
	
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve CPU info from $computer_node_name");
		return;
	}
	
	my ($mhz) = map {$_ =~ /cpu MHz\s*:\s*(\d+)/} @$output;
	if ($mhz) {
		$mhz = int($mhz);
		notify($ERRORS{'DEBUG'}, 0, "retrieved $computer_node_name CPU speed: $mhz MHz");
		return $mhz;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to determine $computer_node_name CPU speed CPU speed, 'cpu MHz :' line does not exist in the cpuinfo output:\n" . join("\n", @$output));
		return;
	}
}

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

=head2 get_total_memory

 Parameters  : none
 Returns     : integer
 Description : Retrieves the computer's total memory capacity in MB.

=cut

sub get_total_memory {
	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;
	}
	
	my $computer_node_name = $self->data->get_computer_node_name();
	
	my $command = "dmesg | grep Memory:";
	my ($exit_status, $output) = $self->execute($command);
	
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve memory info from $computer_node_name");
		return;
	}
	
	# Output should look like this:
	# Memory: 1024016k/1048576k available (2547k kernel code, 24044k reserved, 1289k data, 208k init)
	my ($memory_kb) = map {$_ =~ /Memory:.*\/(\d+)k available/} @$output;
	if ($memory_kb) {
		my $memory_mb = int($memory_kb / 1024);
		notify($ERRORS{'DEBUG'}, 0, "retrieved $computer_node_name total memory capacity: $memory_mb MB");
		return $memory_mb;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to determine $computer_node_name total memory capacity from command: '$command', output:\n" . join("\n", @$output));
		return;
	}
}

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

=head2 get_exclude_list
 
 Parameters  : none
 Returns     : array
 Description : Retrieves /root/.vclcontrol/vcl_exclude_list from the computer
               and constructs an array. Blank lines are ommitted. Spaces at the
               beginning or end of lines are removed.
 
=cut

sub get_exclude_list {
	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;
	}
	
	my $computer_name = $self->data->get_computer_node_name();
	
	# Check if previously retrieved
	if (defined($self->{exclude_list_lines})) {
		#notify($ERRORS{'DEBUG'}, 0, "returning previously retrieved exclude list from $computer_name:\n" . join("\n", @{$self->{exclude_list_lines}}));
		return @{$self->{exclude_list_lines}};
	}
	
	my $exclude_file_path = "/root/.vclcontrol/vcl_exclude_list";
	
	if (!$self->file_exists($exclude_file_path)) {
		$self->{exclude_list_lines} = [];
		return ();
	}
	
	# Retrieve the contents of vcl_exclude_list
	my @exclude_lines = $self->get_file_contents($exclude_file_path);
	
	# Check for blank lines and other problems
	my @exclude_lines_cleaned;
	my $exclude_lines_cleaned_string = '';
	for my $exclude_line (@exclude_lines) {
		# Ignore blank lines
		if ($exclude_line !~ /\w/) {
			next;
		}
		
		# Remove leading and trailing spaces
		$exclude_line =~ s/(^\s+|\s+$)//g;
		
		push @exclude_lines_cleaned, $exclude_line;
		$exclude_lines_cleaned_string .= "'$exclude_line'\n";
	}
	
	$self->{exclude_list_lines} = \@exclude_lines_cleaned;
	
	notify($ERRORS{'DEBUG'}, 0, "retrieved and parsed $exclude_file_path on $computer_name:\n$exclude_lines_cleaned_string");
	return @exclude_lines_cleaned;
}

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

=head2 get_exclude_regex_list
 
 Parameters  : none
 Returns     : array
 Description : Assembles a regular expression string based on the contents of
               each line in /root/.vclcontrol/vcl_exclude_list on the computer.
               If the file doesn't exist or is empty, an empty array is
               returned.

=cut

sub get_exclude_regex_list {
	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;
	}
	
	my $computer_name = $self->data->get_computer_node_name();
	
	# Check if previously retrieved
	if (defined($self->{exclude_regex_list})) {
		#notify($ERRORS{'DEBUG'}, 0, "returning previously retrieved exclude list regex from $computer_name:\n" . join("\n", @{$self->{exclude_regex_list}}));
		return @{$self->{exclude_regex_list}};
	}
	
	# Retrieve exclude_list
	my @exclude_files = $self->get_exclude_list();
	if (!@exclude_files) {
		$self->{exclude_regex_list} = [];
		return ();
	}
	
	my @exclude_regex_list;
	my $exclude_regex_list_string;
	for my $exclude_file (@exclude_files) {
		my $exclude_regex = $exclude_file;
		
		# Add ^ to the beginning, remove any leading spaces
		$exclude_regex =~ s/^[\s\^]*/\^/g;
		
		# Add $ to the end, remove any trailing spaces
		$exclude_regex =~ s/[\s\$]*$/\$/g;
		
		# Escape forward slashes and periods
		$exclude_regex =~ s/\\*([\/\.])/\\$1/g;
		
		# Change asterisk to regex: * --> .*
		$exclude_regex =~ s/\*+/\.\*/g;
		
		push @exclude_regex_list, $exclude_regex;
		$exclude_regex_list_string .= $exclude_regex . "\n";
	}
	chop($exclude_regex_list_string);
	
	$self->{exclude_regex_list} = \@exclude_regex_list;
	
	notify($ERRORS{'DEBUG'}, 0, "assembled regex list from vcl_exclude_list:\n$exclude_regex_list_string");
	return @exclude_regex_list;
}

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

=head2 is_file_in_exclude_list
 
 Parameters  : $file_path
 Returns     : boolean
 Description : Checks if the file matches any lines in
               /root/.vclcontrol/vcl_exclude_list on the computer. If it
               matches, true is returned meaning the file should not be altered.

 
=cut

sub is_file_in_exclude_list {
	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;
	}
	
	my $file_path = shift;
	if (!$file_path) {
		notify($ERRORS{'WARNING'}, 0, "file path argument was not specified");
		return;
	}
	
	my @exclude_regex_list = $self->get_exclude_regex_list();
	return 0 unless @exclude_regex_list;
	
	for my $exclude_regex (@exclude_regex_list) {
		if ($file_path =~ /$exclude_regex/i) {
			my $match = $1;
			notify($ERRORS{'DEBUG'}, 0, "file matches line in vcl_exclude_list:\nfile path: $file_path\nmatching regex: $exclude_regex");
			return 1;
		}
	}
	
	#notify($ERRORS{'DEBUG'}, 0, "file does NOT match any lines in vcl_exclude_list: $file_path");
	return 0;
}

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

=head2 generate_exclude_list_sample
 
 Parameters  : none
 Returns     : boolean
 Description : Generates /root/.vclcontrol/vcl_exclude_list.sample to help image
               creators utilize the file.
 
=cut

sub generate_exclude_list_sample {
	
	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;
	}
	
	my $exclude_file_name = "vcl_exclude_list";
	my $exclude_file_path = "/root/.vclcontrol/$exclude_file_name";
	my $sample_file_path  = "/root/.vclcontrol/$exclude_file_name.sample";
	
	my $sample_file_contents = <<"EOF";
When creating an image, you may create a $exclude_file_path file to prevent VCL from altering certain files during the image capture or load processes. Files listed within $exclude_file_name will not be altered. The $exclude_file_name file does not exist by default. You must create it if you wish to utilize this feature. You can specify full, exact file paths or use asterisk characters as wildcards within $exclude_file_name.

Examples:
/root/.ssh/id_rsa
This would only match the file with the exact path:
/root/.ssh/id_rsa

/root/.ssh/id_rsa*
This would match all files in the '/root/.ssh' directory with names beginning with 'id_rsa' including:
/root/.ssh/id_rsa
/root/.ssh/id_rsa.pub

/root/.ssh/id_rsa.*
This would match all files in the '/root/.ssh' directory with names beginning with 'id_rsa.' (including the period) including:
/root/.ssh/id_rsa.pub

In the previous example, '/root/.ssh/id_rsa' would not match because it does not contain a period after 'id_rsa'.
EOF
	
	# Format the string and add comment characters to the beginning of each line
	$sample_file_contents = wrap_string($sample_file_contents, 80, '# ');
	
	return $self->create_text_file($sample_file_path, $sample_file_contents);
}

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

=head2 user_logged_in

 Parameters  : $username (optional)
 Returns     : boolean
 Description : Determines if the user is currently logged in to the computer.

=cut

sub user_logged_in {
	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();
	
	# Attempt to get the username from the arguments
	# If no argument was supplied, use the user specified in the DataStructure
	my $username = shift || $self->data->get_user_login_id();
	
	my @logged_in_users = $self->get_logged_in_users();
	if (grep { $username eq $_ } @logged_in_users) {
		notify($ERRORS{'DEBUG'}, 0, "$username is logged in to $computer_node_name");
		return 1;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "$username is NOT logged in to $computer_node_name");
		return 0;
	}
}

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

=head2 get_logged_in_users

 Parameters  : none
 Returns     : array
 Description : Retrieves the names of users logged in to the computer.

=cut

sub get_logged_in_users {
	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 = "users";
	my ($exit_status, $output) = $self->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run command to determine logged in users on $computer_node_name: $command");
		return;
	}
	elsif (grep(/^users:/, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to determine logged in users on $computer_node_name, command: $command, output:\n" . join("\n", @$output));
		return;
	}
	
	my @usernames;
	for my $line (@$output) {
		my @line_usernames = split(/[\s+]/, $line);
		push @usernames, @line_usernames if @line_usernames;
	}
	
	my $username_count = scalar(@usernames);
	if ($username_count) {
		notify($ERRORS{'DEBUG'}, 0, "$username_count user" . ($username_count == 1 ? '' : 's') . " logged in to $computer_node_name: " . join(', ', @usernames));
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "no users logged in to $computer_node_name");
	}
	return @usernames;
}

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

=head2 clean_known_files

 Parameters  : none
 Returns     : boolean
 Description : Clears and deletes files defined for the Linux OS module in the
               $CAPTURE_CLEAR_FILE_PATHS and $CAPTURE_DELETE_FILE_PATHS class
               variables.

=cut

sub clean_known_files {
	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 $error_count = 0;
	
	# Clear files
	my @class_clear_file_path_array_refs = $self->get_class_variable_hierarchy('CAPTURE_CLEAR_FILE_PATHS');
	for my $class_clear_file_path_array_ref (@class_clear_file_path_array_refs) {
		for my $file_path (@$class_clear_file_path_array_ref) {
			if ($self->is_file_in_exclude_list($file_path)) {
				notify($ERRORS{'DEBUG'}, 0, "file not cleared because it is in the exclude list: $file_path");
				next;
			}
			$self->clear_file($file_path) || $error_count++;
		}
	}
	
	# Delete files
	my @class_delete_file_path_array_refs = $self->get_class_variable_hierarchy('CAPTURE_DELETE_FILE_PATHS');
	for my $class_delete_file_path_array_ref (@class_delete_file_path_array_refs) {
		for my $file_path (@$class_delete_file_path_array_ref) {
			if ($self->is_file_in_exclude_list($file_path)) {
				notify($ERRORS{'DEBUG'}, 0, "file not deleted because it is in the exclude list: $file_path");
				next;
			}
			$self->delete_file($file_path) || $error_count++;
		}
	}
	
	if ($error_count) {
		notify($ERRORS{'WARNING'}, 0, "encountered $error_count error" . ($error_count > 1 ? 's' : '') . " clearing and deleting files");
		return;
	}
	else {
		return 1;
	}
}

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

=head2 user_exists

 Parameters  : $username (optional)
 Returns     : boolean
 Description : 

=cut

sub user_exists {
	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 $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();
	# Attempt to get the username from the arguments
	# If no argument was supplied, use the user specified in the DataStructure
	my $username = shift;
	if (!$username) {
		$username = $self->data->get_user_login_id();
	}
	
	notify($ERRORS{'DEBUG'}, 0, "checking if user exists on $computer_node_name: $username");
	
	# Attempt to query the user account
	my $query_user_command = "id $username";
	my ($query_user_exit_status, $query_user_output) = $self->execute($query_user_command, 0);
	
	if (grep(/uid/, @$query_user_output)) {
		notify($ERRORS{'DEBUG'}, 0, "user exists on $computer_node_name: $username");
		return 1;
	}
	elsif (grep(/No such user/i, @$query_user_output)) {
		notify($ERRORS{'DEBUG'}, 0, "user does not exist on $computer_node_name: $username");
		return 0;
	}
	elsif (defined($query_user_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to determine if user exists on $computer_node_name: $username, exit status: $query_user_exit_status, output:\n@{$query_user_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to determine if user exists on $computer_node_name: $username");
		return;
	}
}

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

=head2 stop_external_sshd

 Parameters  : none
 Returns     : boolean
 Description : Kills the external sshd process.

=cut

sub stop_external_sshd {
	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();
	
	$self->stop_service('ext_sshd');
	
	# Run pkill to kill all external sshd processes
	# Exit status may be:
	# 0 - One or more processes matched the criteria.
	# 1 - No processes matched.
	my $pkill_command = "pkill -9 -f ext.*sshd";
	my ($pkill_exit_status, $pkill_output) = $self->execute($pkill_command);
	if (!defined($pkill_output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to kill external sshd process on $computer_node_name");
		return;
	}
	elsif ($pkill_exit_status eq '0') {
		notify($ERRORS{'DEBUG'}, 0, "killed external sshd process on $computer_node_name");
	}
	elsif ($pkill_exit_status eq '1') {
		notify($ERRORS{'DEBUG'}, 0, "external sshd process is not running on $computer_node_name");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to kill external sshd process on $computer_node_name, exit status: $pkill_exit_status, output:\n" . join("\n", @$pkill_output));
		return;
	}
	
	$self->delete_file('/var/run/ext_sshd.pid');
	
	return 1;
} ## end sub stop_external_sshd

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

=head2 configure_sshd_config_file

 Parameters  : $custom_parameters (optional), $output_file_path (optional)
 Returns     : boolean
 Description : Configures and generates an output file based
               on the /etc/ssh/sshd_config currently residing on the computer.
               This is used to configure both the sshd_config and
               external_sshd_config files. If no arguments are supplied,
               /etc/ssh/sshd_config is configured to its stock, default state.
               This is done prior to image capture. sshd_config is configured to
               listen on all interfaces.
               
               By default, all of the settings which exist in
               /etc/ssh/sshd_config are retained in the output file except for
               the following:
               StrictModes no
               UseDNS no
               PasswordAuthentication no
               PermitRootLogin without-password
               AllowUsers root
					Banner none
               
               In addition, any ListenAddress lines are not included in the
               output file.
               
               An optional $custom_parameters hash reference argument may be
               supplied. The key/values in this hash will result in the values
               being set in the output file. If a parameter is included with an
               empty value in the hash reference, all lines containing that
               parameter will be removed from the the resulting output file.
               Example:
               $self->configure_sshd_config_file({
                  ListenAddress =>'10.10.0.33',
                  AllowUsers => '',
               });
               
               The output file will be /etc/ssh/sshd_config since the 2nd
               argument was not specified. This file will be based off itself
               except a ListenAddress line will be added and all AllowUsers
               lines will be omitted.

=cut

sub configure_sshd_config_file {
	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 ($custom_parameters, $output_file_path) = @_;
	
	my $sshd_config_file_path = '/etc/ssh/sshd_config';
	
	# If output file path argument wasn't specified, write back to sshd_config
	$output_file_path = $sshd_config_file_path if !$output_file_path;
	
	# Check if the output file is in the exclude list before proceeding
	my @exclude_list = $self->get_exclude_list();
	if (@exclude_list && grep(m|$output_file_path|, @exclude_list)) {
		notify($ERRORS{'OK'}, 0, "skipping reconfiguration of $output_file_path because it is in the exclude file list");
		return 1;
	}
	
	# Get the contents of the sshd_config file already on the computer
	my @sshd_config_file_lines = $self->get_file_contents($sshd_config_file_path);
	if (!@sshd_config_file_lines) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve contents of $sshd_config_file_path from $computer_node_name");
		return;
	}
	
	# Add the following parameters to the end of the sshd_config file
	# Any existing lines containing these parameters will be discarded
	my $parameters = {
		StrictModes => 'no',
		UseDNS => 'no',
		PasswordAuthentication => 'no',
		PermitRootLogin => 'without-password',
		AllowUsers => 'root',
		ListenAddress => '',
		Banner => 'none',
	};
	
	if ($custom_parameters) {
		$parameters = {%$parameters, %$custom_parameters};
	}
	notify($ERRORS{'DEBUG'}, 0, "generating sshd config file: $output_file_path, custom parameters:\n" . format_data($parameters));
	
	my $custom_tag = 'VCL Settings';
	
	# Loop through the lines from the existing sshd_config file
	my $output_file_contents;
	LINE: for my $line (@sshd_config_file_lines) {
		# Ignore lines already in the file which will be added later with custom values
		if ((map { $line =~ /$_/ } ($custom_tag, keys %$parameters))) {
			#notify($ERRORS{'DEBUG'}, 0, "ignoring line in $sshd_config_file_path: '$line'");
			next LINE;
		}
		$output_file_contents .= "$line\n";
	}
	
	# Remove extra blank lines from the end of the file
	$output_file_contents =~ s/[\s\n]*$//gs;
	
	# Add each of the custom parameters to the file
	$output_file_contents .= "\n\n#" . ('-' x 20) . " $custom_tag " . ('-' x 20) . "\n";
	for my $custom_parameter (sort keys %$parameters) {
		my $custom_value = $parameters->{$custom_parameter};
		
		# Add the custom parameter of the value is set
		if (defined($custom_value) && length($custom_value) > 0) {
			$output_file_contents .= "$custom_parameter $custom_value\n";
		}
	}
	
	if (!$self->create_text_file($output_file_path, $output_file_contents)) {
		notify($ERRORS{'WARNING'}, 0, "failed to create file on $computer_node_name: $output_file_path");
		return;
	}
	
	if (!$self->set_file_permissions($output_file_path, '600')) {
		notify($ERRORS{'WARNING'}, 0, "failed to set permissions of $output_file_path on $computer_node_name");
		return;
	}
	
	return 1;
}

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

=head2 configure_ext_sshd_config_file

 Parameters  : none
 Returns     : boolean
 Description : Generates /etc/ssh/external_sshd_config based off of
               /etc/ssh/sshd_config currently residing on the computer with the
               following parameters overridden:
               PidFile /var/run/ext_sshd.pid
               PermitRootLogin no
               X11Forwarding yes
               PasswordAuthentication yes
               AllowUsers 
               ListenAddress => <public IP aaddress>

=cut

sub configure_ext_sshd_config_file {
	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 $ext_sshd_config_file_path = '/etc/ssh/external_sshd_config';
	
	my $public_ip_address = $self->get_public_ip_address();
	if (!$public_ip_address) {
		notify($ERRORS{'WARNING'}, 0, "failed to generate $ext_sshd_config_file_path on $computer_node_name, public IP address could not be determined");
		return;
	}
	
	my $custom_ext_sshd_parameters = {
		PidFile => '/var/run/ext_sshd.pid',
		PermitRootLogin => 'no',
		X11Forwarding => 'yes',
		PasswordAuthentication => 'yes',
		AllowUsers => '',
		ListenAddress => $public_ip_address,
	};
	
	return $self->configure_sshd_config_file($custom_ext_sshd_parameters, $ext_sshd_config_file_path);
}

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

=head2 configure_default_sshd

 Parameters  : none
 Returns     : boolean
 Description : Configures the sshd daemon back to a mostly default state.
               Removes the ext_sshd service from the computer. Reconfigures
               sshd to listen on all interfaces. Restarts sshd.
               
               This is called prior to image
               capture. The purpose is to configure the captured image so that
               it responds to SSH after it is loaded from any interface.

=cut

sub configure_default_sshd {
	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();
	
	if (!$self->service_exists('sshd', 1)) {
		notify($ERRORS{'DEBUG'}, 0, "skipping default sshd configuation, sshd service does not exist");
		return 1;
	}
	
	# Stop existing external sshd process if it is running
	if (!$self->stop_external_sshd()) {
		notify($ERRORS{'WARNING'}, 0, "unable to configure default sshd state, problem occurred attempting to kill external sshd process");
		return;
	}
	
	# Delete the ext_sshd service
	$self->delete_service('ext_sshd') || return;
	
	# Delete the external sshd configuration file
	$self->delete_file('/etc/ssh/ext*ssh*');
	
	# Reconfigure sshd_config back to its default state
	$self->configure_sshd_config_file() || return;
	
	# Restart the sshd service
	$self->restart_service('sshd') || return;
	
	return 1;
}

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

=head2 configure_ext_sshd

 Parameters  : none
 Returns     : boolean
 Description : Adds the external_sshd_config file configured to listen on the
               public network and adds the ext_sshd service to the computer.
               Reconfigures the existing sshd service to only listen on the
               private network and restarts sshd. Stops the ext_sshd service if
               it is started.

=cut

sub configure_ext_sshd {
	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 $private_ip_address = $self->get_private_ip_address();
	if (!$private_ip_address) {
		notify($ERRORS{'WARNING'}, 0, "unable to configure ext_sshd, failed to retrieve private IP address of $computer_node_name, necessary to configure sshd to only listen on private network");
		return;
	}
	
	if (!$self->service_exists('sshd', 1)) {
		notify($ERRORS{'DEBUG'}, 0, "skipping ext_sshd configuation, sshd service does not exist");
		return 1;
	}
	
	# Recreate the sshd_config file, set ListenAddress to the private IP address
	if (!$self->configure_sshd_config_file({ListenAddress => $private_ip_address})) {
		notify($ERRORS{'WARNING'}, 0, "unable to configure ext_sshd, failed to reconfigure sshd_config to only listen on private network on $computer_node_name");
		return;
	}
	
	# Restart sshd to enact the changes
	if (!$self->restart_service('sshd')) {
		notify($ERRORS{'WARNING'}, 0, "unable to configure ext_sshd, failed to restart sshd on $computer_node_name after reconfiguring sshd_config to only listen on private network");
		return;
	}
	
	# Create and configure the ext_sshd service
	if (!$self->configure_ext_sshd_config_file()) {
		notify($ERRORS{'WARNING'}, 0, "unable to configure ext_sshd, failed to configure external_sshd_config file on $computer_node_name");
		return;
	}
	
	# Deterine which init module is currently controlling sshd, use the same module to control ext_sshd
	my ($init_module_index) = $self->service_exists('sshd', 1);
	if (!defined($init_module_index)) {
		notify($ERRORS{'WARNING'}, 0, "unable to configure ext_sshd, init module controlling sshd could not be determined");
		return;
	}
	
	my $init_module = ($self->get_init_modules())[$init_module_index];
	
	# Add the ext_sshd service
	if (!$init_module->add_ext_sshd_service()) {
		notify($ERRORS{'WARNING'}, 0, "unable to configure ext_sshd, failed to add the ext_sshd service to $computer_node_name");
		return;
	}
	
	# Delete the cached service info to make sure ext_sshd isn't incorrectly reported as not existing
	$self->_delete_cached_service_info();
	
	# Stop ext_sshd if it is running
	$self->stop_service('ext_sshd');
	
	notify($ERRORS{'OK'}, 0, "configured ext_sshd on $computer_node_name");
	return 1;
}

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

=head2 configure_rc_local

 Parameters  : none
 Returns     : boolean
 Description : Checks if /etc/rc.local was configured by a previous version of
               VCL. If so, returns file to its default state. Previous versions
               of VCL had been adding commands to rc.local to configure
               networking. This is now done by this backend code.

=cut

sub configure_rc_local {
	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();
	
	
	# Check if rc.local is in the exclude list before proceeding
	my @exclude_list = $self->get_exclude_list();
	if (@exclude_list && grep(m|rc.local|, @exclude_list)) {
		notify($ERRORS{'OK'}, 0, "skipping reconfiguration of rc.local because it is in the exclude file list");
		return 1;
	}
	
	my $rc_local_contents = <<EOF;
#!/bin/sh
#
# This script will be executed *after* all the other init scripts.

touch /var/lock/subsys/local
EOF

	for my $rc_local_file_path ('/etc/rc.d/rc.local', '/etc/rc.local') {
		# Check if the file exists or else get_file_contents will complain
		if (!$self->file_exists($rc_local_file_path)) {
			notify($ERRORS{'DEBUG'}, 0, "skipping $rc_local_file_path reconfiguration, file does not exist on $computer_node_name");
			next;
		}
		
		my @rc_local_lines = $self->get_file_contents($rc_local_file_path);
		if (!@rc_local_lines) {
			notify($ERRORS{'WARNING'}, 0, "failed to retrieve contents of $rc_local_file_path on $computer_node_name");
			next;
		}
		elsif (!grep(/ListenAddress \$IP0/, @rc_local_lines)) {
			notify($ERRORS{'DEBUG'}, 0, "skipping $rc_local_file_path reconfiguration, it does not appear to be configured by a previous version of VCL");
			next;
		}
		else {
			notify($ERRORS{'DEBUG'}, 0, "$rc_local_file_path will be returned to its default state, it appears to have been configured by a previous version of VCL");
		}
		
		if (!$self->create_text_file($rc_local_file_path, $rc_local_contents)) {
			return;
		}
	}
	return 1;
}

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

=head2 command_exists

 Parameters  : $shell_command
 Returns     : boolean
 Description : Determines if a shell command exists on the computer by executing
               'which <command>'.

=cut

sub command_exists {
	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 $shell_command = shift;
	if (!$shell_command) {
		notify($ERRORS{'WARNING'}, 0, "shell command argument was not supplied");
		return;
	}
	
	if ($self->{command_exists}{$shell_command}) {
		return 1;
	}
	
	my $computer_node_name = $self->data->get_computer_node_name();
	
	my $command = "which $shell_command";
	my ($exit_status, $output) = $self->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to determine if the '$shell_command' shell command exists on $computer_node_name");
		return;
	}
	elsif (my ($command_line) = grep(/\/$shell_command$/, @$output)) {
		notify($ERRORS{'DEBUG'}, 0, "verified '$shell_command' command exists on $computer_node_name: $command_line");
		$self->{command_exists}{$shell_command} = 1;
		return 1;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "'$shell_command' command does NOT exist on $computer_node_name, command: $command\noutput:\n" . join("\n", @$output));
		$self->{command_exists}{$shell_command} = 0;
		return 0;
	}
}

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

=head2 notify_user_console

 Parameters  : message, username(optional)
 Returns     : boolean
 Description : Send a message to the user on the console

=cut

sub notify_user_console {
	my $self = shift;
	if (ref($self) !~ /Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}

	my $message = shift;
	if (!$message) {
		notify($ERRORS{'WARNING'}, 0, "message argument was not supplied");
		return;
	}

	my $username = shift;
	if (!$username) {
		$username = $self->data->get_user_login_id();
	}

	my $computer_node_name = $self->data->get_computer_node_name();

	my $cmd = "echo \"$message\" \| write $username";
	my ($exit_status, $output) = $self->execute($cmd, 1);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to determine if the '$cmd' shell command exists on $computer_node_name");
		return;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "executed command to determine if the '$cmd' shell command exists on $computer_node_name");
		return 1;
	}
}

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

=head2 is_64_bit

 Parameters  : none
 Returns     : boolean
 Description : Determines if the OS is 64-bit or not.

=cut

sub is_64_bit {
	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 = 'uname -m';
	my ($exit_status, $output) = $self->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute '$command' command to determine if $computer_node_name contains a 64-bit Linux OS");
		return;
	}
	elsif (grep(/uname:/, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to determine if $computer_node_name contains a 64-bit Linux OS, command: '$command', output:\n" . join("\n", @$output));
		return;
	}
	elsif (grep(/64/, @$output)) {
		#notify($ERRORS{'DEBUG'}, 0, "$computer_node_name contains a 64-bit Linux OS, output:\n" . join("\n", @$output));
		return 1;
	}
	else {
		#notify($ERRORS{'DEBUG'}, 0, "$computer_node_name does NOT contain a 64-bit Linux OS, output:\n" . join("\n", @$output));
		return 0;
	}
}

# !!! DON'T USE get_user_remote_ip_addresses as it's currently written !!!
# Needs testing, the 'who' command only shows pseudo terminal connections
##/////////////////////////////////////////////////////////////////////////////
#
#=head2 get_user_remote_ip_addresses
#
# Parameters  : none
# Returns     : hash reference
# Description : Retrieves info regarding users connected to the computer. A hash
#               reference is returned. The hash keys are the usernames logged in.
#               The value of each username key is an array reference containing
#               the remote IP addresses that user is connected from. Example:
#                  {
#                    "admin" => [
#                      "152.1.1.1"
#                    ],
#                    "root" => [
#                      "10.1.1.1"
#                    ]
#                  }
#
#=cut
#
#sub get_user_remote_ip_addresses {
#	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 $who_command = 'who -u';
#	my ($who_exit_status, $who_output) = $self->execute($who_command, 0);
#	if (!defined($who_output)) {
#		notify($ERRORS{'WARNING'}, 0, "failed to execute '$who_command' command to determine users connected to $computer_node_name");
#		return;
#	}
#	elsif (grep(/who:/, @$who_output)) {
#		notify($ERRORS{'WARNING'}, 0, "failed to determine users connected to $computer_node_name, exit status: $who_exit_status, command: '$who_command', output:\n" . join("\n", @$who_output));
#		return;
#	}
#
#	my $connected_user_info = {};
#	for my $line (@$who_output) {
#		# NAME     LINE         TIME             IDLE          PID COMMENT
#		# root     pts/0        2014-03-12 11:32   .          5403 (10.1.0.1)
#		# admin    pts/1        2014-03-11 13:34 22:01        4336 (152.1.1.1)
#		
#		my ($username, $remote_ip) = $line =~ /^([^\s]+).+\(([\d\.]+)\)/;
#		if ($username && $remote_ip) {
#			if (!defined($connected_user_info->{$username})) {
#				$connected_user_info->{$username} = [];
#			}
#			push @{$connected_user_info->{$username}}, $remote_ip;
#		}
#	}
#	
#	if ($connected_user_info) {
#		notify($ERRORS{'DEBUG'}, 0, "retrieved user connection info using the 'who' command from $computer_node_name:\n" . format_data($connected_user_info));
#		return $connected_user_info;
#	}
#	else {
#		notify($ERRORS{'DEBUG'}, 0, "did not detect any users connected to $computer_node_name using the 'who' command, attempting to call 'last'");
#	}
#	
#	my $last_command = 'last';
#	my ($last_exit_status, $last_output) = $self->execute($last_command, 0);
#	if (!defined($last_output)) {
#		notify($ERRORS{'WARNING'}, 0, "failed to execute '$last_command' command to determine users connected to $computer_node_name");
#		return;
#	}
#	elsif (grep(/last:/, @$last_output)) {
#		notify($ERRORS{'WARNING'}, 0, "failed to determine users connected to $computer_node_name, exit status: $last_exit_status, command: '$last_command', output:\n" . join("\n", @$last_output));
#		return;
#	}
#	
#	for my $line (@$last_output) {
#		# root     pts/0        10.1.0.1      Wed Mar 12 11:32   still logged in
#		# admin    pts/1        152.1.1.1     Tue Mar 11 13:34   still logged in
#		# root     pts/0        10.1.0.1      Tue Mar 11 13:23 - 10:47  (21:24)
#		
#		my ($username, $remote_ip) = $line =~ /^([^\s]+).+[\s\t](\d+\.\d+\.\d+\.\d+)[\s\t].*logged\s+in/i;
#		if ($username && $remote_ip) {
#			if (!defined($connected_user_info->{$username})) {
#				$connected_user_info->{$username} = [];
#			}
#			push @{$connected_user_info->{$username}}, $remote_ip;
#		}
#	}
#	
#	if ($connected_user_info) {
#		notify($ERRORS{'DEBUG'}, 0, "retrieved user connection info using the 'last' command from $computer_node_name:\n" . format_data($connected_user_info));
#	}
#	else {
#		notify($ERRORS{'DEBUG'}, 0, "did not detect any users connected to $computer_node_name");
#	}
#	return $connected_user_info;
#}

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

=head2 get_port_connection_info

 Parameters  : none
 Returns     : hash reference
 Description : Retrieves information about established connections from the
               computer. A hash is constructed:
               {
                 "tcp" => {
                   22 => [
                     {
                       "local_ip" => "10.25.10.194",
                       "pid" => 5400,
                       "program" => "sshd",
                       "remote_ip" => "10.25.0.241"
                     },
                     {
                       "local_ip" => "192.168.18.135",
                       "pid" => 5689,
                       "program" => "sshd",
                       "remote_ip" => "192.168.53.54"
                     },
                   ],
                   3389 => [
                     {
                       "local_ip" => "192.168.18.135",
                       "pid" => 6767,
                       "program" => "xrdp",
                       "remote_ip" => "192.168.53.54"
                     }
                   ]
                 }
               }

=cut

sub get_port_connection_info {
	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 = "netstat -anp";
	my ($exit_status, $output) = $self->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command: $command");
		return;
	}
	elsif (grep(/^netstat: /, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "error occurred executing command: '$command', exit status: $exit_status, output:\n" . join("\n", @$output));
		return;
	}
	
	my $connection_info = {};
	for my $line (@$output) {
		# Proto Recv-Q Send-Q Local Address      Foreign Address     State        PID/Program name
		# tcp        0      0 192.168.13.220:22  152.1.1.100:63497   ESTABLISHED  6199/sshd
		# tcp        0      0 10.10.3.220:22     10.10.14.13:52239   ESTABLISHED  5189/sshd
		my ($protocol, $local_ip_address, $port, $remote_ip_address, $state, $pid, $program) = $line =~ /^(\w+).+\s([\d\.]+):(\d+)\s+([\d\.]+):\d*\s+(\w+)\s+(\d+)?\/?(\w+)?/i;
		if (!$state || $state !~ /ESTABLISHED/i) {
			next;
		}
		
		my $connection = {
			remote_ip => $remote_ip_address,
			local_ip => $local_ip_address,
		};
		$connection->{pid} = $pid if $pid;
		$connection->{program} = $program if $program;
		
		push @{$connection_info->{$protocol}{$port}}, $connection;
	}
	
	if ($connection_info) {
		#notify($ERRORS{'DEBUG'}, 0, "retrieved connection info from $computer_node_name:\n" . format_data($connection_info));
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "did not detect any connections on $computer_node_name");
	}
	return $connection_info;
}

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

=head2 enable_ip_forwarding

 Parameters  : none
 Returns     : boolean
 Description : Adds 'net.ipv4.ip_forward=1' to /etc/sysctl.conf if it doesn't
               already exist. Executes 'sysctl -p' to apply the setting.

=cut

sub enable_ip_forwarding {
	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 $sysctl_conf_path = '/etc/sysctl.conf';
	$self->set_config_file_parameter($sysctl_conf_path, 'net.ipv4.ip_forward', '=', '1');
	
	my $command = "sysctl -p";
	my ($exit_status, $output) = $self->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to enable IP forwarding on $computer_node_name: $command");
		return;
	}
	
	# Output should contain:
	# net.ipv4.ip_forward = 1
	if ($exit_status eq '0' || grep(/net.ipv4.ip_forward.*1/, @$output)) {
		notify($ERRORS{'OK'}, 0, "IP forwarding is enabled on $computer_node_name:\n" . join("\n", @$output));
		return 1;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to enable IP forwarding on $computer_node_name, command: '$command', exit status: $exit_status, output:\n" . join("\n", @$output));
		return 0;
	}
}

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

=head2 should_set_user_password

 Parameters  : $user_id, $no_cache (optional)
 Returns     : boolean
 Description : Determines whether or not a random password should be generated
					and used for the user account created on the computer being
					loaded. A random password WILL be used if any of the following
					are true:
					* The user.uid value is NOT set in the database for the user
					* The managementnode.NOT_STANDALONE value is empty
					* The managementnode.NOT_STANDALONE value is populated but does
					  NOT match the user's affiliation.name value
					
					A federated authentication method such as Kerberos WILL be used
					and a random password will NOT be generated if:
					* The user.uid value SI set in the database for the user
					* The managementnode.NOT_STANDALONE value is populated and
					  matches the user's affiliation.name value
					
					Note: managementnode.NOT_STANDALONE corresponds to the management
					node's 'Affiliations Using Federated Authentication for Linux
					Images' setting on the VCL website

=cut

sub should_set_user_password {
	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 ($user_id, $no_cache) = @_;
	if (!$user_id) {
		notify($ERRORS{'WARNING'}, 0, "user ID argument was not supplied");
		return;
	}
	elsif ($user_id !~ /^\d+$/) {
		notify($ERRORS{'WARNING'}, 0, "invalid user ID argument was supplied, it is not an integer: '$user_id'");
		return;
	}
	
	if (!$no_cache && defined($self->{set_user_password}) && defined($self->{set_user_password}{$user_id})) {
		return $self->{set_user_password}{$user_id};
	}
	
	
	my $user_info = get_user_info($user_id, undef, $no_cache);
	if ($user_info) {
		my $user_login_id = $user_info->{unityid} || '<undefined>';
		my $user_affiliation_name = $user_info->{affiliation}{name} || '<undefined>';
		my $federated_linux_authentication = $user_info->{FEDERATED_LINUX_AUTHENTICATION};
		
		# Generate a reservation password if "standalone" (not using Kerberos authentication)
		if ($federated_linux_authentication) {
			notify($ERRORS{'DEBUG'}, 0, "random password should NOT be set for user ID $user_id ($user_login_id\@$user_affiliation_name), federated Linux authentication: $federated_linux_authentication");
			$self->{set_user_password}{$user_id} = 0;
		}
		else {
			notify($ERRORS{'DEBUG'}, 0, "random password SHOULD be set for user ID $user_id ($user_login_id\@$user_affiliation_name), federated Linux authentication: $federated_linux_authentication");
			$self->{set_user_password}{$user_id} = 1;
		}
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to definitively determine if random password should be set for user ID $user_id, user info could not be retrieved, assuming random password SHOULD be set, returning 1");
		$self->{set_user_password}{$user_id} = 1;
	}
	return $self->{set_user_password}{$user_id};
}

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

=head2 grant_connect_method_access

 Parameters  : user login id 
 Returns     : boolean
 Description : Edits the external_sshd_config. 
 					TODO - in next release pull this out into connect method modules.

=cut

sub grant_connect_method_access {
	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 $user_parameters = shift;

	if (!$user_parameters) {
		notify($ERRORS{'WARNING'}, 0, "unable to create user, user parameters argument was not provided");
		return;
	}
	elsif (!ref($user_parameters) || ref($user_parameters) ne 'HASH') {
		notify($ERRORS{'WARNING'}, 0, "unable to create user, argument provided is not a hash reference");
		return;
	}

	my $username = $user_parameters->{username};
	if (!defined($username)) {
		notify($ERRORS{'WARNING'}, 0, "failed to create user on $computer_node_name, argument hash does not contain a 'username' key:\n" . format_data($user_parameters));
		return;
	}

	my $uid = $user_parameters->{uid};
	if (!defined($uid)) {
		notify($ERRORS{'WARNING'}, 0, "failed to create user on $computer_node_name, argument hash does not contain a 'uid' key:\n" . format_data($user_parameters));
		return;
	}

	my $ssh_public_keys = $user_parameters->{ssh_public_keys};
	if (!defined($ssh_public_keys)) {
		notify($ERRORS{'OK'}, 0, "argument hash does not contain a 'ssh_public_keys' key:\n" . format_data($user_parameters));
	}

	my $home_directory_root = "/home";
	my $home_directory_path = "$home_directory_root/$username";
	my $home_directory_on_local_disk = $self->is_file_on_local_disk($home_directory_root);
	# Add user's public ssh identity keys if exists
	if ($ssh_public_keys) {
		my $ssh_directory_path = "$home_directory_path/.ssh";
		my $authorized_keys_file_path = "$ssh_directory_path/authorized_keys";
		
		# Determine if home directory is on a local device or network share
		# Only add keys to home directories that are local,
		# Don't add keys to network mounted filesystems
		if ($home_directory_on_local_disk) {
			# Create the .ssh directory
			$self->create_directory($ssh_directory_path);
			
			if ($self->append_text_file($authorized_keys_file_path, "$ssh_public_keys\n")) {
				notify($ERRORS{'DEBUG'}, 0, "added user's public SSH keys to $authorized_keys_file_path");
			}
			else {
				notify($ERRORS{'WARNING'}, 0, "failed to add user's public SSH keys to $authorized_keys_file_path");
			}

			if (!$self->set_file_owner($home_directory_path, $username, 'vcl', 1)) {
				notify($ERRORS{'WARNING'}, 0, "failed to set owner of user's home directory: $home_directory_path");
			}
		}
		else {
			notify($ERRORS{'DEBUG'}, 0, "user's public SSH keys not added to $authorized_keys_file_path, home directory is on a network share");
		}
	}


	# Append AllowUsers line to the end of the file
	my $external_sshd_config_file_path = '/etc/ssh/external_sshd_config';
	my $allow_users_line = "AllowUsers $username";
	if ($self->append_text_file($external_sshd_config_file_path, $allow_users_line)) {
		notify($ERRORS{'DEBUG'}, 0, "added line to $external_sshd_config_file_path: '$allow_users_line'");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to add line to $external_sshd_config_file_path: '$allow_users_line'");
		return;
	}
	
	$self->restart_service('ext_sshd') || return;

	# If ssh_public_keys add to authorized_keys

	return 1;
}

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

=head2 kill_process

 Parameters  : $pid, $signal (optional)
 Returns     : boolean
 Description : Kills a process on the computer.

=cut

sub kill_process {
	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 ($pid_argument, $signal) = @_;
	if (!defined($pid_argument)) {
		notify($ERRORS{'WARNING'}, 0, "PID argument was not specified");
		return;
	}
	
	my $computer_node_name = $self->data->get_computer_node_name();
	
	# Suicide prevention
	if ($pid_argument eq $PID) {
		notify($ERRORS{'WARNING'}, 0, "process $pid_argument not killed, it is the currently running process");
		return;
	}
	
	$signal = '9' unless defined $signal;
	$signal =~ s/^-+//g;
	
	my $command = "kill -$signal $pid_argument";
	my ($exit_status, $output) = $self->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to kill process $pid_argument on $computer_node_name");
		return;
	}
	elsif ($exit_status == 1 || grep(/no such process/i, @$output)) {
		notify($ERRORS{'DEBUG'}, 0, "process $pid_argument not running on $computer_node_name");
		return 1;
	}
	elsif ($exit_status != 0 || grep(/^kill:/i, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to kill process $pid_argument with signal $signal on $computer_node_name, command: '$command', exit status: $exit_status, output:\n" . join("\n", @$output));
		return 0;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "killed process $pid_argument with signal $signal on $computer_node_name");
		return 1;
	}
}

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

=head2 is_process_running

 Parameters  : $process_regex
 Returns     : array or hash reference
 Description : Determines if any processes matching the $process_regex
               argument are running on the computer. The $process_regex must be
               a valid Perl regular expression.
               
               The following command is used to determine if a process is
               running:
               ps -e -o pid,args | grep -P "$process_regex"
               
               The behavior is different than if the -P argument is not used.
               The following characters must be escaped with a backslash in
               order for a literal match to be found:
               | ( ) [ ] . +
               
               If these are not escaped, grep will interpret them as the
               corresponing regular expression operational character. For
               example:
               
               To match this literal string:
               |(foo)|
               Pass this:
               \|\(foo\)\|
               
               To match 'foo' or 'bar, pass this:
               (foo|bar)
               
               To match a pipe character ('|'), followed by either 'foo' or
               'bar, followed by another pipe character:
               |foo|
               Pass this:
               \|(foo|bar)\|
               
               The return value differs based on how this subroutine is called.
               If called in scalar context, a hash reference is returned. The
               hash keys are PIDs and the values are the full name of the
               process. If called in list context, an array is returned
               containing the PIDs.

=cut

sub is_process_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;
	}
	
	# Check the arguments
	my ($process_regex) = @_;
	if (!defined($process_regex)) {
		notify($ERRORS{'WARNING'}, 0, "process regex pattern argument was not specified");
		return;
	}
	
	my $computer_name = $self->data->get_computer_short_name();
	
	my $command = "ps -e -o pid,args | grep -P \"$process_regex\"";
	my ($exit_status, $output) = $self->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command on $computer_name to determine if process is running: $command");
		return;
	}
	
	my $processes_running = {};
	for my $line (@$output) {
		my ($pid, $process_name) = $line =~ /^\s*(\d+)\s*(.*[^\s])\s*/g;
		
		if (!defined($pid)) {
			notify($ERRORS{'DEBUG'}, 0, "ignoring line, it does not begin with a number: '$line'");
			next;
		}
		elsif ($pid eq $PID) {
			#notify($ERRORS{'DEBUG'}, 0, "ignoring line for the currently running process: $line");
			next;
		}
		elsif ($line =~ /grep -P/) {
			#notify($ERRORS{'DEBUG'}, 0, "ignoring line containing for this command: $line");
			next;
		}
		elsif ($line =~ /sh -c/) {
			# Ignore lines containing 'sh -c', probably indicating a duplicate process of a command run remotely
			#notify($ERRORS{'DEBUG'}, 0, "ignoring containing 'sh -c': $line");
			next;
		}
		else {
			#notify($ERRORS{'DEBUG'}, 0, "found matching process: $line");
			$processes_running->{$pid} = $process_name;
		}
	}
	
	my $process_count = scalar(keys %$processes_running);
	if ($process_count) {
		if (wantarray) {
			my @process_ids = sort keys %$processes_running;
			notify($ERRORS{'DEBUG'}, 0, "process is running on $computer_name, identifier: '$process_regex', returning array containing PIDs: @process_ids");
			return @process_ids;
		}
		else {
			notify($ERRORS{'DEBUG'}, 0, "process is running on $computer_name, identifier: '$process_regex', returning hash reference:\n" . format_data($processes_running));
			return $processes_running;
		}
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "process is NOT running on $computer_name, identifier: '$process_regex', command: $command");
		return;
	}
}

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

=head2 is_display_manager_running

 Parameters  : none
 Returns     : boolean
 Description : Checks if a display manager (GUI) is running on the computer.

=cut

sub is_display_manager_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 $computer_name = $self->data->get_computer_short_name();
	
	# Note: runlevel isn't reliable for all distros
	# On Ubuntu, it displays 2 even if the GUI is running
	
	my $process_pattern;
	
	# CentOS "Welcome" screen
	#  1700 /usr/bin/Xorg :9 -ac -nolisten tcp vt6 -br
	
	# ' 416 lightdm'
	# '2955 lightdm --session-child 12 21'
	$process_pattern .= '^\s*\d+\s+(kdm|lightdm)(\s|$)';
	
	# Gnome
	# 1870 /usr/sbin/gdm-binary -nodaemon
	# 1898 /usr/libexec/gdm-simple-slave --display-id /org/gnome/DisplayManager/Display1
	# 1901 /usr/bin/Xorg :0 -br -verbose -audit 4 -auth /var/run/gdm/auth-for-gdm-laIZj5/database -nolisten tcp vt1
	# 1989 /usr/bin/gnome-session --autostart=/usr/share/gdm/autostart/LoginWindow/
	$process_pattern .= '|(gnome-session|gdm-binary)';
	
	# ' 2891 /usr/bin/X -core :0 -seat seat0 -auth /var/run/lightdm/root/:0 -nolisten tcp vt7 -novtswitch'
	$process_pattern .= '|bin\/X';
	
	$process_pattern = "($process_pattern)";
	
	my $process_info = $self->is_process_running($process_pattern);
	if ($process_info) {
		notify($ERRORS{'DEBUG'}, 0, "display manager is running on $computer_name:\n" . format_data($process_info));
		return 1;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "display manager is not running on $computer_name");
		return 0;
	}
}

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

=head2 generate_ssh_private_key_file

 Parameters  : $private_key_file_path, $type (optional), $bits (optional), $comment (optional), $passphrase, $options (optional)
 Returns     : boolean
 Description : Calls ssh-keygen or dropbearkey to generate an SSH private key
               file.

=cut

sub generate_ssh_private_key_file {
	my $self = shift;
	if (ref($self) !~ /VCL::Module::OS/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my ($private_key_file_path, $type, $bits, $comment, $passphrase, $options) = @_;
	if (!$private_key_file_path) {
		notify($ERRORS{'WARNING'}, 0, "private key file path argument was not specified");
		return;
	}
	$type = 'rsa' unless $type;
	$passphrase = '' unless $passphrase;
	
	if (defined($comment)) {
		$comment =~ s/\\*(["])/\\"$1/g;
	}
	
	my $computer_name = $self->data->get_computer_short_name();
	
	# Make sure the file does not already exist
	if ($self->file_exists($private_key_file_path)) {
		notify($ERRORS{'WARNING'}, 0, "failed to generate SSH key, file already exists on $computer_name: $private_key_file_path");
		return;
	}
	
	if ($self->command_exists('ssh-keygen')) {
		if ($self->_generate_ssh_private_key_file_helper($private_key_file_path, $type, $bits, $comment, $passphrase, $options, 'ssh-keygen')) {
			return 1;
		}
	}
	if ($self->command_exists('dropbearkey')) {
		if ($self->_generate_ssh_private_key_file_helper($private_key_file_path, $type, $bits, $comment, $passphrase, $options, 'dropbearkey')) {
			return 1;
		}
	}
	
	if (ref($self) =~ /ManagementNode/) {
		notify($ERRORS{'WARNING'}, 0, "failed to generate SSH key on $computer_name: $private_key_file_path");
		return;
	}
	
	my $mn_temp_file_path = mktemp($computer_name . 'XXXXXX');
	notify($ERRORS{'DEBUG'}, 0, "attempting to create SSH private key file on this management node ($mn_temp_file_path) and copy it to $computer_name ($private_key_file_path)");
	my $result = $self->mn_os->generate_ssh_private_key_file($mn_temp_file_path, $type, $bits, $comment, $passphrase, $options);
	if (!$result) {
		notify($ERRORS{'WARNING'}, 0, "failed to create SSH private key file on this management node and copy it to $private_key_file_path on $computer_name");
		$self->mn_os->delete_file($mn_temp_file_path);
		return;
	}
	
	if (!$self->copy_file_to($mn_temp_file_path, $private_key_file_path)) {
		notify($ERRORS{'WARNING'}, 0, "create SSH private key file on this management node but failed to copy it to $private_key_file_path on $computer_name");
		$self->mn_os->delete_file($mn_temp_file_path);
		return;
	}
	else {
		$self->mn_os->delete_file($mn_temp_file_path);
		notify($ERRORS{'OK'}, 0, "created SSH private key file on this management and copied it to $private_key_file_path on $computer_name");
		return 1;
	}
}

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

=head2 _generate_ssh_private_key_file_helper

 Parameters  : $private_key_file_path, $type, $bits, $comment, $passphrase, $options, $utility
 Returns     : boolean
 Description : Calls ssh-keygen to generate an SSH private key file.

=cut

sub _generate_ssh_private_key_file_helper {
	my $self = shift;
	if (ref($self) !~ /VCL::Module::OS/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my ($private_key_file_path, $type, $bits, $comment, $passphrase, $options, $utility) = @_;
	if (!defined($utility)) {
		notify($ERRORS{'WARNING'}, 0, "utility argument was not supplied");
		return;
	}
	
	my $computer_name = $self->data->get_computer_short_name();
	
	my $command;
	if ($utility eq 'ssh-keygen') {
		$command = "ssh-keygen -t $type -f \"$private_key_file_path\" -N \"$passphrase\"";
		$command .= " -b $bits" if (defined($bits) && length($bits));
		$command .= " $options" if (defined($options) && length($options));
		$command .= " -C \"$comment\"" if (defined($comment) && length($comment));
	}
	elsif ($utility eq 'dropbearkey') {
		$command = "dropbearkey -t $type -f \"$private_key_file_path\"";
		$command .= " -s $bits" if (defined($bits) && length($bits));
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "invalid utility argument provided: '$utility', it must either be 'ssh-keygen' or 'dropbearkey'");
		return;
	}
	
	my ($exit_status, $output) = $self->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to generate SSH key using $utility on $computer_name: $command");
		return;
	}
	elsif ($exit_status ne '0') {
		notify($ERRORS{'WARNING'}, 0, "failed to generate SSH key using $utility on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
		return;
	}
	else {
		notify($ERRORS{'OK'}, 0, "generated SSH key using $utility on $computer_name: $private_key_file_path, command: $command");
		return 1;
	}
}

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

=head2 generate_ssh_public_key_file

 Parameters  : $private_key_file_path, $public_key_file_path, $comment (optional)
 Returns     : boolean
 Description : Calls ssh-keygen to retrieve the corresponding SSH public key
               from a private key file and generates a file containing the
               public key.

=cut

sub generate_ssh_public_key_file {
	my $self = shift;
	if (ref($self) !~ /VCL::Module::OS/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my ($private_key_file_path, $public_key_file_path, $comment) = @_;
	if (!$private_key_file_path) {
		notify($ERRORS{'WARNING'}, 0, "private key file path argument was not specified");
		return;
	}
	if (!$public_key_file_path) {
		notify($ERRORS{'WARNING'}, 0, "public key file path argument was not specified");
		return;
	}
	
	my $computer_name = $self->data->get_computer_short_name();
	
	# Make sure the private key file exists
	if (!$self->file_exists($private_key_file_path)) {
		notify($ERRORS{'WARNING'}, 0, "failed to generate SSH public key file, private key file does not exist on $computer_name: $private_key_file_path");
		return;
	}
	
	# Make sure the public key file does not exist
	if ($self->file_exists($public_key_file_path)) {
		notify($ERRORS{'WARNING'}, 0, "failed to create SSH public key file, public key file already exists on $computer_name: $public_key_file_path");
		return;
	}
	
	my $public_key_string = $self->get_ssh_public_key_string($private_key_file_path, $comment);
	if (!$public_key_string) {
		notify($ERRORS{'WARNING'}, 0, "failed to create SSH public key file: $public_key_file_path, public key string could not be retrieved from private key file: $private_key_file_path");
		return;
	}
	
	if ($self->create_text_file($public_key_file_path, $public_key_string)) {
		notify($ERRORS{'DEBUG'}, 0, "created SSH public key file: $public_key_file_path");
		return 1;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to create SSH public key file: $public_key_file_path");
		return;
	}
}

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

=head2 get_ssh_public_key_string

 Parameters  : $private_key_file_path, $comment (optional)
 Returns     : boolean
 Description : Extracts the SSH public key from a private key file.

=cut

sub get_ssh_public_key_string {
	my $self = shift;
	if (ref($self) !~ /VCL::Module::OS/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my ($private_key_file_path, $comment) = @_;
	if (!$private_key_file_path) {
		notify($ERRORS{'WARNING'}, 0, "private key file path argument was not specified");
		return;
	}
	
	my $computer_name = $self->data->get_computer_short_name();
	
	# Make sure the private key file exists
	if (!$self->file_exists($private_key_file_path)) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve SSH public key, private key file does not exist on $computer_name: $private_key_file_path");
		return;
	}
	
	my $public_key_string;
	if ($self->command_exists('ssh-keygen')) {
		$public_key_string = $self->_get_ssh_public_key_string_helper($private_key_file_path, 'ssh-keygen');
	}
	if (!$public_key_string && $self->command_exists('dropbearkey')) {
		$public_key_string = $self->_get_ssh_public_key_string_helper($private_key_file_path, 'dropbearkey');
	}
	if ($public_key_string) {
		#if ($comment) {
		#	$public_key_string =~ s/(ssh-[^\s]+\s[^\s=]+).*$/$1 $comment/g;
		#}
		return $public_key_string;
	}
	
	if (ref($self) =~ /ManagementNode/) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve SSH public key from private key file on $computer_name: $private_key_file_path");
		return;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "attempting to copy SSH private key file $private_key_file_path from $computer_name and extract the public key on this management node");
	}
	
	my ($mn_temp_file_handle, $mn_temp_file_path) = tempfile(SUFFIX => '.key', UNLINK => 1);
	if (!$self->copy_file_from($private_key_file_path, $mn_temp_file_path)) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve SSH public key from private key file on $computer_name: $private_key_file_path, failed to copy temp file to management node");
		$self->mn_os->delete_file($mn_temp_file_path);
		return;
	}
	$self->mn_os->set_file_permissions($mn_temp_file_path, '0400');
	
	$public_key_string = $self->mn_os->get_ssh_public_key_string($mn_temp_file_path, $comment);
	$self->mn_os->delete_file($mn_temp_file_path);
	return $public_key_string;
}

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

=head2 _get_ssh_public_key_string_helper

 Parameters  : $private_key_file_path, $utility
 Returns     : boolean
 Description : 

=cut

sub _get_ssh_public_key_string_helper {
	my $self = shift;
	if (ref($self) !~ /VCL::Module::OS/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my ($private_key_file_path, $utility) = @_;
	if (!$private_key_file_path) {
		notify($ERRORS{'WARNING'}, 0, "private key file path argument was not specified");
		return;
	}
	elsif (!$utility) {
		notify($ERRORS{'WARNING'}, 0, "utility argument (ssh-keygen or dropbearkey) was not specified");
		return;
	}
	
	my $computer_name = $self->data->get_computer_short_name();
	
	my $command;
	if ($utility eq 'ssh-keygen') {
		$command = "ssh-keygen -y -f \"$private_key_file_path\"";
	}
	elsif ($utility eq 'dropbearkey') {
		$command = "/bin/dropbearkey -f \"$private_key_file_path\" -y";
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "invalid utility argument provided: '$utility', it must either be 'ssh-keygen' or 'dropbearkey'");
		return;
	}
	
	my ($exit_status, $output) = $self->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to retrieve SSH public key string using $utility from $private_key_file_path on $computer_name");
		return;
	}
	elsif ($exit_status ne 0) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve SSH public key string using $utility from $private_key_file_path on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
		return;
	}
	
	my ($ssh_public_key_string) = grep(/^ssh-.*/, @$output);
	if ($ssh_public_key_string) {
		notify($ERRORS{'OK'}, 0, "retrieved SSH public key string using $utility from $private_key_file_path on $computer_name:\n$ssh_public_key_string");
		return $ssh_public_key_string;
	}
	else {
		notify($ERRORS{'OK'}, 0, "failed to retrieved SSH public key string using $utility from $private_key_file_path on $computer_name, output does not contain a line beginning with 'ssh-', command:\n$command\noutput:\n" . join("\n", @$output));
		return;
	}
}

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

=head2 install_package

 Parameters  : $package_name, $timeout_seconds (optional)
 Returns     : boolean
 Description : Installs a Linux package using yum.

=cut

sub install_package {
	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 ($package_name, $timeout_seconds) = @_;
	if (!$package_name) {
		notify($ERRORS{'WARNING'}, 0, "package name argument was not supplied");
		return;
	}
	
	my $computer_name = $self->data->get_computer_node_name();
	
	if (!$self->command_exists('yum')) {
		notify($ERRORS{'WARNING'}, 0, "failed to install $package_name on $computer_name, yum command is not available");
		return;
	}
	
	$timeout_seconds = 120 unless $timeout_seconds;
	
	# Delete service info in case package adds a service that was previously detected as not existing
	$self->_delete_cached_service_info();
	
	my $command = "yum install -q -y $package_name";
	notify($ERRORS{'DEBUG'}, 0, "attempting to install $package_name using yum on $computer_name, timeout seconds: $timeout_seconds");
	my ($exit_status, $output) = $self->execute({
		'command' => $command,
		'display_output' => 0,
		'timeout_seconds' => $timeout_seconds,
	});
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to install $package_name using yum on $computer_name");
		return;
	}
	elsif ($exit_status ne 0) {
		notify($ERRORS{'WARNING'}, 0, "failed to install $package_name using yum on $computer_name, exit status: $exit_status, command: '$command', output:\n" . join("\n", @$output));
		return 0;
	}
	elsif (grep(/$package_name.+already installed/, @$output)) {
		notify($ERRORS{'DEBUG'}, 0, "$package_name is already installed on $computer_name, command: '$command', output:\n" . join("\n", @$output));
		return 1;
	}
	else {
		notify($ERRORS{'OK'}, 0, "installed $package_name using yum on $computer_name, command: '$command', output:\n" . join("\n", @$output));
		return 1;
	}
}

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

=head2 nfs_mount_share

 Parameters  : $remote_nfs_share, $local_mount_directory, $options
 Returns     : boolean
 Description : Mounts an NFS share on the computer. The $remote_nfs_share
               argument must be in the for used by the mount command:
               <hostname|IP>:/path-to-share
               
               The $local_mount_directory argument must specify a directory.
               This directory will be created if it does not already exist.
               
               The $options argument allows NFS mount options to be specified
               such as:
               rsize=1048576,wsize=1048576,vers=3
               
               A 'retry=0' option is included in the mount command if $options
               does not explicitly include it. This causes a single mount
               attempt to be made rather than the default behavior of trying for
               up to 2 minutes. This is required because the VCL code will
               timeout before the Linux mount command gives up. As a result, VCL
               does not receive the error message. This prevents automatic
               corrective actions to happen such as creating the remote
               directory.
               
               The $options string must be formatted correctly and is passed
               directly to the mount command.

=cut

sub nfs_mount_share {
	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 ($remote_nfs_share, $local_mount_directory, $ignore_missing_remote_directory_error, $nfs_options, $is_retry_attempt) = @_;
	if (!defined($remote_nfs_share)) {
		notify($ERRORS{'WARNING'}, 0, "remote target argument was not supplied");
		return;
	}
	elsif (!defined($local_mount_directory)) {
		notify($ERRORS{'WARNING'}, 0, "local directory path argument was not supplied");
		return;
	}
	
	my $computer_name = $self->data->get_computer_node_name();
	
	# Try to repair NFS client if 1st attempt failed
	if ($is_retry_attempt) {
		# Check if nfs-utils is installed, if not, try to install it
		# Error looks like this if nfs-utils is not installed:
		#    mount: wrong fs type, bad option, bad superblock on 10.1.2.3:/nfs,
		#    missing codepage or helper program, or other error
		#    (for several filesystems (e.g. nfs, cifs) you might
		#    need a /sbin/mount.<type> helper program)
		#    In some cases useful info is found in syslog - try
		#    dmesg | tail  or so
		if (!$self->command_exists('mount.nfs')) {
			if (ref($self) =~ /Ubuntu/) {
				$self->install_package('nfs-common');
			}
			else {
				$self->install_package('nfs-utils');
			}
		}
		
		# Check if the rpcbind service exists, if not, try to install it
		# Mount may fail if rpcbind service is not installed and running:
		#    mount.nfs: rpc.statd is not running but is required for remote locking.
		#    mount.nfs: Either use '-o nolock' to keep locks local, or start statd.
		#    mount.nfs: an incorrect mount option was specified
		$self->install_package('rpcbind') if !$self->service_exists('rpcbind');
		
		# Try to start the service
		$self->start_service('rpcbind') if $self->service_exists('rpcbind');
	}
	
	# Create the local mount point directory if it does not exist
	my $local_mount_directory_previously_existed = $self->file_exists($local_mount_directory);
	if (!$local_mount_directory_previously_existed && !$self->create_directory($local_mount_directory)) {
		notify($ERRORS{'WARNING'}, 0, "unable to mount $remote_nfs_share on $computer_name, failed to create directory: $local_mount_directory");
		return;
	}
	
	my $mount_command = "mount -t nfs $remote_nfs_share \"$local_mount_directory\" -v";
	if ($nfs_options) {
		# Add retry=0 if it wasn't explicitly specified in the argument
		if ($nfs_options =~ /retry/) {
			$mount_command .= " -o $nfs_options";
		}
		else {
			$mount_command .= " -o retry=0,$nfs_options";
		}
	}
	else {
		$mount_command .= " -o retry=0";
	}
	
	# Save return value if error is encountered and don't return immediately
	# Facilitates a single call to clean up local directory just created if it didn't previously exist
	my $return_value;
	
	notify($ERRORS{'DEBUG'}, 0, "attempting to mount NFS share on $computer_name: $mount_command");
	my ($mount_exit_status, $mount_output) = $self->execute({
		command => $mount_command,
		timeout_seconds => 10,
		max_attempts => 2,
	});
	if (!defined($mount_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to mount NFS share on $computer_name: $mount_command");
		$return_value = undef;
	}
	elsif ($mount_exit_status eq 0) {
		notify($ERRORS{'OK'}, 0, "mounted NFS share on $computer_name: $remote_nfs_share --> $local_mount_directory");
		
		# Add the share to /etc/fstab
		$self->add_fstab_nfs_mount($remote_nfs_share, $local_mount_directory);
		
		return 1;
	}
	elsif (grep(/already mounted/, @$mount_output)) {
		# mount.nfs: /mnt/mymountpoint is busy or already mounted
		if ($self->is_nfs_share_mounted($remote_nfs_share, $local_mount_directory)) {
			return 1;
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to mount NFS share on $computer_name: $remote_nfs_share --> $local_mount_directory, mount command output indicates 'already mounted' but failed to verify mount in /proc/mounts, mount command: '$mount_command', exit status: $mount_exit_status, mount output:\n" . join("\n", @$mount_output));
			$return_value = undef;
		}
	}
	elsif (grep(/(No such file or directory)/, @$mount_output)) {
		# mount.nfs: mount(2): No such file or directory
		# mount.nfs: mounting <hostname>:/<remote directory> failed, reason given by server: No such file or directory
		if ($ignore_missing_remote_directory_error) {
			notify($ERRORS{'DEBUG'}, 0, "unable to mount NFS share on $computer_name because remote directory does not exist: $remote_nfs_share, returning 0");
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "unable to mount NFS share on $computer_name because remote directory does not exist: $remote_nfs_share, returning 0, command: '$mount_command', exit status: $mount_exit_status, output:\n" . join("\n", @$mount_output));
		}
		$return_value = 0;
	}
	elsif (grep(/(Invalid argument|incorrect mount option|Usage:)/, @$mount_output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to mount NFS share on $computer_name: $remote_nfs_share --> $local_mount_directory, command: '$mount_command', exit status: $mount_exit_status, output:\n" . join("\n", @$mount_output));
		$return_value = undef;
	}
	elsif ($is_retry_attempt) {
		notify($ERRORS{'WARNING'}, 0, "failed to mount NFS share on $computer_name on 2nd attempt: $remote_nfs_share --> $local_mount_directory, command: '$mount_command', exit status: $mount_exit_status, output:\n" . join("\n", @$mount_output));
		$return_value = undef;
	}
	else {
		notify($ERRORS{'OK'}, 0, "failed to mount NFS share on $computer_name on 1st attempt: $remote_nfs_share --> $local_mount_directory, command: '$mount_command', exit status: $mount_exit_status, output:\n" . join("\n", @$mount_output));
	}
	
	# Clean up local directory if it didn't previously exist
	if (!$local_mount_directory_previously_existed) {
		my @local_mount_directory_files = $self->find_files($local_mount_directory, '*', 1);
		if (@local_mount_directory_files) {
			notify($ERRORS{'WARNING'}, 0, "local mount directory just created will NOT be deleted: $local_mount_directory, NFS mount seemed to have failed but directory is not empty:\n" . join("\n", @local_mount_directory_files));
		}
		else {
			notify($ERRORS{'DEBUG'}, 0, "local mount directory just created will be deleted: $local_mount_directory");
			$self->delete_file($local_mount_directory);
		}
	}
	
	if ($is_retry_attempt) {
		return $return_value;
	}
	else {
		# Try to mount the NFS share again, set retry flag to avoid endless loop
		return $self->nfs_mount_share($remote_nfs_share, $local_mount_directory, $ignore_missing_remote_directory_error, $nfs_options, 1);
	}
}

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

=head2 nfs_unmount_share

 Parameters  : $local_mount_directory
 Returns     : boolean
 Description : Unmounts an NFS share on the computer.

=cut

sub nfs_unmount_share {
	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 ($local_mount_directory) = @_;
	if (!defined($local_mount_directory)) {
		notify($ERRORS{'WARNING'}, 0, "local directory path argument was not supplied");
		return;
	}
	
	my $computer_name = $self->data->get_computer_node_name();
	
	my $result;
	
	# Make sure lines are removed from /etc/fstab regardless of unmount result
	(my $local_mount_directory_pattern = $local_mount_directory) =~ s/\//\\\//g;
	$local_mount_directory_pattern = '[\s\t]' . $local_mount_directory_pattern . '[\s\t]';
	$self->remove_matching_fstab_lines($local_mount_directory_pattern);
	
	my $umount_command = "umount -v \"$local_mount_directory\"";
	my ($umount_exit_status, $umount_output) = $self->execute({
		command => $umount_command,
		timeout_seconds => 30,
		max_attempts => 1,
	});
	if (!defined($umount_exit_status)) {
		notify($ERRORS{'CRITICAL'}, 0, "failed to execute command to umount NFS share on $computer_name: $umount_command");
		return;
	}
	elsif ($umount_exit_status eq 0 || grep(/\sumounted/, @$umount_output)) {
		notify($ERRORS{'OK'}, 0, "unmounted NFS share on $computer_name: $local_mount_directory, output:\n" . join("\n", @$umount_output));
		$result = 1;
	}
	elsif (grep(/(not mounted|not found|Could not find)/i, @$umount_output)) {
		# umount: /nfs-share: not found
		# Could not find /nfs-share in mtab
		notify($ERRORS{'OK'}, 0, "NFS share is not mounted on $computer_name: $local_mount_directory");
		$result = 1;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "lazy unmount will be attempted after failing to perform normal NFS unmount on $computer_name: $local_mount_directory, command: '$umount_command', exit status: $umount_exit_status, output:\n" . join("\n", @$umount_output));
		my $umount_lazy_command = "umount -v -l \"$local_mount_directory\"";
		my ($umount_lazy_exit_status, $umount_lazy_output) = $self->execute({
			command => $umount_lazy_command,
			timeout_seconds => 30,
			max_attempts => 1,
		});
		
		if ($self->is_nfs_share_mounted('.*', $local_mount_directory)) {
			notify($ERRORS{'WARNING'}, 0, "failed to unmount NFS share on $computer_name: $local_mount_directory, command: '$umount_command', exit status: $umount_exit_status, output:\n" . join("\n", @$umount_output));
			$result = 0;
		}
		else {
			notify($ERRORS{'OK'}, 0, "lazy unmounted of NFS share on $computer_name: $local_mount_directory");
			$result = 1;
		}
	}
	
	# Clean up local directory if it didn't previously exist
	my @local_mount_directory_files = $self->find_files($local_mount_directory, '*', 1);
	if (@local_mount_directory_files) {
		notify($ERRORS{'WARNING'}, 0, "local mount directory will NOT be deleted: $local_mount_directory, directory is not empty:\n" . join("\n", @local_mount_directory_files));
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "unmounted directory is empty and will be deleted: $local_mount_directory");
		$self->delete_file($local_mount_directory);
	}
	
	return $result;
}

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

=head2 is_nfs_share_mounted

 Parameters  : $remote_nfs_share, $local_mount_directory
 Returns     : boolean
 Description : Checks if an NFS share is mounted on the computer matching both
               the remote NFS share path and local mount point directory
               arguments.

=cut

sub is_nfs_share_mounted {
	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 ($remote_nfs_share, $local_mount_directory) = @_;
	if (!defined($remote_nfs_share)) {
		notify($ERRORS{'WARNING'}, 0, "remote target argument was not supplied");
		return;
	}
	elsif (!defined($local_mount_directory)) {
		notify($ERRORS{'WARNING'}, 0, "local directory path argument was not supplied");
		return;
	}
	
	my $computer_name = $self->data->get_computer_node_name();
	
	if ($self->get_nfs_mount_string($remote_nfs_share, $local_mount_directory)) {
		notify($ERRORS{'DEBUG'}, 0, "NFS share is mounted on $computer_name: $remote_nfs_share --> $local_mount_directory");
		return 1;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "NFS share is NOT mounted on $computer_name: $remote_nfs_share --> $local_mount_directory");
		return 0;
	}
}

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

=head2 get_nfs_mount_strings

 Parameters  : none
 Returns     : array
 Description : Retrieves the contents of /proc/mounts and extracts all strings
               that contain 'nfs' or 'nfs4'. An array containing the raw
               /proc/mounts strings is returned.

=cut

sub get_nfs_mount_strings {
	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_name = $self->data->get_computer_node_name();
	
	my $command = "cat /proc/mounts";
	my ($exit_status, $output) = $self->execute($command);
	if (!defined($exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command on $computer_name: $command");
		return;
	}
	
	my @mount_strings;
	for my $line (@$output) {
		if ($line !~ /\snfs\d*\s/) {
			next;
		}
		
		push @mount_strings, $line;
	}
	
	notify($ERRORS{'DEBUG'}, 0, "retrieved NFS mount strings from $computer_name:\n" . join("\n", @mount_strings));
	return @mount_strings;
}

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

=head2 get_nfs_mount_string

 Parameters  : $remote_nfs_share, $local_mount_directory
 Returns     : string
 Description : Examines the contents of /proc/mounts and attempts to locate a
               line matching the arguments. If found, the line is returned which
               may be used in /etc/fstab. If not found, 0 is returned. Undefined
               is returned if an error occurs.

=cut

sub get_nfs_mount_string {
	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 ($remote_nfs_share, $local_mount_directory) = @_;
	if (!defined($remote_nfs_share)) {
		notify($ERRORS{'WARNING'}, 0, "remote target argument was not supplied");
		return;
	}
	elsif (!defined($local_mount_directory)) {
		notify($ERRORS{'WARNING'}, 0, "local directory path argument was not supplied");
		return;
	}
	
	# Remove trailing slashes for consistent comparison
	$remote_nfs_share =~ s/\/+$//;
	$local_mount_directory =~ s/\/+$//;
	
	# If the NFS share or local directory contain a space, the octal value will appear in /proc/mounts
	$remote_nfs_share =~ s/ /\\\\040/g;
	$local_mount_directory =~ s/ /\\\\040/g;
	
	my $computer_name = $self->data->get_computer_node_name();
	
	my $command = "cat /proc/mounts | grep ' nfs'";
	my ($exit_status, $output) = $self->execute($command);
	if (!defined($exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command on $computer_name: $command");
		return;
	}
	
	for my $line (@$output) {
		# 10.1.2.3:/share/data /tmp/data nfs4 rw,relatime,vers=4,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=10.25.10.194,minorversion=0,local_lock=none,addr=10.1.2.3 0 0
		if ($line =~ m|^$remote_nfs_share\/?\s+$local_mount_directory\/?\s|) {
			notify($ERRORS{'DEBUG'}, 0, "found NFS share line in /proc/mounts on $computer_name: $remote_nfs_share --> $local_mount_directory\n$line");
			return $line;
		}
	}
	
	notify($ERRORS{'DEBUG'}, 0, "/proc/mounts on $computer_name does NOT contain a line matching NFS share: $remote_nfs_share --> $local_mount_directory\n" . join("\n", @$output));
	return 0;
}

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

=head2 add_fstab_nfs_mount

 Parameters  : $remote_nfs_share, $local_mount_directory
 Returns     : boolean
 Description : Adds a line to /etc/fstab for an existing NFS mount. The share
               must be mounted prior to calling this subroutine.

=cut

sub add_fstab_nfs_mount {
	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 ($remote_nfs_share, $local_mount_directory) = @_;
	if (!defined($remote_nfs_share)) {
		notify($ERRORS{'WARNING'}, 0, "remote target argument was not supplied");
		return;
	}
	elsif (!defined($local_mount_directory)) {
		notify($ERRORS{'WARNING'}, 0, "local directory path argument was not supplied");
		return;
	}
	
	my $computer_name = $self->data->get_computer_node_name();
	
	my $nfs_mount_string = $self->get_nfs_mount_string($remote_nfs_share, $local_mount_directory);
	if (!$nfs_mount_string) {
		notify($ERRORS{'WARNING'}, 0, "unable to add NFS mount line to /etc/fstab, NFS share is not mounted: $remote_nfs_share --> $local_mount_directory");
		return;
	}
	
	# Add a trailing comment to identify it was added automatically
	my $timestamp = POSIX::strftime("%Y-%m-%d %H-%M-%S", localtime);
	$nfs_mount_string .= "\t# Added by VCL ($timestamp)";
	
	# Remove existing line matching the local mount directory followed by "nfs" to avoid duplicate lines
	$self->remove_matching_fstab_lines("$local_mount_directory nfs");
	
	my @fstab_lines = $self->get_file_contents('/etc/fstab');
	push @fstab_lines, $nfs_mount_string;
	my $new_fstab_contents = join("\n", @fstab_lines);
	
	$self->copy_file('/etc/fstab', "/tmp/fstab.$timestamp");
	
	if ($self->create_text_file('/etc/fstab', $new_fstab_contents)) {
		notify($ERRORS{'OK'}, 0, "added line to /etc/fstab on $computer_name:\n$nfs_mount_string");
		return 1;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to add line to /etc/fstab on $computer_name:\n$nfs_mount_string");
		return;
	}
}

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

=head2 remove_matching_fstab_lines

 Parameters  : $regex_pattern
 Returns     : boolean
 Description : Removes all lines from /etc/fstab matching the pattern.

=cut

sub remove_matching_fstab_lines {
	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 ($regex_pattern) = @_;
	if (!defined($regex_pattern)) {
		notify($ERRORS{'WARNING'}, 0, "pattern argument was not supplied");
		return;
	}
	
	my $computer_name = $self->data->get_computer_node_name();
	
	my $updated_fstab_contents;
	
	my @matching_lines;
	my @fstab_lines = $self->get_file_contents('/etc/fstab');
	for my $fstab_line (@fstab_lines) {
		(my $fstab_line_cleaned = $fstab_line) =~ s/\\040/ /g;
		
		if ($fstab_line =~ m|$regex_pattern| || $fstab_line_cleaned =~ m|$regex_pattern|) {
			push @matching_lines, $fstab_line;
			notify($ERRORS{'DEBUG'}, 0, "removing lines from /etc/fstab matching pattern: $regex_pattern\n$fstab_line");
			next;
		}
		$updated_fstab_contents .= "$fstab_line\n";
	}
	
	my $matching_line_count = scalar(@matching_lines);
	if (!$matching_line_count) {
		notify($ERRORS{'DEBUG'}, 0, "/etc/fstab does not contain any lines matching pattern: $regex_pattern");
		return 1;
	}
	notify($ERRORS{'DEBUG'}, 0, "removing $matching_line_count line" . ($matching_line_count == 1 ? '' : 's') . " from /etc/fstab on $computer_name:\n" . join("\n", @matching_lines));
	
	# Save a backup
	my $timestamp = POSIX::strftime("%Y-%m-%d_%H-%M-%S\n", localtime);
	$self->copy_file('/etc/fstab', "/tmp/fstab.$timestamp");
	
	return $self->create_text_file('/etc/fstab', $updated_fstab_contents);
}

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

=head2 update_resolv_conf

 Parameters  : @public_dns_servers (optional)
 Returns     : boolean
 Description : Updates /etc/resolv.conf on the computer. Existing nameserver
               lines are removed and new nameserver lines are added based on the
               public DNS servers configured for the management node.

=cut

sub update_resolv_conf {
	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_name = $self->data->get_computer_short_name();
	my $public_ip_configuration = $self->data->get_management_node_public_ip_configuration();
	my @mn_dns_servers = shift || $self->data->get_management_node_public_dns_servers();

	my @server_request_dns_servers = $self->data->get_server_request_dns_servers();

	my @dns_servers;
	if (@server_request_dns_servers) {
		@dns_servers = @server_request_dns_servers;
		notify($ERRORS{'DEBUG'}, 0, "server request specific DNS servers will be statically set on $computer_name: " . join(", ", @dns_servers));
	}
	elsif ($public_ip_configuration =~ /static/i && @mn_dns_servers) {
		@dns_servers = @mn_dns_servers;
		notify($ERRORS{'DEBUG'}, 0, "management node IP configuration set to $public_ip_configuration, management node DNS servers will be statically set on $computer_name: " . join(", ", @dns_servers));
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "$computer_name not configured to use static DNS servers:\n" .
			"management node IP configuration               : $public_ip_configuration\n" .
			"management node DNS servers configured         : " . (@mn_dns_servers ? 'yes' : 'no')
		);
		return;
	}

	my $resolv_conf_path = "/etc/resolv.conf";
	
	my @resolv_conf_lines_existing = $self->get_file_contents($resolv_conf_path);
	my @resolv_conf_lines_new;
	for my $line (@resolv_conf_lines_existing) {
		if ($line =~ /\sVCL/) {
			last;
		}
		elsif ($line !~ /^\s*nameserver/) {
			push @resolv_conf_lines_new, $line;
		}
	}

	# Add a comment marking what was added by VCL
	my $timestamp = POSIX::strftime("%m-%d-%Y %H:%M:%S", localtime);
	push @resolv_conf_lines_new, "# $timestamp: The following was added by VCL";
	
	# Add a nameserver line for each configured DNS server
	for my $public_dns_server (@dns_servers) {
		push @resolv_conf_lines_new, "nameserver $public_dns_server";
	}

	my $resolv_conf_contents_new = join("\n", @resolv_conf_lines_new);
	if ($self->create_text_file($resolv_conf_path, $resolv_conf_contents_new)) {
		notify($ERRORS{'DEBUG'}, 0, "updated $resolv_conf_path on $computer_name:\n$resolv_conf_contents_new");
	}
	else {
		return 0;
	}
}

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

=head2 get_product_name

 Parameters  : none
 Returns     : string
 Description : Retrieves the name of the Linux distribution from
               /etc/redhat-release. If this file does not exist, null is
               returned.

=cut

sub get_product_name {
	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;
	}
	
	return $self->{product_name} if defined($self->{product_name});
	
	my $computer_name = $self->data->get_computer_short_name();
	
	my $release_file_path = '/etc/redhat-release';
	if (!$self->file_exists($release_file_path)) {
		notify($ERRORS{'WARNING'}, 0, "unable to determine Linux distribution name installed on $computer_name, file does not exist: $release_file_path");
		return;
	}
	
	my @release_file_lines = $self->get_file_contents($release_file_path);
	if (!@release_file_lines) {
		notify($ERRORS{'WARNING'}, 0, "unable to determine Linux distribution name installed on $computer_name, failed to retrieve contents of: $release_file_path");
		return;
	}
	
	# In case there are multiple lines, get the first one with a word character
	my ($product_name) = grep(/\w/, @release_file_lines);
	if (!$product_name) {
		notify($ERRORS{'WARNING'}, 0, "unable to determine Linux distribution name installed on $computer_name, $release_file_path does not contain a line with a word character, contents:\n" . join("\n", @release_file_lines));
		return;
	}
	
	$self->{product_name} = $product_name;
	notify($ERRORS{'OK'}, 0, "determined Linux distribution name installed on $computer_name: '$self->{product_name}'");
	return $self->{product_name};
}


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

=head2 get_management_node_connected_ip_address

 Parameters  : none
 Returns     : string
 Description : Returns the management node's IP address used to connect to the
               remote computer.

=cut

sub get_management_node_connected_ip_address {
	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_name = $self->data->get_computer_node_name();
	
	my $command = 'echo $SSH_CONNECTION';
	my ($exit_status, $output) = $self->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to determine IP address management node used to connect to $computer_name: $command");
		return;
	}
	elsif ($exit_status ne '0') {
		notify($ERRORS{'WARNING'}, 0, "failed to determine IP address management node used to connect to $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
		return;
	}
	
	# There should be a single line formatted at:
	# <source IP> <source port> <remote IP> <remote port>
	# Example:
	# 10.25.7.2 43570 10.25.10.194 22
	for my $line (@$output) {
		my ($source_ip) = $line =~ /^([\d\.]+)\s/;
		if ($source_ip) {
			notify($ERRORS{'OK'}, 0, "determined IP address management node used to connect to $computer_name: $source_ip");
			return $source_ip;
		}
	}
	
	notify($ERRORS{'WARNING'}, 0, "failed to determine IP address management node used to connect to $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
	return;
}

##/////////////////////////////////////////////////////////////////////////////
1;
__END__

=head1 SEE ALSO

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

=cut
