#!/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::Windows - Windows OS support module

=head1 SYNOPSIS

 Needs to be written

=head1 DESCRIPTION

 This module provides...

=cut

##############################################################################
package VCL::Module::OS::Windows;

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

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

# Specify the version of this module
our $VERSION = '2.2.2';

# Specify the version of Perl to use
use 5.008000;

use strict;
use warnings;
use diagnostics;
use English '-no_match_vars';
use VCL::utils;
use File::Basename;

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

=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/Windows

=cut

our $SOURCE_CONFIGURATION_DIRECTORY = "$TOOLS/Windows";


=head2 $NODE_CONFIGURATION_DIRECTORY

 Data type   : String
 Description : Location on computer on which an image has been loaded where
               configuration files reside. The files residing on the managment
               node in the directory specified by $NODE_CONFIGURATION_DIRECTORY
               are copied to this directory.
               
               Example:
               C:\Cygwin\home\root\VCL

=cut

our $NODE_CONFIGURATION_DIRECTORY = 'C:/Cygwin/home/root/VCL';


=head2 %TIME_ZONE_INFO

 Data type   : Hash
 Description : Windows time zone code information. The hash keys are the
               numerical Windows time zone codes used for things such as
               Sysprep.

=cut

our %TIME_ZONE_INFO = (
	'Afghanistan Standard Time' => {'abbreviation' => 'KAB', 'offset' => '+04:30', 'code' => '175'},
	'Alaskan Standard Time' => {'abbreviation' => 'ALA', 'offset' => '-09:00', 'code' => '3'},
	'Arab Standard Time' => {'abbreviation' => 'BKR', 'offset' => '+03:00', 'code' => '150'},
	'Arabian Standard Time' => {'abbreviation' => 'ABT', 'offset' => '+04:00', 'code' => '165'},
	'Arabic Standard Time' => {'abbreviation' => 'BKR', 'offset' => '+03:00', 'code' => '158'},
	'Atlantic Standard Time' => {'abbreviation' => 'AST', 'offset' => '-04:00', 'code' => '50'},
	'AUS Central Standard Time' => {'abbreviation' => 'ADA', 'offset' => '+09:30', 'code' => '245'},
	'AUS Eastern Standard Time' => {'abbreviation' => 'CMS', 'offset' => '+10:00', 'code' => '255'},
	'Azerbaijan Standard Time' => {'abbreviation' => undef, 'offset' => '+04:00', 'code' => undef},
	'Azores Standard Time' => {'abbreviation' => 'AZO', 'offset' => '-01:00', 'code' => '80'},
	'Canada Central Standard Time' => {'abbreviation' => 'CST', 'offset' => '-06:00', 'code' => '25'},
	'Cape Verde Standard Time' => {'abbreviation' => 'AZO', 'offset' => '-01:00', 'code' => '83'},
	'Caucasus Standard Time' => {'abbreviation' => 'ABT', 'offset' => '+04:00', 'code' => '170'},
	'Cen. Australia Standard Time' => {'abbreviation' => 'ADA', 'offset' => '+09:30', 'code' => '250'},
	'Central America Standard Time' => {'abbreviation' => 'CST', 'offset' => '-06:00', 'code' => '33'},
	'Central Asia Standard Time' => {'abbreviation' => 'ADC', 'offset' => '+06:00', 'code' => '195'},
	'Central Brazilian Standard Time' => {'abbreviation' => undef, 'offset' => '-04:00', 'code' => undef},
	'Central Europe Standard Time' => {'abbreviation' => 'AMS', 'offset' => '+01:00', 'code' => '95'},
	'Central European Standard Time' => {'abbreviation' => 'AMS', 'offset' => '+01:00', 'code' => '100'},
	'Central Pacific Standard Time' => {'abbreviation' => 'MSN', 'offset' => '+11:00', 'code' => '280'},
	'Central Standard Time' => {'abbreviation' => 'CST', 'offset' => '-06:00', 'code' => '20'},
	'Central Standard Time (Mexico)' => {'abbreviation' => 'CST', 'offset' => '-06:00', 'code' => '30'},
	'China Standard Time' => {'abbreviation' => 'SST', 'offset' => '+08:00', 'code' => '210'},
	'Dateline Standard Time' => {'abbreviation' => 'IDLE', 'offset' => '-12:00', 'code' => '0'},
	'E. Africa Standard Time' => {'abbreviation' => 'BKR', 'offset' => '+03:00', 'code' => '155'},
	'E. Australia Standard Time' => {'abbreviation' => 'BGP', 'offset' => '+10:00', 'code' => '260'},
	'E. Europe Standard Time' => {'abbreviation' => 'BCP', 'offset' => '+02:00', 'code' => '115'},
	'E. South America Standard Time' => {'abbreviation' => 'BBA', 'offset' => '-03:00', 'code' => '65'},
	'Eastern Standard Time' => {'abbreviation' => 'EST', 'offset' => '-05:00', 'code' => '35'},
	'Egypt Standard Time' => {'abbreviation' => 'BCP', 'offset' => '+02:00', 'code' => '120'},
	'Ekaterinburg Standard Time' => {'abbreviation' => 'EIK', 'offset' => '+05:00', 'code' => '180'},
	'Fiji Standard Time' => {'abbreviation' => 'FKM', 'offset' => '+12:00', 'code' => '285'},
	'FLE Standard Time' => {'abbreviation' => 'HRI', 'offset' => '+02:00', 'code' => '125'},
	'Georgian Standard Time' => {'abbreviation' => undef, 'offset' => '+04:00', 'code' => undef},
	'GMT Standard Time' => {'abbreviation' => 'GMT', 'offset' => '+00:00', 'code' => '85'},
	'Greenland Standard Time' => {'abbreviation' => 'BBA', 'offset' => '-03:00', 'code' => '73'},
	'Greenwich Standard Time' => {'abbreviation' => 'GMT', 'offset' => '+00:00', 'code' => '90'},
	'GTB Standard Time' => {'abbreviation' => 'AIM', 'offset' => '+02:00', 'code' => '130'},
	'Hawaiian Standard Time' => {'abbreviation' => 'HAW', 'offset' => '-10:00', 'code' => '2'},
	'India Standard Time' => {'abbreviation' => 'BCD', 'offset' => '+05:30', 'code' => '190'},
	'Iran Standard Time' => {'abbreviation' => 'THE', 'offset' => '+03:30', 'code' => '160'},
	'Israel Standard Time' => {'abbreviation' => 'BCP', 'offset' => '+02:00', 'code' => '135'},
	'Korea Standard Time' => {'abbreviation' => 'SYA', 'offset' => '+09:00', 'code' => '230'},
	'Mid-Atlantic Standard Time' => {'abbreviation' => 'MAT', 'offset' => '-02:00', 'code' => '75'},
	'Mountain Standard Time' => {'abbreviation' => 'MST', 'offset' => '-07:00', 'code' => '10'},
	'Mountain Standard Time (Mexico)' => {'abbreviation' => 'MST', 'offset' => '-07:00', 'code' => '13'},
	'Myanmar Standard Time' => {'abbreviation' => 'MMT', 'offset' => '+06:30', 'code' => '203'},
	'N. Central Asia Standard Time' => {'abbreviation' => 'ADC', 'offset' => '+06:00', 'code' => '201'},
	'Namibia Standard Time' => {'abbreviation' => undef, 'offset' => '+02:00', 'code' => undef},
	'Nepal Standard Time' => {'abbreviation' => 'NPT', 'offset' => '+05:45', 'code' => '193'},
	'New Zealand Standard Time' => {'abbreviation' => 'AWE', 'offset' => '+12:00', 'code' => '290'},
	'Newfoundland Standard Time' => {'abbreviation' => 'NWF', 'offset' => '-03:30', 'code' => '60'},
	'North Asia East Standard Time' => {'abbreviation' => 'SST', 'offset' => '+08:00', 'code' => '227'},
	'North Asia Standard Time' => {'abbreviation' => 'BHJ', 'offset' => '+07:00', 'code' => '207'},
	'Pacific SA Standard Time' => {'abbreviation' => 'AST', 'offset' => '-04:00', 'code' => '56'},
	'Pacific Standard Time' => {'abbreviation' => 'PST', 'offset' => '-08:00', 'code' => '4'},
	'Romance Standard Time' => {'abbreviation' => 'AMS', 'offset' => '+01:00', 'code' => '105'},
	'Russian Standard Time' => {'abbreviation' => 'MSV', 'offset' => '+03:00', 'code' => '145'},
	'SA Eastern Standard Time' => {'abbreviation' => 'BBA', 'offset' => '-03:00', 'code' => '70'},
	'SA Pacific Standard Time' => {'abbreviation' => 'EST', 'offset' => '-05:00', 'code' => '45'},
	'SA Western Standard Time' => {'abbreviation' => 'AST', 'offset' => '-04:00', 'code' => '55'},
	'Samoa Standard Time' => {'abbreviation' => 'MIS', 'offset' => '-11:00', 'code' => '1'},
	'SE Asia Standard Time' => {'abbreviation' => 'BHJ', 'offset' => '+07:00', 'code' => '205'},
	'Singapore Standard Time' => {'abbreviation' => 'SST', 'offset' => '+08:00', 'code' => '215'},
	'South Africa Standard Time' => {'abbreviation' => 'BCP', 'offset' => '+02:00', 'code' => '140'},
	'Sri Lanka Standard Time' => {'abbreviation' => 'ADC', 'offset' => '+06:00', 'code' => '200'},
	'Taipei Standard Time' => {'abbreviation' => 'SST', 'offset' => '+08:00', 'code' => '220'},
	'Tasmania Standard Time' => {'abbreviation' => 'HVL', 'offset' => '+10:00', 'code' => '265'},
	'Tokyo Standard Time' => {'abbreviation' => 'OST', 'offset' => '+09:00', 'code' => '235'},
	'Tonga Standard Time' => {'abbreviation' => 'TOT', 'offset' => '+13:00', 'code' => '300'},
	'US Eastern Standard Time' => {'abbreviation' => 'EST', 'offset' => '-05:00', 'code' => '40'},
	'US Mountain Standard Time' => {'abbreviation' => 'MST', 'offset' => '-07:00', 'code' => '15'},
	'Vladivostok Standard Time' => {'abbreviation' => 'HVL', 'offset' => '+10:00', 'code' => '270'},
	'W. Australia Standard Time' => {'abbreviation' => 'SST', 'offset' => '+08:00', 'code' => '225'},
	'W. Central Africa Standard Time' => {'abbreviation' => 'AMS', 'offset' => '+01:00', 'code' => '113'},
	'W. Europe Standard Time' => {'abbreviation' => 'AMS', 'offset' => '+01:00', 'code' => '110'},
	'West Asia Standard Time' => {'abbreviation' => 'EIK', 'offset' => '+05:00', 'code' => '185'},
	'West Pacific Standard Time' => {'abbreviation' => 'BGP', 'offset' => '+10:00', 'code' => '275'},
	'Yakutsk Standard Time' => {'abbreviation' => 'SYA', 'offset' => '+09:00', 'code' => '240'},
);

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

=head1 INTERFACE OBJECT METHODS

=cut

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

=head2 pre_capture

 Parameters  : Hash containing 'end_state' key
 Returns     : If successful: true
               If failed: false
 Description : Performs the steps necessary to prepare a Windows OS before an
               image is captured.
               This subroutine is called by a provisioning module's capture()
               subroutine.
               
               The steps performed are:

=over 3

=cut

sub pre_capture {
	my $self = shift;
	my $args = shift;
	if (ref($self) !~ /windows/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();
	my $image_os_install_type = $self->data->get_image_os_install_type();

	notify($ERRORS{'OK'}, 0, "beginning Windows image capture preparation tasks on $computer_node_name");

=item 1

 Log off all currently logged in users

=cut

	if (!$self->logoff_users()) {
		notify($ERRORS{'WARNING'}, 0, "unable to log off all currently logged in users on $computer_node_name");
		return 0;
	}

=item *

 Set root account password to known value

=cut

	if (!$self->set_password('root', $WINDOWS_ROOT_PASSWORD)) {
		notify($ERRORS{'WARNING'}, 0, "unable to set root password");
		return 0;
	}

=item *

 Delete the users assigned to this reservation

=cut

	my $deleted_users = $self->delete_users();
	if (!$deleted_users) {
		notify($ERRORS{'WARNING'}, 0, "unable to delete users, will try again after reboot");
	}

=item *

 Set root as the owner of /home/root

=cut

	if (!$self->set_file_owner('/home/root', 'root')) {
		notify($ERRORS{'WARNING'}, 0, "unable to set root as the owner of /home/root");
		return 0;
	}

=item *

 Copy the capture configuration files to the computer (scripts, utilities, drivers...)

=cut

	if (!$self->copy_capture_configuration_files()) {
		notify($ERRORS{'WARNING'}, 0, "unable to copy general Windows capture configuration files to $computer_node_name");
		return 0;
	}

=item *

 Apply Windows security templates

=cut

	# This find any .inf security template files configured for the OS and run secedit.exe to apply them
	if (!$self->apply_security_templates()) {
		notify($ERRORS{'WARNING'}, 0, "unable to apply security templates");
		return 0;
	}

=item *

 Disable autoadminlogon before disabling the pagefile and rebooting

=cut

	if (!$self->disable_autoadminlogon()) {
		notify($ERRORS{'WARNING'}, 0, "unable to disable autoadminlogon");
		return 0;
	}

=item *

 Disable ntsyslog service if it exists on the computer - it can prevent Cygwin sshd from working

=cut

	if ($self->service_exists('ntsyslog') && !$self->set_service_startup_mode('ntsyslog', 'disabled')) {
		notify($ERRORS{'WARNING'}, 0, "unable to set ntsyslog service startup mode to disabled");
		return 0;
	}

=item *

 Disable dynamic DNS

=cut

	if (!$self->disable_dynamic_dns()) {
		notify($ERRORS{'WARNING'}, 0, "unable to disable dynamic dns");
	}

=item *

 Disable Shutdown Event Tracker

=cut

	if (!$self->disable_shutdown_event_tracker()) {
		notify($ERRORS{'WARNING'}, 0, "unable to disable shutdown event tracker");
	}

=item *

 Disable System Restore

=cut

	if (!$self->disable_system_restore()) {
		notify($ERRORS{'WARNING'}, 0, "unable to disable system restore");
	}

=item *

 Disable hibernation

=cut

	if (!$self->disable_hibernation()) {
		notify($ERRORS{'WARNING'}, 0, "unable to disable hibernation");
	}

=item *

 Disable sleep

=cut

	if (!$self->disable_sleep()) {
		notify($ERRORS{'WARNING'}, 0, "unable to disable sleep");
	}

=item *

 Disable Windows Customer Experience Improvement program

=cut

	if (!$self->disable_ceip()) {
		notify($ERRORS{'WARNING'}, 0, "unable to disable Windows Customer Experience Improvement program");
	}

=item *

 Disable Internet Explorer configuration page

=cut

	if (!$self->disable_ie_configuration_page()) {
		notify($ERRORS{'WARNING'}, 0, "unable to disable IE configuration");
	}

=item *

 Disable Windows Defender

=cut

	if (!$self->disable_windows_defender()) {
		notify($ERRORS{'WARNING'}, 0, "unable to disable Windows Defender");
	}

=item *

 Disable Automatic Updates

=cut

	if (!$self->disable_automatic_updates()) {
		notify($ERRORS{'WARNING'}, 0, "unable to disable automatic updates");
	}

=item *

 Disable Security Center notifications

=cut

	if (!$self->disable_security_center_notifications()) {
		notify($ERRORS{'WARNING'}, 0, "unable to disable Security Center notifications");
	}

=item *

 Disable login screensaver if computer is a VM

=cut

	if ($image_os_install_type =~ /vm/i) {
		if (!$self->disable_security_center_notifications()) {
			notify($ERRORS{'WARNING'}, 0, "unable to disable Security Center notifications");
		}
	}

=item *

 Enable audio redirection for RDP sessions

=cut

	if (!$self->enable_rdp_audio()) {
		notify($ERRORS{'WARNING'}, 0, "unable to enable RDP audio redirection");
	}

=item *

 Clean up the hard drive

=cut

	if (!$self->clean_hard_drive()) {
		notify($ERRORS{'WARNING'}, 0, "unable to clean unnecessary files the hard drive");
	}

=item *

 Defragment hard drive

=cut

	if (!$self->defragment_hard_drive()) {
		notify($ERRORS{'WARNING'}, 0, "unable to defragment the hard drive");
	}

=item *

 Disable the pagefile, reboot, and delete pagefile.sys
 
 ********* node reboots *********

=cut

	# This will set the registry key to disable the pagefile, reboot, then delete pagefile.sys
	# Calls the reboot() subroutine, which makes sure ssh service is set to auto and firewall is open for ssh
	if (!$self->disable_pagefile()) {
		notify($ERRORS{'WARNING'}, 0, "unable to disable pagefile");
		return 0;
	}

=item *

 Delete the users assigned to this reservation if attempt before reboot failed

=cut

	if (!$deleted_users && !$self->delete_users()) {
		notify($ERRORS{'WARNING'}, 0, "unable to delete users after reboot");
		return 0;
	}

=item *

 Disable RDP access from any IP address

=cut

	if (!$self->firewall_disable_rdp()) {
		notify($ERRORS{'WARNING'}, 0, "unable to disable RDP from all addresses");
		return 0;
	}

=item *

 Enable SSH access from any IP address

=cut

	if (!$self->firewall_enable_ssh()) {
		notify($ERRORS{'WARNING'}, 0, "unable to enable SSH from any IP address");
		return 0;
	}

=item *

 Enable ping from any IP address

=cut

	if (!$self->firewall_enable_ping()) {
		notify($ERRORS{'WARNING'}, 0, "unable to enable ping from any IP address");
		return 0;
	}

=item *

 Reenable the pagefile

=cut

	if (!$self->enable_pagefile()) {
		notify($ERRORS{'WARNING'}, 0, "unable to reenable pagefile");
		return 0;
	}

=item *

 Set the Cygwin SSHD service startup mode to manual

=cut

	if (!$self->set_service_startup_mode('sshd', 'manual')) {
		notify($ERRORS{'WARNING'}, 0, "unable to set sshd service startup mode to manual");
		return 0;
	}
	
=back

=cut

	notify($ERRORS{'OK'}, 0, "returning 1");
	return 1;

} ## end sub pre_capture

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

=head2 post_load

 Parameters  : None.
 Returns     : If successful: true
               If failed: false
 Description : Performs the steps necessary to configure a Windows OS after an
               image has been loaded.
               
               This subroutine is called by a provisioning module's load()
               subroutine.
               
               The steps performed are:

=over 3

=cut

sub post_load {
	my $self = shift;
	if (ref($self) !~ /windows/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 $imagemeta_postoption = $self->data->get_imagemeta_postoption();
	
	notify($ERRORS{'OK'}, 0, "beginning Windows post-load tasks on $computer_node_name");

=item 1

 Wait for computer to respond to SSH

=cut

	if (!$self->wait_for_response(15, 900, 8)) {
		notify($ERRORS{'WARNING'}, 0, "$computer_node_name never responded to SSH");
		return 0;
	}

=item *

 Wait for root to log off

=cut

	if (!$self->wait_for_logoff('root', 2)) {
		notify($ERRORS{'WARNING'}, 0, "root account never logged off");
	}

=item *

 Log off all currently logged on users

 Do this in case autoadminlogon was enabled during the load process and the user
 account was not properly logged off.

=cut

	if (!$self->logoff_users()) {
		notify($ERRORS{'WARNING'}, 0, "failed to log off all currently logged in users");
	}

=item *

 Set root as the owner of /home/root

=cut

	if (!$self->set_file_owner('/home/root', 'root')) {
		notify($ERRORS{'WARNING'}, 0, "unable to set root as the owner of /home/root");
	}

=item *

 Set the Cygwin SSHD service startup mode to automatic
 
 The Cygwin SSHD service startup mode should be set to automatic after an image
 has been loaded and is ready to be reserved. Access will be lost if the service
 is not set to automatic and the computer is rebooted.

=cut

	if (!$self->set_service_startup_mode('sshd', 'auto')) {
		notify($ERRORS{'WARNING'}, 0, "unable to set sshd service startup mode to auto");
		return 0;
	}

=item *

 Update the SSH known_hosts file on the management node

=cut

	if (!$self->update_ssh_known_hosts()) {
		notify($ERRORS{'WARNING'}, 0, "unable to update the SSH known_hosts file on the management node");
	}

=item *

 Enable RDP access on the private network interface

=cut

	if (!$self->firewall_enable_rdp_private()) {
		notify($ERRORS{'WARNING'}, 0, "unable to enable RDP on private network");
		return 0;
	}
	
=item *

 Enable SSH access on the private network interface

=cut

	if (!$self->firewall_enable_ssh_private()) {
		notify($ERRORS{'WARNING'}, 0, "unable to enable SSH from private IP address");
		return 0;
	}

=item *

 Enable ping on the private network interface

=cut

	if (!$self->firewall_enable_ping_private()) {
		notify($ERRORS{'WARNING'}, 0, "unable to enable ping from private IP address");
		return 0;
	}
	
=item *

 Set persistent public default route

=cut

	if (!$self->set_public_default_route()) {
		notify($ERRORS{'WARNING'}, 0, "unable to set persistent public default route");
	}
	
=item *

 Configure and synchronize time

=cut

	if (!$self->configure_time_synchronization()) {
		notify($ERRORS{'WARNING'}, 0, "unable to configure and synchronize time");
	}

=item *

 Set the "My Computer" description to the image pretty name

=cut

	if (!$self->set_my_computer_name()) {
		notify($ERRORS{'WARNING'}, 0, "failed to rename My Computer");
	}

#=item *
#
#Disable NetBIOS
#
#=cut
#
#	if (!$self->disable_netbios()) {
#		notify($ERRORS{'WARNING'}, 0, "failed to disable NetBIOS");
#	}

#=item *
#
#Disable dynamic DNS
#
#=cut
#
#	if (!$self->disable_dynamic_dns()) {
#		notify($ERRORS{'WARNING'}, 0, "failed to disable dynamic DNS");
#	}

=item *

 Remove the Windows root password and other private information from the VCL configuration files

=cut

	if (!$self->sanitize_files()) {
		notify($ERRORS{'WARNING'}, 0, "failed to sanitize the files on the computer");
		return;
	}

=item *

 Randomize the root account password

=cut

	my $root_random_password = getpw();
	if (!$self->set_password('root', $root_random_password)) {
		notify($ERRORS{'WARNING'}, 0, "failed to set random root password");
		return 0;
	}

=item *

 Randomize the Administrator account password

=cut

	my $administrator_random_password = getpw();
	if (!$self->set_password('Administrator', $administrator_random_password)) {
		notify($ERRORS{'WARNING'}, 0, "failed to set random Administrator password");
		return 0;
	}

=item *

 Disable sleep

=cut

	if (!$self->disable_sleep()) {
		notify($ERRORS{'WARNING'}, 0, "unable to disable sleep");
	}

=item *

 Check if the imagemeta postoption is set to reboot, reboot if necessary

=cut

	if ($imagemeta_postoption =~ /reboot/i) {
		notify($ERRORS{'OK'}, 0, "imagemeta postoption reboot is set for image, rebooting computer");
		if (!$self->reboot()) {
			notify($ERRORS{'WARNING'}, 0, "failed to reboot the computer");
			return 0;
		}
	}

=item *

 Add a line to currentimage.txt indicating post_load has run

=cut

	$self->set_vcld_post_load_status();

=back

=cut

	notify($ERRORS{'OK'}, 0, "returning 1");
	return 1;
} ## end sub post_load

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

=head2 reserve

 Parameters  :
 Returns     :
 Description :

=cut

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

	my $request_forimaging   = $self->data->get_request_forimaging();
	my $reservation_password = $self->data->get_reservation_password();

	notify($ERRORS{'OK'}, 0, "beginning Windows reserve tasks");

	# Check if this is an imaging request or not
	if ($request_forimaging) {
		# Imaging request, don't create account, set the Administrator password
		if (!$self->set_password('Administrator', $reservation_password)) {
			notify($ERRORS{'WARNING'}, 0, "unable to set password for Administrator account");
			return 0;
		}
	}
	else {
		# Add the users to the computer
		# The add_users() subroutine will add the primary reservation user and any imagemeta group users
		if (!$self->add_users()) {
			notify($ERRORS{'WARNING'}, 0, "unable to add users");
			return 0;
		}
	}

	notify($ERRORS{'OK'}, 0, "returning 1");
	return 1;
} ## end sub reserve

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

=head2 sanitize

 Parameters  :
 Returns     :
 Description :

=cut

sub sanitize {
	my $self = shift;
	if (ref($self) !~ /windows/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();

	# Revoke access
	if (!$self->revoke_access()) {
		notify($ERRORS{'WARNING'}, 0, "failed to revoke access to $computer_node_name");
		return 0;
	}

	# Delete all users associated with the reservation
	# This includes the primary reservation user and users listed in imagemeta group if it's configured
	if ($self->delete_users()) {
		notify($ERRORS{'OK'}, 0, "users have been deleted from $computer_node_name");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to delete users from $computer_node_name");
		return 0;
	}

	notify($ERRORS{'OK'}, 0, "$computer_node_name has been sanitized");
	return 1;
} ## end sub sanitize

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

=head2 grant_access

 Parameters  :
 Returns     :
 Description :

=cut

sub grant_access {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path();
	my $remote_ip            = $self->data->get_reservation_remote_ip();
	my $multiple_users       = $self->data->get_imagemeta_usergroupmembercount();
	my $request_forimaging   = $self->data->get_request_forimaging();

	# Check to make sure remote IP is defined
	my $remote_ip_range;
	if (!$remote_ip) {
		notify($ERRORS{'WARNING'}, 0, "reservation remote IP address is not set in the data structure, opening RDP to any address");
	}
	elsif ($multiple_users) {
		notify($ERRORS{'OK'}, 0, "reservation has multiple users, opening RDP to any address");
	}
	elsif ($remote_ip !~ /^(\d{1,3}\.?){4}$/) {
		notify($ERRORS{'WARNING'}, 0, "reservation remote IP address format is invalid: $remote_ip, opening RDP to any address");
	}
	else {
		# Assemble the IP range string in CIDR notation
		$remote_ip_range = "$remote_ip/16";
		notify($ERRORS{'OK'}, 0, "RDP will be allowed from $remote_ip_range on $computer_node_name");
	}

	# Set the $remote_ip_range variable to the string 'all' if it isn't already set (for display purposes)
	$remote_ip_range = 'all' if !$remote_ip_range;

	# Allow RDP connections
	if ($self->firewall_enable_rdp($remote_ip_range)) {
		notify($ERRORS{'OK'}, 0, "firewall was configured to allow RDP access from $remote_ip_range on $computer_node_name");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "firewall could not be configured to grant RDP access from $remote_ip_range on $computer_node_name");
		return 0;
	}

	# If this is an imaging request, make sure the Administrator account is enabled
	if ($request_forimaging) {
		notify($ERRORS{'DEBUG'}, 0, "imaging request, making sure Administrator account is enabled");
		if ($self->enable_user('Administrator')) {
			notify($ERRORS{'OK'}, 0, "Administrator account is enabled for imaging request");
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to enable Administrator account for imaging request");
			return 0;
		}
	} ## end if ($request_forimaging)
	
	# Delete legacy VCL logon/logoff scripts
	if (!$self->delete_files_by_pattern("$system32_path/GroupPolicy/User/Scripts", ".*VCL.*cmd", 2)) {
		notify($ERRORS{'WARNING'}, 0, "failed to delete legacy VCL logon and logoff scripts");
	}

	notify($ERRORS{'OK'}, 0, "access has been granted for reservation on $computer_node_name");
	return 1;
} ## end sub grant_access

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

=head2 revoke_access

 Parameters  :
 Returns     :
 Description :

=cut

sub revoke_access {
	my $self = shift;
	if (ref($self) !~ /windows/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();

	# Disallow RDP connections
	if ($self->firewall_disable_rdp()) {
		notify($ERRORS{'OK'}, 0, "firewall was configured to deny RDP access on $computer_node_name");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "firewall could not be configured to deny RDP access on $computer_node_name");
		return 0;
	}

	notify($ERRORS{'OK'}, 0, "access has been revoked to $computer_node_name");
	return 1;
} ## end sub revoke_access

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

=head1 AUXILIARY OBJECT METHODS

=cut

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

=head2 create_directory

 Parameters  :
 Returns     :
 Description :

=cut

sub create_directory {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	
	my $path = shift;
	if (!$path) {
		notify($ERRORS{'WARNING'}, 0, "directory path argument was not specified");
		return;
	}

	notify($ERRORS{'DEBUG'}, 0, "attempting to create directory: '$path'");

	# Assemble the Windows shell mkdir command and execute it
	my $mkdir_command = "cmd.exe /c \"mkdir \\\"$path\\\"\"";
	my ($mkdir_exit_status, $mkdir_output) = run_ssh_command($computer_node_name, $management_node_keys, $mkdir_command, '', '', 1);
	
	if (!defined($mkdir_output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to create directory on $computer_node_name: $path");
		return;
	}
	elsif ($mkdir_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "created directory on $computer_node_name: '$path'");
	}
	elsif (grep(/already exists/i, @$mkdir_output)) {
		notify($ERRORS{'OK'}, 0, "directory already exists on $computer_node_name: '$path'");
		return 1;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to create directory on $computer_node_name: '$path', exit status: $mkdir_exit_status, output:\n" . join("\n", @$mkdir_output));
	}

	# Make sure directory was created
	if (!$self->file_exists($path)) {
		notify($ERRORS{'WARNING'}, 0, "directory does not exist on $computer_node_name: '$path'");
		return 0;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "verified directory exists on $computer_node_name: '$path'");
		return 1;
	}
} ## end sub create_directory

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

=head2 delete_file

 Parameters  :
 Returns     :
 Description :

=cut

sub delete_file {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Get file path subroutine argument
	my $path_argument = shift;
	if (!$path_argument) {
		notify($ERRORS{'WARNING'}, 0, "file path was not specified as an argument");
		return;
	}
	
	# Check if file exists before attempting to delete it
	if (!$self->file_exists($path_argument)) {
		notify($ERRORS{'OK'}, 0, "failed not deleted because it does not exist: '$path_argument'");
		return 1;
	}
	
	my $path_unix = $self->format_path_unix($path_argument);
	my $path_dos = $self->format_path_dos($path_argument);
	
	notify($ERRORS{'DEBUG'}, 0, "attempting to delete file: '$path_argument'");
	
	# Assemble a set of commands concatenated together
	# Try to take ownership, set the permissions, then delete the file using both Cygwin bash and Windows commands
	# This should allow files to be deleted with restrictive ownership, permissions, and attributes
	
	my $command;
	$command .= "echo ---";
	$command .= " ; echo Calling chown.exe to change owner to root...";
	$command .= " ; /usr/bin/chown.exe -Rv root $path_unix 2>&1";
	
	$command .= " ; echo ---";
	$command .= " ; echo Calling chmod.exe to change permissions to 777...";
	$command .= " ; /usr/bin/chmod.exe -Rv 777 $path_unix 2>&1";
	
	$command .= " ; echo ---";
	$command .= " ; echo Calling \\\"rm.exe -rfv $path_unix\\\" to to delete file...";
	$command .= " ; /usr/bin/rm.exe -rfv $path_unix 2>&1";
	
	# Add call to rmdir if the path does not contain a wildcard
	# rmdir does not accept wildcards
	if ($path_dos !~ /\*/) {
		$command .= " ; echo ---";
		$command .= " ; echo Calling \\\"cmd.exe /c rmdir $path_dos\\\" to to delete directory...";
		$command .= " ; cmd.exe /c \"rmdir /s /q \\\"$path_dos\\\"\" 2>&1";
	}
	
	$command .= " ; echo ---";
	$command .= " ; echo Calling \\\"cmd.exe /c del $path_dos\\\" to to delete file...";
	$command .= " ; cmd.exe /c \"del /s /q /f /a \\\"$path_dos\\\" 2>&1\" 2>&1";
	
	$command .= " ; echo ---";
	$command .= " ; echo Calling \\\"cmd.exe /c dir $path_dos\\\" to to list remaining files...";
	$command .= " ; cmd.exe /c \"dir /a /w \\\"$path_dos\\\"\" 2>&1";
	
	$command .= " ; echo ---";
	$command .= " ; date +%r";
	
	
	# Run the command
	my ($exit_status, $output) = run_ssh_command($computer_node_name, $management_node_keys, $command, '', '', 0);
	if (!defined($exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to delete file: '$path_argument'");
		return;
	}
	
	## Sleep 1 second before checking if file was deleted
	#sleep 1;
	
	# Check if file was deleted
	if ($self->file_exists($path_argument)) {
		notify($ERRORS{'WARNING'}, 0, "failed to delete file, it still exists: '$path_argument', command:\n$command\noutput:\n" . join("\n", @$output));
		return;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "deleted file: '$path_argument'");
		return 1;
	}
}

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

=head2 move_file

 Parameters  :
 Returns     :
 Description :

=cut

sub move_file {
	my $self = shift;
	if (ref($self) !~ /windows/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();

	# Get file path subroutine arguments
	my $source_path = shift;
	my $destination_path = shift;
	if (!$source_path) {
		notify($ERRORS{'WARNING'}, 0, "file source path was not specified as an argument");
		return;
	}
	if (!$destination_path) {
		notify($ERRORS{'WARNING'}, 0, "file destination path was not specified as an argument");
		return;
	}
	
	# Replace backslashes with forward slashes
	$source_path =~ s/\\+/\//gs;
	$destination_path =~ s/\\+/\//gs;

	notify($ERRORS{'DEBUG'}, 0, "attempting to move file: $source_path --> $destination_path");

	# Assemble the Windows shell move command and execute it
	my $move_command = "mv -fv \"$source_path\" \"$destination_path\"";
	my ($move_exit_status, $move_output) = run_ssh_command($computer_node_name, $management_node_keys, $move_command, '', '', 1);
	if (defined($move_exit_status) && $move_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "file moved: $source_path --> $destination_path, output:\n@{$move_output}");
	}
	elsif ($move_exit_status) {
		notify($ERRORS{'WARNING'}, 0, "failed to move file: $source_path --> $destination_path, exit status: $move_exit_status, output:\n@{$move_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to move file: $source_path --> $destination_path");
		return;
	}
	
	return 1;
}

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

=head2 delete_files_by_pattern

 Parameters  :
 Returns     :
 Description :

=cut

