managementnode/lib/VCL/Module/OS/Linux/ManagementNode.pm (527 lines of code) (raw):

#!/usr/bin/perl -w ############################################################################### # $Id$ ############################################################################### # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ############################################################################### =head1 NAME VCL::Module::OS::Linux::ManagementNode.pm =head1 SYNOPSIS Needs to be written =head1 DESCRIPTION This module provides VCL support for the management node's Linux operating system. =cut ############################################################################### package VCL::Module::OS::Linux::ManagementNode; # Specify the lib path using FindBin use FindBin; use lib "$FindBin::Bin/../../../.."; # Configure inheritance use base qw(VCL::Module::OS::Linux); # 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 Crypt::CBC; use Crypt::OpenSSL::RSA; use Crypt::Rijndael; use English; use File::Basename; use MIME::Base64; ############################################################################### =head1 CLASS VARIABLES =cut =head2 $MN_STAGE_SCRIPTS_DIRECTORY Data type : String Description : Location on the management node where scripts reside which are executed on the management node at various stages of a reservation. Example: /usr/local/vcl/tools/ManagementNode/Scripts =cut our $MN_STAGE_SCRIPTS_DIRECTORY = "$TOOLS/ManagementNode/Scripts"; =head2 $MN_PRIVATE_ENCRYPTION_KEY_FILE_PATH Data type : String Description : /root/.vcl/<FQDN>.key =cut our $MN_PRIVATE_ENCRYPTION_KEY_FILE_PATH = "/root/.vcl/$FQDN.key"; ############################################################################### =head1 OBJECT METHODS =cut #////////////////////////////////////////////////////////////////////////////// =head2 initialize Parameters : Returns : Description : =cut sub initialize { my $self = shift; unless (ref($self) && $self->isa('VCL::Module')) { notify($ERRORS{'CRITICAL'}, 0, "subroutine can only be called as an object method"); return; } my $management_node_hostname = $self->data->get_management_node_hostname() || return; my $management_node_short_name = $self->data->get_management_node_short_name() || return; my $management_node_ip_address = $self->data->get_management_node_ipaddress() || return; $self->data->set_computer_id(0); $self->data->set_computer_hostname($management_node_hostname); $self->data->set_computer_node_name($management_node_short_name); $self->data->set_computer_short_name($management_node_short_name); $self->data->set_computer_public_ip_address($management_node_ip_address); # TODO: remove all use of management node private IP address my $management_node_private_ip_address = hostname_to_ip_address($management_node_hostname); if ($management_node_private_ip_address) { $self->data->set_computer_private_ip_address($management_node_private_ip_address); } else { notify($ERRORS{'WARNING'}, 0, "failed to initialize management node private IP address in DataStructure object, unable to resolve hostname '$management_node_hostname'"); } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 execute Parameters : $command, $display_output (optional), $timeout_seconds (optional) Returns : array Description : =cut sub execute { my ($argument) = @_; my ($command, $display_output, $timeout_seconds); # Check if this subroutine was called as an object method if (ref($argument) && ref($argument) =~ /VCL::Module/) { # Subroutine was called as an object method ($self->execute) my $self = shift; ($argument) = @_; } # Check the argument type if (ref($argument)) { if (ref($argument) eq 'HASH') { $command = $argument->{command}; $display_output = $argument->{display_output}; $timeout_seconds = $argument->{timeout_seconds}; } else { notify($ERRORS{'WARNING'}, 0, "invalid argument reference type passed: " . ref($argument) . ", if a reference is passed as the argument it may only be a hash or VCL::Module reference"); return; } } else { # Argument is not a reference, get the remaining arguments ($command, $display_output, $timeout_seconds) = @_; } # Run the command my ($exit_status, $output) = run_command($command, 1, $timeout_seconds); if (defined($exit_status) && defined($output)) { if ($display_output) { notify($ERRORS{'OK'}, 0, "executed command: '$command', exit status: $exit_status, output:\n" . join("\n", @$output)) if $display_output; } return ($exit_status, $output); } else { notify($ERRORS{'WARNING'}, 0, "failed to run command on management node: $command"); return; } } #////////////////////////////////////////////////////////////////////////////// =head2 copy_file_to Parameters : $source, $destination Returns : array Description : Copies file(s) from the management node to the Linux computer. =cut sub copy_file_to { 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; } # Get the source and destination arguments my ($source, $destination) = @_; if (!$source || !$destination) { notify($ERRORS{'WARNING'}, 0, "source and destination arguments were not specified"); return; } $destination =~ s/.*://g; return $self->copy_file($source, $destination); } #////////////////////////////////////////////////////////////////////////////// =head2 create_text_file Parameters : $file_path, $file_contents_string, $append (optional) Returns : boolean Description : Creates a text file on the management node. =cut 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(); # Attempt to create the parent directory if it does not exist my $parent_directory_path = parent_directory_path($file_path); if (!$self->file_exists($parent_directory_path)) { $self->create_directory($parent_directory_path); } my $mode; my $mode_string; if ($append) { $mode = '>>'; $mode_string = 'append'; } else { $mode = '>'; $mode_string = 'create'; } if (!open FILE, $mode, $file_path) { notify($ERRORS{'WARNING'}, 0, "failed to $mode_string text file on $computer_node_name, file path could not be opened: $file_path"); return; } if (!print FILE $file_contents_string) { close FILE; notify($ERRORS{'WARNING'}, 0, "failed to $mode_string text file on $computer_node_name: $file_path, contents could not be written to the file"); return; } close FILE; notify($ERRORS{'DEBUG'}, 0, $mode_string . ($append ? 'ed' : 'd') . " text file on $computer_node_name: $file_path"); return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 get_file_contents Parameters : $file_path, $display_warnings (optional) Returns : array or string Description : Retrieves the contents of a file on the management node. =cut sub get_file_contents { 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, $display_warnings) = @_; if (!defined($file_path)) { notify($ERRORS{'WARNING'}, 0, "file path argument was not supplied"); return; } $display_warnings = 1 unless defined($display_warnings); my $computer_node_name = $self->data->get_computer_node_name(); if (!open FILE, '<', $file_path) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve contents of file on $computer_node_name, file could not be opened: $file_path") if $display_warnings; return; } my @lines = <FILE>; close FILE; my $line_count = scalar(@lines); notify($ERRORS{'DEBUG'}, 0, "retrieved contents of file on $computer_node_name: $file_path ($line_count lines)"); if (wantarray) { map { s/[\r\n]+$//g; } (@lines); return @lines; } else { return join('', @lines); } } #////////////////////////////////////////////////////////////////////////////// =head2 get_management_node_reservation_info_json_file_path Parameters : none Returns : string Description : Returns the location where the files resides on the management node that contains JSON formatted information about the reservation. For Linux computers, the location is: /tmp/<reservation ID>.json. =cut sub get_management_node_reservation_info_json_file_path { my $self = shift; if (ref($self) !~ /VCL::Module::OS/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $reservation_id = $self->data->get_reservation_id(); return "/tmp/$reservation_id.json"; } #////////////////////////////////////////////////////////////////////////////// =head2 create_management_node_reservation_info_json_file Parameters : none Returns : boolean Description : Creates a text file on the the management node containing reservation data in JSON format. =cut sub create_management_node_reservation_info_json_file { my $self = shift; if (ref($self) !~ /VCL::Module::OS/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $json_file_path = $self->get_management_node_reservation_info_json_file_path(); # IMPORTANT: Use $self->os->data here to retrieve DataStructure info for the computer being loaded # If $self->data->get_reservation_info_json_string is used, the computer info will be that of the management node, not the computer being loaded my $json_string = $self->os->data->get_reservation_info_json_string() || return; return $self->create_text_file($json_file_path, $json_string); } #////////////////////////////////////////////////////////////////////////////// =head2 delete_management_node_reservation_info_json_file Parameters : none Returns : boolean Description : Deletes the text file on the management node containing reservation data in JSON format. =cut sub delete_management_node_reservation_info_json_file { my $self = shift; if (ref($self) !~ /VCL::Module::OS/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $json_file_path = $self->get_management_node_reservation_info_json_file_path(); return $self->delete_file($json_file_path); } #////////////////////////////////////////////////////////////////////////////// =head2 run_stage_scripts_on_management_node Parameters : $stage Returns : boolean Description : Runs scripts on the management node intended for the state specified by the argument. This is useful if you need to configure something such as a storage unit or firewall device specifically for each reservation. The stage argument may be any of the following: * post_capture * post_initial_connection * post_load * post_reservation * post_reserve * pre_capture * pre_reload The scripts are stored on the management node under: /usr/local/vcl/tools/ManagementNode/Scripts No scripts exist by default. When the vcld process reaches the stage specified by the argument, it will check the subdirectory with a name that matches the stage name. For example: /usr/local/vcl/tools/ManagementNode/Scripts/post_capture It will attempt to execute any files under this directory. Prior to executing the scripts, a JSON file is created under /tmp with information regarding the reservation. The actual file path will be: /tmp/<reservation ID>.json Information about the reservation can be retrieved within the script by simply using grep or using something to parse JSON such as jsawk. Sample script: JSON_FILE="$1" echo "JSON file: ${JSON_FILE}" PRIVATE_IP=`cat ${JSON_FILE} | jsawk 'return this.computer.privateIPaddress'` echo "computer private IP: ${PRIVATE_IP}" =cut sub run_stage_scripts_on_management_node { my $self = shift; if (ref($self) !~ /VCL::/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the stage argument my $stage = shift; if (!$stage) { notify($ERRORS{'WARNING'}, 0, "stage argument was not supplied"); return; } my $management_node_stages = { 'post_capture' => 1, 'post_initial_connection' => 1, 'post_load' => 1, 'post_reservation' => 1, 'post_reserve' => 1, 'pre_capture' => 1, 'pre_reload' => 1, }; if (!defined($management_node_stages->{$stage})) { notify($ERRORS{'WARNING'}, 0, "invalid stage argument was supplied: $stage"); return; } elsif (!$management_node_stages->{$stage}) { # Note: Not currently used, could someday if a particular stage is defined for computer scripts but not MN scripts notify($ERRORS{'DEBUG'}, 0, "'$stage' stage scripts are not supported to be run on a managment node"); return 1; } # Override the die handler local $SIG{__DIE__} = sub{}; my $reservation_id = $self->data->get_reservation_id(); my $management_node_short_name = $self->data->get_management_node_short_name(); my $scripts_directory_path = "$MN_STAGE_SCRIPTS_DIRECTORY/$stage"; my @script_file_paths = $self->find_files($scripts_directory_path, '*'); if (!@script_file_paths) { notify($ERRORS{'DEBUG'}, 0, "no files exist in directory: $scripts_directory_path"); return 1; } # Sort the files so they can be executed in a known order @script_file_paths = sort_by_file_name(@script_file_paths); my $script_count = scalar(@script_file_paths); notify($ERRORS{'DEBUG'}, 0, "found $script_count files under $scripts_directory_path:\n" . join("\n", @script_file_paths)); # Create a JSON file on the management node containing reservation info $self->create_management_node_reservation_info_json_file(); my $mn_json_file_path = $self->get_management_node_reservation_info_json_file_path(); # Execute the scripts for my $script_file_path (@script_file_paths) { # Ignore certain intermediate directory paths if ($script_file_path =~ /\/(\.svn|\.git)\//i) { my $matching_section = $1; notify($ERRORS{'DEBUG'}, 0, "ignoring file on management node because it resides under intermediate directory '$matching_section': $script_file_path"); next; } # Ignore the .gitignore files if ($script_file_path =~ /\.gitignore/i) { my $matching_section = $1; notify($ERRORS{'DEBUG'}, 0, "ignoring gitignore file on management node from script directory '$matching_section': $script_file_path"); next; } my $command = "chmod +x $script_file_path && $script_file_path $mn_json_file_path"; my ($exit_status, $output) = $self->execute($command); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute script on management node: $command"); return; } else { notify($ERRORS{'DEBUG'}, 0, "executed script on management node $management_node_short_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); } } #$self->delete_management_node_reservation_info_json_file(); return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 check_private_ip_addresses Parameters : none Returns : boolean Description : Retrieves private IP information for all computers in the database assigned to the management node and checks if the hostname resolves on the management node. If it resolves to a different address than the value stored in the database, the database is updated. =cut sub check_private_ip_addresses { 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; } # Get computers assigned to this management node my @management_node_computer_ids = get_management_node_computer_ids(); # Get private IP addresses from the database for all computers assigned to this managment node my $database_private_ip_address_info = get_computer_private_ip_address_info(@management_node_computer_ids); my @database_no_resolve; my @database_resolve_match; my @database_resolve_no_match; my @no_database_resolve; my @no_database_no_resolve; for my $hostname (sort keys %$database_private_ip_address_info) { my $database_private_ip_address = $database_private_ip_address_info->{$hostname}; my ($hostname) = $hostname =~ /^([^\.]+)/g; # Attempt to detmine the IP address the hostname resolves to via gethostip #my $resolved_ip_address = get_host_ip($hostname) || get_host_ip($hostname); my $resolved_ip_address = hostname_to_ip_address($hostname); if ($database_private_ip_address) { if (!$resolved_ip_address) { push @database_no_resolve, $hostname; #print "private IP address of $hostname set in the database: $database_private_ip_address, hostname does NOT resolve\n"; #notify($ERRORS{'DEBUG'}, 0, "private IP address of $hostname set in the database: $database_private_ip_address, hostname does NOT resolve"); } elsif ($database_private_ip_address eq $resolved_ip_address) { push @database_resolve_match, $hostname; print "private IP address of $hostname set in database matches IP address hostname resolves to: $database_private_ip_address\n"; notify($ERRORS{'DEBUG'}, 0, "private IP address of $hostname set in database matches IP address hostname resolves to: $database_private_ip_address"); } else { push @database_resolve_no_match, $hostname; print "private IP address $hostname resolves to ($resolved_ip_address) does NOT match database ($database_private_ip_address)\n"; notify($ERRORS{'DEBUG'}, 0, "private IP address $hostname resolves to ($resolved_ip_address) does NOT match database ($database_private_ip_address)"); update_computer_private_ip_address($hostname, $resolved_ip_address); } } else { # Private IP address is not set in the database if ($resolved_ip_address) { push @no_database_resolve, $hostname; print "private IP address of $hostname NOT set in database, hostname resolves to $resolved_ip_address\n"; notify($ERRORS{'DEBUG'}, 0, "private IP address of $hostname NOT set in database, hostname resolves to $resolved_ip_address"); update_computer_private_ip_address($hostname, $resolved_ip_address); } else { push @no_database_no_resolve, $hostname; #print "private IP address of $hostname NOT set in database and hostname does NOT resolve\n"; notify($ERRORS{'DEBUG'}, 0, "private IP address of $hostname NOT set in database and hostname does NOT resolve"); } } } my $database_no_resolve_count = scalar(@database_no_resolve); my $database_resolve_match_count = scalar(@database_resolve_match); my $database_resolve_no_match_count = scalar(@database_resolve_no_match); my $no_database_resolve_count = scalar(@no_database_resolve); my $no_database_no_resolve_count = scalar(@no_database_no_resolve); notify($ERRORS{'DEBUG'}, 0, "private IP address results:\n" . "database set, hostname does not resolve: $database_no_resolve_count\n" . "database set, hostname resolves to matching address: $database_resolve_match_count\n" . "database set, hostname resolves to different address: $database_resolve_no_match_count (" . join(', ', @database_resolve_no_match) . ")\n" . "database not set, hostname resolves: $no_database_resolve_count (" . join(', ', @no_database_resolve) . ")\n" . "database not set, hostname does not resolve: $no_database_no_resolve_count (" . join(', ', @no_database_no_resolve) . ")" ); return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 setup_get_menu Parameters : none Returns : hash reference Description : Assembles the MN-related 'vcld -setup' menu items. =cut sub setup_get_menu { return { 'Management Node Operations' => { 'Check private IP addresses' => \&check_private_ip_addresses, }, }; } #////////////////////////////////////////////////////////////////////////////// =head2 get_private_key_file_path Parameters : none Returns : string Description : Returns the location on the management node where the private key resides that is used to decrypt secrets: /root/.vcl/<FQDN>.key =cut sub get_private_key_file_path { return $MN_PRIVATE_ENCRYPTION_KEY_FILE_PATH; } #////////////////////////////////////////////////////////////////////////////// =head2 _get_private_key_object_from_file Parameters : none Returns : string Description : Retrieves the private key string from the file on the management node and creates a Crypt::OpenSSL::RSA object based on it. =cut sub _get_private_key_object_from_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; } return $self->{private_key_object_from_file} if defined($self->{private_key_object_from_file}); my $management_node_short_name = $self->data->get_management_node_short_name() || return; my $private_key_file_path = $self->get_private_key_file_path(); if (!$self->file_exists($private_key_file_path)) { notify($ERRORS{'OK'}, 0, "unable to retrieve private key from file on $management_node_short_name because file does NOT exist: $private_key_file_path"); return; } my $private_key_file_string = $self->get_file_contents($private_key_file_path); if (!$private_key_file_string) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve private key from file on $management_node_short_name: $private_key_file_path"); return; } # Override the die handler local $SIG{__DIE__} = sub{}; # Create an RSA object based on the existing private key contained in the file my $rsa_private; eval { $rsa_private = Crypt::OpenSSL::RSA->new_private_key($private_key_file_string); }; if ($EVAL_ERROR || !$rsa_private) { notify($ERRORS{'WARNING'}, 0, "failed to create Crypt::OpenSSL::RSA object from $private_key_file_path on $management_node_short_name" . ($EVAL_ERROR ? ", error:\n" . $EVAL_ERROR : '')); return; } $self->{private_key_object_from_file} = $rsa_private; return $rsa_private; } #////////////////////////////////////////////////////////////////////////////// =head2 extract_public_key_from_private_key_file Parameters : none Returns : string Description : Retrieves the private key from the file on the management node and extracts the public key from the private key. The public key string is returned. =cut sub extract_public_key_from_private_key_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 $rsa_private = $self->_get_private_key_object_from_file() || return; my $management_node_short_name = $self->data->get_management_node_short_name(); my $private_key_file_path = $self->get_private_key_file_path(); # Override the die handler local $SIG{__DIE__} = sub{}; # Retrieve the public key string from the RSA object my $public_key_string; eval { $public_key_string = $rsa_private->get_public_key_x509_string(); }; if ($EVAL_ERROR || !$public_key_string) { notify($ERRORS{'WARNING'}, 0, "failed to extract public key from private key file $private_key_file_path on $management_node_short_name, failed to retrieve public key from private key contained in the file" . ($EVAL_ERROR ? ", error:\n" . $EVAL_ERROR : '')); return; } return $public_key_string; } #////////////////////////////////////////////////////////////////////////////// =head2 generate_private_key_file Parameters : none Returns : boolean Description : Creates a 4096 bit RSA private key file on the management node at /root/.vcl/<FQDN>.key. =cut sub generate_private_key_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 $arguments_hash_ref = shift; if (defined($arguments_hash_ref) && !ref($arguments_hash_ref) || ref($arguments_hash_ref) ne 'HASH') { notify($ERRORS{'WARNING'}, 0, "argument was supplied but is not a hash reference:\n" . format_data($arguments_hash_ref)); return; } my $private_key_file_path = $self->get_private_key_file_path(); my $bits = 4096; # If provided and true, existing private key file will be deleted if it exists my $force = $arguments_hash_ref->{force}; my $management_node_id = $self->data->get_management_node_id() || return; my $management_node_short_name = $self->data->get_management_node_short_name() || return; my $reservation_id = $self->data->get_reservation_id(); notify($ERRORS{'DEBUG'}, 0, "*** attempting to generate a new private key file on $management_node_short_name: $private_key_file_path ***"); # Make sure the private key file does not already exist if ($self->file_exists($private_key_file_path)) { if ($force) { (my $timestamp = makedatestring()) =~ s/\s+/_/g; my $backup_private_key_file_path = $private_key_file_path . "_$timestamp"; if ($self->copy_file($private_key_file_path, $backup_private_key_file_path)) { notify($ERRORS{'OK'}, 0, "force argument was specified, existing private key file will be overwritten, created backup copy: $private_key_file_path --> $backup_private_key_file_path"); } else { notify($ERRORS{'WARNING'}, 0, "failed to generate encryption keys, force argument was specified, existing private key file exists but failed to create backup copy: $private_key_file_path --> $backup_private_key_file_path"); return; } } else { notify($ERRORS{'WARNING'}, 0, "failed to generate encryption keys, private key file already exists: $private_key_file_path"); return; } } # Delete cached RSA object if ($self->{private_key_object_from_file}) { notify($ERRORS{'DEBUG'}, 0, "deleting cached RSA private key object"); delete $self->{private_key_object_from_file}; } # Delete all existing cryptsecret entries for the management node # The website's API won't delete any that may have been created with an earlier key delete_management_node_cryptsecret($management_node_id); # Override the die handler local $SIG{__DIE__} = sub{}; # Create a new RSA object containing a private/public key pair # Create an RSA object based on the existing private key contained in the file my $rsa_generate; eval { $rsa_generate = Crypt::OpenSSL::RSA->generate_key($bits); }; if ($EVAL_ERROR || !$rsa_generate) { notify($ERRORS{'WARNING'}, 0, "failed to create private key file on management node $management_node_short_name: $private_key_file_path, RSA object could not be created" . ($EVAL_ERROR ? ", error:\n" . $EVAL_ERROR : '')); return; } my $private_key_string; eval { $private_key_string = $rsa_generate->get_private_key_string(); }; if ($EVAL_ERROR || !$private_key_string) { notify($ERRORS{'WARNING'}, 0, "failed to create private key file on management node $management_node_short_name: $private_key_file_path, private key string could not be retireved from RSA object" . ($EVAL_ERROR ? ", error:\n" . $EVAL_ERROR : '')); return; } my $public_key_string; eval { $public_key_string = $rsa_generate->get_public_key_x509_string(); }; if ($EVAL_ERROR || !$public_key_string) { notify($ERRORS{'WARNING'}, 0, "failed to create private key file on management node $management_node_short_name: $private_key_file_path, public key string could not be retireved from RSA object" . ($EVAL_ERROR ? ", error:\n" . $EVAL_ERROR : '')); return; } $self->create_text_file($private_key_file_path, $private_key_string) || return; # Update cryptkey table with the public key string if (!set_management_node_cryptkey_pubkey($management_node_id, $public_key_string)) { notify($ERRORS{'WARNING'}, 0, "created private key file on management node $management_node_short_name: $private_key_file_path, failed to update cryptkey table in database, attempting to delete private key file just created: $private_key_file_path"); $self->delete_file($private_key_file_path); return; } # Call the XML-RPC API to create a new cryptsecret entry for this reservation call_check_crypt_secrets($reservation_id); notify($ERRORS{'OK'}, 0, "created private key file on management node $management_node_short_name: $private_key_file_path, updated cryptkey.pubkey value in database"); return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 decrypt_cryptsecret Parameters : $secret_id, $encrypted_string Returns : string Description : Decrypts an encrypted string stored in the database such as addomain.password. The $secret_id argument must match a cryptsecret.secretid value in the database. The corresponding cryptsecret.cryptsecret value is a base64-encoded string that is encrypted using the management node's public key stored in cryptkey.pubkey. The management node's private key is used to decrypt it. =cut sub decrypt_cryptsecret { 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 ($secret_id, $encrypted_string, $recreate_key) = @_; if (!defined($secret_id)) { notify($ERRORS{'WARNING'}, 0, "secret ID argument was not supplied"); return; } if (!defined($encrypted_string)) { notify($ERRORS{'WARNING'}, 0, "encrypted string argument was not supplied"); return; } my $management_node_id = $self->data->get_management_node_id() || return; my $management_node_short_name = $self->data->get_management_node_short_name() || return; my $private_key_file_path = $self->get_private_key_file_path(); if ($recreate_key) { notify($ERRORS{'DEBUG'}, 0, "previous attempt to decrypt cryptsecret failed, attempting to regenerate private key and cryptsecret entries"); if (!$self->generate_private_key_file({force => 1})) { notify($ERRORS{'WARNING'}, 0, "unable to decrypt secret ID $secret_id, failed to verify private key stored in file on management node is valid and its public key matches the cryptkey.pubkey value in the database"); return; } } # Pass opposite of $recreate_key as $suppress_warning argument my $cryptsecret = get_management_node_cryptsecret_value($management_node_id, $secret_id, !$recreate_key); if (!$cryptsecret) { #notify($ERRORS{'WARNING'}, 0, "unable to decrypt secret ID $secret_id, failed to retrieve cryptsecret.cryptsecret value for management node ID $management_node_id"); $recreate_key ? return : return $self->decrypt_cryptsecret($secret_id, $encrypted_string, 1); } # The encrypted string (addomain.password, etc) and cryptsecret.cryptsecret should ALWAYS be base64 encoded # They must be decoded to binary before being passed to decrypt functions my $encrypted_string_decoded = decode_base64($encrypted_string); my $cryptsecret_decoded = decode_base64($cryptsecret); my $rsa_private = $self->_get_private_key_object_from_file(); if (!$rsa_private) { #notify($ERRORS{'WARNING'}, 0, "unable to decrypt secret ID $secret_id, failed to create RSA object based on management node's private key"); $recreate_key ? return : return $self->decrypt_cryptsecret($secret_id, $encrypted_string, 1); } # Override the die handler local $SIG{__DIE__} = sub{}; my $key; eval { $key = $rsa_private->decrypt($cryptsecret_decoded); }; if ($EVAL_ERROR || !$key) { # Wrong key error: # RSA.xs:202: OpenSSL error: oaep decoding error notify($ERRORS{'WARNING'}, 0, "unable to decrypt secret ID $secret_id, failed to decrypt cryptsecret using RSA object based on management node's private key file: $private_key_file_path" . ($EVAL_ERROR ? ", error:\n" . $EVAL_ERROR : '')); $recreate_key ? return : return $self->decrypt_cryptsecret($secret_id, $encrypted_string, 1); } my $encrypted_string_length = length($encrypted_string_decoded); if ($encrypted_string_length < 32) { # This should always be at least 32 bytes # If less than 16, the next 2 substr commands may fail with 'substr outside of string' errors notify($ERRORS{'WARNING'}, 0, "unable to decrypt secret ID $secret_id, encrypted string length: $encrypted_string_length bytes, it must be 32 bytes or more:\n$encrypted_string\n\n" . decode_base64($encrypted_string)); return; } my $iv = substr($encrypted_string_decoded, 0, 16); my $ciphered_string = substr($encrypted_string_decoded, 16); my $cipher; eval { $cipher = Crypt::CBC->new( { 'key' => $key, 'cipher' => 'Crypt::Rijndael', 'iv' => $iv, 'header' => 'none', 'literal_key' => 1, } ); }; if (!$cipher || $EVAL_ERROR) { notify($ERRORS{'WARNING'}, 0, "unable to decrypt secret ID $secret_id, failed to create Crypt::CBC object" . ($EVAL_ERROR ? ", error:\n" . $EVAL_ERROR : '')); return; } my $decrypted_string = $cipher->decrypt($ciphered_string); if (defined($decrypted_string)) { notify($ERRORS{'OK'}, 0, "decrypted secret ID $secret_id"); return $decrypted_string; } else { notify($ERRORS{'WARNING'}, 0, "failed to decrypt secret ID $secret_id"); $recreate_key ? return : return $self->decrypt_cryptsecret($secret_id, $encrypted_string, 1); } } #////////////////////////////////////////////////////////////////////////////// 1; __END__ =head1 SEE ALSO L<http://cwiki.apache.org/VCL/> =cut