| #!/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 |