sub delete_files_by_pattern {
	my $self = shift;
	if (ref($self) !~ /windows/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();

	my $base_directory = shift;
	my $pattern        = shift;
	my $max_depth      = shift || '5';

	# Make sure base directory and pattern were specified
	if (!($base_directory && $pattern)) {
		notify($ERRORS{'WARNING'}, 0, "base directory and pattern must be specified as arguments");
		return;
	}
	
	# Check if the path begins with an environment variable and extract it
	my ($base_directory_variable) = $base_directory =~ /(\$[^\/\\]*)/g;
	
	# Remove trailing slashes from base directory
	$base_directory =~ s/[\/\\]*$/\//;

	notify($ERRORS{'DEBUG'}, 0, "attempting to delete files under $base_directory matching pattern $pattern, max depth: $max_depth");
	
	# Assemble command
	# Use find to locate all the files under the base directory matching the pattern specified
	my $command = "/bin/find.exe \"$base_directory\" -mindepth 1 -maxdepth $max_depth -iregex \"$pattern\"";
	$command .= " -exec chown -R root {} \\;";
	$command .= " -exec chmod -R 777 {} \\;";
	$command .= " -exec rm -rvf {} \\;";
	
	# If the path begins with an environment variable, check if the variable is defined by passing it to cygpath.exe
	# Unintended files will be deleted if the environment variable is not defined because the base directory would change from "$TEMP/" to "/"
	if ($base_directory_variable) {
		$command = "/bin/cygpath.exe \"$base_directory_variable\" && $command";
	}
	
	my ($exit_status, $output) = run_ssh_command($computer_node_name, $management_node_keys, $command, '', '', 1);
	
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to delete files under $base_directory matching pattern $pattern, command: $command");
		return;
	}
	elsif (grep(/cygpath:/i, @$output)) {
		notify($ERRORS{'OK'}, 0, "files not deleted because environment variable is not set: $base_directory_variable");
		return;
	}
	elsif (grep(/find:.*no such file/i, @$output)) {
		notify($ERRORS{'OK'}, 0, "files not deleted because base directory does not exist: $base_directory");
		return 1;
	}
	elsif (grep(/(^Usage:)/i, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to delete files under $base_directory matching pattern $pattern\ncommand: $command\noutput:\n" . join("\n", @$output));
		return;
	}
	else {
		my @deleted = grep(/removed /, @$output);
		my @not_deleted = grep(/cannot remove/, @$output);
		notify($ERRORS{'OK'}, 0, scalar @deleted . "/" . scalar @not_deleted . " files deleted deleted under '$base_directory' matching '$pattern'");
		notify($ERRORS{'DEBUG'}, 0, "files/directories which were deleted:\n" . join("\n", @deleted)) if @deleted;
		notify($ERRORS{'DEBUG'}, 0, "files/directories which were NOT deleted:\n" . join("\n", @not_deleted)) if @not_deleted;
	}
	
	return 1;
} ## end sub delete_files_by_pattern

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

=head2 file_exists

 Parameters  :
 Returns     :
 Description :

=cut

sub file_exists {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	
	# Get the path from the subroutine arguments and make sure it was passed
	my $path = shift;
	if (!$path) {
		notify($ERRORS{'WARNING'}, 0, "unable to detmine if file exists, path was not specified as an argument");
		return;
	}
	
	my $path_dos = $self->format_path_dos($path);
	
	# Assemble the dir command and execute it
	my $dir_command = "cmd.exe /c \"dir /a /b \\\"$path_dos\\\"\"";
	my ($dir_exit_status, $dir_output) = run_ssh_command($computer_node_name, $management_node_keys, $dir_command, '', '', 0);
	if (!defined($dir_output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to determine if file exists on $computer_node_name: $path");
		return;
	}
	
	if ($dir_exit_status == 1 || grep(/(file not found|cannot find)/i, @$dir_output)) {
		notify($ERRORS{'DEBUG'}, 0, "file does NOT exist on $computer_node_name: '$path'");
		return 0;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "file exists on $computer_node_name: '$path'");
		return 1;
	}
}

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

=head2 set_file_owner

 Parameters  : file path, owner
 Returns     : If successful: true
               If failed: false
 Description : Recursively sets the owner of the file path.  The file path can
               be a file or directory. The owner must be a valid user account. A
               group can optionally be specified by appending a semicolon and
               the group name to the owner.
               Examples:
               set_file_owner('/home/root', 'root')
               set_file_owner('/home/root', 'root:Administrators')

=cut

sub set_file_owner {
	my $self = shift;
	if (ref($self) !~ /windows/i) {
		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 $file_path = shift;
	if (!$file_path) {
		notify($ERRORS{'WARNING'}, 0, "file path argument was not specified");
		return;
	}
	
	# Get the owner argument
	my $owner = shift;
	if (!$owner) {
		notify($ERRORS{'WARNING'}, 0, "owner argument was not specified");
		return;
	}

	my $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();
	
	# Run chown
	my ($chown_exit_status, $chown_output) = run_ssh_command($computer_node_name, $management_node_keys, "/usr/bin/chown.exe -vR \"$owner\" \"$file_path\"", '', '', 0);
	
	# Check if exit status is defined - if not, SSH command failed
	if (!defined($chown_output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to set $owner as the owner of $file_path");
		return;
	}
	
	# Check if any known error lines exist in the chown output
	my @chown_error_lines = grep(/(chown:|cannot access|no such file|failed to)/ig, @$chown_output);
	if (@chown_error_lines) {
		notify($ERRORS{'WARNING'}, 0, "error occurred setting $owner as the owner of $file_path, error output:\n" . join("\n", @chown_error_lines));
		return;
	}
	
	# Make sure an "ownership of" line exists in the chown output
	my @chown_success_lines = grep(/(ownership of)/ig, @$chown_output);
	if (@chown_success_lines) {
		notify($ERRORS{'OK'}, 0, "set $owner as the owner of $file_path, files and directories modified: " . scalar(@chown_success_lines));
	}
	else {
		notify($ERRORS{'OK'}, 0, "$owner is already the owner of $file_path");
	}
	
	return 1;
}

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

=head2 logoff_users

 Parameters  :
 Returns     :
 Description :

=cut

sub logoff_users {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;

	my ($exit_status, $output) = run_ssh_command($computer_node_name, $management_node_keys, "$system32_path/qwinsta.exe");
	if ($exit_status > 0) {
		notify($ERRORS{'WARNING'}, 0, "failed to run qwinsta.exe on $computer_node_name, exit status: $exit_status, output:\n@{$output}");
		return;
	}
	elsif (!defined($exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run qwinsta.exe SSH command on $computer_node_name");
		return;
	}
	
	# Find lines with the state = Active or Disc
	# Disc will occur if the user disconnected the RDP session but didn't logoff
	my @connection_lines = grep(/(Active)/, @{$output});
	return 1 if !@connection_lines;
	
	#notify($ERRORS{'OK'}, 0, "connections on $computer_node_name:\n@connection_lines");
	#  SESSIONNAME        USERNAME                 ID  STATE   TYPE        DEVICE
	# '>                  root                      0  Disc    rdpwd               '
	# '>rdp-tcp#24        root                      0  Active  rdpwd               '
	foreach my $connection_line (@connection_lines) {
		my ($session_id) = $connection_line =~ /(\d+)\s+(?:Active|Listen|Conn|Disc)/g;
		my ($session_name) = $connection_line =~ /^\s?>?([^ ]+)/g;
		
		# Determine if the session ID or name will be used to kill the session
		# logoff.exe has trouble killing sessions with ID=0
		# Use the ID if it's > 0, otherwise use the session name
		my $session_identifier;
		if ($session_id) {
			$session_identifier = $session_id;
		}
		elsif ($session_name) {
			$session_identifier = $session_name;
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "session ID or name could not be determined from line:\n$connection_line");
			next;
		}
		notify($ERRORS{'DEBUG'}, 0, "attempting to kill connection:\nline: '$connection_line'\nsession identifier: $session_identifier");
		
		#LOGOFF [sessionname | sessionid] [/SERVER:servername] [/V]
		#  sessionname         The name of the session.
		#  sessionid           The ID of the session.
		#  /SERVER:servername  Specifies the Terminal server containing the user
		#							 session to log off (default is current).
		#  /V                  Displays information about the actions performed.
		# Call logoff.exe, pass it the session
		my ($logoff_exit_status, $logoff_output) = run_ssh_command($computer_node_name, $management_node_keys, "$system32_path/logoff.exe $session_identifier /V");
		if ($logoff_exit_status == 0) {
			notify($ERRORS{'OK'}, 0, "logged off session: $session_identifier, output:\n" . join("\n", @$logoff_output));
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to log off session: $session_identifier, exit status: $logoff_exit_status, output:\n@{$logoff_output}");
		}
	}
	return 1;
}

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

=head2 add_users

 Parameters  : 
 Returns     : 
 Description : 

=cut

sub add_users {
	my $self = shift;
	if (ref($self) !~ /windows/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 user array from the arguments
	# If no argument was supplied, use the users specified in the DataStructure
	my $user_array_ref = shift;
	my @users;
	if ($user_array_ref) {
		$user_array_ref = $self->data->get_imagemeta_usergroupmembers();
		@users          = @{$user_array_ref};
	}
	else {
		# User list was not specified as an argument
		# Use the imagemeta group members and the primary reservation user
		my $user_login_id      = $self->data->get_user_login_id();
		my $user_group_members = $self->data->get_imagemeta_usergroupmembers();

		push @users, $user_login_id;

		foreach my $user_group_member_uid (keys(%{$user_group_members})) {
			my $user_group_member_login_id = $user_group_members->{$user_group_member_uid};
			push @users, $user_group_member_login_id;
		}

		# Remove duplicate users
		@users = keys %{{map {$_, 1} @users}};
	}

	notify($ERRORS{'DEBUG'}, 0, "attempting to add " . scalar @users . " users to $computer_node_name: " . join(", ", @users));

	# Attempt to get the password from the arguments
	# If no argument was supplied, use the password specified in the DataStructure
	my $password = shift;
	if (!$password) {
		$password = $self->data->get_reservation_password();
	}

	# Loop through the users in the imagemeta group and attempt to add them
	for my $username (@users) {
		if (!$self->create_user($username, $password)) {
			notify($ERRORS{'WARNING'}, 0, "failed to add users to $computer_node_name");
			return 0;
		}
	}

	notify($ERRORS{'OK'}, 0, "added " . scalar @users . " users to $computer_node_name");
	return 1;
} ## end sub add_users

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

=head2 delete_users

 Parameters  : 
 Returns     : 
 Description : 

=cut

sub delete_users {
	my $self = shift;
	if (ref($self) !~ /windows/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 user array from the arguments
	# If no argument was supplied, use the users specified in the DataStructure
	my $user_array_ref = shift;
	my @users;
	if ($user_array_ref) {
		$user_array_ref = $self->data->get_imagemeta_usergroupmembers();
		@users          = @{$user_array_ref};
	}
	else {
		# User list was not specified as an argument
		# Use the imagemeta group members and the primary reservation user
		my $user_login_id      = $self->data->get_user_login_id();
		my $user_group_members = $self->data->get_imagemeta_usergroupmembers();

		push @users, $user_login_id;

		foreach my $user_group_member_uid (keys(%{$user_group_members})) {
			my $user_group_member_login_id = $user_group_members->{$user_group_member_uid};
			push @users, $user_group_member_login_id;
		}

		# Remove duplicate users
		@users = keys %{{map {$_, 1} @users}};
	} ## end else [ if ($user_array_ref)

	# Loop through the users and attempt to delete them
	for my $username (@users) {
		if (!$self->delete_user($username)) {
			notify($ERRORS{'WARNING'}, 0, "failed to delete user $username from $computer_node_name");
			return 0;
		}
	}

	notify($ERRORS{'OK'}, 0, "deleted " . scalar @users . " users from $computer_node_name");
	return 1;
} ## end sub delete_users

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

=head2 user_exists

 Parameters  : 
 Returns     : 
 Description : 

=cut

sub user_exists {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;

	# 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 $username exists on $computer_node_name");

	# Attempt to query the user account
	my $query_user_command = "$system32_path/net.exe user \"$username\"";
	my ($query_user_exit_status, $query_user_output) = run_ssh_command($computer_node_name, $management_node_keys, $query_user_command, '', '', '1');
	if (defined($query_user_exit_status) && $query_user_exit_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "user $username exists on $computer_node_name");
		return 1;
	}
	elsif (defined($query_user_exit_status) && $query_user_exit_status == 2) {
		notify($ERRORS{'DEBUG'}, 0, "user $username does not exist on $computer_node_name");
		return 0;
	}
	elsif (defined($query_user_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to determine if user $username exists on $computer_node_name, 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 $username exists on $computer_node_name");
		return;
	}
} ## end sub user_exists

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

=head2 create_user

 Parameters  : 
 Returns     : 
 Description : 

=cut

sub create_user {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $imagemeta_rootaccess = $self->data->get_imagemeta_rootaccess();

	# Attempt to get the username from the arguments
	# If no argument was supplied, use the user specified in the DataStructure
	my $username = shift;
	my $password = shift;
	if (!$username) {
		$username = $self->data->get_user_login_id();
	}
	if (!$password) {
		$password = $self->data->get_reservation_password();
	}

	# Check if user already exists
	if ($self->user_exists($username)) {
		notify($ERRORS{'OK'}, 0, "user $username already exists on $computer_node_name, attempting to delete user");

		# Attempt to delete the user
		if (!$self->delete_user($username)) {
			notify($ERRORS{'WARNING'}, 0, "failed to add user $username to $computer_node_name, user already exists and could not be deleted");
			return 0;
		}
	}

	notify($ERRORS{'DEBUG'}, 0, "attempting to add user $username to $computer_node_name ($password)");

	# Attempt to add the user account
	my $add_user_command = "$system32_path/net.exe user \"$username\" \"$password\" /ADD /EXPIRES:NEVER /COMMENT:\"Account created by VCL\"";
	$add_user_command .= " && $system32_path/net.exe localgroup \"Remote Desktop Users\" \"$username\" /ADD";
	
	# Add the user to the Administrators group if imagemeta.rootaccess isn't 0
	if (defined($imagemeta_rootaccess) && $imagemeta_rootaccess eq '0') {
		notify($ERRORS{'DEBUG'}, 0, "user will NOT be added to the Administrators group");
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "user will be added to the Administrators group");
		$add_user_command .= " && $system32_path/net.exe localgroup \"Administrators\" \"$username\" /ADD";
	}

	my ($add_user_exit_status, $add_user_output) = run_ssh_command($computer_node_name, $management_node_keys, $add_user_command, '', '', '1');
	if (defined($add_user_exit_status) && $add_user_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "added user $username ($password) to $computer_node_name");
	}
	elsif (defined($add_user_exit_status) && $add_user_exit_status == 2) {
		notify($ERRORS{'OK'}, 0, "user $username was not added, user already exists");
		return 1;
	}
	elsif (defined($add_user_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to add user $username to $computer_node_name, exit status: $add_user_exit_status, output:\n@{$add_user_output}");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command add user $username to $computer_node_name");
		return;
	}

	return 1;
} ## end sub create_user

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

=head2 add_user_to_group

 Parameters  : 
 Returns     : 
 Description : 

=cut

sub add_user_to_group {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;

	# Attempt to get the username from the arguments
	# If no argument was supplied, use the user specified in the DataStructure
	my $username = shift;
	my $group    = shift;
	if (!$username || !$group) {
		notify($ERRORS{'WARNING'}, 0, "unable to add user to group, arguments were not passed correctly");
		return;
	}

	# Attempt to add the user to the group using net.exe localgroup
	my $localgroup_user_command = "$system32_path/net.exe localgroup \"$group\" $username /ADD";
	my ($localgroup_user_exit_status, $localgroup_user_output) = run_ssh_command($computer_node_name, $management_node_keys, $localgroup_user_command);
	if (defined($localgroup_user_exit_status) && $localgroup_user_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "added user $username to \"$group\" group on $computer_node_name");
	}
	elsif (defined($localgroup_user_exit_status) && $localgroup_user_exit_status == 2) {
		# Exit status is 2, this could mean the user is already a member or that the group doesn't exist
		# Check the output to determine what happened
		if (grep(/error 1378/, @{$localgroup_user_output})) {
			# System error 1378 has occurred.
			# The specified account name is already a member of the group.
			notify($ERRORS{'OK'}, 0, "user $username was not added to $group group because user already a member");
			return 1;
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to add user $username to $group group on $computer_node_name, exit status: $localgroup_user_exit_status, output:\n@{$localgroup_user_output}");
			return 0;
		}
	} ## end elsif (defined($localgroup_user_exit_status) ... [ if (defined($localgroup_user_exit_status) ...
	elsif (defined($localgroup_user_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to add user $username to $group group on $computer_node_name, exit status: $localgroup_user_exit_status, output:\n@{$localgroup_user_output}");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to add user $username to $group group on $computer_node_name");
		return;
	}

	return 1;
} ## end sub add_user_to_group

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

=head2 delete_user

 Parameters  : $node, $user, $type, $osname
 Returns     : 1 success 0 failure
 Description : removes user account and profile directory from specificed node

=cut

sub delete_user {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;

	# Attempt to get the username from the arguments
	# If no argument was supplied, use the user specified in the DataStructure
	my $username = shift;
	if (!(defined($username))) {
		$username = $self->data->get_user_login_id();
	}

	notify($ERRORS{'OK'}, 0, "attempting to delete user $username from $computer_node_name");

	# Attempt to delete the user account
	my $delete_user_command = "$system32_path/net.exe user $username /DELETE";
	my ($delete_user_exit_status, $delete_user_output) = run_ssh_command($computer_node_name, $management_node_keys, $delete_user_command);
	if (defined($delete_user_exit_status) && $delete_user_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "deleted user $username from $computer_node_name");
	}
	elsif (defined($delete_user_exit_status) && $delete_user_exit_status == 2) {
		notify($ERRORS{'OK'}, 0, "user $username was not deleted because user does not exist");
	}
	elsif (defined($delete_user_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to delete user $username from $computer_node_name, exit status: $delete_user_exit_status, output:\n@{$delete_user_output}");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command delete user $username from $computer_node_name");
		return;
	}

	# Delete the user's home directory
	if ($self->delete_file("C:/Documents and Settings/$username")) {
		notify($ERRORS{'OK'}, 0, "deleted profile for user $username from $computer_node_name");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to delete profile for user $username from $computer_node_name");
		return 0;
	}

	return 1;
} ## end sub delete_user

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

=head2 set_password

 Parameters  : $username, $password
 Returns     : 
 Description : 

=cut
sub set_password {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Attempt to get the username from the arguments
	my $username = shift;
	my $password = shift;

	# If no argument was supplied, use the user specified in the DataStructure
	if (!defined($username)) {
		$username = $self->data->get_user_logon_id();
	}
	if (!defined($password)) {
		$password = $self->data->get_reservation_password();
	}

	# Make sure both the username and password were determined
	if (!defined($username) || !defined($password)) {
		notify($ERRORS{'WARNING'}, 0, "username and password could not be determined");
		return 0;
	}

	# Attempt to set the password
	notify($ERRORS{'DEBUG'}, 0, "setting password of $username to $password on $computer_node_name");
	my ($set_password_exit_status, $set_password_output) = run_ssh_command($computer_node_name, $management_node_keys, "$system32_path/net.exe user $username '$password'");
	if ($set_password_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "password changed to '$password' for user '$username' on $computer_node_name");
	}
	elsif (defined $set_password_exit_status) {
		notify($ERRORS{'WARNING'}, 0, "failed to change password to '$password' for user '$username' on $computer_node_name, exit status: $set_password_exit_status, output:\n@{$set_password_output}");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to change password to '$password' for user '$username' on $computer_node_name");
		return 0;
	}

	# Check if root user, must set sshd service password too
	if ($username eq 'root') {
		notify($ERRORS{'DEBUG'}, 0, "root account password changed, must also change sshd service credentials");
		if (!$self->set_service_credentials('sshd', $username, $password)) {
			notify($ERRORS{'WARNING'}, 0, "failed to set sshd service credentials to $username ($password)");
			return 0;
		}
	}

	# Attempt to change scheduled task passwords
	notify($ERRORS{'DEBUG'}, 0, "changing passwords for scheduled tasks");
	my ($schtasks_query_exit_status, $schtasks_query_output) = run_ssh_command($computer_node_name, $management_node_keys, "$system32_path/schtasks.exe /Query /V /FO LIST", '', '', 0);
	if (defined($schtasks_query_exit_status) && $schtasks_query_exit_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "queried scheduled tasks on $computer_node_name");
	}
	elsif (defined $schtasks_query_exit_status) {
		notify($ERRORS{'WARNING'}, 0, "failed to query scheduled tasks on $computer_node_name, exit status: $schtasks_query_exit_status, output:\n@{$schtasks_query_output}");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to query scheduled tasks on $computer_node_name");
		return 0;
	}

	# Find scheduled tasks configured to run as this user
	my $task_name;
	my @task_names_to_update;
	for my $schtasks_output_line (@{$schtasks_query_output}) {
		if ($schtasks_output_line =~ /TaskName:\s+(.+)/i) {
			$task_name = $1;
		}
		if ($schtasks_output_line =~ /Run As User.*[\W]$username\s*$/) {
			notify($ERRORS{'DEBUG'}, 0, "password needs to be updated for scheduled task: '$task_name'");
			push @task_names_to_update, $task_name;
		}
	}

	# Loop through the scheduled tasks configured to run as the user, update the password
	for my $task_name_to_update (@task_names_to_update) {
		my $schtasks_command = "$system32_path/schtasks.exe /Change /RU \"$username\" /RP \"$password\" /TN \"$task_name_to_update\"";
		my ($schtasks_change_exit_status, $schtasks_change_output) = run_ssh_command($computer_node_name, $management_node_keys, $schtasks_command, '', '', 0);
		if (!defined($schtasks_change_output)) {
			notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to change password for scheduled task: $task_name_to_update");
			return;
		}
		elsif (grep (/^SUCCESS:/, @$schtasks_change_output)) {
			notify($ERRORS{'OK'}, 0, "changed password for scheduled task: $task_name_to_update");
		}
		elsif (grep (/The parameter is incorrect/, @$schtasks_change_output)) {
			notify($ERRORS{'WARNING'}, 0, "encountered Windows bug while attempting to change password for scheduled task: $task_name_to_update, output:\n@{$schtasks_change_output}");
			# Don't return - There is a bug in Windows 7
			# If a scheduled task is created using the GUI using a schedule the password cannot be set via schtasks.exe
			# schtasks.exe displays: ERROR: The parameter is incorrect.
			# If the same task is changed to run on an event such as logon it works
		}
		elsif (grep (/^ERROR:/, @$schtasks_change_output)) {
			notify($ERRORS{'WARNING'}, 0, "failed to change password for scheduled task: $task_name_to_update, command:\n$schtasks_command\noutput:\n@{$schtasks_change_output}");
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "unexpected output returned while attempting to change password for scheduled task: $task_name_to_update, command:\n$schtasks_command\noutput:\n@{$schtasks_change_output}");
		}
	}
	
	notify($ERRORS{'OK'}, 0, "changed password for user: $username");
	return 1;
} ## end sub set_password

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

=head2 enable_user

 Parameters  : $username (optional
 Returns     : 
 Description : 

=cut

sub enable_user {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;

	# Attempt to get the username from the arguments
	my $username = shift;

	# If no argument was supplied, use the user specified in the DataStructure
	if (!defined($username)) {
		$username = $self->data->get_user_logon_id();
	}

	# Make sure the username was determined
	if (!defined($username)) {
		notify($ERRORS{'WARNING'}, 0, "username could not be determined");
		return 0;
	}

	# Attempt to enable the user account (set ACTIVE=YES)
	notify($ERRORS{'DEBUG'}, 0, "enabling user $username on $computer_node_name");
	my ($enable_exit_status, $enable_output) = run_ssh_command($computer_node_name, $management_node_keys, "$system32_path/net.exe user $username /ACTIVE:YES");
	if ($enable_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "user $username enabled on $computer_node_name");
	}
	elsif ($enable_exit_status) {
		notify($ERRORS{'WARNING'}, 0, "failed to enable user $username on $computer_node_name, exit status: $enable_exit_status, output:\n@{$enable_output}");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to enable user $username on $computer_node_name");
		return 0;
	}

	return 1;
} ## end sub enable_user

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

=head2 disable_pagefile

 Parameters  :
 Returns     :
 Description :

=cut

sub disable_pagefile {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Set the registry key to blank
	my $memory_management_key = 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management';
	my $reg_add_command = $system32_path . '/reg.exe add "' . $memory_management_key . '" /v PagingFiles /d "" /t REG_MULTI_SZ /f';
	my ($reg_add_exit_status, $reg_add_output) = run_ssh_command($computer_node_name, $management_node_keys, $reg_add_command, '', '', 1);
	if (defined($reg_add_exit_status) && $reg_add_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "set registry key to disable pagefile");
	}
	elsif (defined($reg_add_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to set registry key to disable pagefile, exit status: $reg_add_exit_status, output:\n@{$reg_add_output}");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to set registry key to disable pagefile");
		return;
	}

	# Attempt to reboot the computer in order to delete the pagefile
	if ($self->reboot()) {
		notify($ERRORS{'DEBUG'}, 0, "computer was rebooted after disabling pagefile in the registry");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to reboot computer after disabling pagefile");
		return;
	}
	
	# Attempt to delete the pagefile from all drives
	# A pagefile may reside on drives other than C: if additional volumes are configured in the image
	my @volume_list = $self->get_volume_list();
	if (!@volume_list || !(grep(/c/, @volume_list))) {
		@volume_list = ('c');
	}
	
	# Loop through the drive letters and attempt to delete pagefile.sys on each drive
	for my $drive_letter (@volume_list) {
		if ($self->delete_file("$drive_letter:/pagefile.sys")) {
			notify($ERRORS{'DEBUG'}, 0, "deleted pagefile.sys on all $drive_letter:");
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to delete pagefile.sys on all $drive_letter:");
			return;
		}
	}

	return 1;
} ## end sub disable_pagefile

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

=head2 enable_pagefile

 Parameters  :
 Returns     :
 Description :

=cut

sub enable_pagefile {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $memory_management_key = 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management';
	
	my $reg_add_command = $system32_path . '/reg.exe add "' . $memory_management_key . '" /v PagingFiles /d "$SYSTEMDRIVE\\pagefile.sys 0 0" /t REG_MULTI_SZ /f';
	my ($reg_add_exit_status, $reg_add_output) = run_ssh_command($computer_node_name, $management_node_keys, $reg_add_command, '', '', 1);
	if (defined($reg_add_exit_status) && $reg_add_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "set registry key to enable pagefile");
	}
	elsif (defined($reg_add_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to set registry key to enable pagefile, exit status: $reg_add_exit_status, output:\n@{$reg_add_output}");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to set registry key to enable pagefile");
		return;
	}

	return 1;
} ## end sub enable_pagefile

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

=head2 enable_ipv6

 Parameters  :
 Returns     :
 Description :

=cut

sub enable_ipv6 {
	my $self = shift;
	if (ref($self) !~ /windows/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();

	my $registry_string .= <<"EOF";
Windows Registry Editor Version 5.00

; This registry file contains the entries to disable all IPv6 components 
; http://support.microsoft.com/kb/929852

[HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters]
"DisabledComponents"=dword:00000000
EOF

	# Import the string into the registry
	if ($self->import_registry_string($registry_string)) {
		notify($ERRORS{'OK'}, 0, "set registry keys to enable IPv6");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to set the registry keys to enable IPv6");
		return 0;
	}

	return 1;
}

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

=head2 disable_ipv6

 Parameters  :
 Returns     :
 Description :

=cut

sub disable_ipv6 {
	my $self = shift;
	if (ref($self) !~ /windows/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();

	my $registry_string .= <<"EOF";
Windows Registry Editor Version 5.00

; This registry file contains the entries to disable all IPv6 components 
; http://support.microsoft.com/kb/929852

[HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters]
"DisabledComponents"=dword:ffffffff
EOF

	# Import the string into the registry
	if ($self->import_registry_string($registry_string)) {
		notify($ERRORS{'OK'}, 0, "set registry keys to disable IPv6");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to set the registry keys to disable IPv6");
		return 0;
	}

	return 1;
} ## end sub disable_ipv6

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

=head2 import_registry_file

 Parameters  :
 Returns     :
 Description :

=cut

sub import_registry_file {
	my $self = shift;
	if (ref($self) !~ /windows/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();

	my $registry_file_path = shift;
	if (!defined($registry_file_path) || !$registry_file_path) {
		notify($ERRORS{'WARNING'}, 0, "registry file path was not passed correctly as an argument");
		return;
	}

	my $registry_file_contents = `cat $registry_file_path`;
	notify($ERRORS{'DEBUG'}, 0, "registry file '$registry_file_path' contents:\n$registry_file_contents");

	$registry_file_contents =~ s/([\"])/\\$1/gs;
	$registry_file_contents =~ s/\\+"/\\"/gs;

	# Specify where on the node the temporary registry file will reside
	my $temp_registry_file_path = 'C:/Cygwin/tmp/vcl_import.reg';

	# Echo the registry string to a file on the node
	my $echo_registry_command = "/usr/bin/echo.exe -E \"$registry_file_contents\" > " . $temp_registry_file_path;
	my ($echo_registry_exit_status, $echo_registry_output) = run_ssh_command($computer_node_name, $management_node_keys, $echo_registry_command, '', '', 1);
	if (defined($echo_registry_exit_status) && $echo_registry_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "registry file contents echoed to $temp_registry_file_path");
	}
	elsif ($echo_registry_exit_status) {
		notify($ERRORS{'WARNING'}, 0, "failed to echo registry file contents to $temp_registry_file_path, exit status: $echo_registry_exit_status, output:\n@{$echo_registry_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to echo registry file contents to $temp_registry_file_path");
		return;
	}
	
	# Run reg.exe IMPORT
	if (!$self->reg_import($temp_registry_file_path)) {
		notify($ERRORS{'WARNING'}, 0, "failed to import registry string contents from $temp_registry_file_path");
		return;
	}
	
	return 1;
} ## end sub import_registry_file

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

=head2 import_registry_string

 Parameters  :
 Returns     :
 Description :

=cut

sub import_registry_string {
	my $self = shift;
	if (ref($self) !~ /windows/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();

	my $registry_string = shift;
	if (!defined($registry_string) || !$registry_string) {
		notify($ERRORS{'WARNING'}, 0, "registry file path was not passed correctly as an argument");
		return;
	}

	#notify($ERRORS{'DEBUG'}, 0, "registry string:\n" . $registry_string);

	# Escape special characters with a backslash:
	# \
	# "
	#notify($ERRORS{'DEBUG'}, 0, "registry string:\n$registry_string");
	#$registry_string =~ s/\\+/\\\\\\\\/gs;
	$registry_string =~ s/\\/\\\\/gs;
	$registry_string =~ s/"/\\"/gs;

	# Replace \\" with \"
	#$registry_string =~ s/\\+(")/\\\\$1/gs;

	# Replace regular newlines with Windows newlines
	$registry_string =~ s/\r?\n/\r\n/gs;
	
	# Remove spaces from end of file
	$registry_string =~ s/\s+$//;

	# Assemble a temporary registry file path
	# Name the file after the sub which called this so you can tell where the .reg file was generated from
	my @caller = caller(1);
	my ($calling_sub) = $caller[3] =~ /([^:]+)$/;
	my $calling_line = $caller[2];
	my $temp_registry_file_path = "C:/Cygwin/tmp/$calling_sub\_$calling_line.reg";

	# Echo the registry string to a file on the node
	my $echo_registry_command = "rm -f $temp_registry_file_path; /usr/bin/echo.exe -E \"$registry_string\" > " . $temp_registry_file_path;
	my ($echo_registry_exit_status, $echo_registry_output) = run_ssh_command($computer_node_name, $management_node_keys, $echo_registry_command, '', '', 0);
	if (defined($echo_registry_exit_status) && $echo_registry_exit_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "registry string contents echoed to $temp_registry_file_path");
	}
	elsif ($echo_registry_exit_status) {
		notify($ERRORS{'WARNING'}, 0, "failed to echo registry string contents to $temp_registry_file_path, exit status: $echo_registry_exit_status, output:\n@{$echo_registry_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to echo registry string contents to $temp_registry_file_path");
		return;
	}

	# Run reg.exe IMPORT
	if (!$self->reg_import($temp_registry_file_path)) {
		notify($ERRORS{'WARNING'}, 0, "failed to import registry string contents from $temp_registry_file_path");
		return;
	}
	
	# Delete the temporary .reg file
	if (!$self->delete_file($temp_registry_file_path)) {
		notify($ERRORS{'WARNING'}, 0, "failed to delete the temporary registry file: $temp_registry_file_path");
	}

	return 1;
} ## end sub import_registry_string

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

=head2 reg_query

 Parameters  : $registry_key, $registry_value (optional)
 Returns     : If $registry_value argument is specified: scalar
               If $registry_value argument is specified: hash reference
 Description : Queries the registry on the Windows computer. The $registry_key
               argument is required. The $registry_value argument is optional.
               
               If $registry_value is specified, a scalar containing the value's
               data is returned. The '(Default)' value's data is returned if the
               $registry_value is either an empty string or exactly matches the
               string '(Default)'.
               
               If $registry_value is NOT specified, a hash reference containing
               the keys's subkey names, values, and each value's data is
               returned. The hash has 2 keys: 'subkeys', 'values'.
               
               The 'subkeys' key contains an array reference. This array contains
               the names of the key arguments subkeys.
               
               The 'values' key contain a hash reference. The keys of this hash
               are the names of the values that are set for the key argument.
               Each of theses contains a 'type' and 'data' key containing the
               registry value type and value data.
               
               Example:
               my $registry_data = $self->os->reg_query('HKLM/SYSTEM/CurrentControlSet/Services/NetBT/Parameters');
               @{$registry_data->{subkeys}}[0] = 'Interfaces'
               my @value_names = @{$registry_data->{values}};
               $registry_data->{values}{$value_names[0]}{type} = 'REG_DWORD'
               $registry_data->{values}{$value_names[0]}{data} = '123'

=cut

sub reg_query {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Get the arguments
	my $key_argument = shift;
	if (!defined($key_argument) || !$key_argument) {
		notify($ERRORS{'WARNING'}, 0, "registry key was not passed correctly as an argument");
		return;
	}
	my $value_argument = shift;
	
	# Replace forward slashes and double backslashes with a single backslashes
	$key_argument =~ s/[\\\/]+/\\/g;
	
	# Removing trailing slashes
	$key_argument =~ s/\\+$//g;
	
	# Replace abbreviated key names so argument matches reg.exe output
	$key_argument =~ s/^HKLM/HKEY_LOCAL_MACHINE/;
	$key_argument =~ s/^HKCU/HKEY_CURRENT_USER/;
	$key_argument =~ s/^HKCR/HKEY_CLASSES_ROOT/;
	$key_argument =~ s/^HKU/HKEY_USERS/;
	$key_argument =~ s/^HKCC/HKEY_CURRENT_CONFIG/;
	
	# Assemble the reg.exe QUERY command
	my $command .= "$system32_path/reg.exe QUERY \"$key_argument\" ";
	
	if (!defined($value_argument)) {
		# Do not add any switches
		$command .= "/s";
	}
	elsif ($value_argument eq '(Default)') {
		# Add /ve switch to query the default value
		$command .= "/ve";
	}
	else {
		# Escape slashes and double-quotes in the value argument
		(my $value_argument_escaped = $value_argument) =~ s/([\\\"])/\\$1/g;
		
		# Add /v switch to query a specific value
		$command .= "/v \"$value_argument_escaped\"";
	}
	
	# Run reg.exe QUERY
	my ($exit_status, $output) = run_ssh_command($computer_node_name, $management_node_keys, $command, '', '', 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to query registry key: $key_argument");
		return;
	}
	elsif (grep(/^Error:/, @$output)) {
		my $message = "failed to query registry:\nkey: '$key_argument'\n";
		$message .= "value: '$value_argument'\n" if defined($value_argument);
		$message .= "command: '$command'\noutput:\n" . join("\n", @{$output});
		notify($ERRORS{'WARNING'}, 0, $message);
		return;
	}
	
	notify($ERRORS{'DEBUG'}, 0, "reg.exe QUERY output:\n" . join("\n", @$output));
	
	# If value argument was specified, parse and return the data
	if (defined($value_argument)) {
		# Find the line containing the value information and parse it
		my ($value, $type, $data) = map { $_ =~ /^\s*(.*)\s+(REG_\w+)\s+(.*)/ } @$output;
		$value =~ s/(^\s+|\s+$)//g;
		$type =~ s/(^\s+|\s+$)//g;
		$data =~ s/(^\s+|\s+$)//g;
		
		$value = '(Default)' if $value =~ /NO NAME/;
		
		if ($type && defined($data)) {
			$data = $self->reg_query_convert_data($type, $data);
			notify($ERRORS{'DEBUG'}, 0, "retrieved registry data:\nkey: '$key_argument'\nvalue: '$value'\ntype: $type\ndata: '$data'");
			return $data;
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to retrieve registry data:\nkey: '$key_argument'\nvalue: '$value'\ncommand: '$command'\noutput:\n" . string_to_ascii(join("\n", @$output)));
			return;
		}
	}
	else {
		# Value argument was not specified, construct a hash containing the contents of the key
		my %registry_hash;
		
		my $key;
		for my $line (@$output) {
			
			if ($line =~ /^HKEY/) {
				$key = $line;
				$registry_hash{$key} = {};
				next;
			}
			elsif ($line =~ /^\s*(.*)\s+(REG_\w+)\s+(.*)/) {
				my ($value, $type, $data) = ($1, $2, $3);
				$value =~ s/(^\s+|\s+$)//g;
				$type =~ s/(^\s+|\s+$)//g;
				$data =~ s/(^\s+|\s+$)//g;
				
				$value = '(Default)' if $value =~ /NO NAME/;
				
				$data = $self->reg_query_convert_data($type, $data);
				
				#notify($ERRORS{'DEBUG'}, 0, "line: " . string_to_ascii($line) . "\n" .
						 #"value: " . string_to_ascii($value) . "\n" .
						 #"data: " . string_to_ascii($data)
						 #);
				
				#$registry_hash{$key}{$value}{type} = $type;
				$registry_hash{$key}{$value} = $data;
			}
			elsif ($line =~ /^!/) {
				# Ignore lines beginning with '!'
				next;
			}
			else {
				notify($ERRORS{'WARNING'}, 0, "unexpected output in line: '" . string_to_ascii($line) . "'");
			}
		}
		
		notify($ERRORS{'DEBUG'}, 0, "retrieved registry data:\n" . format_data(\%registry_hash));
		return \%registry_hash;
	}
}

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

=head2 reg_query_convert_data

 Parameters  : $type, $data
 Returns     : scalar
 Description :

=cut

sub reg_query_convert_data {
	my $self = shift;
	if (ref($self) !~ /windows/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my ($type, $data) = @_;
	if (!$type || !defined($data)) {
		notify($ERRORS{'WARNING'}, 0, "registry data type and data value arguments were not specified");
		return;
	}
	
	if ($type eq 'REG_DWORD') {
		# Convert the hex value to decimal
		$data = hex($data);
	}
	if ($type eq 'REG_MULTI_SZ') {
		# Split data into an array, data values are separated in the output by '\0'
		my @data_values = split(/\\0/, $data);
		$data = \@data_values;
	}
	
	return $data;
}

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

=head2 reg_add

 Parameters  : key, value, type, data
 Returns     : If successful: true
               If failed: false
 Description : Adds or sets a registry key.

=cut

sub reg_add {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Get the arguments
	my $registry_key = shift;
	if (!defined($registry_key) || !$registry_key) {
		notify($ERRORS{'WARNING'}, 0, "registry key was not passed correctly as an argument");
		return;
	}
	
	my $registry_value = shift;
	if (!defined($registry_value) || !$registry_value) {
		notify($ERRORS{'WARNING'}, 0, "registry value was not passed correctly as an argument");
		return;
	}
	
	my $registry_type = shift;
	if (!defined($registry_type) || !$registry_type) {
		notify($ERRORS{'WARNING'}, 0, "registry type was not passed correctly as an argument");
		return;
	}
	if ($registry_type !~ /^(REG_SZ|REG_MULTI_SZ|REG_DWORD_BIG_ENDIAN|REG_DWORD|REG_BINARY|REG_DWORD_LITTLE_ENDIAN|REG_NONE|REG_EXPAND_SZ)$/) {
		notify($ERRORS{'WARNING'}, 0, "invalid registry type was specified: $registry_type");
		return;
	}
	
	my $registry_data = shift;
	if (!defined($registry_data)) {
		notify($ERRORS{'WARNING'}, 0, "registry data was not passed correctly as an argument");
		return;
	}
	
	# Fix the value parameter to allow 'default' to be specified
	my $value_parameter;
	if ($registry_value =~ /^default$/i) {
		$value_parameter = '/ve';
	}
	else {
		$value_parameter = "/v \"$registry_value\"";
	}
	
	# Replace forward slashes with backslashes in registry key
	$registry_key =~ s/\//\\\\/g;
	
	# Run reg.exe ADD
	my $add_registry_command = $system32_path . "/reg.exe ADD \"$registry_key\" $value_parameter /t $registry_type /d \"$registry_data\" /f";
	my ($add_registry_exit_status, $add_registry_output) = run_ssh_command($computer_node_name, $management_node_keys, $add_registry_command, '', '', 1);
	if (defined($add_registry_exit_status) && $add_registry_exit_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "added registry key: $registry_key, output:\n" . join("\n", @$add_registry_output));
	}
	elsif ($add_registry_exit_status) {
		notify($ERRORS{'WARNING'}, 0, "failed to add registry key: $registry_key, value: $registry_value, exit status: $add_registry_exit_status, output:\n@{$add_registry_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to add registry key: $registry_key, value: $registry_value");
		return;
	}
	
	return 1;
}

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

=head2 reg_delete

 Parameters  : registry key, registry value
 Returns     :
 Description :

=cut

sub reg_delete {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Get the arguments
	my $registry_key = shift;
	if (!defined($registry_key) || !$registry_key) {
		notify($ERRORS{'WARNING'}, 0, "registry key was not passed correctly as an argument");
		return;
	}
	my $registry_value = shift;
	
	# Replace forward slashes with backslashes in registry key
	$registry_key =~ s/\//\\\\/g;
	
	# Run reg.exe DELETE
	my $delete_registry_command;
	if ($registry_value) {
		$delete_registry_command = $system32_path . "/reg.exe DELETE \"$registry_key\" /v \"$registry_value\" /f";
	}
	else {
		$delete_registry_command = $system32_path . "/reg.exe DELETE \"$registry_key\" /f";
		$registry_value = '*';
	}
	my ($delete_registry_exit_status, $delete_registry_output) = run_ssh_command($computer_node_name, $management_node_keys, $delete_registry_command, '', '', 1);
	if (defined($delete_registry_exit_status) && $delete_registry_exit_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "deleted registry key: $registry_key, value: $registry_value, output:\n" . join("\n", @$delete_registry_output));
	}
	elsif ($delete_registry_exit_status) {
		notify($ERRORS{'WARNING'}, 0, "failed to delete registry key: $registry_key, value: $registry_value, exit status: $delete_registry_exit_status, output:\n@{$delete_registry_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to delete registry key: $registry_key, value: $registry_value");
		return;
	}
	
	return 1;
}

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

=head2 reg_import

 Parameters  :
 Returns     :
 Description :

=cut

sub reg_import {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Get the registry file path argument
	my $registry_file_path = shift;
	if (!defined($registry_file_path) || !$registry_file_path) {
		notify($ERRORS{'WARNING'}, 0, "registry file path was not passed correctly as an argument");
		return;
	}
	
	# Run reg.exe IMPORT
	my $command .= $system32_path . "/reg.exe IMPORT $registry_file_path";
	my ($exit_status, $output) = run_ssh_command($computer_node_name, $management_node_keys, $command, '', '', 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to import registry file: $registry_file_path");
		return;
	}
	elsif (grep(/completed successfully/i, @$output)) {
		notify($ERRORS{'DEBUG'}, 0, "imported registry file: $registry_file_path");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to import registry file: $registry_file_path, exit status: $exit_status, output:\n" . join("\n", @$output));
		return;
	}
	
	return 1;
}

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

=head2 reg_export

 Parameters  :
 Returns     :
 Description :

=cut

sub reg_export {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Get the arguments
	my $root_key = shift;
	if (!$root_key) {
		notify($ERRORS{'WARNING'}, 0, "registry root key was not passed correctly as an argument");
		return;
	}
	
	# Get the registry file path argument
	my $registry_file_path = shift;
	if (!defined($registry_file_path) || !$registry_file_path) {
		notify($ERRORS{'WARNING'}, 0, "registry file path was not passed correctly as an argument");
		return;
	}
	$registry_file_path = $self->format_path_unix($registry_file_path);
	
	# Escape backslashes in the root key
	$root_key =~ s/\\+/\\\\/;
	
	# Run reg.exe EXPORT
	my $command .= $system32_path . "/reg.exe EXPORT $root_key $registry_file_path /y";
	my ($exit_status, $output) = run_ssh_command($computer_node_name, $management_node_keys, $command, '', '', 1);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to export registry key $root_key to file: $registry_file_path");
		return;
	}
	elsif (grep(/completed successfully/i, @$output)) {
		notify($ERRORS{'DEBUG'}, 0, "exported registry key $root_key to file: $registry_file_path");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to export registry key $root_key to file: $registry_file_path, exit status: $exit_status, output:\n" . join("\n", @$output));
		return;
	}
	
	return 1;
}

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

=head2 reg_load

 Parameters  : $root_key, $hive_file_path
 Returns     :
 Description :

=cut

sub reg_load {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Get the arguments
	my $root_key = shift;
	if (!$root_key) {
		notify($ERRORS{'WARNING'}, 0, "registry root key was not passed correctly as an argument");
		return;
	}
	my $hive_file_path = shift;
	if (!$hive_file_path) {
		notify($ERRORS{'WARNING'}, 0, "registry hive file path was not passed correctly as an argument");
		return;
	}
	$hive_file_path = $self->format_path_unix($hive_file_path);
	
	# Escape backslashes in the root key
	$root_key =~ s/\\+/\\\\/;
	
	# Run reg.exe LOAD
	my $command .= "$system32_path/reg.exe LOAD $root_key $hive_file_path";
	my ($exit_status, $output) = run_ssh_command($computer_node_name, $management_node_keys, $command, '', '', 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to load registry hive file '$hive_file_path' into key $root_key");
		return;
	}
	elsif (grep(/completed successfully/i, @$output)) {
		notify($ERRORS{'DEBUG'}, 0, "loaded registry hive file '$hive_file_path' into key $root_key");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to load registry hive file '$hive_file_path' into key $root_key, exit status: $exit_status, output:\n" . join("\n", @$output));
		return;
	}
	
	return 1;
}

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

=head2 reg_unload

 Parameters  : $root_key
 Returns     :
 Description :

=cut

sub reg_unload {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Get the arguments
	my $root_key = shift;
	if (!$root_key) {
		notify($ERRORS{'WARNING'}, 0, "registry root key was not passed correctly as an argument");
		return;
	}
	
	# Escape backslashes in the root key
	$root_key =~ s/\\+/\\\\/;
	
	# Run reg.exe UNLOAD
	my $command .= "$system32_path/reg.exe UNLOAD $root_key";
	my ($exit_status, $output) = run_ssh_command($computer_node_name, $management_node_keys, $command, '', '', 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to unload registry hive: $root_key");
		return;
	}
	elsif (grep(/completed successfully/i, @$output)) {
		notify($ERRORS{'DEBUG'}, 0, "unloaded registry hive key: $root_key");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to unload registry hive: $root_key, exit status: $exit_status, output:\n" . join("\n", @$output));
		return;
	}
	
	return 1;
}

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

=head2 add_hklm_run_registry_key

 Parameters  :
 Returns     :
 Description :

=cut

sub add_hklm_run_registry_key {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $command_name = shift;
	my $command      = shift;

	notify($ERRORS{'DEBUG'}, 0, "command name: " . $command_name);
	notify($ERRORS{'DEBUG'}, 0, "command: " . $command);

	# Replace forward slashes with backslashes, unless a space precedes the forward slash
	$command =~ s/([^ ])\//$1\\/g;
	notify($ERRORS{'DEBUG'}, 0, "forward to backslash: " . $command);

	# Escape backslashes, can never have enough...
	$command =~ s/\\/\\\\/g;
	notify($ERRORS{'DEBUG'}, 0, "escape backslashes: " . $command);

	# Escape quotes
	$command =~ s/"/\\"/g;
	notify($ERRORS{'DEBUG'}, 0, "escaped quotes: " . $command);

	# Make sure arguments were supplied
	if (!defined($command_name) && !defined($command)) {
		notify($ERRORS{'WARNING'}, 0, "HKLM run registry key not added, arguments were not passed correctly");
		return 0;
	}

	my $registry_string .= <<"EOF";
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run]
"$command_name"="$command"
EOF

	notify($ERRORS{'DEBUG'}, 0, "registry string:\n" . $registry_string);

	# Import the string into the registry
	if ($self->import_registry_string($registry_string)) {
		notify($ERRORS{'OK'}, 0, "added HKLM run registry value, name: $command_name, command: $command");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to add HKLM run registry value, name: $command_name, command: $command");
		return 0;
	}
	
	# Attempt to query the registry key to make sure it was added
	my $reg_query_command = $system32_path . '/reg.exe query "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"';
	my ($reg_query_exit_status, $reg_query_output) = run_ssh_command($computer_node_name, $management_node_keys, $reg_query_command, '', '', 1);
	if (defined($reg_query_exit_status) && $reg_query_exit_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "queried '$command_name' registry key:\n" . join("\n", @{$reg_query_output}));
	}
	elsif (defined($reg_query_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to query '$command_name' registry key, exit status: $reg_query_exit_status, output:\n@{$reg_query_output}");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to query '$command_name' registry key");
		return;
	}
	
	return 1;
} ## end sub add_hklm_run_registry_key

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

=head2 delete_hklm_run_registry_value

 Parameters  :
 Returns     :
 Description :

=cut

sub delete_hklm_run_registry_key {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $key_name = shift;
	
	# Make sure argument was supplied
	if (!defined($key_name) && !defined($key_name)) {
		notify($ERRORS{'WARNING'}, 0, "HKLM run registry key not deleted, argument was not passed correctly");
		return 0;
	}
	
	# Attempt to query the registry key to make sure it was added
	my $reg_delete_command = $system32_path . '/reg.exe delete "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run" /v "' . $key_name . '" /F';
	my ($reg_delete_exit_status, $reg_delete_output) = run_ssh_command($computer_node_name, $management_node_keys, $reg_delete_command, '', '', 1);
	if (defined($reg_delete_exit_status) && $reg_delete_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "deleted '$key_name' run registry key:\n" . join("\n", @{$reg_delete_output}));
	}
	elsif (defined($reg_delete_output) && grep(/unable to find/i, @{$reg_delete_output})) {
		notify($ERRORS{'OK'}, 0, "'$key_name' run registry key was not deleted, it does not exist");
	}
	elsif (defined($reg_delete_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to delete '$key_name' run registry key, exit status: $reg_delete_exit_status, output:\n@{$reg_delete_output}");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to delete '$key_name' run registry key");
		return;
	}
	
	return 1;
}

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

=head2 delete_scheduled_task

 Parameters  :
 Returns     :
 Description :

=cut

sub delete_scheduled_task {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $task_name     = shift;
	
	# Run schtasks.exe to delete any existing task
	my $delete_task_command = "$system32_path/schtasks.exe /Delete /F /TN \"$task_name\"";
	my ($delete_task_exit_status, $delete_task_output) = run_ssh_command($computer_node_name, $management_node_keys, $delete_task_command);
	if (defined($delete_task_exit_status) && $delete_task_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "deleted existing scheduled task '$task_name' on $computer_node_name");
	}
	elsif (defined($delete_task_output) && grep(/(task.*does not exist|cannot find the file specified)/i, @{$delete_task_output})) {
		notify($ERRORS{'DEBUG'}, 0, "scheduled task '$task_name' does not exist on $computer_node_name");
	}
	elsif (defined($delete_task_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to delete existing scheduled task '$task_name' on $computer_node_name, exit status: $delete_task_exit_status, output:\n@{$delete_task_output}");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to execute ssh command to delete existing scheduled task '$task_name' on $computer_node_name");
		return;
	}
	
	return 1;
}

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

