sub create_text_file()

in managementnode/lib/VCL/Module/OS.pm [2833:2964]


sub create_text_file {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my ($file_path, $file_contents_string, $append) = @_;
	if (!defined($file_path)) {
		notify($ERRORS{'WARNING'}, 0, "file path argument was not supplied");
		return;
	}
	elsif (!defined($file_contents_string)) {
		notify($ERRORS{'WARNING'}, 0, "file contents argument was not supplied");
		return;
	}
	
	my $computer_node_name = $self->data->get_computer_node_name();
	
	my $image_os_type = $self->data->get_image_os_type();
	my $newline;
	if ($image_os_type =~ /win/i) {
		$newline = "\r\n";
	}
	else {
		$newline = "\n";
	}
	
	# Used only to format notify messages
	my $mode_string;
	if ($append) {
		$mode_string = 'append';
		
		# Retrieve the contents of the existing file if necessary
		if ($self->file_exists($file_path)) {
			my $existing_file_contents = $self->get_file_contents($file_path);
			if (!defined($existing_file_contents)) {
				# Do not proceed if any problem occurred retrieving the existing file contents
				# Otherwise, it would be overwritten and data would be lost
				notify($ERRORS{'WARNING'}, 0, "failed to $mode_string text file on $computer_node_name, append argument was specified but contents of the existing file could not be retrieved: $file_path");
				return;
			}
			elsif ($existing_file_contents && $existing_file_contents !~ /\n$/) {
				# Add a newline to the end of the existing contents if one isn't present
				$existing_file_contents .= $newline;
				$file_contents_string = $existing_file_contents . $file_contents_string;
			}
			
		}
	}
	else {
		$mode_string = 'create';
	}

	# Make sure the file contents ends with a newline
	# This is helpful to prevent problems with files such as sshd_config and sudoers where a line might be echo'd to the end of it
	# Without the newline, the last line could wind up being a merged line if a simple echo is used to add a line
	if (length($file_contents_string) && $file_contents_string !~ /\n$/) {
		$file_contents_string .= $newline;
	}
	
	# Make line endings consistent
	$file_contents_string =~ s/\r*\n/$newline/g;

	# Attempt to create the parent directory if it does not exist
	if ($file_path =~ /[\\\/]/ && $self->can('create_directory')) {
		my $parent_directory_path = parent_directory_path($file_path);
		$self->create_directory($parent_directory_path) if $parent_directory_path;
	}
	
	# The echo method will fail of the file contents are too large
	# An "Argument list too long" error will be displayed
	my $file_contents_length = length($file_contents_string);
	if ($file_contents_length < 32000) {
		# Create a command to echo the string and another to echo the hex string to the file
		# The hex string command is preferred because it will handle special characters including single quotes
		# Use -e to enable interpretation of backslash escapes
		# Use -n to prevent trailing newline from being added
		# However, the command may become very long and fail
		# Convert the string to a string containing the hex value of each character
		# This is done to avoid problems with special characters in the file contents
		# Split the string up into an array if integers representing each character's ASCII decimal value
		my @decimal_values = unpack("C*", $file_contents_string);
		
		# Convert the ASCII decimal values into hex values and add '\x' before each hex value
		my @hex_values = map { '\x' . sprintf("%x", $_) } @decimal_values;
		
		# Join the hex values together into a string
		my $hex_string = join('', @hex_values);
		
		# Enclose the file path in quotes if it contains a space
		if ($file_path =~ /[\s]/) {
			$file_path = "\"$file_path\"";
		}
		
		# Attempt to create the file using the hex string
		my $command = "echo -n -e \"$hex_string\" > $file_path";
		my ($exit_status, $output) = $self->execute($command, 0, 15, 1);
		if (!defined($output)) {
			notify($ERRORS{'DEBUG'}, 0, "failed to execute command to $mode_string file on $computer_node_name, attempting to create file on management node and copy it to $computer_node_name");
		}
		elsif ($exit_status != 0 || grep(/^\w+:/i, @$output)) {
			notify($ERRORS{'WARNING'}, 0, "failed to execute command to $mode_string a file on $computer_node_name, command: '$command', exit status: $exit_status, output:\n" . join("\n", @$output) . "\nattempting to create file on management node and copy it to $computer_node_name");
		}
		elsif ($append) {
			notify($ERRORS{'DEBUG'}, 0, $mode_string . ($append ? 'ed' : 'd') . " text file on $computer_node_name: $file_path");
			return 1;
		}
		else {
			notify($ERRORS{'DEBUG'}, 0, $mode_string . ($append ? 'ed' : 'd') . " text file on $computer_node_name: $file_path");
			return 1;
		}
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "skipping attempt to $mode_string on $computer_node_name using echo, file content string length: $file_contents_length");
	}
	
	# File was not created using the quicker echo method
	# Create a temporary file on the management node, copy it to the computer, then delete temporary file from management node
	my $mn_temp_file_path = tmpnam();
	if (!$self->mn_os->create_text_file($mn_temp_file_path, $file_contents_string, $append)) {
		notify($ERRORS{'WARNING'}, 0, "failed to create text file on $computer_node_name, temporary file could not be created on the management node");
		return;
	}
	if (!$self->copy_file_to($mn_temp_file_path, $file_path)) {
		notify($ERRORS{'WARNING'}, 0, "failed to create text file, temporary file could not be dopied from management node to $computer_node_name: $mn_temp_file_path --> $file_path");
		return;
	}
	$self->mn_os->delete_file($mn_temp_file_path);
	notify($ERRORS{'DEBUG'}, 0, "created text file on $computer_node_name by copying a file created on management node");
	return 1;
}