#!/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 - VCL base module

=head1 SYNOPSIS

In a derived module:

 use base qw(VCL::Module);

 sub initialize {
    my $self = shift;
    my $image_id = $self->data->get_image_id();
    <perform module initialization tasks...>
    return 1;
 }

=head1 DESCRIPTION

C<VCL::Module> is the base class for the modularized VCL architecture. All VCL
modules should inherit from C<VCL::Module> or from another class which inherits
from C<VCL::Module> (multilevel inheritance).

To inherit directly from C<VCL::Module>:

C<use base qw(VCL::Module);>

To inherit from a class which ultimately inherits from C<VCL::Module>:

C<use base qw(VCL::Module::OS::Windows);>

C<VCL::Module> provides a common constructor which all derived modules should
use. Derived modules should not implement their own constructors. The
constructor provides derived modules the ability to implement an C<initialize()>
subroutine which will be automatically called when a derived module object is
created. This method should be used if a module needs to perform any functions
to initialize a newly created module object.

Modules derived from C<VCL::Module> have access to the common backend
reservation data API to access and set the data for the reservation being
processed via C<< $self->data >>. (C<$self> being a reference to a derived
module object)

=cut

###############################################################################
package VCL::Module;

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

# Configure inheritance
use base qw();

# 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 English '-no_match_vars';
use Digest::SHA1 qw(sha1_hex);

use VCL::utils;
use VCL::DataStructure;

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

=head1 CONSTRUCTOR

=cut

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

=head2 new

 Parameters  : Hash reference - hash must contain a key called data_structure.
               The value of this key must be a reference to a VCL::DataStructure
               object.
 Returns     : Success - new object which inherits from VCL::Module
               Failure - undefined
 Description : Constructor for VCL modules. All VCL modules should use this
               constructor. Objects created using this constructor have a base
               class of VCL::Module. A module may have other intermediate
               classes it is derived from if multilevel inheritance is used.
               
               This constructor must be passed a reference to a previously
               created VCL::DataStructure object. Derived objects will have
               access to the data() object method: $self->data->get...()
               
               During object creation, this constructor will attempt to call an
               initialize() subroutine defined in a child class. This allows
               tasks to be automatically performed during object creation.
               Implementing an initialize() subroutine is optional.
               
               Any arguments passed to new() will be passed unchanged to
               initialize().
               
               Example:
               use VCL::Module::TestModule;
               my $test_module = new VCL::Module::TestModule({data_structure => $self->data});

=cut

sub new {
	my $class = shift;
	my $args  = shift;
	
	# Create a variable to store the newly created class object
	my $self;
	
	# Make sure a hash reference argument was passed
	if (!$args) {
		my $data_structure = new VCL::DataStructure();
		if ($data_structure) {
			$args->{data_structure} = $data_structure;
		}
		else {
			notify($ERRORS{'CRITICAL'}, 0, "no argument was passed and default DataStructure object could not be created");
			return;
		}
	}
	elsif (!ref($args) || ref($args) ne 'HASH') {
		notify($ERRORS{'CRITICAL'}, 0, "argument passed is not a hash reference");
		return;
	}
	
	# Make sure the data structure was passed as an argument called 'data_structure'
	if (!defined $args->{data_structure}) {
		notify($ERRORS{'CRITICAL'}, 0, "required 'data_structure' argument was not passed");
		return;
	}

	# Make sure the 'data_structure' argument contains a VCL::DataStructure object
	if (ref $args->{data_structure} ne 'VCL::DataStructure') {
		notify($ERRORS{'CRITICAL'}, 0, "'data_structure' argument passed is not a reference to a VCL::DataStructure object");
		return;
	}
	
	# Add the DataStructure reference to the class object
	$self->{data} = $args->{data_structure};
	
	for my $arg_key (keys %$args) {
		next if ($arg_key eq 'data_structure');
		
		$self->{$arg_key} = $args->{$arg_key};
		#notify($ERRORS{'DEBUG'}, 0, "set '$arg_key' key for $class object from arguments");
	}
	
	# Bless the object as the class which new was called with
	bless $self, $class;
	
	# Get the memory address of this newly created object - useful for debugging object creation problems
	my $address = sprintf('%x', $self);
	
	my $type = ref($self);
	
	# Display a message based on the type of object created
	if ($self->isa('VCL::Module::State')) {
		my $request_state_name = $self->data->get_request_state_name(0) || '<not set>';
		notify($ERRORS{'DEBUG'}, 0, "$type object created for state $request_state_name, address: $address");
	}
	elsif ($self->isa('VCL::Module::OS') && !$self->isa('VCL::Module::OS::Linux::ManagementNode')) {
		my $image_name = $self->data->get_image_name(0) || '<not set>';
		notify($ERRORS{'DEBUG'}, 0, "$type object created for image $image_name, address: $address");
	}
	elsif ($self->isa('VCL::Module::Provisioning')) {
		my $computer_name = $self->data->get_computer_short_name(0) || '<not set>';
		notify($ERRORS{'DEBUG'}, 0, "$type object created for computer $computer_name, address: $address");
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "$type object created, address: $address");
	}
	
	# Create a management node OS object
	# Check to make sure the object currently being created is not a MN OS object to avoid endless loop
	if (!$self->isa('VCL::Module::OS::Linux::ManagementNode') && !$self->isa('VCL::Module::State')) {
		my $mn_os;
		# Check if the mn_os argument was provided
		if ($args->{mn_os}) {
			$mn_os = $args->{mn_os};
		}
		elsif ($self->mn_os(0)) {
			$mn_os = $self->mn_os();
		}
		else {
			$mn_os = $self->create_mn_os_object();
		}
		
		if ($mn_os) {
			$self->set_mn_os($mn_os);
			$self->data->set_mn_os($mn_os);
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to create management node OS object");
			return;
		}
	}
	
	# Check if not running in setup mode and if initialize() subroutine is defined for this module
	if (!$SETUP_MODE || $self->isa('VCL::Module::OS::Linux::ManagementNode')) {
		if ($self->can("initialize")) {
			# Call the initialize() subroutine, if it returns 0, return 0
			# If it doesn't return 0, return the object reference
			return if (!$self->initialize($args));
		}
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "initialize not called for $type object ($address) because \$SETUP_MODE is true");
	}

	return $self;
} ## end sub new

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

=head1 OBJECT METHODS

=cut

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

=head2 create_datastructure_object

 Parameters  : $arguments
 Returns     : VCL::DataStructure object
 Description : Creates a DataStructure object. The arguments are the same as
               those passed to the DataStructure constructor.

=cut

sub create_datastructure_object {
	my $arguments = shift;
	
	if (my $type = ref($arguments)) {
		if ($type =~ /VCL::/) {
			# First argument is an object reference, assume this was called as an object method
			$arguments = shift;
		}
		elsif ($type ne 'HASH') {
			# First argument is not a hash reference
			notify($ERRORS{'CRITICAL'}, 0, "subroutine was not called as a VCL::Module object method and first argument is a $type reference");
			return;
		}
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "no arguments specified, creating default DataStructure object");
		$arguments = {};
	}
	
	my $data;
	eval {
		$data = new VCL::DataStructure($arguments);
	};
	
	if ($EVAL_ERROR) {
		notify($ERRORS{'WARNING'}, 0, "failed to create DataStructure object, arguments:\n" . format_data($arguments) . "\nerror:\n" . $EVAL_ERROR);
		return;
	}
	elsif (!$data) {
		notify($ERRORS{'WARNING'}, 0, "failed to create DataStructure object, arguments:\n" . format_data($arguments));
		return;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "created DataStructure object, arguments:\n" . format_data($arguments));
		return $data;
	}
}

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

=head2 create_object

 Parameters  : $perl_package, $data_structure_arguments (optional), $object_argument_hashref (optional)
 Returns     : VCL::Module object reference
 Description : This is a general constructor to create VCL::Module objects. It
               contains the code to call 'use $perl_package', instantiate an
               object, and catch errors.

=cut