=head2 create_startup_scheduled_task

 Parameters  :
 Returns     :
 Description :

=cut

sub create_startup_scheduled_task {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $task_name     = shift;
	my $task_command  = shift;
	my $task_user     = shift;
	my $task_password = shift;

	# Escape backslashes, can never have enough...
	$task_command =~ s/\\/\\\\/g;

	# Replace forward slashes with backslashes
	$task_command =~ s/([^\s])\//$1\\\\/g;
	
	# Escape quote characters
	$task_command =~ s/"/\\"/g;

	# Make sure arguments were supplied
	if (!defined($task_name) || !defined($task_command) || !defined($task_user) || !defined($task_password)) {
		notify($ERRORS{'WARNING'}, 0, "startup scheduled task not added, arguments were not passed correctly");
		return;
	}

	# You cannot create a task if one with the same name already exists
	# Windows 6.x schtasks.exe has a /F which forces a new task to be created if one with the same name already exists
	# This option isn't supported with XP and other older versions of Windows
	if (!$self->delete_scheduled_task($task_name)) {
		notify($ERRORS{'WARNING'}, 0, "unable to delete existing scheduled task '$task_name' on $computer_node_name");
	}
	
	# Run schtasks.exe to add the task
	# Occasionally see this error even though it schtasks.exe returns exit status 0:
	# WARNING: The Scheduled task "System Startup Script" has been created, but may not run because the account information could not be set.
	my $create_task_command = "$system32_path/schtasks.exe /Create /RU \"$task_user\" /RP \"$task_password\" /SC ONSTART /TN \"$task_name\" /TR \"$task_command\"";
	my ($create_task_exit_status, $create_task_output) = run_ssh_command($computer_node_name, $management_node_keys, $create_task_command);
	if (defined($create_task_output) && grep(/could not be set/i, @{$create_task_output})) {
		notify($ERRORS{'WARNING'}, 0, "created scheduled task '$task_name' on $computer_node_name but error occurred: " . join("\n", @{$create_task_output}));
		return 0;
	}
	elsif (defined($create_task_exit_status) && $create_task_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "created scheduled task '$task_name' on $computer_node_name");
	}
	elsif (defined($create_task_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to create scheduled task '$task_name' on $computer_node_name, exit status: $create_task_exit_status, output:\n@{$create_task_output}");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to execute ssh command created scheduled task '$task_name' on $computer_node_name");
		return;
	}

	return 1;
} ## end sub create_startup_scheduled_task

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

=head2 enable_autoadminlogon

 Parameters  :
 Returns     :
 Description :

=cut

sub enable_autoadminlogon {
	my $self = shift;
	if (ref($self) !~ /windows/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();

	my $registry_string .= <<"EOF";
Windows Registry Editor Version 5.00

; This file enables autoadminlogon for the root account

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon]
"AutoAdminLogon"="1"
"DefaultUserName"="root"
"DefaultPassword"= "$WINDOWS_ROOT_PASSWORD"

EOF

	# Import the string into the registry
	if ($self->import_registry_string($registry_string)) {
		notify($ERRORS{'OK'}, 0, "enabled autoadminlogon");
		return 1;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to enable autoadminlogon");
		return 0;
	}
} ## end sub enable_autoadminlogon

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

=head2 disable_autoadminlogon

 Parameters  :
 Returns     :
 Description :

=cut

sub disable_autoadminlogon {
	my $self = shift;
	if (ref($self) !~ /windows/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();

	my $registry_string .= <<EOF;
Windows Registry Editor Version 5.00

; This file disables autoadminlogon for the root account

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon]
"AutoAdminLogon"="0"
"AutoLogonCount"="0"
"DefaultPassword"= ""
EOF

	# Import the string into the registry
	if ($self->import_registry_string($registry_string)) {
		notify($ERRORS{'OK'}, 0, "disabled autoadminlogon");
		return 1;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to disable autoadminlogon");
		return 0;
	}
} ## end sub disable_autoadminlogon

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

=head2 create_eventlog_entry

 Parameters  :
 Returns     :
 Description :

=cut

sub create_eventlog_entry {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $message = shift;

	# Make sure the message was passed as an argument
	if (!defined($message)) {
		notify($ERRORS{'WARNING'}, 0, "failed to create eventlog entry, message was passed as an argument");
		return 0;
	}

	# Run eventcreate.exe to create an event log entry
	my $eventcreate_command = $system32_path . '/eventcreate.exe /T INFORMATION /L APPLICATION /SO VCL /ID 555 /D "' . $message . '"';
	my ($eventcreate_exit_status, $eventcreate_output) = run_ssh_command($computer_node_name, $management_node_keys, $eventcreate_command);
	if (defined($eventcreate_exit_status) && $eventcreate_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "created event log entry on $computer_node_name: $message");
	}
	elsif (defined($eventcreate_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to create event log entry on $computer_node_name: $message, exit status: $eventcreate_exit_status, output:\n@{$eventcreate_output}");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to create event log entry on $computer_node_name: $message");
		return;
	}

	return 1;
} ## end sub create_eventlog_entry

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

=head2 reboot

 Parameters  : $wait_for_reboot
 Returns     : 
 Description : 

=cut

sub reboot {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Check if an argument was supplied
	my $wait_for_reboot = shift;
	if (!defined($wait_for_reboot) || $wait_for_reboot !~ /0/) {
		notify($ERRORS{'DEBUG'}, 0, "rebooting $computer_node_name and waiting for ssh to become active");
		$wait_for_reboot = 1;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "rebooting $computer_node_name and NOT waiting");
		$wait_for_reboot = 0;
	}

	my $reboot_start_time = time();
	notify($ERRORS{'DEBUG'}, 0, "reboot will be attempted on $computer_node_name");

	# Check if computer responds to ssh before preparing for reboot
	if ($self->wait_for_ssh(0)) {
		# Make sure SSH access is enabled from private IP addresses
		if (!$self->firewall_enable_ssh_private()) {
			notify($ERRORS{'WARNING'}, 0, "reboot not attempted, failed to enable ssh from private IP addresses");
			return 0;
		}

		# Set sshd service startup mode to auto
		if (!$self->set_service_startup_mode('sshd', 'auto')) {
			notify($ERRORS{'WARNING'}, 0, "reboot not attempted, unable to set sshd service startup mode to auto");
			return 0;
		}

		# Make sure ping access is enabled from private IP addresses
		if (!$self->firewall_enable_ping_private()) {
			notify($ERRORS{'WARNING'}, 0, "reboot not attempted, failed to enable ping from private IP addresses");
			return 0;
		}
		
		# Kill the screen saver process, it occasionally prevents reboots and shutdowns from working
		$self->kill_process('logon.scr');
		
		# Check if tsshutdn.exe exists on the computer
		# tsshutdn.exe is the preferred utility, shutdown.exe often fails on Windows Server 2003
		my $reboot_command;
		my $windows_product_name = $self->get_product_name() || '';
		if ($windows_product_name =~ /2003/ && $self->file_exists("$system32_path/tsshutdn.exe")) {
			$reboot_command = "$system32_path/tsshutdn.exe 0 /REBOOT /DELAY:0 /V";
		}
		else {
			$reboot_command = "$system32_path/shutdown.exe /r /t 0 /f";
		}
		
		my ($reboot_exit_status, $reboot_output) = run_ssh_command($computer_node_name, $management_node_keys, $reboot_command);
		if (!defined($reboot_output)) {
			notify($ERRORS{'WARNING'}, 0, "failed to execute ssh command to reboot $computer_node_name");
			return;
		}
		
		if ($reboot_exit_status == 0) {
			notify($ERRORS{'OK'}, 0, "executed reboot command on $computer_node_name");
		}
		else {
			# The following message may be displayed causing the reboot to fail:
			# The computer is processing another action and thus cannot be shut down. Wait until the computer has finished its action, and then try again.(21) 
			notify($ERRORS{'WARNING'}, 0, "failed to reboot $computer_node_name, attempting power reset, output:\n" . join("\n", @$reboot_output));
			
			# Call provisioning module's power_reset() subroutine
			if ($self->provisioner->power_reset()) {
				notify($ERRORS{'OK'}, 0, "initiated power reset on $computer_node_name");
			}
			else {
				notify($ERRORS{'WARNING'}, 0, "reboot failed, failed to initiate power reset on $computer_node_name");
				return;
			}
		}
	}
	else {
		# Computer did not respond to ssh
		notify($ERRORS{'WARNING'}, 0, "$computer_node_name did not respond to ssh, graceful reboot cannot be performed, attempting hard reset");

		# Call provisioning module's power_reset() subroutine
		if ($self->provisioner->power_reset()) {
			notify($ERRORS{'OK'}, 0, "initiated power reset on $computer_node_name");
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "reboot failed, failed to initiate power reset on $computer_node_name");
			return 0;
		}
	} ## end else [ if ($self->wait_for_ssh(0))

	# Check if wait for reboot is set
	if (!$wait_for_reboot) {
		return 1;
	}

	# Make multiple attempts to wait for the reboot to complete
	my $wait_attempt_limit = 2;
	WAIT_ATTEMPT:
	for (my $wait_attempt = 1; $wait_attempt <= $wait_attempt_limit; $wait_attempt++) {
		if ($wait_attempt > 1) {
			# Computer did not become fully responsive on previous wait attempt
			notify($ERRORS{'OK'}, 0, "$computer_node_name reboot failed to complete on previous attempt, attempting hard power reset");

			# Call provisioning module's power_reset() subroutine
			if ($self->provisioner->power_reset()) {
				notify($ERRORS{'OK'}, 0, "reboot attempt $wait_attempt/$wait_attempt_limit: initiated power reset on $computer_node_name");
			}
			else {
				notify($ERRORS{'WARNING'}, 0, "reboot failed, failed to initiate power reset on $computer_node_name");
				return 0;
			}
		} ## end if ($wait_attempt > 1)

		# Wait maximum of 3 minutes for the computer to become unresponsive
		if (!$self->wait_for_no_ping(180, 3)) {
			# Computer never stopped responding to ping
			notify($ERRORS{'WARNING'}, 0, "$computer_node_name never became unresponsive to ping");
			next WAIT_ATTEMPT;
		}

		# Computer is unresponsive, reboot has begun
		# Wait for 5 seconds before beginning to check if computer is back online
		notify($ERRORS{'DEBUG'}, 0, "$computer_node_name reboot has begun, sleeping for 5 seconds");
		sleep 5;

		# Wait maximum of 6 minutes for the computer to come back up
		if (!$self->wait_for_ping(360, 5)) {
			# Check if the computer was ever offline, it should have been or else reboot never happened
			notify($ERRORS{'WARNING'}, 0, "$computer_node_name never responded to ping");
			next WAIT_ATTEMPT;
		}

		notify($ERRORS{'DEBUG'}, 0, "$computer_node_name is pingable, waiting for ssh to respond");

		# Wait maximum of 3 minutes for ssh to respond
		if (!$self->wait_for_ssh(180, 5)) {
			notify($ERRORS{'WARNING'}, 0, "ssh never responded on $computer_node_name");
			next WAIT_ATTEMPT;
		}

		notify($ERRORS{'DEBUG'}, 0, "$computer_node_name responded to ssh");
		
		## Wait then check ssh again in case initialization scripts are running
		## ssh may be available when the computer first boots, then network configuration scripts may automatically run
		## Make sure ssh is available a short time after it's first available
		#notify($ERRORS{'DEBUG'}, 0, "sleeping for 20 seconds then checking ssh again");
		#sleep 20;
		#
		## Wait maximum of 2 minutes for ssh to respond
		#if (!$self->wait_for_ssh(120)) {
		#	notify($ERRORS{'WARNING'}, 0, "ssh responded then stopped responding on $computer_node_name");
		#	next WAIT_ATTEMPT;
		#}

		# Reboot was successful, calculate how long reboot took
		my $reboot_end_time = time();
		my $reboot_duration = ($reboot_end_time - $reboot_start_time);
		notify($ERRORS{'OK'}, 0, "reboot complete on $computer_node_name, took $reboot_duration seconds");
		return 1;
	} ## end for (my $wait_attempt = 1; $wait_attempt <=...

	# If loop completed, maximum number of reboot attempts was reached
	notify($ERRORS{'WARNING'}, 0, "reboot failed on $computer_node_name, made $wait_attempt_limit attempts");
	return 0;
} ## end sub reboot

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

=head2 shutdown

 Parameters  : $enable_dhcp
 Returns     : 
 Description : 

=cut

sub shutdown {
	my $self = shift;
	if (ref($self) !~ /windows/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the argument that determines whether or not to disable DHCP before shutting down computer
	my $disable_dhcp = shift;

	my $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Kill the screen saver process, it occasionally prevents reboots and shutdowns from working
	$self->kill_process('logon.scr');
	
	# Clear the event log before shutting down
	$self->clear_event_log();
	
	my $shutdown_command = "/bin/cygstart.exe \$SYSTEMROOT/system32/cmd.exe /c \"";
	
	if ($disable_dhcp) {
		notify($ERRORS{'DEBUG'}, 0, "enabling DHCP and shutting down $computer_node_name");
		
		my $private_interface_name = $self->get_private_interface_name();
		my $public_interface_name = $self->get_public_interface_name();
		if (!$private_interface_name || !$public_interface_name) {
			notify($ERRORS{'WARNING'}, 0, "unable to determine private and public interface names, failed to enable DHCP and shut down $computer_node_name");
			return;
		}
		
		$shutdown_command .= "$system32_path/netsh.exe interface ip set address name=\\\"$private_interface_name\\\" source=dhcp & ";
		$shutdown_command .= "$system32_path/netsh.exe interface ip set dnsservers name=\\\"$private_interface_name\\\" source=dhcp & ";
		$shutdown_command .= "$system32_path/netsh.exe interface ip set address name=\\\"$public_interface_name\\\" source=dhcp & ";
		$shutdown_command .= "$system32_path/netsh.exe interface ip set dnsservers name=\\\"$public_interface_name\\\" source=dhcp & ";
		$shutdown_command .= "$system32_path/netsh.exe interface ip reset & ";
		$shutdown_command .= "$system32_path/ipconfig.exe /release & ";
		$shutdown_command .= "$system32_path/ipconfig.exe /flushdns & ";
		$shutdown_command .= "$system32_path/arp.exe -d * & ";
		$shutdown_command .= "$system32_path/route.exe DELETE 0.0.0.0 MASK 0.0.0.0 & ";
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "shutting down $computer_node_name");
	}
	
	# Check if tsshutdn.exe exists on the computer
	# tsshutdn.exe is the preferred utility for Windows 2003, shutdown.exe often fails
	my $windows_product_name = $self->get_product_name() || '';
	if ($windows_product_name =~ /2003/ && $self->file_exists("$system32_path/tsshutdn.exe")) {
		$shutdown_command .= "$system32_path/tsshutdn.exe 0 /POWERDOWN /DELAY:0 /V";
	}
	else {
		$shutdown_command .= "$system32_path/shutdown.exe /s /t 0 /f";
	}
	
	$shutdown_command .= "\"";
	
	my $attempt_count = 0;
	my $attempt_limit = 12;
	while ($attempt_count < $attempt_limit) {
		$attempt_count++;
		if ($attempt_count > 1) {
			notify($ERRORS{'DEBUG'}, 0, "sleeping for 10 seconds before making next shutdown attempt");
			sleep 10;
		}
		
		my ($shutdown_exit_status, $shutdown_output) = run_ssh_command($computer_node_name, $management_node_keys, $shutdown_command);
		if (!defined($shutdown_output)) {
			notify($ERRORS{'WARNING'}, 0, "failed to execute ssh command to shutdown $computer_node_name");
			last;
		}
		elsif (grep(/(processing another action)/i, @$shutdown_output)) {
			notify($ERRORS{'WARNING'}, 0, "attempt $attempt_count/$attempt_limit: failed to execute shutdown command on $computer_node_name, exit status: $shutdown_exit_status, output:\n@{$shutdown_output}");
			next;
		}
		else {
			notify($ERRORS{'DEBUG'}, 0, "attempt $attempt_count/$attempt_limit: executed shutdown command on $computer_node_name");
			last;
		}
	}
	
	# Wait maximum of 3 minutes for the computer to become unresponsive
	if (!$self->wait_for_no_ping(180)) {
		# Computer never stopped responding to ping
		notify($ERRORS{'WARNING'}, 0, "$computer_node_name never became unresponsive to ping after shutdown command was issued");
		return;
	}
	
	# Wait maximum of 5 minutes for computer to power off
	my $power_off = $self->provisioner->wait_for_power_off(300);
	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 shutdown");
		sleep 60;
	}
	elsif (!$power_off) {
		notify($ERRORS{'WARNING'}, 0, "$computer_node_name never powered off");
		return;
	}
	
	return 1;
}

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

=head2 set_service_startup_mode

 Parameters  : 
 Returns     : 1 if succeeded, 0 otherwise
 Description : 

=cut

sub set_service_startup_mode {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $service_name = shift;
	my $startup_mode = shift;

	# Make sure both arguments were supplied
	if (!defined($service_name) && !defined($startup_mode)) {
		notify($ERRORS{'WARNING'}, 0, "set service startup mode failed, service name and startup mode arguments were not passed correctly");
		return 0;
	}

	# Make sure the startup mode is valid
	if ($startup_mode !~ /boot|system|auto|demand|disabled|delayed-auto|manual/i) {
		notify($ERRORS{'WARNING'}, 0, "set service startup mode failed, invalid startup mode: $startup_mode");
		return 0;
	}

	# Set the mode to demand if manual was specified, specific to sc command
	$startup_mode = "demand" if ($startup_mode eq "manual");

	# Use sc.exe to change the start mode
	my $service_startup_command = $system32_path . '/sc.exe config ' . "$service_name start= $startup_mode";
	my ($service_startup_exit_status, $service_startup_output) = run_ssh_command($computer_node_name, $management_node_keys, $service_startup_command);
	if (defined($service_startup_output) && grep(/service does not exist/, @$service_startup_output)) {
		notify($ERRORS{'WARNING'}, 0, "$service_name service startup mode not set because service does not exist");
		return;
	}
	elsif (defined($service_startup_exit_status) && $service_startup_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "$service_name service startup mode set to $startup_mode");
	}
	elsif ($service_startup_exit_status) {
		notify($ERRORS{'WARNING'}, 0, "failed to set $service_name service startup mode to $startup_mode, exit status: $service_startup_exit_status, output:\n@{$service_startup_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to set $service_name service startup mode to $startup_mode");
		return;
	}

	return 1;
} ## end sub set_service_startup_mode

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

=head2 defragment_hard_drive

 Parameters  : 
 Returns     : 1 if succeeded, 0 otherwise
 Description : 

=cut

sub defragment_hard_drive {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Defragment the hard drive
	notify($ERRORS{'OK'}, 0, "beginning to defragment the hard drive on $computer_node_name");
	my ($defrag_exit_status, $defrag_output) = run_ssh_command($computer_node_name, $management_node_keys, $system32_path . '/defrag.exe $SYSTEMDRIVE -v');
	if (defined($defrag_exit_status) && $defrag_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "hard drive defragmentation complete on $computer_node_name");
		return 1;
	}
	elsif (defined($defrag_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to defragment the hard drive, exit status: $defrag_exit_status, output:\n@{$defrag_output}");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run the SSH command to defragment the hard drive");
		return;
	}
} ## end sub defragment_hard_drive

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

=head2 prepare_post_load

 Parameters  : None.
 Returns     : If successful: true
               If failed: false
 Description : This subroutine should be called as the last step before an image
               is captured if Sysprep is not is used. It enables autoadminlogon
               so that root automatically logs on the next time the computer is
               booted and creates a registry key under
               HKLM\Software\Microsoft\Windows\CurrentVersion\Run.
               
               This key causes the post_load.cmd script after the image is
               loaded when root automatically logs on. This script needs to run
               in order to configure networking and the Cygwin SSH service.

=cut

sub prepare_post_load {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $end_state = $self->{end_state} || 'off';
	
	# Set the DevicePath registry key
	# This is used to locate device drivers
	if (!$self->set_device_path_key()) {
		notify($ERRORS{'WARNING'}, 0, "failed to set the DevicePath registry key");
		return;
	}
	
	# Get the node configuration directory
	my $node_configuration_directory = $self->get_node_configuration_directory();
	unless ($node_configuration_directory) {
		notify($ERRORS{'WARNING'}, 0, "node configuration directory could not be determined");
		return;
	}
	
	# Add HKLM run key to call post_load.cmd after the image comes up
	if (!$self->add_hklm_run_registry_key('post_load.cmd', $node_configuration_directory . '/Scripts/post_load.cmd  >> ' . $node_configuration_directory . '/Logs/post_load.log')) {
		notify($ERRORS{'WARNING'}, 0, "unable to create run key to call post_load.cmd");
		return;
	}
	
	# Enable autoadminlogon
	if (!$self->enable_autoadminlogon()) {
		notify($ERRORS{'WARNING'}, 0, "unable to enable autoadminlogon");
		return 0;
	}
	
	# Shut down computer unless end_state argument was passed with a value other than 'off'
	if ($end_state eq 'off') {
		if (!$self->shutdown(1)) {
			notify($ERRORS{'WARNING'}, 0, "failed to shut down computer");
			return;
		}
	}
	
	return 1;
}

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

=head2 set_service_credentials

 Parameters  : $service_name, $username, $password
 Returns     : 
 Description : 

=cut

sub set_service_credentials {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Attempt to get the username from the arguments
	my $service_name = shift;
	my $username     = shift;
	my $password     = shift;

	# Make sure arguments were supplied
	if (!$service_name || !$username || !$password) {
		notify($ERRORS{'WARNING'}, 0, "set service logon failed, service name, username, and password arguments were not passed correctly");
		return 0;
	}

	# Attempt to set the service logon user name and password
	my $service_logon_command = $system32_path . '/sc.exe config ' . $service_name . ' obj= ".\\' . $username . '" password= "' . $password . '"';
	my ($service_logon_exit_status, $service_logon_output) = run_ssh_command($computer_node_name, $management_node_keys, $service_logon_command);
	if (defined($service_logon_exit_status) && $service_logon_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "changed logon credentials for '$service_name' service to $username ($password) on $computer_node_name");
	}
	elsif (defined($service_logon_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to change $service_name service logon credentials to $username ($password) on $computer_node_name, exit status: $service_logon_exit_status, output:\n@{$service_logon_output}");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to change $service_name service logon credentials to $username ($password) on $computer_node_name");
		return;
	}

	return 1;
} ## end sub set_service_credentials

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

=head2 get_service_list

 Parameters  : 
 Returns     : 
 Description : 

=cut

sub get_service_list {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Attempt to delete the user account
	my $sc_query_command = $system32_path . "/sc.exe query | grep SERVICE_NAME | cut --fields=2 --delimiter=' '";
	my ($sc_query_exit_status, $sc_query_output) = run_ssh_command($computer_node_name, $management_node_keys, $sc_query_command);
	if (defined($sc_query_exit_status) && $sc_query_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "retrieved service list on $computer_node_name");
	}
	elsif (defined($sc_query_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve service list from $computer_node_name, exit status: $sc_query_exit_status, output:\n@{$sc_query_output}");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to failed to retrieve service list from $computer_node_name");
		return;
	}

	my @service_name_array = split("\n", $sc_query_output);
	notify($ERRORS{'DEBUG'}, 0, "found " . @service_name_array . " services on $computer_node_name");
	return @service_name_array;
} ## end sub get_service_list

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

=head2 get_service_login_ids

 Parameters  : 
 Returns     : 
 Description : 

=cut

sub get_services_using_login_id {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $login_id = shift;
	if (!$login_id) {
		notify($ERRORS{'WARNING'}, 0, "unable to get services using login id, login id argument was not passed correctly");
		return;
	}

	# Get a list of the services on the node
	my @service_list = $self->get_service_list();
	if (!@service_list) {
		notify($ERRORS{'WARNING'}, 0, "unable to get service logon ids, failed to retrieve service name list from $computer_node_name, service credentials cannot be changed");
		return 0;
	}

	my @services_using_login_id;
	for my $service_name (@service_list) {
		# Attempt to get the service start name using sc.exe qc
		my $sc_qc_command = $system32_path . "/sc.exe qc $service_name | grep SERVICE_START_NAME | cut --fields=2 --delimiter='\\'";
		my ($sc_qc_exit_status, $sc_qc_output) = run_ssh_command($computer_node_name, $management_node_keys, $sc_qc_command);
		if (defined($sc_qc_exit_status) && $sc_qc_exit_status == 0) {
			notify($ERRORS{'OK'}, 0, "retrieved $service_name service start name from $computer_node_name");
		}
		elsif (defined($sc_qc_exit_status)) {
			notify($ERRORS{'WARNING'}, 0, "failed to retrieve $service_name service start name from $computer_node_name, exit status: $sc_qc_exit_status, output:\n@{$sc_qc_output}");
			return 0;
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to failed to retrieve $service_name service start name from $computer_node_name");
			return;
		}

		my $service_logon_id = @{$sc_qc_output}[0];
		if ($service_logon_id =~ /^$login_id$/i) {
			push @services_using_login_id, $service_logon_id;
		}
	} ## end for my $service_name (@service_list)

	return @services_using_login_id;
} ## end sub get_services_using_login_id

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

=head2 disable_scheduled_task

 Parameters  : 
 Returns     : 1 success 0 failure
 Description : 

=cut

