You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@vcl.apache.org by ar...@apache.org on 2016/07/14 17:18:58 UTC

svn commit: r1752706 - /vcl/trunk/managementnode/lib/VCL/Module/OS/Windows.pm

Author: arkurth
Date: Thu Jul 14 17:18:57 2016
New Revision: 1752706

URL: http://svn.apache.org/viewvc?rev=1752706&view=rev
Log:
VCL-915
Added start_nfs_client_service and stop_nfs_client_service to Windows.pm which use nfsadmin.exe to control the service. Updated start_service and stop_service to call these new subroutines if the service name is NfsClnt.

Other
Cleaned up old code in add_user_group_policy_script and remove_user_group_policy_script. Added get_user_group_policy_script_info.

Modified:
    vcl/trunk/managementnode/lib/VCL/Module/OS/Windows.pm

Modified: vcl/trunk/managementnode/lib/VCL/Module/OS/Windows.pm
URL: http://svn.apache.org/viewvc/vcl/trunk/managementnode/lib/VCL/Module/OS/Windows.pm?rev=1752706&r1=1752705&r2=1752706&view=diff
==============================================================================
--- vcl/trunk/managementnode/lib/VCL/Module/OS/Windows.pm (original)
+++ vcl/trunk/managementnode/lib/VCL/Module/OS/Windows.pm Thu Jul 14 17:18:57 2016
@@ -5894,8 +5894,8 @@ sub delete_capture_configuration_files {
 		$self->delete_files_by_pattern($system32_path . '/GroupPolicy/User/Scripts', '.*\(Prepare\|prepare\|Cleanup\|cleanup\|post_load\).*');
 		
 		# 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');
+		$self->remove_user_group_policy_script('logon', 'VCLprepare.cmd');
+		$self->remove_user_group_policy_script('logoff', 'VCLcleanup.cmd');
 	}
 	
 	# Remove old scripts and utilities