sub create_object {
	my $argument = shift;
	
	# Check if called as an object method
	my $self;
	if ($argument && ref($argument)) {
		$self = $argument;
		$argument = shift;
	}
	
	if (!$argument) {
		notify($ERRORS{'WARNING'}, 0, "Perl package path argument was not specified");
		return;
	}
	elsif (my $type = ref($argument)) {
		notify($ERRORS{'WARNING'}, 0, "first argument must be the Perl package path scalar, not a $type reference");
		return;
	}
	
	my $perl_package = $argument;
	
	my $data;
	my $data_structure_arguments = shift;
	if ($data_structure_arguments) {
		if (ref($data_structure_arguments) && ref($data_structure_arguments) =~ /DataStructure/) {
			notify($ERRORS{'DEBUG'}, 0, "DataStructure object argument will be passed to the new $perl_package object");
			$data = $data_structure_arguments;
		}
		else {
			notify($ERRORS{'DEBUG'}, 0, "new DataStructure object will be created for the $perl_package object, data structure arguments passed:\n" . format_data($data_structure_arguments));
			$data = create_datastructure_object($data_structure_arguments);
		}
	}
	elsif (!$self) {
		notify($ERRORS{'DEBUG'}, 0, "new DataStructure object will be created for the $perl_package object, data structure arguments not passed and not called as an object reference");
		$data = create_datastructure_object();
	}
	elsif ($self) {
		notify($ERRORS{'DEBUG'}, 0, "existing DataStructure object will be passed to the new $perl_package object");
		$data = $self->data;
	}
	
	my $object_argument_hashref = shift;
	if ($object_argument_hashref) {
		my $type = ref($object_argument_hashref);
		if (!$type) {
			notify($ERRORS{'WARNING'}, 0, "3rd argument is not a reference, it must be a hash reference: $object_argument_hashref");
			return;
		}
		elsif ($type ne 'HASH') {
			notify($ERRORS{'WARNING'}, 0, "3rd argument is a $type reference, it must be a hash reference");
			return;
		}
	}
	$object_argument_hashref->{data_structure} = $data;

	# Attempt to load the module
	eval "use $perl_package";
	if ($EVAL_ERROR) {
		notify($ERRORS{'WARNING'}, 0, "$perl_package module could not be loaded, error:\n" . $EVAL_ERROR);
		return;
	}
	notify($ERRORS{'DEBUG'}, 0, "$perl_package module loaded");
	
	# Attempt to create the object
	my $object;
	eval {
		$object = ($perl_package)->new($object_argument_hashref)
	};
	
	if ($EVAL_ERROR) {
		notify($ERRORS{'WARNING'}, 0, "failed to create $perl_package object, error: $EVAL_ERROR");
		return;
	}
	elsif (!$object) {
		notify($ERRORS{'WARNING'}, 0, "failed to create $perl_package object");
		return;
	}
	else {
		my $address = sprintf('%x', $object);
		notify($ERRORS{'DEBUG'}, 0, "$perl_package object created, address: $address");
		return $object;
	}
}

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

=head2 create_os_object

 Parameters  : none
 Returns     : boolean
 Description : Creates an OS object if one has not already been created for the
               calling object.

=cut