sub disable_scheduled_task {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Attempt to get the task name from the arguments
	my $task_name = shift;
	if (!$task_name) {
		notify($ERRORS{'OK'}, 0, "failed to disable scheduled task, task name argument was not correctly passed");
		return;
	}

	# Attempt to delete the user account
	my $schtasks_command = $system32_path . '/schtasks.exe /Change /DISABLE /TN "' . $task_name . '"';
	my ($schtasks_exit_status, $schtasks_output) = run_ssh_command($computer_node_name, $management_node_keys, $schtasks_command, '', '', 1);
	if (!defined($schtasks_output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to disable $task_name scheduled task on $computer_node_name");
		return;
	}
	elsif (grep(/have been changed/, @$schtasks_output)) {
		notify($ERRORS{'OK'}, 0, "$task_name scheduled task disabled on $computer_node_name");
	}
	elsif (grep(/does not exist/, @$schtasks_output)) {
		notify($ERRORS{'OK'}, 0, "$task_name was not disabled on $computer_node_name because it does not exist");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to disable $task_name scheduled task on $computer_node_name, exit status: $schtasks_exit_status, output:\n@{$schtasks_output}");
		return 0;
	}
	

	return 1;
} ## end sub disable_scheduled_task

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

=head2 get_scheduled_tasks

 Parameters  : 
 Returns     : array reference if successful, false if failed
 Description : Queries the scheduled tasks on a computer and returns the
               configuration for each task. An array reference is returned.
               Each array element represents a scheduled task and contains
               a hash reference. The hash contains the schedule task
               configuration.  The hash keys are:
                  $scheduled_task_hash{"HostName"},
                  $scheduled_task_hash{"TaskName"},
                  $scheduled_task_hash{"Next Run Time"},
                  $scheduled_task_hash{"Status"},
                  $scheduled_task_hash{"Last Run Time"},
                  $scheduled_task_hash{"Last Result"},
                  $scheduled_task_hash{"Creator"},
                  $scheduled_task_hash{"Schedule"},
                  $scheduled_task_hash{"Task To Run"},
                  $scheduled_task_hash{"Start In"},
                  $scheduled_task_hash{"Comment"},
                  $scheduled_task_hash{"Scheduled Task State"},
                  $scheduled_task_hash{"Scheduled Type"},
                  $scheduled_task_hash{"Start Time"},
                  $scheduled_task_hash{"Start Date"},
                  $scheduled_task_hash{"End Date"},
                  $scheduled_task_hash{"Days"},
                  $scheduled_task_hash{"Months"},
                  $scheduled_task_hash{"Run As User"},
                  $scheduled_task_hash{"Delete Task If Not Rescheduled"},
                  $scheduled_task_hash{"Stop Task If Runs X Hours and X Mins"},
                  $scheduled_task_hash{"Repeat: Every"},
                  $scheduled_task_hash{"Repeat: Until: Time"},
                  $scheduled_task_hash{"Repeat: Until: Duration"},
                  $scheduled_task_hash{"Repeat: Stop If Still Running"},
                  $scheduled_task_hash{"Idle Time"},
                  $scheduled_task_hash{"Power Management"}

=cut

sub get_scheduled_tasks {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Attempt to retrieve scheduled task information
	my $schtasks_command = $system32_path . '/schtasks.exe /Query /NH /V /FO CSV';
	my ($schtasks_exit_status, $schtasks_output) = run_ssh_command($computer_node_name, $management_node_keys, $schtasks_command);
	if (defined($schtasks_exit_status) && $schtasks_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "retrieved scheduled task information");
	}
	elsif (defined($schtasks_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve scheduled task information, exit status: $schtasks_exit_status, output:\n@{$schtasks_output}");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to retrieve scheduled task information");
		return;
	}
	
	my @scheduled_task_data;
	for my $scheduled_task_line (@{$schtasks_output}) {
		# Remove quotes from the hash values
		$scheduled_task_line =~ s/"//g;
		
		# Split the line up
		my @scheduled_task_fields = split(/,/, $scheduled_task_line);
		
		# Create a hash containing the line data
		my %scheduled_task_hash;
		($scheduled_task_hash{"HostName"},
		$scheduled_task_hash{"TaskName"},
		$scheduled_task_hash{"Next Run Time"},
		$scheduled_task_hash{"Status"},
		$scheduled_task_hash{"Last Run Time"},
		$scheduled_task_hash{"Last Result"},
		$scheduled_task_hash{"Creator"},
		$scheduled_task_hash{"Schedule"},
		$scheduled_task_hash{"Task To Run"},
		$scheduled_task_hash{"Start In"},
		$scheduled_task_hash{"Comment"},
		$scheduled_task_hash{"Scheduled Task State"},
		$scheduled_task_hash{"Scheduled Type"},
		$scheduled_task_hash{"Start Time"},
		$scheduled_task_hash{"Start Date"},
		$scheduled_task_hash{"End Date"},
		$scheduled_task_hash{"Days"},
		$scheduled_task_hash{"Months"},
		$scheduled_task_hash{"Run As User"},
		$scheduled_task_hash{"Delete Task If Not Rescheduled"},
		$scheduled_task_hash{"Stop Task If Runs X Hours and X Mins"},
		$scheduled_task_hash{"Repeat: Every"},
		$scheduled_task_hash{"Repeat: Until: Time"},
		$scheduled_task_hash{"Repeat: Until: Duration"},
		$scheduled_task_hash{"Repeat: Stop If Still Running"},
		$scheduled_task_hash{"Idle Time"},
		$scheduled_task_hash{"Power Management"}) = @scheduled_task_fields;
		
		push @scheduled_task_data, \%scheduled_task_hash;
	}
	
	notify($ERRORS{'DEBUG'}, 0, "found " . scalar(@scheduled_task_data) . " scheduled tasks");
	
	return \@scheduled_task_data;
} ## end sub disable_scheduled_task

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

=head2 disable_dynamic_dns

 Parameters  :
 Returns     :
 Description :

=cut

sub disable_dynamic_dns {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $registry_string .= <<"EOF";
Windows Registry Editor Version 5.00

; This file disables dynamic DNS updates

[HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters]
"DisableDynamicUpdate"=dword:00000001

[HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters]
"DisableReverseAddressRegistrations"=dword:00000001
EOF

	# Import the string into the registry
	if ($self->import_registry_string($registry_string)) {
		notify($ERRORS{'OK'}, 0, "disabled dynamic dns");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to disable dynamic dns");
		return;
	}

	# Get the network configuration
	my $network_configuration = $self->get_network_configuration();
	if (!$network_configuration) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve network configuration");
		return;
	}
	
	# Get the public and private interface names
	my $public_interface_name = $self->get_public_interface_name();
	my $private_interface_name = $self->get_private_interface_name();
	
	# Assemble netsh.exe commands to disable DNS registration
	my $netsh_command;
	$netsh_command .= "$system32_path/netsh.exe interface ip set dns";
	$netsh_command .= " name = \"$public_interface_name\"";
	$netsh_command .= " source = dhcp";
	$netsh_command .= " register = none";
	$netsh_command .= " ;";
	
	$netsh_command .= "$system32_path/netsh.exe interface ip set dns";
	$netsh_command .= " name = \"$private_interface_name\"";
	$netsh_command .= " source = dhcp";
	$netsh_command .= " register = none";
	$netsh_command .= " ;";
	
	# Execute the netsh.exe command
	my ($netsh_exit_status, $netsh_output) = run_ssh_command($computer_node_name, $management_node_keys, $netsh_command);
	if (defined($netsh_exit_status)  && $netsh_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "disabled dynamic DNS registration on public and private adapters");
	}
	elsif (defined($netsh_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to disable dynamic DNS registration on public and private adapters, exit status: $netsh_exit_status, output:\n@{$netsh_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to disable dynamic DNS registration on public and private adapters");
		return;
	}

	return 1;
} ## end sub disable_dynamic_dns

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

=head2 disable_netbios

 Parameters  :
 Returns     :
 Description :

=cut

sub disable_netbios {
	my $self = shift;
	if (ref($self) !~ /windows/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 query the registry for the NetBT service parameters
	my $interface_registry_data = $self->reg_query('HKLM/SYSTEM/CurrentControlSet/Services/NetBT/Parameters/Interfaces');
	if (!$interface_registry_data) {
		notify($ERRORS{'WARNING'}, 0, "failed to query registry to determine NetBT network interface strings");
		return;
	}
	
	my @interface_keys = grep(/Tcpip_/i, keys %{$interface_registry_data});
	notify($ERRORS{'DEBUG'}, 0, "retrieved NetBT interface keys:\n" . join("\n", @interface_keys));
	
	for my $interface_key (@interface_keys) {
		my $netbios_options = $interface_registry_data->{$interface_key}{NetbiosOptions};
		
		if ($self->reg_add($interface_key, 'NetbiosOptions', 'REG_DWORD', 2)) {
			notify($ERRORS{'OK'}, 0, "disabled Netbios for interface: $interface_key");
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to disabled Netbios for interface: $interface_key");
			return;
		}
	}

	return 1;
} ## end sub disable_netbios

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

=head2 set_computer_description

 Parameters  :
 Returns     :
 Description :

=cut

sub set_computer_description {
	my $self = shift;
	if (ref($self) !~ /windows/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 description from the arguments
	my $description = shift;
	if (!$description) {
		my $image_name       = $self->data->get_image_name();
		my $image_prettyname = $self->data->get_image_prettyname();
		$description = "$image_prettyname ($image_name)";
	}

	my $registry_string .= <<"EOF";
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\LanmanServer\\Parameters]
"srvcomment"="$description"
EOF

	# Import the string into the registry
	if ($self->import_registry_string($registry_string)) {
		notify($ERRORS{'OK'}, 0, "set computer description to '$description'");
		return 1;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to set computer description to '$description'");
		return 0;
	}
} ## end sub set_computer_description

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

=head2 set_my_computer_name

 Parameters  :
 Returns     :
 Description :

=cut

sub set_my_computer_name {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	my $image_prettyname     = $self->data->get_image_prettyname();
	
	my $value = shift;
	$value = $image_prettyname if !$value;

	my $add_registry_command .= $system32_path . "/reg.exe add \"HKCR\\CLSID\\{20D04FE0-3AEA-1069-A2D8-08002B30309D}\" /v LocalizedString /t REG_EXPAND_SZ /d \"$value\" /f";
	my ($add_registry_exit_status, $add_registry_output) = run_ssh_command($computer_node_name, $management_node_keys, $add_registry_command, '', '', 1);
	if (defined($add_registry_exit_status) && $add_registry_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "my computer name changed to '$value'");
	}
	elsif ($add_registry_exit_status) {
		notify($ERRORS{'WARNING'}, 0, "failed to change my computer name to '$value', exit status: $add_registry_exit_status, output:\n@{$add_registry_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to change my computer name to '$value'");
		return;
	}

	return 1;
} ## end sub set_my_computer_name

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

=head2 firewall_enable_ping

 Parameters  : 
 Returns     : 1 if succeeded, 0 otherwise
 Description : 

=cut

sub firewall_enable_ping {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $netsh_command;
	$netsh_command .= "$system32_path/netsh.exe firewall set icmpsetting";
	$netsh_command .= " type = 8";
	$netsh_command .= " mode = ENABLE";
	$netsh_command .= " profile = ALL";

	# Execute the netsh.exe command
	my ($netsh_exit_status, $netsh_output) = run_ssh_command($computer_node_name, $management_node_keys, $netsh_command);
	
	if (defined($netsh_output)  && @$netsh_output[-1] =~ /(Ok|The object already exists)/i) {
		notify($ERRORS{'OK'}, 0, "configured firewall to allow ping");
	}
	elsif (defined($netsh_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to configure firewall to allow ping, exit status: $netsh_exit_status, output:\n@{$netsh_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to configure firewall to allow ping");
		return;
	}

	return 1;
}

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

=head2 firewall_enable_ping_private

 Parameters  : 
 Returns     : 1 if succeeded, 0 otherwise
 Description : 

=cut

sub firewall_enable_ping_private {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $netsh_command;
	
	# Get the public interface name
	# Add command to disable ping on public interface if its name is found
	my $public_interface_name = $self->get_public_interface_name();
	if ($public_interface_name) {
		notify($ERRORS{'DEBUG'}, 0, "ping will be disabled on public interface: $public_interface_name");
		
		$netsh_command .= "$system32_path/netsh.exe firewall set icmpsetting";
		$netsh_command .= " type = 8";
		$netsh_command .= " mode = DISABLE";
		$netsh_command .= " interface = \"$public_interface_name\"";
		$netsh_command .= ' ;';
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "ping will not be disabled on public interface because public interface name could not be determined");
	}
	
	# Get the private interface name
	# Add command to ensable ping on private interface if its name is found
	my $private_interface_name = $self->get_private_interface_name();
	if ($private_interface_name) {
		notify($ERRORS{'DEBUG'}, 0, "ping will be enabled on private interface: $private_interface_name");
		
		$netsh_command .= "$system32_path/netsh.exe firewall set icmpsetting";
		$netsh_command .= " type = 8";
		$netsh_command .= " mode = DISABLE";
		$netsh_command .= " profile = ALL";
		$netsh_command .= ' ;';
		
		$netsh_command .= "$system32_path/netsh.exe firewall set icmpsetting";
		$netsh_command .= " type = 8";
		$netsh_command .= " mode = ENABLE";
		$netsh_command .= " interface = \"$private_interface_name\"";
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "private interface name could not be determined, ping will be enabled for all profiles");
		
		$netsh_command .= "$system32_path/netsh.exe firewall set icmpsetting";
		$netsh_command .= " type = 8";
		$netsh_command .= " mode = ENABLE";
		$netsh_command .= " profile = ALL";
		$netsh_command .= ' ;';
	}
	
	# Execute the netsh.exe command
	my ($netsh_exit_status, $netsh_output) = run_ssh_command($computer_node_name, $management_node_keys, $netsh_command);
	
	if (defined($netsh_output)  && @$netsh_output[-1] =~ /(Ok|The object already exists)/i) {
		notify($ERRORS{'OK'}, 0, "configured firewall to allow ping on private interface");
	}
	elsif (defined($netsh_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to configure firewall to allow ping on private interface, exit status: $netsh_exit_status, output:\n@{$netsh_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to configure firewall to allow ping on private interface");
		return;
	}
	
	return 1;
} ## end sub firewall_enable_ping_private

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

=head2 firewall_disable_ping

 Parameters  : 
 Returns     : 1 if succeeded, 0 otherwise
 Description : 

=cut

sub firewall_disable_ping {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $netsh_command;
	
	# Get the private interface name
	# Add command to disable ping on private interface if its name is found
	my $private_interface_name = $self->get_private_interface_name();
	if ($private_interface_name) {
		notify($ERRORS{'DEBUG'}, 0, "retrieved private interface name: $private_interface_name");
		
		$netsh_command .= "$system32_path/netsh.exe firewall set icmpsetting";
		$netsh_command .= " type = 8";
		$netsh_command .= " mode = DISABLE";
		$netsh_command .= " interface = \"$private_interface_name\"";
		$netsh_command .= ' ;';
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "private interface name could not be determined");
	}
	
	# Get the public interface name
	# Add command to disable ping on public interface if its name is found
	my $public_interface_name = $self->get_public_interface_name();
	if ($public_interface_name) {
		notify($ERRORS{'DEBUG'}, 0, "retrieved public interface name: $public_interface_name");
		
		$netsh_command .= "$system32_path/netsh.exe firewall set icmpsetting";
		$netsh_command .= " type = 8";
		$netsh_command .= " mode = DISABLE";
		$netsh_command .= " interface = \"$public_interface_name\"";
		$netsh_command .= ' ;';
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "public interface name could not be determined");
	}
	
	# Add command to disable ping for all profiles
	$netsh_command .= "$system32_path/netsh.exe firewall set icmpsetting";
	$netsh_command .= " type = 8";
	$netsh_command .= " mode = DISABLE";
	$netsh_command .= " profile = ALL";
	
	# Execute the netsh.exe command
	my ($netsh_exit_status, $netsh_output) = run_ssh_command($computer_node_name, $management_node_keys, $netsh_command);
	
	if (defined($netsh_output)  && @$netsh_output[-1] =~ /(Ok|The object already exists)/i) {
		notify($ERRORS{'OK'}, 0, "configured firewall to disallow ping");
	}
	elsif (defined($netsh_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to configure firewall to disallow ping, exit status: $netsh_exit_status, output:\n@{$netsh_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to configure firewall to disallow ping");
		return;
	}
	
	return 1;
}

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

=head2 firewall_enable_ssh

 Parameters  : 
 Returns     : 1 if succeeded, 0 otherwise
 Description : 

=cut

sub firewall_enable_ssh {
	my $self = shift;
	if (ref($self) !~ /windows/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Check if the remote IP was passed correctly as an argument
	my $remote_ip = shift;
	
	my $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $netsh_command;
	
	# Get the public interface name
	# Add command to disable SSH on public interface if its name is found
	my $public_interface_name = $self->get_public_interface_name();
	if ($public_interface_name) {
		notify($ERRORS{'DEBUG'}, 0, "SSH will be disabled on public interface: $public_interface_name");
		
		$netsh_command .= "$system32_path/netsh.exe firewall delete portopening";
		$netsh_command .= " protocol = TCP";
		$netsh_command .= " port = 22";
		$netsh_command .= " interface = \"$public_interface_name\"";
		$netsh_command .= ' ;';
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "SSH will not be disabled on public interface because public interface name could not be determined");
	}
	
	# Get the private interface name
	# Add command to disable SSH on private interface if its name is found
	my $private_interface_name = $self->get_private_interface_name();
	if ($private_interface_name) {
		notify($ERRORS{'DEBUG'}, 0, "SSH will be disabled on private interface: $private_interface_name");
		
		$netsh_command .= "$system32_path/netsh.exe firewall delete portopening";
		$netsh_command .= " protocol = TCP";
		$netsh_command .= " port = 22";
		$netsh_command .= " interface = \"$private_interface_name\"";
		$netsh_command .= ' ;';
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "SSH will not be disabled on private interface because private interface name could not be determined");
	}
	
	$netsh_command .= "$system32_path/netsh.exe firewall set portopening";
	$netsh_command .= " name = \"Cygwin SSHD\"";
	$netsh_command .= " protocol = TCP";
	$netsh_command .= " port = 22";
	$netsh_command .= " mode = ENABLE";
	
	if (!defined($remote_ip) || $remote_ip !~ /[\d\.\/]/) {
		$remote_ip = 'all addresses'; # Set only to display in output
		$netsh_command .= " scope = ALL";
	}
	else {
		$netsh_command .= " scope = CUSTOM";
		$netsh_command .= " addresses = $remote_ip";
	}

	# Execute the netsh.exe command
	my ($netsh_exit_status, $netsh_output) = run_ssh_command($computer_node_name, $management_node_keys, $netsh_command);
	
	if (defined($netsh_output)  && @$netsh_output[-1] =~ /(Ok|The object already exists)/i) {
		notify($ERRORS{'OK'}, 0, "configured firewall to allow SSH from $remote_ip");
	}
	elsif (defined($netsh_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to configure firewall to allow SSH from $remote_ip, exit status: $netsh_exit_status, output:\n@{$netsh_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to configure firewall to allow SSH from $remote_ip");
		return;
	}
	
	return 1;
} ## end sub firewall_enable_ssh_private

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

=head2 firewall_enable_ssh_private

 Parameters  : 
 Returns     : 1 if succeeded, 0 otherwise
 Description : 

=cut

sub firewall_enable_ssh_private {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $netsh_command;
	
	# Get the public interface name
	# Add command to disable SSH on public interface if its name is found
	my $public_interface_name = $self->get_public_interface_name();
	if ($public_interface_name) {
		notify($ERRORS{'DEBUG'}, 0, "SSH will be disabled on public interface: $public_interface_name");
		
		$netsh_command .= "$system32_path/netsh.exe firewall delete portopening";
		$netsh_command .= " protocol = TCP";
		$netsh_command .= " port = 22";
		$netsh_command .= " interface = \"$public_interface_name\"";
		$netsh_command .= ' ;';
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "SSH will not be disabled on public interface because public interface name could not be determined");
	}
	
	# Get the private interface name
	# Add command to ensable SSH on private interface if its name is found
	my $private_interface_name = $self->get_private_interface_name();
	if ($private_interface_name) {
		notify($ERRORS{'DEBUG'}, 0, "SSH will be enabled on private interface: $private_interface_name");
		
		$netsh_command .= "$system32_path/netsh.exe firewall delete portopening";
		$netsh_command .= " protocol = TCP";
		$netsh_command .= " port = 22";
		$netsh_command .= " profile = ALL";
		$netsh_command .= ' ;';
		
		$netsh_command .= "$system32_path/netsh.exe firewall set portopening";
		$netsh_command .= " name = \"Cygwin SSHD\"";
		$netsh_command .= " protocol = TCP";
		$netsh_command .= " port = 22";
		$netsh_command .= " mode = ENABLE";
		$netsh_command .= " interface = \"$private_interface_name\"";
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "private interface name could not be determined, SSH will be enabled for all profiles");
		
		$netsh_command .= "$system32_path/netsh.exe firewall set portopening";
		$netsh_command .= " name = \"Cygwin SSHD\"";
		$netsh_command .= " protocol = TCP";
		$netsh_command .= " port = 22";
		$netsh_command .= " profile = ALL";
	}
	
	# Execute the netsh.exe command
	my ($netsh_exit_status, $netsh_output) = run_ssh_command($computer_node_name, $management_node_keys, $netsh_command);
	
	if (defined($netsh_output)  && @$netsh_output[-1] =~ /(Ok|The object already exists)/i) {
		notify($ERRORS{'OK'}, 0, "configured firewall to allow SSH on private interface");
	}
	elsif (defined($netsh_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to configure firewall to allow SSH on private interface, exit status: $netsh_exit_status, output:\n@{$netsh_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to configure firewall to allow SSH on private interface");
		return;
	}
	
	return 1;
} ## end sub firewall_enable_ssh_private

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

=head2 firewall_enable_rdp

 Parameters  : 
 Returns     : 1 if succeeded, 0 otherwise
 Description : 

=cut

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

	# Check if the remote IP was passed correctly as an argument
	my $remote_ip = shift;
	
	my $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $netsh_command;
	
	# Set the key to allow remote connections whenever enabling RDP
	# Include this in the SSH command along with the netsh.exe commands rather than calling it separately for faster execution
	$netsh_command .= $system32_path . '/reg.exe ADD "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server" /t REG_DWORD /v fDenyTSConnections /d 0 /f ; ';
	
	$netsh_command .= "$system32_path/netsh.exe firewall set portopening";
	$netsh_command .= " name = \"Remote Desktop\"";
	$netsh_command .= " protocol = TCP";
	$netsh_command .= " port = 3389";
	$netsh_command .= " mode = ENABLE";
	
	if (!defined($remote_ip) || $remote_ip !~ /[\d\.\/]/) {
		$remote_ip = 'all addresses'; # Set only to display in output
		$netsh_command .= " scope = ALL";
	}
	else {
		$netsh_command .= " scope = CUSTOM";
		$netsh_command .= " addresses = $remote_ip";
	}

	# Execute the netsh.exe command
	my ($netsh_exit_status, $netsh_output) = run_ssh_command($computer_node_name, $management_node_keys, $netsh_command);
	
	if (defined($netsh_output)  && @$netsh_output[-1] =~ /(Ok|The object already exists)/i) {
		notify($ERRORS{'OK'}, 0, "configured firewall to allow RDP from $remote_ip");
	}
	elsif (defined($netsh_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to configure firewall to allow RDP from $remote_ip, exit status: $netsh_exit_status, output:\n@{$netsh_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to configure firewall to allow RDP from $remote_ip");
		return;
	}
	
	return 1;
} ## end sub firewall_enable_rdp

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

=head2 firewall_enable_rdp_private

 Parameters  : 
 Returns     : 1 if succeeded, 0 otherwise
 Description : 

=cut

sub firewall_enable_rdp_private {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $netsh_command;
	
	# Set the key to allow remote connections whenever enabling RDP
	# Include this in the SSH command along with the netsh.exe commands rather than calling it separately for faster execution
	$netsh_command .= $system32_path . '/reg.exe ADD "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server" /t REG_DWORD /v fDenyTSConnections /d 0 /f ; ';
	
	# Get the public interface name
	# Add command to disable RDP on public interface if its name is found
	my $public_interface_name = $self->get_public_interface_name();
	if ($public_interface_name) {
		notify($ERRORS{'DEBUG'}, 0, "RDP will be disabled on public interface: $public_interface_name");
		
		$netsh_command .= "$system32_path/netsh.exe firewall delete portopening";
		$netsh_command .= " protocol = TCP";
		$netsh_command .= " port = 3389";
		$netsh_command .= " interface = \"$public_interface_name\"";
		$netsh_command .= ' ;';
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "RDP will not be disabled on public interface because public interface name could not be determined");
	}
	
	# Get the private interface name
	# Add command to ensable RDP on private interface if its name is found
	my $private_interface_name = $self->get_private_interface_name();
	if ($private_interface_name) {
		notify($ERRORS{'DEBUG'}, 0, "RDP will be enabled on private interface: $private_interface_name");
		
		$netsh_command .= "netsh.exe firewall delete portopening";
		$netsh_command .= " protocol = TCP";
		$netsh_command .= " port = 3389";
		$netsh_command .= " profile = ALL";
		$netsh_command .= ' ;';
		
		$netsh_command .= "$system32_path/netsh.exe firewall set portopening";
		$netsh_command .= " name = \"Remote Desktop\"";
		$netsh_command .= " protocol = TCP";
		$netsh_command .= " port = 3389";
		$netsh_command .= " mode = ENABLE";
		$netsh_command .= " interface = \"$private_interface_name\"";
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "private interface name could not be determined, RDP will be enabled for all profiles");
		
		$netsh_command .= "$system32_path/netsh.exe firewall set portopening";
		$netsh_command .= " name = \"Remote Desktop\"";
		$netsh_command .= " protocol = TCP";
		$netsh_command .= " port = 3389";
		$netsh_command .= " profile = ALL";
	}
	
	# Execute the netsh.exe command
	my ($netsh_exit_status, $netsh_output) = run_ssh_command($computer_node_name, $management_node_keys, $netsh_command);
	
	if (defined($netsh_output)  && @$netsh_output[-1] =~ /(Ok|The object already exists)/i) {
		notify($ERRORS{'OK'}, 0, "configured firewall to allow RDP on private interface");
	}
	elsif (defined($netsh_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to configure firewall to allow RDP on private interface, exit status: $netsh_exit_status, output:\n@{$netsh_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to configure firewall to allow RDP on private interface");
		return;
	}
	
	return 1;
} ## end sub firewall_enable_ssh_private

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

=head2 firewall_disable_rdp

 Parameters  : 
 Returns     : 1 if succeeded, 0 otherwise
 Description : 

=cut

sub firewall_disable_rdp {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $netsh_command;
	
	# Get the private interface name
	# Add command to disable RDP on private interface if its name is found
	my $private_interface_name = $self->get_private_interface_name();
	if ($private_interface_name) {
		notify($ERRORS{'DEBUG'}, 0, "RDP will be disabled on private interface: $private_interface_name");
		
		$netsh_command .= "$system32_path/netsh.exe firewall delete portopening";
		$netsh_command .= " protocol = TCP";
		$netsh_command .= " port = 3389";
		$netsh_command .= " interface = \"$private_interface_name\"";
		$netsh_command .= ' ;';
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "private interface name could not be determined");
	}
	
	# Get the public interface name
	# Add command to disable RDP on public interface if its name is found
	my $public_interface_name = $self->get_public_interface_name();
	if ($public_interface_name) {
		notify($ERRORS{'DEBUG'}, 0, "RDP will be disabled on public interface: $public_interface_name");
		
		$netsh_command .= "$system32_path/netsh.exe firewall delete portopening";
		$netsh_command .= " protocol = TCP";
		$netsh_command .= " port = 3389";
		$netsh_command .= " interface = \"$public_interface_name\"";
		$netsh_command .= ' ;';
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "public interface name could not be determined");
	}
	
	# Add command to disable RDP for all profiles
	$netsh_command .= "$system32_path/netsh.exe firewall delete portopening";
	$netsh_command .= " protocol = TCP";
	$netsh_command .= " port = 3389";
	$netsh_command .= " profile = ALL";

	# Execute the netsh.exe command
	my ($netsh_exit_status, $netsh_output) = run_ssh_command($computer_node_name, $management_node_keys, $netsh_command);
	
	if (defined($netsh_output)  && @$netsh_output[-1] =~ /(Ok|The object already exists)/i) {
		notify($ERRORS{'OK'}, 0, "configured firewall to disallow RDP");
	}
	elsif (defined($netsh_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to configure firewall to disallow RDP, exit status: $netsh_exit_status, output:\n@{$netsh_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to configure firewall to disallow RDP");
		return;
	}
	
	return 1;
} ## end sub firewall_disable_rdp

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

=head2 get_network_configuration

 Parameters  : 
 Returns     :
 Description : Retrieves the network configuration from the computer. Returns
               a hash. The hash keys are the interface names:
					$hash{<interface name>}{dhcp_enabled}
					$hash{<interface name>}{description}
					$hash{<interface name>}{ip_address}
					$hash{<interface name>}{subnet_mask}
					$hash{<interface name>}{default_gateway}
					
					The hash also contains 2 keys containing the names of the
					public and private interfaces:
					$hash{public_name}
					$hash{private_name}

=cut

sub get_network_configuration {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Check if a 'public' or 'private' network type argument was specified
	my $network_type = lc(shift());
	if ($network_type && $network_type !~ /(public|private)/i) {
		notify($ERRORS{'WARNING'}, 0, "network type argument can only be 'public' or 'private'");
		return;
	}
	
	my %network_configuration;
	if (!$self->{network_configuration}) {
		notify($ERRORS{'DEBUG'}, 0, "attempting to retrieve network configuration information from $computer_node_name");
		
		# Run ipconfig /all, try twice in case it fails the first time
		my $ipconfig_attempt = 0;
		my $ipconfig_attempt_limit = 2;
		my ($ipconfig_exit_status, $ipconfig_output);
		while (++$ipconfig_attempt) {
			($ipconfig_exit_status, $ipconfig_output) = run_ssh_command($computer_node_name, $management_node_keys, $system32_path . '/ipconfig.exe /all', '', '', 1);
			if (defined($ipconfig_exit_status) && $ipconfig_exit_status == 0) {
				last;
			}
			elsif (defined($ipconfig_exit_status)) {
				notify($ERRORS{'WARNING'}, 0, "attempt $ipconfig_attempt: failed to run ipconfig, exit status: $ipconfig_exit_status, output:\n@{$ipconfig_output}");
			}
			else {
				notify($ERRORS{'WARNING'}, 0, "attempt $ipconfig_attempt: failed to run the SSH command to run ipconfig");
			}
			
			if ($ipconfig_attempt >= $ipconfig_attempt_limit) {
				notify($ERRORS{'WARNING'}, 0, "failed to get network configuration, made $ipconfig_attempt attempts to run ipconfig");
				return;
			}
			
			sleep 2;
		}
	
		my $interface_name;
		my $previous_ip = 0;
		my $setting;
		
		for my $line (@{$ipconfig_output}) {
			# Find beginning of interface section
			if ($line =~ /\A[^\s].*adapter (.*):\s*\Z/i) {
				# Get the interface name
				$interface_name = $1;
				notify($ERRORS{'DEBUG'}, 0, "found interface: $interface_name");
				next;
			}
			
			# Skip line if interface hasn't been found yet
			next if !$interface_name;
			
			# Check if the interface should be ignored based on the name or description
			if ($interface_name =~ /loopback|vmnet|afs|tunnel|6to4|isatap|teredo/i) {
				next;
			}
			
			# Take apart the line finding the setting name and value with a hideous regex
			my ($line_setting, $value) = $line =~ /^[ ]{1,8}(\w[^\.]*\w)?[ \.:]+([^\r\n]*)/i;
			
			# If the setting was found in the line, use it
			# Otherwise, use the last found setting
			$setting = $line_setting if $line_setting;
			
			# Skip line if value wasn't found
			next if !$value;
			
			# Normalize the setting format, make it lowercase, convert dashes and spaces to underscores
			$setting = lc($setting);
			$setting =~ s/[ -]/_/g;
			
			# Windows 6.x includes a version indicator in IP address lines such as IPv4, remove this
			$setting =~ s/ip(v\d)?_address/ip_address/;
			
			# Autoconfiguration ip address will be displayed as "Autoconfiguration IP Address. . . : 169.x.x.x"
			$setting =~ s/autoconfiguration_ip/ip/;
			
			# Remove the trailing s from dns_servers
			$setting =~ s/dns_servers/dns_server/;
			
			# Check which setting was found and add to hash
			if ($setting =~ /dns_servers/) {
				push(@{$network_configuration{$interface_name}{$setting}}, $value);
				#notify($ERRORS{'OK'}, 0, "$interface_name:$setting = @{$network_configuration{$interface_name}{$setting}}");
			}
			elsif ($setting =~ /ip_address/) {
				$value =~ s/[^\.\d]//g;
				$network_configuration{$interface_name}{$setting}{$value} = '';
				$previous_ip = $value;
			}
			elsif ($setting =~ /subnet_mask/) {
				$network_configuration{$interface_name}{ip_address}{$previous_ip} = $value;
			}
			elsif ($setting) {
				$network_configuration{$interface_name}{$setting} = $value;
			}
		}
		notify($ERRORS{'DEBUG'}, 0, 'saving network configuration in $self->{network_configuration}');
		$self->{network_configuration} = \%network_configuration;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "network configuration has already been retrieved");
		%network_configuration = %{$self->{network_configuration}};
	}
	
	# 'public' or 'private' wasn't specified, return all network interface information
	if (!$network_type) {
		notify($ERRORS{'DEBUG'}, 0, "retrieved network configuration:\n" . format_data(\%network_configuration));
		return \%network_configuration;
	}
	
	# Get the computer private IP address
	my $computer_private_ip_address = $self->data->get_computer_private_ip_address();
	if (!$computer_private_ip_address) {
		notify($ERRORS{'DEBUG'}, 0, "unable to retrieve computer private IP address from reservation data");
		return;
	}
	
	my $public_interface_name;
	my $private_interface_name;
	
	# Loop through all of the network interfaces found
	INTERFACE: foreach my $interface_name (sort keys %network_configuration) {
		my @ip_addresses  = keys %{$network_configuration{$interface_name}{ip_address}};
		my $description = $network_configuration{$interface_name}{description};
		$description = '' if !$description;

		# Make sure an IP address was found
		if (!@ip_addresses) {
			notify($ERRORS{'DEBUG'}, 0, "interface does not have an ip address: $interface_name");
			next;
		}
		
		# Check if interface has private IP address assigned to it
		if (grep { $_ eq $computer_private_ip_address } @ip_addresses) {
			# If private interface information was requested, return a hash containing only this interface
			notify($ERRORS{'DEBUG'}, 0, "private interface found: $interface_name, description: $description, address(es): " . join (", ", @ip_addresses));
			$private_interface_name = $interface_name;
			if ($network_type =~ /private/i) {
				last INTERFACE;
			}
			else {
				next INTERFACE;
			}
		}
		elsif ($network_type =~ /private/i) {
			notify($ERRORS{'DEBUG'}, 0, "interface is not assigned the private IP address: $interface_name (" . join (", ", @ip_addresses) . ")");
			next INTERFACE;
		}
		
		# Check if the interface should be ignored based on the name or description
		if ($interface_name =~ /(loopback|vmnet|afs|tunnel|6to4|isatap|teredo)/i) {
			notify($ERRORS{'DEBUG'}, 0, "interface '$interface_name' ignored because name contains '$1', address(es): " . join (", ", @ip_addresses));
			next INTERFACE;
		}
		elsif ($description =~ /(loopback|virtual|afs|tunnel|pseudo|6to4|isatap)/i) {
			notify($ERRORS{'DEBUG'}, 0, "interface '$interface_name' ignored because description contains '$1': '$description', address(es): " . join (", ", @ip_addresses));
			next INTERFACE;
		}
		
		# Loop through the IP addresses for the interface
		# Once a public address is found, return the data for that interface
		IP_ADDRESS: for my $ip_address (@ip_addresses) {
			my $is_public = is_public_ip_address($ip_address);
			my $default_gateway = $network_configuration{$interface_name}{default_gateway};
			
			if ($is_public) {
				if ($default_gateway) {
					notify($ERRORS{'DEBUG'}, 0, "public interface found with default gateway: $interface_name, address(es): " . join (", ", @ip_addresses) . ", default gateway: $default_gateway");
					$public_interface_name = $interface_name;
					last INTERFACE;
				}
				else {
					notify($ERRORS{'DEBUG'}, 0, "interface found with public address but default gateway is not set: $interface_name, address(es): " . join (", ", @ip_addresses));
					$public_interface_name = $interface_name;
				}
			}
			else {
				notify($ERRORS{'DEBUG'}, 0, "interface found with non-public address not matching private address for reservation: $interface_name, address(es): " . join (", ", @ip_addresses));
				
				if ($public_interface_name) {
					notify($ERRORS{'DEBUG'}, 0, "already found another interface with a non-public address not matching private address for reservation, the one previously found will be used if a public address isn't found");
					next;
				}
				else {
					notify($ERRORS{'DEBUG'}, 0, "interface will be returned if another with a public address isn't found");
					$public_interface_name = $interface_name;
				}
			}
			
		}
	}

	if ($network_type =~ /private/i) {
		if ($private_interface_name) {
			return {$private_interface_name => $network_configuration{$private_interface_name}};
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "did not find an interface using the private IP address for the reservation: $computer_private_ip_address\n" . format_data(\%network_configuration));
			return;
		}
	}
	else {
		if ($public_interface_name) {
			return {$public_interface_name => $network_configuration{$public_interface_name}};
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to find a public interface:\n" . format_data(\%network_configuration));
			return;
		}
	}
} ## end sub get_network_configuration

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

=head2 get_private_interface_name

 Parameters  : 
 Returns     : 
 Description : 

=cut

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

	# Make sure network configuration was retrieved
	my $network_configuration = $self->get_network_configuration('private');
	if (!$network_configuration) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve network configuration");
		return;
	}
	
	my $interface_name = (keys(%{$network_configuration}))[0];
	notify($ERRORS{'DEBUG'}, 0, "returning private interface name: $interface_name");
	
	return $interface_name;
}

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

=head2 get_public_interface_name

 Parameters  : 
 Returns     : 
 Description : 

=cut

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

	# Make sure network configuration was retrieved
	my $network_configuration = $self->get_network_configuration('public');
	if (!$network_configuration) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve network configuration");
		return;
	}
	
	my $interface_name = (keys(%{$network_configuration}))[0];
	notify($ERRORS{'DEBUG'}, 0, "returning public interface name: $interface_name");
	
	return $interface_name;
}

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

=head2 get_private_mac_address

 Parameters  : 
 Returns     : 
 Description : 

=cut

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

	# Make sure network configuration was retrieved
	my $network_configuration = $self->get_network_configuration('private');
	if (!$network_configuration) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve network configuration");
		return;
	}

	my $interface_name = (keys(%{$network_configuration}))[0];
	my $mac_address = $network_configuration->{$interface_name}{physical_address};
	notify($ERRORS{'DEBUG'}, 0, "returning private MAC address: $mac_address");
	return $mac_address;
}

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

