#!/usr/bin/perl -w
###############################################################################
# $Id: reserved.pm 1670477 2015-03-31 20:48:08Z arkurth $
###############################################################################
# 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::reserved - Perl module for the VCL reserved state

=head1 SYNOPSIS

 use VCL::reserved;
 use VCL::utils;

 # Set variables containing the IDs of the request and reservation
 my $request_id = 5;
 my $reservation_id = 6;

 # Call the VCL::utils::get_request_info subroutine to populate a hash
 my $request_info = get_request_info($request_id);

 # Set the reservation ID in the hash
 $request_info->{RESERVATIONID} = $reservation_id;

 # Create a new VCL::reserved object based on the request information
 my $reserved = VCL::reserved->new($request_info);

=head1 DESCRIPTION

 This module supports the VCL "reserved" state. The reserved state is reached
 after a computer has been loaded. This module checks if the user has
 acknowledged the reservation by clicking the Connect button and has connected
 to the computer. Once connected, the reservation will be put into the "inuse"
 state and the reserved process exits.

=cut

##############################################################################
package VCL::reserved;

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

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

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

# Specify the version of Perl to use
use 5.008000;

use strict;
use warnings;
use diagnostics;

use VCL::utils;
use POSIX qw(strftime);

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

=head1 OBJECT METHODS

=cut

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

=head2 process

 Parameters  : none
 Returns     : exits
 Description : Processes a reservation in the reserved state. Waits for user
               acknowledgement and connection.

=cut