@@ -5922,15 +5922,33 @@ sub delete_capture_configuration_files {
 
 #/////////////////////////////////////////////////////////////////////////////
 
-=head2 add_group_policy_script
+=head2 get_user_group_policy_script_info
 
- Parameters  : 
- Returns     :
- Description : 
+ Parameters  : none
+ Returns     : hash reference
+ Description : Parses the script.ini file and constructs a hash reference:
+               {
+                 "logoff" => {
+                   0 => {
+                     "cmdline" => "C:\\logoff.cmd",
+                     "parameters" => ">> C:\\logoff.log"
+                   },
+                   1 => {
+                     "cmdline" => "logoff2.cmd",
+                     "parameters" => ""
+                   },
+                 },
+                 "logon" => {
+                   0 => {
+                     "cmdline" => "logon.cmd",
+                     "parameters" => ""
+                   }
+                 }
+               }
 
 =cut
 
-sub add_group_policy_script {
+sub get_user_group_policy_script_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");
@@ -5939,246 +5957,192 @@ sub add_group_policy_script {
 
 	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) = $self->execute($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) = $self->execute($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) = $self->execute($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) = $self->execute($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_ini_file_path = "$system32_path/GroupPolicy/User/Scripts/scripts.ini";
 	
-	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");
+	my @lines;
+	if ($self->file_exists($scripts_ini_file_path)) {
+		@lines = $self->get_file_contents($scripts_ini_file_path);
+		notify($ERRORS{'DEBUG'}, 0, "retrieved contents of scripts.ini:\n" . join("\n", @lines));
+	}
+	
+	# Format of scripts.ini
+	#     <--- BLANK FIRST LINE
+	# [Logon]
+	# 0CmdLine=logon.cmd
+	# 0Parameters=>> C:\logon.log
+	# 1CmdLine=logon1.cmd
+	# 1Parameters=
+	# [Logoff]
+	# 0CmdLine=C:\logoff.cmd
+	# 0Parameters=>> C:\logoff.log
+	
+	my $info = {
+		'logon' => {},
+		'logoff' => {},
+	};
+	
+	my $current_stage;
+	for my $line (@lines) {
+		# Ignore blank lines
+		next unless $line =~ /\w/;
+		
+		# Remove Unicode nul and special characters added to beginning of file
+		$line =~ s/(\x00|\xFE|\xFF)//g;
+		
+		# Find script stage section heading: [Logon] or [Logoff]
+		if ($line =~ /\[(logon|logoff)\]/i) {
+			$current_stage = lc($1);
 			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}");
+		# Parse the line, format should be either:
+		# 0CmdLine=logon.cmd
+		# 0Parameters=>> C:\logon.log
+		my ($index, $property, $value) = $line =~ /^\s*(\d+)(\w+)=(.*)$/;
+		if (!defined($index)) {
+			notify($ERRORS{'WARNING'}, 0, "failed to parse scripts.ini, unable to parse line: '$line'\n" . join("\n", @lines));
+			return;
 		}
-		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++;
+		
+		$property = lc($property);
+		
+		if (defined($info->{$current_stage}{$index}{$property})) {
+			notify($ERRORS{'WARNING'}, 0, "failed to parse scripts.ini, multiple [$current_stage] '$property' lines exist for index $index:\n" . join("\n", @lines));
+			return;
 		}
+		
+		$info->{$current_stage}{$index}{$property} = $value;
 	}
 	
-	# 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";
+	# Guarantee each script index has both a CmdLine and Parameters entry
+	for my $check_stage (keys %$info) {
+		for my $index (keys %{$info->{$check_stage}}) {
+			$info->{$check_stage}{$index}{'cmdline'} = '' unless defined($info->{$check_stage}{$index}{'cmdline'});
+			$info->{$check_stage}{$index}{'parameters'} = '' unless defined($info->{$check_stage}{$index}{'parameters'});
+		}
 	}
 	
-	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, "retrieved user group policy info:\n" . format_data($info));
+	return $info;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 add_user_group_policy_script
+
+ Parameters  : $stage_argument, $cmdline_argument, $parameters_argument
+ Returns     : boolean
+ Description : Adds a traditional (non-Powershell) user group policy script to
+               the computer to be automatically executed at logon or logoff. The
+               stage argument must either be 'logon' or 'logoff'.
+
+=cut
+
+sub add_user_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;
 	}
-	notify($ERRORS{'DEBUG'}, 0, "modified scripts.ini contents:\n$scripts_ini_modified");
-	
-	# Escape quote characters
-	$scripts_ini_modified =~ s/"/\\"/gs;
+
+	my $system32_path = $self->get_system32_path() || return;
 	
-	# Echo the modified contents to scripts.ini
-	my $echo_command = "echo \"$scripts_ini_modified\" > $scripts_ini";
-	my ($echo_status, $echo_output) = $self->execute($echo_command);
-	if (defined($echo_status) && $echo_status == 0) {
-		notify($ERRORS{'DEBUG'}, 0, "echo'd modified contents to scripts.ini");
+	# Get the arguments
+	my ($stage_argument, $cmdline_argument, $parameters_argument) = @_;
+	if (!$stage_argument) {
+		notify($ERRORS{'WARNING'}, 0, "stage (logon/logoff) argument was not specified");
+		return;
 	}
-	elsif (defined($echo_status)) {
-		notify($ERRORS{'WARNING'}, 0, "failed to echo modified contents to scripts.ini, exit status: $echo_status, output:\n@{$echo_output}");
+	elsif (!$stage_argument || $stage_argument !~ /^(logon|logoff)$/i) {
+		notify($ERRORS{'WARNING'}, 0, "stage argument is not valid: $stage_argument, it must be 'logon' or 'logoff'");
 		return;
 	}
-	else {
-		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to echo modified contents to scripts.ini");
+	elsif (!$cmdline_argument) {
+		notify($ERRORS{'WARNING'}, 0, "CmdLine argument was not specified");
 		return;
 	}
 	
-	# Apply Windows-style line endings to scripts.ini
-	$self->set_text_file_line_endings($scripts_ini);
+	# Use lower case for all comparisons
+	$stage_argument = lc($stage_argument);
 	
-	# Get the modified contents of scripts.ini
-	my $cat_modified_command = "cat $scripts_ini";
-	my ($cat_modified_status, $cat_modified_output) = $self->execute($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");
-	}
+	# Replace slashes with double backslashes
+	$cmdline_argument =~ s/[\\\/]+/\\/g;
+	my $cmdline_argument_escaped = quotemeta($cmdline_argument);
+	
+	# Construct a hash which will be inserted into the scripts.ini info hash
+	my $script_argument = {
+		'cmdline' => $cmdline_argument,
+		'parameters' => $parameters_argument,
+	};
 	
-	## Run gpupdate so the new settings take effect immediately
-	#$self->run_gpupdate();
+	# Path to scripts.ini file
+	my $scripts_ini_directory_path = "$system32_path/GroupPolicy/User/Scripts";
+	my $scripts_ini_file_path = "$scripts_ini_directory_path/scripts.ini";
 	
-	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;
+	my $info = $self->get_user_group_policy_script_info() || return;
+	
+	# Figure out the index to use
+	# Check if a script exists with an idential command line
+	my $add_index;
+	for my $index (sort {$a <=> $b} keys %{$info->{$stage_argument}}) {
+		my $cmdline = $info->{$stage_argument}{$index}{cmdline};
+		my $cmdline_escaped = quotemeta($cmdline);
+		
+		if (lc($cmdline_escaped) eq lc($cmdline_argument_escaped)) {
+			$add_index = $index;
+			notify($ERRORS{'DEBUG'}, 0, "replacing existing $stage_argument script at index $add_index:\n" .
+				"existing script:\n" . format_data($info->{$stage_argument}{$index}) . "\n" .
+				"argument:\n" . format_data($script_argument)
+			);
+			last;
+		}
+		else {
+			notify($ERRORS{'DEBUG'}, 0, "command line of existing $stage_argument script at index $index does not match argument:\n" .
+				"existing: '$cmdline_escaped'\n" .
+				"argument: '$cmdline_argument_escaped'"
+			);
+		}
+	}
+	if (!defined($add_index)) {
+		# Existing matching script not found, add to end
+		$add_index = scalar keys %{$info->{$stage_argument}};
+		notify($ERRORS{'DEBUG'}, 0, "existing $stage_argument script was not found with command line matching argument, script will be added with index $add_index");
+	}
+	
+	# Add or replace the script defined by the arguments
+	$info->{$stage_argument}{$add_index} = $script_argument;
+	notify($ERRORS{'DEBUG'}, 0, "updated scripts.ini content:\n" . format_data($info));
+	
+	# Assemble the updated scripts.ini content
+	my $updated_contents;
+	for my $stage ('logon', 'logoff') {
+		# Capitalize the first letter and add it to the contents
+		my $stage_uc = ucfirst($stage);
+		$updated_contents .= "[$stage_uc]\n";
+		
+		for my $index (sort {$a <=> $b} keys %{$info->{$stage}}) {
+			my $script = $info->{$stage}{$index};
+			$updated_contents .= $index . "CmdLine=" . $script->{cmdline} . "\n";
+			$updated_contents .= $index . "Parameters=" . $script->{parameters} . "\n";
+		}
+	}
+	notify($ERRORS{'DEBUG'}, 0, "updated $scripts_ini_file_path contents:\n$updated_contents");
+	return $self->create_text_file($scripts_ini_file_path, $updated_contents);
 }
 
 #/////////////////////////////////////////////////////////////////////////////
 
-=head2 remove_group_policy_script
+=head2 remove_user_group_policy_script
 
- Parameters  : 
- Returns     :
- Description : 
+ Parameters  : $stage_argument, $cmdline_argument
+ Returns     : boolean
+ Description : Removes a traditional (non-Powershell) user logon or logoff
+               script group policy from the computer. The stage argument must
+               either be 'logon' or 'logoff'.
 
 =cut
 
-sub remove_group_policy_script {
+sub remove_user_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");
@@ -6188,226 +6152,97 @@ sub remove_group_policy_script {
 	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) {
+	my ($stage_argument, $cmdline_argument, $parameters_argument) = @_;
+	if (!$stage_argument) {
 		notify($ERRORS{'WARNING'}, 0, "stage (logon/logoff) argument was not specified");
 		return;
 	}
-	if (!$cmdline_argument) {
+	elsif (!$stage_argument || $stage_argument !~ /^(logon|logoff)$/i) {
+		notify($ERRORS{'WARNING'}, 0, "stage argument is not valid: $stage_argument, it must be 'logon' or 'logoff'");
+		return;
+	}
+	elsif (!$cmdline_argument) {
 		notify($ERRORS{'WARNING'}, 0, "CmdLine argument was not specified");
 		return;
 	}
 	
-	# Capitalize the first letter of logon/logoff
+	# Use lower case for all comparisons
 	$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';
-	}
+	# Replace slashes with double backslashes
+	$cmdline_argument =~ s/[\\\/]+/\\/g;
+	my $cmdline_argument_escaped = quotemeta($cmdline_argument);
 	
-	# 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) = $self->execute($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");
-	}
+	# Extract the last part of the command line if a full path was specified
+	my ($cmdline_argument_executable) = $cmdline_argument =~ /([^\\]+)$/;
 	
-	# Set the permissions of scripts.ini to 664
-	my $chmod_command = "chmod 664 $scripts_ini";
-	my ($chmod_status, $chmod_output) = $self->execute($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");
-	}
+	# Path to scripts.ini file
+	my $scripts_ini_directory_path = "$system32_path/GroupPolicy/User/Scripts";
+	my $scripts_ini_file_path = "$scripts_ini_directory_path/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) = $self->execute($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");
-	}
+	my $info = $self->get_user_group_policy_script_info() || return;
 	
-	# Get the contents of scripts.ini
-	my $cat_command = "cat $scripts_ini";
-	my ($cat_status, $cat_output) = $self->execute($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");
+	# Attempt to delete batch or script files specified by the argument
+	if ($cmdline_argument =~ /\\/) {
+		$self->delete_file($cmdline_argument);
 	}
 	else {
-		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to scripts.ini contents");
+		$self->delete_files_by_pattern("$system32_path/GroupPolicy/User/Scripts", ".*$cmdline_argument.*", 2);
 	}
 	
-	# 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");
+	# Find matching scripts, delete from hash if found
+	my $found_match = 0;
+	for my $index (sort {$a <=> $b} keys %{$info->{$stage_argument}}) {
+		my $cmdline = $info->{$stage_argument}{$index}{cmdline};
+		my $cmdline_escaped = quotemeta($cmdline);
+		
+		# Extract the last part of the command line if it contains a full path
+		my ($cmdline_executable) = $cmdline =~ /([^\\]+)$/;
+		
+		if (lc($cmdline_escaped) eq lc($cmdline_argument_escaped)) {
+			$found_match = 1;
+			notify($ERRORS{'DEBUG'}, 0, "existing command line matches argument, removing $stage_argument script at index $index:\n" .
+				"argument: $cmdline_argument\n" .
+				"existing script:\n" . format_data($info->{$stage_argument}{$index})
+			);
+			delete $info->{$stage_argument}{$index};
 			next;
 		}
-		elsif ($scripts_original{$index}{CmdLine} =~ /^\s*$/) {
-			notify($ERRORS{'WARNING'}, 0, "CmdLine blank for index $index");
+		elsif ($cmdline_argument_executable && $cmdline_executable && lc($cmdline_argument_executable) eq lc($cmdline_executable)) {
+			$found_match = 1;
+			notify($ERRORS{'DEBUG'}, 0, "existing command line executable matches argument executable, removing $stage_argument script at index $index:\n" .
+				"argument: $cmdline_argument\n" .
+				"argument executable: $cmdline_argument_executable\n" .
+				"existing script:\n" . format_data($info->{$stage_argument}{$index})
+			);
+			delete $info->{$stage_argument}{$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) = $self->execute($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;
+	if (!$found_match) {
+		notify($ERRORS{'DEBUG'}, 0, "scripts.ini update not necessary, did not find existing script matching command line argument: $cmdline_argument");
+		return 1;
 	}
 	
-	# Apply Windows-style line endings to scripts.ini
-	$self->set_text_file_line_endings($scripts_ini);
-	
-	# Get the modified contents of scripts.ini
-	my $cat_modified_command = "cat $scripts_ini";
-	my ($cat_modified_status, $cat_modified_output) = $self->execute($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");
+	# Assemble the updated scripts.ini content
+	my $updated_contents;
+	for my $stage ('logon', 'logoff') {
+		# Capitalize the first letter and add it to the contents
+		my $stage_uc = ucfirst($stage);
+		$updated_contents .= "[$stage_uc]\n";
+		
+		# Reindex in case there are now gaps
+		my $new_index = 0;
+		for my $original_index (sort {$a <=> $b} keys %{$info->{$stage}}) {
+			my $script = $info->{$stage}{$original_index};
+			$updated_contents .= $new_index . "CmdLine=" . $script->{cmdline} . "\n";
+			$updated_contents .= $new_index . "Parameters=" . $script->{parameters} . "\n";
+			$new_index++;
+		}
 	}
-	
-	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;
+	notify($ERRORS{'DEBUG'}, 0, "updated $scripts_ini_file_path contents:\n$updated_contents");
+	return $self->create_text_file($scripts_ini_file_path, $updated_contents);
 }
 
 #/////////////////////////////////////////////////////////////////////////////
@@ -6994,9 +6829,9 @@ sub is_process_running {
 
 =head2 start_service
 
- Parameters  : 
- Returns     :
- Description : 
+ Parameters  : $service_name
+ Returns     : boolean
+ Description : Starts a service on the computer.
 
 =cut
 
@@ -7014,24 +6849,35 @@ sub start_service {
 		notify($ERRORS{'WARNING'}, 0, "service name was not passed as an argument");
 		return;
 	}
+	
+	# The Client for NFS service should be controlled with the nfsadmin.exe utility
+	if ($service_name =~ /^NfsClnt$/) {
+		if ($self->start_nfs_client_service()) {
+			return 1;
+		}
+	}
 
 	my $command = $system32_path . '/net.exe start "' . $service_name . '"';
-	my ($status, $output) = $self->execute($command);
-	if (defined($status) && $status == 0) {
-		notify($ERRORS{'OK'}, 0, "started service: $service_name");
+	my ($exit_status, $output) = $self->execute($command);
+	if (!defined($output)) {
+		notify($ERRORS{'WARNING'}, 0, "failed to execute command to to start service: $service_name");
+		return;
 	}
-	elsif (defined($output) && grep(/already been started/i, @{$output})) {
-		notify($ERRORS{'OK'}, 0, "service has already been started: $service_name");
+	elsif (grep(/is not started/i, @{$output})) {
+		notify($ERRORS{'OK'}, 0, "service is not started: $service_name");
+		return 1;
 	}
-	elsif (defined($status)) {
-		notify($ERRORS{'WARNING'}, 0, "unable to start service: $service_name, exit status: $status, output:\n@{$output}");
+	elsif (grep(/(does not exist|service name is invalid)/i, @$output)) {
+		notify($ERRORS{'WARNING'}, 0, "service could not be started because it does not exist: $service_name, output:\n" . join("\n", @$output));
 		return 0;
 	}
-	else {
-		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to to start service: $service_name");
+	elsif ($exit_status || grep(/could not be started/i, @$output)) {
+		notify($ERRORS{'WARNING'}, 0, "failed to start service: $service_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
 		return 0;
 	}
-
+	else {
+		notify($ERRORS{'OK'}, 0, "started service: $service_name" . join("\n", @$output));
+	}
 	return 1;
 } ## end sub start_service
 
@@ -7039,9 +6885,9 @@ sub start_service {
 
 =head2 stop_service
 
- Parameters  : 
- Returns     :
- Description : 
+ Parameters  : $service_name
+ Returns     : boolean
+ Description : Stops a service on the computer.
 
 =cut
 
@@ -7059,28 +6905,35 @@ sub stop_service {
 		notify($ERRORS{'WARNING'}, 0, "service name was not passed as an argument");
 		return;
 	}
-
+	
+	# The Client for NFS service should be controlled with the nfsadmin.exe utility
+	if ($service_name =~ /^NfsClnt$/) {
+		if ($self->stop_nfs_client_service()) {
+			return 1;
+		}
+	}
+	
 	my $command = $system32_path . '/net.exe stop "' . $service_name . '"';
-	my ($status, $output) = $self->execute($command);
-	if (defined($status) && $status == 0) {
-		notify($ERRORS{'OK'}, 0, "stopped service: $service_name");
+	my ($exit_status, $output) = $self->execute($command);
+	if (!defined($output)) {
+		notify($ERRORS{'WARNING'}, 0, "failed to execute command to to stop service: $service_name");
+		return;
 	}
-	elsif (defined($output) && grep(/is not started/i, @{$output})) {
+	elsif (grep(/is not started/i, @{$output})) {
 		notify($ERRORS{'OK'}, 0, "service is not started: $service_name");
+		return 1;
 	}
-	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 (grep(/(does not exist|service name is invalid)/i, @$output)) {
+		notify($ERRORS{'OK'}, 0, "service was not stopped because it does not exist: $service_name, output:\n" . join("\n", @$output));
+		return 1;
 	}
-	elsif (defined($status)) {
-		notify($ERRORS{'WARNING'}, 0, "unable to stop service: $service_name, exit status: $status, output:\n@{$output}");
+	elsif ($exit_status || grep(/could not be stopped/i, @$output)) {
+		notify($ERRORS{'WARNING'}, 0, "failed to stop service: $service_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
 		return 0;
 	}
 	else {
-		notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to to stop service: $service_name");
-		return 0;
+		notify($ERRORS{'OK'}, 0, "stopped service: $service_name" . join("\n", @$output));
 	}
-
 	return 1;
 } ## end sub stop_service
 
@@ -7101,7 +6954,7 @@ sub restart_service {
 		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
 		return;
 	}
-
+	
 	my $system32_path = $self->get_system32_path() || return;
 	
 	my $service_name = shift;
@@ -7110,21 +6963,8 @@ sub restart_service {
 		return;
 	}
 
-	my $command = "$system32_path/net.exe stop \"$service_name\" ; $system32_path/net.exe start \"$service_name\"";
-	my ($status, $output) = $self->execute($command, 1);
-	if (!defined($output)) {
-		notify($ERRORS{'WARNING'}, 0, "failed to run command to restart service: $service_name");
-		return;
-	}
-	elsif ($status == 0) {
-		notify($ERRORS{'OK'}, 0, "restarted service: $service_name, output:\n" . join("\n", @$output));
-	}
-	else {
-		notify($ERRORS{'WARNING'}, 0, "failed to restart service: $service_name, exit status: $status, output:\n" . join("\n", @$output));
-		return 0;
-	}
-
-	return 1;
+	$self->stop_service($service_name);
+	return $self->start_service($service_name);
 }
 
 #/////////////////////////////////////////////////////////////////////////////
@@ -7175,6 +7015,94 @@ sub service_exists {
 	return 1;
 }
 
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 start_nfs_client_service
+
+ Parameters  : none
+ Returns     : boolean
+ Description : Starts the Client for NFS (NfsClnt) service on the computer using
+               the nfsadmin.exe utility.
+
+=cut
+
+sub start_nfs_client_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 $system32_path = $self->get_system32_path() || return;
+
+	my $command = $system32_path . '/nfsadmin.exe client start';
+	my ($exit_status, $output) = $self->execute($command);
+	if (!defined($output)) {
+		notify($ERRORS{'WARNING'}, 0, "failed to execute command to to start Client for NFS service");
+		return;
+	}
+	elsif (grep(/already started/i, @$output)) {
+		notify($ERRORS{'OK'}, 0, "Client for NFS service is already started");
+		return 1;
+	}
+	elsif (grep(/no such file/i, @$output)) {
+		notify($ERRORS{'OK'}, 0, "failed to start Client for NFS service using nfsadmin.exe because utility does not exist on computer");
+		return;
+	}
+	elsif ($exit_status) {
+		notify($ERRORS{'WARNING'}, 0, "failed to start Client for NFS service, exit status: $exit_status, command: '$command', output:\n" . join("\n", @$output));
+		return 0;
+	}
+	else {
+		notify($ERRORS{'OK'}, 0, "started Client for NFS service, output:\n" . join("\n", @$output));
+	}
+	return 1;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 stop_nfs_client_service
+
+ Parameters  : none
+ Returns     : boolean
+ Description : Stops the Client for NFS (NfsClnt) service on the computer using
+               the nfsadmin.exe utility.
+
+=cut
+
+sub stop_nfs_client_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 $system32_path = $self->get_system32_path() || return;
+
+	my $command = $system32_path . '/nfsadmin.exe client stop';
+	my ($exit_status, $output) = $self->execute($command);
+	if (!defined($output)) {
+		notify($ERRORS{'WARNING'}, 0, "failed to execute command to to stop Client for NFS service");
+		return;
+	}
+	elsif (grep(/not been started/i, @$output)) {
+		notify($ERRORS{'OK'}, 0, "Client for NFS service is not started");
+		return 1;
+	}
+	elsif (grep(/no such file/i, @$output)) {
+		notify($ERRORS{'OK'}, 0, "failed to stop Client for NFS service using nfsadmin.exe because utility does not exist on computer");
+		return;
+	}
+	elsif ($exit_status) {
+		notify($ERRORS{'WARNING'}, 0, "failed to stop Client for NFS service, exit status: $exit_status, command: '$command', output:\n" . join("\n", @$output));
+		return 0;
+	}
+	else {
+		notify($ERRORS{'OK'}, 0, "stopped Client for NFS service, output:\n" . join("\n", @$output));
+	}
+	return 1;
+}
+
 #/////////////////////////////////////////////////////////////////////////////
 
 =head2 get_installed_applications