=head2 get_public_mac_address

 Parameters  : 
 Returns     : 
 Description : 

=cut

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

	# Make sure network configuration was retrieved
	my $network_configuration = $self->get_network_configuration('public');
	if (!$network_configuration) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve network configuration");
		return;
	}

	my $interface_name = (keys(%{$network_configuration}))[0];
	my $mac_address = $network_configuration->{$interface_name}{physical_address};
	notify($ERRORS{'DEBUG'}, 0, "returning public MAC address: $mac_address");
	return $mac_address;
}

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

=head2 get_private_ip_address

 Parameters  : 
 Returns     : 
 Description : 

=cut

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

	# Make sure network configuration was retrieved
	my $network_configuration = $self->get_network_configuration('private');
	if (!$network_configuration) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve network configuration");
		return;
	}

	my $interface_name = (keys(%{$network_configuration}))[0];
	my $ip_address_config = $network_configuration->{$interface_name}{ip_address};
	my $ip_address = (keys(%$ip_address_config))[0];
	notify($ERRORS{'DEBUG'}, 0, "returning private IP address: $ip_address");
	return $ip_address;
}

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

=head2 get_public_ip_address

 Parameters  : none
 Returns     : string
 Description : Retrieves the public IP address assigned to the computer. If an
					auto-generated public IP address is detected (169.254.x.x or
					0.0.0.0), the public interface is using DHCP, and the management
					node is configured to use DHCP, an attempt will be made to call
					'ipconfig /renew' to obtain a valid public IP address.

=cut

sub get_public_ip_address {
	my $self = shift;
	if (ref($self) !~ /windows/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my $argument = shift;
	
	# Make sure network configuration was retrieved
	my $network_configuration = $self->get_network_configuration('public');
	if (!$network_configuration) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve network configuration");
		return;
	}
	
	my $public_ip_configuration = $self->data->get_management_node_public_ip_configuration();
	
	my $interface_name = (keys(%{$network_configuration}))[0];
	
	my $ip_address_config = $network_configuration->{$interface_name}{ip_address};
	my $ip_address = (keys(%$ip_address_config))[0];
	
	my $dhcp_enabled = $network_configuration->{$interface_name}{dhcp_enabled};
	
	# Check if DHCP is to be used and an auto-generated IP address is detected
	if ($public_ip_configuration =~ /dhcp/i && $dhcp_enabled =~ /yes/i && $ip_address =~ /^(169\.254\.|0\.0\.0\.0)/) {
		# Check if this is the 2nd attempt to retrieve the public IP address
		# This subroutine calls itself with a 'renewed' argument if an auto-generated IP address was detected and 'ipconfig /renew' was called
		if ($argument && $argument eq 'renewed') {
			notify($ERRORS{'WARNING'}, 0, "unable to retrieve public IP address, public interface '$interface_name' is still assigned an auto-generated IP address after attempting 'ipconfig /renew \"$interface_name\"'");
			return;
		}
		
		notify($ERRORS{'WARNING'}, 0, "public interface '$interface_name' has DHCP enabled and is assigned an auto-generated IP address: $ip_address, management node DHCP configuration: '$public_ip_configuration'");
		
		# Attempt to renew the IP address
		if (!$self->ipconfig_renew($interface_name)) {
			notify($ERRORS{'WARNING'}, 0, "unable to retrieve public IP address, failed to renew the IP address for the public interface '$interface_name'");
			return;
		}
		
		# Call this subroutine again, pass the 'renewed' argument so that 'ipconfig /renew' isn't called again (prevent infinite loop)
		return $self->get_public_ip_address('renewed');
	}
	
	notify($ERRORS{'DEBUG'}, 0, "returning public IP address: $ip_address");
	return $ip_address;
}

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

=head2 get_public_default_gateway

 Parameters  : None
 Returns     : If successful: string containing IP address
               If failed: false
 Description : Returns the default gateway currently configured for the
               computer's public interface.

=cut

sub get_public_default_gateway {
	my $self = shift;
	if (ref($self) !~ /windows/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my $computer_nodename = $self->data->get_computer_node_name() || 'node';
	
	my $default_gateway;
	
	# Check the management node's DHCP IP configuration mode (static or dynamic)
	my $ip_configuration = $self->data->get_management_node_public_ip_configuration();
	notify($ERRORS{'DEBUG'}, 0, "IP configuration mode in use: $ip_configuration");
	if ($ip_configuration !~ /static/i) {
		# Management node is using dynamic public IP addresses
		# Retrieve public network configuration currently in use on computer
		my $network_configuration = $self->get_network_configuration('public');
		if ($network_configuration) {
			# Get the default gateway out of the network configuration currently being used
			my $interface_name = (keys(%{$network_configuration}))[0];
			$default_gateway = $network_configuration->{$interface_name}{default_gateway};
			if ($default_gateway) {
				notify($ERRORS{'DEBUG'}, 0, "returning default gateway currently in use on $computer_nodename: $default_gateway");
				return $default_gateway;
			}
			else {
				notify($ERRORS{'WARNING'}, 0, "unable to determine default gateway currently in use on $computer_nodename");
			}
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "unable to retrieve public network configuration currently in use on $computer_nodename");
		}
	}
	
	# Static addresses used, get default gateway address configured for management node
	$default_gateway = $self->data->get_management_node_public_default_gateway();
	
	# Make sure default gateway was retrieved
	if ($default_gateway) {
		notify($ERRORS{'DEBUG'}, 0, "returning management node's default gateway address: $default_gateway");
		return $default_gateway;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve management node's default gateway address");
		return;
	}
}

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

=head2 get_private_subnet_mask

 Parameters  : 
 Returns     : 
 Description : 

=cut

sub get_private_subnet_mask {
	my $self = shift;
	if (ref($self) !~ /windows/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
		
	# Get private network configuration
	my $network_configuration = $self->get_network_configuration('private');
	if (!$network_configuration) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve network configuration");
		return;
	}
	
	my $interface_name = (keys(%{$network_configuration}))[0];	
	my $ip_addresses = $network_configuration->{$interface_name}{ip_address};
	my $ip_address = (keys(%$ip_addresses))[0];
	my $subnet_mask = $ip_addresses->{$ip_address};
	
	if (!$subnet_mask) {
		notify($ERRORS{'WARNING'}, 0, "unable to determine private subnet mask, network configuration:\n" . format_data($network_configuration));
		return;
	}
	
	notify($ERRORS{'DEBUG'}, 0, "returning private subnet mask: $subnet_mask");
	return $subnet_mask;
}

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

=head2 enable_dhcp

 Parameters  : 
 Returns     :
 Description : 

=cut

sub enable_dhcp {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $interface_name_argument = shift;
	my @interface_names;
	if (!$interface_name_argument) {
		push(@interface_names, $self->get_public_interface_name());
		push(@interface_names, $self->get_private_interface_name());
	}
	elsif ($interface_name_argument =~ /public/i) {
		push(@interface_names, $self->get_public_interface_name());
	}
	elsif ($interface_name_argument =~ /private/i) {
		push(@interface_names, $self->get_private_interface_name());
	}
	else {
		push(@interface_names, $interface_name_argument);
	}

	for my $interface_name (@interface_names) {
		# Use netsh.exe to set the NIC to use DHCP
		my $set_dhcp_command = $system32_path . '/netsh.exe interface ip set address name="' . $interface_name . '" source=dhcp';
		my ($set_dhcp_status, $set_dhcp_output) = run_ssh_command($computer_node_name, $management_node_keys, $set_dhcp_command);
		if (defined($set_dhcp_status) && $set_dhcp_status == 0) {
			notify($ERRORS{'OK'}, 0, "set interface '$interface_name' to use dhcp");
		}
		elsif (defined($set_dhcp_output) && grep(/dhcp is already enabled/i, @{$set_dhcp_output})) {
			notify($ERRORS{'OK'}, 0, "dhcp is already enabled on interface '$interface_name'");
		}
		elsif (defined($set_dhcp_status)) {
			notify($ERRORS{'WARNING'}, 0, "unable to set interface '$interface_name' to use dhcp, exit status: $set_dhcp_status, output:\n@{$set_dhcp_output}");
			return 0;
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to set interface '$interface_name' to use dhcp");
			return 0;
		}
	} ## end for my $interface_name (@interface_names)
	
	# Run ipconfig /renew after setting the adapters to use DHCP
	# The default gateway gets lost otherwise
	return $self->ipconfig_renew();
} ## end sub enable_dhcp

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

=head2 ipconfig_renew

 Parameters  : 
 Returns     :
 Description : 

=cut

sub ipconfig_renew {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $interface_name_argument = shift;
	
	# Assemble the ipconfig command, include the interface name if argument was specified
	my $ipconfig_command = $system32_path . '/ipconfig.exe /renew';
	if ($interface_name_argument) {
		$ipconfig_command .= " \"$interface_name_argument\"";
	}
	
	# Run ipconfig
	my ($ipconfig_status, $ipconfig_output) = run_ssh_command($computer_node_name, $management_node_keys, $ipconfig_command);
	if (defined($ipconfig_status) && $ipconfig_status == 0) {
		notify($ERRORS{'OK'}, 0, "ran ipconfig /renew");
		
		# Undefined previously retrieved network configuration so that it is retrieved again
		$self->{network_configuration} = undef;
	}
	elsif (defined($ipconfig_status)) {
		notify($ERRORS{'WARNING'}, 0, "unable to run ipconfig /renew, exit status: $ipconfig_status, output:\n@{$ipconfig_output}");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to to run ipconfig /renew");
		return 0;
	}
	
	return 1;
} 

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

=head2 delete_capture_configuration_files

 Parameters  : 
 Returns     :
 Description : Deletes the capture configuration directory.

=cut

sub delete_capture_configuration_files {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Remove old logon and logoff scripts
	$self->delete_files_by_pattern($system32_path . '/GroupPolicy/User/Scripts', '.*\(Prepare\|prepare\|Cleanup\|cleanup\|post_load\).*');

	# Remove old scripts and utilities
	$self->delete_files_by_pattern('C:/Cygwin/home/root', '.*\(vbs\|exe\|cmd\|bat\|log\)');

	# Remove old C:\VCL directory if it exists
	$self->delete_file('C:/VCL');

	# Delete VCL scheduled task if it exists
	$self->delete_scheduled_task('VCL Startup Configuration');

	# Remove VCLprepare.cmd and VCLcleanup.cmd lines from scripts.ini file
	$self->remove_group_policy_script('logon', 'VCLprepare.cmd');
	$self->remove_group_policy_script('logoff', 'VCLcleanup.cmd');

	# Remove old root Application Data/VCL directory
	$self->delete_file('$SYSTEMDRIVE/Documents and Settings/root/Application Data/VCL');

	# Remove existing configuration files if they exist
	notify($ERRORS{'OK'}, 0, "attempting to remove old configuration directory if it exists: $NODE_CONFIGURATION_DIRECTORY");
	if (!$self->delete_file($NODE_CONFIGURATION_DIRECTORY)) {
		notify($ERRORS{'WARNING'}, 0, "unable to remove existing configuration directory: $NODE_CONFIGURATION_DIRECTORY");
		return 0;
	}
	
	return 1;
}

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

=head2 add_group_policy_script

 Parameters  : 
 Returns     :
 Description : 

=cut

sub add_group_policy_script {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Get the arguments
	my $stage_argument = shift;
	my $cmdline_argument = shift;
	my $parameters_argument = shift;
	if (!$stage_argument || $stage_argument !~ /^(logon|logoff)$/i) {
		notify($ERRORS{'WARNING'}, 0, "stage (logon/logoff) argument was not specified");
		return;
	}	
	if (!$cmdline_argument) {
		notify($ERRORS{'WARNING'}, 0, "CmdLine argument was not specified");
		return;
	}
	if (!$parameters_argument) {
		$parameters_argument = '';
	}
	
	# Capitalize the first letter of logon/logoff
	$stage_argument = lc($stage_argument);
	$stage_argument = "L" . substr($stage_argument, 1);
	
	# Store the stage name (logon/logoff) not being modified
	my $opposite_stage_argument;
	if ($stage_argument =~ /logon/i) {
		$opposite_stage_argument = 'Logoff';
	}
	else {
		$opposite_stage_argument = 'Logon';
	}

	# Path to scripts.ini file
	my $scripts_ini = $system32_path . '/GroupPolicy/User/Scripts/scripts.ini';
	
	# Set the owner of scripts.ini to root
	my $chown_command = "touch $scripts_ini && chown root $scripts_ini";
	my ($chown_status, $chown_output) = run_ssh_command($computer_node_name, $management_node_keys, $chown_command);
	if (defined($chown_status) && $chown_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "set root as owner of scripts.ini");
	}
	elsif (defined($chown_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to set root as owner of scripts.ini, exit status: $chown_status, output:\n@{$chown_output}");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to set root as owner of scripts.ini");
	}
	
	# Set the permissions of scripts.ini to 664
	my $chmod_command = "chmod 664 $scripts_ini";
	my ($chmod_status, $chmod_output) = run_ssh_command($computer_node_name, $management_node_keys, $chmod_command);
	if (defined($chmod_status) && $chmod_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "ran chmod on scripts.ini");
	}
	elsif (defined($chmod_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run chmod 664 on scripts.ini, exit status: $chmod_status, output:\n@{$chmod_output}");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to run chmod 664 on scripts.ini");
	}
	
	# Clear hidden, system, and readonly flags on scripts.ini
	my $attrib_command = "attrib -H -S -R $scripts_ini";
	my ($attrib_status, $attrib_output) = run_ssh_command($computer_node_name, $management_node_keys, $attrib_command);
	if (defined($attrib_status) && $attrib_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "ran attrib -H -S -R on scripts.ini");
	}
	elsif (defined($attrib_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run attrib -H -S -R on scripts.ini, exit status: $attrib_status, output:\n@{$attrib_output}");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to run attrib -H -S -R on scripts.ini");
	}
	
	# Get the contents of scripts.ini
	my $cat_command = "cat $scripts_ini";
	my ($cat_status, $cat_output) = run_ssh_command($computer_node_name, $management_node_keys, $cat_command, '', '', 1);
	if (defined($cat_status) && $cat_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "retrieved scripts.ini contents:\n" . join("\n", @{$cat_output}));
	}
	elsif (defined($cat_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to cat scripts.ini contents");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to scripts.ini contents");
	}
	
	# Create a string containing all of the lines in scripts.ini
	my $scripts_ini_string = join("\n", @{$cat_output}) || '';
	
	# Remove any carriage returns to make pattern matching easier
	$scripts_ini_string =~ s/\r//gs;
	
	# Get a string containing just the section being modified (logon/logoff)
	my ($section_string) = $scripts_ini_string =~ /(\[$stage_argument\][^\[\]]*)/is;
	$section_string = "[$stage_argument]" if !$section_string;
	notify($ERRORS{'DEBUG'}, 0, "scripts.ini $stage_argument section:\n" . string_to_ascii($section_string));
	
	my ($opposite_section_string) = $scripts_ini_string =~ /(\[$opposite_stage_argument\][^\[\]]*)/is;
	$opposite_section_string = "[$opposite_stage_argument]" if !$opposite_section_string;
	notify($ERRORS{'DEBUG'}, 0, "scripts.ini $opposite_stage_argument section:\n" . string_to_ascii($opposite_section_string));
	
	my @section_lines = split(/[\r\n]+/, $section_string);
	notify($ERRORS{'DEBUG'}, 0, "scripts.ini $stage_argument section line count: " . scalar @section_lines);
	
	my %scripts_original;
	for my $section_line (@section_lines) {
		if ($section_line =~ /(\d+)Parameters\s*=(.*)/i) {
			my $index = $1;
			my $parameters = $2;
			if (!defined $scripts_original{$index}{Parameters}) {
				$scripts_original{$index}{Parameters} = $parameters;
				#notify($ERRORS{'DEBUG'}, 0, "found $stage_argument parameters:\nline: '$section_line'\nparameters: '$parameters'\nindex: $index");
			}
			else {
				notify($ERRORS{'WARNING'}, 0, "found duplicate $stage_argument parameters line for index $index");
			}
		}
		elsif ($section_line =~ /(\d+)CmdLine\s*=(.*)/i) {
			my $index = $1;
			my $cmdline = $2;
			if (!defined $scripts_original{$index}{CmdLine}) {
				$scripts_original{$index}{CmdLine} = $cmdline;
				#notify($ERRORS{'DEBUG'}, 0, "found $stage_argument cmdline:\nline: '$section_line'\ncmdline: '$cmdline'\nindex: $index");
			}
			else {
				notify($ERRORS{'WARNING'}, 0, "found duplicate $stage_argument CmdLine line for index $index");
			}
		}
		elsif ($section_line =~ /\[$stage_argument\]/i) {
			#notify($ERRORS{'DEBUG'}, 0, "found $stage_argument heading:\nline: '$section_line'");
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "found unexpected line: '$section_line'");
		}
	}
	
	my %scripts_modified;
	my $index_modified = 0;
	foreach my $index (sort keys %scripts_original) {
		if (!defined $scripts_original{$index}{CmdLine}) {
			notify($ERRORS{'WARNING'}, 0, "CmdLine not specified for index $index");
			next;
		}
		elsif ($scripts_original{$index}{CmdLine} =~ /^\s*$/) {
			notify($ERRORS{'WARNING'}, 0, "CmdLine blank for index $index");
			next;
		}
		if (!defined $scripts_original{$index}{Parameters}) {
			notify($ERRORS{'WARNING'}, 0, "Parameters not specified for index $index");
			$scripts_original{$index}{Parameters} = '';
		}
		
		if ($scripts_original{$index}{CmdLine} =~ /$cmdline_argument/i && $scripts_original{$index}{Parameters} =~ /$parameters_argument/i) {
			notify($ERRORS{'DEBUG'}, 0, "replacing existing $stage_argument script at index $index:\ncmdline: $scripts_original{$index}{CmdLine}\nparameters: $scripts_original{$index}{Parameters}");
		}
		else {
			notify($ERRORS{'DEBUG'}, 0, "retaining existing $stage_argument script at index $index:\ncmdline: $scripts_original{$index}{CmdLine}\nparameters: $scripts_original{$index}{Parameters}");
			$scripts_modified{$index_modified}{CmdLine} = $scripts_original{$index}{CmdLine};
			$scripts_modified{$index_modified}{Parameters} = $scripts_original{$index}{Parameters};
			$index_modified++;
		}
	}
	
	# Add the argument script to the hash
	$scripts_modified{$index_modified}{CmdLine} = $cmdline_argument;
	$scripts_modified{$index_modified}{Parameters} = $parameters_argument;
	$index_modified++;
	
	#notify($ERRORS{'DEBUG'}, 0, "arguments:\ncmdline: $cmdline_argument\nparameters: $parameters_argument");
	#notify($ERRORS{'DEBUG'}, 0, "original $stage_argument scripts data:\n" . format_data(\%scripts_original));
	#notify($ERRORS{'DEBUG'}, 0, "modified $stage_argument scripts data:\n" . format_data(\%scripts_modified));
	
	my $section_string_new = "[$stage_argument]\n";
	foreach my $index_new (sort keys(%scripts_modified)) {
		$section_string_new .= $index_new . "CmdLine=$scripts_modified{$index_new}{CmdLine}\n";
		$section_string_new .= $index_new . "Parameters=$scripts_modified{$index_new}{Parameters}\n";
	}
	
	notify($ERRORS{'DEBUG'}, 0, "original $stage_argument scripts section:\n$section_string");
	notify($ERRORS{'DEBUG'}, 0, "modified $stage_argument scripts section:\n$section_string_new");
	
	my $scripts_ini_modified;
	if ($stage_argument =~ /logon/i) {
		$scripts_ini_modified = "$section_string_new\n$opposite_section_string";
	}
	else {
		$scripts_ini_modified = "$opposite_section_string\n$section_string_new";
	}
	notify($ERRORS{'DEBUG'}, 0, "modified scripts.ini contents:\n$scripts_ini_modified");
	
	# Escape quote characters
	$scripts_ini_modified =~ s/"/\\"/gs;
	
	# Echo the modified contents to scripts.ini
	my $echo_command = "echo \"$scripts_ini_modified\" > $scripts_ini";
	my ($echo_status, $echo_output) = run_ssh_command($computer_node_name, $management_node_keys, $echo_command);
	if (defined($echo_status) && $echo_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "echo'd modified contents to scripts.ini");
	}
	elsif (defined($echo_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to echo modified contents to scripts.ini, exit status: $echo_status, output:\n@{$echo_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to echo modified contents to scripts.ini");
		return;
	}
	
	# Run unix2dos on scripts.ini
	$self->run_unix2dos($scripts_ini);
	
	# Get the modified contents of scripts.ini
	my $cat_modified_command = "cat $scripts_ini";
	my ($cat_modified_status, $cat_modified_output) = run_ssh_command($computer_node_name, $management_node_keys, $cat_modified_command, '', '', 1);
	if (defined($cat_modified_status) && $cat_modified_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "retrieved modified scripts.ini contents");
	}
	elsif (defined($cat_modified_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to cat scripts.ini contents");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to scripts.ini contents");
	}
	
	## Run gpupdate so the new settings take effect immediately
	#$self->run_gpupdate();
	
	notify($ERRORS{'OK'}, 0, "added '$cmdline_argument' $stage_argument script to scripts.ini\noriginal contents:\n$scripts_ini_string\n-----\nnew contents:\n" . join("\n", @{$cat_modified_output}));
	return 1;
}

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

=head2 remove_group_policy_script

 Parameters  : 
 Returns     :
 Description : 

=cut

sub remove_group_policy_script {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Get the arguments
	my $stage_argument = shift;
	my $cmdline_argument = shift;
	if (!$stage_argument || $stage_argument !~ /^(logon|logoff)$/i) {
		notify($ERRORS{'WARNING'}, 0, "stage (logon/logoff) argument was not specified");
		return;
	}	
	if (!$cmdline_argument) {
		notify($ERRORS{'WARNING'}, 0, "CmdLine argument was not specified");
		return;
	}
	
	# Capitalize the first letter of logon/logoff
	$stage_argument = lc($stage_argument);
	$stage_argument = "L" . substr($stage_argument, 1);
	
	# Store the stage name (logon/logoff) not being modified
	my $opposite_stage_argument;
	if ($stage_argument =~ /logon/i) {
		$opposite_stage_argument = 'Logoff';
	}
	else {
		$opposite_stage_argument = 'Logon';
	}
	
	# Attempt to delete batch or script files specified by the argument
	$self->delete_files_by_pattern("$system32_path/GroupPolicy/User/Scripts", ".*$cmdline_argument.*", 2);

	# Path to scripts.ini file
	my $scripts_ini = $system32_path . '/GroupPolicy/User/Scripts/scripts.ini';
	
	# Set the owner of scripts.ini to root
	my $chown_command = "touch $scripts_ini && chown root $scripts_ini";
	my ($chown_status, $chown_output) = run_ssh_command($computer_node_name, $management_node_keys, $chown_command);
	if (defined($chown_output) && grep(/no such file/i, @$chown_output)) {
		notify($ERRORS{'DEBUG'}, 0, "scripts.ini file does not exist, nothing to remove");
		return 1;
	}
	elsif (defined($chown_status) && $chown_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "set root as owner of scripts.ini");
	}
	elsif (defined($chown_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to set root as owner of scripts.ini, exit status: $chown_status, output:\n@{$chown_output}");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to set root as owner of scripts.ini");
	}
	
	# Set the permissions of scripts.ini to 664
	my $chmod_command = "chmod 664 $scripts_ini";
	my ($chmod_status, $chmod_output) = run_ssh_command($computer_node_name, $management_node_keys, $chmod_command);
	if (defined($chmod_status) && $chmod_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "ran chmod on scripts.ini");
	}
	elsif (defined($chmod_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run chmod 664 on scripts.ini, exit status: $chmod_status, output:\n@{$chmod_output}");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to run chmod 664 on scripts.ini");
	}
	
	# Clear hidden, system, and readonly flags on scripts.ini
	my $attrib_command = "attrib -H -S -R $scripts_ini";
	my ($attrib_status, $attrib_output) = run_ssh_command($computer_node_name, $management_node_keys, $attrib_command);
	if (defined($attrib_status) && $attrib_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "ran attrib -H -S -R on scripts.ini");
	}
	elsif (defined($attrib_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run attrib -H -S -R on scripts.ini, exit status: $attrib_status, output:\n@{$attrib_output}");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to run attrib -H -S -R on scripts.ini");
	}
	
	# Get the contents of scripts.ini
	my $cat_command = "cat $scripts_ini";
	my ($cat_status, $cat_output) = run_ssh_command($computer_node_name, $management_node_keys, $cat_command, '', '', 1);
	if (defined($cat_status) && $cat_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "retrieved scripts.ini contents:\n" . join("\n", @{$cat_output}));
	}
	elsif (defined($cat_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to cat scripts.ini contents");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to scripts.ini contents");
	}
	
	# Create a string containing all of the lines in scripts.ini
	my $scripts_ini_string = join("\n", @{$cat_output}) || '';
	
	# Remove any carriage returns to make pattern matching easier
	$scripts_ini_string =~ s/\r//gs;
	
	# Get a string containing just the section being modified (logon/logoff)
	my ($section_string) = $scripts_ini_string =~ /(\[$stage_argument\][^\[\]]*)/is;
	$section_string = "[$stage_argument]" if !$section_string;
	notify($ERRORS{'DEBUG'}, 0, "scripts.ini $stage_argument section:\n" . string_to_ascii($section_string));
	
	my ($opposite_section_string) = $scripts_ini_string =~ /(\[$opposite_stage_argument\][^\[\]]*)/is;
	$opposite_section_string = "[$opposite_stage_argument]" if !$opposite_section_string;
	notify($ERRORS{'DEBUG'}, 0, "scripts.ini $opposite_stage_argument section:\n" . string_to_ascii($opposite_section_string));
	
	my @section_lines = split(/[\r\n]+/, $section_string);
	notify($ERRORS{'DEBUG'}, 0, "scripts.ini $stage_argument section line count: " . scalar @section_lines);
	
	my %scripts_original;
	for my $section_line (@section_lines) {
		if ($section_line =~ /(\d+)Parameters\s*=(.*)/i) {
			my $index = $1;
			my $parameters = $2;
			if (!defined $scripts_original{$index}{Parameters}) {
				$scripts_original{$index}{Parameters} = $parameters;
				#notify($ERRORS{'DEBUG'}, 0, "found $stage_argument parameters:\nline: '$section_line'\nparameters: '$parameters'\nindex: $index");
			}
			else {
				notify($ERRORS{'WARNING'}, 0, "found duplicate $stage_argument parameters line for index $index");
			}
		}
		elsif ($section_line =~ /(\d+)CmdLine\s*=(.*)/i) {
			my $index = $1;
			my $cmdline = $2;
			if (!defined $scripts_original{$index}{CmdLine}) {
				$scripts_original{$index}{CmdLine} = $cmdline;
				#notify($ERRORS{'DEBUG'}, 0, "found $stage_argument cmdline:\nline: '$section_line'\ncmdline: '$cmdline'\nindex: $index");
			}
			else {
				notify($ERRORS{'WARNING'}, 0, "found duplicate $stage_argument CmdLine line for index $index");
			}
		}
		elsif ($section_line =~ /\[$stage_argument\]/i) {
			#notify($ERRORS{'DEBUG'}, 0, "found $stage_argument heading:\nline: '$section_line'");
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "found unexpected line: '$section_line'");
		}
	}
	
	my %scripts_modified;
	my $index_modified = 0;
	foreach my $index (sort keys %scripts_original) {
		if (!defined $scripts_original{$index}{CmdLine}) {
			notify($ERRORS{'WARNING'}, 0, "CmdLine not specified for index $index");
			next;
		}
		elsif ($scripts_original{$index}{CmdLine} =~ /^\s*$/) {
			notify($ERRORS{'WARNING'}, 0, "CmdLine blank for index $index");
			next;
		}
		if (!defined $scripts_original{$index}{Parameters}) {
			notify($ERRORS{'WARNING'}, 0, "Parameters not specified for index $index");
			$scripts_original{$index}{Parameters} = '';
		}
		
		if ($scripts_original{$index}{CmdLine} =~ /$cmdline_argument/i) {
			notify($ERRORS{'DEBUG'}, 0, "removing $stage_argument script at index $index:\ncmdline: $scripts_original{$index}{CmdLine}\nparameters: $scripts_original{$index}{Parameters}");
		}
		else {
			notify($ERRORS{'DEBUG'}, 0, "retaining existing $stage_argument script at index $index:\ncmdline: $scripts_original{$index}{CmdLine}\nparameters: $scripts_original{$index}{Parameters}");
			$scripts_modified{$index_modified}{CmdLine} = $scripts_original{$index}{CmdLine};
			$scripts_modified{$index_modified}{Parameters} = $scripts_original{$index}{Parameters};
			$index_modified++;
		}
	}
	
	my $section_string_new = "[$stage_argument]\n";
	foreach my $index_new (sort keys(%scripts_modified)) {
		$section_string_new .= $index_new . "CmdLine=$scripts_modified{$index_new}{CmdLine}\n";
		$section_string_new .= $index_new . "Parameters=$scripts_modified{$index_new}{Parameters}\n";
	}
	
	notify($ERRORS{'DEBUG'}, 0, "original $stage_argument scripts section:\n$section_string");
	notify($ERRORS{'DEBUG'}, 0, "modified $stage_argument scripts section:\n$section_string_new");
	
	my $scripts_ini_modified;
	if ($stage_argument =~ /logon/i) {
		$scripts_ini_modified = "$section_string_new\n$opposite_section_string";
	}
	else {
		$scripts_ini_modified = "$opposite_section_string\n$section_string_new";
	}
	notify($ERRORS{'DEBUG'}, 0, "modified scripts.ini contents:\n$scripts_ini_modified");
	
	$scripts_ini_modified =~ s/"/\\"/gs;
	
	# Echo the modified contents to scripts.ini
	my $echo_command = "echo \"$scripts_ini_modified\" > $scripts_ini";
	my ($echo_status, $echo_output) = run_ssh_command($computer_node_name, $management_node_keys, $echo_command);
	if (defined($echo_status) && $echo_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "echo'd modified contents to scripts.ini");
	}
	elsif (defined($echo_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to echo modified contents to scripts.ini, exit status: $echo_status, output:\n@{$echo_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to echo modified contents to scripts.ini");
		return;
	}
	
	# Run unix2dos on scripts.ini
	$self->run_unix2dos($scripts_ini);
	
	# Get the modified contents of scripts.ini
	my $cat_modified_command = "cat $scripts_ini";
	my ($cat_modified_status, $cat_modified_output) = run_ssh_command($computer_node_name, $management_node_keys, $cat_modified_command, '', '', 1);
	if (defined($cat_modified_status) && $cat_modified_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "retrieved modified scripts.ini contents");
	}
	elsif (defined($cat_modified_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to cat scripts.ini contents");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to scripts.ini contents");
	}
	
	notify($ERRORS{'OK'}, 0, "removed '$cmdline_argument' $stage_argument script from scripts.ini\noriginal contents:\n$scripts_ini_string\n-----\nnew contents:\n" . join("\n", @{$cat_modified_output}));
	
	## Run gpupdate so the new settings take effect immediately
	#$self->run_gpupdate();
	
	return 1;
}

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

=head2 run_gpupdate

 Parameters  : 
 Returns     :
 Description : 

=cut