sub create_os_object {
	my $self = shift;
	unless (ref($self) && $self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Make sure calling object isn't an OS module to avoid an infinite loop
	if ($self->isa('VCL::Module::OS')) {
		notify($ERRORS{'WARNING'}, 0, "this subroutine cannot be called from an existing OS module");
		return;
	}
	
	my $os_perl_package_argument = shift;
	my $os_perl_package;
	
	if ($os_perl_package_argument) {
		$os_perl_package = $os_perl_package_argument;
	}
	else {
		# Get the Perl package for the OS
		$os_perl_package = $self->data->get_image_os_module_perl_package();
	}
	
	if (!$os_perl_package) {
		notify($ERRORS{'WARNING'}, 0, "OS object could not be created, OS module Perl package could not be retrieved");
		return;
	}
	
	# Check if an OS object has already been stored in the calling object
	# Return this object if a Perl package argument wasn't passed
	if (!$os_perl_package_argument && $self->{os}) {
		my $os_address = sprintf('%x', $self->{os});
		my $os_image_name = $self->{os}->data->get_image_name();
		notify($ERRORS{'DEBUG'}, 0, "OS object has already been created for $os_image_name, address: $os_address, returning 1");
		return 1;
	}
	
	# Attempt to load the OS module
	eval "use $os_perl_package";
	if ($EVAL_ERROR) {
		notify($ERRORS{'WARNING'}, 0, "$os_perl_package module could not be loaded, error:\n" . $EVAL_ERROR);
		return 0;
	}
	notify($ERRORS{'DEBUG'}, 0, "$os_perl_package module loaded");
	
	# Attempt to create the object, pass it the mn_os object if it has already been created
	my $os;
	if (my $mn_os = $self->mn_os(0)) {
		$os = ($os_perl_package)->new({data_structure => $self->data, mn_os => $mn_os});
	}
	else {
		$os = ($os_perl_package)->new({data_structure => $self->data})
	}
	
	if ($os) {
		my $os_address = sprintf('%x', $os);
		notify($ERRORS{'DEBUG'}, 0, "$os_perl_package OS object created, address: $os_address");
		return $os;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to create OS object");
		return;
	}
}

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

=head2 create_current_os_object

 Parameters  : $computer_identifier (optional)
 Returns     : string
 Description : Attempts to determine the Perl package which should be used to
               control the computer.

=cut

sub create_current_os_object {
	my ($self, $computer_identifier, $suppress_warning) = @_;
	
	my $os_perl_package = VCL::Module::OS::get_os_perl_package(@_);
	if (!$os_perl_package) {
		notify($ERRORS{'WARNING'}, 0, "failed to create object for OS currently loaded on computer, correct Perl package path could not be determined") unless $suppress_warning;
		return;
	}
	
	if (ref($self) && ref($self) eq $os_perl_package) {
		notify($ERRORS{'DEBUG'}, 0, "returning object used to call this subroutine becuase it is the correct module type: " . ref($self));
		return $self;
	}
	
	return $self->create_os_object($os_perl_package);
}

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

=head2 create_mn_os_object

 Parameters  : none
 Returns     : boolean
 Description : Creates a management node OS object if one has not already been
               created for the calling object.

=cut

sub create_mn_os_object {
	my $self = shift;
	
	my $datastructure_arguments = {
		'image_identifier' => 'noimage'
	};
	
	# Check if called as an object reference
	if ($self && ref($self) =~ /VCL/) {
		# Add the reservation ID to the DataStructure arguments
		# Otherwise, get_reservation_id won't be available
		my $reservation_id = $self->data->get_reservation_id();
		$datastructure_arguments->{reservation_id} = $reservation_id;
	}
	
	# Create a DataStructure object containing computer data for the management node
	my $mn_data;
	eval {
		$mn_data = new VCL::DataStructure($datastructure_arguments);
	};
	
	# Attempt to load the OS module
	my $mn_os_perl_package = 'VCL::Module::OS::Linux::ManagementNode';
	eval "use $mn_os_perl_package";
	if ($EVAL_ERROR) {
		notify($ERRORS{'WARNING'}, 0, "$mn_os_perl_package module could not be loaded, error:\n" . $EVAL_ERROR);
		return 0;
	}
	notify($ERRORS{'DEBUG'}, 0, "$mn_os_perl_package module loaded");
	
	# Attempt to create the object
	if (my $mn_os = ($mn_os_perl_package)->new({data_structure => $mn_data})) {
		my $address = sprintf('%x', $mn_os);
		notify($ERRORS{'DEBUG'}, 0, "$mn_os_perl_package OS object created, address: $address");
		
		# Allow $mn_os->data to access $mn_os
		$mn_data->set_mn_os($mn_os);
		
		return $mn_os;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to create management node OS object");
		return;
	}
}

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

=head2 create_vmhost_os_object

 Parameters  : $vmhost_identifier (optional)
 Returns     : boolean
 Description : Creates an OS object for the VM host.

=cut

sub create_vmhost_os_object {
	my $self = shift;
	unless (ref($self) && $self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my $vmhost_identifier = shift;
	
	if (!$vmhost_identifier) {
		# Check if an OS object has already been stored in the calling object
		if (my $vmhost_os = $self->vmhost_os(0)) {
			my $address = sprintf('%x', $vmhost_os);
			notify($ERRORS{'DEBUG'}, 0, "returning existing VM host OS object ($address)");
			return $vmhost_os;
		}
	}
	
	# Make sure calling object isn't an OS module to avoid an infinite loop
	if ($self->isa('VCL::Module::OS')) {
		notify($ERRORS{'WARNING'}, 0, "this subroutine cannot be called from an existing OS module: " . ref($self));
		return;
	}
	
	my $request_data = $self->data->get_request_data();
	my $reservation_id = $self->data->get_reservation_id();
	
	my $vmhost_computer_id;
	my $vmhost_hostname;
	my $vmhost_profile_image_id;
	if ($vmhost_identifier) {
		my $vmhost_info = get_vmhost_info($vmhost_identifier);
		if (!$vmhost_info) {
			notify($ERRORS{'WARNING'}, 0, "unable to create VM host OS object for host specified by argument: $vmhost_identifier, VM host info could not be retrieved");
			return;
		}
		
		$vmhost_computer_id = $vmhost_info->{computerid};
		if (!$vmhost_computer_id) {
			notify($ERRORS{'WARNING'}, 0, "unable to create VM host OS object for host specified by argument: $vmhost_identifier, VM host computer ID could not be determined from VM host info:\n" . format_data($vmhost_info));
			return;
		}
		
		$vmhost_hostname = $vmhost_info->{computer}{hostname};
		if (!$vmhost_hostname) {
			notify($ERRORS{'WARNING'}, 0, "unable to create VM host OS object for host specified by argument: $vmhost_identifier, VM host computer hostname could not be determined from VM host info:\n" . format_data($vmhost_info));
			return;
		}
		
		$vmhost_profile_image_id = $vmhost_info->{vmprofile}{imageid};
		if (!$vmhost_profile_image_id) {
			notify($ERRORS{'WARNING'}, 0, "unable to create VM host OS object for host specified by argument: $vmhost_identifier, VM host profile image ID could not be determined from VM host info:\n" . format_data($vmhost_info));
			return;
		}
	}
	else {
		# Argument was not supplied, use reservation data
		$vmhost_computer_id = $self->data->get_vmhost_computer_id();
		$vmhost_hostname = $self->data->get_vmhost_hostname();
		$vmhost_profile_image_id = $self->data->get_vmhost_profile_image_id();
		if (!$vmhost_computer_id || !$vmhost_hostname || !defined($vmhost_profile_image_id)) {
			notify($ERRORS{'WARNING'}, 0, "unable to create VM host OS object, VM host computer ID, hostname, and profile image ID could not be determined from reservation data");
			return;
		}
	}
	
	# Create a DataStructure object containing computer data for the VM host
	my $vmhost_data;
	eval {
		$vmhost_data = new VCL::DataStructure({
			request_data => $request_data,
			reservation_id => $reservation_id,
			computer_identifier => $vmhost_computer_id,
			image_identifier => $vmhost_profile_image_id
			}
		);
	};
	
	if ($EVAL_ERROR) {
		notify($ERRORS{'WARNING'}, 0, "unable to create DataStructure object for VM host, error: $EVAL_ERROR");
		return;
	}
	elsif (!$vmhost_data) {
		notify($ERRORS{'WARNING'}, 0, "unable to create DataStructure object for VM host, DataStructure object is not defined");
		return;
	}
	
	# Get the VM host OS module Perl package name
	my $vmhost_os_perl_package = $vmhost_data->get_image_os_module_perl_package();
	if (!$vmhost_os_perl_package) {
		notify($ERRORS{'WARNING'}, 0, "unable to create DataStructure or OS object for VM host, failed to retrieve VM host image OS module Perl package name");
		return;
	}
	
	# Do not try to load the UnixLab module for VM hosts -- most likely not the intended OS module
	# TODO: add additional checks here, VM host image may be something like XP
	if ($vmhost_os_perl_package =~ /(UnixLab|2003|XP|Vista)/i || $vmhost_os_perl_package =~ /^VCL::Module::OS$/) {
		my $vmhost_os_perl_package_override = 'VCL::Module::OS::Linux';
		notify($ERRORS{'OK'}, 0, "VM host OS image Perl package is $vmhost_os_perl_package, most likely will not work correctly, changing to Linux");
		$vmhost_os_perl_package = $vmhost_os_perl_package_override;
	}
	
	# Load the VM host OS module
	notify($ERRORS{'DEBUG'}, 0, "attempting to load VM host OS module: $vmhost_os_perl_package (image: $vmhost_profile_image_id)");
	eval "use $vmhost_os_perl_package";
	if ($EVAL_ERROR) {
		notify($ERRORS{'WARNING'}, 0, "VM host OS module could NOT be loaded: $vmhost_os_perl_package, error: $EVAL_ERROR");
		return;
	}
	notify($ERRORS{'DEBUG'}, 0, "VM host OS module loaded: $vmhost_os_perl_package");
	
	# Attempt to create the object
	my $vmhost_os;
	if (my $mn_os = $self->mn_os(0)) {
		$vmhost_os = ($vmhost_os_perl_package)->new({data_structure => $vmhost_data, mn_os => $mn_os});
	}
	else {
		$vmhost_os = ($vmhost_os_perl_package)->new({data_structure => $vmhost_data})
	}
	
	if ($vmhost_os) {
		my $address = sprintf('%x', $vmhost_os);
		notify($ERRORS{'DEBUG'}, 0, "$vmhost_os_perl_package OS object created, address: $address");
		return $vmhost_os;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to create VM host OS object");
		return;
	}
}

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

=head2 create_nathost_os_object

 Parameters  : none
 Returns     : VCL::Module::OS object reference
 Description : Creates an OS module object to control the reservation computer's
               NAT host.

=cut

sub create_nathost_os_object {
my $self = shift;
	unless (ref($self) && $self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Check if an OS object has already been stored in the calling object
	if (my $nathost_os = $self->nathost_os(0)) {
		return $nathost_os;
	}
	
	notify($ERRORS{'DEBUG'}, 0, "attempting to create NAT host OS object");
	
	# Make sure calling object isn't an OS module to avoid an infinite loop
	if ($self->isa('VCL::Module::OS')) {
		notify($ERRORS{'WARNING'}, 0, "this subroutine cannot be called from an existing OS module: " . ref($self));
		return;
	}
	
	my $request_data = $self->data->get_request_data();
	my $reservation_id = $self->data->get_reservation_id();
	
	my $nathost_id = $self->data->get_nathost_id();
	my $nathost_hostname = $self->data->get_nathost_hostname();
	my $nathost_public_ip_address = $self->data->get_nathost_public_ip_address(0);
	my $nathost_internal_ip_address = $self->data->get_nathost_internal_ip_address(0);
	my $nathost_resource_subid = $self->data->get_nathost_resource_subid();
	my $nathost_resource_type = $self->data->get_nathost_resource_type();
	
	# Make sure computer is mapped to a NAT host and all the required variables are set
	if (!defined($nathost_id)) {
		notify($ERRORS{'WARNING'}, 0, "failed to create NAT host OS object, NAT host ID is not defined");
		return;
	}
	elsif (!defined($nathost_hostname)) {
		notify($ERRORS{'WARNING'}, 0, "failed to create NAT host OS object, NAT host hostname is not defined");
		return;
	}
	elsif (!defined($nathost_public_ip_address)) {
		notify($ERRORS{'WARNING'}, 0, "failed to create NAT host OS object, NAT host public IP address is not defined");
		return;
	}
	elsif (!defined($nathost_internal_ip_address)) {
		notify($ERRORS{'WARNING'}, 0, "failed to create NAT host OS object, NAT host internal IP address is not defined");
		return;
	}
	
	my $nathost_os;
	
	if ($nathost_resource_type eq 'managementnode') {
		notify($ERRORS{'DEBUG'}, 0, "NAT host resource type is $nathost_resource_type, returning management node OS object to control $nathost_hostname");
		$nathost_os = $self->mn_os();
	}
	elsif ($nathost_resource_type eq 'computer') {
		# Get the computer info in order to determine the OS module to use
		my $computer_info = get_computer_info($nathost_resource_subid);
		if (!$computer_info) {
			notify($ERRORS{'WARNING'}, 0, "failed to create NAT host OS object, failed to retrieve info for computer ID: $nathost_resource_subid, NAT host info:\n" . format_data($self->data->get_nathost_info()));
		}
		my $computer_os_package = $computer_info->{currentimagerevision}{image}{OS}{module}{perlpackage};
		
		# Make sure the OS module for NAT host computer.currentimagerevision is Linux and not UnixLab
		# UnixLab.pm overrides the firewall initialization step and will have a generic VCL::Module::OS::Linux::firewall object which doesn't implement nat_configure_reservation
		if ($computer_os_package !~ /VCL::Module::OS::Linux/ || $computer_os_package =~ /UnixLab/) {
			notify($ERRORS{'DEBUG'}, 0, "NAT host resource type is $nathost_resource_type, OS module that controls $nathost_hostname\'s current computer.currentimagerevision value is $computer_os_package, overriding to VCL::Module::OS::Linux");
			$computer_os_package = 'VCL::Module::OS::Linux';
		}
		else {
			notify($ERRORS{'DEBUG'}, 0, "NAT host resource type is $nathost_resource_type, creating $computer_os_package OS object to control $nathost_hostname based its current computer.currentimagerevision value");
		}
		
		$nathost_os = $self->create_object($computer_os_package, {
			#request_data => $request_data,
			reservation_id => $reservation_id,
			computer_identifier => $nathost_resource_subid
		});
		if (!$nathost_os) {
			notify($ERRORS{'WARNING'}, 0, "failed to create NAT host OS object to control $nathost_hostname");
			return;
		}
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to create NAT host OS object to control $nathost_hostname, NAT host resource type is not supported: $nathost_resource_type, NAT host info:\n" . format_data($self->data->get_nathost_info()));
		return;
	}
	
	# All of the following should always be configured
	my $nathost_os_type = ref($nathost_os);
	if (!$nathost_os->firewall()) {
		notify($ERRORS{'WARNING'}, 0, "created $nathost_os_type NAT host OS object but firewall object is not available");
		return;
	}
	
	my $firewall_type = ref($nathost_os->firewall());
	if (!$nathost_os->firewall->can('nat_configure_host')) {
		notify($ERRORS{'WARNING'}, 0, "created $nathost_os_type NAT host OS object but NAT host OS's $firewall_type firewall object does NOT implement a 'nat_configure_host' method");
		return;
	}
	elsif (!$nathost_os->firewall->can('nat_configure_reservation')) {
		notify($ERRORS{'WARNING'}, 0, "created $nathost_os_type NAT host OS object but NAT host OS's $firewall_type firewall object does NOT implement a 'nat_configure_reservation' method");
		return;
	}
	
	# Set NAT host DataStructure values so they can be accessed from $self->nathost_os and $self->nathost_os->firewall
	$nathost_os->data->set_nathost_public_ip_address($nathost_public_ip_address);
	$nathost_os->data->set_nathost_internal_ip_address($nathost_internal_ip_address);
	
	return $nathost_os
}

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

=head2 create_provisioning_object

 Parameters  : $provisioning_perl_package (optional)
 Returns     : VCL::Module::Provisioning object reference
 Description : Creates an provisioning module object if one has not already been
               created for the calling object.

=cut

sub create_provisioning_object {
	my $self = shift;
	unless (ref($self) && $self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Make sure calling object isn't a provisioning module to avoid an infinite loop
	if ($self->isa('VCL::Module::Provisioning')) {
		notify($ERRORS{'WARNING'}, 0, "this subroutine cannot be called from an existing provisioning module");
		return;
	}
	
	# Check if an OS object has already been stored in the calling object
	if ($self->{provisioner}) {
		my $address = sprintf('%x', $self->{provisioner});
		my $provisioner_computer_name = $self->{provisioner}->data->get_computer_short_name();
		notify($ERRORS{'DEBUG'}, 0, "provisioning object has already been created, address: $address, returning 1");
		return 1;
	}
	
	# Get the Perl package for the provisioning module
	my $provisioning_perl_package = shift || $self->data->get_computer_provisioning_module_perl_package();
	if (!$provisioning_perl_package) {
		notify($ERRORS{'WARNING'}, 0, "provisioning object could not be created, provisioning module Perl package could not be retrieved");
		return;
	}
	
	# Attempt to load the computer provisioning module
	eval "use $provisioning_perl_package";
	if ($EVAL_ERROR) {
		notify($ERRORS{'WARNING'}, 0, "$provisioning_perl_package module could not be loaded, error:\n" . $EVAL_ERROR);
		return 0;
	}
	notify($ERRORS{'DEBUG'}, 0, "$provisioning_perl_package module loaded");
	
	# Attempt to provisioner the object, pass it the mn_os object if it has already been created
	my $constructor_arguments = {};
	$constructor_arguments->{data_structure} = $self->data();
	$constructor_arguments->{os} = $self->os(0) if $self->os(0);
	$constructor_arguments->{mn_os} = $self->mn_os(0) if $self->mn_os(0);
	$constructor_arguments->{vmhost_os} = $self->vmhost_os(0) if $self->vmhost_os(0);
	my $provisioner = ($provisioning_perl_package)->new($constructor_arguments);
	
	if ($provisioner) {
		my $provisioner_address = sprintf('%x', $provisioner);
		my $provisioner_computer_name = $provisioner->data->get_computer_short_name();
		notify($ERRORS{'DEBUG'}, 0, "$provisioning_perl_package provisioning object created for $provisioner_computer_name, address: $provisioner_address");
		return $provisioner;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "provisioning object could not be created, returning 0");
		return 0;
	}
}

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

=head2 data

 Parameters  : $display_warning (optional)
 Returns     : Reference to the DataStructure object
 Description : This subroutine allows VCL module objects to retrieve data using
               the object's DataStructure object as follows:
               my $image_id = $self->data->get_image_id();

=cut

sub data {
	my $self = shift;
	if (!ref($self) || !$self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was not called as a VCL::Module or VCL::DataStructure class method");
		return;
	}
	
	my $display_warning = shift;
	if (!defined($display_warning)) {
		$display_warning = 1;
	}
	
	if (!$self->{data}) {
		if ($display_warning) {
			notify($ERRORS{'WARNING'}, 0, "unable to return DataStructure object, \$self->{data} is not set");
		}
		return;
	}
	else {
		return $self->{data};
	}
} ## end sub data

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

=head2 provisioner

 Parameters  : $display_warning (optional)
 Returns     : Process's provisioner object
 Description : Allows OS modules to access the reservation's provisioner
               object.

=cut

sub provisioner {
	my $self = shift;
	if (!ref($self) || !$self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was not called as a VCL::Module or VCL::DataStructure class method");
		return;
	}
	
	my $display_warning = shift;
	if (!defined($display_warning)) {
		$display_warning = 1;
	}
	
	if (!$self->{provisioner}) {
		if ($display_warning) {
			notify($ERRORS{'WARNING'}, 0, "unable to return provisioner object, \$self->{provisioner} is not set");
		}
		return;
	}
	else {
		return $self->{provisioner};
	}
}

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

=head2 os

 Parameters  : $display_warning (optional)
 Returns     : Process's OS object
 Description : Allows modules to access the reservation's OS object.

=cut

sub os {
	my $self = shift;
	if (!ref($self) || !$self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was not called as a VCL::Module or VCL::DataStructure class method");
		return;
	}
	
	my $display_warning = shift;
	if (!defined($display_warning)) {
		$display_warning = 1;
	}
	
	if (!$self->{os}) {
		if ($display_warning) {
			notify($ERRORS{'WARNING'}, 0, "unable to return OS object, \$self->{os} is not set");
		}
		return;
	}
	else {
		return $self->{os};
	}
}

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

=head2 mn_os

 Parameters  : $display_warning (optional)
 Returns     : Management node's OS object
 Description : Allows modules to access the management node's OS object.

=cut

sub mn_os {
	my $self = shift;
	if (!ref($self) || !$self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was not called as a VCL::Module or VCL::DataStructure class method");
		return;
	}
	
	my $display_warning = shift;
	if (!defined($display_warning)) {
		$display_warning = 1;
	}
	
	if (!$ENV->{mn_os}) {
		if ($display_warning) {
			notify($ERRORS{'WARNING'}, 0, "unable to return management node OS object, \$ENV->{mn_os} is not set");
		}
		return;
	}
	else {
		return $ENV->{mn_os};
	}
}

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

=head2 vmhost_os

 Parameters  : $display_warning (optional)
 Returns     : VM hosts's OS object
 Description : Allows modules to access the VM host's OS object.

=cut

sub vmhost_os {
	my $self = shift;
	if (!ref($self) || !$self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was not called as a VCL::Module or VCL::DataStructure class method");
		return;
	}
	
	my $display_warning = shift;
	if (!defined($display_warning)) {
		$display_warning = 1;
	}
	
	if (!$self->{vmhost_os}) {
		if ($display_warning) {
			notify($ERRORS{'WARNING'}, 0, "unable to return VM host OS object, \$self->{vmhost_os} is not set");
		}
		return;
	}
	else {
		return $self->{vmhost_os};
	}
}

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

=head2 nathost_os

 Parameters  : $display_warning (optional)
 Returns     : NAT hosts's OS object
 Description : Allows modules to access the NAT host's OS object.

=cut

sub nathost_os {
	my $self = shift;
	if (!ref($self) || !$self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was not called as a VCL::Module or VCL::DataStructure class method");
		return;
	}
	
	my $display_warning = shift;
	if (!defined($display_warning)) {
		$display_warning = 1;
	}
	
	if (!$self->{nathost_os}) {
		if ($display_warning) {
			notify($ERRORS{'WARNING'}, 0, "unable to return NAT host OS object, \$self->{nathost_os} is not set");
		}
		return;
	}
	else {
		return $self->{nathost_os};
	}
}

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

=head2 set_data

 Parameters  : $data
 Returns     : boolean
 Description : Sets the DataStructure object for the module to access.

=cut

sub set_data {
	my $self = shift;
	if (!ref($self) || !$self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was not called as a VCL::Module class method");
		return;
	}
	
	my $data = shift;
	if (!defined($data)) {
		notify($ERRORS{'WARNING'}, 0, "DataStructure object reference argument not supplied");
		return;
	}
	elsif (!ref($data) || !$data->isa('VCL::DataStructure')) {
		notify($ERRORS{'WARNING'}, 0, "supplied argument is not a DataStructure object reference:\n" . format_data($data));
		return;
	}
	$self->{data} = $data;
	return 1;
}

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

=head2 set_os

 Parameters  : $os
 Returns     : boolean
 Description : Sets the OS object for the module to access.

=cut

sub set_os {
	my $self = shift;
	if (!ref($self) || !$self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was not called as a VCL::Module class method");
		return;
	}
	
	my $os = shift;
	if (!defined($os)) {
		notify($ERRORS{'WARNING'}, 0, "OS object reference argument not supplied");
		return;
	}
	elsif (!ref($os) || !$os->isa('VCL::Module::OS')) {
		notify($ERRORS{'WARNING'}, 0, "supplied argument is not a VCL::Module::OS object reference:\n" . format_data($os));
		return;
	}
	$self->{os} = $os;
	return 1;
}

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

=head2 set_mn_os

 Parameters  : $mn_os
 Returns     : boolean
 Description : Sets the management node OS object for the module to access.

=cut

sub set_mn_os {
	my $self = shift;
	if (!ref($self) || !$self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was not called as a VCL::Module class method");
		return;
	}
	
	my $mn_os = shift;
	if (!defined($mn_os)) {
		notify($ERRORS{'WARNING'}, 0, "OS object reference argument not supplied");
		return;
	}
	elsif (!ref($mn_os) || !$mn_os->isa('VCL::Module::OS')) {
		notify($ERRORS{'WARNING'}, 0, "supplied argument is not a VCL::Module::OS object reference:\n" . format_data($mn_os));
		return;
	}
	
	my $address = sprintf('%x', $self);
	my $type = ref($self);
	my $mn_os_address = sprintf('%x', $mn_os);
	notify($ERRORS{'DEBUG'}, 0, "storing reference to managment node OS object (address: $mn_os_address) in this $type object (address: $address)");
	$ENV->{mn_os} = $mn_os;
	return 1;
}

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

=head2 set_vmhost_os

 Parameters  : $vmhost_os
 Returns     : boolean
 Description : Sets the VM host OS object for the module to access.

=cut

sub set_vmhost_os {
	my $self = shift;
	if (!ref($self) || !$self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was not called as a VCL::Module class method");
		return;
	}
	
	my $vmhost_os = shift;
	if (!defined($vmhost_os)) {
		notify($ERRORS{'WARNING'}, 0, "OS object reference argument not supplied");
		return;
	}
	elsif (!ref($vmhost_os) || !$vmhost_os->isa('VCL::Module')) {
		notify($ERRORS{'WARNING'}, 0, "supplied argument is not a VCL::Module object reference:\n" . format_data($vmhost_os));
		return;
	}
	
	my $address = sprintf('%x', $self);
	my $type = ref($self);
	my $vmhost_os_address = sprintf('%x', $vmhost_os);
	notify($ERRORS{'DEBUG'}, 0, "storing reference to VM host OS object (address: $vmhost_os_address) in this $type object (address: $address)");
	$self->{vmhost_os} = $vmhost_os;
	return 1;
}

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

=head2 set_nathost_os

 Parameters  : $nathost_os
 Returns     : boolean
 Description : Sets the NAT host OS object for the module to access.

=cut

sub set_nathost_os {
	my $self = shift;
	if (!ref($self) || !$self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was not called as a VCL::Module class method");
		return;
	}
	
	my $nathost_os = shift;
	if (!defined($nathost_os)) {
		notify($ERRORS{'WARNING'}, 0, "OS object reference argument not supplied");
		return;
	}
	elsif (!ref($nathost_os) || !$nathost_os->isa('VCL::Module')) {
		notify($ERRORS{'WARNING'}, 0, "supplied argument is not a VCL::Module object reference:\n" . format_data($nathost_os));
		return;
	}
	
	my $address = sprintf('%x', $self);
	my $type = ref($self);
	my $nathost_os_address = sprintf('%x', $nathost_os);
	notify($ERRORS{'DEBUG'}, 0, "storing reference to NAT host OS object (address: $nathost_os_address) in this $type object (address: $address)");
	$self->{nathost_os} = $nathost_os;
	return 1;
}

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

=head2 set_provisioner

 Parameters  : $provisioner
 Returns     : boolean
 Description : Sets the provisioner object for the module to access.

=cut

sub set_provisioner {
	my $self = shift;
	if (!ref($self) || !$self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was not called as a VCL::Module class method");
		return;
	}
	
	my $provisioner = shift;
	if (!defined($provisioner)) {
		notify($ERRORS{'WARNING'}, 0, "provisioner object reference argument not supplied");
		return;
	}
	elsif (!ref($provisioner) || !$provisioner->isa('VCL::Module::Provisioning')) {
		notify($ERRORS{'WARNING'}, 0, "supplied argument is not a VCL::Module::Provisioning object reference:\n" . format_data($provisioner));
		return;
	}
	$self->{provisioner} = $provisioner;
	return 1;
}

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

=head2 get_package_hierarchy

 Parameters  : String containing the name of a Perl package
               (note: parameter is optional if called as an object method,
               required if called as a class function
 Returns     : Array containing class package names
 Description : Determines the Perl package inheritance hierarchy given a
               package name or object reference.
               
               Returns an array containing the names of the originating
               Perl package and any parent packages it inherits from.
               
               This subroutine does not support multiple inheritance.
               If any package up the chain inherits from multiple classes,
               only the first class listed in the package's @ISA array is
               used.
               
               The package name on which this subroutine is called is the
               lowest in the hierarchy and has the lowest index in the
               array.
               
               If the package on which this subroutine is called does not
               explicitly inherit from any other packages, the array
               returned will only contain 1 element which is the calling
               package name.
               
               Example: call as object method:
               my $os = VCL::Module::OS::Windows::Version_5::XP->new({data_structure => $self->data});
               my @packages = $os->get_package_hierarchy();
               
               Example: call as class function:
               my @packages = get_package_hierarchy("VCL::Module::OS::Windows::Version_5::XP");
               
               Both examples return the following array:
               [0] = 'VCL::Module::OS::Windows::Version_5::XP'
               [1] = 'VCL::Module::OS::Windows::Version_5'
               [2] = 'VCL::Module::OS::Windows'
               [3] = 'VCL::Module::OS'
               [4] = 'VCL::Module'


=cut

sub get_package_hierarchy {
	my $argument = shift;
	if (!$argument) {
		notify($ERRORS{'WARNING'}, 0, "subroutine was not called as an object method and argument was not passed");
		return;
	}
	
	my @return_package_names;
	my $package_name;
	
	# Check if this was called as an object method
	# If it was, check if an argument was supplied
	if (ref($argument) && $argument->isa('VCL::Module')) {
		my $argument2 = shift;
		# If called as object method and argument was supplied, use the argument
		$argument = $argument2 if defined($argument2);
	}
	
	# Check if argument is an object reference or a package name string
	if (ref($argument)) {
		# Argument is a reference, get package hierarchy of object type which called this
		# Add the calling package name as the first element of the return array
		$package_name = ref($argument);
		push @return_package_names, $package_name;
	}
	else {
		# Argument is not a reference, assume argument is a string containing a package name
		$package_name = $argument;
	}
	#notify($ERRORS{'DEBUG'}, 0, "finding package hierarchy for: $package_name");
	
	# Use eval to retrieve the package name's @ISA array
	my @package_isa = eval '@' . $package_name . '::ISA';
	if ($EVAL_ERROR) {
		notify($ERRORS{'WARNING'}, 0, "unable to determine \@ISA array for package: $package_name, error:\n$EVAL_ERROR");
		return;
	}
	
	# Get the number of elements in the package's @ISA array
	my $package_isa_count = scalar @package_isa;
	
	# Check if @ISA is empty
	if ($package_isa_count == 0) {
		#notify($ERRORS{'DEBUG'}, 0, "$package_name has no parent packages");
		return ();
	}
	
	#notify($ERRORS{'DEBUG'}, 0, "parent package names for $package_name:\n" . format_data(\@package_isa));
	my $parent_package_name = $package_isa[0];
	
	# Warn if package uses multiple inheritance, only use 1st element of package's @ISA array
	if ($package_isa_count > 1) {
		notify($ERRORS{'WARNING'}, 0, "$package_name has multiple parent packages, only using $parent_package_name");
	}
	
	# Add this package's parent package name to the return array
	push @return_package_names, $parent_package_name;
	
	# Recursively call this sub on the parent package and add the results to the return array
	push @return_package_names, get_package_hierarchy($parent_package_name);
	
	# Print the package names only for the original argument, not for recursive packages
	my $calling_subroutine = get_calling_subroutine();
	if ($calling_subroutine !~ /get_package_hierarchy/) {
		notify($ERRORS{'DEBUG'}, 0, "returning for $package_name:\n" . join("\n", @return_package_names));
	}
	return @return_package_names;
}

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

=head2 get_class_variable_hierarchy

 Parameters  : $class_variable
 Returns     : array
 Description : VCL objects inherit from multiple parent classes:
               Ubuntu > Linux > OS > Module
               
               This subroutine allows a class variable which is defined in
               multiple parent class levels to be retrieved for each level. It
               traverse the object's parent classes from highest to lowest and
               return an array containing the value of the variable for each
               level. For example,
               
               Linux.pm defines this array reference:
               our $CAPTURE_DELETE_FILE_PATHS = [
                  '/root/.ssh/id_rsa',
                  '/root/.ssh/id_rsa.pub',
                  '/etc/udev/rules.d/70-persistent-net.rules',
               ];
               
               Ubuntu.pm defines this array reference with the same name:
               our $CAPTURE_DELETE_FILE_PATHS = [
                  '/etc/network/interfaces.20*',
               ];
               
               $self->os->get_class_variable_hierarchy('CAPTURE_DELETE_FILE_PATHS') =
               (
                  [
                     "/root/.ssh/id_rsa",
                     "/root/.ssh/id_rsa.pub",
                     "/etc/udev/rules.d/70-persistent-net.rules"
                  ],
                  [
                     "/etc/network/interfaces.20*"
                  ]
               )

=cut

sub get_class_variable_hierarchy {
	my $self = shift;
	unless (ref($self) && $self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine can only be called as a VCL module object method");
		return;	
	}
	
	my $class_variable_name = shift;
	if (!defined($class_variable_name)) {
		notify($ERRORS{'WARNING'}, 0, "class variable name argument was not supplied");
		return;
	}
	
	# Get an array containing the names of the Perl packages the OS object is a class of
	my @package_hierarchy = $self->get_package_hierarchy();
	
	# Loop through each classes, retrieve any which have a matching variable defined
	my @values = ();
	for my $package_name (@package_hierarchy) {
		my $value = eval '$' . $package_name . "::$class_variable_name";
		if ($EVAL_ERROR) {
			notify($ERRORS{'WARNING'}, 0, "unable to determine value of \$$class_variable_name for $package_name, error:\n$EVAL_ERROR");
			next;	
		}
		elsif (!$value) {
			notify($ERRORS{'DEBUG'}, 0, "\$$class_variable_name is not defined for $package_name");
			next;
		}
		
		notify($ERRORS{'DEBUG'}, 0, "\$$class_variable_name for $package_name: " . format_data($value));
		
		# Add the value to the return array
		# Use unshift to add to the beginning to the array
		unshift @values, $value; 
	}
	
	notify($ERRORS{'DEBUG'}, 0, "retrieved class variable hierarchy for '$class_variable_name':\n" . format_data(\@values));
	return @values;
}

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

=head2 code_loop_timeout

 Parameters  : 1: code reference
               2: array reference containing arguments to pass to code reference
               3: message to display when attempting to execute code reference
               4: timeout seconds, maximum number of seconds to attempt to execute code until it returns true
               5: seconds to wait in between code execution attempts (optional)
               6: message interval seconds (optional)
 Returns     : If code returns true: returns result returned by code reference
               If code never returns true: 0
 Description : Executes the code contained in the code reference argument until
               it returns true or until the timeout is reached.
               
               Example:
               Call the _pingnode subroutine, pass it a single argument,
               continue calling _pingnode until 20 seconds have passed, wait 4
               seconds in between attempts:
               $self->os->code_loop_timeout(\&_pingnode, ['vclh3-8'], 'checking ping', 20, 4);

=cut

sub code_loop_timeout {
	my $self = shift;
	unless (ref($self) && $self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my ($code_ref, $args_array_ref, $message, $total_wait_seconds, $attempt_delay_seconds, $message_interval_seconds) = @_;
	
	# Make sure the code reference argument was passed correctly
	if (!defined($code_ref)) {
		notify($ERRORS{'WARNING'}, 0, "code reference argument is undefined");
		return;
	}
	elsif (ref($code_ref) ne 'CODE') {
		notify($ERRORS{'WARNING'}, 0, "1st argument must be a code reference, not " . format_data($code_ref));
		return;
	}
	
	if (!defined($args_array_ref)) {
		notify($ERRORS{'WARNING'}, 0, "2nd argument (arguments to pass to code reference) is undefined");
		return;
	}
	elsif (!ref($args_array_ref) || ref($args_array_ref) ne 'ARRAY') {
		notify($ERRORS{'WARNING'}, 0, "2nd argument (arguments to pass to code reference) is not an array reference");
		return;
	}
	
	if (!defined($message)) {
		notify($ERRORS{'WARNING'}, 0, "3nd argument (message to display) is undefined");
		return;
	}
	elsif (!$message) {
		$message = 'executing code reference';
	}
	
	if (!defined($total_wait_seconds) || $total_wait_seconds !~ /^\d+$/) {
		notify($ERRORS{'WARNING'}, 0, "4th argument (total wait seconds) was not passed correctly");
		return;
	}
	
	if (!$attempt_delay_seconds) {
		$attempt_delay_seconds = 15;
	}
	elsif (defined($attempt_delay_seconds) && $attempt_delay_seconds !~ /^\d+$/) {
		notify($ERRORS{'WARNING'}, 0, "5th argument (attempt delay) was not passed correctly: $attempt_delay_seconds");
		return;
	}
	
	if ($message_interval_seconds) {
		if ($message_interval_seconds !~ /^\d+$/) {
			notify($ERRORS{'WARNING'}, 0, "6th argument (message interval) was not passed correctly");
			return;
		}
		
		# Message interval is pointless if it's set to a value less than $attempt_delay_seconds
		if ($message_interval_seconds < $attempt_delay_seconds) {
			$message_interval_seconds = 0;
		}
	}
	else {
		$message_interval_seconds = 0;
	}
	
	notify($ERRORS{'DEBUG'}, 0, "$message, maximum of $total_wait_seconds seconds");
	
	my $start_time = time();
	my $current_time = $start_time;
	my $end_time = ($start_time + $total_wait_seconds);
	
	# Loop until code returns true
	my $attempt = 0;
	while (($current_time = time) <= $end_time) {
		$attempt++;
		
		# Execute the code reference
		if (my $result = &$code_ref(@{$args_array_ref})) {
			notify($ERRORS{'OK'}, 0, "$message, code returned true");
			return $result;
		}
		
		$current_time = time;
		my $seconds_elapsed = ($current_time - $start_time);
		my $seconds_remaining = ($end_time > $current_time) ? ($end_time - $current_time) : 0;
		my $sleep_seconds = ($seconds_remaining < $attempt_delay_seconds) ? $seconds_remaining : $attempt_delay_seconds;
		
		if (!$message_interval_seconds) {
			notify($ERRORS{'OK'}, 0, "attempt $attempt: $message ($seconds_elapsed/$seconds_remaining elapsed/remaining seconds), sleeping for $sleep_seconds seconds");
		}
		elsif ($attempt == 1 || ($seconds_remaining <= $attempt_delay_seconds) || ($seconds_elapsed % $message_interval_seconds) < $attempt_delay_seconds) {
			notify($ERRORS{'OK'}, 0, "attempt $attempt: $message ($seconds_elapsed/$seconds_remaining elapsed/remaining seconds)");
		}
		
		if (!$sleep_seconds) {
			last;
		}
		
		sleep $sleep_seconds;
	}

	notify($ERRORS{'OK'}, 0, "$message, code did not return true after waiting $total_wait_seconds seconds");
	return 0;
} ## end sub code_loop_timeout

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

=head2 get_semaphore

 Parameters  : $semaphore_identifier, $semaphore_expire_seconds (optional), $attempt_delay_seconds (optional)
 Returns     : VCL::Module::Semaphore object
 Description : This subroutine is used to ensure that only 1 process performs a
               particular task at a time. An example would be the retrieval of
               an image from another management node. If multiple reservations
               are being processed for the same image, each reservation may
               attempt to retrieve it via SCP at the same time. This subroutine
               can be used to only allow 1 process to retrieve the image. The
               others will wait until the semaphore is released by the
               retrieving process.
               
               A semaphore object is returned. The semaphore will be retained as
               long as the semaphore object remains defined. Once undefined, the
               semaphore is released.
               
               Examples:
               
               Semaphore is released when it is undefined:
               my $semaphore = $self->get_semaphore('test');
               ... <semaphore in place>
               undef $semaphore;
               ... <semaphore released>
               
               Semaphore is released when it goes out of scope:
               if (blah) {
                  my $semaphore = $self->get_semaphore('test');
                  ... <semaphore in place>
               }
               ... <semaphore released>

=cut

sub get_semaphore {
	my $self = shift;
	unless (ref($self) && $self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the file path argument
	my ($semaphore_identifier, $semaphore_expire_seconds, $attempt_delay_seconds) = @_;
	if (!$semaphore_identifier) {
		notify($ERRORS{'WARNING'}, 0, "semaphore identifier argument was not supplied");
		return;
	}
	
	# Attempt to create a new semaphore object
	# Load Semaphore.pm here instead of calling use
	# This prevents "Subroutine ... redefined" warnings
	eval {
		require "VCL/Module/Semaphore.pm";
		import VCL::Module::Semaphore;
	};
	my $semaphore = VCL::Module::Semaphore->new({'data_structure' => $self->data, mn_os => $self->mn_os});
	if (!$semaphore) {
		notify($ERRORS{'WARNING'}, 0, "failed to create semaphore object");
		return;
	}
	
	my $semaphore_object_address = sprintf('%x', $semaphore);
	
	if ($semaphore->obtain($semaphore_identifier, $semaphore_expire_seconds, $attempt_delay_seconds)) {
		notify($ERRORS{'DEBUG'}, 0, "obtained semaphore with identifier: '$semaphore_identifier', memory address: $semaphore_object_address");
		return $semaphore;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "failed to obtain semaphore with identifier: '$semaphore_identifier'");
		return;
	}
}

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

=head2 set_admin_message_variable

 Parameters  : $admin_message_key, $subject, $message
 Returns     : boolean
 Description : Sets an administrative message variable in the database.

=cut

sub set_admin_message_variable {
	my $self = shift;
	unless (ref($self) && $self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my ($admin_message_key, $subject, $message) = @_;
	if (!defined($admin_message_key)) {
		notify($ERRORS{'WARNING'}, 0, "message key argument was not supplied");
		return;
	}
	elsif (!defined($subject)) {
		notify($ERRORS{'WARNING'}, 0, "subject argument was not supplied\n" . format_data(\@_));
		return;
	}
	elsif (!defined($message)) {
		notify($ERRORS{'WARNING'}, 0, "message argument was not supplied");
		return;
	}
	
	my $variable_name = "adminmessage|$admin_message_key";
	
	my $variable_value = {
		subject => $subject,
		message => $message,
	};
	
	if (!set_variable($variable_name, $variable_value)) {
		return;
	}
	
	# Test retrieving the variable
	return $self->get_admin_message($admin_message_key);
}

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

=head2 set_user_message_variable

 Parameters  : $user_message_key, $affiliation_identifier, $subject, $message, $short_message (optional)
 Returns     : boolean
 Description : Sets a user message variable in the database.

=cut

sub set_user_message_variable {
	my $self = shift;
	unless (ref($self) && $self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my ($user_message_key, $affiliation_identifier, $subject, $message, $short_message) = @_;
	if (!defined($user_message_key)) {
		notify($ERRORS{'WARNING'}, 0, "key argument was not supplied");
		return;
	}
	elsif (!defined($affiliation_identifier)) {
		notify($ERRORS{'WARNING'}, 0, "affiliation identifier argument was not supplied");
		return;
	}
	elsif (!defined($subject)) {
		notify($ERRORS{'WARNING'}, 0, "subject argument was not supplied\n" . format_data(\@_));
		return;
	}
	elsif (!defined($message)) {
		notify($ERRORS{'WARNING'}, 0, "message argument was not supplied");
		return;
	}
	
	
	# Determine the affiliation name from the $affiliation_identifier argument
	my $affiliation_info = get_affiliation_info($affiliation_identifier);
	if (!$affiliation_info) {
		notify($ERRORS{'WARNING'}, 0, "failed to set user message variable, affiliation info could not be retrieved for identifier argument: '$affiliation_identifier'");
		return;
	}
	my $affiliation_name = $affiliation_info->{name};
	
	my $variable_name = "usermessage|$user_message_key|$affiliation_name";
	
	my $variable_value = {
		subject => $subject,
		message => $message,
		short_message => $short_message,
	};
	
	if (!set_variable($variable_name, $variable_value)) {
		return;
	}
	
	# Test retrieving the variable
	return $self->_get_message_variable($user_message_key);
}

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

=head2 _get_message_variable

 Parameters  : $message_key, $return_short_message (optional), $admin_message (optional)
 Returns     : array context, array: ($subject, $message)
               scalar context, string: $message
 Description : Retrieves message components from the variable table in the
               database. This is a helper subroutine and should not be called
               directly from outside this module file.
               
               The composition of the variable.name field varies based on
               whether the message is intended for end users or for
               administrators of the VCL system. variable.name will begin with
               either of the following:
               usermessage|
               adminmessage|
               
               The $message_key argument is a string that identifies the
               message to retrieve. It is treated the same for both user and
               admin-intended messages.
               
               Admin-intended messages cannot be customized per affiliation. The
               composition of variable.name is as follows:
               adminmessage|<Message Key>
               
               Example:
               adminmessage|image_creation_failed
               
               User-intended messages may be customized based on the user's
               affiliation and the variable.name field contains an additional
               affiliation name component:
               usermessage|<Message Key>|<Affiliation Name>
               
               Example:
               usermessage|timeout_inactivity|Global
               
               The database schema contains default message entries for the
               'Global' affiliation. For user-intended messages, if there is an
               entry that matches the user's affiliation name, that message will
               be returned. If not, the Global affiliation message will be
               returned by default.
               
               The variable.value field contains a YAML-encoded hash data
               structure. The following hash keys are recognized:
               * subject (required)
               * message (required)
               * short_message (optional)
               
               The subject and message values will be used when sending email
               messages. The short_message key is optional and will be used when
               sending console, desktop, or IM messages to users.
               
               The $return_short_message argument controls whether to return the
               value of message (default) or short_message.
               
               The $admin_message argument controls whether to retrieve
               messages with a variable.name beginning with 'usermessage'
               (default) or 'adminmessage'.

=cut

sub _get_message_variable {
	my $self = shift;
	unless (ref($self) && $self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my ($message_key, $return_short_message, $admin_message) = @_;
	if (!defined($message_key)) {
		notify($ERRORS{'WARNING'}, 0, "key argument was not supplied");
		return;
	}
	
	my $message_type = ($admin_message ? 'admin' : 'user');
	
	# Assemble the variable name
	my $variable_name;
	if ($admin_message) {
		# Assemble admin message variable name
		$variable_name= "adminmessage|$message_key";
	}
	else {
		# Assemble user message variable name
		my $user_affiliation_name = $self->data->get_user_affiliation_name();
		$variable_name= "usermessage|$message_key|$user_affiliation_name";
		
		# Check if the affiliation-specific variable is set, if not revert to Global
		if (!is_variable_set($variable_name)) {
			notify($ERRORS{'DEBUG'}, 0, "affiliation-specific variable is NOT set in database: $variable_name");
			$variable_name = "usermessage|$message_key|Global";
		}
	}
	
	# Retrieve the variable from the database
	my $variable = get_variable($variable_name);
	if (!defined($variable)) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve $message_type message variable, failed to retieve variable matching name: '$variable_name'");
		return;
	}
	
	# Make sure the variable contains subject key
	my $subject = $variable->{subject};
	if (!defined($subject)) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve $message_type message variable: '$variable_name', variable stored in database does not contain a {subject} key:\n" . format_data($variable));
		return;
	}
	
	# Check if supposed to return short message, return long message if not defined
	my $message;
	if ($return_short_message) {
		if ($variable->{short_message}) {
			$message = $variable->{short_message};
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "short message was requested but not defined in '$variable_name' variable");
		}
	}
	$message = $variable->{message} if !defined($message);
	
	# Make sure message was determined
	if (!defined($message)) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve $message_type message variable: '$variable_name', variable stored in database does not contain a {message} key:\n" . format_data($variable));
		return;
	}
	
	my $subject_substituted = $self->data->substitute_string_variables($subject);
	my $message_substituted = $self->data->substitute_string_variables($message);
	if (!defined($subject_substituted) || !defined($message_substituted)) {
		notify($ERRORS{'WARNING'}, 0, "retrieved $message_type message variable '$variable_name' but failed to substitute text");
		return;
	}
	
	# Remove leading and trailing newlines from message
	$message_substituted =~ s/(^\n+|\n+$)//g;
	
	if (wantarray) {
		notify($ERRORS{'DEBUG'}, 0, "retrieved $message_type message variable: $variable_name, returning array:\n" .
			"subject: $subject_substituted\n" .
			"message:\n$message_substituted"
		);
		return ($subject_substituted, $message_substituted);
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "retrieved $message_type message variable: '$variable_name', returning message string:\n$message_substituted");
		return $message_substituted;
	}
}

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

=head2 get_user_message

 Parameters  : $user_message_key
 Returns     : array context, array: ($subject, $message)
               scalar context, string: $message
 Description : Retrieves user messages.

=cut

sub get_user_message {
	my $self = shift;
	unless (ref($self) && $self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my ($user_message_key) = @_;
	return $self->_get_message_variable($user_message_key);
}

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

=head2 get_user_short_message

 Parameters  : $user_message_key
 Returns     : array context, array: ($subject, $short_message)
               scalar context, string: $short_message
 Description : Retrieves user short messages.

=cut

sub get_user_short_message {
	my $self = shift;
	unless (ref($self) && $self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my ($user_message_key) = @_;
	return $self->_get_message_variable($user_message_key, 1);
}

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

=head2 get_admin_message

 Parameters  : $admin_message_key
 Returns     : array context, array: ($subject, $message)
               scalar context, string: $message
 Description : Retrieves administrative messages.

=cut

sub get_admin_message {
	my $self = shift;
	unless (ref($self) && $self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my ($admin_message_key) = @_;
	return $self->_get_message_variable($admin_message_key, 0, 1);
}

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

=head2 setup_get_menu

 Parameters  : none
 Returns     : hash reference
 Description : Constructs the general menu items used when 'vcld -setup' is
               invoked.

=cut

sub setup_get_menu {
	return {
		'User Accounts' => {
			'Add Local VCL User Account' => \&setup_add_local_account,
			'Set Local VCL User Account Password' => \&setup_set_local_account_password,
		},
		'Management Node Configuration' => {
			'Test RPC-XML Access' => \&setup_test_rpc_xml,
		}
	};
}

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

=head2 setup_add_local_account

 Parameters  : none
 Returns     : boolean
 Description : Presents an interface to create a local VCL user account. This
               subroutine is executed when vcld is run with the -setup argument.

=cut

sub setup_add_local_account {
	my $self = shift;
	unless (ref($self) && $self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	#myusername', 'myfirstname', 'mylastname', 'myemailaddr',
	
	# Get the username (user.unityid)
	my $username;
	while (!$username) {
		$username = setup_get_input_string("Enter the user login name");
		return if (!defined($username));
		
		# Check format of username
		if ($username !~ /^[\w\-_]+$/i) {
			print "User name is not valid: '$username'\n\n";
			$username = undef;
		}
		
		# Make sure username does not already exist
		my $user_info = get_user_info($username, 'Local');
		if ($user_info && $user_info->{unityid} eq $username) {
			print "Local VCL user account already exists: $username\n\n";
			$username = undef;
		}
	}
	print "\n";
	
	# Get the other required information
	my $first_name;
	while (!$first_name) {
		$first_name = setup_get_input_string("Enter the first name");
		return if (!defined($first_name));
	}
	print "\n";
	
	my $last_name;
	while (!$last_name) {
		$last_name = setup_get_input_string("Enter the last name");
		return if (!defined($last_name));
	}
	print "\n";
	
	my $email_address;
	while (!defined($email_address)) {
		$email_address = setup_get_input_string("Enter the email address", 'not set');
		return if (!defined($email_address));
		
		# Check format of the email address
		if ($email_address eq 'not set') {
			$email_address = '';
		}
		elsif ($email_address !~ /^([A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}(,?))+$/i) {
			print "Email address is not valid: '$email_address'\n\n";
			$email_address = undef;
		}
	}
	print "\n";
	
	my $password;
	while (!$password) {
		$password = setup_get_input_string("Enter the password");
		return if (!defined($password));
	}
	print "\n";
	
	# Generate an 8-character random string
	my @characters = ("a" .. "z", "A" .. "Z", "0" .. "9");
	my $random_string;
	srand;
	for (1 .. 8) {
		$random_string .= $characters[rand((scalar(@characters) - 1))];
	}
	
	# Get an SHA1 hex digest from the password and random string
	my $digest = sha1_hex("$password$random_string");
	
	# Insert a row into the user table
	my $insert_user_statement = <<EOF;
INSERT INTO user
(unityid, affiliationid, firstname, lastname, email, lastupdated)
VALUES
('$username', (SELECT id FROM affiliation WHERE name LIKE 'Local'), '$first_name', '$last_name', '$email_address', NOW())
EOF
	
	my $user_id = database_execute($insert_user_statement);
	if (!defined($user_id)) {
		print "ERROR: failed to insert into user table\n";
		return;
	}
	
	# Insert a row into the localauth table
	my $insert_localauth_statement = <<EOF;
INSERT INTO localauth
(userid, passhash, salt, lastupdated)
VALUES
($user_id, '$digest', '$random_string', NOW())
EOF
	
	my $localauth_id = database_execute($insert_localauth_statement);
	if (!defined($localauth_id)) {
		print "ERROR: failed to insert into localauth table\n";
		return;
	}
	
	print "Local VCL user account successfully created: $username\n";
	
	return 1;
}

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

=head2 setup_add_local_account

 Parameters  : none
 Returns     : boolean
 Description : Presents an interface to create a local VCL user account. This
               subroutine is executed when vcld is run with the -setup argument.

=cut

sub setup_test_rpc_xml {
	my $self = shift;
	unless (ref($self) && $self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	my $verbose = shift;
	if (!defined($verbose)) {
		$verbose = 1;
	}

	my $error_count = 0;
	my $user_id;
	
	if (!$XMLRPC_URL) {
		print "PROBLEM: xmlrpc_url is not configured in $CONF_FILE_PATH\n";
		$error_count++;
	}
	
	if (!$XMLRPC_USER) {
		print "PROBLEM: xmlrpc_username is not configured in $CONF_FILE_PATH\n";
		$error_count++;
	}
	elsif ($XMLRPC_USER !~ /.@./) {
		print "PROBLEM: xmlrpc_username value is not valid: '$XMLRPC_USER', the format must be 'username" . '@' . "affiliation_name'\n";
		$error_count++;
	}
	else {
		my ($username, $user_affiliation_name) = $XMLRPC_USER =~ /(.+)@(.+)/;
		
		my $affiliation_ok = 0;
		
		my $affiliation_info = get_affiliation_info();
		if (!$affiliation_info) {
			print "WARNING: unable to retrieve affiliation info from the database, unable to determine if affilation '$user_affiliation_name' is valid\n";
		}
		else {
			for my $affiliation_id (keys(%$affiliation_info)) {
				my $affiliation_name = $affiliation_info->{$affiliation_id}{name};
				if ($user_affiliation_name =~ /^$affiliation_name$/i) {
					print "OK: verified user affiliation exists in the database: '$affiliation_name'\n";
					$affiliation_ok = 1;
					last;
				}
			}
			if (!$affiliation_ok) {
				print "PROBLEM: user affiliation '$user_affiliation_name' does not exist in the database\n";
				$error_count++;
			}
		}
		
		if ($affiliation_ok) {
			my $user_info = get_user_info($username, $user_affiliation_name);
			if ($user_info) {
				print "OK: verified user exists in the database: '$XMLRPC_USER'\n";
				$user_id = $user_info->{id};
			}
			else {
				print "PROBLEM: user does not exist in the database database: username: '$username', affiliation: '$user_affiliation_name'\n";
				$error_count++;
			}
			
			if (!$XMLRPC_PASS) {
				print "not verifying user password because xmlrpc_pass is not set in $CONF_FILE_PATH\n";
			}
			elsif ($user_affiliation_name !~ /^local$/i) {
				print "not verifying user password because $XMLRPC_USER is not a local account\n";
			}
			elsif (!$user_info->{localauth}) {
				print "WARNING: not verifying user password because localauth information could not be retrieved from the database\n";
			}
			else {
				my $passhash = $user_info->{localauth}{passhash};
				my $salt = $user_info->{localauth}{salt};
				
				#print "verifying user password: '$XMLRPC_PASS':'$salt' =? '$passhash'\n";
				
				# Get an SHA1 hex digest from the password and random string
				my $digest = sha1_hex("$XMLRPC_PASS$salt");
				
				if ($passhash eq $digest) {
					print "OK: verfied xmlrpc_pass value is the correct password for $XMLRPC_USER\n";
				}
				else {
					print "PROBLEM: xmlrpc_pass value configured in $CONF_FILE_PATH is not correct\n";
					#print "localauth.passhash: $passhash\n";
					#print "localauth.salt: $salt\n";
					#print "xmlrpc_pass: $XMLRPC_PASS\n";
					#print "calculated SHA1 digest ('$XMLRPC_PASS$salt'): $digest\n";
					#print "'$digest' != '$passhash'";
					$error_count++;
				}
			}
		}
	}

	if (!$XMLRPC_PASS) {
		print "PROBLEM: xmlrpc_pass is not configured in $CONF_FILE_PATH\n";
		$error_count++;
	}
	
	print "\n";
	
	if ($error_count) {
		print "FAILURE: RPC-XML access is not configured correctly, errors encountered: $error_count\n";
		return 0;
	}
	
	my $xmlrpc_function = 'system.listMethods';
	my @xmlrpc_arguments = (
		$xmlrpc_function,
	);
	
	my $response = xmlrpc_call(@xmlrpc_arguments);
	if ($response && $response->value) {
		print "SUCCESS: RPC-XML access is configured correctly\n" . format_data($response->value) . "\n" if ($verbose == 1);
		return 1;
	}
	
	
	if (!$ENV->{rpc_xml_error}) {
		print "FAILURE: RPC-XML access is not configured correctly, view the log file for more information: $LOGFILE\n";
		return 0;
	}
	
	print "FAILURE: RPC-XML access is not configured correctly, error message:\n$ENV->{rpc_xml_error}\n\n";
	
	if ($ENV->{rpc_xml_error} =~ /access denied/i) {
		# Affiliation not correct
		# Affiliation not included, default affiliation isn't Local
		# Incorrect password
		print "SUGGESTION: make sure the xmlrpc_username and xmlrpc_pass values are correct in $CONF_FILE_PATH\n";
	}
	if ($ENV->{rpc_xml_error} =~ /internal server error/i) {
		# Affiliation not included in username
		# User doesn't exist but affiliation does
		# Affiliation does not exist
		print "SUGGESTION:  make sure the xmlrpc_username is correct in $CONF_FILE_PATH, current value: '$XMLRPC_USER'\n";
	}
	if ($ENV->{rpc_xml_error} =~ /internal error while processing/i) {
		# Affiliation not included in username
		# User doesn't exist but affiliation does
		# Affiliation does not exist
		print "SUGGESTION: make sure user ID $user_id has been added to the \$xmlrpcBlockAPIUsers line in the conf.php file on the web server\n";
	}
	
	return 0;
}

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

=head2 setup_set_local_account_password

 Parameters  : none
 Returns     : boolean
 Description : 

=cut

sub setup_set_local_account_password {
	my $self = shift;
	unless (ref($self) && $self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my $local_user_info = get_local_user_info();
	
	print "Select a local VCL user account:\n";
	my $user_id = setup_get_hash_choice($local_user_info, 'unityid');
	return if (!defined($user_id));
	
	my $user_login_name = $local_user_info->{$user_id}{unityid};
	
	print "Selected user: $user_login_name (id: $user_id)\n";
	
	my $password;
	while (!$password) {
		$password = setup_get_input_string("Enter the new password");
		return if (!defined($password));
	}
	
	
	# Generate an 8-character random string
	my @characters = ("a" .. "z", "A" .. "Z", "0" .. "9");
	my $random_string;
	srand;
	for (1 .. 8) {
		$random_string .= $characters[rand((scalar(@characters) - 1))];
	}
	
	# Get an SHA1 hex digest from the password and random string
	my $digest = sha1_hex("$password$random_string");
	
	# Insert a row into the localauth table
	my $insert_localauth_statement = <<EOF;
UPDATE localauth SET
passhash = '$digest',
salt = '$random_string'
WHERE
userid = $user_id
EOF
	
	if (database_execute($insert_localauth_statement)) {
		print "Reset password for local '$user_login_name' account to '$password'\n";
	}
	else {
		print "ERROR: failed to update localauth table\n";
		return;
	}
	
}

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

=head2 DESTROY

 Parameters  : none
 Returns     : nothing
 Description : Displays the module objects address and calls the super class
               destroy method if available.

=cut

sub DESTROY {
	my $self = shift;
	if (!defined($self)) {
		notify($ERRORS{'DEBUG'}, 0, "skipping VCL::Module DESTROY tasks, \$self is not defined");
		return;
	}
	
	my $address = sprintf('%x', $self);
	my $type = ref($self);
	notify($ERRORS{'DEBUG'}, 0, "destroying $type object, address: $address");
	
	# Check for an overridden destructor
	$self->SUPER::DESTROY if $self->can("SUPER::DESTROY");
} ## end sub DESTROY

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

1;
__END__

=head1 SEE ALSO

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

=cut