sub process {
	my $self = shift;
	
	my $request_id                      = $self->data->get_request_id();
	my $request_data                    = $self->data->get_request_data();
	my $request_logid                   = $self->data->get_request_log_id();
	my $request_checkuser               = $self->data->get_request_checkuser();
	my $reservation_id                  = $self->data->get_reservation_id();
	my $reservation_count               = $self->data->get_reservation_count();
	my $computer_id                     = $self->data->get_computer_id();
	my $computer_short_name             = $self->data->get_computer_short_name();
	my $is_parent_reservation           = $self->data->is_parent_reservation();
	my $parent_reservation_id           = $self->data->get_parent_reservation_id();
	my $server_request_id               = $self->data->get_server_request_id();
	my $imagemeta_checkuser             = $self->data->get_imagemeta_checkuser();
	
	my $acknowledge_timeout_seconds     = $self->os->get_timings('acknowledgetimeout');
	my $initial_connect_timeout_seconds = $self->os->get_timings('initialconnecttimeout');
	
	# Update the log loaded time to now for this request
	update_log_loaded_time($request_logid);
	
	# Update the computer state to reserved
	# This causes pending to change to the Connect button on the Current Reservations page
	update_computer_state($computer_id, 'reserved');
	insertloadlog($reservation_id, $computer_id, "reserved", "$computer_short_name successfully reserved");
	
	
	if ($is_parent_reservation) {
		# Send an email and/or IM to the user
		# Do this after updating the computer state to reserved because this is when the Connect button appears
		$self->_notify_user_ready();
		
		# Insert acknowledgetimeout immediately before beginning to check user clicked Connect
		# Web uses timestamp of this to determine when next to refresh the page
		# Important because page should refresh as soon as possible to reservation timing out
		insertloadlog($reservation_id, $computer_id, "acknowledgetimeout", "begin acknowledge timeout ($acknowledge_timeout_seconds seconds)");
	}
	
	my $acknowledge_check_start_epoch_seconds = $self->wait_for_reservation_loadstate($parent_reservation_id, "acknowledgetimeout", $acknowledge_timeout_seconds, 5);
	if (!$acknowledge_check_start_epoch_seconds) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve timestamp of parent reservation $parent_reservation_id 'acknowledgetimeout' computerloadlog entry");
		return;
	}
	
	# Get the current time
	my $now_epoch_seconds = time;
	
	# Calculate the exact time when connection checking should end
	my $acknowledge_check_end_epoch_seconds = ($acknowledge_check_start_epoch_seconds + $acknowledge_timeout_seconds);
	my $acknowledge_timeout_remaining_seconds = ($acknowledge_check_end_epoch_seconds - $now_epoch_seconds);
	
	my $now_string                           = strftime('%H:%M:%S', localtime($now_epoch_seconds));
	my $acknowledge_check_start_string       = strftime('%H:%M:%S', localtime($acknowledge_check_start_epoch_seconds));
	my $acknowledge_check_end_string         = strftime('%H:%M:%S', localtime($acknowledge_check_end_epoch_seconds));
	my $acknowledge_timeout_string           = strftime('%H:%M:%S', gmtime($acknowledge_timeout_seconds));
	my $acknowledge_timeout_remaining_string = strftime('%H:%M:%S', gmtime($acknowledge_timeout_remaining_seconds));
	
	notify($ERRORS{'DEBUG'}, 0, "beginning to check for user acknowledgement:\n" .
		"acknowledge check start   :   $acknowledge_check_start_string\n" .
		"acknowledge timeout total : + $acknowledge_timeout_string\n" .
		"--------------------------------------\n" .
		"acknowledge check end     : = $acknowledge_check_end_string\n" .
		"current time              : - $now_string\n" .
		"--------------------------------------\n" .
		"acknowledge timeout remaining : = $acknowledge_timeout_remaining_string ($acknowledge_timeout_remaining_seconds seconds)\n"
	);
	
	# Wait for the user to acknowledge the request by clicking Connect button or from API
	my $user_acknowledged = $self->code_loop_timeout(sub{$self->user_acknowledged()}, [], 'waiting for user acknowledgement', $acknowledge_timeout_remaining_seconds, 1, 10);
	if (!$user_acknowledged) {
		$self->_notify_user_timeout($request_data);
		$self->state_exit('timeout', 'available', 'noack');
	}
	
	# Add noinitialconnection and then delete acknowledgetimeout
	insertloadlog($reservation_id, $computer_id, "noinitialconnection", "user clicked Connect");
	delete_computerloadlog_reservation($reservation_id, 'acknowledgetimeout');
	
	# The frontend should have inserted an 'initialconnecttimeout' computerloadlog entry for the parent reservation, retrieve its timestamp
	my $connection_check_start_epoch_seconds = get_reservation_computerloadlog_time($parent_reservation_id, 'initialconnecttimeout');
	if ($connection_check_start_epoch_seconds) {
		notify($ERRORS{'DEBUG'}, 0, "retrieved timestamp of computerloadlog 'initialconnecttimeout' entry inserted by web frontend: $connection_check_start_epoch_seconds");
	}
	else {
		$connection_check_start_epoch_seconds = time;
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve timestamp of computerloadlog 'initialconnecttimeout' entry, web frontend should have inserted this, inserting new entry");
		
		# initialconnecttimeout should be inserted immediately after user acknowledged
		# Web uses timestamp of this to determine when next to refresh the page
		# The timestamp of this computerloadlog entry will be used to determine when to timeout the connection checking during the inuse state
		insertloadlog($reservation_id, $computer_id, "initialconnecttimeout", "begin initial connection timeout ($initial_connect_timeout_seconds seconds)");
	}
	
	# Call OS module's grant_access() subroutine which adds user accounts to computer
	if ($self->os->can("grant_access") && !$self->os->grant_access()) {
		$self->reservation_failed("OS module grant_access failed");
	}
	
	# User acknowledged request
	# Add the cluster information to the loaded computers if this is a cluster reservation
	if ($reservation_count > 1 && !$self->os->update_cluster()) {
		$self->reservation_failed("update_cluster failed");
	}
	
	# Check if OS module's post_reserve() subroutine exists
	if ($self->os->can("post_reserve") && !$self->os->post_reserve()) {
		$self->reservation_failed("OS module post_reserve failed");
	}
	
	# Add a 'postreserve' computerloadlog entry
	# Do this last - important for cluster reservation timing
	# Parent's reserved process will loop until this exists for all child reservations
	insertloadlog($reservation_id, $computer_id, "postreserve", "$computer_short_name post reserve successful");
	
	# Get the current time
	$now_epoch_seconds = time;
	
	# Calculate the exact time when connection checking should end
	my $connection_check_end_epoch_seconds = ($connection_check_start_epoch_seconds + $initial_connect_timeout_seconds);
	my $connect_timeout_remaining_seconds = ($connection_check_end_epoch_seconds - $now_epoch_seconds);
	
	$now_string                       = strftime('%H:%M:%S', localtime($now_epoch_seconds));
	my $connection_check_start_string    = strftime('%H:%M:%S', localtime($connection_check_start_epoch_seconds));
	my $connection_check_end_string      = strftime('%H:%M:%S', localtime($connection_check_end_epoch_seconds));
	my $connect_timeout_string           = strftime('%H:%M:%S', gmtime($initial_connect_timeout_seconds));
	my $connect_timeout_remaining_string = strftime('%H:%M:%S', gmtime($connect_timeout_remaining_seconds));
	
	notify($ERRORS{'DEBUG'}, 0, "beginning to check for initial user connection:\n" .
		"connection check start    :   $connection_check_start_string\n" .
		"connect timeout total     : + $connect_timeout_string\n" .
		"--------------------------------------\n" .
		"connection check end      : = $connection_check_end_string\n" .
		"current time              : - $now_string\n" .
		"--------------------------------------\n" .
		"connect timeout remaining : = $connect_timeout_remaining_string ($connect_timeout_remaining_seconds seconds)\n"
	);
	
	# Check to see if user is connected. user_connected will true(1) for servers and requests > 24 hours
	my $user_connected = $self->code_loop_timeout(sub{$self->user_connected()}, [], "waiting for initial user connection to $computer_short_name", $connect_timeout_remaining_seconds, 15);
	
	# Delete the connecttimeout immediately after acknowledgement loop ends
	delete_computerloadlog_reservation($reservation_id, 'connecttimeout');
	
	if (!$user_connected) {
		if (!$imagemeta_checkuser || !$request_checkuser) {
			notify($ERRORS{'OK'}, 0, "never detected user connection, skipping timeout, imagemeta checkuser: $imagemeta_checkuser, request checkuser: $request_checkuser");
		}
		elsif ($server_request_id) {
			notify($ERRORS{'OK'}, 0, "never detected user connection, skipping timeout, server reservation");
		}
		elsif (is_request_deleted($request_id)) {
			$self->state_exit();
		}
		else {
			$self->_notify_user_no_login();
			$self->state_exit('timeout', 'reserved', 'nologin');
		}
	}
	
	# Update reservation lastcheck, otherwise inuse request will be processed immediately again
	update_reservation_lastcheck($reservation_id);
	
	# Tighten up the firewall
	# Process the connect methods again, lock the firewall down to the address the user connected from
	my $remote_ip = $self->data->get_reservation_remote_ip();
	if (!$self->os->process_connect_methods($remote_ip, 1)) {
		notify($ERRORS{'CRITICAL'}, 0, "failed to process connect methods after user connected to computer");
	}
	
	# Run custom post_initial_connection scripts residing on the management node
	$self->os->run_management_node_tools_scripts('post_initial_connection');
	
	# For cluster reservations, the parent must wait until all child reserved processes have exited
	# Otherwise, the state will change to inuse while the child processes are still finishing up the reserved state
	# vcld will then fail to fork inuse processes for the child reservations
	if ($reservation_count > 1 && $is_parent_reservation) {
		if (!$self->code_loop_timeout(sub{$self->wait_for_child_reservations()}, [], "waiting for child reservation reserved processes to complete", 360, 5)) {
			$self->reservation_failed('all child reservation reserved processes did not complete');
		}
		
		# Parent can't tell if reserved processes on other management nodes have terminated
		# Wait a short time in case processes on other management nodes are terminating
		sleep 3;
	}
	
	# Change the request and computer state to inuse then exit
	$self->state_exit('inuse', 'inuse');
} ## end sub process

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