sub run_gpupdate {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $gpupdate_command = "cmd.exe /c $system32_path/gpupdate.exe /Force";
	my ($gpupdate_status, $gpupdate_output) = run_ssh_command($computer_node_name, $management_node_keys, $gpupdate_command);
	if (defined($gpupdate_output) && !grep(/error/i, @{$gpupdate_output})) {
		notify($ERRORS{'OK'}, 0, "ran gpupdate /force");
	}
	elsif (defined($gpupdate_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run gpupdate /force, exit status: $gpupdate_status, output:\n@{$gpupdate_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to run gpupdate /force");
		return;
	}
	
	return 1;
}

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

=head2 run_unix2dos

 Parameters  : 
 Returns     :
 Description : 

=cut

sub run_unix2dos {
	my $self = shift;
	if (ref($self) !~ /windows/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();

	# Get the arguments
	my $file_path = shift;
	if (!$file_path) {
		notify($ERRORS{'WARNING'}, 0, "file path was not specified as an argument");
		return;
	}

	# Run unix2dos on scripts.ini
	my $unix2dos_command = "unix2dos $file_path";
	my ($unix2dos_status, $unix2dos_output) = run_ssh_command($computer_node_name, $management_node_keys, $unix2dos_command);
	if (defined($unix2dos_status) && $unix2dos_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "ran unix2dos on $file_path");
	}
	elsif (defined($unix2dos_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run unix2dos on $file_path, exit status: $unix2dos_status, output:\n@{$unix2dos_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to run unix2dos on $file_path");
		return;
	}
	
	return 1;
}

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

=head2 search_and_replace_in_files

 Parameters  : 
 Returns     :
 Description : 

=cut

sub search_and_replace_in_files {
	my $self = shift;
	if (ref($self) !~ /windows/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();

	# Get the arguments
	my $base_directory = shift;
	my $search_pattern = shift;
	my $replace_string = shift;
	if (!$base_directory) {
		notify($ERRORS{'WARNING'}, 0, "base directory was not specified as an argument");
		return;
	}
	if (!$search_pattern) {
		notify($ERRORS{'WARNING'}, 0, "search pattern was not specified as an argument");
		return;
	}
	if (!$replace_string) {
		notify($ERRORS{'WARNING'}, 0, "replace string was not specified as an argument");
		return;
	}
	
	# Replace backslashes with a forward slash in the base directory path
	$base_directory =~ s/\\+/\//g;
	
	# Escape forward slashes in the search pattern and replace string
	$search_pattern =~ s/\//\\\//g;
	$replace_string =~ s/\//\\\//g;
	
	# Escape special characters in the search pattern
	$search_pattern =~ s/([!-])/\\$1/g;
	
	# Run grep to find files matching pattern
	my $grep_command = "/bin/grep -ilr \"$search_pattern\" \"$base_directory\" 2>&1 | grep -Ev \"\.(exe|dll)\"";
	my ($grep_status, $grep_output) = run_ssh_command($computer_node_name, $management_node_keys, $grep_command, '', '', 0);
	if (!defined($grep_status)) {
		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to run grep on directory: $base_directory, pattern: $search_pattern");
		return;
	}
	elsif ("@$grep_output" =~ /No such file/i) {
		notify($ERRORS{'DEBUG'}, 0, "no files to process, base directory does not exist: $base_directory");
		return 1;
	}
	elsif ("@$grep_output" =~ /(grep|bash):/i) {
		notify($ERRORS{'WARNING'}, 0, "error occurred running command '$grep_command':\n" . join("\n", @$grep_output));
		return;
	}
	elsif ($grep_status == 1) {
		notify($ERRORS{'OK'}, 0, "no files were found matching pattern '$search_pattern' in: $base_directory");
		return 1;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "found files matching pattern '$search_pattern' in $base_directory:\n" . join("\n", @$grep_output));
	}
	
	# Run sed on each matching file to replace string
	my $sed_error_count = 0;
	for my $matching_file (@$grep_output) {
		$matching_file =~ s/\\+/\//g;
		# Run grep to find files matching pattern
		my $sed_command = "/bin/sed -i -e \"s/$search_pattern/$replace_string/\" \"$matching_file\"";
		my ($sed_status, $sed_output) = run_ssh_command($computer_node_name, $management_node_keys, $sed_command, '', '', 0);
		if (!defined($sed_status)) {
			notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to run sed on file: $matching_file");
			$sed_error_count++;
		}
		elsif ("@$sed_output" =~ /No such file/i) {
			notify($ERRORS{'WARNING'}, 0, "file was not found: $matching_file, sed output:\n" . join("\n", @$sed_output));
			$sed_error_count++;
		}
		elsif ("@$grep_output" =~ /(grep|sed):/i) {
			notify($ERRORS{'WARNING'}, 0, "error occurred running command '$sed_command':\n" . join("\n", @$sed_output));
			$sed_error_count++;
		}
		elsif ($sed_status != 0) {
			notify($ERRORS{'WARNING'}, 0, "sed exit status is $sed_status, output:\n" . join("\n", @$sed_output));
			$sed_error_count++;
		}
		else {
			notify($ERRORS{'OK'}, 0, "replaced '$search_pattern' with '$replace_string' in $matching_file");
			
			# sed replaces Windows newlines with \n
			# There is a sed -b option which prevents this but it is not available on all versions of sed
			$self->run_unix2dos($matching_file);
		}
	}
	
	# Return false if any errors occurred
	if ($sed_error_count) {
		return;
	}
	
	return 1;
	
}

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

=head2 copy_capture_configuration_files

 Parameters  : $source_configuration_directory
 Returns     :
 Description : Copies all required configuration files to the computer,
               including scripts, utilities, drivers needed to capture an
				   image.

=cut

sub copy_capture_configuration_files {
	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 $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();
	
	# Get an array containing the configuration directory paths on the management node
	# This is made up of all the the $SOURCE_CONFIGURATION_DIRECTORY values for the OS class and it's parent classes
	# The first array element is the value from the top-most class the OS object inherits from
	my @source_configuration_directories = $self->get_source_configuration_directories();
	if (!@source_configuration_directories) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve source configuration directories");
		return;
	}
	
	# Delete existing configuration directory if it exists
	if (!$self->delete_capture_configuration_files()) {
		notify($ERRORS{'WARNING'}, 0, "unable to delete existing capture configuration files");
		return;
	}

	# Attempt to create the configuration directory if it doesn't already exist
	if (!$self->create_directory($NODE_CONFIGURATION_DIRECTORY)) {
		notify($ERRORS{'WARNING'}, 0, "unable to create directory on $computer_node_name: $NODE_CONFIGURATION_DIRECTORY");
		return;
	}

	# Copy configuration files
	for my $source_configuration_directory (@source_configuration_directories) {
		# Check if source configuration directory exists on this management node
		unless (-d "$source_configuration_directory") {
			notify($ERRORS{'OK'}, 0, "source directory does not exist on this management node: $source_configuration_directory");
			next;
		}
		
		notify($ERRORS{'OK'}, 0, "copying image capture configuration files from $source_configuration_directory to $computer_node_name");
		if (run_scp_command("$source_configuration_directory/*", "$computer_node_name:$NODE_CONFIGURATION_DIRECTORY", $management_node_keys)) {
			notify($ERRORS{'OK'}, 0, "copied $source_configuration_directory directory to $computer_node_name:$NODE_CONFIGURATION_DIRECTORY");
	
			notify($ERRORS{'DEBUG'}, 0, "attempting to set permissions on $computer_node_name:$NODE_CONFIGURATION_DIRECTORY");
			if (run_ssh_command($computer_node_name, $management_node_keys, "/usr/bin/chmod.exe -R 777 $NODE_CONFIGURATION_DIRECTORY")) {
				notify($ERRORS{'OK'}, 0, "chmoded -R 777 $computer_node_name:$NODE_CONFIGURATION_DIRECTORY");
			}
			else {
				notify($ERRORS{'WARNING'}, 0, "could not chmod -R 777 $computer_node_name:$NODE_CONFIGURATION_DIRECTORY");
				return;
			}
		} ## end if (run_scp_command("$source_configuration_directory/*"...
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to copy $source_configuration_directory to $computer_node_name");
			return;
		}
	}
	
	# Delete any Subversion files which may have been copied
	if (!$self->delete_files_by_pattern($NODE_CONFIGURATION_DIRECTORY, '.*\.svn.*')) {
		notify($ERRORS{'WARNING'}, 0, "unable to delete Subversion files under: $NODE_CONFIGURATION_DIRECTORY");
	}
	
	# Find any files containing a 'WINDOWS_ROOT_PASSWORD' string and replace it with the root password
	if ($self->search_and_replace_in_files($NODE_CONFIGURATION_DIRECTORY, 'WINDOWS_ROOT_PASSWORD', $WINDOWS_ROOT_PASSWORD)) {
		notify($ERRORS{'DEBUG'}, 0, "set the Windows root password in configuration files");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to set the Windows root password in configuration files");
		return;
	}

	return 1;
} ## end sub copy_capture_configuration_files

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

=head2 clean_hard_drive

 Parameters  : 
 Returns     :
 Description : Removed unnecessary files from the hard drive. This is done
               before capturing an image. Examples of unnecessary files:
					-temp files and temp directories
					-cache files
					-downloaded patch files

=cut

sub clean_hard_drive {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Note: attempt to delete everything under C:\RECYCLER before running cleanmgr.exe
	# The Recycle Bin occasionally becomes corrupted
	# cleanmgr.exe will hang with an "OK/Cancel" box on the screen if this happens
	my @patterns_to_delete = (
		'$SYSTEMDRIVE/RECYCLER,.*',
		'$TEMP,.*',
		'$TMP,.*',
		'$SYSTEMDRIVE/cygwin/tmp,.*',
		'$SYSTEMDRIVE/Temp,.*',
		'$SYSTEMROOT/Temp,.*',
		'$SYSTEMROOT/ie7updates,.*',
		'$SYSTEMROOT/ServicePackFiles,.*',
		'$SYSTEMROOT/SoftwareDistribution/Download,.*',
		'$SYSTEMROOT/Minidump,.*',
		'$ALLUSERSPROFILE/Application Data/Microsoft/Dr Watson,.*',
		'$SYSTEMROOT,.*\\.tmp,1',
		'$SYSTEMROOT,.*\\$hf_mig\\$.*,1',
		'$SYSTEMROOT,.*\\$NtUninstall.*,1',
		'$SYSTEMROOT,.*\\$NtServicePackUninstall.*,1',
		'$SYSTEMROOT,.*\\$MSI.*Uninstall.*,1',
		'$SYSTEMROOT,.*AFSCache,1',
		'$SYSTEMROOT,.*afsd_init\\.log,1',
		'$SYSTEMDRIVE/Documents and Settings,.*Recent\\/.*,10',
		'$SYSTEMDRIVE/Documents and Settings,.*Cookies\\/.*,10',
		'$SYSTEMDRIVE/Documents and Settings,.*Temp\\/.*,10',
		'$SYSTEMDRIVE/Documents and Settings,.*Temporary Internet Files\\/Content.*\\/.*,10',
		'$SYSTEMDRIVE,.*pagefile\\.sys,1',
		'$SYSTEMDRIVE/cygwin/home/root,.*%USERPROFILE%,1',
		"$system32_path/GroupPolicy/User/Scripts,.*VCL.*cmd"
	);
	
	# Attempt to stop the AFS service, needed to delete AFS files
	if ($self->service_exists('TransarcAFSDaemon')) {
		$self->stop_service('TransarcAFSDaemon');
	}

	# Loop through the directories to empty
	# Don't care if they aren't emptied
	for my $base_pattern (@patterns_to_delete) {
		my ($base_directory, $pattern, $max_depth) = split(',', $base_pattern);
		notify($ERRORS{'DEBUG'}, 0, "attempting to delete files under $base_directory matching pattern $pattern");
		$self->delete_files_by_pattern($base_directory, $pattern, $max_depth);
	}
	
	# Add the cleanmgr.exe settings to the registry
	my $registry_string .= <<"EOF";
Windows Registry Editor Version 5.00

; This registry file contains the entries to turn on all cleanmgr options 
; The state flags below are set to 1, so use the command: 'CLEANMGR /sagerun:1'

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches]

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches\\Active Setup Temp Folders]
"StateFlags0001"=dword:00000002

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches\\Content Indexer Cleaner]
"StateFlags0001"=dword:00000002

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches\\Downloaded Program Files]
"StateFlags0001"=dword:00000002

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches\\Hibernation File]
"StateFlags0001"=dword:00000002

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches\\Internet Cache Files]
"StateFlags0001"=dword:00000002

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches\\Memory Dump Files]
"StateFlags0001"=dword:00000002

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches\\Offline Pages Files]
"StateFlags0001"=dword:00000002

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches\\Old ChkDsk Files]
"StateFlags0001"=dword:00000002

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches\\Previous Installations]
"StateFlags0001"=dword:00000002

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches\\Recycle Bin]
"StateFlags0001"=dword:00000002

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches\\Setup Log Files]
"StateFlags0001"=dword:00000000

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches\\System error memory dump files]
"StateFlags0001"=dword:00000002

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches\\System error minidump files]
"StateFlags0001"=dword:00000002

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches\\Temporary Files]
"StateFlags0001"=dword:00000002

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches\\Temporary Setup Files]
"StateFlags0001"=dword:00000002

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches\\Temporary Sync Files]
"StateFlags0001"=dword:00000002

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches\\Thumbnail Cache]
"StateFlags0001"=dword:00000002

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches\\Upgrade Discarded Files]
"StateFlags0001"=dword:00000002

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches\\Windows Error Reporting Archive Files]
"StateFlags0001"=dword:00000002

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches\\Windows Error Reporting Queue Files]
"StateFlags0001"=dword:00000002

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches\\Windows Error Reporting System Archive Files]
"StateFlags0001"=dword:00000002

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\explorer\\VolumeCaches\\Windows Error Reporting System Queue Files]
"StateFlags0001"=dword:00000002

EOF

	# Import the string into the registry
	if ($self->import_registry_string($registry_string)) {
		notify($ERRORS{'DEBUG'}, 0, "set registry settings to configure the disk cleanup utility");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to set registry settings to configure the disk cleanup utility");
	}
	
	# Run cleanmgr.exe
	# The cleanmgr.exe file may not be present - it is not installed by default on Windows Server 2008 and possibly others
	my $cleanmgr_command = "/bin/cygstart.exe $system32_path/cleanmgr.exe /SAGERUN:01";
	my ($cleanmgr_exit_status, $cleanmgr_output) = run_ssh_command($computer_node_name, $management_node_keys, $cleanmgr_command);
	if (!defined($cleanmgr_output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to run cleanmgr.exe");
		return;
	}
	elsif (grep(/not found/i, @$cleanmgr_output)) {
		notify($ERRORS{'OK'}, 0, "cleanmgr.exe is not present on $computer_node_name, this is usually because the Desktop Experience feature is not installed");
	}
	else {
		# Wait for cleanmgr.exe to finish
		my $message = 'waiting for cleanmgr.exe to finish';
		my $total_wait_seconds = 120;
		notify($ERRORS{'OK'}, 0, "started cleanmgr.exe, waiting up to $total_wait_seconds seconds for it to finish");
		
		if ($self->code_loop_timeout(sub{!$self->is_process_running(@_)}, ['cleanmgr.exe'], $message, $total_wait_seconds, 5)) {
			notify($ERRORS{'DEBUG'}, 0, "cleanmgr.exe has finished");
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "cleanmgr.exe has not finished after waiting $total_wait_seconds seconds, the Recycle Bin may be corrupt");
		}
	}
	
	return 1;
} ## end sub clean_hard_drive

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

=head2 is_process_running

 Parameters  : $process_identifier
 Returns     : boolean
 Description : Determines if a process is running identified by the argument.
               The argument should be the name of an executable. Wildcards (*)
               are allowed.

=cut

sub is_process_running {
	my $self = shift;
	if (ref($self) !~ /windows/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method, arguments:\n" . format_data(\@_));
		return;
	}
	
	my $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $process_identifier = shift;
	if (!defined($process_identifier)) {
		notify($ERRORS{'WARNING'}, 0, "process identifier argument was not supplied");
		return;
	}
	
	my $command = "$system32_path/tasklist.exe /FI \"IMAGENAME eq $process_identifier\"";
	my ($status, $output) = run_ssh_command($computer_node_name, $management_node_keys, $command, '', '', 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to determine if process is running: $process_identifier");
		return;
	}
	elsif (grep(/No tasks/i, @$output)) {
		notify($ERRORS{'DEBUG'}, 0, "process is NOT running: $process_identifier");
		return 0;
	}
	elsif (grep(/PID/, @$output)) {
		notify($ERRORS{'DEBUG'}, 0, "process is running: $process_identifier");
		return 1;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unexpected output returned from command to determine if process is running: '$command', output:\n" . join("\n", @$output));
		return;
	}
}

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

=head2 start_service

 Parameters  : 
 Returns     :
 Description : 

=cut

sub start_service {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $service_name = shift;
	if (!$service_name) {
		notify($ERRORS{'WARNING'}, 0, "service name was not passed as an argument");
		return;
	}

	my $command = $system32_path . '/net.exe start "' . $service_name . '"';
	my ($status, $output) = run_ssh_command($computer_node_name, $management_node_keys, $command);
	if (defined($status) && $status == 0) {
		notify($ERRORS{'OK'}, 0, "started service: $service_name");
	}
	elsif (defined($output) && grep(/already been started/i, @{$output})) {
		notify($ERRORS{'OK'}, 0, "service has already been started: $service_name");
	}
	elsif (defined($status)) {
		notify($ERRORS{'WARNING'}, 0, "unable to start service: $service_name, exit status: $status, output:\n@{$output}");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to to start service: $service_name");
		return 0;
	}

	return 1;
} ## end sub start_service

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

=head2 stop_service

 Parameters  : 
 Returns     :
 Description : 

=cut

sub stop_service {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $service_name = shift;
	if (!$service_name) {
		notify($ERRORS{'WARNING'}, 0, "service name was not passed as an argument");
		return;
	}

	my $command = $system32_path . '/net.exe stop "' . $service_name . '"';
	my ($status, $output) = run_ssh_command($computer_node_name, $management_node_keys, $command);
	if (defined($status) && $status == 0) {
		notify($ERRORS{'OK'}, 0, "stopped service: $service_name");
	}
	elsif (defined($output) && grep(/is not started/i, @{$output})) {
		notify($ERRORS{'OK'}, 0, "service is not started: $service_name");
	}
	elsif (defined($output) && grep(/does not exist/i, @{$output})) {
		notify($ERRORS{'WARNING'}, 0, "service was not stopped because it does not exist: $service_name");
		return;
	}
	elsif (defined($status)) {
		notify($ERRORS{'WARNING'}, 0, "unable to stop service: $service_name, exit status: $status, output:\n@{$output}");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to to stop service: $service_name");
		return 0;
	}

	return 1;
} ## end sub stop_service

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

=head2 service_exists

 Parameters  : $service_name
 Returns     : If service exists: 1
               If service does not exist: 0
               If error occurred: undefined
 Description : Runs sc.exe query to determine if a service exists.

=cut

sub service_exists {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $service_name = shift;
	if (!$service_name) {
		notify($ERRORS{'WARNING'}, 0, "service name was not passed as an argument");
		return;
	}

	my $command = $system32_path . '/sc.exe query "' . $service_name . '"';
	my ($status, $output) = run_ssh_command($computer_node_name, $management_node_keys, $command, '', '', 1);
	if (defined($output) && grep(/service does not exist/i, @{$output})) {
		notify($ERRORS{'DEBUG'}, 0, "service does not exist: $service_name");
		return 0;
	}
	elsif (defined($status) && $status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "service exists: $service_name");
	}
	elsif (defined($status)) {
		notify($ERRORS{'WARNING'}, 0, "unable to determine if service exists: $service_name, exit status: $status, output:\n@{$output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to determine if service exists");
		return;
	}

	return 1;
}

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

=head2 get_installed_applications

 Parameters  :
 Returns     :
 Description : Queries the registry for applications that are installed on the computer.
               Subkeys under the following key contain this information:
					HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall
					
					A reference to a hash is returned. The keys of this hash are the names of the subkeys under the Uninstall key.
					Each subkey contains additional data formatted as follows:
					my $installed_applications = $self->os->get_installed_applications();
					$installed_applications->{pdfFactory Pro}{DisplayName} = 'pdfFactory Pro'
               $installed_applications->{pdfFactory Pro}{UninstallString} = 'C:\WINDOWS\System32\spool\DRIVERS\W32X86\3\fppinst2.exe /uninstall'

=cut

sub get_installed_applications {
	my $self = shift;
	if (!ref($self)) {
		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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Get an optional regex filter string
	my $regex_filter = shift;
	if ($regex_filter) {
		notify($ERRORS{'DEBUG'}, 0, "attempting to retrieve applications installed on $computer_node_name matching filter: $regex_filter");
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "attempting to retrieve all applications installed on $computer_node_name");
	}

	# Attempt to query the registry for installed applications
	my $reg_query_command = $system32_path . '/reg.exe QUERY "HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall" /s';
	my ($reg_query_exit_status, $reg_query_output) = run_ssh_command($computer_node_name, $management_node_keys, $reg_query_command, '', '', 1);
	if (defined($reg_query_exit_status) && $reg_query_exit_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "queried Uninstall registry keys");
	}
	elsif (defined($reg_query_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to query Uninstall registry keys, exit status: $reg_query_exit_status, output:\n@{$reg_query_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to query Uninstall registry keys");
		return;
	}

	# Make sure output was retrieved
	if (!$reg_query_output || scalar @{$reg_query_output} == 0) {
		notify($ERRORS{'WARNING'}, 0, "registry query did not product any output");
		return;
	}

	#notify($ERRORS{'DEBUG'}, 0, "reg.exe query output: " . join("\n", @{$reg_query_output}));

	# Loop through the lines of output
	my $product_key;
	my %installed_products;
	for my $query_output_line (@{$reg_query_output}) {
		#notify($ERRORS{'DEBUG'}, 0, "reg.exe query output line: '" . string_to_ascii($query_output_line) . "'");

		# Remove spaces from beginning and end of line
		$query_output_line =~ s/(^\s+)|(\s+$)//g;

		# Skip lines which don't contain a word character and lines starting with ! like this one:
		#    ! REG.EXE VERSION 3.0
		if ($query_output_line =~ /^!/ || $query_output_line !~ /\w/) {
			next;
		}

		# Check if line starts with HKEY, as in:
		#    HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall\ATI Display Driver
		if ($query_output_line =~ /^HKEY.*\\(.*)\s*/) {
			# Skip first line showing the base key that was searched for:
			#    HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall
			next if ($1 eq 'Uninstall');

			$product_key = $1;
			#notify($ERRORS{'DEBUG'}, 0, "found product key: '" . string_to_ascii($product_key) . "'");
			next;
		}

		# Line is a child key of one of the products, take apart the line, looks like this:
		#    <NO NAME>       REG_SZ
		#    DisplayName     REG_SZ  F-Secure SSH Client
		my ($info_key, $info_value) = ($query_output_line =~ /\s*([^\t]+)\s+\w+\s*([^\r\n]*)/);

		# Make sure the regex found the registry key name, if not, regex needs improvement
		if (!$info_key) {
			notify($ERRORS{'WARNING'}, 0, "regex didn't work correctly finding key name and value, line:\n" . string_to_ascii($query_output_line));
			next;
		}

		# Make sure the product key was found by this point, it should have been
		if (!$product_key) {
			notify($ERRORS{'WARNING'}, 0, "product key was not determined by the time the following line was processed, line:\n$query_output_line\nreg.exe query output: @{$reg_query_output}");
			next;
		}

		# Add the key and value to the hash
		$installed_products{$product_key}{$info_key} = $info_value;
	} ## end for my $query_output_line (@{$reg_query_output...

	# If filter was specified, remove keys not matching filter
	if ($regex_filter) {
		notify($ERRORS{'DEBUG'}, 0, "finding applications matching filter: $regex_filter");
		my %matching_products;
		foreach my $product_key (sort keys %installed_products) {
			#notify($ERRORS{'DEBUG'}, 0, "checking product key: $product_key");
			if (eval "\$product_key =~ $regex_filter") {
				notify($ERRORS{'DEBUG'}, 0, "found matching product key:\n$product_key");
				$matching_products{$product_key} = $installed_products{$product_key};
				next;
			}

			foreach my $info_key (sort keys %{$installed_products{$product_key}}) {
				my $info_value = $installed_products{$product_key}{$info_key};
				#notify($ERRORS{'DEBUG'}, 0, "checking value of {$info_key}: $info_value");
				if (eval "\$info_value =~ $regex_filter") {
					notify($ERRORS{'DEBUG'}, 0, "found matching value:\n{$product_key}{$info_key} = '$info_value'");
					$matching_products{$product_key} = $installed_products{$product_key};
					last;
				}
				else {
					next;
				}
			} ## end foreach my $info_key (sort keys %{$installed_products...
		} ## end foreach my $product_key (sort keys %installed_products)
		%installed_products = %matching_products;
	} ## end if ($regex_filter)

	if (%installed_products && $regex_filter) {
		notify($ERRORS{'DEBUG'}, 0, "found the following installed applications matching filter:\n$regex_filter\n" . format_data(\%installed_products));
		return \%installed_products;
	}
	elsif (%installed_products && !$regex_filter) {
		notify($ERRORS{'DEBUG'}, 0, "found the following installed applications:\n" . format_data(\%installed_products));
		return \%installed_products;
	}
	if (!%installed_products && $regex_filter) {
		notify($ERRORS{'DEBUG'}, 0, "did not find any installed applications matching filter:\n$regex_filter");
		return 0;
	}
	elsif (!%installed_products && !$regex_filter) {
		notify($ERRORS{'DEBUG'}, 0, "did not find any installed applications");
		return 0;
	}
} ## end sub get_installed_applications

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

=head2 get_task_list

 Parameters  : None, must be called as an object method ($self->os->get_task_list())
 Returns     : If successful: Reference to an array containing the lines of output generated by tasklist.exe
               If failed: false
 Description : Runs tasklist.exe and returns its output. Tasklist.exe displays a list of applications and associated tasks running on the computer.
               The following switches are used when tasklist.exe is executed:
               /NH - specifies the column header should not be displayed in the output
					/V  - specifies that verbose information should be displayed
					The output is formatted as follows (column header is not included):
					Image Name                   PID Session Name     Session#    Mem Usage Status          User Name                                              CPU Time Window Title                                                            
               System Idle Process            0 Console                 0         16 K Running         NT AUTHORITY\SYSTEM           

=cut

sub get_task_list {
	my $self = shift;
	if (!ref($self)) {
		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();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Attempt to run tasklist.exe with /NH for no header
	my $tasklist_command = $system32_path . '/tasklist.exe /NH /V';
	my ($tasklist_exit_status, $tasklist_output) = run_ssh_command($computer_node_name, $management_node_keys, $tasklist_command, '', '', 1);
	if (defined($tasklist_exit_status) && $tasklist_exit_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "ran tasklist.exe");
	}
	elsif (defined($tasklist_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run tasklist.exe, exit status: $tasklist_exit_status, output:\n@{$tasklist_output}");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to run tasklist.exe");
		return;
	}

	return $tasklist_output;
} ## end sub get_task_list

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

=head2 apply_security_templates

 Parameters  : None
 Returns     : If successful: true
               If failed: false
 Description : Runs secedit.exe to apply the security template files configured
               for the OS. Windows security template files use the .inf
               extension.
               
               Security templates are always copied from the management node
               rather than using a copy stored locally on the computer. This
               allows templates updated centrally to always be applied to the
               computer. Template files residing locally on the computer are not
               processed.
               
               The template files should reside in a directory named "Security"
               under the OS source configuration directory. An example would be:
               
               /usr/local/vcl/tools/Windows_XP/Security/xp_security.inf
               
               This subroutine supports OS module inheritence meaning that if an
               OS module inherits from another OS module, the security templates
               of both will be applied. The order is from the highest parent
               class down to any template files configured specifically for the
               OS module which was instantiated.
               
               This allows any Windows OS module to inherit from another class
               which has security templates defined and override any settings
               from above.
               
               Multiple .inf security template files may be configured for each
               OS. They will be applied in alphabetical order.
               
               Example: Inheritence is configured as follows, with the XP module
               being the instantiated (lowest) class:
               
               VCL::Module
               ^
               VCL::Module::OS
               ^
               VCL::Module::OS::Windows
               ^
               VCL::Module::OS::Windows::Version_5
               ^
               VCL::Module::OS::Windows::Version_5::XP
               
               The XP and Windows classes each have 2 security template files
               configured in their respective Security directories:
               
               /usr/local/vcl/tools/Windows/Security/eventlog_512.inf
               /usr/local/vcl/tools/Windows/Security/windows_security.inf
               /usr/local/vcl/tools/Windows_XP/Security/xp_eventlog_4096.inf
               /usr/local/vcl/tools/Windows_XP/Security/xp_security.inf
               
               The templates will be applied in the order shown above. The
               Windows templates are applied first because it is a parent class
               of XP. For each class being processed, the files are applied in
               alphabetical order.
               
               Assume in the example above that the Windows module's
               eventlog_512.inf file configures the event log to be a maximum of
               512 KB and that it is desirable under Windows XP to configure a
               larger maximum event log size. In order to achieve this,
               xp_eventlog_4096.inf was placed in XP's Security directory which
               contains settings to set the maximum size to 4,096 KB. The
               xp_eventlog_4096.inf file is applied after the eventlog_512.inf
               file, thus overridding the setting configured in the
               eventlog_512.inf file. The resultant maximum event log size will
               be set to 4,096 KB.

=cut

sub apply_security_templates {
	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 $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Get an array containing the configuration directory paths on the management node
	# This is made up of all the the $SOURCE_CONFIGURATION_DIRECTORY values for the OS class and it's parent classes
	# The first array element is the value from the top-most class the OS object inherits from
	my @source_configuration_directories = $self->get_source_configuration_directories();
	if (!@source_configuration_directories) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve source configuration directories");
		return;
	}
	
	# Loop through the configuration directories for each OS class on the management node
	# Find any .inf files residing under Security
	my @inf_file_paths;
	for my $source_configuration_directory (@source_configuration_directories) {
		notify($ERRORS{'OK'}, 0, "checking if any security templates exist in: $source_configuration_directory/Security");
		
		# Check each source configuration directory for .inf files under a Security subdirectory
		my $find_command = "find $source_configuration_directory/Security -name \"*.inf\" 2>&1 | sort -f";
		my ($find_exit_status, $find_output) = run_command($find_command);
		if (defined($find_output) && grep(/No such file/i, @$find_output)) {
			notify($ERRORS{'DEBUG'}, 0, "path does not exist: $source_configuration_directory/Security");
		}
		elsif (defined($find_exit_status) && $find_exit_status == 0) {
			notify($ERRORS{'DEBUG'}, 0, "ran find, output:\n" . join("\n", @$find_output));
			push @inf_file_paths, @$find_output;
		}
		elsif (defined($find_exit_status)) {
			notify($ERRORS{'WARNING'}, 0, "failed to run find, exit status: $find_exit_status, output:\n@{$find_output}");
			return;
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to run local command to run find");
			return;
		}
	}
	
	# Remove any newlines from the file paths in the array
	chomp(@inf_file_paths);
	notify($ERRORS{'DEBUG'}, 0, "security templates will be applied in this order:\n" . join("\n", @inf_file_paths));
	
	# Make sure the Security directory exists before attempting to copy files or SCP will fail
	if (!$self->create_directory("$NODE_CONFIGURATION_DIRECTORY/Security")) {
		notify($ERRORS{'WARNING'}, 0, "unable to create directory: $NODE_CONFIGURATION_DIRECTORY/Security");
	}
	
	# Loop through the .inf files and apply them to the node using secedit.exe
	my $inf_count = 0;
	my $error_occurred = 0;
	for my $inf_file_path (@inf_file_paths) {
		$inf_count++;
		
		# Get the name of the file
		my ($inf_file_name) = $inf_file_path =~ /.*[\\\/](.*)/g;
		my ($inf_file_root) = $inf_file_path =~ /.*[\\\/](.*).inf/gi;
		
		# Construct the target path, prepend a number to indicate the order the files were processed
		my $inf_target_path = "$NODE_CONFIGURATION_DIRECTORY/Security/$inf_count\_$inf_file_name";
		
		# Copy the file to the node and set the permissions to 644
		notify($ERRORS{'DEBUG'}, 0, "attempting to copy file to: $inf_target_path");
		if (run_scp_command($inf_file_path, "$computer_node_name:$inf_target_path", $management_node_keys)) {
			notify($ERRORS{'DEBUG'}, 0, "copied file: $computer_node_name:$inf_target_path");
	
			# Set permission on the copied file
			if (!run_ssh_command($computer_node_name, $management_node_keys, "/usr/bin/chmod.exe -R 644 $inf_target_path", '', '', 0)) {
				notify($ERRORS{'WARNING'}, 0, "could not set permissions on $inf_target_path");
			}
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to copy $inf_file_path to $inf_target_path");
			next;
		}
		
		# Assemble the paths secedit needs
		my $secedit_exe = $system32_path . '/secedit.exe';
		my $secedit_db = '$SYSTEMROOT/security/Database/' . "$inf_count\_$inf_file_root.sdb";
		my $secedit_log = '$SYSTEMROOT/security/Logs/' . "$inf_count\_$inf_file_root.log";
		
		# Attempt to delete an existing log file
		$self->delete_file($secedit_log);
		
		# The inf path must use backslashes or secedit.exe will fail
		$inf_target_path =~ s/\//\\\\/g;
		
		# Run secedit.exe
		# Note: secedit.exe returns exit status 3 if a warning occurs, this will appear in the log file:
		# Task is completed. Warnings occurred for some attributes during this operation. It's ok to ignore.
		my $secedit_command = "$secedit_exe /configure /cfg \"$inf_target_path\" /db $secedit_db /log $secedit_log /overwrite /quiet";
		my ($secedit_exit_status, $secedit_output) = run_ssh_command($computer_node_name, $management_node_keys, $secedit_command, '', '', 0);
		if (defined($secedit_exit_status) && ($secedit_exit_status == 0 || $secedit_exit_status == 3)) {
			notify($ERRORS{'OK'}, 0, "ran secedit.exe to apply $inf_file_name");
		}
		elsif (defined($secedit_exit_status)) {
			notify($ERRORS{'WARNING'}, 0, "failed to run secedit.exe to apply $inf_target_path, exit status: $secedit_exit_status, output:\n" . join("\n", @$secedit_output));
			$error_occurred++;
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to run secedit.exe to apply $inf_target_path");
			$error_occurred++;
		}
	}
	
	if ($error_occurred) {
		return 0;
	}
	else {
		return 1;
	}
}

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

=head2 kill_process

 Parameters  : String containing task name pattern
 Returns     : If successful: true
               If failed: false
 Description : Runs taskkill.exe to kill processes with names matching a
					pattern. Wildcards can be specified using *, but task name
					patterns cannot begin with a *.
               
               Example pattern: notepad*

=cut

sub kill_process {
	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;	
	}
	
	# Get the task name pattern argument
	my $task_pattern = shift;
	unless ($task_pattern) {
		notify($ERRORS{'WARNING'}, 0, "task name pattern argument was not specified");
		return;	
	}
	
	my $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Typical output:
	# Task was killed, exit status = 0:
	# SUCCESS: The process with PID 3476 child of PID 5876 has been terminated.
	
	# No tasks match pattern, exit status = 0:
	# INFO: No tasks running with the specified criteria.
	
	# Bad search filter, exit status = 1:
	# ERROR: The search filter cannot be recognized.
	
	# Attempt to kill task
	my $taskkill_command = $system32_path . "/taskkill.exe /F /T /FI \"IMAGENAME eq $task_pattern\"";
	my ($taskkill_exit_status, $taskkill_output) = run_ssh_command($computer_node_name, $management_node_keys, $taskkill_command, '', '', '1');
	if (defined($taskkill_exit_status) && $taskkill_exit_status == 0 && (my @killed = grep(/SUCCESS/, @$taskkill_output))) {
		notify($ERRORS{'OK'}, 0, scalar @killed . "processe(s) killed matching pattern: $task_pattern\n" . join("\n", @killed));
	}
	elsif (defined($taskkill_exit_status) && $taskkill_exit_status == 0 && grep(/No tasks running/i, @{$taskkill_output})) {
		notify($ERRORS{'DEBUG'}, 0, "process does not exist matching pattern: $task_pattern");
	}
	elsif (defined($taskkill_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "unable to kill process matching pattern: $task_pattern\n" . join("\n", @{$taskkill_output}));
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to kill process matching pattern: $task_pattern");
		return;
	}
	
	return 1;
}

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

=head2 disable_ie_configuration_page

 Parameters  : None.
 Returns     : If successful: true
               If failed: false
 Description : Sets registry keys which prevent Internet Explorer's
					configuration page from appearing the first time a user launches
					it. This subroutine also enables the Internet Explorer Phishing
					Filter and sets it to not display a balloon message.

=cut

sub disable_ie_configuration_page {
	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 $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();

	my $registry_string .= <<"EOF";
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Internet Explorer\\Main]
"DisableFirstRunCustomize"=dword:00000001
"RunOnceHasShown"=dword:00000001
"RunOnceComplete"=dword:00000001

[HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Internet Explorer\\PhishingFilter]
"Enabled"=dword:00000002
"ShownVerifyBalloon"=dword:00000001

[HKEY_LOCAL_MACHINE\\Software\\Policies\\Microsoft\\Internet Explorer\\Main]
"DisableFirstRunCustomize"=dword:00000001

EOF

	# Import the string into the registry
	if ($self->import_registry_string($registry_string)) {
		notify($ERRORS{'OK'}, 0, "set the registry keys to disable IE runonce");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to set the registry key to disable IE runonce");
		return 0;
	}

	return 1;
}

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

=head2 enable_rdp_audio

 Parameters  : None.
 Returns     : If successful: true
               If failed: false
 Description : Sets the registry keys to allow audio redirection via RDP
               sessions. This is disabled by default under Windows Server 2008
               and possibly other versions of Windows. Also sets the Windows
               Audio service to start automatically.

=cut

sub enable_rdp_audio {
	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 $registry_string .= <<"EOF";
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp]
"fDisableCam"=dword:00000000

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\Terminal Services]
"fDisableCam"=dword:00000000

EOF

	# Import the string into the registry
	if ($self->import_registry_string($registry_string)) {
		notify($ERRORS{'OK'}, 0, "set the registry keys to enable RDP audio");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to set the registry key to enable RDP audio");
		return 0;
	}
	
	# Configure the Windows Audio service to start automatically
	if ($self->set_service_startup_mode('AudioSrv', 'auto')) {
		notify($ERRORS{'DEBUG'}, 0, "set the Windows Audio service startup mode to auto");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to set the Windows Audio service startup mode to auto");
		return 0;
	}

	return 1;
}

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

=head2 get_node_configuration_directory

 Parameters  : None.
 Returns     : String containing filesystem path
 Description : Retrieves the $NODE_CONFIGURATION_DIRECTORY variable value 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;
}

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

=head2 set_computer_name

 Parameters  : $computer_name (optional)
 Returns     : If successful: true
               If failed: false
 Description : Sets the registry keys to set the computer name. This subroutine
               does not attempt to reboot the computer.
               The computer name argument is optional. If not supplied, the
               computer's short name stored in the database will be used,
               followed by a hyphen and the image ID that is loaded.

=cut

sub set_computer_name {
	my $self = shift;
	if (ref($self) !~ /windows/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the computer name
	my $new_computer_name = shift;
	if (!$new_computer_name) {
		$new_computer_name = $self->data->get_computer_short_name();
		if (!$new_computer_name) {
			notify($ERRORS{'WARNING'}, 0, "computer name argument was not supplied and could not be retrieved from the reservation data");
			return;
		}
		
		# Append the image ID to the computer name
		my $image_id = $self->data->get_image_id();
		$new_computer_name .= "-$image_id" if $image_id;
	}
	
	my $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();
	
	my $registry_string .= <<"EOF";
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\ComputerName\\ComputerName]
"ComputerName"="$new_computer_name"

[HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters]
"Hostname"="$new_computer_name"
"NV Hostname"="$new_computer_name"
EOF
	
	# Import the string into the registry
	if ($self->import_registry_string($registry_string)) {
		notify($ERRORS{'DEBUG'}, 0, "set registry keys to change the computer name of $computer_node_name to $new_computer_name");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to set registry keys to change the computer name of $computer_node_name to $new_computer_name");
		return;
	}

}

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

=head2 disable_security_center_notifications

 Parameters  : None.
 Returns     : If successful: true
               If failed: false
 Description : Disables Windows Security Center notifications which are
               displayed in the notification area (system tray).

=cut

sub disable_security_center_notifications {
	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 $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();

	my $registry_string .= <<'EOF';
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Security Center]
"AntiSpywareDisableNotify"=dword:00000001
"AntiVirusDisableNotify"=dword:00000001
"FirewallDisableNotify"=dword:00000001
"UacDisableNotify"=dword:00000001
"UpdatesDisableNotify"=dword:00000001
"FirstRunDisabled"=dword:00000001

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Security Center\Svc]
"AntiVirusOverride"=dword:00000001
"AntiSpywareOverride"=dword:00000001
"FirewallOverride"=dword:00000001
EOF

	# Import the string into the registry
	if ($self->import_registry_string($registry_string)) {
		notify($ERRORS{'OK'}, 0, "set the registry keys to disable security center notifications");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to set the registry key to disable security center notifications");
		return 0;
	}

	return 1;
}

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

=head2 disable_automatic_updates

 Parameters  : None
 Returns     : If successful: true
               If failed: false
 Description : Disables Windows Automatic Updates by configuring a local group
               policy:
               HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU\NoAutoUpdate=1
               
               This must be done using a policy in order to prevent
               Windows Security Center will display a warning icon in the
               notification area. Windows Update can be disabled via the GUI
               which configures the following key but a warning will be
               presented to the user:
               HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update

=cut

sub disable_automatic_updates {
	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 $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();

	my $registry_string .= <<'EOF';
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU]
"NoAutoUpdate"=dword:00000001
EOF

	# Import the string into the registry
	if ($self->import_registry_string($registry_string)) {
		notify($ERRORS{'OK'}, 0, "disabled automatic updates");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to set the registry key to disable automatic updates");
		return 0;
	}

	return 1;
}

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

=head2 disable_windows_defender

 Parameters  : None
 Returns     : If successful: true
               If failed: false
 Description : Disables Windows Defender by doing the following:
               -Configures local group policy to disable Windows Defender
               -Removes HKLM...Run registry key to start Windows Defender at logon
               -Stops the Windows Defender service
               -Disables the Windows Defender service

=cut

sub disable_windows_defender {
	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 $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();

	my $registry_string .= <<'EOF';
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows Defender]
"DisableAntiSpyware"=dword:00000001

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run]
"Windows Defender"=-
EOF

	# Import the string into the registry
	if ($self->import_registry_string($registry_string)) {
		notify($ERRORS{'DEBUG'}, 0, "set the registry keys to disable Windows defender");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to set the registry key to disable Windows defender");
		return 0;
	}
	
	# Check if WinDefend service exists
	if ($self->service_exists('WinDefend')) {	
		# Stop the Windows Defender service
		if ($self->stop_service('WinDefend')) {
			notify($ERRORS{'DEBUG'}, 0, "stopped the Windows Defender service");
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to stop the Windows Defender service");
			return 0;
		}
		
		# Disable the Windows Defender service
		if ($self->set_service_startup_mode('WinDefend', 'disabled')) {
			notify($ERRORS{'DEBUG'}, 0, "disabled the Windows Defender service");
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to disable the Windows Defender service");
			return 0;
		}
	}
	
	notify($ERRORS{'OK'}, 0, "disabled Windows Defender");

	return 1;
}

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

=head2 registry_query_value

 Parameters  : $key_name (required), $value_name (optional)
 Returns     : If successful: true
               If failed: false
 Description : Queries the registry. If a value name is specified as the 2nd
               argument, the value is returned. If a value name is not
               specified, the output from reg.exe /s is returned containing the
               subkeys and values.

=cut

