| #!/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::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.5.1'; |
| |
| # 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_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 $is_server_request = $self->data->is_server_request(); |
| 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); |
| |
| # Make sure firewall object is initialized early to reduce time it takes to configure things after user clicks Connect |
| $self->os->firewall() if ($self->os->can('firewall')); |
| |
| # 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 |
| # Note: for server requests, this will always return true because the frontend inserts reservation.remoteIP when the reservation is made |
| 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_no_acknowledgement(); |
| $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'); |
| |
| # For non-server requests, the frontend should have inserted an 'initialconnecttimeout' computerloadlog entry for the parent reservation when the user clicks Connect |
| # 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 |
| my $connection_check_start_epoch_seconds; |
| if ($is_server_request) { |
| $connection_check_start_epoch_seconds = time; |
| insertloadlog($parent_reservation_id, $computer_id, "initialconnecttimeout", "begin initial connection timeout ($initial_connect_timeout_seconds seconds)"); |
| } |
| else { |
| $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 { |
| notify($ERRORS{'DEBUG'}, 0, "could not retrieve timestamp of computerloadlog 'initialconnecttimeout' entry, web frontend should have inserted this, inserting new entry"); |
| $connection_check_start_epoch_seconds = time; |
| 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"); |
| } |
| |
| # Create a JSON file containing the reservation info |
| my $enable_experimental_features = get_variable('enable_experimental_features', 0); |
| if ($enable_experimental_features) { |
| $self->os->create_reservation_info_json_file(); |
| } |
| |
| # 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 ($is_server_request) { |
| notify($ERRORS{'OK'}, 0, "never detected user connection, skipping timeout, server reservation"); |
| } |
| elsif (is_request_deleted($request_id) || $self->request_state_changed()) { |
| $self->state_exit(); |
| } |
| else { |
| $self->notify_user_timeout_no_initial_connection(); |
| $self->state_exit('timeout', 'reserved', 'nologin'); |
| } |
| } |
| |
| # Add a line to currentimage.txt indicating it's possible a user logged on to the computer |
| $self->os->set_tainted_status('user may have logged in'); |
| |
| # 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->can('firewall') && $self->os->firewall->can('process_inuse')) { |
| $self->os->firewall->process_inuse($remote_ip); |
| } |
| else { |
| if (!$self->os->process_connect_methods($remote_ip, 1)) { |
| notify($ERRORS{'CRITICAL'}, 0, "failed to process connect methods after user connected to computer"); |
| } |
| } |
| |
| # Perform steps after a user makes an initial connection |
| $self->os->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 the request state changed for any reason |
| # This will occur if the user deletes the request or makeproduction is initiated before the user acknowledges |
| if ($self->request_state_changed()) { |
| $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_state_name = $self->data->get_request_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() || 'none';; |
| my $user_im_id = $self->data->get_user_im_id(); |
| my $affiliation_helpaddress = $self->data->get_user_affiliation_helpaddress(); |
| my $is_parent_reservation = $self->data->is_parent_reservation(); |
| |
| my $user_message_key; |
| if ($request_state_name =~ /^(reinstall)$/) { |
| $user_message_key = 'reinstalled'; |
| } |
| else { |
| $user_message_key = 'reserved'; |
| } |
| |
| my ($subject, $message) = $self->get_user_message($user_message_key); |
| if (!defined($subject) || !defined($message)) { |
| return; |
| } |
| |
| if ($is_parent_reservation && $user_emailnotices) { |
| mail($user_email, $subject, $message, $affiliation_helpaddress); |
| } |
| else { |
| notify($ERRORS{'MAILMASTERS'}, 0, "$user_email\n$message"); |
| } |
| |
| if ($user_imtype_name ne "none") { |
| notify_via_im($user_imtype_name, $user_im_id, $message, $affiliation_helpaddress); |
| } |
| |
| return 1; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 notify_user_timeout_no_initial_connection |
| |
| Parameters : none |
| Returns : boolean |
| Description : Notifies the user that the request has timed out because no |
| initial connection was made. An e-mail and/or IM message will |
| be sent to the user. |
| |
| =cut |
| |
| sub notify_user_timeout_no_initial_connection { |
| my $self = shift; |
| |
| 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() || 'none';; |
| my $user_im_id = $self->data->get_user_im_id(); |
| my $affiliation_helpaddress = $self->data->get_user_affiliation_helpaddress(); |
| my $is_parent_reservation = $self->data->is_parent_reservation(); |
| |
| my $user_message_key = 'timeout_no_initial_connection'; |
| my ($subject, $message) = $self->get_user_message($user_message_key); |
| if (!defined($subject) || !defined($message)) { |
| return; |
| } |
| |
| if ($is_parent_reservation && $user_emailnotices) { |
| mail($user_email, $subject, $message, $affiliation_helpaddress); |
| } |
| if ($user_im_name ne "none") { |
| notify_via_im($user_im_name, $user_im_id, $message); |
| } |
| |
| return 1; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 notify_user_timeout_no_acknowledgement |
| |
| Parameters : none |
| Returns : boolean |
| Description : Notifies the user that the request has timed out because no |
| initial connection was made. An e-mail and/or IM message will |
| be sent to the user. |
| |
| =cut |
| |
| sub notify_user_timeout_no_acknowledgement { |
| my $self = shift; |
| |
| 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() || 'none';; |
| my $user_im_id = $self->data->get_user_im_id(); |
| my $affiliation_helpaddress = $self->data->get_user_affiliation_helpaddress(); |
| my $is_parent_reservation = $self->data->is_parent_reservation(); |
| |
| my $user_message_key = 'timeout_no_acknowledgement'; |
| my ($subject, $message) = $self->get_user_message($user_message_key); |
| if (!defined($subject) || !defined($message)) { |
| return; |
| } |
| |
| if ($is_parent_reservation && $user_emailnotices) { |
| mail($user_email, $subject, $message, $affiliation_helpaddress); |
| } |
| if ($user_im_name ne "none") { |
| notify_via_im($user_im_name, $user_im_id, $message); |
| } |
| |
| return 1; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| 1; |
| __END__ |
| |
| =head1 SEE ALSO |
| |
| L<http://cwiki.apache.org/VCL/> |
| |
| =cut |