=head2 wait_for_child_reservations

 Parameters  : none
 Returns     : boolean
 Description : Checks if all child reservation 'reserved' processes have
               completed.

=cut

sub wait_for_child_reservations {
	my $self = shift;
	my $request_id = $self->data->get_request_id();
	
	exit if is_request_deleted($request_id);
	
	# Check if 'reserved' computerloadlog entry exists for all reservations
	my $request_loadstate_names = get_request_loadstate_names($request_id);
	if (!$request_loadstate_names) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve request loadstate names");
		return;
	}
	
	my @reserved_exists;
	my @reserved_does_not_exist;
	my @failed;
	for my $reservation_id (keys %$request_loadstate_names) {
		my @loadstate_names = @{$request_loadstate_names->{$reservation_id}};
		if (grep { $_ eq 'postreserve' } @loadstate_names) {
			push @reserved_exists, $reservation_id;
		}
		else {
			push @reserved_does_not_exist, $reservation_id;
		}
		
		if (grep { $_ eq 'failed' } @loadstate_names) {
			push @failed, $reservation_id;
		}
	}
	
	# Check if any child reservations failed
	if (@failed) {
		$self->reservation_failed("child reservation reserve process failed: " . join(', ', @failed));
		return;
	}
	
	if (@reserved_does_not_exist) {
		notify($ERRORS{'DEBUG'}, 0, "computerloadlog 'postreserve' entry does NOT exist for all reservations:\n" .
			"exists for reservation IDs: " . join(', ', @reserved_exists) . "\n" .
			"does not exist for reservation IDs: " . join(', ', @reserved_does_not_exist)
		);
		return 0;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "computerloadlog 'postreserve' entry exists for all reservations");
	}
	
	notify($ERRORS{'DEBUG'}, 0, "all child reservation reserved processes have completed");
	return 1;
}

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