sub registry_query_value {
	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 $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Get and check the arguments
	my $key_name = shift;
	my $value_name = shift;
	if (!$key_name) {
		notify($ERRORS{'WARNING'}, 0, "registry key name argument was not specified");
		return;	
	}
	
	# Assemble the query command string
	my $reg_query_command = $system32_path . "/reg.exe QUERY \"$key_name\"";
	
	# Check if the value name argument was specified
	my $query_mode;
	if ($value_name && $value_name eq '(Default)') {
		# Value name argument is (Default), query default value using /ve switch
		$reg_query_command .= " /ve";
		$query_mode = 'default';
	}
	elsif ($value_name) {
		# Value name argument was specified, query it using /v switch
		$reg_query_command .= " /v \"$value_name\"";
		$query_mode = 'value';
	}
	else {
		# Value name argument was not specified, query all subkeys and values
		$reg_query_command .= " /s";
		$query_mode = 'subkeys';
	}
	
	# Attempt to query the registry key
	my ($reg_query_exit_status, $reg_query_output) = run_ssh_command($computer_node_name, $management_node_keys, $reg_query_command, '', '', 1);
	if (defined($reg_query_output) && grep(/unable to find the specified registry/i, @$reg_query_output)) {
		notify($ERRORS{'OK'}, 0, "registry key or value does not exist");
		return;
	}
	if (defined($reg_query_exit_status) && $reg_query_exit_status == 0) {
		notify($ERRORS{'DEBUG'}, 0, "queried registry key, output:\n" . join("\n", @{$reg_query_output}));
	}
	elsif (defined($reg_query_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to query registry key, exit status: $reg_query_exit_status, output:\n@{$reg_query_output}");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to query registry key");
		return;
	}
	
	# Check to see if the output appears normal	
	if (@{$reg_query_output}[0] !~ /reg\.exe version/i) {
		notify($ERRORS{'WARNING'}, 0, "unexpected output, 1st line doesn't contain REG.EXE VERSION:\n" . join("\n", @{$reg_query_output}));
	}
	
	# Check what was asked for, if subkeys, return entire query output string joined with newlines
	if ($query_mode eq 'subkeys') {
		return join("\n", @{$reg_query_output});
	}
	
	# Find the array element containing the line with the value
	my ($value_line) =  grep(/($value_name|no name)/i, @{$reg_query_output});
	notify($ERRORS{'DEBUG'}, 0, "value output line: $value_line");
	
	# Split the line up and return the value
	my ($retrieved_key_name, $type, $retrieved_value);
	if ($query_mode eq 'value') {
		($retrieved_key_name, $type, $retrieved_value) = $value_line =~ /\s*([^\s]+)\s+([^\s]+)\s+([^\s]+)/;
	}
	else {
		($retrieved_key_name, $type, $retrieved_value) = $value_line =~ /\s*(<NO NAME>)\s+([^\s]+)\s+([^\s]+)/;
	}
	
	return $retrieved_value;
}

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

=head2 set_static_public_address

 Parameters  : 
 Returns     : If successful: true
               If failed: false
 Description : Sets a static IP address for the public interface. The IP address
               stored in the database for the computer is used. The subnet
					mask, default gateway, and DNS servers configured for the
					management node are used.
					
					A persistent route is added to the routing table to the public
					default gateway.

=cut

sub set_static_public_address {
	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 $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $computer_name = $self->data->get_computer_short_name();
	
	# Make sure public IP configuration is static
	my $ip_configuration = $self->data->get_management_node_public_ip_configuration();
	if ($ip_configuration !~ /static/i) {
		notify($ERRORS{'WARNING'}, 0, "static public address can only be set if IP configuration is static, current value: $ip_configuration");
		return;
	}

	# Get the IP configuration
	my $interface_name = $self->get_public_interface_name() || '<undefined>';
	my $ip_address = $self->data->get_computer_ip_address() || '<undefined>';
	my $subnet_mask = $self->data->get_management_node_public_subnet_mask() || '<undefined>';
	my $default_gateway = $self->data->get_management_node_public_default_gateway() || '<undefined>';
	my @dns_servers = $self->data->get_management_node_public_dns_servers();
	
	# Assemble a string containing the static IP configuration
	my $configuration_info_string = <<EOF;
public interface name: $interface_name
public IP address: $ip_address
public subnet mask: $subnet_mask
public default gateway: $default_gateway
public DNS server(s): @dns_servers
EOF
	
	# Make sure required info was retrieved
	if ("$interface_name $ip_address $subnet_mask $default_gateway" =~ /undefined/) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve required network configuration for $computer_name:\n$configuration_info_string");
		return;
	}
	else {
		notify($ERRORS{'OK'}, 0, "attempting to set static public IP address on $computer_name:\n$configuration_info_string");
	}
	
	# Set the static public IP address
	my $address_command = "$system32_path/netsh.exe interface ip set address name=\"$interface_name\" source=static addr=$ip_address mask=$subnet_mask gateway=$default_gateway gwmetric=0";
	
	# Set number of attempts to try netsh.exe commands
	my $max_attempts = 3;
	my $address_attempts = 0;
	while ($address_attempts < $max_attempts) {
		$address_attempts++;
		my ($address_exit_status, $address_output) = run_ssh_command($computer_node_name, $management_node_keys, $address_command);
		if (defined($address_exit_status) && $address_exit_status == 0) {
			notify($ERRORS{'DEBUG'}, 0, "set static public IP address to $ip_address");
			last;
		}
		elsif (defined($address_exit_status)) {
			notify($ERRORS{'WARNING'}, 0, "attempt $address_attempts/$max_attempts: failed to set static public IP address to $ip_address, exit status: $address_exit_status, output:\n@{$address_output}");
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "attempt $address_attempts/$max_attempts: failed to run ssh command to set static public IP address to $ip_address");
		}
		
		# Check if max attempts has been reached.
		if ($address_attempts >= $max_attempts) {
			notify($ERRORS{'WARNING'}, 0, "failed to set static public IP address after making $address_attempts attempts");
			return 0;
		}
		
		sleep 2;
	}
	
	my $primary_dns_server_address = shift @dns_servers;
	notify($ERRORS{'DEBUG'}, 0, "primary DNS server address: $primary_dns_server_address\nalternate DNS server address(s):\n" . (join("\n", @dns_servers) || '<none>'));
	
	# Set the static DNS server address
	my $dns_command = "$system32_path/netsh.exe interface ip set dns name=\"$interface_name\" source=static addr=$primary_dns_server_address register=none";
	my ($dns_exit_status, $dns_output) = run_ssh_command($computer_node_name, $management_node_keys, $dns_command);
	if (!defined($dns_output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to set static primary DNS server address to $primary_dns_server_address");
		return;
	}
	elsif ($dns_exit_status) {
		notify($ERRORS{'WARNING'}, 0, "failed to set static primary DNS server address to $primary_dns_server_address, exit status: $dns_exit_status, output:\n@{$dns_output}");
		return 0;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "set static primary DNS server address to $primary_dns_server_address");
	}
	
	
	# We are only going to set up alternate dns server		
	for my $alternate_dns_server_address (@dns_servers) {
		my $alternate_dns_command = "$system32_path/netsh.exe interface ip add dns name=\"$interface_name\" addr=$alternate_dns_server_address";
		my ($alternate_dns_exit_status, $alternate_dns_output) = run_ssh_command($computer_node_name, $management_node_keys, $dns_command);
		if (!defined($alternate_dns_output)) {
			notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to set static alternate DNS server address to $alternate_dns_server_address");
		}
		elsif ($alternate_dns_exit_status) {
			notify($ERRORS{'WARNING'}, 0, "failed to set static alternate DNS server address to $alternate_dns_server_address, exit status: $alternate_dns_exit_status, output:\n" . join("\n", @$alternate_dns_output));
		}
		else {
			notify($ERRORS{'DEBUG'}, 0, "set static alternate DNS server address to $alternate_dns_server_address");
		}
	}
	
	# Add persistent static public default route
	if (!$self->set_public_default_route()) {
		notify($ERRORS{'WARNING'}, 0, "failed to add persistent static public default route");
		return;
	}
	
	notify($ERRORS{'OK'}, 0, "configured static address for public interface '$interface_name'");
	return 1;
}

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

=head2 delete_default_routes

 Parameters  : 
 Returns     : If successful: true
               If failed: false
 Description : Deletes all default (0.0.0.0) routes.

=cut

sub delete_default_routes {
	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 $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();
	
	# Delete all default routes
	my $route_delete_command = "route delete 0.0.0.0";
	my ($route_delete_exit_status, $route_delete_output) = run_ssh_command($computer_node_name, $management_node_keys, $route_delete_command);
	if (defined($route_delete_exit_status) && $route_delete_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "deleted all default routes");
	}
	elsif (defined($route_delete_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to delete all default routes, exit status: $route_delete_exit_status, output:\n@{$route_delete_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to delete all default routes");
		return;
	}
	
	return 1;
}

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

=head2 set_public_default_route

 Parameters  : None
 Returns     : If successful: true
               If failed: false
 Description : Adds a persistent route to the default gateway for the public
               network.

=cut

sub set_public_default_route {
	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;	
	}
	
	# Check the management node's DHCP IP configuration mode
	# Get the default gateway address
	my $default_gateway;
	my $ip_configuration = $self->data->get_management_node_public_ip_configuration();
	if ($ip_configuration && $ip_configuration =~ /static/i) {
		# Static addresses used, get default gateway address configured for management node
		$default_gateway = $self->data->get_management_node_public_default_gateway();
	}
	else {
		# Dynamic addresses used, get default gateway address assigned to computer
		$default_gateway = $self->get_public_default_gateway();
		if (!$default_gateway) {
			$default_gateway = $self->data->get_management_node_public_default_gateway();
		}
	}
	
	# Make sure default gateway was retrieved
	if (!$default_gateway) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve default gateway address");
		return;
	}
	
	# Delete all default routes before adding
	# Do this only after successfully retrieving default gateway address
	if (!$self->delete_default_routes()) {
		notify($ERRORS{'WARNING'}, 0, "unable to delete existing default routes");
		return;	
	}
	
	my $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();
	
	# Add a persistent route to the public default gateway
	my $route_add_command    = "route -p ADD 0.0.0.0 MASK 0.0.0.0 $default_gateway METRIC 1";
	my ($route_add_exit_status, $route_add_output) = run_ssh_command($computer_node_name, $management_node_keys, $route_add_command);
	if (defined($route_add_exit_status) && $route_add_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "added persistent route to public default gateway: $default_gateway");
	}
	elsif (defined($route_add_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to add persistent route to public default gateway: $default_gateway, exit status: $route_add_exit_status, output:\n@{$route_add_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to add persistent route to public default gateway: $default_gateway");
		return;
	}
	
	return 1;
}

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

=head2 get_volume_list

 Parameters  : None
 Returns     : If successful: array containing volume drive letters
               If failed: false
 Description : 

=cut

sub get_volume_list {
	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 $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();

	# Echo the diskpart script to a temp file on the node
	my $for_command = 'for i in `ls /cygdrive 2>/dev/null`; do echo $i; done;';
	my ($for_exit_status, $for_output) = run_ssh_command($computer_node_name, $management_node_keys, $for_command, '', '', 1);
	if (defined($for_exit_status) && $for_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "retrieved drive letter list under /cygdrive:\n" . join("\n", @$for_output));
	}
	elsif ($for_exit_status) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve drive letter list under /cygdrive, exit status: $for_exit_status, output:\n@{$for_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to retrieve drive letter list under /cygdrive");
		return;
	}
	
	my @drive_letters;
	for my $for_output_line (@$for_output) {
		if ($for_output_line =~ /^[a-z]$/) {
			push @drive_letters, $for_output_line;
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "unexpected output from for command: $for_output_line");
		}
	}
	
	return @drive_letters;
}

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

=head2 configure_time_synchronization

 Parameters  : None
 Returns     : If successful: true
               If failed: false
 Description : Configures the Windows Time service and synchronizes the time.

=cut

sub configure_time_synchronization {
	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 $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();
	my $system32_path        = $self->get_system32_path() || return;
	
	my $time_source = "time.nist.gov time-a.nist.gov time-b.nist.gov time.windows.com";
	
	# Assemble the time command
	my $time_command;
	
	# Kill d4.exe if it's running, this will prevent Windows built-in time synchronization from working
	$time_command .= "$system32_path/taskkill.exe /IM d4.exe /F 2>/dev/null ; ";
	
	# Register the w32time service
	$time_command .= "$system32_path/w32tm.exe /register ; ";
	
	# Start the service and configure it
	$time_command .= "$system32_path/net.exe start w32time 2>/dev/null ; ";
	$time_command .= "$system32_path/w32tm.exe /config /manualpeerlist:\"$time_source\" /syncfromflags:manual /update ; ";
	$time_command .= "$system32_path/net.exe stop w32time && $system32_path/net.exe start w32time ; ";
	
	# Synchronize the time
	$time_command .= "$system32_path/w32tm.exe /resync /nowait";
	
	# Run the assembled command
	my ($time_exit_status, $time_output) = run_ssh_command($computer_node_name, $management_node_keys, $time_command);
	if (defined($time_output)  && @$time_output[-1] =~ /The command completed successfully/i) {
		notify($ERRORS{'DEBUG'}, 0, "configured and synchronized Windows time");
	}
	elsif (defined($time_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to configure configure and synchronize Windows time, exit status: $time_exit_status, output:\n@{$time_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to configure and synchronize Windows time");
		return;
	}
	
	# Set the w32time service startup mode to auto
	if ($self->set_service_startup_mode('w32time', 'auto')) {
		notify($ERRORS{'DEBUG'}, 0, "set w32time service startup mode to auto");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to set w32time service startup mode to auto");
		return;
	}
	
	return 1;
}

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

=head2 is_64_bit

 Parameters  : None
 Returns     : If 64-bit: true
               If 32-bit: false
 Description : Determines if Windows OS is 64 or 32-bit.

=cut

sub is_64_bit {
	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;	
	}
	
	# Check if architecture has previously been determined
	if (defined($self->{OS_ARCHITECTURE}) && $self->{OS_ARCHITECTURE} eq '64') {
		notify($ERRORS{'DEBUG'}, 0, '64-bit Windows OS previously detected');
		return 1;
	}
	elsif (defined($self->{OS_ARCHITECTURE}) && $self->{OS_ARCHITECTURE} eq '32') {
		notify($ERRORS{'DEBUG'}, 0, '32-bit Windows OS previously detected');
		return 0;
	}
	
	my $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();
	
	my $registry_key = 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment';
	my $registry_value = 'PROCESSOR_IDENTIFIER';
	
	# Run reg.exe QUERY
	my $query_registry_command .= "reg.exe QUERY \"$registry_key\" /v \"$registry_value\"";
	my ($query_registry_exit_status, $query_registry_output) = run_ssh_command($computer_node_name, $management_node_keys, $query_registry_command, '', '', 0);
	
	if (!defined($query_registry_output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to query registry key: $registry_key, value: $registry_value");
		return;
	}
	
	my ($output_line) = grep(/^\s*$registry_value/i, @$query_registry_output);
	if (!$output_line) {
		notify($ERRORS{'WARNING'}, 0, "unable to find registry value line in reg.exe output:\n" . join("\n", @$query_registry_output));
		return;
	}
	
	my ($registry_data) = $output_line =~ /\s*$registry_value\s+[\w_]+\s+(.*)/;
	
	if ($registry_data && $registry_data =~ /64/) {
		$self->{OS_ARCHITECTURE} = 64;
		notify($ERRORS{'DEBUG'}, 0, "64-bit Windows OS detected, PROCESSOR_IDENTIFIER: $registry_data");
		return 1;
	}
	elsif ($registry_value) {
		$self->{OS_ARCHITECTURE} = 32;
		notify($ERRORS{'DEBUG'}, 0, "32-bit Windows OS detected, PROCESSOR_IDENTIFIER: $registry_data");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to determine if OS is 32 or 64-bit, failed to query PROCESSOR_IDENTIFIER registry key, reg.exe output:\n" . join("\n", @$query_registry_output));
		return;
	}
}

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

=head2 get_system32

 Parameters  : None
 Returns     : If 64-bit: true
               If 32-bit: false
 Description : 

=cut

sub get_system32_path {
	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;
	}
	
	# Check if architecture has previously been determined
	return $self->{SYSTEM32_PATH} if $self->{SYSTEM32_PATH};
	
	my $computer_name = $self->data->get_computer_short_name();
	
	# Make sure SSH is responding
	if (!$self->is_ssh_responding()) {
		notify($ERRORS{'WARNING'}, 0, "unable to determine System32 path to use for $computer_name, computer is not responding to SSH");
		return;	
	}
	
	if ($self->is_64_bit()) {
		$self->{SYSTEM32_PATH} = 'C:/Windows/Sysnative';
		notify($ERRORS{'DEBUG'}, 0, "64-bit Windows OS installed on $computer_name, using $self->{SYSTEM32_PATH}");
	}
	else {
		$self->{SYSTEM32_PATH} = 'C:/Windows/System32';
		notify($ERRORS{'DEBUG'}, 0, "32-bit Windows OS installed on $computer_name, using $self->{SYSTEM32_PATH}");
	}
	
	return $self->{SYSTEM32_PATH};
}

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

=head2 get_product_name

 Parameters  : None
 Returns     : If successful: string containing Windows product name
               If failed: false
 Description : Retrieves the Windows product name from the registry. This is
               stored at:
               HKLM\Software\Microsoft\Windows NT\CurrentVersion\ProductName
               
               The product name stored in the registry is used in the
               winProductKey table to match a product key up with a product. It
               must match exactly. Known strings for some versions of Windows:
               "Microsoft Windows XP"
               "Microsoft Windows Server 2003"
               "Windows Server (R) 2008 Datacenter"
               "Windows Vista (TM) Enterprise"

=cut

sub get_product_name {
	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;	
	}
	
	# Check if product name has previously been retrieved from registry
	if ($self->{PRODUCT_NAME}) {
		notify($ERRORS{'DEBUG'}, 0, "Windows product name previously retrieved: $self->{PRODUCT_NAME}");
		return $self->{PRODUCT_NAME};
	}
	
	my $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();
	
	# Get the Windows product name from the registry
	my $product_name = $self->reg_query('HKLM/Software/Microsoft/Windows NT/CurrentVersion', 'ProductName');
	if ($product_name) {
		notify($ERRORS{'DEBUG'}, 0, "retrieved Windows product name: $product_name");
		$self->{PRODUCT_NAME} = $product_name;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve Windows product name from registry");
		return;
	}
	
	return $self->{PRODUCT_NAME};
}

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

=head2 format_path_unix

 Parameters  : path
 Returns     : If successful: path formatted for Unix
               If failed: false
 Description : 

=cut

sub format_path_unix {
	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;	
	}
	
	# Get the path argument
	my $path = shift;
	if (!$path) {
		notify($ERRORS{'WARNING'}, 0, "path argument was not specified");
		return;	
	}
	
	# Replace all forward slashes and backslashes with a single forward slash
	$path =~ s/[\/\\]+/\//g;
	
	# Escape all spaces
	$path =~ s/ /\\ /g;
	
	# Change %VARIABLE% to $VARIABLE
	$path =~ s/\%(.+)\%/\$$1/g;
	
	#notify($ERRORS{'DEBUG'}, 0, "formatted path for Unix: $path");
	return $path;
}

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

=head2 format_path_dos

 Parameters  : path
 Returns     : If successful: path formatted for DOS
               If failed: false
 Description : 

=cut

sub format_path_dos {
	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;	
	}
	
	# Get the path argument
	my $path = shift;
	if (!$path) {
		notify($ERRORS{'WARNING'}, 0, "path argument was not specified");
		return;	
	}
	
	# Replace all forward slashes with 2 backslashes
	$path =~ s/[\/\\]/\\\\/g;
	
	# Change $VARIABLE to %VARIABLE%
	$path =~ s/\$([^\\]+)/\%$1\%/g;
	
	#notify($ERRORS{'DEBUG'}, 0, "formatted path for DOS: $path");
	return $path;
}

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

=head2 disable_system_restore

 Parameters  : None
 Returns     : If successful: true
               If failed: false
 Description : Sets registry key to disable Windows System Restore. Disabling
               System Restore helps reduce the image size.

=cut

sub disable_system_restore {
	my $self = shift;
	if (ref($self) !~ /windows/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();

	my $registry_string .= <<"EOF";
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\SystemRestore]
"DisableConfig"=dword:00000001
"DisableSR"=dword:00000001
EOF

	# Import the string into the registry
	if ($self->import_registry_string($registry_string)) {
		notify($ERRORS{'OK'}, 0, "disabled system restore");
		return 1;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to disable system restore");
		return 0;
	}
}

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

=head2 user_logged_in

 Parameters  : 
 Returns     : 
 Description : 

=cut