=head2 user_acknowledged

 Parameters  : none
 Returns     : boolean
 Description : Used as a helper function to the call to code_loop_timeout() in
               process. First checks if the request has been deleted. If so, the
               process exits. If not deleted, checks if the user has
               acknowledged the request by checking if reservation.remoteip is
               set.

=cut

sub user_acknowledged {
	my $self = shift;
	if (ref($self) !~ /VCL::reserved/) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine can only be called as a class method of a VCL::reserved object");
		return;
	}
	
	my $request_id = $self->data->get_request_id();
	
	# Check if user deleted the request
	if (is_request_deleted($request_id)) {
		notify($ERRORS{'DEBUG'}, 0, "request deleted, exiting");
		$self->state_exit();
	}
	
	my $remote_ip = $self->data->get_reservation_remote_ip();
	if ($remote_ip) {
		notify($ERRORS{'DEBUG'}, 0, "user acknowledged from remote IP address: $remote_ip");
		return 1;
	}
	else {
		return 0;
	}
}

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

=head2 _notify_user_ready

 Parameters  : none
 Returns     : boolean
 Description : Notifies the user that the reservation is ready.

=cut

sub _notify_user_ready {
	my $self = shift;
	
	#my $request_id                 = $self->data->get_request_id();
	my $request_state_name         = $self->data->get_request_id();
	#my $reservation_id             = $self->data->get_reservation_id();
	my $user_email                 = $self->data->get_user_email();
	my $user_emailnotices          = $self->data->get_user_emailnotices();
	my $user_imtype_name               = $self->data->get_user_imtype_name();
	my $user_im_id                 = $self->data->get_user_im_id();
	my $affiliation_sitewwwaddress = $self->data->get_user_affiliation_sitewwwaddress();
	my $affiliation_helpaddress    = $self->data->get_user_affiliation_helpaddress();
	my $image_prettyname           = $self->data->get_image_prettyname();
	my $is_parent_reservation      = $self->data->is_parent_reservation();

	my $mailstring;
	my $subject;
	
	# Assemble the message body reservations
	if ($request_state_name =~ /^(reinstall)$/) {
		$subject = "VCL -- $image_prettyname reservation reinstalled";
		
		$mailstring = <<"EOF";
Your reservation was successfully reinstalled and you can proceed to reconnect. 
Please revisit the 'Current Reservations' page for any additional information.
EOF
	}
	else {
		$subject = "VCL -- $image_prettyname reservation";
		
		$mailstring = <<"EOF";
The resources for your VCL reservation have been successfully reserved.
Connection will not be allowed until you click the 'Connect' button on the 'Current Reservations' page.
You must acknowledge the reservation within the next 15 minutes or the resources will be reclaimed for other VCL users.

-Visit $affiliation_sitewwwaddress
-Select "Current Reservations"
-Click the "Connect" button
Upon acknowledgement, all of the remaining connection details will be displayed.
EOF
	}
	
	$mailstring .= <<"EOF";

Thank You,
VCL Team

******************************************************************
This is an automated notice. If you need assistance please respond 
with detailed information on the issue and a help ticket will be 
generated.

To disable email notices
-Visit $affiliation_sitewwwaddress
-Select User Preferences
-Select General Preferences

******************************************************************
EOF
	
	if ($is_parent_reservation && $user_emailnotices) {
		mail($user_email, $subject, $mailstring, $affiliation_helpaddress);
	}
	else {
		# For email record keeping
		notify($ERRORS{'MAILMASTERS'}, 0, " $user_email\n$mailstring");
	}
	
	if ($user_imtype_name ne "none") {
		notify_via_im($user_imtype_name, $user_im_id, $mailstring, $affiliation_helpaddress);
	}
	
	return 1;
} ## end sub _notify_user_no_login

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

=head2 _notify_user_no_login

 Parameters  : none
 Returns     : boolean
 Description : Notifies the user that the request has timed out becuase no
               initial connection was made. An e-mail and/or IM message will
               be sent to the user.

=cut

sub _notify_user_no_login {
	my $self = shift;
	
	my $request_id                 = $self->data->get_request_id();
	my $reservation_id             = $self->data->get_reservation_id();
	my $user_email                 = $self->data->get_user_email();
	my $user_emailnotices          = $self->data->get_user_emailnotices();
	my $user_im_name               = $self->data->get_user_imtype_name();
	my $user_im_id                 = $self->data->get_user_im_id();
	my $affiliation_sitewwwaddress = $self->data->get_user_affiliation_sitewwwaddress();
	my $affiliation_helpaddress    = $self->data->get_user_affiliation_helpaddress();
	my $image_prettyname           = $self->data->get_image_prettyname();
	my $is_parent_reservation      = $self->data->is_parent_reservation();

	my $message = <<"EOF";

Your reservation has timed out for image $image_prettyname because no initial connection was made.

To make another reservation, please revisit $affiliation_sitewwwaddress.

Thank You,
VCL Team


******************************************************************
This is an automated notice. If you need assistance
please respond with detailed information on the issue
and a help ticket will be generated.

To disable email notices
-Visit $affiliation_sitewwwaddress
-Select User Preferences
-Select General Preferences
******************************************************************
EOF

	my $subject = "VCL -- Reservation Timeout";

	if ($is_parent_reservation && $user_emailnotices) {
		#if  "0" user does not care to get additional notices
		mail($user_email, $subject, $message, $affiliation_helpaddress);
		notify($ERRORS{'OK'}, 0, "sent reservation timeout e-mail to $user_email");
	}
	if ($user_im_name ne "none") {
		notify_via_im($user_im_name, $user_im_id, $message);
		notify($ERRORS{'OK'}, 0, "sent reservation timeout IM to $user_im_name");
	}
	return 1;
} ## end sub _notify_user_no_login

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

=head2 _notify_user_timeout

 Parameters  : none
 Returns     : boolean
 Description : Notifies the user that the request has timed out becuase no
               initial connection was made. An e-mail and/or IM message will
               be sent to the user.

=cut

sub _notify_user_timeout {
	my $self = shift;
	
	my $request_id                 = $self->data->get_request_id();
	my $reservation_id             = $self->data->get_reservation_id();
	my $user_email                 = $self->data->get_user_email();
	my $user_emailnotices          = $self->data->get_user_emailnotices();
	my $user_im_name               = $self->data->get_user_imtype_name();
	my $user_im_id                 = $self->data->get_user_im_id();
	my $affiliation_sitewwwaddress = $self->data->get_user_affiliation_sitewwwaddress();
	my $affiliation_helpaddress    = $self->data->get_user_affiliation_helpaddress();
	my $image_prettyname           = $self->data->get_image_prettyname();
	my $is_parent_reservation      = $self->data->is_parent_reservation();

	my $message = <<"EOF";

Your reservation has timed out for image $image_prettyname because no initial connection was made.

To make another reservation, please revisit $affiliation_sitewwwaddress.

Thank You,
VCL Team


******************************************************************
This is an automated notice. If you need assistance
please respond with detailed information on the issue
and a help ticket will be generated.

To disable email notices
-Visit $affiliation_sitewwwaddress
-Select User Preferences
-Select General Preferences
******************************************************************
EOF

	my $subject = "VCL -- Reservation Timeout";

	if ($is_parent_reservation && $user_emailnotices) {
		#if  "0" user does not care to get additional notices
		mail($user_email, $subject, $message, $affiliation_helpaddress);
		notify($ERRORS{'OK'}, 0, "sent reservation timeout e-mail to $user_email");
	}
	if ($user_im_name ne "none") {
		notify_via_im($user_im_name, $user_im_id, $message);
		notify($ERRORS{'OK'}, 0, "sent reservation timeout IM to $user_im_name");
	}
	return 1;
} ## end sub _notify_user_timeout

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

1;
__END__

=head1 SEE ALSO

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

=cut