sub user_logged_in {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	my $system32_path        = $self->get_system32_path() || return;

	# Attempt to get the username from the arguments
	# If no argument was supplied, use the user specified in the DataStructure
	my $username = shift;
	
	# Remove spaces from beginning and end of username argument
	# Fixes problem if string containing only spaces is passed
	$username =~ s/(^\s+|\s+$)//g if $username;
	
	# Check if username argument was passed
	if (!$username) {
		$username = $self->data->get_user_login_id();
	}
	notify($ERRORS{'DEBUG'}, 0, "checking if $username is logged in to $computer_node_name");

	# Run qwinsta.exe to display terminal session information
	my ($exit_status, $output) = run_ssh_command($computer_node_name, $management_node_keys, "$system32_path/qwinsta.exe");
	if ($exit_status > 0) {
		notify($ERRORS{'WARNING'}, 0, "failed to run qwinsta.exe on $computer_node_name, exit status: $exit_status, output:\n@{$output}");
		return;
	}
	elsif (!defined($exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run qwinsta.exe SSH command on $computer_node_name");
		return;
	}
	
	# Find lines in qwinsta.exe output indicating a logged in user, lines may look like this:
	# ' rdp-tcp#2         root                      2  Active  rdpwd'
	# '>console           root                      0  Active  wdcon'
	my @user_connection_lines = grep(/[\s>]+(\S+)\s+($username)\s+(\d+)\s+(Active)/, @{$output});
	if (@user_connection_lines) {
		notify($ERRORS{'OK'}, 0, "$username appears to be logged in on $computer_node_name:\n" . join("\n", @user_connection_lines));
		return 1;
	}
	else {
		notify($ERRORS{'OK'}, 0, "$username does NOT appear to be logged in on $computer_node_name");
		return 0;
	}
}

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

=head2 wait_for_logoff

 Parameters  : Username (optional), maximum number of minutes to wait (optional)
 Returns     : True if user is not logged in
               False if user is still logged in after waiting
 Description : Waits the specified amount of time for the user to log off. The
               default username is the reservation user and the default time to
               wait is 2 minutes.

=cut

sub wait_for_logoff {
	my $self = shift;
	if (ref($self) !~ /windows/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;
	
	# Remove spaces from beginning and end of username argument
	# Fixes problem if string containing only spaces is passed
	$username =~ s/(^\s+|\s+$)//g if $username;
	
	# Check if username argument was passed
	if (!$username) {
		$username = $self->data->get_user_login_id();
	}

	# Attempt to get the total number of minutes to wait from the arguments
	my $total_wait_minutes = shift;
	if (!defined($total_wait_minutes) || $total_wait_minutes !~ /^\d+$/) {
		$total_wait_minutes = 2;
	}

	# Looping configuration variables
	# Seconds to wait in between loop attempts
	my $attempt_delay = 5;
	# Total loop attempts made
	# Add 1 to the number of attempts because if you're waiting for x intervals, you check x+1 times including at 0
	my $attempts = ($total_wait_minutes * 12) + 1;

	notify($ERRORS{'DEBUG'}, 0, "waiting for $username to logoff, maximum of $total_wait_minutes minutes");

	# Loop until computer is user is not logged in
	for (my $attempt = 1; $attempt <= $attempts; $attempt++) {
		if ($attempt > 1) {
			notify($ERRORS{'OK'}, 0, "attempt " . ($attempt - 1) . "/" . ($attempts - 1) . ": $username is logged in, sleeping for $attempt_delay seconds");
			sleep $attempt_delay;
		}

		if (!$self->user_logged_in($username)) {
			notify($ERRORS{'OK'}, 0, "$username is NOT logged in to $computer_node_name, returning 1");
			return 1;
		}
	}

	# Calculate how long this waited
	my $total_wait = ($attempts * $attempt_delay);
	notify($ERRORS{'WARNING'}, 0, "$username is still logged in to $computer_node_name after waiting for $total_wait seconds");
	return 0;
}

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

=head2 get_product_key

 Parameters  : $affiliation_identifier (optional), $product_name (optional)
 Returns     : If successful: string containing product key
               If failed: false
 Description : Retrieves the Windows product key from the database. This is
               stored in the winProductKey table.
               
               Optional affiliation identifier and product name arguments may be
               passed. Either both arguments must be passed or none. The
               affiliation identifier may either be an affiliation ID or name.
               If passed, the only data returned will be the data matching that
               specific identifier. Global affiliation data will not be
               returned.
               
               If the affiliation identifier argument is not passed, the
               affiliation is determined by the affiliation of the owner of the
               image for the reservation. If a product key has not been
               configured for that specific affiliation, the product key
               configured for the Global affiliation is returned.

=cut

sub get_product_key {
	my $self = shift;
	if (ref($self) !~ /windows/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Remember if this sub was called with arguments
	# Used to determine whether or not Global activation data will be checked
	# If affiliation ID argument is specified, assume caller only wants the data for that affiliation and not the Global data
	my $include_global;
	if (scalar(@_) == 2) {
		$include_global = 0;
		notify($ERRORS{'DEBUG'}, 0, "subroutine was called with arguments, global affiliation data will be ignored");
	}
	elsif (scalar(@_) == 0) {
		$include_global = 1;
		notify($ERRORS{'DEBUG'}, 0, "subroutine was NOT called with arguments, global affiliation data will be included");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "subroutine argument count = " . scalar(@_) . ", it must only be called with 0 or 2 arguments");
		return;
	}
	
	# Get the affiliation identifer, may be ID or name
	my $affiliation_identifier = shift;
	if (!defined($affiliation_identifier)) {
		$affiliation_identifier = $self->data->get_image_affiliation_id();
	}
	if (!defined($affiliation_identifier)) {
		notify($ERRORS{'WARNING'}, 0, "affiliation identifier argument was not passed and could not be determined from image");
		return;
	}
	
	# Get the product name from the registry on the computer
	my $product_name = shift || $self->get_product_name();
	if (!$product_name) {
		notify($ERRORS{'WARNING'}, 0, "product name argument was not passed and could not be determined from computer");
		return;
	}
	
	# Normalize the product name string from the registry
	# Remove Microsoft from the beginning - some products have this and some don't
	$product_name =~ s/Microsoft//ig;
	# Remove anything in parenthesis such as (R) or (TM)
	$product_name =~ s/\(.*\)//ig;
	# Replace spaces with %
	$product_name =~ s/\s/%/ig;
	# Add % to the beginning and end
	$product_name = "%$product_name%";
	# Replace multiple % characters with a single %
	$product_name =~ s/%+/%/ig;
	
	# Create the affiliation-specific select statement
	# Check if the affiliation identifier is a number or word
	# If a number, use affiliation.id directly
	# If a word, reference affiliation.name
	my $affiliation_select_statement;
	if ($affiliation_identifier =~ /^\d+$/) {
		notify($ERRORS{'DEBUG'}, 0, "affiliation identifier is a number, retrieving winProductKey.affiliationid=$affiliation_identifier");
		$affiliation_select_statement = <<EOF;
SELECT
winProductKey.*
FROM
winProductKey
WHERE
winProductKey.productname LIKE '$product_name'
AND winProductKey.affiliationid = $affiliation_identifier
EOF
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "affiliation identifier is NOT a number, retrieving affiliation.name=$affiliation_identifier");
		$affiliation_select_statement = <<EOF;
SELECT
winProductKey.*
FROM
winProductKey,
affiliation
WHERE
winProductKey.productname LIKE '$product_name'
AND winProductKey.affiliationid = affiliation.id
AND affiliation.name LIKE '$affiliation_identifier'
EOF
	}
	
	# Create the select statement
	my $global_select_statement = <<EOF;
SELECT
winProductKey.*
FROM
winProductKey,
affiliation
WHERE
winProductKey.productname LIKE '$product_name'
AND winProductKey.affiliationid = affiliation.id
AND affiliation.name LIKE 'Global'
EOF
	
	# Call the database select subroutine
	my @affiliation_rows = database_select($affiliation_select_statement);
	
	# Get the rows for the Global affiliation if this subroutine wasn't called with arguments
	my @global_rows = ();
	if ($include_global) {
		@global_rows = database_select($global_select_statement);
	}
	
	# Create an array containing the combined rows
	my @combined_rows = (@affiliation_rows, @global_rows);

	# Check to make sure rows were returned
	if (!@combined_rows) {
		notify($ERRORS{'WARNING'}, 0, "0 rows were retrieved from winProductKey table for affiliation=$affiliation_identifier, product=$product_name");
		return;
	}
	notify($ERRORS{'DEBUG'}, 0, "retrieved rows from winProductKey table for affiliation=$affiliation_identifier, product=$product_name:\n" . format_data(\@combined_rows));
	
	my $product_key = $combined_rows[0]->{productkey};
	notify($ERRORS{'DEBUG'}, 0, "returning product key: $product_key");
	
	return $product_key;
}

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

=head2 get_product_key_info

 Parameters  : none
 Returns     : hash reference
 Description : Returns the contents of the winProductKey table as a hash
               reference. The hash keys are the affiliation IDs.

=cut

sub get_product_key_info {
	my $self = shift;
	if (ref($self) !~ /windows/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Create the select statement
	my $select_statement = <<EOF;
SELECT
*
FROM
winProductKey
EOF
	
	# Call the database select subroutine
	my @selected_rows = database_select($select_statement);
	
	# Transform the array of database rows into a hash
	my %product_key_info;
	map { $product_key_info{$_->{affiliationid}}{$_->{productname}} = $_->{productkey} } @selected_rows;
	
	notify($ERRORS{'DEBUG'}, 0, "retrieved product key info:\n" . format_data(\%product_key_info));
	return \%product_key_info;
}

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

=head2 get_kms_server_info

 Parameters  : none
 Returns     : hash reference
 Description : Returns the contents of the winKMS table as a hash
               reference. The hash keys are the affiliation IDs.

=cut

sub get_kms_server_info {
	my $self = shift;
	if (ref($self) !~ /windows/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Create the select statement
	my $select_statement = <<EOF;
SELECT
*
FROM
winKMS
EOF
	
	# Call the database select subroutine
	my @selected_rows = database_select($select_statement);
	
	# Transform the array of database rows into a hash
	my %kms_server_info;
	map { $kms_server_info{$_->{affiliationid}}{$_->{address}} = $_->{port} } @selected_rows;
	
	notify($ERRORS{'DEBUG'}, 0, "retrieved KMS server info:\n" . format_data(\%kms_server_info));
	return \%kms_server_info;
}

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

=head2 set_product_key

 Parameters  : $affiliation_id, $product_name, $product_key
 Returns     : If successful: true
               If failed: false
 Description : Inserts or updates a row in the winProductKey table in the
               database.

=cut

sub set_product_key {
	my $self = shift;
	if (ref($self) !~ /windows/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get and check the arguments
	my ($affiliation_identifier, $product_name, $product_key) = @_;
	if (!defined($affiliation_identifier) || !defined($product_name) || !defined($product_key)) {
		notify($ERRORS{'WARNING'}, 0, "affiliation ID, product name, and product key arguments not passed correctly");
		return;
	}
	
	# Create the insert statement
	# Check if the affiliation identifier is a number or word
	# If a number, set affiliation.id directly
	# If a word, reference affiliation.name
	my $insert_statement;
	if ($affiliation_identifier =~ /^\d+$/) {
		notify($ERRORS{'DEBUG'}, 0, "affiliation identifier is a number, setting winProductKey.affiliationid=$affiliation_identifier");
		$insert_statement = <<"EOF";
INSERT INTO winProductKey
(
affiliationid,
productname,
productkey
)
VALUES
(
'$affiliation_identifier',
'$product_name',
'$product_key'
)
ON DUPLICATE KEY UPDATE
affiliationid=VALUES(affiliationid),
productname=VALUES(productname),
productkey=VALUES(productkey)
EOF
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "affiliation identifier is NOT a number, setting affiliation.name=$affiliation_identifier");
		$insert_statement = <<"EOF";
INSERT INTO winProductKey
(
affiliationid,
productname,
productkey
)
VALUES
(
(SELECT id FROM affiliation WHERE name='$affiliation_identifier'),
'$product_name',
'$product_key'
)
ON DUPLICATE KEY UPDATE
affiliationid=VALUES(affiliationid),
productname=VALUES(productname),
productkey=VALUES(productkey)
EOF
	}
	
	# Execute the insert statement, the return value should be the id of the row
	my $insert_result = database_execute($insert_statement);
	if (defined($insert_result)) {
		notify($ERRORS{'DEBUG'}, 0, "set product key in database:\naffiliation ID: $affiliation_identifier\nproduct name: $product_name\nproduct key: $product_key");
	
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to set product key in database:\naffiliation ID: $affiliation_identifier\nproduct name: $product_name\nproduct key: $product_key");
		return;
	}
	
	return 1;
}

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

=head2 delete_product_key

 Parameters  : $affiliation_id, $product_name
 Returns     : If successful: true
               If failed: false
 Description : Deletes a row from the winProductKey table in the database.

=cut

sub delete_product_key {
	my $self = shift;
	if (ref($self) !~ /windows/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get and check the arguments
	my ($affiliation_id, $product_name, $product_key) = @_;
	if (!defined($affiliation_id) || !defined($product_name) || !defined($product_key)) {
		notify($ERRORS{'WARNING'}, 0, "affiliation ID, product name, and product key arguments not passed correctly");
		return;
	}
	
	# Construct the delete statement
	my $delete_statement = <<"EOF";
DELETE FROM
winProductKey
WHERE
affiliationid = $affiliation_id
AND productname = '$product_name'
AND productkey = '$product_key'
EOF

	# Execute the delete statement
	my $delete_result = database_execute($delete_statement);
	if (defined($delete_result)) {
		notify($ERRORS{'DEBUG'}, 0, "deleted product key from database:\naffiliation ID: $affiliation_id\nproduct name: $product_name\nproduct key: $product_key, result: $delete_result");
	
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to delete product key from database:\naffiliation ID: $affiliation_id\nproduct name: $product_name\nproduct key: $product_key");
		return;
	}
	
	return 1;
}

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

=head2 delete_kms_server

 Parameters  : $affiliation_id, $address
 Returns     : If successful: true
               If failed: false
 Description : Deletes a row from the winKMS table in the database.

=cut

sub delete_kms_server {
	my $self = shift;
	if (ref($self) !~ /windows/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get and check the arguments
	my ($affiliation_id, $address) = @_;
	if (!defined($affiliation_id) || !defined($address)) {
		notify($ERRORS{'WARNING'}, 0, "affiliation ID and KMS server address arguments not passed correctly");
		return;
	}
	
	# Construct the delete statement
	my $delete_statement = <<"EOF";
DELETE FROM
winKMS
WHERE
affiliationid = $affiliation_id
AND address = '$address'
EOF

	# Execute the delete statement
	my $delete_result = database_execute($delete_statement);
	if (defined($delete_result)) {
		notify($ERRORS{'DEBUG'}, 0, "deleted KMS server from database:\naffiliation ID: $affiliation_id\naddress: $address, result: $delete_result");
	
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to delete product key from database:\naffiliation ID: $affiliation_id\naddress: $address");
		return;
	}
	
	return 1;
}

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

=head2 get_kms_servers

 Parameters  : $affiliation_identifier (optional)
 Returns     : If successful: reference to array of hashes
               If failed: false
 Description : Retrieves the KMS server data from the database. This is
               stored in the winKMS table.
               
               An optional affiliation identifier argument may be passed. This
               may either be an affiliation ID or name. If passed, the only data
               returned will be the data matching that specific identifier.
               Global affiliation data will not be returned.
               
               If the affiliation identifier argument is not passed, the
               affiliation is determined by the affiliation of the owner of the
               image for the reservation. If a KMS server has not been
               configured for that specific affiliation, the KMS server
               configured for the Global affiliation is returned.
               
               This subroutine returns an array reference. Each array element
               contains a hash reference representing a row in the winKMS table.
               
               Example of returned data:
               @{$kms_servers}[0] =
                  |--{address} = 'kms.affiliation.edu'
                  |--{affiliationid} = '1'
                  |--{port} = '1688'
               @{$kms_servers}[1] =
                  |--{address} = 'kms.global.edu'
                  |--{affiliationid} = '0'
                  |--{port} = '1688'
                  
               Example usage:
               my $kms_servers = $self->os->get_kms_servers();
               my $kms_address = @{$kms_servers[0]}->{address};

=cut

sub get_kms_servers {
	my $self = shift;
	if (ref($self) !~ /windows/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Remember if this sub was called with arguments
	# Used to determine whether or not global affiliation data will be checked
	my $include_global;
	if (scalar(@_) == 1) {
		$include_global = 0;
		notify($ERRORS{'DEBUG'}, 0, "subroutine was called with an affiliation argument, global affiliation data will be ignored");
	}
	elsif (scalar(@_) == 0) {
		$include_global = 1;
		notify($ERRORS{'DEBUG'}, 0, "subroutine was NOT called with arguments, global affiliation data will be included");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "subroutine argument count = " . scalar(@_) . ", it must only be called with 0 or 1 arguments");
		return;
	}
	
	# Get the image affiliation identifier, may be ID or name
	my $affiliation_identifier = shift;
	if (!defined($affiliation_identifier)) {
		$affiliation_identifier = $self->data->get_image_affiliation_id();
	}
	if (!defined($affiliation_identifier)) {
		notify($ERRORS{'WARNING'}, 0, "affiliation argument was not passed and could not be determined from image");
		return;
	}
	
	# Create the affiliation-specific select statement
	# Check if the affiliation identifier is a number or word
	# If a number, use affiliation.id directly
	# If a word, reference affiliation.name
	my $affiliation_select_statement;
	if ($affiliation_identifier =~ /^\d+$/) {
		notify($ERRORS{'DEBUG'}, 0, "affiliation identifier is a number, retrieving winKMS.affiliationid=$affiliation_identifier");
		$affiliation_select_statement = <<EOF;
SELECT
winKMS.*
FROM
winKMS
WHERE
winKMS.affiliationid = $affiliation_identifier
EOF
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "affiliation identifier is NOT a number, retrieving affiliation.name=$affiliation_identifier");
		$affiliation_select_statement .= <<EOF;
SELECT
winKMS.*
FROM
winKMS,
affiliation
WHERE
winKMS.affiliationid = affiliation.id
AND affiliation.name LIKE '$affiliation_identifier'
EOF
	}
	
	# Create the Global affiliation select statement
	my $global_select_statement .= <<EOF;
SELECT
winKMS.*
FROM
winKMS,
affiliation
WHERE
winKMS.affiliationid = affiliation.id
AND affiliation.name LIKE 'Global'
EOF

	# Call the database select subroutine
	my @affiliation_rows = database_select($affiliation_select_statement);
	
	# Get the rows for the Global affiliation if this subroutine wasn't called with arguments
	my @global_rows = ();
	if ($include_global) {
		@global_rows = database_select($global_select_statement);
	}
	
	# Create an array containing the combined rows
	my @combined_rows = (@affiliation_rows, @global_rows);

	# Check to make sure rows were returned
	if (!@combined_rows) {
		notify($ERRORS{'WARNING'}, 0, "0 rows were retrieved from winKMS table for affiliation=$affiliation_identifier");
		return;
	}
	
	notify($ERRORS{'DEBUG'}, 0, "returning row array from winKMS table for affiliation=$affiliation_identifier:\n" . format_data(\@combined_rows));
	return \@combined_rows;
}

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

=head2 set_kms_server

 Parameters  : $affiliation_id, $address, $port (optional)
 Returns     : If successful: true
               If failed: false
 Description : Inserts or updates a row in the winKMS table in the database.

=cut

sub set_kms_server {
	my $self = shift;
	if (ref($self) !~ /windows/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get and check the arguments
	my ($affiliation_identifier, $address, $port) = @_;
	if (!defined($affiliation_identifier) || !defined($address)) {
		notify($ERRORS{'WARNING'}, 0, "affiliation ID and KMS address arguments not passed correctly");
		return;
	}
	
	# Set the default port if argument wasn't passed
	if (!defined($port)) {
		$port = 1688;
	}
	
		# Create the insert statement
	# Check if the affiliation identifier is a number or word
	# If a number, set affiliation.id directly
	# If a word, reference affiliation.name
	my $insert_statement;
	if ($affiliation_identifier =~ /^\d+$/) {
		notify($ERRORS{'DEBUG'}, 0, "affiliation identifier is a number, setting winKMS.affiliationid=$affiliation_identifier");
		$insert_statement = <<"EOF";
INSERT INTO winKMS
(
affiliationid,
address,
port
)
VALUES
(
'$affiliation_identifier',
'$address',
'$port'
)
ON DUPLICATE KEY UPDATE
affiliationid=VALUES(affiliationid),
address=VALUES(address),
port=VALUES(port)
EOF
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "affiliation identifier is NOT a number, setting affiliation.name=$affiliation_identifier");
		$insert_statement = <<"EOF";
INSERT INTO winKMS
(
affiliationid,
address,
port
)
VALUES
(
(SELECT id FROM affiliation WHERE name='$affiliation_identifier'),
'$address',
'$port'
)
ON DUPLICATE KEY UPDATE
affiliationid=VALUES(affiliationid),
address=VALUES(address),
port=VALUES(port)
EOF
	}

	# Execute the insert statement, the return value should be the id of the row
	my $insert_result = database_execute($insert_statement);
	if (defined($insert_result)) {
		notify($ERRORS{'OK'}, 0, "set KMS address in database:\naffiliation ID: $affiliation_identifier\naddress: $address\nport: $port");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to set KMS address in database:\naffiliation ID: $affiliation_identifier\naddress: $address\nport: $port");
		return;
	}
	
	return 1;
}


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

=head2 get_driver_inf_paths

 Parameters  : Driver class (optional)
 Returns     : Array containing driver .inf paths
 Description : This subroutine searches the node configuration drivers directory
               on the computer for .inf files and returns an array containing
               the paths of the .inf files. The node configuration drivers
               directory is: C:\cygwin\home\root\VCL\Drivers
               
               An optional driver class argument can be supplied which will
               cause this subroutine to only return drivers matching the class
               specified. Each driver .inf file should have a Class= line which
               specified the type of device the driver is intended for. This
               argument can be a regular expression. For example, to search for
               all storage drivers, pass the following string to this
               subroutine:
               (scsiadapter|hdc)
               
               The driver paths are formatted with forward slashes.

=cut

sub get_driver_inf_paths {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	
	# Check if a driver class argument was specified
	my $driver_class = shift;
	if ($driver_class) {
		notify($ERRORS{'DEBUG'}, 0, "attempting to locate driver .inf paths matching class: $driver_class");
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "attempting to locate driver .inf paths matching any class");
	}
	
	my $drivers_directory = $self->get_node_configuration_directory() . '/Drivers';
	
	# Find the paths of .inf files in the drivers directory with a Class=SCSIAdapter or HDC line
	# These are the storage driver .inf files
	my @inf_paths = ();
	my $grep_command .= '/usr/bin/grep.exe -Eirl --include="*.[iI][nN][fF]" ';
	if ($driver_class) {
		$grep_command .= '"class[ ]*=[ ]*' . $driver_class . '" ';
	}
	else {
		$grep_command .= '".*" ';
	}
	$grep_command .= $drivers_directory;
	
	my ($grep_exit_status, $grep_output) = run_ssh_command($computer_node_name, $management_node_keys, $grep_command, '', '', 1);
	if (defined($grep_exit_status) && $grep_exit_status > 1) {
		notify($ERRORS{'WARNING'}, 0, "failed to find driver paths, exit status: $grep_exit_status, output:\n@{$grep_output}");
		return;
	}
	elsif (defined($grep_output)) {
		my @inf_paths = grep(/:[\\\/]/, @$grep_output);
		notify($ERRORS{'DEBUG'}, 0, "found " . scalar(@inf_paths) . " driver .inf paths, grep output:\n". join("\n", @$grep_output));
		return @inf_paths;
	}
	elsif (defined($grep_exit_status)) {
		notify($ERRORS{'WARNING'}, 0, "failed to find driver paths, exit status: $grep_exit_status, output:\n@{$grep_output}");
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to find driverpaths");
		return;
	}
}

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

=head2 set_device_path_key

 Parameters  : None
 Returns     : If successful: true
               If failed: false
 Description : Determines the paths to all of the driver .inf files copied to
               the computer and sets the following Windows registry key:
               HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\DevicePath
               
               This key contains paths to driver .inf files. Windows searches
               these files when attempting to load a device driver.

=cut

sub set_device_path_key {
	my $self = shift;
	if (ref($self) !~ /windows/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();
	
	my $device_path_value;
	
	# Find the paths of .inf files in the drivers directory
	my @inf_paths = $self->get_driver_inf_paths();
	if (!@inf_paths || $inf_paths[0] eq '0') {
		# No driver paths were found, just use the inf path
		$device_path_value = '%SystemRoot%\\inf';
		notify($ERRORS{'DEBUG'}, 0, "no driver .inf paths were found");
	}
	else {
		# Remove the .inf filenames from the paths
		map(s/\/[^\/]*$//, @inf_paths);
		
		# Remove duplicate paths, occurs if a directory has more than 1 .inf file
		my %inf_path_hash;
		my @inf_paths_unique = grep { !$inf_path_hash{$_}++ } @inf_paths;
		notify($ERRORS{'DEBUG'}, 0, "found " . scalar(@inf_paths_unique) . " unique driver .inf paths");
		
		# Assemble the device path value
		$device_path_value = '%SystemRoot%\\inf;' . join(";", @inf_paths_unique);
		
		# Replace forward slashes with backslashes
		$device_path_value =~ s/\//\\/g;
	}
	
	notify($ERRORS{'DEBUG'}, 0, "device path value: $device_path_value");
	
	# Attempt to set the DevicePath key
	my $registry_key = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion';
	if ($self->reg_add($registry_key, 'DevicePath', 'REG_EXPAND_SZ', $device_path_value)) {
		notify($ERRORS{'OK'}, 0, "set the DevicePath registry key");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to set the DevicePath registry key");
		return;
	}
	
	return 1;
}

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

=head2 disable_hibernation

 Parameters  : None
 Returns     : If successful: true
               If failed: false
 Description : Disables hibernation mode.

=cut

sub disable_hibernation {
	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 $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();
	my $system32_path        = $self->get_system32_path() || return;

	# Run powercfg.exe to disable hibernation
	my $powercfg_command = "$system32_path/powercfg.exe -HIBERNATE OFF";
	my ($powercfg_exit_status, $powercfg_output) = run_ssh_command($computer_node_name, $management_node_keys, $powercfg_command, '', '', 1);
	if (defined($powercfg_exit_status) && $powercfg_exit_status == 0) {
		notify($ERRORS{'OK'}, 0, "disabled hibernation");
	}
	elsif ($powercfg_exit_status) {
		notify($ERRORS{'WARNING'}, 0, "failed to disable hibernation, exit status: $powercfg_exit_status, output:\n" . join("\n", @$powercfg_output));
		return;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to disable hibernation");
		return;
	}
	
	# Delete hiberfil.sys
	if (!$self->delete_file('$SYSTEMDRIVE/hiberfil.sys')) {
		notify($ERRORS{'WARNING'}, 0, "failed to disable hibernation, hiberfil.sys could not be deleted");
		return;
	}
	
	return 1;
}

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

=head2 disable_ceip

 Parameters  : None
 Returns     : If successful: true
               If failed: false
 Description : Disables the Windows Customer Experience Improvement Program
               features.

=cut

sub disable_ceip {
	my $self = shift;
	if (ref($self) !~ /windows/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Attempt to set the CEIPEnable key
	my $registry_key_software = 'HKEY_LOCAL_MACHINE\\Software\\Microsoft\\SQMClient\\Windows';
	if ($self->reg_add($registry_key_software, 'CEIPEnable', 'REG_DWORD', 0)) {
		notify($ERRORS{'OK'}, 0, "set the CEIPEnable software registry key to 0");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to set the CEIPEnable registry key to 0");
		return;
	}
	
	# Attempt to set the CEIPEnable policy key
	my $registry_key_policy = 'HKEY_LOCAL_MACHINE\\Software\\Policies\\Microsoft\\SQMClient\\Windows';
	if ($self->reg_add($registry_key_policy, 'CEIPEnable', 'REG_DWORD', 0)) {
		notify($ERRORS{'OK'}, 0, "set the CEIPEnable policy registry key to 0");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to set the CEIPEnable policy registry key to 0");
		return;
	}
	
	return 1;
}

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

=head2 disable_shutdown_event_tracker

 Parameters  : None
 Returns     : If successful: true
               If failed: false
 Description : Disables the Shutdown Event Tracker. This is enabled by default
					on Windows Server 2003. It is what causes a box to appear which
					asks for a reason when the computer is shutdown or rebooted. The
					box also appears during login if the computer is shut down
					unexpectedly. This causes the autologon sequence to break.

=cut

sub disable_shutdown_event_tracker {
	my $self = shift;
	if (ref($self) !~ /windows/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Attempt to set the ShutdownReasonOn key
	my $registry_key_software = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\Reliability';
	if ($self->reg_add($registry_key_software, 'ShutdownReasonOn', 'REG_DWORD', 0)) {
		notify($ERRORS{'OK'}, 0, "set the ShutdownReasonOn software registry key to 0");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to set the ShutdownReasonOn registry key to 0");
		return;
	}
	
	return 1;
}

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

=head2 setup

 Parameters  : none
 Returns     : 
 Description : Presents a command-line menu interface to the user to configure
               the Windows OS modules when vcld is run in setup mode.

=cut

sub setup {
	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;
	}
	
	push @{$ENV{setup_path}}, 'Windows';
	
	my @operation_choices = (
		'Configure Product Keys',
		'Configure KMS Servers',
	);
	
	my @setup_path = @{$ENV{setup_path}};
	OPERATION: while (1) {
		@{$ENV{setup_path}} = @setup_path;
		
		print '-' x 76 . "\n";
		
		$self->setup_check();
		
		print "Choose an operation:\n";
		my $operation_choice_index = setup_get_array_choice(@operation_choices);
		last if (!defined($operation_choice_index));
		my $operation_name = $operation_choices[$operation_choice_index];
		print "\n";
		
		push @{$ENV{setup_path}}, $operation_name;
		
		if ($operation_name =~ /product keys/i) {
			$self->setup_product_keys();
		}
		elsif ($operation_name =~ /kms/i) {
			$self->setup_kms_servers();
		}
	}
	
	pop @{$ENV{setup_path}};
	return 1;
}

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

=head2 setup_check

 Parameters  : none
 Returns     :
 Description : Checks various configuration settings and displays a message to
					the user if any important settings are not configured. This gets
					called every time Windows.pm::setup() is called.

=cut

sub setup_check {
	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 @messages;
	
	# Get a hash containing all of the information from the affiliation table
	my $affiliation_info = get_affiliation_info();
	if (!$affiliation_info) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve affiliation info");
		return;
	}
	
	my ($global_affiliation_id) = grep { $affiliation_info->{$_}{name} =~ /^global$/i } (keys %$affiliation_info);
	if (!defined($global_affiliation_id)) {
		print "ERROR: unable to determine global affiliation ID:\n" . format_data($affiliation_info) . "\n";
		return;
	}
	
	# Get the product key information from the database
	my $product_key_info = $self->get_product_key_info();
	if (!defined($product_key_info)) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve product key information from the database");
		return;
	}
	
	my @product_names = keys %{$product_key_info->{$global_affiliation_id}};
	
	if (!grep(/Windows XP/, @product_names)) {
		push @messages, "A Windows XP product key is not configured for the Global affiliation. Captured Windows XP images using Sysprep may fail to load if the product key is not configured.";
	}
	if (!grep(/Server 2003/, @product_names)) {
		push @messages, "A Windows Server 2003 product key is not configured for the Global affiliation. Captured Windows Server 2003 images using Sysprep may fail to load if the product key is not configured.";
	}
	
	for my $message (@messages) {
		chomp $message;
		setup_print_wrap("*** $message ***\n\n");
	}
}

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

=head2 setup_product_keys

 Parameters  : none
 Returns     : nothing
 Description : Used to list, set, and delete product keys from the winProductKey
               table in the database when vcld is run in setup mode.

=cut

sub setup_product_keys {
	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 a hash containing all of the information from the affiliation table
	my $affiliation_info = get_affiliation_info();
	if (!$affiliation_info) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve affiliation info");
		return;
	}
	
	my @product_names = (
		'Windows XP',
		'Windows Server 2003',
		'Windows Vista Business',
		'Windows Vista Business N',
		'Windows Vista Enterprise',
		'Windows Vista Enterprise N',
		'Windows Server 2008 Datacenter',
		'Windows Server 2008 Datacenter without Hyper-V',
		'Windows Server 2008 for Itanium-Based Systems',
		'Windows Server 2008 Enterprise',
		'Windows Server 2008 Enterprise without Hyper-V',
		'Windows Server 2008 Standard',
		'Windows Server 2008 Standard without Hyper-V',
		'Windows Web Server 2008',
		'Windows Server 2008 HPC',
		'Windows 7 Professional',
		'Windows 7 Professional N',
		'Windows 7 Professional E',
		'Windows 7 Enterprise',
		'Windows 7 Enterprise N',
		'Windows 7 Enterprise E',
		'Windows Server 2008 R2 Web',
		'Windows Server 2008 R2 HPC edition',
		'Windows Server 2008 R2 Standard',
		'Windows Server 2008 R2 Enterprise',
		'Windows Server 2008 R2 Datacenter',
		'Windows Server 2008 R2 for Itanium-based Systems',
		'Other',
	);
	
	my @operation_choices = (
		'List Product Keys',
		'Add Product Key',
		'Delete Product Key',
	);
	
	my @setup_path = @{$ENV{setup_path}};
	OPERATION: while (1) {
		@{$ENV{setup_path}} = @setup_path;
		
		print '-' x 76 . "\n";
		
		print "Choose an operation:\n";
		my $operation_choice_index = setup_get_array_choice(@operation_choices);
		last if (!defined($operation_choice_index));
		my $operation_name = $operation_choices[$operation_choice_index];
		print "\n";
		
		push @{$ENV{setup_path}}, $operation_name;
		
		if ($operation_name =~ /list/i) {
			$self->setup_display_product_key_info();
			print "\n";
		}
		
		elsif ($operation_name =~ /add/i) {
			print "Choose an affiliation:\n";
			my $affiliation_id = setup_get_hash_choice($affiliation_info, 'name');
			next if (!defined($affiliation_id));
			my $affiliation_name = $affiliation_info->{$affiliation_id}{name};
			print "Selected affiliation: $affiliation_name\n\n";
			
			$self->setup_display_product_key_info($affiliation_id);
			print "\n";
			
			print "Choose a Windows product:\n";
			my $product_choice_index = setup_get_array_choice(@product_names);
			next OPERATION if (!defined($product_choice_index));
			
			my $product_name = $product_names[$product_choice_index];
			if ($product_name eq 'Other') {
				$product_name = setup_get_input_string("Enter a product name");
				next OPERATION if (!defined($product_name));
			}
			print "Windows product: $product_name\n\n";
			
			my $product_key;
			while (!$product_key) {
				$product_key = setup_get_input_string("Enter the product key xxxxx-xxxxx-xxxxx-xxxxx-xxxxx");
				next OPERATION if (!defined($product_key));
				if ($product_key !~ /(\w{5}-?){5}/) {
					print "Product key is not in the correct format: $product_key\n";
					$product_key = 0;
				}
			}
			$product_key = uc($product_key);
			print "\n";
			
			# Attempt to set the product key in the database
			if ($self->set_product_key($affiliation_id, $product_name, $product_key)) {
				print "Product key has been saved to the database:\nAffiliation: $affiliation_name\nProduct name: $product_name\nProduct key: $product_key\n";
			}
			else {
				print "ERROR: failed to save product key to the database:\nAffiliation: $affiliation_name\nProduct name: $product_name\nProduct key: $product_key\n";
			}
		}
		
		elsif ($operation_name =~ /delete/i) {
			# Get the product key information from the database
			my $product_key_info = $self->get_product_key_info();
			if (!defined($product_key_info)) {
				notify($ERRORS{'WARNING'}, 0, "failed to retrieve product key information from the database");
				return;
			}
			
			my %product_keys;
			for my $affiliation_id (keys %$product_key_info) {
				my $affiliation_name = $affiliation_info->{$affiliation_id}{name};
				
				for my $product_name (keys %{$product_key_info->{$affiliation_id}}) {
					my $product_key = $product_key_info->{$affiliation_id}{$product_name};
					
					my $product_key_choice_name = "$affiliation_name: '$product_name' ($product_key)";
					
					$product_keys{$product_key_choice_name}{affiliation_id} = $affiliation_id;
					$product_keys{$product_key_choice_name}{product_name} = $product_name;
					$product_keys{$product_key_choice_name}{product_key} = $product_key;
				}
			}
			
			# Choose an affiliation with populated product keys
			print "Choose a product key to delete:\n";
			my $product_key_choice_name = setup_get_hash_choice(\%product_keys);
			next if (!defined($product_key_choice_name));
			print "\n";
			
			my $affiliation_id = $product_keys{$product_key_choice_name}{affiliation_id};
			my $affiliation_name = $affiliation_info->{$affiliation_id}{name};
			my $product_name = $product_keys{$product_key_choice_name}{product_name};
			my $product_key = $product_keys{$product_key_choice_name}{product_key};
			
			# Attempt to delete the product key from the database
			if ($self->delete_product_key($affiliation_id, $product_name, $product_key)) {
				print "Product key for has been deleted from the database:\nAffiliation: $affiliation_name\nProduct name: $product_name\nProduct key: $product_key\n";
			}
			else {
				print "ERROR: failed to delete product key from the database:\nAffiliation: $affiliation_name\nProduct name: $product_name\nProduct key: $product_key\n";
			}
		}
	}
}

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

=head2 setup_display_product_key_info

 Parameters  : $affiliation_id (optional)
 Returns     :
 Description : Displays the product keys configured in the winProductKey table
               in the database. If an affiliation ID argument is specified, only
               the information for that affiliation is displayed.

=cut

sub setup_display_product_key_info {
	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 a hash containing all of the information from the affiliation table
	my $affiliation_info = get_affiliation_info();
	if (!$affiliation_info) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve affiliation info");
		return;
	}
	
	# Get the affiliation ID argument if it was specified
	my $affiliation_id_argument = shift;
	if ($affiliation_id_argument && !defined($affiliation_info->{$affiliation_id_argument})) {
		notify($ERRORS{'WARNING'}, 0, "affiliation does not exist for affiliation ID argument: $affiliation_id_argument");
		return;
	}
	
	# Get the product key information from the database
	my $product_key_info = $self->get_product_key_info();
	if (!defined($product_key_info)) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve product key information from the database");
		return;
	}
	
	# Print the title
	if ($affiliation_id_argument) {
		my $affiliation_name = $affiliation_info->{$affiliation_id_argument}{name};
		print "Product key configuration for $affiliation_name ($affiliation_id_argument):\n";
	}
	else {
		print "Product key configuration for all affiliations:\n";
	}
	
	
	my $product_key_info_string;
	for my $affiliation_id (sort { $a <=> $b } keys %$product_key_info) {
		
		if (defined($affiliation_id_argument) && $affiliation_id ne $affiliation_id_argument) {
			next;
		}
		
		my $affiliation_name = $affiliation_info->{$affiliation_id}{name};
		
		$product_key_info_string .= "$affiliation_name ($affiliation_id):\n";
		for my $product_name (keys %{$product_key_info->{$affiliation_id}}) {
			my $product_key = $product_key_info->{$affiliation_id}{$product_name};
			$product_key_info_string .= "   $product_name: $product_key\n";
		}
	}
	
	$product_key_info_string = "<not configured>\n" if !$product_key_info_string;
	print "$product_key_info_string";
}

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

=head2 setup_kms_servers

 Parameters  : none
 Returns     : nothing
 Description : Configures KMS servers in the winKMS table in the database when
               vcld is run in setup mode.

=cut

sub setup_kms_servers {
	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 a hash containing all of the information from the affiliation table
	my $affiliation_info = get_affiliation_info();
	if (!$affiliation_info) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve affiliation info");
		return;
	}
	
	my @operation_choices = (
		'List KMS Servers',
		'Add KMS Server',
		'Delete KMS Server',
	);
	
	my @setup_path = @{$ENV{setup_path}};
	OPERATION: while (1) {
		@{$ENV{setup_path}} = @setup_path;
		print '-' x 76 . "\n";
		
		print "Choose an operation:\n";
		my $operation_choice_index = setup_get_array_choice(@operation_choices);
		last if (!defined($operation_choice_index));
		my $operation_name = $operation_choices[$operation_choice_index];
		print "\n";
		
		push @{$ENV{setup_path}}, $operation_name;
		
		if ($operation_name =~ /list/i) {
			$self->setup_display_kms_server_info();
			print "\n";
		}
		
		elsif ($operation_name =~ /add/i) {
			print "Choose an affiliation:\n";
			my $affiliation_id = setup_get_hash_choice($affiliation_info, 'name');
			next if (!defined($affiliation_id));
			my $affiliation_name = $affiliation_info->{$affiliation_id}{name};
			print "Selected affiliation: $affiliation_name\n\n";
			
			$self->setup_display_kms_server_info($affiliation_id);
			print "\n";
			
			my $address;
			while (!$address) {
				$address = setup_get_input_string("Enter the KMS server host name or address");
				next OPERATION if (!defined($address));
				if (!is_valid_dns_host_name($address) && !is_valid_ip_address($address)) {
					print "Address is not a valid DNS host name or IP address: $address\n";
					$address = '';
				}
			}
			print "\n";
			
			my $port;
			while (!$port) {
				$port = setup_get_input_string("Enter the KMS server port", 1688);
				next OPERATION if (!defined($port));
				if ($port !~ /^\d+$/) {
					print "Port must be an integer: $port\n";
					$port = '';
				}
			}
			
			print "\n";
			
			# Attempt to set the KMS server in the database
			if ($self->set_kms_server($affiliation_id, $address, $port)) {
				print "KMS server added to the database:\nAffiliation: $affiliation_name\nAddress: $address\nPort: $port\n";
			}
			else {
				print "ERROR: failed to save product key to the database:\nAffiliation: $affiliation_name\nAddress: $address\nPort: $port\n";
			}
		}
		
		elsif ($operation_name =~ /delete/i) {
			# Get the KMS server information from the database
			my $kms_server_info = $self->get_kms_server_info();
			if (!defined($kms_server_info)) {
				notify($ERRORS{'WARNING'}, 0, "failed to retrieve KMS server information from the database");
				return;
			}
			
			my %kms_servers;
			for my $affiliation_id (keys %$kms_server_info) {
				my $affiliation_name = $affiliation_info->{$affiliation_id}{name};
				
				for my $address (keys %{$kms_server_info->{$affiliation_id}}) {
					my $port = $kms_server_info->{$affiliation_id}{$address};
					
					my $kms_server_choice_name = "$affiliation_name: $address:$port";
					
					$kms_servers{$kms_server_choice_name}{affiliation_id} = $affiliation_id;
					$kms_servers{$kms_server_choice_name}{address} = $address;
					$kms_servers{$kms_server_choice_name}{port} = $port;
				}
			}
			
			# Choose an affiliation populated with a KMS server
			print "Choose a KMS server to delete:\n";
			my $kms_server_choice_name = setup_get_hash_choice(\%kms_servers);
			next if (!defined($kms_server_choice_name));
			print "\n";
			
			my $affiliation_id = $kms_servers{$kms_server_choice_name}{affiliation_id};
			my $affiliation_name = $affiliation_info->{$affiliation_id}{name};
			my $address = $kms_servers{$kms_server_choice_name}{address};
			my $port = $kms_servers{$kms_server_choice_name}{port};
			
			## Attempt to delete the product key from the database
			if ($self->delete_kms_server($affiliation_id, $address)) {
				print "KMS server has been deleted from the database:\nAffiliation: $affiliation_name\nAddress: $address\nPort: $port\n";
			}
			else {
				print "ERROR: failed to delete product key from the database:\nAffiliation: $affiliation_name\nAddress: $address\nPort: $port\n";
			}
		}
	}
}

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

=head2 setup_display_kms_server_info

 Parameters  : $affiliation_id (optional)
 Returns     :
 Description : Displays the KMS server configuration stored in the winKMS table
               in the database to STDOUT.

=cut

sub setup_display_kms_server_info {
	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 a hash containing all of the information from the affiliation table
	my $affiliation_info = get_affiliation_info();
	if (!$affiliation_info) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve affiliation info");
		return;
	}
	
	# Get the affiliation ID argument if it was specified
	my $affiliation_id_argument = shift;
	if ($affiliation_id_argument && !defined($affiliation_info->{$affiliation_id_argument})) {
		notify($ERRORS{'WARNING'}, 0, "affiliation does not exist for affiliation ID argument: $affiliation_id_argument");
		return;
	}
	
	# Get the KMS server information from the database
	my $kms_server_info = $self->get_kms_server_info();
	if (!defined($kms_server_info)) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve KMS server information from the database");
		return;
	}
	
	# Print the title
	if ($affiliation_id_argument) {
		my $affiliation_name = $affiliation_info->{$affiliation_id_argument}{name};
		print "KMS server configuration for $affiliation_name ($affiliation_id_argument):\n";
	}
	else {
		print "KMS server configuration for all affiliations:\n";
	}
	
	# Print the KMS serer information
	my $kms_server_info_string;
	for my $affiliation_id (sort { $a <=> $b } keys %$kms_server_info) {
		# Ignore non-matching affiliations if the affiliation ID argument was specified
		if (defined($affiliation_id_argument) && $affiliation_id ne $affiliation_id_argument) {
			next;
		}
		
		my $affiliation_name = $affiliation_info->{$affiliation_id}{name};
		
		$kms_server_info_string .= "$affiliation_name ($affiliation_id):\n";
		for my $address (keys %{$kms_server_info->{$affiliation_id}}) {
			my $port = $kms_server_info->{$affiliation_id}{$address};
			$kms_server_info_string .= "   $address:$port\n";
		}
	}
	
	$kms_server_info_string = "<not configured>\n" if !$kms_server_info_string;
	print "$kms_server_info_string";
}

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

=head2 get_time_zone_name

 Parameters  : none
 Returns     : string
 Description : Returns the name of the time zone configured for the management
               node. The date command is run locally on the management node and
               the time zone abbreviation is parsed from the output. This
               %TIME_ZONE_INFO hash is searched for matching time zone
               information and the time zone name is returned. If a matching
               time zone is not found, 'Eastern Standard Time' is returned.
               Example: 'HVL' returns 'Tasmania Standard Time'

=cut

sub get_time_zone_name {
	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 $default_time_zone_name = 'Eastern Standard Time';
	
	# Call date to determine the time zone abbreviation in use on the management node
	my ($exit_status, $output) = run_command('date +%Z');
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run command to determine time zone configured for management node, returning '$default_time_zone_name'");
		return $default_time_zone_name;
	}
	
	# Extract the time zone abbreviation from the output
	my ($set_abbreviation) = grep(/^\w{3}$/, @$output);
	if (!$set_abbreviation) {
		notify($ERRORS{'WARNING'}, 0, "failed to determine time zone abbreviation from output, returning '$default_time_zone_name':\n" . join("\n", @$output));
		return $default_time_zone_name;
	}
	
	# Windows time zone codes don't include corresponding daylight time abbreviations, e.g. EDT
	# Convert *DT --> *ST
	if ($set_abbreviation =~ /(.)DT/i) {
		$set_abbreviation = "$1ST";
		notify($ERRORS{'DEBUG'}, 0, "time zone abbreviation converted to standard time: $1DT --> $set_abbreviation");
	}
	
	# Loop through the time zone codes until a matching abbreviation is found
	for my $time_zone_name (sort keys %TIME_ZONE_INFO) {
		my $time_zone_abbreviation = $TIME_ZONE_INFO{$time_zone_name}{abbreviation};
		
		next if (!$time_zone_abbreviation || $set_abbreviation !~ /^$time_zone_abbreviation$/i);
		
		notify($ERRORS{'DEBUG'}, 0, "determined name of time zone configured for management node: '$time_zone_name'");
		return $time_zone_name;
	}
	
	# Return the code for EST if a match was not found
	notify($ERRORS{'WARNING'}, 0, "unable to determine name of time zone configured for management node, abbreviation: $set_abbreviation, returning '$default_time_zone_name'");
	return $default_time_zone_name;
}

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

=head2 get_time_zone_code

 Parameters  : none
 Returns     : string
 Description : Returns the Windows numerical code of the time zone configured
               for the management node. If a matching time zone is not found, 35
               is returned.
               Example: 'HVL' returns 265

=cut

sub get_time_zone_code {
	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 $time_zone_name = $self->get_time_zone_name() || return;
	
	my $time_zone_code = $TIME_ZONE_INFO{$time_zone_name}{code};
	if ($time_zone_code) {
		notify($ERRORS{'DEBUG'}, 0, "determined Windows code of time zone configured for management node: $time_zone_code");
		return $time_zone_code;
	}
	else {
		my $default = 35;
		notify($ERRORS{'WARNING'}, 0, "time zone code could not be determined for time zone: '$time_zone_name', returning $default");
		return $default;
	}
}

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

=head2 sanitize_files

 Parameters  : @file_paths (optional)
 Returns     : boolean
 Description : Removes the Windows root password from files on the computer.

=cut

sub sanitize_files {
	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 arguments, add the node configuration directory
	my @file_paths = @_;
	push @file_paths, $self->get_node_configuration_directory();
	
	# Loop through each file path, remove the Windows root password from each
	my $error_occurred = 0;
	for my $file_path (@file_paths) {
		if (!$self->search_and_replace_in_files($file_path, $WINDOWS_ROOT_PASSWORD, 'WINDOWS_ROOT_PASSWORD')) {
			notify($ERRORS{'WARNING'}, 0, "failed to remove the Windows root password from: $file_path");
			$error_occurred = 1;
		}
	}
	
	if ($error_occurred) {
		return;
	}
	else {
		return 1;
	}
}

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

=head2 clear_event_log

 Parameters  : @logfile_names (optional)
 Returns     : boolean
 Description : Clears the Windows 'Application', 'Security', 'System' event
               logs. One or more event logfile names may be specified to only
               clear certain event logs.

=cut

sub clear_event_log {
	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 @logfile_names = @_;
	@logfile_names = ('Application', 'Security', 'System') if !@logfile_names;
	
	my $management_node_keys = $self->data->get_management_node_keys();
	my $computer_node_name   = $self->data->get_computer_node_name();
	my $system32_path        = $self->get_system32_path() || return;
	
	# Assemble the command
	# Call wmic.exe - the WMI shell
	# wmic.exe will hang if it is called by itself.  It has something to do with TTY/PTY
	# Piping the echo command seems to prevent it from hanging
	my $command;
	for my $logfile_name (@logfile_names) {
		$command .= "echo | $system32_path/Wbem/wmic.exe NTEVENTLOG WHERE LogFileName=\\\"$logfile_name\\\" CALL ClearEventLog ; ";
	}
	
	# Remove the last ' ; ' added to the command
	$command =~ s/[\s;]*$//g;
	
	my ($status, $output) = run_ssh_command($computer_node_name, $management_node_keys, $command);
	if (!defined($output)) {
		notify($ERRORS{'DEBUG'}, 0, "failed to run SSH command to clear the event log: @logfile_names");
		return;
	}
	elsif (grep(/ERROR/i, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to clear event log: @logfile_names, output:\n" . join("\n", @$output));
		return;
	}
	elsif (grep(/Method execution successful/i, @$output)) {
		notify($ERRORS{'DEBUG'}, 0, "cleared event log: @logfile_names");
		$self->create_eventlog_entry("Event log cleared by VCL");
		return 1;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unexpected output while clearing event log: @logfile_names, output:\n" . join("\n", @$output));
		return;
	}
}

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

=head2 disable_login_screensaver

 Parameters  : None
 Returns     : 
 Description : Sets the registry keys to disable to login screensaver.

=cut

sub disable_login_screensaver {
	my $self = shift;
	if (ref($self) !~ /windows/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my $registry_key = 'HKEY_USERS\\.DEFAULT\\Control Panel\\Desktop';
	if ($self->reg_add($registry_key, 'ScreenSaveActive', 'REG_SZ', 0) &&
		 $self->reg_add($registry_key, 'ScreenSaveTimeOut', 'REG_SZ', 0)) {
		notify($ERRORS{'DEBUG'}, 0, "set registry keys to disable the login screensaver");
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to set registry keys to disable the login screensaver");
		return;
	}
	
	return 1;
}

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

=head2 fix_default_profile

 Parameters  : none
 Returns     : boolean
 Description : Attempts to correct common problems with the default user
               profile by loading the default user registry hive from the
               ntuser.dat file into the registry, making changes, then unloading
               the hive.

=cut

sub fix_default_profile {
	my $self = shift;
	if (ref($self) !~ /windows/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 $root_key = 'HKEY_USERS\DEFAULT_USER_PROFILE';
	my $profile_list_key = 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList';
	
	# Determine the default user profile path
	my $profile_list_registry_info = $self->reg_query($profile_list_key);
	if (!$profile_list_registry_info) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve profile information from the registry on $computer_node_name");
		return;
	}
	elsif (!$profile_list_registry_info->{$profile_list_key}) {
		notify($ERRORS{'WARNING'}, 0, "unable to determine default profile path, '$profile_list_key' key does not exist in the registry data:\n" . format_data($profile_list_registry_info));
		return;
	}
	
	# The default profile path should either be stored in the 'Default' value or can be assembled from combining the 'ProfilesDirectory' and 'DefaultUserProfile' values
	my $default_profile_path;
	if ($profile_list_registry_info->{$profile_list_key}{Default}) {
		$default_profile_path = $profile_list_registry_info->{$profile_list_key}{Default};
	}
	elsif ($profile_list_registry_info->{$profile_list_key}{ProfilesDirectory} && $profile_list_registry_info->{$profile_list_key}{DefaultUserProfile}) {
		$default_profile_path = "$profile_list_registry_info->{$profile_list_key}{ProfilesDirectory}\\$profile_list_registry_info->{$profile_list_key}{DefaultUserProfile}";
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to determine default profile path from the registry on $computer_node_name:\n" . format_data($profile_list_registry_info->{$profile_list_key}));
		return;
	}
	notify($ERRORS{'DEBUG'}, 0, "determined default profile path from the registry on $computer_node_name: '$default_profile_path'");
	
	# Load the default profile hive file into the registry
	my $hive_file_path = "$default_profile_path\\ntuser.dat";
	if (!$self->reg_load($root_key, $hive_file_path)) {
		notify($ERRORS{'WARNING'}, 0, "failed to load the default profile hive into the registry on $computer_node_name");
		return;
	}
	
	# Fix registry values known to cause problems
	# The "Shell Folders" key may contain paths pointing to a specific user's profile
	# Any paths under "Shell Folders" can be deleted
	my $registry_string .= <<EOF;
Windows Registry Editor Version 5.00
[-$root_key\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders]
[$root_key\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders]
EOF

	# Import the string into the registry
	if (!$self->import_registry_string($registry_string)) {
		notify($ERRORS{'WARNING'}, 0, "failed to fix problematic registry settings in the default profile");
		return;
	}
	
	# Unoad the default profile hive
	if (!$self->reg_unload($root_key)) {
		notify($ERRORS{'WARNING'}, 0, "failed to unload the default profile hive from the registry on $computer_node_name");
		return;
	}

	return 1;
}

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

1;
__END__

=head1 SEE ALSO

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

=cut
