| #!/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.pm - VCL module to support Linux operating systems |
| |
| =head1 SYNOPSIS |
| |
| Needs to be written |
| |
| =head1 DESCRIPTION |
| |
| This module provides VCL support for Linux operating systems. |
| |
| =cut |
| |
| ############################################################################### |
| package VCL::Module::OS::Linux; |
| |
| # Specify the lib path using FindBin |
| use FindBin; |
| use lib "$FindBin::Bin/../../.."; |
| |
| # Configure inheritance |
| use base qw(VCL::Module::OS); |
| |
| # 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; |
| no warnings 'redefine'; |
| |
| use VCL::utils; |
| use English qw(-no_match_vars); |
| use Net::Netmask; |
| use File::Basename; |
| use File::Temp qw(tempfile mktemp); |
| |
| ############################################################################### |
| |
| =head1 CLASS VARIABLES |
| |
| =cut |
| |
| =head2 $SOURCE_CONFIGURATION_DIRECTORY |
| |
| Data type : String |
| Description : Location on the management node of the files specific to this OS |
| module which are needed to configure the loaded OS on a computer. |
| This is normally the directory under 'tools' named after this OS |
| module. |
| |
| Example: |
| /usr/local/vcl/tools/Linux |
| |
| =cut |
| |
| our $SOURCE_CONFIGURATION_DIRECTORY = "$TOOLS/Linux"; |
| |
| =head2 $NODE_CONFIGURATION_DIRECTORY |
| |
| Data type : String |
| Description : Location on computer loaded with a VCL image where configuration |
| files and scripts reside. |
| |
| =cut |
| |
| our $NODE_CONFIGURATION_DIRECTORY = '/root/VCL'; |
| |
| =head2 $CAPTURE_DELETE_FILE_PATHS |
| |
| Data type : Array |
| Description : List of files to be deleted during the image capture process. |
| |
| =cut |
| |
| our $CAPTURE_DELETE_FILE_PATHS = [ |
| '/root/.ssh/id_rsa', |
| '/root/.ssh/id_rsa.pub', |
| '/root/*-v*.xml', |
| '/etc/sysconfig/iptables*old*', |
| '/etc/sysconfig/iptables_pre*', |
| '/etc/udev/rules.d/70-persistent-net.rules', |
| '/tmp/*', |
| '/var/log/*.0*', |
| '/var/log/*.1*', |
| '/var/log/*-20*', |
| '/var/log/*.gz', |
| '/var/log/*.old', |
| ]; |
| |
| =head2 $CAPTURE_CLEAR_FILE_PATHS |
| |
| Data type : Array |
| Description : List of files to be cleared during the image capture process. |
| |
| =cut |
| |
| our $CAPTURE_CLEAR_FILE_PATHS = [ |
| '/etc/hostname', |
| '/var/log/audit/audit.log', |
| '/var/log/auth.log', |
| '/var/log/boot.log', |
| '/var/log/kern.log', |
| '/var/log/lastlog', |
| '/var/log/maillog', |
| '/var/log/messages', |
| '/var/log/secure', |
| '/var/log/syslog', |
| '/var/log/udev', |
| '/var/log/ufw.log', |
| '/var/log/wtmp', |
| ]; |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 get_node_configuration_directory |
| |
| Parameters : none |
| Returns : string |
| Description : Retrieves the $NODE_CONFIGURATION_DIRECTORY variable value for |
| the OS. This is the path on the computer's hard drive where image |
| configuration files and scripts are copied. |
| |
| =cut |
| |
| sub get_node_configuration_directory { |
| return $NODE_CONFIGURATION_DIRECTORY; |
| } |
| |
| ############################################################################### |
| |
| =head1 OBJECT METHODS |
| |
| =cut |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 get_init_modules |
| |
| Parameters : none |
| Returns : array of Linux init module references |
| Description : Determines the Linux init daemon being used by the computer |
| (SysV, systemd, etc.) and creates an object. The default is SysV |
| if no other modules in the lib/VCL/Module/OS/Linux/init directory |
| match the init daemon on the computer. The init module is mainly |
| used to control services on the computer. |
| |
| =cut |
| |
| sub get_init_modules { |
| 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->{init_modules}} if $self->{init_modules}; |
| |
| notify($ERRORS{'DEBUG'}, 0, "beginning Linux init daemon module initialization"); |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| # Get the absolute path of the init module directory |
| my $init_directory_path = "$FindBin::Bin/../lib/VCL/Module/OS/Linux/init"; |
| notify($ERRORS{'DEBUG'}, 0, "Linux init module directory path: $init_directory_path"); |
| |
| # Get a list of all *.pm files in the init module directory |
| my @init_module_paths = $self->mn_os->find_files($init_directory_path, '*.pm'); |
| |
| # Attempt to create an initialize an object for each init module |
| my %init_module_hash; |
| INIT_MODULE: for my $init_module_path (@init_module_paths) { |
| my $init_name = fileparse($init_module_path, qr/\.pm$/i); |
| my $init_perl_package = "VCL::Module::OS::Linux::init::$init_name"; |
| |
| # Attempt to load the init module |
| notify($ERRORS{'DEBUG'}, 0, "attempting to load $init_name init module: $init_perl_package"); |
| eval "use $init_perl_package"; |
| if ($EVAL_ERROR || $@) { |
| notify($ERRORS{'CRITICAL'}, 0, "failed to load $init_name init module: $init_perl_package, error: $EVAL_ERROR"); |
| next INIT_MODULE; |
| } |
| |
| # Attempt to create an init module object |
| # The 'new' constructor will automatically call the module's initialize subroutine |
| # initialize will check the computer to determine if it contains the corresponding Linux init daemon installed |
| # If not installed, the constructor will return false |
| my $init; |
| eval { $init = ($init_perl_package)->new({ |
| data_structure => $self->data, |
| os => $self, |
| mn_os => $self->mn_os, |
| init_modules => $self->{init_modules}, |
| }) }; |
| if ($init) { |
| my @required_commands = eval "@" . $init_perl_package . "::REQUIRED_COMMANDS"; |
| if ($EVAL_ERROR) { |
| notify($ERRORS{'CRITICAL'}, 0, "\@REQUIRED_COMMANDS variable is not defined in the $init_perl_package Linux init daemon module"); |
| next INIT_MODULE; |
| } |
| if (@required_commands) { |
| for my $command (@required_commands) { |
| if (!$self->command_exists($command)) { |
| next INIT_MODULE; |
| } |
| } |
| } |
| |
| my @prohibited_commands = eval "@" . $init_perl_package . "::PROHIBITED_COMMANDS"; |
| if (@prohibited_commands) { |
| for my $command (@prohibited_commands) { |
| if ($self->command_exists($command)) { |
| notify($ERRORS{'DEBUG'}, 0, "ignoring $init_perl_package Linux init daemon module, '$command' command exists on $computer_node_name"); |
| next INIT_MODULE; |
| } |
| } |
| } |
| |
| |
| # init object successfully created, retrieve the module's $INIT_DAEMON_ORDER variable |
| # An OS may have/support multiple Linux init daemons, services may be registered under different init daemons |
| # In some cases, need to try multple init modules to control a service |
| # This $INIT_DAEMON_ORDER integer determines the order in which the modules are tried |
| my $init_daemon_order = eval '$' . $init_perl_package . '::INIT_DAEMON_ORDER'; |
| if ($EVAL_ERROR) { |
| notify($ERRORS{'CRITICAL'}, 0, "\$INIT_DAEMON_ORDER variable is not defined in the $init_perl_package Linux init daemon module"); |
| next INIT_MODULE; |
| } |
| elsif ($init_module_hash{$init_daemon_order}) { |
| notify($ERRORS{'CRITICAL'}, 0, "multiple Linux init daemon modules are configured to use \$INIT_DAEMON_ORDER=$init_daemon_order: " . ref($init_module_hash{$init_daemon_order}) . ", " . ref($init) . ", the value of this variable must be unique"); |
| next INIT_MODULE; |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "$init_name init object created and initialized to control $computer_node_name, order: $init_daemon_order"); |
| $init_module_hash{$init_daemon_order} = $init; |
| } |
| } |
| elsif ($EVAL_ERROR) { |
| notify($ERRORS{'WARNING'}, 0, "$init_perl_package init object could not be created, error:\n$EVAL_ERROR"); |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "$init_name init object could not be initialized to control $computer_node_name"); |
| } |
| } |
| |
| # Make sure at least 1 init module object was successfully initialized |
| if (!%init_module_hash) { |
| notify($ERRORS{'WARNING'}, 0, "failed to create Linux init daemon module"); |
| return; |
| } |
| |
| # Construct an array of init module objects from highest to lowest $INIT_DAEMON_ORDER |
| $self->{init_modules} = []; |
| my $init_module_order_string; |
| for my $init_daemon_order (sort {$a <=> $b} keys %init_module_hash) { |
| push @{$self->{init_modules}}, $init_module_hash{$init_daemon_order}; |
| $init_module_order_string .= "$init_daemon_order: " . ref($init_module_hash{$init_daemon_order}) . "\n"; |
| } |
| notify($ERRORS{'DEBUG'}, 0, "constructed array containing init module objects which may be used to control $computer_node_name:\n$init_module_order_string"); |
| return @{$self->{init_modules}}; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 firewall |
| |
| Parameters : none |
| Returns : Linux firewall module reference |
| Description : Determines the Linux firewall module to use and creates an |
| object. |
| |
| =cut |
| |
| sub firewall { |
| 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->{firewall} if $self->{firewall}; |
| |
| notify($ERRORS{'DEBUG'}, 0, "beginning Linux firewall daemon module initialization"); |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| # Get the absolute path of the init module directory |
| my $firewall_directory_path = "$FindBin::Bin/../lib/VCL/Module/OS/Linux/firewall"; |
| notify($ERRORS{'DEBUG'}, 0, "Linux firewall module directory path: $firewall_directory_path"); |
| |
| # Get a list of all *.pm files in the firewall module directory |
| my @firewall_module_paths = $self->mn_os->find_files($firewall_directory_path, '*.pm'); |
| |
| # Attempt to create an initialize an object for each firewall module |
| my %firewall_module_hash; |
| FIREWALL_MODULE: for my $firewall_module_path (@firewall_module_paths) { |
| my $firewall_name = fileparse($firewall_module_path, qr/\.pm$/i); |
| my $firewall_perl_package = "VCL::Module::OS::Linux::firewall::$firewall_name"; |
| |
| # Attempt to load the module |
| eval "use $firewall_perl_package"; |
| if ($EVAL_ERROR) { |
| notify($ERRORS{'WARNING'}, 0, "$firewall_perl_package module could not be loaded, error:\n" . $EVAL_ERROR); |
| return; |
| } |
| notify($ERRORS{'DEBUG'}, 0, "$firewall_perl_package module loaded"); |
| |
| # Attempt to create the object |
| my $firewall_object; |
| eval { |
| $firewall_object = ($firewall_perl_package)->new({ |
| data_structure => $self->data, |
| os => $self, |
| mn_os => $self->mn_os, |
| }) |
| }; |
| |
| if ($EVAL_ERROR) { |
| notify($ERRORS{'WARNING'}, 0, "failed to create $firewall_perl_package object, error: $EVAL_ERROR"); |
| } |
| elsif (!$firewall_object) { |
| notify($ERRORS{'DEBUG'}, 0, "$firewall_perl_package object could not be initialized"); |
| } |
| else { |
| $self->{firewall} = $firewall_object; |
| my $linux_address = sprintf('%x', $self); |
| my $firewall_object_address = sprintf('%x', $firewall_object); |
| my $self_firewall_address = sprintf('%x', $self->{firewall}); |
| notify($ERRORS{'DEBUG'}, 0, "$firewall_perl_package object created for $computer_node_name, Linux object address: $linux_address, firewall object address: $firewall_object_address, \$self->{firewall} address: $self_firewall_address"); |
| return $firewall_object; |
| } |
| } |
| |
| notify($ERRORS{'DEBUG'}, 0, "unable to initialize suitable specific firewall module, returning generic VCL::Module::OS::Linux::firewall object"); |
| return bless {}, 'VCL::Module::OS::Linux::firewall'; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 pre_capture |
| |
| Parameters : none |
| Returns : boolean |
| Description : |
| |
| =cut |
| |
| sub pre_capture { |
| my $self = shift; |
| my $args = 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; |
| } |
| |
| # Check if end_state argument was passed |
| if (defined $args->{end_state}) { |
| $self->{end_state} = $args->{end_state}; |
| } |
| else { |
| $self->{end_state} = 'off'; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| # Call OS::pre_capture to perform the pre-capture tasks common to all OS's |
| if (!$self->SUPER::pre_capture($args)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute parent class pre_capture() subroutine"); |
| return; |
| } |
| |
| notify($ERRORS{'OK'}, 0, "beginning Linux-specific image capture preparation tasks"); |
| |
| if (!$self->generate_exclude_list_sample()) { |
| notify($ERRORS{'DEBUG'}, 0, "could not create /root/.vclcontrol/vcl_exclude_list.sample"); |
| } |
| |
| # Force user off computer |
| if (!$self->logoff_user()) { |
| notify($ERRORS{'WARNING'}, 0, "unable to log user off $computer_node_name"); |
| } |
| |
| # Attempt to unmount NFS shares configured for the management node (Site Configuration > NFS Mounts) |
| $self->unmount_nfs_shares() || return; |
| $self->remove_matching_fstab_lines('Added by VCL'); |
| |
| # Remove user accounts |
| if ($self->delete_user_accounts()) { |
| notify($ERRORS{'OK'}, 0, "deleted user accounts added by VCL from $computer_node_name"); |
| } |
| |
| # Attempt to set the root password to a known value |
| # This is useful for troubleshooting image problems |
| $self->set_password("root", $WINDOWS_ROOT_PASSWORD); |
| |
| # Prevent the "Text Mode Setup Utility" - "Choose a Tool" screen from appearing |
| if ($self->service_exists('firstboot')) { |
| $self->disable_service('firstboot'); |
| } |
| |
| if (!$self->configure_default_sshd()) { |
| return; |
| } |
| |
| if (!$self->configure_rc_local()) { |
| return; |
| } |
| |
| if ($self->can('firewall') && $self->firewall->can('process_pre_capture')) { |
| $self->firewall->process_pre_capture() || return; |
| } |
| |
| if (!$self->clean_known_files()) { |
| notify($ERRORS{'WARNING'}, 0, "unable to clean known files"); |
| } |
| |
| # Configure the private and public interfaces to use DHCP |
| my $private_interface_name = $self->get_private_interface_name(); |
| my $public_interface_name = $self->get_public_interface_name(); |
| |
| if (!$self->enable_dhcp($private_interface_name)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to enable DHCP on the private interface"); |
| return; |
| } |
| if (!$self->enable_dhcp($public_interface_name)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to enable DHCP on the public interface"); |
| return; |
| } |
| |
| # Delete route files if they exist for either the private or public interface |
| $self->delete_file("/etc/sysconfig/network-scripts/route-$private_interface_name"); |
| $self->delete_file("/etc/sysconfig/network-scripts/route-$public_interface_name"); |
| |
| # Remove computer/reservation specific lines from network file |
| $self->remove_lines_from_file('/etc/sysconfig/network', 'HOSTNAME'); |
| $self->remove_lines_from_file('/etc/sysconfig/network', 'GATEWAY'); |
| |
| # Shut the computer down |
| if ($self->{end_state} =~ /off/i) { |
| notify($ERRORS{'DEBUG'}, 0, "shutting down $computer_node_name, provisioning module specified end state: $self->{end_state}"); |
| if (!$self->shutdown()) { |
| notify($ERRORS{'WARNING'}, 0, "failed to shut down $computer_node_name"); |
| return; |
| } |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "$computer_node_name not shut down, provisioning module specified end state: $self->{end_state}"); |
| } |
| |
| notify($ERRORS{'OK'}, 0, "Linux pre-capture steps complete"); |
| return 1; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 post_load |
| |
| Parameters : none |
| Returns : boolean |
| Description : |
| |
| =cut |
| |
| sub post_load { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $image_name = $self->data->get_image_name(); |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| my $image_os_install_type = $self->data->get_image_os_install_type(); |
| |
| notify($ERRORS{'OK'}, 0, "beginning Linux post_load tasks, image: $image_name, computer: $computer_node_name"); |
| |
| # Wait for computer to respond to SSH |
| if (!$self->wait_for_response(5, 600, 5)) { |
| notify($ERRORS{'WARNING'}, 0, "$computer_node_name never responded to SSH"); |
| return; |
| } |
| |
| # Attempt to generate ifcfg-eth* files and start any interfaces which the file does not exist |
| $self->activate_interfaces(); |
| |
| # Configure the firewall to allow SSH traffic only from the management node |
| if ($self->can('firewall') && $self->firewall->can('process_post_load')) { |
| $self->firewall->process_post_load() || return; |
| } |
| |
| # Make sure the public IP address assigned to the computer matches the database |
| if (!$self->update_public_ip_address()) { |
| notify($ERRORS{'WARNING'}, 0, "failed to update public IP address"); |
| return; |
| } |
| |
| # Configure sshd to only listen on the private interface and add ext_sshd service listening on the public interface |
| # This locks down sshd so that it isn't listening on the public interface -- ext_sshd isn't started yet |
| if (!$self->configure_ext_sshd()) { |
| notify($ERRORS{'WARNING'}, 0, "failed to configure ext_sshd on $computer_node_name"); |
| return 0; |
| } |
| |
| # Remove commands from rc.local added by previous versions of VCL |
| $self->configure_rc_local(); |
| |
| # Kickstart installations likely won't have currentimage.txt, generate it |
| if ($image_os_install_type eq "kickstart") { |
| notify($ERRORS{'OK'}, 0, "detected kickstart install on $computer_node_name, writing current_image.txt"); |
| if (!$self->create_currentimage_txt()) { |
| notify($ERRORS{'WARNING'}, 0, "failed to create currentimage.txt on $computer_node_name"); |
| } |
| } |
| |
| # Update time and ntpservers |
| if (!$self->synchronize_time()) { |
| notify($ERRORS{'WARNING'}, 0, "unable to synchroinze date and time on $computer_node_name"); |
| } |
| |
| # Change password |
| if (!$self->set_password("root")) { |
| notify($ERRORS{'OK'}, 0, "failed to edit root password on $computer_node_name"); |
| } |
| |
| # Clear ssh idenity keys from /root/.ssh |
| if (!$self->clear_private_keys()) { |
| notify($ERRORS{'WARNING'}, 0, "failed to clear known identity keys"); |
| } |
| |
| # Update computer hostname if imagemeta.sethostname is not set to 0 |
| my $set_hostname = $self->data->get_imagemeta_sethostname(0); |
| if (defined($set_hostname) && $set_hostname =~ /0/) { |
| notify($ERRORS{'DEBUG'}, 0, "not setting computer hostname, imagemeta.sethostname = $set_hostname"); |
| } |
| else { |
| $self->update_public_hostname(); |
| } |
| |
| # Run the vcl_post_load script if it exists in the image |
| my @post_load_script_paths = ('/usr/local/vcl/vcl_post_load', '/etc/init.d/vcl_post_load'); |
| |
| foreach my $script_path (@post_load_script_paths) { |
| notify($ERRORS{'DEBUG'}, 0, "script_path $script_path"); |
| if ($self->file_exists($script_path)) { |
| my $result = $self->run_script($script_path, '1', '300', '1'); |
| if (!defined($result)) { |
| notify($ERRORS{'WARNING'}, 0, "error occurred running $script_path"); |
| } |
| elsif ($result == 0) { |
| notify($ERRORS{'DEBUG'}, 0, "$script_path does not exist in image: $image_name"); |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "ran $script_path"); |
| } |
| } |
| } |
| |
| return $self->SUPER::post_load(); |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 post_reserve |
| |
| Parameters : none |
| Returns : boolean |
| Description : |
| |
| =cut |
| |
| sub post_reserve { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return 0; |
| } |
| |
| my $reservation_id = $self->data->get_reservation_id(); |
| my $image_name = $self->data->get_image_name(); |
| my $computer_short_name = $self->data->get_computer_short_name(); |
| |
| |
| # User supplied data |
| #check if variable is set |
| #get variable from variable table related to server reservation id ‘userdata|<reservation id>’ |
| # write contents to local temp file /tmp/resrvationid_post_reserve_userdata |
| # scp tmpfile to ‘/root/.vclcontrol/post_reserve_userdata’ |
| # assumes the image has the call in vcl_post_reserve to import/read the user data file |
| my $variable_name = "userdata|$reservation_id"; |
| my $variable_data; |
| my $target_location = "/root/.vclcontrol/post_reserve_userdata"; |
| if ($self->data->is_variable_set($variable_name)) { |
| $variable_data = get_variable($variable_name); |
| |
| #write to local temp file |
| my $tmpfile = "/tmp/$reservation_id" ."_post_reserve_userdata"; |
| if (open(TMP, ">$tmpfile")) { |
| print TMP $variable_data; |
| close(TMP); |
| |
| if ($self->copy_file_to($tmpfile, $target_location)) { |
| notify($ERRORS{'DEBUG'}, 0, "copied $tmpfile to $target_location on $computer_short_name"); |
| } |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "failed to open $tmpfile for writing userdata"); |
| } |
| #Clean variable from variable table |
| if (delete_variable($variable_name)) { |
| notify($ERRORS{'DEBUG'}, 0, "deleted variable_name $variable_name from variable table"); |
| } |
| } |
| |
| # Check if script exists |
| my @post_reserve_script_paths = ('/usr/local/vcl/vcl_post_reserve', '/etc/init.d/vcl_post_reserve'); |
| foreach my $script_path (@post_reserve_script_paths) { |
| if ($self->file_exists($script_path)) { |
| # If post_reserve script exists, assume it does user or reservation-specific actions |
| # If the user never connects and the reservation times out, there's no way to revert these actions in order to clean the computer for another user |
| # Tag the image as tainted so it is reloaded |
| $self->set_tainted_status('post-reserve scripts residing in the image executed'); |
| |
| # Run the vcl_post_reserve script if it exists in the image |
| my $result = $self->run_script($script_path, '1', '300', '1'); |
| if (!defined($result)) { |
| notify($ERRORS{'WARNING'}, 0, "error occurred running $script_path"); |
| } |
| elsif ($result == 0) { |
| notify($ERRORS{'DEBUG'}, 0, "$script_path does not exist in image: $image_name"); |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "ran $script_path"); |
| } |
| } |
| } |
| |
| return $self->SUPER::post_reserve(); |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 post_reservation |
| |
| Parameters : none |
| Returns : boolean |
| Description : Checks for and runs vcl_post_reservation script at the end of a |
| reservation. |
| |
| =cut |
| |
| sub post_reservation { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return 0; |
| } |
| |
| my $script_path = '/usr/local/vcl/vcl_post_reservation'; |
| |
| # Check if script exists |
| if ($self->file_exists($script_path)) { |
| # Run the vcl_post_reserve script if it exists in the image |
| $self->run_script($script_path, '1', '300', '1'); |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "script does NOT exist: $script_path"); |
| } |
| |
| return $self->SUPER::post_reservation(); |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 update_public_hostname |
| |
| Parameters : none |
| Returns : boolean |
| Description : Retrieves the public IP address being used on the Linux computer. |
| Determines the hostname the IP address resolves to. Sets the |
| hostname on the Linux computer. |
| |
| =cut |
| |
| sub update_public_hostname { |
| my $self = shift; |
| unless (ref($self) && $self->isa('VCL::Module')) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine can only be called as a VCL::Module module object method"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my $public_hostname = shift; |
| if (!$public_hostname) { |
| # Get the IP address of the public adapter |
| my $public_ip_address = $self->get_public_ip_address(); |
| if (!$public_ip_address) { |
| notify($ERRORS{'WARNING'}, 0, "hostname cannot be set, unable to determine public IP address"); |
| return; |
| } |
| notify($ERRORS{'DEBUG'}, 0, "retrieved public IP address of $computer_node_name: $public_ip_address"); |
| |
| # Get the hostname for the public IP address |
| $public_hostname = ip_address_to_hostname($public_ip_address) || $computer_node_name; |
| } |
| |
| my $error_occurred = 0; |
| |
| # Check if hostname file exists and update if necessary |
| my $hostname_file_path = '/etc/hostname'; |
| if ($self->file_exists($hostname_file_path)) { |
| if ($self->create_text_file($hostname_file_path, $public_hostname)) { |
| notify($ERRORS{'DEBUG'}, 0, "updated $hostname_file_path on $computer_node_name with hostname '$public_hostname'"); |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "failed to update $hostname_file_path on $computer_node_name with '$public_hostname'"); |
| $error_occurred = 1; |
| } |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "$hostname_file_path not updated on $computer_node_name because the file does not exist"); |
| } |
| |
| |
| # Check if network file exists and update if necessary |
| my $network_file_path = '/etc/sysconfig/network'; |
| if ($self->file_exists($network_file_path)) { |
| my $sed_command = "sed -i -e \"/^HOSTNAME=/d\" $network_file_path; echo \"HOSTNAME=$public_hostname\" >> $network_file_path"; |
| my ($sed_exit_status, $sed_output) = $self->execute($sed_command); |
| if (!defined($sed_output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to update hostname in $network_file_path on $computer_node_name"); |
| return; |
| } |
| elsif ($sed_exit_status != 0) { |
| notify($ERRORS{'WARNING'}, 0, "failed to update hostname in $network_file_path on $computer_node_name, exit status: $sed_exit_status, output:\n" . join("\n", @$sed_output)); |
| $error_occurred = 1; |
| } |
| else { |
| notify($ERRORS{'OK'}, 0, "updated hostname in $network_file_path on $computer_node_name to '$public_hostname'"); |
| } |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "$network_file_path not updated on $computer_node_name because the file does not exist"); |
| } |
| |
| # Check if hostnamectl exists, this is provided by systemd on CentOS/RHEL 7+ |
| if ($self->command_exists('hostnamectl')) { |
| my $hostnamectl_command = "hostnamectl set-hostname $public_hostname"; |
| my ($hostnamectl_exit_status, $hostnamectl_output) = $self->execute($hostnamectl_command); |
| if (!defined($hostnamectl_output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to set hostname using hostnamectl command on $computer_node_name to $public_hostname"); |
| return; |
| } |
| elsif ($hostnamectl_exit_status != 0) { |
| notify($ERRORS{'WARNING'}, 0, "failed to set hostname using hostnamectl command on $computer_node_name to $public_hostname, exit status: $hostnamectl_exit_status, command: '$hostnamectl_command', output:\n" . join("\n", @$hostnamectl_output)); |
| $error_occurred = 1; |
| } |
| else { |
| notify($ERRORS{'OK'}, 0, "set hostname using hostnamectl command on $computer_node_name to $public_hostname"); |
| } |
| } |
| else { |
| my $hostname_command = "hostname $public_hostname"; |
| my ($hostname_exit_status, $hostname_output) = $self->execute($hostname_command); |
| if (!defined($hostname_output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to set hostname using hostname command on $computer_node_name to $public_hostname"); |
| return; |
| } |
| elsif ($hostname_exit_status != 0) { |
| notify($ERRORS{'WARNING'}, 0, "failed to set hostname using hostname command on $computer_node_name to $public_hostname, exit status: $hostname_exit_status, command: '$hostname_command', output:\n" . join("\n", @$hostname_output)); |
| $error_occurred = 1; |
| } |
| else { |
| notify($ERRORS{'OK'}, 0, "set hostname using hostname command on $computer_node_name to $public_hostname"); |
| } |
| } |
| |
| return !$error_occurred; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 clear_private_keys |
| |
| Parameters : |
| Returns : |
| Description : |
| |
| =cut |
| |
| sub clear_private_keys { |
| my $self = shift; |
| unless (ref($self) && $self->isa('VCL::Module')) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine can only be called as a VCL::Module module object method"); |
| return; |
| } |
| |
| notify($ERRORS{'DEBUG'}, 0, "perparing to clear known identity keys"); |
| my $management_node_keys = $self->data->get_management_node_keys(); |
| my $computer_short_name = $self->data->get_computer_short_name(); |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| # Clear ssh idenity keys from /root/.ssh |
| my $clear_private_keys = "/bin/rm -f /root/.ssh/id_rsa /root/.ssh/id_rsa.pub"; |
| if ($self->execute($clear_private_keys)) { |
| notify($ERRORS{'DEBUG'}, 0, "cleared any id_rsa keys from /root/.ssh"); |
| return 1; |
| } |
| else { |
| notify($ERRORS{'CRITICAL'}, 0, "failed to clear any id_rsa keys from /root/.ssh"); |
| return 0; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 set_static_public_address |
| |
| Parameters : none |
| Returns : boolean |
| Description : Configures the public interface with a static IP address. |
| |
| =cut |
| |
| sub set_static_public_address { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return 0; |
| } |
| |
| my $computer_name = $self->data->get_computer_short_name(); |
| my $ip_configuration = $self->data->get_management_node_public_ip_configuration(); |
| my $public_ip_address = $self->data->get_computer_public_ip_address(); |
| my $subnet_mask = $self->data->get_management_node_public_subnet_mask(); |
| my @dns_servers = $self->data->get_management_node_public_dns_servers(); |
| |
| # TODO: Get this out of here. OS modules shouldn't have to figure this out. $self->data should always return correct value. |
| my $server_request_fixed_ip = $self->data->get_server_request_fixed_ip(); |
| if ($server_request_fixed_ip) { |
| $public_ip_address = $server_request_fixed_ip; |
| $subnet_mask = $self->data->get_server_request_netmask(); |
| @dns_servers = $self->data->get_server_request_dns_servers(); |
| } |
| |
| # Make sure public IP configuration is static or this is a server request |
| if ($ip_configuration !~ /static/i && !$server_request_fixed_ip) { |
| notify($ERRORS{'WARNING'}, 0, "management node IP configuration is $ip_configuration, static public IP address can only be set if the IP configuration is static or if a fixed IP was requested"); |
| return; |
| } |
| elsif (!$public_ip_address) { |
| notify($ERRORS{'WARNING'}, 0, "failed to retrieve public IP address to assign to $computer_name"); |
| return; |
| } |
| elsif (!$subnet_mask) { |
| notify($ERRORS{'WARNING'}, 0, "failed to retrieve public subnet mask to assign to $computer_name"); |
| return; |
| } |
| |
| # Determine the public interface name |
| my $public_interface_name = $self->get_public_interface_name(); |
| if (!$public_interface_name) { |
| notify($ERRORS{'WARNING'}, 0, "unable to set static public IP address, public interface name could not be determined"); |
| return; |
| } |
| |
| # Get the current public IP address being used by the computer |
| # Use cached data if available (0), ignore errors (1) |
| my $current_public_ip_address = $self->get_public_ip_address(0, 1); |
| if ($current_public_ip_address && $current_public_ip_address eq $public_ip_address) { |
| notify($ERRORS{'DEBUG'}, 0, "static public IP address does not need to be set, $computer_name is already configured to use $current_public_ip_address"); |
| } |
| else { |
| if ($current_public_ip_address) { |
| notify($ERRORS{'DEBUG'}, 0, "static public IP address needs to be set, public IP address currently being used by $computer_name $current_public_ip_address does NOT match correct public IP address: $public_ip_address"); |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "static public IP address needs to be set, unable to determine public IP address currently in use on $computer_name"); |
| } |
| |
| |
| # Try to ping address to make sure it's available |
| # FIXME -- need to add other tests for checking ip_address is or is not available. |
| if (_pingnode($public_ip_address)) { |
| notify($ERRORS{'CRITICAL'}, 0, "ip_address $public_ip_address is pingable, can not assign to $computer_name "); |
| return; |
| } |
| |
| notify($ERRORS{'DEBUG'}, 0, "attempting to set static public IP address on $computer_name:\n" . |
| "interface: $public_interface_name\n" . |
| "IP address: $public_ip_address\n" . |
| "subnet mask: $subnet_mask" |
| ); |
| |
| my $ifcfg_parameters = { |
| bootproto => 'static', |
| ipaddr => $public_ip_address, |
| netmask => $subnet_mask, |
| }; |
| |
| if (!$self->generate_ifcfg_file($public_interface_name, $ifcfg_parameters)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to set static public IP address on $computer_name, ifcfg file could not be created"); |
| return; |
| } |
| |
| # Restart the interface |
| if (!$self->restart_network_interface($public_interface_name)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to restart public interface $public_interface_name on $computer_name"); |
| return; |
| } |
| } |
| |
| # Set default gateway |
| if (!$self->set_static_default_gateway()) { |
| notify($ERRORS{'WARNING'}, 0, "failed to set static public IP address on $computer_name, default gateway could not be set"); |
| return; |
| } |
| |
| # Update resolv.conf if DNS server address is configured for the management node |
| if (@dns_servers) { |
| if (!$self->update_resolv_conf(@dns_servers)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to set static public IP address on $computer_name, DNS servers could not be configured"); |
| return; |
| } |
| } |
| |
| # Delete cached network configuration info - forces next call to get_network_configuration to retrieve changed network info from computer |
| delete $self->{network_configuration}; |
| |
| notify($ERRORS{'OK'}, 0, "set static public IP address on $computer_name"); |
| return 1; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 start_network_interface |
| |
| Parameters : $interface_name |
| Returns : boolean |
| Description : Calls ifup on the network interface. |
| |
| =cut |
| |
| sub start_network_interface { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return 0; |
| } |
| |
| my $interface_name = shift; |
| if (!$interface_name) { |
| notify($ERRORS{'WARNING'}, 0, "unable to start network interface, interface name argument was not supplied"); |
| return; |
| } |
| |
| my $computer_name = $self->data->get_computer_short_name(); |
| |
| notify($ERRORS{'DEBUG'}, 0, "attempting to start network interface $interface_name on $computer_name"); |
| |
| my $command = "/sbin/ifup $interface_name"; |
| my ($exit_status, $output) = $self->execute($command, 0); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to start $interface_name interface on $computer_name"); |
| return; |
| } |
| elsif (grep(/already configured/i, @$output)) { |
| notify($ERRORS{'DEBUG'}, 0, "$interface_name interface on $computer_name is already started, output:\n" . join("\n", @$output)); |
| } |
| elsif ($exit_status == 0 || grep(/done/i, @$output)) { |
| notify($ERRORS{'DEBUG'}, 0, "started $interface_name interface on $computer_name, " . (@$output ? "output:\n" . join("\n", @$output) : 'no output')); |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "failed to start $interface_name interface on $computer_name, exit status: $exit_status, command: '$command', output:\n" . join("\n", @$output)); |
| return; |
| } |
| |
| return 1; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 stop_network_interface |
| |
| Parameters : $interface_name |
| Returns : boolean |
| Description : Calls ifdown on the network interface. |
| |
| =cut |
| |
| sub stop_network_interface { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return 0; |
| } |
| |
| my $interface_name = shift; |
| if (!$interface_name) { |
| notify($ERRORS{'WARNING'}, 0, "unable to stop network interface, interface name argument was not supplied"); |
| return; |
| } |
| |
| my $computer_name = $self->data->get_computer_short_name(); |
| |
| notify($ERRORS{'DEBUG'}, 0, "attempting to stop network interface $interface_name on $computer_name"); |
| |
| my $command = "/sbin/ifdown $interface_name"; |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to stop $interface_name interface on $computer_name"); |
| return; |
| } |
| elsif (grep(/not configured/i, @$output)) { |
| notify($ERRORS{'DEBUG'}, 0, "$interface_name interface on $computer_name is already stopped, output:\n" . join("\n", @$output)); |
| return 1; |
| } |
| elsif ($exit_status) { |
| notify($ERRORS{'WARNING'}, 0, "failed to stop $interface_name interface on $computer_name, exit status: $exit_status, command: '$command', output:\n" . join("\n", @$output)); |
| return; |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "stopped $interface_name interface on $computer_name, output:\n" . join("\n", @$output)); |
| } |
| |
| return 1; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 restart_network_interface |
| |
| Parameters : $interface_name |
| Returns : boolean |
| Description : Calls ifdown and then ifup on the network interface. |
| |
| =cut |
| |
| sub restart_network_interface { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return 0; |
| } |
| |
| my $interface_name = shift; |
| if (!$interface_name) { |
| notify($ERRORS{'WARNING'}, 0, "unable to restart network interface, interface name argument was not supplied"); |
| return; |
| } |
| |
| my $computer_name = $self->data->get_computer_short_name(); |
| |
| notify($ERRORS{'DEBUG'}, 0, "attempting to restart network interface $interface_name on $computer_name"); |
| |
| my $command = "/sbin/ifdown $interface_name ; /sbin/ifup $interface_name"; |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to restart $interface_name interface on $computer_name"); |
| return; |
| } |
| elsif ($exit_status) { |
| notify($ERRORS{'WARNING'}, 0, "failed to restart $interface_name interface on $computer_name, exit status: $exit_status, command: '$command', output:\n" . join("\n", @$output)); |
| return; |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "restarted $interface_name interface on $computer_name, output:\n" . join("\n", @$output)); |
| } |
| |
| return 1; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 delete_default_gateway |
| |
| Parameters : none |
| Returns : boolean |
| Description : Deletes the existing default gateway from the routing table. |
| |
| =cut |
| |
| sub delete_default_gateway { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return 0; |
| } |
| |
| my $computer_name = $self->data->get_computer_short_name(); |
| |
| my $command = "/sbin/route del default"; |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to delete default gateway on $computer_name: $command"); |
| return; |
| } |
| elsif (grep(/No such process/i, @$output)) { |
| notify($ERRORS{'DEBUG'}, 0, "default gateway not set on $computer_name"); |
| } |
| elsif ($exit_status ne '0') { |
| notify($ERRORS{'WARNING'}, 0, "failed to delete default gateway on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); |
| return 0; |
| } |
| else { |
| notify($ERRORS{'OK'}, 0, "deleted default gateway on $computer_name"); |
| } |
| |
| return 1; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 set_static_default_gateway |
| |
| Parameters : none |
| Returns : boolean |
| Description : Sets the default route. |
| |
| =cut |
| |
| sub set_static_default_gateway { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return 0; |
| } |
| |
| my $computer_name = $self->data->get_computer_short_name(); |
| |
| my $default_gateway = $self->get_correct_default_gateway(); |
| if (!$default_gateway) { |
| notify($ERRORS{'WARNING'}, 0, "unable to set static default gateway on $computer_name, correct default gateway IP address could not be determined"); |
| return; |
| } |
| |
| my $current_default_gateway = $self->get_public_default_gateway(); |
| if ($current_default_gateway && $current_default_gateway eq $default_gateway) { |
| notify($ERRORS{'OK'}, 0, "default gateway on $computer_name is already set to $current_default_gateway"); |
| return 1; |
| } |
| |
| my $interface_name = $self->get_public_interface_name(); |
| if (!$interface_name) { |
| notify($ERRORS{'WARNING'}, 0, "unable to set static default gateway on $computer_name, public interface name could not be determined"); |
| return; |
| } |
| |
| # Delete existing default gateway or else error will occur: SIOCADDRT: File exists |
| $self->delete_default_gateway(); |
| |
| my $command = "/sbin/route add default gw $default_gateway metric 0 $interface_name"; |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to set default gateway on $computer_name: $command"); |
| return; |
| } |
| elsif ($exit_status ne '0') { |
| notify($ERRORS{'WARNING'}, 0, "failed to set default gateway on $computer_name to $default_gateway, interface: $interface_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); |
| return 0; |
| } |
| |
| # Create a route file so default route persists across reboots |
| my $route_file_path = "/etc/sysconfig/network-scripts/route-$interface_name"; |
| # For testing: |
| #$self->delete_file($route_file_path); |
| my $route_file_contents = "default via $default_gateway dev $interface_name"; |
| $self->create_text_file($route_file_path, $route_file_contents); |
| |
| # Adding a route-* file does not prevent computer from obtaining a default route via DHCP |
| # Add a 'DEFROUTE=no' line to the ifcfg-<interface> file |
| my $interface_file = "/etc/sysconfig/network-scripts/ifcfg-$interface_name"; |
| # For testing: |
| #$self->remove_lines_from_file($interface_file, 'DEFROUTE'); |
| if ($self->file_exists($interface_file)) { |
| $self->set_config_file_parameter($interface_file, 'DEFROUTE', '=', 'no'); |
| } |
| |
| # Note: leave for future reference, this doesn't seem to work on CentOS/RHEL 7 |
| # Add a 'GATEWAY=' line to /etc/sysconfig/network |
| #my $network_file = "/etc/sysconfig/network"; |
| # For testing: $self->remove_lines_from_file($network_file, 'GATEWAY'); |
| #$self->set_config_file_parameter($network_file, 'GATEWAY', '=', $default_gateway); |
| |
| notify($ERRORS{'OK'}, 0, "set default gateway on $computer_name to $default_gateway, interface: $interface_name"); |
| return 1; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 logoff_user |
| |
| Parameters : |
| Returns : |
| Description : |
| |
| =cut |
| |
| sub logoff_user { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return 0; |
| } |
| |
| # Make sure the user login ID was passed |
| my $user_login_id = shift || $self->data->get_user_login_id(); |
| if (!$user_login_id) { |
| notify($ERRORS{'WARNING'}, 0, "user could not be determined"); |
| return 0; |
| } |
| |
| # Make sure the user login ID was passed |
| my $computer_node_name = shift || $self->data->get_computer_node_name(); |
| if (!$computer_node_name) { |
| notify($ERRORS{'WARNING'}, 0, "computer node name could not be determined"); |
| return 0; |
| } |
| |
| my $logoff_cmd = "pkill -KILL -u $user_login_id"; |
| my ($exit_status, $output) = $self->execute($logoff_cmd); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to log off $user_login_id from $computer_node_name"); |
| return; |
| } |
| elsif (grep(/invalid user name/i, @$output)) { |
| notify($ERRORS{'DEBUG'}, 0, "user $user_login_id does not exist on $computer_node_name"); |
| return 1; |
| } |
| elsif ($exit_status ne '0' && $exit_status ne '1') { |
| # pkill will exit with status = 1 if one or more processes were killed, and 1 if no processes matched |
| notify($ERRORS{'WARNING'}, 0, "error occurred attempting to log off $user_login_id from $computer_node_name, exit status: $exit_status, output:\n" . join("\n", @$output)); |
| return; |
| } |
| else { |
| notify($ERRORS{'OK'}, 0, "logged off $user_login_id from $computer_node_name"); |
| } |
| |
| return 1; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 reserve |
| |
| Parameters : none |
| Returns : boolean |
| Description : Performs the steps necessary to reserve a computer for a user. |
| A "vcl" user group is added. |
| |
| =cut |
| |
| sub reserve { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return 0; |
| } |
| |
| notify($ERRORS{'OK'}, 0, "beginning Linux reserve tasks"); |
| |
| # Add a local vcl user group if it doesn't already exist |
| # Do this before OS.pm::reserve calls add_user_accounts |
| $self->add_vcl_usergroup(); |
| |
| # Configure sshd to only listen on the private interface and add ext_sshd service listening on the public interface |
| # This needs to be done after update_public_ip_address is called from OS.pm::reserve |
| $self->configure_ext_sshd() || return; |
| |
| # Call OS.pm's reserve subroutine |
| $self->SUPER::reserve() || return; |
| |
| # Attempt to mount NFS shares configured for the management node (Site Configuration > NFS Mounts) |
| $self->mount_nfs_shares(); |
| |
| notify($ERRORS{'OK'}, 0, "Linux reserve tasks complete"); |
| return 1; |
| } ## end sub reserve |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 grant_access |
| |
| Parameters : none |
| Returns : boolean |
| Description : adds username to external_sshd_config and and starts sshd with |
| custom config |
| |
| =cut |
| |
| sub grant_access { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return 0; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| if ($self->can('firewall') && $self->firewall->can('process_reserved')) { |
| if (!$self->firewall->process_reserved()) { |
| notify($ERRORS{'WARNING'}, 0, "failed to grant access to $computer_node_name, firewall configuration failed"); |
| return; |
| } |
| } |
| |
| # Process the connection methods, allow firewall access from any address |
| if ($self->process_connect_methods("", 1)) { |
| notify($ERRORS{'DEBUG'}, 0, "granted access to $computer_node_name by processing the connection methods"); |
| return 1; |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "failed to grant access to $computer_node_name by processing the connection methods"); |
| return; |
| } |
| } ## end sub grant_access |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 synchronize_time |
| |
| Parameters : none |
| Returns : boolean |
| Description : |
| |
| =cut |
| |
| sub synchronize_time { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return 0; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| my $management_node_hostname = $self->data->get_management_node_hostname(); |
| |
| my $variable_name = "timesource|$management_node_hostname"; |
| my $variable_name_global = "timesource|global"; |
| |
| my $time_source_variable; |
| if (is_variable_set($variable_name)) { |
| $time_source_variable = get_variable($variable_name); |
| notify($ERRORS{'DEBUG'}, 0, "retrieved time source variable '$variable_name': $time_source_variable"); |
| } |
| elsif (is_variable_set($variable_name_global)) { |
| $time_source_variable = get_variable($variable_name_global); |
| notify($ERRORS{'DEBUG'}, 0, "retrieved global time source variable '$variable_name_global': $time_source_variable"); |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "unable to sync time, neither '$variable_name' or '$variable_name_global' time source variable is set in database"); |
| return; |
| } |
| |
| # Split the time source variable into an array |
| my @time_sources = split(/[,; ]+/, $time_source_variable); |
| |
| # Assemble the rdate command |
| # Ubuntu doesn't accept multiple servers in a single command |
| my $rdate_command; |
| for my $time_source (@time_sources) { |
| $rdate_command .= "rdate -t 3 -s $time_source || "; |
| } |
| $rdate_command =~ s/[ \|]+$//g; |
| my ($rdate_exit_status, $rdate_output) = $self->execute($rdate_command, 0, 180); |
| if (!defined($rdate_output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute rdate command to synchronize time on $computer_node_name"); |
| return; |
| } |
| elsif (grep(/not found/i, @$rdate_output)) { |
| notify($ERRORS{'DEBUG'}, 0, "unable to synchronize time on $computer_node_name, rdate is not installed"); |
| } |
| elsif ($rdate_exit_status > 0) { |
| notify($ERRORS{'WARNING'}, 0, "failed to synchronize time on $computer_node_name using rdate, exit status: $rdate_exit_status, command:\n$rdate_command\noutput:\n" . join("\n", @$rdate_output)); |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "synchronized time on $computer_node_name using rdate"); |
| } |
| |
| # Check if the ntpd service exists before attempting to configure it |
| if (!$self->service_exists('ntpd')) { |
| notify($ERRORS{'DEBUG'}, 0, "skipping ntpd configuration, ntpd service does not exist"); |
| return 1; |
| } |
| |
| # Update ntpservers file |
| my $ntpservers_contents = join("\n", @time_sources); |
| if (!$self->create_text_file('/etc/ntp/ntpservers', $ntpservers_contents)) { |
| return; |
| } |
| |
| return $self->restart_service('ntpd'); |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 set_password |
| |
| Parameters : $username, $password (optional) |
| Returns : boolean |
| Description : Sets password for the account specified by the username argument. |
| If no password argument is supplied, a random password is |
| generated. |
| |
| =cut |
| |
| sub set_password { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return 0; |
| } |
| |
| my $username = shift; |
| my $password = shift; |
| |
| if (!$username) { |
| notify($ERRORS{'WARNING'}, 0, "username argument was not provided"); |
| return; |
| } |
| |
| if (!$password) { |
| $password = getpw(15); |
| } |
| |
| my $command = "echo -e '"; |
| $command .= qq[$password]; |
| $command .= "' \| /usr/bin/passwd -f $username --stdin"; |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to set password for $username"); |
| return; |
| } |
| elsif (grep(/(unknown user|warning|error)/i, @$output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to change password for $username to '$password', command: '$command', output:\n" . join("\n", @$output)); |
| return; |
| } |
| else { |
| notify($ERRORS{'OK'}, 0, "changed password for $username to '$password', output:\n" . join("\n", @$output)); |
| return 1; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 sanitize |
| |
| Parameters : |
| Returns : |
| Description : |
| |
| =cut |
| |
| sub sanitize { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| # Make sure user is not connected |
| if ($self->is_connected()) { |
| notify($ERRORS{'WARNING'}, 0, "unable to sanitize $computer_node_name, user is connected"); |
| return 0; |
| } |
| |
| if ($self->can('firewall') && $self->firewall->can('process_sanitize')) { |
| $self->firewall->process_sanitize() || return; |
| } |
| |
| # Call process_connect_methods with the overwrite flag to remove firewall exceptions |
| $self->process_connect_methods() || return; |
| |
| # Attempt to unmount NFS shares configured for the management node (Site Configuration > NFS Mounts) |
| $self->unmount_nfs_shares() || return; |
| $self->remove_matching_fstab_lines('Added by VCL'); |
| |
| # Delete all user associated with the reservation |
| $self->delete_user_accounts() || return; |
| |
| # Make sure ext_sshd is stopped |
| $self->stop_external_sshd() || return; |
| |
| notify($ERRORS{'OK'}, 0, "$computer_node_name has been sanitized"); |
| return 1; |
| } ## end sub sanitize |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 add_vcl_usergroup |
| |
| Parameters : |
| Returns : 1 |
| Description : step to add a user group to avoid group errors from useradd cmd |
| |
| =cut |
| |
| sub add_vcl_usergroup { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| if ($self->execute("groupadd vcl")) { |
| notify($ERRORS{'DEBUG'}, 0, "successfully added the vcl user group to $computer_node_name"); |
| } |
| |
| return 1; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 is_connected |
| |
| Parameters : none |
| Returns : boolean, undefined if error occurred |
| Description : Checks if a connection on port 22 is established to the |
| computer's public IP address. |
| |
| =cut |
| |
| sub is_connected { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my $computer_public_ip_address = $self->data->get_computer_public_ip_address(); |
| if (!$computer_public_ip_address) { |
| notify($ERRORS{'WARNING'}, 0, "unable to determine if connection exists to $computer_node_name, public IP address could not be determined"); |
| return; |
| } |
| |
| my $command = "netstat -an | grep ESTABLISHED"; |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command on $computer_node_name: $command"); |
| return; |
| } |
| |
| if (grep(/(Warning|Connection refused)/i, @$output)) { |
| notify($ERRORS{'WARNING'}, 0, "unable to determine if connection exists to $computer_public_ip_address on $computer_node_name, output:\n" . join("\n", @$output)); |
| return; |
| } |
| elsif (my ($line) = grep(/tcp\s+([0-9]*)\s+([0-9]*)\s($computer_public_ip_address:22)\s+([.0-9]*):([0-9]*)(.*)(ESTABLISHED)/, @$output)) { |
| notify($ERRORS{'DEBUG'}, 0, "connection exists to $computer_public_ip_address on $computer_node_name:\n$line"); |
| return 1; |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "connection does not exist to $computer_public_ip_address on $computer_node_name"); |
| return 0; |
| } |
| } ## end sub is_connected |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 run_script |
| |
| Parameters : script path |
| Returns : boolean |
| Description : Checks if script exists on the Linux node and attempts to run it. |
| |
| =cut |
| |
| sub run_script { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| # Get the script path argument |
| my $script_path = shift; |
| if (!$script_path) { |
| notify($ERRORS{'WARNING'}, 0, "script path argument was not specified"); |
| return; |
| } |
| my $display_output = shift || 0; |
| my $timeout_seconds = shift || 60; |
| my $max_attempts = shift || 3; |
| |
| # Check if script exists |
| if ($self->file_exists($script_path)) { |
| notify($ERRORS{'DEBUG'}, 0, "script exists: $script_path"); |
| } |
| else { |
| notify($ERRORS{'OK'}, 0, "script does NOT exist: $script_path"); |
| return 0; |
| } |
| |
| # Determine the script name |
| my ($script_name) = $script_path =~ /\/([^\/]+)$/; |
| notify($ERRORS{'DEBUG'}, 0, "script name: $script_name"); |
| |
| # Get the node configuration directory, make sure it exists, create if necessary |
| my $node_log_directory = $self->get_node_configuration_directory() . '/Logs'; |
| if (!$self->create_directory($node_log_directory)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to create node log file directory: $node_log_directory"); |
| return; |
| } |
| |
| my $management_node_keys = $self->data->get_management_node_keys(); |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| # Assemble the log file path |
| my $log_file_path = $node_log_directory . "/$script_name.log"; |
| notify($ERRORS{'DEBUG'}, 0, "script log file path: $log_file_path"); |
| |
| # Assemble the command |
| my $command = "chmod +rx \"$script_path\" && \"$script_path\" >> \"$log_file_path\" 2>&1"; |
| |
| # Execute the command |
| my ($exit_status, $output) = $self->execute($command, $display_output, $timeout_seconds, $max_attempts); |
| if (defined($exit_status) && $exit_status == 0) { |
| notify($ERRORS{'OK'}, 0, "executed $script_path, exit status: $exit_status"); |
| } |
| elsif (defined($exit_status)) { |
| notify($ERRORS{'WARNING'}, 0, "$script_path returned a non-zero exit status: $exit_status, command: '$command'"); |
| return; |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to execute $script_path"); |
| return; |
| } |
| |
| return 1; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 file_exists |
| |
| Parameters : $file_path, $display_output (optional) |
| Returns : boolean |
| Description : Checks if a file or directory exists on the Linux computer. |
| |
| =cut |
| |
| sub file_exists { |
| my $self = shift; |
| if (ref($self) !~ /module/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return 0; |
| } |
| |
| # Get the path from the subroutine arguments and make sure it was passed |
| my $file_path = shift; |
| if (!$file_path) { |
| notify($ERRORS{'WARNING'}, 0, "path argument was not specified"); |
| return 0; |
| } |
| |
| my $display_output = shift; |
| if (!defined($display_output)) { |
| $display_output = 1; |
| } |
| |
| # Remove any quotes from the beginning and end of the path |
| $file_path = normalize_file_path($file_path); |
| |
| # Escape all spaces in the path |
| my $escaped_path = escape_file_path($file_path); |
| |
| my $computer_short_name = $self->data->get_computer_short_name(); |
| |
| # Check if the file or directory exists |
| # Do not enclose the path in quotes or else wildcards won't work |
| my $command = "stat $escaped_path"; |
| my ($exit_status, $output) = $self->execute($command, 0); |
| if (!defined($output)) { |
| notify($ERRORS{'DEBUG'}, 0, "failed to run command to determine if file or directory exists on $computer_short_name:\npath: '$file_path'\ncommand: '$command'"); |
| return 0; |
| } |
| elsif (grep(/no such file/i, @$output)) { |
| notify($ERRORS{'DEBUG'}, 0, "file or directory does not exist on $computer_short_name: '$file_path'") if $display_output; |
| return 0; |
| } |
| elsif (grep(/stat: /i, @$output)) { |
| notify($ERRORS{'DEBUG'}, 0, "failed to determine if file or directory exists on $computer_short_name:\npath: '$file_path'\ncommand: '$command'\nexit status: $exit_status, output:\n" . join("\n", @$output)); |
| return 0; |
| } |
| |
| # Count the lines beginning with "Size:" and ending with "file", "directory", or "link" to determine how many files and/or directories were found |
| my $files_found = grep(/^\s*Size:.*file$/i, @$output); |
| my $directories_found = grep(/^\s*Size:.*directory$/i, @$output); |
| my $links_found = grep(/^\s*Size:.*link$/i, @$output); |
| |
| if ($files_found || $directories_found || $links_found) { |
| notify($ERRORS{'DEBUG'}, 0, "'$file_path' exists on $computer_short_name, files: $files_found, directories: $directories_found, links: $links_found") if $display_output; |
| return 1; |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "unexpected output returned while attempting to determine if file or directory exists on $computer_short_name: '$file_path'\ncommand: '$command'\nexit status: $exit_status, output:\n" . join("\n", @$output)); |
| return 0; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 delete_file |
| |
| Parameters : $path |
| Returns : boolean |
| Description : Deletes files or directories on the Linux computer. |
| |
| =cut |
| |
| sub delete_file { |
| my $self = shift; |
| if (ref($self) !~ /module/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| # Get the path argument |
| my $path = shift; |
| if (!$path) { |
| notify($ERRORS{'WARNING'}, 0, "path argument were not specified"); |
| return; |
| } |
| |
| # Remove any quotes from the beginning and end of the path |
| $path = normalize_file_path($path); |
| |
| # Escape all spaces in the path |
| my $escaped_path = escape_file_path($path); |
| |
| my $computer_short_name = $self->data->get_computer_short_name(); |
| |
| # Delete the file |
| my $command = "rm -rfv $escaped_path"; |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to run command to delete file or directory on $computer_short_name:\npath: '$path'\ncommand: '$command'"); |
| return; |
| } |
| elsif (grep(/(cannot access|no such file)/i, @$output)) { |
| notify($ERRORS{'OK'}, 0, "file or directory not deleted because it does not exist on $computer_short_name: $path"); |
| } |
| elsif (grep(/rm: /i, @$output)) { |
| notify($ERRORS{'WARNING'}, 0, "error occurred attempting to delete file or directory on $computer_short_name: '$path':\ncommand: '$command'\nexit status: $exit_status\noutput:\n" . join("\n", @$output)); |
| } |
| else { |
| notify($ERRORS{'OK'}, 0, "deleted '$path' on $computer_short_name"); |
| } |
| |
| # Make sure the path does not exist |
| my $file_exists = $self->file_exists($path, 0); |
| if (!defined($file_exists)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to confirm file doesn't exist on $computer_short_name: '$path'"); |
| return; |
| } |
| elsif ($file_exists) { |
| notify($ERRORS{'WARNING'}, 0, "file was not deleted, it still exists on $computer_short_name: '$path'"); |
| return; |
| } |
| else { |
| #notify($ERRORS{'DEBUG'}, 0, "confirmed file does not exist on $computer_short_name: '$path'"); |
| return 1; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 clear_file |
| |
| Parameters : $file_path |
| Returns : boolean |
| Description : Clears a file on the computer via 'cat /dev/null'. If the file |
| doesn't exist it is not created and true is returned. |
| |
| =cut |
| |
| sub clear_file { |
| my $self = shift; |
| if (ref($self) !~ /module/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| # Get the path argument |
| my $file_path = shift; |
| if (!$file_path) { |
| notify($ERRORS{'WARNING'}, 0, "file path argument was not specified"); |
| return; |
| } |
| |
| my $computer_short_name = $self->data->get_computer_short_name(); |
| |
| # Check if the file exists |
| if (!$self->file_exists($file_path, 0)) { |
| notify($ERRORS{'DEBUG'}, 0, "file not cleared on $computer_short_name because it doesn't exist: $file_path"); |
| return 1; |
| } |
| |
| # Remove any quotes from the beginning and end of the path |
| $file_path = normalize_file_path($file_path); |
| |
| # Escape all spaces in the path |
| my $escaped_file_path = escape_file_path($file_path); |
| |
| # Clear the file |
| my $command = "cat /dev/null > $escaped_file_path"; |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to clear file on $computer_short_name: '$file_path'"); |
| return; |
| } |
| elsif ($exit_status ne 0) { |
| notify($ERRORS{'WARNING'}, 0, "error occurred attempting to clear file on $computer_short_name: '$file_path', exit status: $exit_status, command: '$command', output:\n" . join("\n", @$output)); |
| return; |
| } |
| else { |
| notify($ERRORS{'OK'}, 0, "cleared file on $computer_short_name: '$file_path'"); |
| } |
| |
| return 1; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 create_directory |
| |
| Parameters : $directory_path |
| Returns : boolean |
| Description : Creates a directory on the Linux computer as indicated by the |
| $directory_path argument. |
| |
| =cut |
| |
| sub create_directory { |
| my $self = shift; |
| if (ref($self) !~ /module/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| # Get the directory path argument |
| my $directory_path = shift; |
| if (!$directory_path) { |
| notify($ERRORS{'WARNING'}, 0, "directory path argument was not supplied"); |
| return; |
| } |
| |
| # Remove any quotes from the beginning and end of the path |
| $directory_path = normalize_file_path($directory_path); |
| |
| # If ~ is passed as the directory path, skip directory creation attempt |
| # The command will create a /root/~ directory since the path is enclosed in quotes |
| return 1 if $directory_path eq '~'; |
| |
| my $computer_short_name = $self->data->get_computer_short_name(); |
| |
| # Attempt to create the directory |
| my $command = "ls -d --color=never \"$directory_path\" 2>/dev/null || (mkdir -p \"$directory_path\" 2>&1 && ls -d --color=never \"$directory_path\")"; |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to run command to create directory on $computer_short_name:\npath: '$directory_path'\ncommand: '$command'"); |
| return; |
| } |
| elsif (grep(/mkdir:/i, @$output)) { |
| notify($ERRORS{'WARNING'}, 0, "error occurred attempting to create directory on $computer_short_name: '$directory_path':\ncommand: '$command'\nexit status: $exit_status\noutput:\n" . join("\n", @$output)); |
| return; |
| } |
| elsif (grep(/^\s*$directory_path\s*$/, @$output)) { |
| if (grep(/ls:/, @$output)) { |
| notify($ERRORS{'OK'}, 0, "directory created on $computer_short_name: '$directory_path'"); |
| } |
| else { |
| #notify($ERRORS{'OK'}, 0, "directory already exists on $computer_short_name: '$directory_path'"); |
| } |
| return 1; |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "unexpected output returned from command to create directory on $computer_short_name: '$directory_path':\ncommand: '$command'\nexit status: $exit_status\noutput:\n" . join("\n", @$output) . "\nlast line:\n" . string_to_ascii(@$output[-1])); |
| return; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 move_file |
| |
| Parameters : $source_path, $destination_path |
| Returns : boolean |
| Description : Moves or renames a file on a Linux computer. |
| |
| =cut |
| |
| sub move_file { |
| my $self = shift; |
| if (ref($self) !~ /module/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| # Get the path arguments |
| my $source_path = shift; |
| my $destination_path = shift; |
| if (!$source_path || !$destination_path) { |
| notify($ERRORS{'WARNING'}, 0, "source and destination path arguments were not specified"); |
| return; |
| } |
| |
| # Remove any quotes from the beginning and end of the path |
| $source_path = normalize_file_path($source_path); |
| $destination_path = normalize_file_path($destination_path); |
| |
| # Escape all spaces in the path |
| my $escaped_source_path = escape_file_path($source_path); |
| my $escaped_destination_path = escape_file_path($destination_path); |
| |
| my $computer_short_name = $self->data->get_computer_short_name(); |
| |
| # Execute the command to move the file |
| my $command = "mv -f $escaped_source_path $escaped_destination_path"; |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to run command to move file on $computer_short_name:\nsource path: '$source_path'\ndestination path: '$destination_path'\ncommand: '$command'"); |
| return; |
| } |
| elsif (grep(/^mv: /i, @$output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to move file on $computer_short_name:\nsource path: '$source_path'\ndestination path: '$destination_path'\ncommand: '$command'\noutput:\n" . join("\n", @$output)); |
| return; |
| } |
| else { |
| notify($ERRORS{'OK'}, 0, "moved file on $computer_short_name:\n'$source_path' --> '$destination_path'"); |
| return 1; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 get_available_space |
| |
| Parameters : $path |
| Returns : If successful: integer |
| If failed: undefined |
| Description : Returns the bytes available in the path specified by the |
| argument. 0 is returned if no space is available. Undefined is |
| returned if an error occurred. |
| |
| =cut |
| |
| sub get_available_space { |
| my $self = shift; |
| if (ref($self) !~ /module/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| # Get the path argument |
| my $path = shift; |
| if (!$path) { |
| notify($ERRORS{'WARNING'}, 0, "path argument was not specified"); |
| return; |
| } |
| |
| my $computer_short_name = $self->data->get_computer_short_name(); |
| |
| # Run stat -f specifying the path as an argument |
| # Don't use df because you can't specify a path under ESX and parsing would be difficult |
| my $command = "stat -f \"$path\""; |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to determine available space on $computer_short_name, command: $command"); |
| return; |
| } |
| elsif (grep(/^stat: /i, @$output)) { |
| notify($ERRORS{'WARNING'}, 0, "error occurred running command to determine available space on $computer_short_name\ncommand: $command\noutput:\n" . join("\n", @$output)); |
| return; |
| } |
| |
| # Create an output string from the array of lines for easier regex parsing |
| my $output_string = join("\n", @$output); |
| |
| # Extract the block size value |
| # Search case sensitive for 'Block size:' because the line may also contain "Fundamental block size:" |
| # Some versions of Linux may not display a "Size:" value instead of "Block size:" |
| # Blocks: Total: 8720776 Free: 8288943 Available: 7845951 Size: 4096 |
| my ($block_size) = $output_string =~ /(?:Block size|Size): (\d+)/; |
| if (!$block_size) { |
| notify($ERRORS{'WARNING'}, 0, "unable to locate 'Block size:' or 'Size:' value in stat output:\ncommand: $command\noutput:\n" . join("\n", @$output)); |
| return; |
| } |
| |
| # Extract the blocks free value |
| my ($blocks_available) = $output_string =~ /Blocks:[^\n]*Available: (\d+)/; |
| if (!defined($blocks_available)) { |
| notify($ERRORS{'WARNING'}, 0, "unable to locate blocks available value in stat output:\ncommand: $command\noutput:\n" . join("\n", @$output)); |
| return; |
| } |
| |
| # Calculate the bytes available |
| my $bytes_available = ($block_size * $blocks_available); |
| my $mb_available = format_number(($bytes_available / 1024 / 1024), 2); |
| my $gb_available = format_number(($bytes_available / 1024 / 1024 / 1024), 1); |
| |
| notify($ERRORS{'DEBUG'}, 0, "space available on volume on $computer_short_name containing '$path': " . get_file_size_info_string($bytes_available)); |
| return $bytes_available; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 get_total_space |
| |
| Parameters : $path |
| Returns : If successful: integer |
| If failed: undefined |
| Description : Returns the total size in bytes of the volume where the path |
| resides specified by the argument. Undefined is returned if an |
| error occurred. |
| |
| =cut |
| |
| sub get_total_space { |
| my $self = shift; |
| if (ref($self) !~ /module/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| # Get the path argument |
| my $path = shift; |
| if (!$path) { |
| notify($ERRORS{'WARNING'}, 0, "path argument was not specified"); |
| return; |
| } |
| |
| my $computer_short_name = $self->data->get_computer_short_name(); |
| |
| # Run stat -f specifying the path as an argument |
| # Don't use df because you can't specify a path under ESX and parsing would be difficult |
| my $command = "stat -f \"$path\""; |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to determine available space on $computer_short_name, command: $command"); |
| return; |
| } |
| elsif (grep(/^stat: /i, @$output)) { |
| notify($ERRORS{'WARNING'}, 0, "error occurred running command to determine available space on $computer_short_name\ncommand: $command\noutput:\n" . join("\n", @$output)); |
| return; |
| } |
| |
| # Create an output string from the array of lines for easier regex parsing |
| my $output_string = join("\n", @$output); |
| |
| # Extract the block size value |
| # Search case sensitive for 'Block size:' because the line may also contain "Fundamental block size:" |
| # Some versions of Linux may not display a "Size:" value instead of "Block size:" |
| # Blocks: Total: 8720776 Free: 8288943 Available: 7845951 Size: 4096 |
| my ($block_size) = $output_string =~ /(?:Block size|Size): (\d+)/; |
| if (!$block_size) { |
| notify($ERRORS{'WARNING'}, 0, "unable to locate 'Block size:' or 'Size:' value in stat output:\ncommand: $command\noutput:\n" . join("\n", @$output)); |
| return; |
| } |
| |
| # Extract the blocks total value |
| my ($blocks_total) = $output_string =~ /Blocks:[^\n]*Total: (\d+)/; |
| if (!defined($blocks_total)) { |
| notify($ERRORS{'WARNING'}, 0, "unable to locate blocks total value in stat output:\ncommand: $command\noutput:\n" . join("\n", @$output)); |
| return; |
| } |
| |
| # Calculate the bytes free |
| my $bytes_total = ($block_size * $blocks_total); |
| my $mb_total = format_number(($bytes_total / 1024 / 1024), 2); |
| my $gb_total = format_number(($bytes_total / 1024 / 1024 / 1024), 1); |
| |
| notify($ERRORS{'DEBUG'}, 0, "total size of volume on $computer_short_name containing '$path': " . get_file_size_info_string($bytes_total)); |
| return $bytes_total; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 copy_file_from |
| |
| Parameters : $source_file_path, $destination_file_path |
| Returns : boolean |
| Description : Copies file(s) from the Linux computer to the management node. |
| |
| =cut |
| |
| sub copy_file_from { |
| my $self = shift; |
| if (ref($self) !~ /VCL::Module/) { |
| 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_file_path, $destination_file_path) = @_; |
| if (!$source_file_path || !$destination_file_path) { |
| notify($ERRORS{'WARNING'}, 0, "source and destination file path arguments were not specified"); |
| return; |
| } |
| |
| # Get the computer name |
| my $computer_node_name = $self->data->get_computer_node_name() || return; |
| |
| # Get the destination parent directory path and create the directory on the management node |
| my $destination_directory_path = parent_directory_path($destination_file_path); |
| if (!$destination_directory_path) { |
| notify($ERRORS{'WARNING'}, 0, "unable to determine destination parent directory path: $destination_file_path"); |
| return; |
| } |
| create_management_node_directory($destination_directory_path) || return; |
| |
| # Get the identity keys used by the management node |
| my $management_node_keys = $self->data->get_management_node_keys() || ''; |
| |
| # Run the SCP command |
| if (run_scp_command("$computer_node_name:\"$source_file_path\"", $destination_file_path, $management_node_keys)) { |
| notify($ERRORS{'DEBUG'}, 0, "copied file from $computer_node_name to management node: $computer_node_name:'$source_file_path' --> '$destination_file_path'"); |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "failed to copy file from $computer_node_name to management node: $computer_node_name:'$source_file_path' --> '$destination_file_path'"); |
| return; |
| } |
| |
| return 1; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 get_file_size |
| |
| Parameters : @file_paths |
| Returns : integer or array |
| Description : Determines the size of the file specified by the file path |
| argument in bytes. The file path argument may be a directory or |
| contain wildcards. Directories are processed recursively. |
| |
| When called in sclar context, the actual bytes used on the disk by the file |
| is returned. This correlates to the size reported by the `du` |
| command. This value is not the same as what is reported by the `ls` |
| command. This is important when determining the size of |
| compressed files or thinly-provisioned virtual disk images. |
| |
| When called in array context, 3 values are returned: |
| [0] bytes used (`du` size) |
| [1] bytes reserved (`ls` size) |
| [2] file count |
| |
| =cut |
| |
| sub get_file_size { |
| 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 $calling_sub = (caller(1))[3] || ''; |
| |
| # Get the path argument |
| my @file_paths = @_; |
| if (!@file_paths) { |
| notify($ERRORS{'WARNING'}, 0, "file paths argument was not specified"); |
| return; |
| } |
| |
| # Get the computer name |
| my $computer_node_name = $self->data->get_computer_node_name() || return; |
| |
| my $file_count = 0; |
| my $total_bytes_reserved = 0; |
| my $total_bytes_used = 0; |
| |
| for my $file_path (@file_paths) { |
| # Normalize the file path |
| $file_path = normalize_file_path($file_path); |
| |
| # Escape all spaces in the path |
| my $escaped_file_path = escape_file_path($file_path); |
| |
| # Run stat rather than du because du is not available on VMware ESX |
| # -L Dereference links |
| # %F File type |
| # %n File name |
| # %b Number of blocks allocated (see %B) |
| # %B The size in bytes of each block reported by %b |
| # %s Total size, in bytes |
| |
| my $command = 'stat -L -c "%F:%n:%s:%b:%B" ' . $escaped_file_path; |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to run command to determine file size on $computer_node_name: $file_path\ncommand: '$command'"); |
| return; |
| } |
| elsif (grep(/no such file/i, @$output)) { |
| if ($calling_sub !~ /get_file_size/) { |
| notify($ERRORS{'DEBUG'}, 0, "unable to determine size of file on $computer_node_name because it does not exist: $file_path\ncommand: '$command'"); |
| } |
| return; |
| } |
| elsif (grep(/^stat:/i, @$output)) { |
| notify($ERRORS{'WARNING'}, 0, "error occurred attempting to determine file size on $computer_node_name: $file_path\ncommand: $command\noutput:\n" . join("\n", @$output)); |
| return; |
| } |
| |
| # Loop through the stat output lines |
| for my $line (@$output) { |
| # Take the stat output line apart |
| my ($type, $path, $file_bytes, $file_blocks, $block_size) = split(/:/, $line); |
| if (!defined($type) || !defined($file_bytes) || !defined($file_blocks) || !defined($block_size) || !defined($path)) { |
| notify($ERRORS{'WARNING'}, 0, "unexpected output returned from stat, line: $line\ncommand: $command\noutput:\n" . join("\n", @$output)); |
| return; |
| } |
| |
| # Add the size to the total if the type is file |
| if ($type =~ /file/) { |
| $file_count++; |
| |
| my $file_bytes_allocated = ($file_blocks * $block_size); |
| |
| $total_bytes_used += $file_bytes_allocated; |
| $total_bytes_reserved += $file_bytes; |
| } |
| elsif ($type =~ /directory/) { |
| $path =~ s/[\\\/\*]+$//g; |
| #notify($ERRORS{'DEBUG'}, 0, "recursively retrieving size of files under directory: '$path'"); |
| my ($subdirectory_bytes_allocated, $subdirectory_bytes_used, $subdirectory_file_count) = $self->get_file_size("$path/*"); |
| |
| # Values will be null if there are no files under the subdirectory |
| if (!defined($subdirectory_bytes_allocated)) { |
| next; |
| } |
| |
| $file_count += $subdirectory_file_count; |
| $total_bytes_reserved += $subdirectory_bytes_used; |
| $total_bytes_used += $subdirectory_bytes_allocated; |
| } |
| } |
| } |
| |
| if ($calling_sub !~ /get_file_size/) { |
| notify($ERRORS{'DEBUG'}, 0, "size of " . join(", ", @file_paths) . " on $computer_node_name:\n" . |
| "file count: $file_count\n" . |
| "reserved: " . get_file_size_info_string($total_bytes_reserved) . "\n" . |
| "used: " . get_file_size_info_string($total_bytes_used)); |
| } |
| |
| if (wantarray) { |
| return ($total_bytes_used, $total_bytes_reserved, $file_count); |
| } |
| else { |
| return $total_bytes_used; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 set_file_permissions |
| |
| Parameters : $file_path, $chmod_mode, $recursive (optional) |
| Returns : boolean |
| Description : Calls chmod to set the file permissions on the Linux computer. |
| The $chmod_mode argument may be any valid chmod mode (+rw, 0755, |
| etc). The $recursive argument is optional. The default is false. |
| |
| =cut |
| |
| sub set_file_permissions { |
| 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 arguments |
| my $path = shift; |
| if (!defined($path)) { |
| notify($ERRORS{'WARNING'}, 0, "path argument was not specified"); |
| return; |
| } |
| |
| # Escape the file path in case it contains spaces |
| $path = escape_file_path($path); |
| |
| my $chmod_mode = shift; |
| if (!defined($chmod_mode)) { |
| notify($ERRORS{'WARNING'}, 0, "chmod mode argument was not specified"); |
| return; |
| } |
| |
| my $recursive = shift; |
| my $recursive_string = ''; |
| $recursive_string = "recursively " if $recursive; |
| |
| # Get the computer short and hostname |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| # Run the chmod command |
| my $command = "chmod "; |
| $command .= "-R " if $recursive; |
| $command .= "$chmod_mode $path"; |
| |
| my ($exit_status, $output) = $self->execute($command, 0); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to run command to " . $recursive_string . "set file permissions on $computer_node_name: '$command'"); |
| return; |
| } |
| elsif (grep(/No such file or directory/i, @$output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to " . $recursive_string . "set permissions of '$path' to '$chmod_mode' on $computer_node_name because the file does not exist, command: '$command', output:\n" . join("\n", @$output)); |
| return; |
| } |
| elsif (grep(/^chmod:/i, @$output)) { |
| notify($ERRORS{'WARNING'}, 0, "error occurred attempting to " . $recursive_string . "set permissions of '$path' to '$chmod_mode' on $computer_node_name, command: '$command'\noutput:\n" . join("\n", @$output)); |
| return; |
| } |
| else { |
| notify($ERRORS{'OK'}, 0, $recursive_string . "set permissions of '$path' to '$chmod_mode' on $computer_node_name"); |
| return 1; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 set_file_owner |
| |
| Parameters : $file_path, $owner, $group, $recursive (optional) |
| Returns : boolean |
| Description : Calls chown to set the owner of a file or directory. |
| |
| =cut |
| |
| sub set_file_owner { |
| 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 arguments |
| my $path = shift; |
| if (!defined($path)) { |
| notify($ERRORS{'WARNING'}, 0, "path argument was not specified"); |
| return; |
| } |
| |
| # Escape the file path in case it contains spaces |
| $path = escape_file_path($path); |
| |
| my $owner = shift; |
| if (!defined($owner)) { |
| notify($ERRORS{'WARNING'}, 0, "owner argument was not specified"); |
| return; |
| } |
| |
| my $group = shift; |
| $owner .= ":$group" if $group; |
| |
| my $recursive = shift; |
| $recursive = 1 if !defined($recursive); |
| |
| my $recursive_string = ''; |
| $recursive_string = "recursively " if $recursive; |
| |
| # Get the computer short and hostname |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| # Run the chown command |
| my $command = "chown "; |
| $command .= "-R " if $recursive; |
| $command .= "$owner $path"; |
| |
| my ($exit_status, $output) = $self->execute($command, 0); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to run command to " . $recursive_string . "set file owner on $computer_node_name: '$command'"); |
| return; |
| } |
| elsif (grep(/No such file or directory/i, @$output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to " . $recursive_string . "set owner of '$path' to '$owner' on $computer_node_name because the file does not exist, command: '$command', output:\n" . join("\n", @$output)); |
| return; |
| } |
| elsif (grep(/^chown:/i, @$output)) { |
| notify($ERRORS{'WARNING'}, 0, "error occurred attempting to " . $recursive_string . "set owner of '$path' to '$owner' on $computer_node_name, command: '$command'\noutput:\n" . join("\n", @$output)); |
| return; |
| } |
| else { |
| notify($ERRORS{'OK'}, 0, $recursive_string . "set owner of '$path' to '$owner' on $computer_node_name"); |
| return 1; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 activate_interfaces |
| |
| Parameters : none |
| Returns : true |
| Description : Finds all networking interfaces with an active link. Checks if an |
| ifcfg-eth* file exists for the interface. An ifcfg-eth* file is |
| generated if it does not exist using DHCP and the interface is |
| brought up. This is useful if additional interfaces are added by |
| the provisioning module when an image is loaded. |
| |
| =cut |
| |
| sub activate_interfaces { |
| 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 0; |
| } |
| |
| # Run 'ip link' to find all interfaces with links |
| my $command = "ip link"; |
| notify($ERRORS{'DEBUG'}, 0, "attempting to find network interfaces with an active link"); |
| my ($exit_status, $output) = $self->execute($command, 1); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to run command to find network interfaces with an active link:\n$command"); |
| return; |
| } |
| |
| # Extract the interface names from the 'ip link' output |
| my @interface_names = grep {/^\d+:\s+(eth\d+)/; $_ = $1} @$output; |
| notify($ERRORS{'DEBUG'}, 0, "found interface names:\n" . join("\n", @interface_names)); |
| |
| # Find existing ifcfg-eth* files |
| my $ifcfg_directory = '/etc/sysconfig/network-scripts'; |
| my @ifcfg_paths = $self->find_files($ifcfg_directory, 'ifcfg-eth*'); |
| notify($ERRORS{'DEBUG'}, 0, "found existing ifcfg-eth* files:\n" . join("\n", @ifcfg_paths)); |
| |
| # Loop through the linked interfaces |
| for my $interface_name (@interface_names) { |
| my $ifcfg_path = "$ifcfg_directory/ifcfg-$interface_name"; |
| |
| # Check if an ifcfg-eth* file already exists for the interface |
| if (grep(/$ifcfg_path/, @ifcfg_paths)) { |
| notify($ERRORS{'DEBUG'}, 0, "ifcfg file already exists for $interface_name"); |
| next; |
| } |
| |
| notify($ERRORS{'DEBUG'}, 0, "ifcfg file does not exist for $interface_name"); |
| |
| # Assemble the contents of the ifcfg-eth* file for the interface |
| my $ifcfg_contents = <<EOF; |
| DEVICE=$interface_name |
| BOOTPROTO=dhcp |
| STARTMODE=onboot |
| ONBOOT=yes |
| EOF |
| |
| # Create the ifcfg file |
| if (!$self->create_text_file($ifcfg_path, $ifcfg_contents)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to create $ifcfg_path for interface: $interface_name"); |
| return; |
| } |
| |
| $self->start_network_interface($interface_name); |
| } |
| |
| return 1; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 get_network_configuration |
| |
| Parameters : $no_cache (optional) |
| Returns : hash reference |
| Description : Retrieves the network configuration on the Linux computer and |
| constructs a hash. The hash reference returned is formatted as |
| follows: |
| { |
| "eth0" => { |
| "broadcast_address" => "10.25.15.255", |
| "ip_address" => { |
| "10.25.10.194" => "255.255.240.0" |
| }, |
| "name" => "eth0", |
| "physical_address" => "00:50:56:23:00:bc" |
| }, |
| "eth1" => { |
| "name" => "eth1", |
| "physical_address" => "00:50:56:23:00:bd" |
| }, |
| "lo" => { |
| "name" => "lo" |
| }, |
| "xbr1" => { |
| "bridge" => { |
| "bridge_id" => "8000.0050562300bd", |
| "interfaces" => [ |
| "eth1" |
| ], |
| "stp_enabled" => "8000.0050562300bd" |
| }, |
| "broadcast_address" => "192.168.53.255", |
| "default_gateway" => "192.168.53.254", |
| "ip_address" => { |
| "152.46.18.135" => "255.255.248.0" |
| }, |
| "name" => "xbr1", |
| "physical_address" => "00:50:56:23:00:bd" |
| } |
| } |
| |
| =cut |
| |
| sub get_network_configuration { |
| 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 $no_cache = shift || 0; |
| notify($ERRORS{'DEBUG'}, 0, "attempting to retrieve network configuration, no cache: $no_cache"); |
| |
| # Delete previously retrieved data if $no_cache was specified |
| if ($no_cache) { |
| delete $self->{network_configuration}; |
| } |
| elsif ($self->{network_configuration}) { |
| return $self->{network_configuration} |
| } |
| |
| # Run ipconfig |
| my $ifconfig_command = "/sbin/ifconfig -a"; |
| my ($ifconfig_exit_status, $ifconfig_output) = $self->execute($ifconfig_command); |
| if (!defined($ifconfig_output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to run command to retrieve network configuration: $ifconfig_command"); |
| return; |
| } |
| #notify($ERRORS{'DEBUG'}, 0, "ifconfig output:\n" . join("\n", @$ifconfig_output)); |
| |
| # Loop through the ifconfig output lines |
| my $network_configuration; |
| my $interface_name; |
| for my $ifconfig_line (@$ifconfig_output) { |
| # Extract the interface name from the Link line: |
| # eth2 Link encap:Ethernet HWaddr 00:0C:29:78:77:AB |
| #eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 |
| #if ($ifconfig_line =~ /^([^\s]+).*Link/) { |
| if ($ifconfig_line =~ /^([^\s:]+).*(Link|flags)/) { |
| $interface_name = $1; |
| $network_configuration->{$interface_name}{name} = $interface_name; |
| } |
| |
| # Skip to the next line if the interface name has not been determined yet |
| next if !$interface_name; |
| |
| # Parse the HWaddr line: |
| # eth2 Link encap:Ethernet HWaddr 00:0C:29:78:77:AB |
| #if ($ifconfig_line =~ /HWaddr\s+([\w:]+)/) { |
| if ($ifconfig_line =~ /(ether|HWaddr)\s+([\w:]+)/) { |
| $network_configuration->{$interface_name}{physical_address} = lc($2); |
| } |
| |
| # Parse the IP address line: |
| # inet addr:10.10.4.35 Bcast:10.10.15.255 Mask:255.255.240.0 |
| if ($ifconfig_line =~ /inet addr:([\d\.]+)\s+Bcast:([\d\.]+)\s+Mask:([\d\.]+)/) { |
| $network_configuration->{$interface_name}{ip_address}{$1} = $3; |
| $network_configuration->{$interface_name}{broadcast_address} = $2; |
| } |
| |
| # inet 10.25.14.3 netmask 255.255.240.0 broadcast 10.25.15.255 |
| if ($ifconfig_line =~ /inet\s+([\d\.]+)\s+netmask\s+([\d\.]+)\s+broadcast\s+([\d\.]+)/) { |
| $network_configuration->{$interface_name}{ip_address}{$1} = $2; |
| $network_configuration->{$interface_name}{broadcast_address} = $3; |
| } |
| } |
| |
| |
| # Run route |
| my $route_command = "/sbin/route -n"; |
| my ($route_exit_status, $route_output) = $self->execute($route_command); |
| if (!defined($route_output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to run command to retrieve routing configuration: $route_command"); |
| return; |
| } |
| |
| # Loop through the route output lines |
| for my $route_line (@$route_output) { |
| my ($default_gateway, $interface_name) = $route_line =~ /^0\.0\.0\.0\s+([\d\.]+).*\s([^\s]+)$/g; |
| |
| if (!defined($interface_name) || !defined($default_gateway)) { |
| #notify($ERRORS{'DEBUG'}, 0, "route output line does not contain a default gateway: '$route_line'"); |
| } |
| elsif (!defined($network_configuration->{$interface_name})) { |
| notify($ERRORS{'WARNING'}, 0, "found default gateway for '$interface_name' interface but the network configuration for '$interface_name' was not previously retrieved, route output:\n" . join("\n", @$route_output) . "\nnetwork configuation:\n" . format_data($network_configuration)); |
| } |
| elsif (defined($network_configuration->{$interface_name}{default_gateway}) && $default_gateway ne $network_configuration->{$interface_name}{default_gateway}) { |
| notify($ERRORS{'WARNING'}, 0, "multiple default gateways are configured for '$interface_name' interface, route output:\n" . join("\n", @$route_output)); |
| } |
| else { |
| $network_configuration->{$interface_name}{default_gateway} = $default_gateway; |
| notify($ERRORS{'DEBUG'}, 0, "found default route configured for '$interface_name' interface: $default_gateway"); |
| } |
| } |
| |
| # Check if bridge is configured |
| my $network_bridge_info = $self->get_network_bridge_info(); |
| for my $bridge_name (keys %$network_bridge_info) { |
| # Add bridge info under 'bridge' key for the bridge |
| if (defined($network_configuration->{$bridge_name})) { |
| $network_configuration->{$bridge_name}{bridge} = $network_bridge_info->{$bridge_name}; |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "'$bridge_name' bridge was not found in 'ifconfig' output:" . |
| "ifconfig output:\n" . join("\n", @$ifconfig_output) . "\n" . |
| "network bridge info:\n" . format_data($network_bridge_info) |
| ); |
| } |
| |
| # Add name of bridge to 'master' key for the physical interface |
| for my $bridge_interface_name (@{$network_bridge_info->{$bridge_name}{interfaces}}) { |
| if (defined($network_configuration->{$bridge_interface_name})) { |
| $network_configuration->{$bridge_interface_name}{master} = $bridge_name; |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "'$bridge_name' bridge contains '$bridge_interface_name' interface but '$bridge_interface_name' was not found in 'ifconfig' output:\n" . |
| "ifconfig output:\n" . join("\n", @$ifconfig_output) . "\n" . |
| "network bridge info:\n" . format_data($network_bridge_info) |
| ); |
| } |
| } |
| } |
| |
| $self->{network_configuration} = $network_configuration; |
| #can produce large output, if you need to monitor the configuration setting uncomment the below output statement |
| notify($ERRORS{'DEBUG'}, 0, "retrieved network configuration:\n" . format_data($self->{network_configuration})); |
| return $self->{network_configuration}; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 reboot |
| |
| Parameters : none |
| Returns : boolean |
| Description : Attempts to gracefully reboot the computer by executing |
| 'shutdown -r now' command. Attempts to detect reboot began and |
| completed. If this fails or if the computer is not responding to |
| SSH, the provisioning module will attempt to forcefully perform a |
| hard reset of the computer. |
| |
| =cut |
| |
| sub reboot { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| notify($ERRORS{'DEBUG'}, 0, "rebooting $computer_node_name and waiting for SSH to become active"); |
| |
| my $reboot_start_time = time(); |
| |
| # Check if computer responds to ssh before preparing for reboot |
| if ($self->wait_for_ssh(0)) { |
| my $reboot_command = '/sbin/shutdown -r now &'; |
| notify($ERRORS{'DEBUG'}, 0, "attempting to gracefully reboot $computer_node_name by executing '$reboot_command'"); |
| my ($reboot_exit_status, $reboot_output) = $self->execute( |
| { |
| command => $reboot_command, |
| timeout => 30, |
| max_attempts => 1, |
| display_output => 0, |
| } |
| ); |
| |
| if ($self->wait_for_reboot()) { |
| my $reboot_duration = (time() - $reboot_start_time); |
| notify($ERRORS{'OK'}, 0, "gracefully rebooted $computer_node_name, took $reboot_duration seconds"); |
| return 1; |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "did not detect $computer_node_name rebooting after executing '$reboot_command', attempting hard reset using the provisioning module"); |
| } |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "$computer_node_name is not responding to SSH, graceful reboot cannot be performed, attempting hard reset using the provisioning module"); |
| } |
| |
| $self->provisioner->power_reset() || return; |
| if ($self->wait_for_reboot()) { |
| my $reboot_duration = (time() - $reboot_start_time); |
| notify($ERRORS{'OK'}, 0, "hard reset of $computer_node_name complete, took $reboot_duration seconds"); |
| return 1; |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "$computer_node_name may not have rebooted, did not detect reboot after attempting hard reset using the provisioning module"); |
| return; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 shutdown |
| |
| Parameters : none |
| Returns : boolean |
| Description : Attempts to gracefully shut down the computer by executing the |
| shutdown command. Waits for provisioning module to report that |
| the computer is off. If this fails or if the computer is not |
| responding to SSH, the provisioning module will attempt to |
| forcefully power off the computer. |
| |
| =cut |
| |
| sub shutdown { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| # Check if computer responds to ssh before preparing for shut down |
| if ($self->wait_for_ssh(0)) { |
| my $shutdown_command = '/sbin/shutdown -h now &'; |
| notify($ERRORS{'DEBUG'}, 0, "attempting to gracefully shut down $computer_node_name by executing '$shutdown_command'"); |
| my ($exit_status, $output) = $self->execute( |
| { |
| command => $shutdown_command, |
| timeout => 30, |
| max_attempts => 1, |
| display_output => 0, |
| } |
| ); |
| |
| if ($self->provisioner->wait_for_power_off(300, 10)) { |
| notify($ERRORS{'OK'}, 0, "gracefully shut down $computer_node_name by executing the OS's shutdown command"); |
| return 1; |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "$computer_node_name is still on after executing shutdown command, attempting to power off the computer using the provisioning module"); |
| } |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "$computer_node_name is NOT responding to SSH, attempting to power off the computer using the provisioning module"); |
| } |
| |
| $self->provisioner->power_off() || return; |
| if ($self->provisioner->wait_for_power_off(300, 10)) { |
| notify($ERRORS{'OK'}, 0, "forcefully powered off $computer_node_name using the provisioning module"); |
| return 1; |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "failed to shut down $computer_node_name, computer is still on after attempting to power off the computer using the provisioning module"); |
| return; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 hibernate |
| |
| Parameters : none |
| Returns : boolean |
| Description : Hibernates the computer. |
| |
| =cut |
| |
| sub hibernate { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my $command = 'echo disk > /sys/power/state &'; |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to hibernate $computer_node_name"); |
| return; |
| } |
| elsif ($exit_status eq 0) { |
| notify($ERRORS{'OK'}, 0, "executed command to hibernate $computer_node_name: $command" . (scalar(@$output) ? "\noutput:\n" . join("\n", @$output) : '')); |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "failed to hibernate $computer_node_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); |
| return; |
| } |
| |
| # Wait for computer to power off |
| my $power_off = $self->provisioner->wait_for_power_off(300, 5); |
| if (!defined($power_off)) { |
| # wait_for_power_off result will be undefined if the provisioning module doesn't implement a power_status subroutine |
| notify($ERRORS{'OK'}, 0, "unable to determine power status of $computer_node_name from provisioning module, sleeping 1 minute to allow computer time to hibernate"); |
| sleep 60; |
| return 1; |
| } |
| elsif (!$power_off) { |
| notify($ERRORS{'WARNING'}, 0, "$computer_node_name never powered off after executing hibernate command: $command"); |
| return; |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "$computer_node_name powered off after executing hibernate command"); |
| return 1; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 create_user |
| |
| Parameters : $argument_hash_ref |
| Returns : boolean |
| Description : Creates a user on the computer. The argument hash reference |
| should be constructed as follows: |
| { |
| username => $username, |
| password => $password, (optional) |
| root_access => $root_access, |
| uid => $uid, (optional) |
| ssh_public_keys => $ssh_public_keys, (optional) |
| }); |
| |
| =cut |
| |
| sub create_user { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my $user_parameters = shift; |
| if (!$user_parameters) { |
| notify($ERRORS{'WARNING'}, 0, "unable to create user, user parameters argument was not provided"); |
| return; |
| } |
| elsif (!ref($user_parameters) || ref($user_parameters) ne 'HASH') { |
| notify($ERRORS{'WARNING'}, 0, "unable to create user, argument provided is not a hash reference"); |
| return; |
| } |
| |
| my $username = $user_parameters->{username}; |
| if (!defined($username)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to create user on $computer_node_name, argument hash does not contain a 'username' key:\n" . format_data($user_parameters)); |
| return; |
| } |
| |
| my $root_access = $user_parameters->{root_access}; |
| if (!defined($root_access)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to create user on $computer_node_name, argument hash does not contain a 'root_access' key:\n" . format_data($user_parameters)); |
| return; |
| } |
| |
| my $password = $user_parameters->{password}; |
| my $uid = $user_parameters->{uid}; |
| my $ssh_public_keys = $user_parameters->{ssh_public_keys}; |
| |
| # If user account does not already exist - create it, then |
| # -- Set password if using local authentication |
| # -- update sudoers file if root access allowed |
| # -- process connect_methods_access |
| |
| if (!$self->user_exists($username)) { |
| |
| notify($ERRORS{'DEBUG'}, 0, "creating user on $computer_node_name:\n" . |
| "username: $username\n" . |
| "password: " . (defined($password) ? $password : '<not set>') . "\n" . |
| "UID: " . ($uid ? $uid : '<not set>') . "\n" . |
| "root access: " . ($root_access ? 'yes' : 'no') . "\n" . |
| "SSH public keys: " . (defined($ssh_public_keys) ? $ssh_public_keys : '<not set>') |
| ); |
| |
| my $home_directory_root = "/home"; |
| my $home_directory_path = "$home_directory_root/$username"; |
| my $home_directory_on_local_disk = $self->is_file_on_local_disk($home_directory_root); |
| if ($home_directory_on_local_disk) { |
| my $useradd_command = "/usr/sbin/useradd -s /bin/bash -m -d /home/$username -g vcl"; |
| $useradd_command .= " -u $uid" if ($uid); |
| $useradd_command .= " $username"; |
| |
| my ($useradd_exit_status, $useradd_output) = $self->execute($useradd_command); |
| if (!defined($useradd_output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to add user '$username' to $computer_node_name: '$useradd_command'"); |
| return; |
| } |
| elsif (grep(/^useradd: /, @$useradd_output)) { |
| notify($ERRORS{'WARNING'}, 0, "warning detected on add user '$username' to $computer_node_name\ncommand: '$useradd_command'\noutput:\n" . join("\n", @$useradd_output)); |
| } |
| else { |
| notify($ERRORS{'OK'}, 0, "added user '$username' to $computer_node_name, output:" . (scalar(@$useradd_output) ? "\n" . join("\n", @$useradd_output) : ' <none>')); |
| } |
| } |
| else { |
| notify($ERRORS{'OK'}, 0, "$home_directory_path is NOT on local disk, skipping useradd attempt"); |
| } |
| } |
| |
| # Set the password |
| if ($password) { |
| # Set password |
| if (!$self->set_password($username, $password)) { |
| notify($ERRORS{'CRITICAL'}, 0, "failed to set password of user '$username' on $computer_node_name"); |
| return; |
| } |
| } |
| |
| # Process connect_methods |
| if ($self->can("grant_connect_method_access")) { |
| if (!$self->grant_connect_method_access({ |
| username => $username, |
| uid => $uid, |
| ssh_public_keys => $ssh_public_keys, |
| })) { |
| notify($ERRORS{'WARNING'}, 0, "failed to process grant_connect_method_access for $username"); |
| } |
| } |
| |
| # Add user to sudoers if necessary |
| if ($root_access) { |
| if (!$self->grant_administrative_access($username)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to process grant_administrative_access for $username"); |
| return; |
| } |
| } |
| else { |
| # Make sure user does not have root access |
| $self->revoke_administrative_access($username); |
| } |
| |
| return 1; |
| } ## end sub create_user |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 grant_administrative_access |
| |
| Parameters : $username |
| Returns : boolean |
| Description : Adds the user to the sudoers file. |
| |
| =cut |
| |
| sub grant_administrative_access { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $username = shift; |
| if (!defined($username)) { |
| notify($ERRORS{'WARNING'}, 0, "username argument was not supplied"); |
| return; |
| } |
| |
| my $timestamp = makedatestring(); |
| |
| my $sudoers_file_path = '/etc/sudoers'; |
| |
| my @existing_lines = $self->get_file_contents($sudoers_file_path); |
| my @matching_lines; |
| for my $line (@existing_lines) { |
| if ($line =~ /^\s*$username\s/) { |
| push @matching_lines, $line; |
| } |
| } |
| if (@matching_lines) { |
| notify($ERRORS{'DEBUG'}, 0, "$username was previously added to $sudoers_file_path:\n" . join("\n", @matching_lines)); |
| return 1; |
| } |
| |
| my $sudoers_line = "$username ALL= NOPASSWD: ALL\t# Added by VCL, ($timestamp)"; |
| if ($self->append_text_file($sudoers_file_path, $sudoers_line)) { |
| notify($ERRORS{'DEBUG'}, 0, "appended line to $sudoers_file_path: '$sudoers_line'"); |
| return 1; |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "failed to append line to $sudoers_file_path: '$sudoers_line'"); |
| return 0; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 revoke_administrative_access |
| |
| Parameters : $username |
| Returns : boolean |
| Description : Removes all entries from the sudoers file for the user. |
| |
| =cut |
| |
| sub revoke_administrative_access { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $username = shift; |
| if (!defined($username)) { |
| notify($ERRORS{'WARNING'}, 0, "username argument was not supplied"); |
| return; |
| } |
| |
| my $sudoers_file_path = '/etc/sudoers'; |
| |
| # Remove lines from sudoers |
| if (defined($self->remove_lines_from_file($sudoers_file_path, "^[\\s#]*$username\\s"))) { |
| return 1; |
| } |
| else { |
| return; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 delete_user |
| |
| Parameters : $username |
| Returns : |
| Description : |
| |
| =cut |
| |
| sub delete_user { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return 0; |
| } |
| |
| # Make sure the user login ID was passed |
| my $username = shift; |
| $username = $self->data->get_user_login_id() if (!$username); |
| if (!$username) { |
| notify($ERRORS{'WARNING'}, 0, "user could not be determined"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| # Make sure the user exists |
| if (!$self->user_exists($username)) { |
| notify($ERRORS{'DEBUG'}, 0, "user NOT deleted from $computer_node_name because it does not exist: $username"); |
| |
| # Make sure user does not exist in sudoers |
| $self->revoke_administrative_access($username); |
| |
| return 1; |
| } |
| |
| # Check if the user is logged in |
| if ($self->user_logged_in($username)) { |
| if (!$self->logoff_user($username)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to delete user $username from $computer_node_name, user appears to be logged in but could NOT be logged off"); |
| return; |
| } |
| } |
| |
| # Determine if home directory is on a local device or network share |
| my $home_directory_path = "/home/$username"; |
| my $home_directory_on_local_disk = $self->is_file_on_local_disk($home_directory_path); |
| |
| # Assemble the userdel command |
| my $userdel_command = "/usr/sbin/userdel"; |
| |
| my $delete_home_directory = 0; |
| |
| if ($home_directory_on_local_disk) { |
| $delete_home_directory = 1; |
| |
| # Fetch exclude_list |
| my @exclude_list = $self->get_exclude_list(); |
| if ((grep(/\/home\/$username/, @exclude_list))) { |
| notify($ERRORS{'DEBUG'}, 0, "home directory will NOT be deleted: $home_directory_path"); |
| $delete_home_directory = 0; |
| } |
| else { |
| # Make sure no NFS shares are mounted under home directory |
| my @nfs_mount_strings = $self->get_nfs_mount_strings(); |
| for my $nfs_mount_string (@nfs_mount_strings) { |
| my ($nfs_remote_host, $nfs_remote_path, $nfs_local_path) = $nfs_mount_string =~ |
| / |
| ^ |
| ([^:]+) # Remote hostname or IP address |
| : |
| (\/.+) # Remote path |
| \s+ |
| (\/.+) # Local path |
| \s+ |
| nfs\d* # ' nfs ' or ' nfs4 ' |
| \s+ |
| /gx; |
| |
| if ($nfs_local_path) { |
| if ($nfs_local_path =~ /^$home_directory_path/) { |
| notify($ERRORS{'WARNING'}, 0, "home directory will NOT be deleted, NFS share is mounted under it\n" . |
| "NFS mount string : $nfs_mount_string\n" . |
| "home directory path : $home_directory_path\n" . |
| "local mount path : $nfs_local_path" |
| ); |
| $delete_home_directory = 0; |
| last; |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "NFS share is NOT mounted under home directory\n" . |
| "NFS mount string : $nfs_mount_string\n" . |
| "home directory path : $home_directory_path\n" . |
| "local mount path : $nfs_local_path" |
| ); |
| } |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "home directory will NOT be deleted: $home_directory_path, failed to parse NFS mount string: $nfs_mount_string"); |
| $delete_home_directory = 0; |
| last; |
| } |
| } |
| } |
| } |
| |
| if ($delete_home_directory) { |
| notify($ERRORS{'DEBUG'}, 0, "home directory will be deleted: $home_directory_path"); |
| $userdel_command .= ' -r'; |
| } |
| $userdel_command .= " $username"; |
| |
| # Call userdel to delete the user |
| my ($userdel_exit_status, $userdel_output) = $self->execute($userdel_command); |
| if (!defined($userdel_output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to delete user from $computer_node_name: $username"); |
| return; |
| } |
| elsif (grep(/does not exist/i, @$userdel_output)) { |
| notify($ERRORS{'DEBUG'}, 0, "user '$username' NOT deleted from $computer_node_name because it does not exist"); |
| } |
| elsif (grep(/not found/i, @$userdel_output)) { |
| notify($ERRORS{'DEBUG'}, 0, "userdel warning '$username' $computer_node_name :\n" . join("\n", @$userdel_output)); |
| } |
| elsif (grep(/userdel: /i, @$userdel_output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to delete user '$username' from $computer_node_name, command: '$userdel_command', exit status: $userdel_exit_status, output:\n" . join("\n", @$userdel_output)); |
| return; |
| } |
| else { |
| notify($ERRORS{'OK'}, 0, "deleted user '$username' from $computer_node_name"); |
| } |
| |
| # Call groupdel to delete the user's group |
| my $groupdel_command = "/usr/sbin/groupdel $username"; |
| my ($groupdel_exit_status, $groupdel_output) = $self->execute($groupdel_command); |
| if (!defined($groupdel_output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to delete group from $computer_node_name: $username"); |
| return; |
| } |
| elsif (grep(/does not exist/i, @$groupdel_output)) { |
| notify($ERRORS{'DEBUG'}, 0, "group '$username' NOT deleted from $computer_node_name because it does not exist"); |
| } |
| elsif (grep(/groupdel: /i, @$groupdel_output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to delete group '$username' from $computer_node_name, command: '$groupdel_command', output:\n" . join("\n", @$groupdel_output)); |
| } |
| else { |
| notify($ERRORS{'OK'}, 0, "deleted group '$username' from $computer_node_name"); |
| } |
| |
| # Remove username from AllowUsers lines in ssh/external_sshd_config |
| my $external_sshd_config_file_path = '/etc/ssh/external_sshd_config'; |
| my @original_lines = $self->get_file_contents($external_sshd_config_file_path); |
| my @modified_lines; |
| my $new_file_contents; |
| for my $line (@original_lines) { |
| if ($line =~ /AllowUsers.*\s$username(\s|$)/) { |
| push @modified_lines, $line; |
| $line =~ s/\s*$username//g; |
| # If user was only username listed on line, don't add empty AllowUsers line back to file |
| if ($line !~ /AllowUsers\s+\w/) { |
| next; |
| } |
| } |
| $new_file_contents .= "$line\n"; |
| } |
| if (@modified_lines) { |
| notify($ERRORS{'OK'}, 0, "removing or modifying AllowUsers lines in $external_sshd_config_file_path:\n" . join("\n", @modified_lines)); |
| $self->create_text_file($external_sshd_config_file_path, $new_file_contents) || return; |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "no AllowUsers lines were found in $external_sshd_config_file_path containing '$username'"); |
| } |
| |
| # Remove lines from sudoers |
| $self->revoke_administrative_access($username); |
| |
| return 1; |
| } ## end sub delete_user |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 is_file_on_local_disk |
| |
| Parameters : $file_path |
| Returns : boolean |
| Description : Determines if the file or directory is located on a local disk or |
| network share. |
| |
| =cut |
| |
| sub is_file_on_local_disk { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return 0; |
| } |
| |
| my $file_path = shift; |
| if (!$file_path) { |
| notify($ERRORS{'WARNING'}, 0, "file path argument was not specified"); |
| return; |
| } |
| |
| my $computer_name = $self->data->get_computer_short_name(); |
| |
| # Run df to determine if file is on a local device or network share |
| my $df_command = "df -T -P $file_path"; |
| my ($df_exit_status, $df_output) = $self->execute($df_command); |
| if (!defined($df_output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to determine if file is on a local disk"); |
| return; |
| } |
| elsif (grep(/(no such file|no file system)/i, @$df_output)) { |
| notify($ERRORS{'DEBUG'}, 0, "file does NOT exist on $computer_name: $file_path"); |
| return; |
| } |
| elsif (grep(m|/dev/|i, @$df_output) && !grep(/ (nfs|afs) /i, @$df_output)) { |
| notify($ERRORS{'DEBUG'}, 0, "file is on a local disk: $file_path, output:\n" . join("\n", @$df_output)); |
| return 1; |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "file is NOT on a local disk: $file_path, output:\n" . join("\n", @$df_output)); |
| return 0; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| =head2 enable_dhcp |
| |
| Parameters : $interface_name |
| Returns : boolean |
| Description : Configures the ifcfg-* file for the specified interface to use |
| DHCP. |
| |
| =cut |
| |
| sub enable_dhcp { |
| 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 $computer_name = $self->data->get_computer_node_name(); |
| |
| my $interface_name = shift; |
| if (!$interface_name) { |
| notify($ERRORS{'WARNING'}, 0, "interface name argument was not supplied"); |
| return; |
| } |
| |
| # Delete existing static route file for the interface if one exists |
| $self->delete_file("/etc/sysconfig/network-scripts/route-$interface_name"); |
| |
| my $ifcfg_file_info = $self->get_ifcfg_file_info($interface_name) || {}; |
| |
| my $calling_subroutine = get_calling_subroutine(); |
| if ($calling_subroutine !~ /enable_dhcp/) { |
| # Check if interface is configured as a bridge |
| my @bridge_interface_names; |
| if ($ifcfg_file_info->{bridge}) { |
| # ifcfg file contains something like: BRIDGE=br1 |
| push @bridge_interface_names, $ifcfg_file_info->{bridge}; |
| } |
| elsif ($ifcfg_file_info->{type} && $ifcfg_file_info->{type} =~ /Bridge/i) { |
| # ifcfg file contains something like: TYPE=Bridge |
| # For ifcfg-br* files, the name of the physical interface usually isn't listed in the file |
| # Get the network bridge info |
| my $network_bridge_info = $self->get_network_bridge_info(); |
| if (defined($network_bridge_info) && defined($network_bridge_info->{$interface_name})) { |
| @bridge_interface_names = @{$network_bridge_info->{$interface_name}{interfaces}}; |
| } |
| } |
| for my $bridge_interface_name (@bridge_interface_names) { |
| # Make sure the bridge isn't the same name as the interface being checked to avoid recurive loop |
| next if ($bridge_interface_name eq $interface_name); |
| |
| notify($ERRORS{'DEBUG'}, 0, "$interface_name is bridged, attempting to enable DHCP on bridge interface: $bridge_interface_name"); |
| $self->enable_dhcp($bridge_interface_name) || return; |
| } |
| } |
| |
| return $self->generate_ifcfg_file($interface_name, { 'bootproto' => 'dhcp' }); |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| =head2 generate_ifcfg_file |
| |
| Parameters : $interface_name, $parameters |
| Returns : boolean |
| Description : Creates an interface configuration file in |
| /etc/sysconfig/network-scripts. The parameters argument contains |
| key value pairs and must contain a 'bootproto' key. The key names |
| must be completely lowercase for consistency. The resulting file |
| will contain uppercase parameter names. |
| |
| =cut |
| |
| sub generate_ifcfg_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 ($interface_name, $parameters_argument) = @_; |
| if (!$interface_name) { |
| notify($ERRORS{'WARNING'}, 0, "interface name argument was not supplied"); |
| return; |
| } |
| elsif (!$parameters_argument) { |
| notify($ERRORS{'WARNING'}, 0, "parameters argument was not supplied"); |
| return; |
| } |
| elsif (!ref($parameters_argument) || ref($parameters_argument) ne 'HASH') { |
| notify($ERRORS{'WARNING'}, 0, "parameters argument is not a hash reference:\n" . format_data($parameters_argument)); |
| return; |
| } |
| elsif (!$parameters_argument->{bootproto}) { |
| notify($ERRORS{'WARNING'}, 0, "parameters argument must contain a 'bootproto' key:\n" . format_data($parameters_argument)); |
| return; |
| } |
| |
| my $computer_name = $self->data->get_computer_node_name(); |
| |
| my $ifcfg_directory_path = "/etc/sysconfig/network-scripts"; |
| my $ifcfg_file_name = "ifcfg-$interface_name"; |
| my $ifcfg_file_path = "$ifcfg_directory_path/$ifcfg_file_name"; |
| |
| if ($self->file_exists($ifcfg_file_path)) { |
| my $timestamp = POSIX::strftime("%Y-%m-%d_%H-%M-%S\n", localtime); |
| my $ifcfg_backup_file_path = "/tmp/$ifcfg_file_name.$timestamp"; |
| $self->copy_file($ifcfg_file_path, $ifcfg_backup_file_path); |
| } |
| |
| my $ifcfg_file_info = $self->get_ifcfg_file_info($interface_name) || {}; |
| |
| # Remove parameters which are specific to a particular network or computer |
| my @remove_parameter_patterns = ( |
| 'addr', |
| 'broadcast', |
| 'dns', |
| 'gateway', |
| 'hostname', |
| 'metric', |
| 'netmask', |
| 'network', |
| 'prefix', |
| 'uuid', |
| ); |
| for my $remove_pattern (@remove_parameter_patterns) { |
| my @matching_properties = grep { $_ =~ /.*$remove_pattern.*/ } sort keys %$ifcfg_file_info; |
| if (@matching_properties) { |
| notify($ERRORS{'DEBUG'}, 0, "removing parameters from ifcfg-$interface_name file matching pattern '$remove_pattern': " . join(', ', @matching_properties)); |
| map { delete $ifcfg_file_info->{$_} } @matching_properties; |
| } |
| } |
| |
| # Add/overwrite required parameters to file contents |
| my $common_parameters = { |
| 'device' => $interface_name, |
| 'name' => $interface_name, |
| 'onboot' => 'yes', |
| }; |
| for my $parameter (keys %$common_parameters) { |
| my $value = $common_parameters->{$parameter}; |
| $ifcfg_file_info->{$parameter} = $value; |
| } |
| |
| # Add/overwrite parameters specified by argument to file contents |
| for my $parameter (keys %$parameters_argument) { |
| my $value = $parameters_argument->{$parameter}; |
| $ifcfg_file_info->{$parameter} = $value; |
| } |
| |
| # Convert the parameter/value hash to a string |
| my $updated_ifcfg_contents; |
| for my $parameter (sort keys %$ifcfg_file_info) { |
| my $value = $ifcfg_file_info->{$parameter}; |
| $updated_ifcfg_contents .= uc($parameter) . "=$value\n"; |
| } |
| |
| # Create the text file |
| notify($ERRORS{'DEBUG'}, 0, "attempting to generate file on $computer_name: $ifcfg_file_path, contents:\n$updated_ifcfg_contents"); |
| return $self->create_text_file($ifcfg_file_path, $updated_ifcfg_contents); |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| =head2 get_ifcfg_file_info |
| |
| Parameters : $interface_name |
| Returns : hash reference |
| Description : Parses the file: |
| /etc/sysconfig/network-scripts/ifcfg-<interface name> |
| |
| A hash is constructed such as: |
| { |
| "bootproto" => "dhcp", |
| "device" => "eth0", |
| "onboot" => "yes" |
| } |
| |
| The hash key names are guaranteed to be lowercase. |
| |
| =cut |
| |
| sub get_ifcfg_file_info { |
| 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 $interface_name = shift; |
| if (!$interface_name) { |
| notify($ERRORS{'WARNING'}, 0, "interface name argument was not supplied"); |
| return; |
| } |
| |
| my $ifcfg_file_path = "/etc/sysconfig/network-scripts/ifcfg-$interface_name"; |
| |
| my $info = {}; |
| my @lines = $self->get_file_contents($ifcfg_file_path); |
| for my $line (@lines) { |
| next if $line =~ /^\s*#/; |
| my ($property, $value) = $line =~ /^\s*([^=]+)\s*=\s*(.*)\s*$/g; |
| if (defined($property)) { |
| $info->{lc($property)} = $value; |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "failed to parse line from $ifcfg_file_path: '$line'"); |
| } |
| } |
| |
| notify($ERRORS{'DEBUG'}, 0, "parsed $ifcfg_file_path:\n" . format_data($info)); |
| return $info; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| =head2 get_network_bridge_info |
| |
| Parameters : none |
| Returns : hash reference |
| Description : Executes 'brctl show' and parses the output. A hash is |
| constructed: |
| { |
| "br0" => { |
| "bridge_id" => "8000.00505623001c", |
| "bridge_name" => "br0", |
| "interfaces" => [ |
| "eth0", |
| ], |
| "stp_enabled" => "no" |
| }, |
| "xbr1" => { |
| "bridge_id" => "8000.00505623001d", |
| "bridge_name" => "xbr1", |
| "interfaces" => [ |
| "eth1", |
| "vnet1" |
| ], |
| "stp_enabled" => "no" |
| } |
| } |
| |
| =cut |
| |
| sub get_network_bridge_info { |
| 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; |
| } |
| |
| # This only gets cached if the brctl command does not exist |
| if (defined($self->{network_bridge_info})) { |
| return $self->{network_bridge_info}; |
| } |
| |
| my $computer_name = $self->data->get_computer_short_name(); |
| |
| # It's possible that a bridge will have multiple interfaces: |
| # [root@bn19-183 network-scripts]# brctl show |
| # bridge name bridge id STP enabled interfaces |
| # br1 8000.000c29494c97 no eth1 |
| # eth2 |
| |
| # It's possible to have no interfaces listed: |
| # bridge name bridge id STP enabled interfaces |
| # xbr1 8000.000000000000 no |
| |
| my $command = "brctl show"; |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command on $computer_name: $command"); |
| return; |
| } |
| elsif ($exit_status == 127 || grep(/command not found/i, @$output)) { |
| notify($ERRORS{'DEBUG'}, 0, "network bridge configuration does not exist on $computer_name, brctl is not installed"); |
| # Cache an empty hash reference so this command isn't needlessly run multiple times |
| $self->{network_bridge_info} = {}; |
| return $self->{network_bridge_info}; |
| } |
| elsif ($exit_status > 0) { |
| notify($ERRORS{'WARNING'}, 0, "failed to retrieve network bridge configuration from $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); |
| return; |
| } |
| |
| my $network_bridge_info = {}; |
| my $current_bridge_name; |
| for my $line (@$output) { |
| # Ignore blank and heading lines |
| next if ($line !~ /\w/ || $line =~ /(bridge name)/); |
| |
| my ($bridge_name, $bridge_id, $stp_enabled, $interface_name) = $line =~ / |
| ^ |
| ([^\s]+) |
| \s+ |
| ([^\s]+) |
| \s+ |
| ([^\s]+) |
| \s* |
| ([^\s]*) |
| $ |
| /gx; |
| |
| if (defined($bridge_name)) { |
| $current_bridge_name = $bridge_name; |
| } |
| elsif (defined($current_bridge_name)) { |
| # Bridge name not found in line but current bridge name was previously determined |
| # Check if line only contains an interface name: |
| ($interface_name) = $line =~ /^\s+([^\s]+)$/gx; |
| if (!defined($interface_name)) { |
| notify($ERRORS{'DEBUG'}, 0, "ignoring line, neither bridge name nor interface name were not found\n" . |
| "line: '$line'\n" . |
| "output:\n" . join("\n", @$output) |
| ); |
| next; |
| } |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "ignoring line, it does not contain the bridge name and bridge name was not previously determined\n" . |
| "line: '$line'\n" . |
| "output:\n" . join("\n", @$output) |
| ); |
| next; |
| } |
| |
| $network_bridge_info->{$current_bridge_name}{bridge_id} = $bridge_id if defined($bridge_id); |
| $network_bridge_info->{$current_bridge_name}{stp_enabled} = $stp_enabled if defined($stp_enabled); |
| |
| # Guarantee 'interfaces' key exists |
| if (!defined($network_bridge_info->{$current_bridge_name}{interfaces})) { |
| $network_bridge_info->{$current_bridge_name}{interfaces} = []; |
| } |
| push @{$network_bridge_info->{$current_bridge_name}{interfaces}}, $interface_name if $interface_name; |
| } |
| |
| notify($ERRORS{'OK'}, 0, "retrieved network bridge configuration from $computer_name:" . format_data($network_bridge_info)); |
| return $network_bridge_info; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 _delete_cached_service_info |
| |
| Parameters : none |
| Returns : true |
| Description : |
| |
| =cut |
| |
| sub _delete_cached_service_info { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| if (defined($self->{service_init_module})) { |
| delete $self->{service_init_module}; |
| notify($ERRORS{'DEBUG'}, 0, "deleted cached service init module info stored in \$self->{service_init_module}"); |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "cached service init module info is NOT stored in \$self->{service_init_module}"); |
| } |
| |
| return 1; |
| } |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 service_exists |
| |
| Parameters : $service_name, $no_cache (optional) |
| Returns : If called in scalar/boolean context: boolean |
| If called in array context: array |
| Description : Checks if the service exists on the computer. The return value |
| differs depending on if this subroutine was called in |
| scalar/boolean or array context. |
| |
| Scalar/boolean context returns either '0' or '1': |
| if ($self->service_exists('xxx')) |
| |
| Array context returns an array with a single, integer element. |
| The value of this integer is the index of the init module |
| returned by get_init_modules which controls the service. This is |
| done so the calling subroutine doesn't need to perform the same |
| steps to determine which init module to use when controlling |
| services. The value of the array element may be 0, meaning the |
| service exists and is controlled by the first init module |
| returned by get_init_modules. Therefore, be sure to check if the |
| return value is defined and not whether it is true/false when |
| called in array context. |
| |
| my ($init_module_index) = $self->service_exists('xxx'); |
| |
| if (defined($init_module_index))... means service exists, |
| $init_module_index may be 0 or another positive integer. |
| |
| if ($init_module_index)... WRONG! This will evaluate to false if |
| the service does not exist or if it does exist and the first init |
| module controls it. |
| |
| =cut |
| |
| sub service_exists { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my ($service_name, $no_cache) = @_; |
| if (!$service_name) { |
| notify($ERRORS{'WARNING'}, 0, "service name was not passed as an argument"); |
| return; |
| } |
| |
| if ($no_cache) { |
| $self->_delete_cached_service_info(); |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| if (!defined($self->{service_init_module}{$service_name})) { |
| my @init_modules = $self->get_init_modules(); |
| for (my $init_module_index = 0; $init_module_index < scalar(@init_modules); $init_module_index++) { |
| my $init_module = $init_modules[$init_module_index]; |
| my ($init_module_name) = ref($init_module) =~ /([^:]+)$/; |
| my @service_names = $init_module->get_service_names();; |
| for my $service_name (@service_names) { |
| $self->{service_init_module}{$service_name} = { |
| init_module_index => $init_module_index, |
| init_module_name => $init_module_name, |
| }; |
| } |
| } |
| } |
| |
| # Initialize an empty hash reference if the service name was not found to |
| # prevent another full retrieval if this is called again for the same service |
| if (!defined($self->{service_init_module}{$service_name})) { |
| $self->{service_init_module}{$service_name} = {}; |
| } |
| |
| my $init_module_index = $self->{service_init_module}{$service_name}{init_module_index}; |
| my $init_module_name = $self->{service_init_module}{$service_name}{init_module_name}; |
| if (defined($init_module_index)) { |
| notify($ERRORS{'DEBUG'}, 0, "'$service_name' exists, contolled by $init_module_name init module ($init_module_index)"); |
| return (wantarray) ? ($init_module_index) : 1; |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "'$service_name' does NOT exist"); |
| return (wantarray) ? () : 0; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 is_service_enabled |
| |
| Parameters : $service_name |
| Returns : boolean |
| Description : Determines if a service is enabled on the computer. |
| |
| =cut |
| |
| sub is_service_enabled { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $service_name = shift; |
| if (!$service_name) { |
| notify($ERRORS{'WARNING'}, 0, "service name was not passed as an argument"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my ($init_module_index) = $self->service_exists($service_name); |
| if (!defined($init_module_index)) { |
| notify($ERRORS{'WARNING'}, 0, "unable to determine if '$service_name' service is enabled, it does not exist on $computer_node_name"); |
| return; |
| } |
| |
| my $init_module = ($self->get_init_modules())[$init_module_index]; |
| if (!$init_module->can('service_enabled')) { |
| notify($ERRORS{'WARNING'}, 0, "unable to determine if '$service_name' service is enabled on $computer_node_name, " . ref($init_module) . " module does not implement a 'service_running' subroutine"); |
| return; |
| } |
| return $init_module->service_enabled($service_name); |
| } |
| |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 is_service_running |
| |
| Parameters : $service_name |
| Returns : boolean |
| Description : Determines if a service is running on the computer. |
| |
| =cut |
| |
| sub is_service_running { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $service_name = shift; |
| if (!$service_name) { |
| notify($ERRORS{'WARNING'}, 0, "service name was not passed as an argument"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my ($init_module_index) = $self->service_exists($service_name); |
| if (!defined($init_module_index)) { |
| notify($ERRORS{'WARNING'}, 0, "unable to determine if '$service_name' service is running, it does not exist on $computer_node_name"); |
| return; |
| } |
| |
| my $init_module = ($self->get_init_modules())[$init_module_index]; |
| if (!$init_module->can('service_running')) { |
| notify($ERRORS{'WARNING'}, 0, "unable to determine if '$service_name' service is running on $computer_node_name, " . ref($init_module) . " module does not implement a 'service_running' subroutine"); |
| return; |
| } |
| return $init_module->service_running($service_name); |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 enable_service |
| |
| Parameters : $service_name |
| Returns : boolean |
| Description : Enables a service on the computer. |
| |
| =cut |
| |
| sub enable_service { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $service_name = shift; |
| if (!$service_name) { |
| notify($ERRORS{'WARNING'}, 0, "service name was not passed as an argument"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my ($init_module_index) = $self->service_exists($service_name); |
| if (!defined($init_module_index)) { |
| notify($ERRORS{'WARNING'}, 0, "unable to enable '$service_name' service, it does not exist on $computer_node_name"); |
| return; |
| } |
| |
| my $init_module = ($self->get_init_modules())[$init_module_index]; |
| if (!$init_module->can('enable_service')) { |
| notify($ERRORS{'WARNING'}, 0, "unable to enable '$service_name' service on $computer_node_name, " . ref($init_module) . " module does not implement an 'enable_service' subroutine"); |
| return; |
| } |
| return $init_module->enable_service($service_name); |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 disable_service |
| |
| Parameters : $service_name |
| Returns : boolean |
| Description : disables a service on the computer. |
| |
| =cut |
| |
| sub disable_service { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $service_name = shift; |
| if (!$service_name) { |
| notify($ERRORS{'WARNING'}, 0, "service name was not passed as an argument"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my ($init_module_index) = $self->service_exists($service_name); |
| if (!defined($init_module_index)) { |
| notify($ERRORS{'WARNING'}, 0, "unable to disable '$service_name' service, it does not exist on $computer_node_name"); |
| return; |
| } |
| |
| my $init_module = ($self->get_init_modules())[$init_module_index]; |
| if (!$init_module->can('disable_service')) { |
| notify($ERRORS{'WARNING'}, 0, "unable to disable '$service_name' service on $computer_node_name, " . ref($init_module) . " module does not implement an 'disable_service' subroutine"); |
| return; |
| } |
| return $init_module->disable_service($service_name); |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 start_service |
| |
| Parameters : $service_name |
| Returns : boolean |
| Description : |
| |
| =cut |
| |
| sub start_service { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $service_name = shift; |
| if (!$service_name) { |
| notify($ERRORS{'WARNING'}, 0, "service name was not passed as an argument"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my ($init_module_index) = $self->service_exists($service_name); |
| if (!defined($init_module_index)) { |
| notify($ERRORS{'WARNING'}, 0, "unable to start '$service_name' service because it does not exist on $computer_node_name"); |
| return; |
| } |
| |
| my $init_module = ($self->get_init_modules())[$init_module_index]; |
| return $init_module->start_service($service_name); |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 stop_service |
| |
| Parameters : $service_name |
| Returns : boolean |
| Description : |
| |
| =cut |
| |
| sub stop_service { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $service_name = shift; |
| if (!$service_name) { |
| notify($ERRORS{'WARNING'}, 0, "service name was not passed as an argument"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my ($init_module_index) = $self->service_exists($service_name); |
| if (!defined($init_module_index)) { |
| notify($ERRORS{'DEBUG'}, 0, "unable to stop '$service_name' service because it does not exist on $computer_node_name"); |
| return 1; |
| } |
| |
| my $init_module = ($self->get_init_modules())[$init_module_index]; |
| return $init_module->stop_service($service_name); |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 restart_service |
| |
| Parameters : $service_name |
| Returns : boolean |
| Description : |
| |
| =cut |
| |
| sub restart_service { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $service_name = shift; |
| if (!$service_name) { |
| notify($ERRORS{'WARNING'}, 0, "service name was not passed as an argument"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my ($init_module_index) = $self->service_exists($service_name); |
| if (!defined($init_module_index)) { |
| notify($ERRORS{'WARNING'}, 0, "unable to restart '$service_name' service because it does not exist on $computer_node_name"); |
| return; |
| } |
| |
| my $init_module = ($self->get_init_modules())[$init_module_index]; |
| return $init_module->restart_service($service_name); |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 delete_service |
| |
| Parameters : $service_name |
| Returns : boolean |
| Description : |
| |
| =cut |
| |
| sub delete_service { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $service_name = shift; |
| if (!$service_name) { |
| notify($ERRORS{'WARNING'}, 0, "service name was not passed as an argument"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my ($init_module_index) = $self->service_exists($service_name, 1); |
| if (!defined($init_module_index)) { |
| notify($ERRORS{'DEBUG'}, 0, "unable to delete '$service_name' service because it does not exist on $computer_node_name"); |
| return 1; |
| } |
| |
| my $init_module = ($self->get_init_modules())[$init_module_index]; |
| if ($init_module->delete_service($service_name)) { |
| $self->_delete_cached_service_info(); |
| } |
| else { |
| return; |
| } |
| |
| return 1; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 check_connection_on_port |
| |
| Parameters : $port |
| Returns : boolean (1=connected, 0=not connected, NULL=error) |
| Description : Checks if a connection exists on the port specified. |
| |
| =cut |
| |
| sub check_connection_on_port { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| my $remote_ip = $self->data->get_reservation_remote_ip(); |
| my $computer_public_ip_address = $self->get_public_ip_address(); |
| |
| my $port = shift; |
| if (!$port) { |
| notify($ERRORS{'WARNING'}, 0, "port variable was not passed as an argument"); |
| return; |
| } |
| |
| my $port_connection_info = $self->get_port_connection_info(); |
| |
| for my $protocol (keys %$port_connection_info) { |
| if (!defined($port_connection_info->{$protocol}{$port})) { |
| next; |
| } |
| |
| for my $connection (@{$port_connection_info->{$protocol}{$port}}) { |
| my $connection_local_ip = $connection->{local_ip}; |
| my $connection_remote_ip = $connection->{remote_ip}; |
| |
| if (defined($computer_public_ip_address) && $connection_local_ip ne $computer_public_ip_address) { |
| notify($ERRORS{'DEBUG'}, 0, "ignoring connection to $computer_node_name, not connected to public IP address ($computer_public_ip_address): $connection_remote_ip --> $connection_local_ip:$port ($protocol)"); |
| next; |
| } |
| |
| if ($connection_remote_ip eq $remote_ip) { |
| notify($ERRORS{'DEBUG'}, 0, "connection to $computer_node_name detected from reservation remote IP: $connection_remote_ip --> $connection_local_ip:$port ($protocol)"); |
| return 1; |
| } |
| |
| # Connection is not from reservation remote IP address, check if user is logged in |
| if ($self->user_logged_in()) { |
| notify($ERRORS{'DEBUG'}, 0, "connection to $computer_node_name detected from different remote IP address than current reservation remote IP ($remote_ip): $connection_remote_ip --> $connection_local_ip:$port ($protocol), updating reservation remote IP to $connection_remote_ip"); |
| $self->data->set_reservation_remote_ip($connection_remote_ip); |
| return 1; |
| } |
| |
| notify($ERRORS{'DEBUG'}, 0, "ignoring connection to $computer_node_name, user is not logged in and remote IP address does not match current reservation remote IP ($remote_ip): $connection_remote_ip --> $connection_local_ip:$port ($protocol)"); |
| } |
| } |
| |
| notify($ERRORS{'DEBUG'}, 0, "connection to $computer_node_name NOT detected on port $port"); |
| return 0; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 get_cpu_core_count |
| |
| Parameters : none |
| Returns : integer |
| Description : Retrieves the quantitiy of CPU cores the computer has. |
| |
| =cut |
| |
| sub get_cpu_core_count { |
| 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 $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my $command = "cat /proc/cpuinfo"; |
| my ($exit_status, $output) = $self->execute($command); |
| |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to retrieve CPU info from $computer_node_name"); |
| return; |
| } |
| |
| # Get the number of 'processor :' lines and the 'cpu cores :' and 'siblings :' values from the cpuinfo output |
| my $processor_count = scalar(grep(/^processor\s*:/, @$output)); |
| if (!$processor_count) { |
| notify($ERRORS{'WARNING'}, 0, "unable to determine $computer_node_name CPU core count, output does not contain any 'processor :' lines:\n" . join("\n", @$output)); |
| return; |
| } |
| my ($cpu_cores) = map {$_ =~ /cpu cores\s*:\s*(\d+)/} @$output; |
| $cpu_cores = 1 unless $cpu_cores; |
| |
| my ($siblings) = map {$_ =~ /siblings\s*:\s*(\d+)/} @$output; |
| $siblings = 1 unless $siblings; |
| |
| # The actual CPU core count can be determined by the equation: |
| my $cpu_core_count = ($processor_count * $cpu_cores / $siblings); |
| |
| # If hyperthreading is enabled, siblings will be greater than CPU cores |
| # If hyperthreading is not enabled, they will be equal |
| my $hyperthreading_enabled = ($siblings > $cpu_cores) ? 'yes' : 'no'; |
| |
| notify($ERRORS{'DEBUG'}, 0, "retrieved $computer_node_name CPU core count: $cpu_core_count |
| cpuinfo 'processor' line count: $processor_count |
| cpuinfo 'cpu cores': $cpu_cores |
| cpuinfo 'siblings': $siblings |
| hyperthreading enabled: $hyperthreading_enabled" |
| ); |
| |
| return $cpu_core_count; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 get_cpu_speed |
| |
| Parameters : none |
| Returns : integer |
| Description : Retrieves the speed of the computer's CPUs in MHz. |
| |
| =cut |
| |
| sub get_cpu_speed { |
| 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 $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my $command = "cat /proc/cpuinfo"; |
| my ($exit_status, $output) = $self->execute($command); |
| |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to retrieve CPU info from $computer_node_name"); |
| return; |
| } |
| |
| my ($mhz) = map {$_ =~ /cpu MHz\s*:\s*(\d+)/} @$output; |
| if ($mhz) { |
| $mhz = int($mhz); |
| notify($ERRORS{'DEBUG'}, 0, "retrieved $computer_node_name CPU speed: $mhz MHz"); |
| return $mhz; |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "failed to determine $computer_node_name CPU speed CPU speed, 'cpu MHz :' line does not exist in the cpuinfo output:\n" . join("\n", @$output)); |
| return; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 get_total_memory |
| |
| Parameters : none |
| Returns : integer |
| Description : Retrieves the computer's total memory capacity in MB. |
| |
| =cut |
| |
| sub get_total_memory { |
| 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 $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my $command = "dmesg | grep Memory:"; |
| my ($exit_status, $output) = $self->execute($command); |
| |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to retrieve memory info from $computer_node_name"); |
| return; |
| } |
| |
| # Output should look like this: |
| # Memory: 1024016k/1048576k available (2547k kernel code, 24044k reserved, 1289k data, 208k init) |
| my ($memory_kb) = map {$_ =~ /Memory:.*\/(\d+)k available/} @$output; |
| if ($memory_kb) { |
| my $memory_mb = int($memory_kb / 1024); |
| notify($ERRORS{'DEBUG'}, 0, "retrieved $computer_node_name total memory capacity: $memory_mb MB"); |
| return $memory_mb; |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "failed to determine $computer_node_name total memory capacity from command: '$command', output:\n" . join("\n", @$output)); |
| return; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 get_exclude_list |
| |
| Parameters : none |
| Returns : array |
| Description : Retrieves /root/.vclcontrol/vcl_exclude_list from the computer |
| and constructs an array. Blank lines are ommitted. Spaces at the |
| beginning or end of lines are removed. |
| |
| =cut |
| |
| sub get_exclude_list { |
| 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 $computer_name = $self->data->get_computer_node_name(); |
| |
| # Check if previously retrieved |
| if (defined($self->{exclude_list_lines})) { |
| #notify($ERRORS{'DEBUG'}, 0, "returning previously retrieved exclude list from $computer_name:\n" . join("\n", @{$self->{exclude_list_lines}})); |
| return @{$self->{exclude_list_lines}}; |
| } |
| |
| my $exclude_file_path = "/root/.vclcontrol/vcl_exclude_list"; |
| |
| if (!$self->file_exists($exclude_file_path)) { |
| $self->{exclude_list_lines} = []; |
| return (); |
| } |
| |
| # Retrieve the contents of vcl_exclude_list |
| my @exclude_lines = $self->get_file_contents($exclude_file_path); |
| |
| # Check for blank lines and other problems |
| my @exclude_lines_cleaned; |
| my $exclude_lines_cleaned_string = ''; |
| for my $exclude_line (@exclude_lines) { |
| # Ignore blank lines |
| if ($exclude_line !~ /\w/) { |
| next; |
| } |
| |
| # Remove leading and trailing spaces |
| $exclude_line =~ s/(^\s+|\s+$)//g; |
| |
| push @exclude_lines_cleaned, $exclude_line; |
| $exclude_lines_cleaned_string .= "'$exclude_line'\n"; |
| } |
| |
| $self->{exclude_list_lines} = \@exclude_lines_cleaned; |
| |
| notify($ERRORS{'DEBUG'}, 0, "retrieved and parsed $exclude_file_path on $computer_name:\n$exclude_lines_cleaned_string"); |
| return @exclude_lines_cleaned; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 get_exclude_regex_list |
| |
| Parameters : none |
| Returns : array |
| Description : Assembles a regular expression string based on the contents of |
| each line in /root/.vclcontrol/vcl_exclude_list on the computer. |
| If the file doesn't exist or is empty, an empty array is |
| returned. |
| |
| =cut |
| |
| sub get_exclude_regex_list { |
| 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 $computer_name = $self->data->get_computer_node_name(); |
| |
| # Check if previously retrieved |
| if (defined($self->{exclude_regex_list})) { |
| #notify($ERRORS{'DEBUG'}, 0, "returning previously retrieved exclude list regex from $computer_name:\n" . join("\n", @{$self->{exclude_regex_list}})); |
| return @{$self->{exclude_regex_list}}; |
| } |
| |
| # Retrieve exclude_list |
| my @exclude_files = $self->get_exclude_list(); |
| if (!@exclude_files) { |
| $self->{exclude_regex_list} = []; |
| return (); |
| } |
| |
| my @exclude_regex_list; |
| my $exclude_regex_list_string; |
| for my $exclude_file (@exclude_files) { |
| my $exclude_regex = $exclude_file; |
| |
| # Add ^ to the beginning, remove any leading spaces |
| $exclude_regex =~ s/^[\s\^]*/\^/g; |
| |
| # Add $ to the end, remove any trailing spaces |
| $exclude_regex =~ s/[\s\$]*$/\$/g; |
| |
| # Escape forward slashes and periods |
| $exclude_regex =~ s/\\*([\/\.])/\\$1/g; |
| |
| # Change asterisk to regex: * --> .* |
| $exclude_regex =~ s/\*+/\.\*/g; |
| |
| push @exclude_regex_list, $exclude_regex; |
| $exclude_regex_list_string .= $exclude_regex . "\n"; |
| } |
| chop($exclude_regex_list_string); |
| |
| $self->{exclude_regex_list} = \@exclude_regex_list; |
| |
| notify($ERRORS{'DEBUG'}, 0, "assembled regex list from vcl_exclude_list:\n$exclude_regex_list_string"); |
| return @exclude_regex_list; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 is_file_in_exclude_list |
| |
| Parameters : $file_path |
| Returns : boolean |
| Description : Checks if the file matches any lines in |
| /root/.vclcontrol/vcl_exclude_list on the computer. If it |
| matches, true is returned meaning the file should not be altered. |
| |
| |
| =cut |
| |
| sub is_file_in_exclude_list { |
| 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 = shift; |
| if (!$file_path) { |
| notify($ERRORS{'WARNING'}, 0, "file path argument was not specified"); |
| return; |
| } |
| |
| my @exclude_regex_list = $self->get_exclude_regex_list(); |
| return 0 unless @exclude_regex_list; |
| |
| for my $exclude_regex (@exclude_regex_list) { |
| if ($file_path =~ /$exclude_regex/i) { |
| my $match = $1; |
| notify($ERRORS{'DEBUG'}, 0, "file matches line in vcl_exclude_list:\nfile path: $file_path\nmatching regex: $exclude_regex"); |
| return 1; |
| } |
| } |
| |
| #notify($ERRORS{'DEBUG'}, 0, "file does NOT match any lines in vcl_exclude_list: $file_path"); |
| return 0; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 generate_exclude_list_sample |
| |
| Parameters : none |
| Returns : boolean |
| Description : Generates /root/.vclcontrol/vcl_exclude_list.sample to help image |
| creators utilize the file. |
| |
| =cut |
| |
| sub generate_exclude_list_sample { |
| |
| 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 $exclude_file_name = "vcl_exclude_list"; |
| my $exclude_file_path = "/root/.vclcontrol/$exclude_file_name"; |
| my $sample_file_path = "/root/.vclcontrol/$exclude_file_name.sample"; |
| |
| my $sample_file_contents = <<"EOF"; |
| When creating an image, you may create a $exclude_file_path file to prevent VCL from altering certain files during the image capture or load processes. Files listed within $exclude_file_name will not be altered. The $exclude_file_name file does not exist by default. You must create it if you wish to utilize this feature. You can specify full, exact file paths or use asterisk characters as wildcards within $exclude_file_name. |
| |
| Examples: |
| /root/.ssh/id_rsa |
| This would only match the file with the exact path: |
| /root/.ssh/id_rsa |
| |
| /root/.ssh/id_rsa* |
| This would match all files in the '/root/.ssh' directory with names beginning with 'id_rsa' including: |
| /root/.ssh/id_rsa |
| /root/.ssh/id_rsa.pub |
| |
| /root/.ssh/id_rsa.* |
| This would match all files in the '/root/.ssh' directory with names beginning with 'id_rsa.' (including the period) including: |
| /root/.ssh/id_rsa.pub |
| |
| In the previous example, '/root/.ssh/id_rsa' would not match because it does not contain a period after 'id_rsa'. |
| EOF |
| |
| # Format the string and add comment characters to the beginning of each line |
| $sample_file_contents = wrap_string($sample_file_contents, 80, '# '); |
| |
| return $self->create_text_file($sample_file_path, $sample_file_contents); |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 user_logged_in |
| |
| Parameters : $username (optional) |
| Returns : boolean |
| Description : Determines if the user is currently logged in to the computer. |
| |
| =cut |
| |
| sub user_logged_in { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| # Attempt to get the username from the arguments |
| # If no argument was supplied, use the user specified in the DataStructure |
| my $username = shift || $self->data->get_user_login_id(); |
| |
| my @logged_in_users = $self->get_logged_in_users(); |
| if (grep { $username eq $_ } @logged_in_users) { |
| notify($ERRORS{'DEBUG'}, 0, "$username is logged in to $computer_node_name"); |
| return 1; |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "$username is NOT logged in to $computer_node_name"); |
| return 0; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 get_logged_in_users |
| |
| Parameters : none |
| Returns : array |
| Description : Retrieves the names of users logged in to the computer. |
| |
| =cut |
| |
| sub get_logged_in_users { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my $command = "users"; |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to run command to determine logged in users on $computer_node_name: $command"); |
| return; |
| } |
| elsif (grep(/^users:/, @$output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to determine logged in users on $computer_node_name, command: $command, output:\n" . join("\n", @$output)); |
| return; |
| } |
| |
| my @usernames; |
| for my $line (@$output) { |
| my @line_usernames = split(/[\s+]/, $line); |
| push @usernames, @line_usernames if @line_usernames; |
| } |
| |
| my $username_count = scalar(@usernames); |
| if ($username_count) { |
| notify($ERRORS{'DEBUG'}, 0, "$username_count user" . ($username_count == 1 ? '' : 's') . " logged in to $computer_node_name: " . join(', ', @usernames)); |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "no users logged in to $computer_node_name"); |
| } |
| return @usernames; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 clean_known_files |
| |
| Parameters : none |
| Returns : boolean |
| Description : Clears and deletes files defined for the Linux OS module in the |
| $CAPTURE_CLEAR_FILE_PATHS and $CAPTURE_DELETE_FILE_PATHS class |
| variables. |
| |
| =cut |
| |
| sub clean_known_files { |
| my $self = shift; |
| if (ref($self) !~ /Linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $error_count = 0; |
| |
| # Clear files |
| my @class_clear_file_path_array_refs = $self->get_class_variable_hierarchy('CAPTURE_CLEAR_FILE_PATHS'); |
| for my $class_clear_file_path_array_ref (@class_clear_file_path_array_refs) { |
| for my $file_path (@$class_clear_file_path_array_ref) { |
| if ($self->is_file_in_exclude_list($file_path)) { |
| notify($ERRORS{'DEBUG'}, 0, "file not cleared because it is in the exclude list: $file_path"); |
| next; |
| } |
| $self->clear_file($file_path) || $error_count++; |
| } |
| } |
| |
| # Delete files |
| my @class_delete_file_path_array_refs = $self->get_class_variable_hierarchy('CAPTURE_DELETE_FILE_PATHS'); |
| for my $class_delete_file_path_array_ref (@class_delete_file_path_array_refs) { |
| for my $file_path (@$class_delete_file_path_array_ref) { |
| if ($self->is_file_in_exclude_list($file_path)) { |
| notify($ERRORS{'DEBUG'}, 0, "file not deleted because it is in the exclude list: $file_path"); |
| next; |
| } |
| $self->delete_file($file_path) || $error_count++; |
| } |
| } |
| |
| if ($error_count) { |
| notify($ERRORS{'WARNING'}, 0, "encountered $error_count error" . ($error_count > 1 ? 's' : '') . " clearing and deleting files"); |
| return; |
| } |
| else { |
| return 1; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 user_exists |
| |
| Parameters : $username (optional) |
| Returns : boolean |
| Description : |
| |
| =cut |
| |
| sub user_exists { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $management_node_keys = $self->data->get_management_node_keys(); |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| # Attempt to get the username from the arguments |
| # If no argument was supplied, use the user specified in the DataStructure |
| my $username = shift; |
| if (!$username) { |
| $username = $self->data->get_user_login_id(); |
| } |
| |
| notify($ERRORS{'DEBUG'}, 0, "checking if user exists on $computer_node_name: $username"); |
| |
| # Attempt to query the user account |
| my $query_user_command = "id $username"; |
| my ($query_user_exit_status, $query_user_output) = $self->execute($query_user_command, 0); |
| |
| if (grep(/uid/, @$query_user_output)) { |
| notify($ERRORS{'DEBUG'}, 0, "user exists on $computer_node_name: $username"); |
| return 1; |
| } |
| elsif (grep(/No such user/i, @$query_user_output)) { |
| notify($ERRORS{'DEBUG'}, 0, "user does not exist on $computer_node_name: $username"); |
| return 0; |
| } |
| elsif (defined($query_user_exit_status)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to determine if user exists on $computer_node_name: $username, exit status: $query_user_exit_status, output:\n@{$query_user_output}"); |
| return; |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to determine if user exists on $computer_node_name: $username"); |
| return; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 stop_external_sshd |
| |
| Parameters : none |
| Returns : boolean |
| Description : Kills the external sshd process. |
| |
| =cut |
| |
| sub stop_external_sshd { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| $self->stop_service('ext_sshd'); |
| |
| # Run pkill to kill all external sshd processes |
| # Exit status may be: |
| # 0 - One or more processes matched the criteria. |
| # 1 - No processes matched. |
| my $pkill_command = "pkill -9 -f ext.*sshd"; |
| my ($pkill_exit_status, $pkill_output) = $self->execute($pkill_command); |
| if (!defined($pkill_output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to kill external sshd process on $computer_node_name"); |
| return; |
| } |
| elsif ($pkill_exit_status eq '0') { |
| notify($ERRORS{'DEBUG'}, 0, "killed external sshd process on $computer_node_name"); |
| } |
| elsif ($pkill_exit_status eq '1') { |
| notify($ERRORS{'DEBUG'}, 0, "external sshd process is not running on $computer_node_name"); |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "failed to kill external sshd process on $computer_node_name, exit status: $pkill_exit_status, output:\n" . join("\n", @$pkill_output)); |
| return; |
| } |
| |
| $self->delete_file('/var/run/ext_sshd.pid'); |
| |
| return 1; |
| } ## end sub stop_external_sshd |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 configure_sshd_config_file |
| |
| Parameters : $custom_parameters (optional), $output_file_path (optional) |
| Returns : boolean |
| Description : Configures and generates an output file based |
| on the /etc/ssh/sshd_config currently residing on the computer. |
| This is used to configure both the sshd_config and |
| external_sshd_config files. If no arguments are supplied, |
| /etc/ssh/sshd_config is configured to its stock, default state. |
| This is done prior to image capture. sshd_config is configured to |
| listen on all interfaces. |
| |
| By default, all of the settings which exist in |
| /etc/ssh/sshd_config are retained in the output file except for |
| the following: |
| StrictModes no |
| UseDNS no |
| PasswordAuthentication no |
| PermitRootLogin without-password |
| AllowUsers root |
| Banner none |
| |
| In addition, any ListenAddress lines are not included in the |
| output file. |
| |
| An optional $custom_parameters hash reference argument may be |
| supplied. The key/values in this hash will result in the values |
| being set in the output file. If a parameter is included with an |
| empty value in the hash reference, all lines containing that |
| parameter will be removed from the the resulting output file. |
| Example: |
| $self->configure_sshd_config_file({ |
| ListenAddress =>'10.10.0.33', |
| AllowUsers => '', |
| }); |
| |
| The output file will be /etc/ssh/sshd_config since the 2nd |
| argument was not specified. This file will be based off itself |
| except a ListenAddress line will be added and all AllowUsers |
| lines will be omitted. |
| |
| =cut |
| |
| sub configure_sshd_config_file { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my ($custom_parameters, $output_file_path) = @_; |
| |
| my $sshd_config_file_path = '/etc/ssh/sshd_config'; |
| |
| # If output file path argument wasn't specified, write back to sshd_config |
| $output_file_path = $sshd_config_file_path if !$output_file_path; |
| |
| # Check if the output file is in the exclude list before proceeding |
| my @exclude_list = $self->get_exclude_list(); |
| if (@exclude_list && grep(m|$output_file_path|, @exclude_list)) { |
| notify($ERRORS{'OK'}, 0, "skipping reconfiguration of $output_file_path because it is in the exclude file list"); |
| return 1; |
| } |
| |
| # Get the contents of the sshd_config file already on the computer |
| my @sshd_config_file_lines = $self->get_file_contents($sshd_config_file_path); |
| if (!@sshd_config_file_lines) { |
| notify($ERRORS{'WARNING'}, 0, "failed to retrieve contents of $sshd_config_file_path from $computer_node_name"); |
| return; |
| } |
| |
| # Add the following parameters to the end of the sshd_config file |
| # Any existing lines containing these parameters will be discarded |
| my $parameters = { |
| StrictModes => 'no', |
| UseDNS => 'no', |
| PasswordAuthentication => 'no', |
| PermitRootLogin => 'without-password', |
| AllowUsers => 'root', |
| ListenAddress => '', |
| Banner => 'none', |
| }; |
| |
| if ($custom_parameters) { |
| $parameters = {%$parameters, %$custom_parameters}; |
| } |
| notify($ERRORS{'DEBUG'}, 0, "generating sshd config file: $output_file_path, custom parameters:\n" . format_data($parameters)); |
| |
| my $custom_tag = 'VCL Settings'; |
| |
| # Loop through the lines from the existing sshd_config file |
| my $output_file_contents; |
| LINE: for my $line (@sshd_config_file_lines) { |
| # Ignore lines already in the file which will be added later with custom values |
| if ((map { $line =~ /$_/ } ($custom_tag, keys %$parameters))) { |
| #notify($ERRORS{'DEBUG'}, 0, "ignoring line in $sshd_config_file_path: '$line'"); |
| next LINE; |
| } |
| $output_file_contents .= "$line\n"; |
| } |
| |
| # Remove extra blank lines from the end of the file |
| $output_file_contents =~ s/[\s\n]*$//gs; |
| |
| # Add each of the custom parameters to the file |
| $output_file_contents .= "\n\n#" . ('-' x 20) . " $custom_tag " . ('-' x 20) . "\n"; |
| for my $custom_parameter (sort keys %$parameters) { |
| my $custom_value = $parameters->{$custom_parameter}; |
| |
| # Add the custom parameter of the value is set |
| if (defined($custom_value) && length($custom_value) > 0) { |
| $output_file_contents .= "$custom_parameter $custom_value\n"; |
| } |
| } |
| |
| if (!$self->create_text_file($output_file_path, $output_file_contents)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to create file on $computer_node_name: $output_file_path"); |
| return; |
| } |
| |
| if (!$self->set_file_permissions($output_file_path, '600')) { |
| notify($ERRORS{'WARNING'}, 0, "failed to set permissions of $output_file_path on $computer_node_name"); |
| return; |
| } |
| |
| return 1; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 configure_ext_sshd_config_file |
| |
| Parameters : none |
| Returns : boolean |
| Description : Generates /etc/ssh/external_sshd_config based off of |
| /etc/ssh/sshd_config currently residing on the computer with the |
| following parameters overridden: |
| PidFile /var/run/ext_sshd.pid |
| PermitRootLogin no |
| X11Forwarding yes |
| PasswordAuthentication yes |
| AllowUsers |
| ListenAddress => <public IP aaddress> |
| |
| =cut |
| |
| sub configure_ext_sshd_config_file { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my $ext_sshd_config_file_path = '/etc/ssh/external_sshd_config'; |
| |
| my $public_ip_address = $self->get_public_ip_address(); |
| if (!$public_ip_address) { |
| notify($ERRORS{'WARNING'}, 0, "failed to generate $ext_sshd_config_file_path on $computer_node_name, public IP address could not be determined"); |
| return; |
| } |
| |
| my $custom_ext_sshd_parameters = { |
| PidFile => '/var/run/ext_sshd.pid', |
| PermitRootLogin => 'no', |
| X11Forwarding => 'yes', |
| PasswordAuthentication => 'yes', |
| AllowUsers => '', |
| ListenAddress => $public_ip_address, |
| }; |
| |
| return $self->configure_sshd_config_file($custom_ext_sshd_parameters, $ext_sshd_config_file_path); |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 configure_default_sshd |
| |
| Parameters : none |
| Returns : boolean |
| Description : Configures the sshd daemon back to a mostly default state. |
| Removes the ext_sshd service from the computer. Reconfigures |
| sshd to listen on all interfaces. Restarts sshd. |
| |
| This is called prior to image |
| capture. The purpose is to configure the captured image so that |
| it responds to SSH after it is loaded from any interface. |
| |
| =cut |
| |
| sub configure_default_sshd { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| if (!$self->service_exists('sshd', 1)) { |
| notify($ERRORS{'DEBUG'}, 0, "skipping default sshd configuation, sshd service does not exist"); |
| return 1; |
| } |
| |
| # Stop existing external sshd process if it is running |
| if (!$self->stop_external_sshd()) { |
| notify($ERRORS{'WARNING'}, 0, "unable to configure default sshd state, problem occurred attempting to kill external sshd process"); |
| return; |
| } |
| |
| # Delete the ext_sshd service |
| $self->delete_service('ext_sshd') || return; |
| |
| # Delete the external sshd configuration file |
| $self->delete_file('/etc/ssh/ext*ssh*'); |
| |
| # Reconfigure sshd_config back to its default state |
| $self->configure_sshd_config_file() || return; |
| |
| # Restart the sshd service |
| $self->restart_service('sshd') || return; |
| |
| return 1; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 configure_ext_sshd |
| |
| Parameters : none |
| Returns : boolean |
| Description : Adds the external_sshd_config file configured to listen on the |
| public network and adds the ext_sshd service to the computer. |
| Reconfigures the existing sshd service to only listen on the |
| private network and restarts sshd. Stops the ext_sshd service if |
| it is started. |
| |
| =cut |
| |
| sub configure_ext_sshd { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my $private_ip_address = $self->get_private_ip_address(); |
| if (!$private_ip_address) { |
| notify($ERRORS{'WARNING'}, 0, "unable to configure ext_sshd, failed to retrieve private IP address of $computer_node_name, necessary to configure sshd to only listen on private network"); |
| return; |
| } |
| |
| if (!$self->service_exists('sshd', 1)) { |
| notify($ERRORS{'DEBUG'}, 0, "skipping ext_sshd configuation, sshd service does not exist"); |
| return 1; |
| } |
| |
| # Recreate the sshd_config file, set ListenAddress to the private IP address |
| if (!$self->configure_sshd_config_file({ListenAddress => $private_ip_address})) { |
| notify($ERRORS{'WARNING'}, 0, "unable to configure ext_sshd, failed to reconfigure sshd_config to only listen on private network on $computer_node_name"); |
| return; |
| } |
| |
| # Restart sshd to enact the changes |
| if (!$self->restart_service('sshd')) { |
| notify($ERRORS{'WARNING'}, 0, "unable to configure ext_sshd, failed to restart sshd on $computer_node_name after reconfiguring sshd_config to only listen on private network"); |
| return; |
| } |
| |
| # Create and configure the ext_sshd service |
| if (!$self->configure_ext_sshd_config_file()) { |
| notify($ERRORS{'WARNING'}, 0, "unable to configure ext_sshd, failed to configure external_sshd_config file on $computer_node_name"); |
| return; |
| } |
| |
| # Deterine which init module is currently controlling sshd, use the same module to control ext_sshd |
| my ($init_module_index) = $self->service_exists('sshd', 1); |
| if (!defined($init_module_index)) { |
| notify($ERRORS{'WARNING'}, 0, "unable to configure ext_sshd, init module controlling sshd could not be determined"); |
| return; |
| } |
| |
| my $init_module = ($self->get_init_modules())[$init_module_index]; |
| |
| # Add the ext_sshd service |
| if (!$init_module->add_ext_sshd_service()) { |
| notify($ERRORS{'WARNING'}, 0, "unable to configure ext_sshd, failed to add the ext_sshd service to $computer_node_name"); |
| return; |
| } |
| |
| # Delete the cached service info to make sure ext_sshd isn't incorrectly reported as not existing |
| $self->_delete_cached_service_info(); |
| |
| # Stop ext_sshd if it is running |
| $self->stop_service('ext_sshd'); |
| |
| notify($ERRORS{'OK'}, 0, "configured ext_sshd on $computer_node_name"); |
| return 1; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 configure_rc_local |
| |
| Parameters : none |
| Returns : boolean |
| Description : Checks if /etc/rc.local was configured by a previous version of |
| VCL. If so, returns file to its default state. Previous versions |
| of VCL had been adding commands to rc.local to configure |
| networking. This is now done by this backend code. |
| |
| =cut |
| |
| sub configure_rc_local { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| |
| # Check if rc.local is in the exclude list before proceeding |
| my @exclude_list = $self->get_exclude_list(); |
| if (@exclude_list && grep(m|rc.local|, @exclude_list)) { |
| notify($ERRORS{'OK'}, 0, "skipping reconfiguration of rc.local because it is in the exclude file list"); |
| return 1; |
| } |
| |
| my $rc_local_contents = <<EOF; |
| #!/bin/sh |
| # |
| # This script will be executed *after* all the other init scripts. |
| |
| touch /var/lock/subsys/local |
| EOF |
| |
| for my $rc_local_file_path ('/etc/rc.d/rc.local', '/etc/rc.local') { |
| # Check if the file exists or else get_file_contents will complain |
| if (!$self->file_exists($rc_local_file_path)) { |
| notify($ERRORS{'DEBUG'}, 0, "skipping $rc_local_file_path reconfiguration, file does not exist on $computer_node_name"); |
| next; |
| } |
| |
| my @rc_local_lines = $self->get_file_contents($rc_local_file_path); |
| if (!@rc_local_lines) { |
| notify($ERRORS{'WARNING'}, 0, "failed to retrieve contents of $rc_local_file_path on $computer_node_name"); |
| next; |
| } |
| elsif (!grep(/ListenAddress \$IP0/, @rc_local_lines)) { |
| notify($ERRORS{'DEBUG'}, 0, "skipping $rc_local_file_path reconfiguration, it does not appear to be configured by a previous version of VCL"); |
| next; |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "$rc_local_file_path will be returned to its default state, it appears to have been configured by a previous version of VCL"); |
| } |
| |
| if (!$self->create_text_file($rc_local_file_path, $rc_local_contents)) { |
| return; |
| } |
| } |
| return 1; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 command_exists |
| |
| Parameters : $shell_command |
| Returns : boolean |
| Description : Determines if a shell command exists on the computer by executing |
| 'which <command>'. |
| |
| =cut |
| |
| sub command_exists { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $shell_command = shift; |
| if (!$shell_command) { |
| notify($ERRORS{'WARNING'}, 0, "shell command argument was not supplied"); |
| return; |
| } |
| |
| if ($self->{command_exists}{$shell_command}) { |
| return 1; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my $command = "which $shell_command"; |
| my ($exit_status, $output) = $self->execute($command, 0); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to determine if the '$shell_command' shell command exists on $computer_node_name"); |
| return; |
| } |
| elsif (my ($command_line) = grep(/\/$shell_command$/, @$output)) { |
| notify($ERRORS{'DEBUG'}, 0, "verified '$shell_command' command exists on $computer_node_name: $command_line"); |
| $self->{command_exists}{$shell_command} = 1; |
| return 1; |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "'$shell_command' command does NOT exist on $computer_node_name, command: $command\noutput:\n" . join("\n", @$output)); |
| $self->{command_exists}{$shell_command} = 0; |
| return 0; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 notify_user_console |
| |
| Parameters : message, username(optional) |
| Returns : boolean |
| Description : Send a message to the user on the console |
| |
| =cut |
| |
| sub notify_user_console { |
| my $self = shift; |
| if (ref($self) !~ /Module/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $message = shift; |
| if (!$message) { |
| notify($ERRORS{'WARNING'}, 0, "message argument was not supplied"); |
| return; |
| } |
| |
| my $username = shift; |
| if (!$username) { |
| $username = $self->data->get_user_login_id(); |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my $cmd = "echo \"$message\" \| write $username"; |
| my ($exit_status, $output) = $self->execute($cmd, 1); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to determine if the '$cmd' shell command exists on $computer_node_name"); |
| return; |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "executed command to determine if the '$cmd' shell command exists on $computer_node_name"); |
| return 1; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 is_64_bit |
| |
| Parameters : none |
| Returns : boolean |
| Description : Determines if the OS is 64-bit or not. |
| |
| =cut |
| |
| sub is_64_bit { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my $command = 'uname -m'; |
| my ($exit_status, $output) = $self->execute($command, 0); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute '$command' command to determine if $computer_node_name contains a 64-bit Linux OS"); |
| return; |
| } |
| elsif (grep(/uname:/, @$output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to determine if $computer_node_name contains a 64-bit Linux OS, command: '$command', output:\n" . join("\n", @$output)); |
| return; |
| } |
| elsif (grep(/64/, @$output)) { |
| #notify($ERRORS{'DEBUG'}, 0, "$computer_node_name contains a 64-bit Linux OS, output:\n" . join("\n", @$output)); |
| return 1; |
| } |
| else { |
| #notify($ERRORS{'DEBUG'}, 0, "$computer_node_name does NOT contain a 64-bit Linux OS, output:\n" . join("\n", @$output)); |
| return 0; |
| } |
| } |
| |
| # !!! DON'T USE get_user_remote_ip_addresses as it's currently written !!! |
| # Needs testing, the 'who' command only shows pseudo terminal connections |
| ##///////////////////////////////////////////////////////////////////////////// |
| # |
| #=head2 get_user_remote_ip_addresses |
| # |
| # Parameters : none |
| # Returns : hash reference |
| # Description : Retrieves info regarding users connected to the computer. A hash |
| # reference is returned. The hash keys are the usernames logged in. |
| # The value of each username key is an array reference containing |
| # the remote IP addresses that user is connected from. Example: |
| # { |
| # "admin" => [ |
| # "152.1.1.1" |
| # ], |
| # "root" => [ |
| # "10.1.1.1" |
| # ] |
| # } |
| # |
| #=cut |
| # |
| #sub get_user_remote_ip_addresses { |
| # my $self = shift; |
| # if (ref($self) !~ /linux/i) { |
| # notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| # return; |
| # } |
| # |
| # my $computer_node_name = $self->data->get_computer_node_name(); |
| # |
| # my $who_command = 'who -u'; |
| # my ($who_exit_status, $who_output) = $self->execute($who_command, 0); |
| # if (!defined($who_output)) { |
| # notify($ERRORS{'WARNING'}, 0, "failed to execute '$who_command' command to determine users connected to $computer_node_name"); |
| # return; |
| # } |
| # elsif (grep(/who:/, @$who_output)) { |
| # notify($ERRORS{'WARNING'}, 0, "failed to determine users connected to $computer_node_name, exit status: $who_exit_status, command: '$who_command', output:\n" . join("\n", @$who_output)); |
| # return; |
| # } |
| # |
| # my $connected_user_info = {}; |
| # for my $line (@$who_output) { |
| # # NAME LINE TIME IDLE PID COMMENT |
| # # root pts/0 2014-03-12 11:32 . 5403 (10.1.0.1) |
| # # admin pts/1 2014-03-11 13:34 22:01 4336 (152.1.1.1) |
| # |
| # my ($username, $remote_ip) = $line =~ /^([^\s]+).+\(([\d\.]+)\)/; |
| # if ($username && $remote_ip) { |
| # if (!defined($connected_user_info->{$username})) { |
| # $connected_user_info->{$username} = []; |
| # } |
| # push @{$connected_user_info->{$username}}, $remote_ip; |
| # } |
| # } |
| # |
| # if ($connected_user_info) { |
| # notify($ERRORS{'DEBUG'}, 0, "retrieved user connection info using the 'who' command from $computer_node_name:\n" . format_data($connected_user_info)); |
| # return $connected_user_info; |
| # } |
| # else { |
| # notify($ERRORS{'DEBUG'}, 0, "did not detect any users connected to $computer_node_name using the 'who' command, attempting to call 'last'"); |
| # } |
| # |
| # my $last_command = 'last'; |
| # my ($last_exit_status, $last_output) = $self->execute($last_command, 0); |
| # if (!defined($last_output)) { |
| # notify($ERRORS{'WARNING'}, 0, "failed to execute '$last_command' command to determine users connected to $computer_node_name"); |
| # return; |
| # } |
| # elsif (grep(/last:/, @$last_output)) { |
| # notify($ERRORS{'WARNING'}, 0, "failed to determine users connected to $computer_node_name, exit status: $last_exit_status, command: '$last_command', output:\n" . join("\n", @$last_output)); |
| # return; |
| # } |
| # |
| # for my $line (@$last_output) { |
| # # root pts/0 10.1.0.1 Wed Mar 12 11:32 still logged in |
| # # admin pts/1 152.1.1.1 Tue Mar 11 13:34 still logged in |
| # # root pts/0 10.1.0.1 Tue Mar 11 13:23 - 10:47 (21:24) |
| # |
| # my ($username, $remote_ip) = $line =~ /^([^\s]+).+[\s\t](\d+\.\d+\.\d+\.\d+)[\s\t].*logged\s+in/i; |
| # if ($username && $remote_ip) { |
| # if (!defined($connected_user_info->{$username})) { |
| # $connected_user_info->{$username} = []; |
| # } |
| # push @{$connected_user_info->{$username}}, $remote_ip; |
| # } |
| # } |
| # |
| # if ($connected_user_info) { |
| # notify($ERRORS{'DEBUG'}, 0, "retrieved user connection info using the 'last' command from $computer_node_name:\n" . format_data($connected_user_info)); |
| # } |
| # else { |
| # notify($ERRORS{'DEBUG'}, 0, "did not detect any users connected to $computer_node_name"); |
| # } |
| # return $connected_user_info; |
| #} |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 get_port_connection_info |
| |
| Parameters : none |
| Returns : hash reference |
| Description : Retrieves information about established connections from the |
| computer. A hash is constructed: |
| { |
| "tcp" => { |
| 22 => [ |
| { |
| "local_ip" => "10.25.10.194", |
| "pid" => 5400, |
| "program" => "sshd", |
| "remote_ip" => "10.25.0.241" |
| }, |
| { |
| "local_ip" => "192.168.18.135", |
| "pid" => 5689, |
| "program" => "sshd", |
| "remote_ip" => "192.168.53.54" |
| }, |
| ], |
| 3389 => [ |
| { |
| "local_ip" => "192.168.18.135", |
| "pid" => 6767, |
| "program" => "xrdp", |
| "remote_ip" => "192.168.53.54" |
| } |
| ] |
| } |
| } |
| |
| =cut |
| |
| sub get_port_connection_info { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my $command = "netstat -anp"; |
| my ($exit_status, $output) = $self->execute($command, 0); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command: $command"); |
| return; |
| } |
| elsif (grep(/^netstat: /, @$output)) { |
| notify($ERRORS{'WARNING'}, 0, "error occurred executing command: '$command', exit status: $exit_status, output:\n" . join("\n", @$output)); |
| return; |
| } |
| |
| my $connection_info = {}; |
| for my $line (@$output) { |
| # Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name |
| # tcp 0 0 192.168.13.220:22 152.1.1.100:63497 ESTABLISHED 6199/sshd |
| # tcp 0 0 10.10.3.220:22 10.10.14.13:52239 ESTABLISHED 5189/sshd |
| my ($protocol, $local_ip_address, $port, $remote_ip_address, $state, $pid, $program) = $line =~ /^(\w+).+\s([\d\.]+):(\d+)\s+([\d\.]+):\d*\s+(\w+)\s+(\d+)?\/?(\w+)?/i; |
| if (!$state || $state !~ /ESTABLISHED/i) { |
| next; |
| } |
| |
| my $connection = { |
| remote_ip => $remote_ip_address, |
| local_ip => $local_ip_address, |
| }; |
| $connection->{pid} = $pid if $pid; |
| $connection->{program} = $program if $program; |
| |
| push @{$connection_info->{$protocol}{$port}}, $connection; |
| } |
| |
| if ($connection_info) { |
| #notify($ERRORS{'DEBUG'}, 0, "retrieved connection info from $computer_node_name:\n" . format_data($connection_info)); |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "did not detect any connections on $computer_node_name"); |
| } |
| return $connection_info; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 enable_ip_forwarding |
| |
| Parameters : none |
| Returns : boolean |
| Description : Adds 'net.ipv4.ip_forward=1' to /etc/sysctl.conf if it doesn't |
| already exist. Executes 'sysctl -p' to apply the setting. |
| |
| =cut |
| |
| sub enable_ip_forwarding { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| my $sysctl_conf_path = '/etc/sysctl.conf'; |
| $self->set_config_file_parameter($sysctl_conf_path, 'net.ipv4.ip_forward', '=', '1'); |
| |
| my $command = "sysctl -p"; |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to enable IP forwarding on $computer_node_name: $command"); |
| return; |
| } |
| |
| # Output should contain: |
| # net.ipv4.ip_forward = 1 |
| if ($exit_status eq '0' || grep(/net.ipv4.ip_forward.*1/, @$output)) { |
| notify($ERRORS{'OK'}, 0, "IP forwarding is enabled on $computer_node_name:\n" . join("\n", @$output)); |
| return 1; |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "failed to enable IP forwarding on $computer_node_name, command: '$command', exit status: $exit_status, output:\n" . join("\n", @$output)); |
| return 0; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 should_set_user_password |
| |
| Parameters : $user_id, $no_cache (optional) |
| Returns : boolean |
| Description : Determines whether or not a random password should be generated |
| and used for the user account created on the computer being |
| loaded. A random password WILL be used if any of the following |
| are true: |
| * The user.uid value is NOT set in the database for the user |
| * The managementnode.NOT_STANDALONE value is empty |
| * The managementnode.NOT_STANDALONE value is populated but does |
| NOT match the user's affiliation.name value |
| |
| A federated authentication method such as Kerberos WILL be used |
| and a random password will NOT be generated if: |
| * The user.uid value SI set in the database for the user |
| * The managementnode.NOT_STANDALONE value is populated and |
| matches the user's affiliation.name value |
| |
| Note: managementnode.NOT_STANDALONE corresponds to the management |
| node's 'Affiliations Using Federated Authentication for Linux |
| Images' setting on the VCL website |
| |
| =cut |
| |
| sub should_set_user_password { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my ($user_id, $no_cache) = @_; |
| if (!$user_id) { |
| notify($ERRORS{'WARNING'}, 0, "user ID argument was not supplied"); |
| return; |
| } |
| elsif ($user_id !~ /^\d+$/) { |
| notify($ERRORS{'WARNING'}, 0, "invalid user ID argument was supplied, it is not an integer: '$user_id'"); |
| return; |
| } |
| |
| if (!$no_cache && defined($self->{set_user_password}) && defined($self->{set_user_password}{$user_id})) { |
| return $self->{set_user_password}{$user_id}; |
| } |
| |
| |
| my $user_info = get_user_info($user_id, undef, $no_cache); |
| if ($user_info) { |
| my $user_login_id = $user_info->{unityid} || '<undefined>'; |
| my $user_affiliation_name = $user_info->{affiliation}{name} || '<undefined>'; |
| my $federated_linux_authentication = $user_info->{FEDERATED_LINUX_AUTHENTICATION}; |
| |
| # Generate a reservation password if "standalone" (not using Kerberos authentication) |
| if ($federated_linux_authentication) { |
| notify($ERRORS{'DEBUG'}, 0, "random password should NOT be set for user ID $user_id ($user_login_id\@$user_affiliation_name), federated Linux authentication: $federated_linux_authentication"); |
| $self->{set_user_password}{$user_id} = 0; |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "random password SHOULD be set for user ID $user_id ($user_login_id\@$user_affiliation_name), federated Linux authentication: $federated_linux_authentication"); |
| $self->{set_user_password}{$user_id} = 1; |
| } |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "unable to definitively determine if random password should be set for user ID $user_id, user info could not be retrieved, assuming random password SHOULD be set, returning 1"); |
| $self->{set_user_password}{$user_id} = 1; |
| } |
| return $self->{set_user_password}{$user_id}; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 grant_connect_method_access |
| |
| Parameters : user login id |
| Returns : boolean |
| Description : Edits the external_sshd_config. |
| TODO - in next release pull this out into connect method modules. |
| |
| =cut |
| |
| sub grant_connect_method_access { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| my $user_parameters = shift; |
| |
| if (!$user_parameters) { |
| notify($ERRORS{'WARNING'}, 0, "unable to create user, user parameters argument was not provided"); |
| return; |
| } |
| elsif (!ref($user_parameters) || ref($user_parameters) ne 'HASH') { |
| notify($ERRORS{'WARNING'}, 0, "unable to create user, argument provided is not a hash reference"); |
| return; |
| } |
| |
| my $username = $user_parameters->{username}; |
| if (!defined($username)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to create user on $computer_node_name, argument hash does not contain a 'username' key:\n" . format_data($user_parameters)); |
| return; |
| } |
| |
| my $uid = $user_parameters->{uid}; |
| if (!defined($uid)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to create user on $computer_node_name, argument hash does not contain a 'uid' key:\n" . format_data($user_parameters)); |
| return; |
| } |
| |
| my $ssh_public_keys = $user_parameters->{ssh_public_keys}; |
| if (!defined($ssh_public_keys)) { |
| notify($ERRORS{'OK'}, 0, "argument hash does not contain a 'ssh_public_keys' key:\n" . format_data($user_parameters)); |
| } |
| |
| my $home_directory_root = "/home"; |
| my $home_directory_path = "$home_directory_root/$username"; |
| my $home_directory_on_local_disk = $self->is_file_on_local_disk($home_directory_root); |
| # Add user's public ssh identity keys if exists |
| if ($ssh_public_keys) { |
| my $ssh_directory_path = "$home_directory_path/.ssh"; |
| my $authorized_keys_file_path = "$ssh_directory_path/authorized_keys"; |
| |
| # Determine if home directory is on a local device or network share |
| # Only add keys to home directories that are local, |
| # Don't add keys to network mounted filesystems |
| if ($home_directory_on_local_disk) { |
| # Create the .ssh directory |
| $self->create_directory($ssh_directory_path); |
| |
| if ($self->append_text_file($authorized_keys_file_path, "$ssh_public_keys\n")) { |
| notify($ERRORS{'DEBUG'}, 0, "added user's public SSH keys to $authorized_keys_file_path"); |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "failed to add user's public SSH keys to $authorized_keys_file_path"); |
| } |
| |
| if (!$self->set_file_owner($home_directory_path, $username, 'vcl', 1)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to set owner of user's home directory: $home_directory_path"); |
| } |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "user's public SSH keys not added to $authorized_keys_file_path, home directory is on a network share"); |
| } |
| } |
| |
| |
| # Append AllowUsers line to the end of the file |
| my $external_sshd_config_file_path = '/etc/ssh/external_sshd_config'; |
| my $allow_users_line = "AllowUsers $username"; |
| if ($self->append_text_file($external_sshd_config_file_path, $allow_users_line)) { |
| notify($ERRORS{'DEBUG'}, 0, "added line to $external_sshd_config_file_path: '$allow_users_line'"); |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "failed to add line to $external_sshd_config_file_path: '$allow_users_line'"); |
| return; |
| } |
| |
| $self->restart_service('ext_sshd') || return; |
| |
| # If ssh_public_keys add to authorized_keys |
| |
| return 1; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 kill_process |
| |
| Parameters : $pid, $signal (optional) |
| Returns : boolean |
| Description : Kills a process on the computer. |
| |
| =cut |
| |
| sub kill_process { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my ($pid_argument, $signal) = @_; |
| if (!defined($pid_argument)) { |
| notify($ERRORS{'WARNING'}, 0, "PID argument was not specified"); |
| return; |
| } |
| |
| my $computer_node_name = $self->data->get_computer_node_name(); |
| |
| # Suicide prevention |
| if ($pid_argument eq $PID) { |
| notify($ERRORS{'WARNING'}, 0, "process $pid_argument not killed, it is the currently running process"); |
| return; |
| } |
| |
| $signal = '9' unless defined $signal; |
| $signal =~ s/^-+//g; |
| |
| my $command = "kill -$signal $pid_argument"; |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to kill process $pid_argument on $computer_node_name"); |
| return; |
| } |
| elsif ($exit_status == 1 || grep(/no such process/i, @$output)) { |
| notify($ERRORS{'DEBUG'}, 0, "process $pid_argument not running on $computer_node_name"); |
| return 1; |
| } |
| elsif ($exit_status != 0 || grep(/^kill:/i, @$output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to kill process $pid_argument with signal $signal on $computer_node_name, command: '$command', exit status: $exit_status, output:\n" . join("\n", @$output)); |
| return 0; |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "killed process $pid_argument with signal $signal on $computer_node_name"); |
| return 1; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 is_process_running |
| |
| Parameters : $process_regex |
| Returns : array or hash reference |
| Description : Determines if any processes matching the $process_regex |
| argument are running on the computer. The $process_regex must be |
| a valid Perl regular expression. |
| |
| The following command is used to determine if a process is |
| running: |
| ps -e -o pid,args | grep -P "$process_regex" |
| |
| The behavior is different than if the -P argument is not used. |
| The following characters must be escaped with a backslash in |
| order for a literal match to be found: |
| | ( ) [ ] . + |
| |
| If these are not escaped, grep will interpret them as the |
| corresponing regular expression operational character. For |
| example: |
| |
| To match this literal string: |
| |(foo)| |
| Pass this: |
| \|\(foo\)\| |
| |
| To match 'foo' or 'bar, pass this: |
| (foo|bar) |
| |
| To match a pipe character ('|'), followed by either 'foo' or |
| 'bar, followed by another pipe character: |
| |foo| |
| Pass this: |
| \|(foo|bar)\| |
| |
| The return value differs based on how this subroutine is called. |
| If called in scalar context, a hash reference is returned. The |
| hash keys are PIDs and the values are the full name of the |
| process. If called in list context, an array is returned |
| containing the PIDs. |
| |
| =cut |
| |
| sub is_process_running { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| # Check the arguments |
| my ($process_regex) = @_; |
| if (!defined($process_regex)) { |
| notify($ERRORS{'WARNING'}, 0, "process regex pattern argument was not specified"); |
| return; |
| } |
| |
| my $computer_name = $self->data->get_computer_short_name(); |
| |
| my $command = "ps -e -o pid,args | grep -P \"$process_regex\""; |
| my ($exit_status, $output) = $self->execute($command, 0); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command on $computer_name to determine if process is running: $command"); |
| return; |
| } |
| |
| my $processes_running = {}; |
| for my $line (@$output) { |
| my ($pid, $process_name) = $line =~ /^\s*(\d+)\s*(.*[^\s])\s*/g; |
| |
| if (!defined($pid)) { |
| notify($ERRORS{'DEBUG'}, 0, "ignoring line, it does not begin with a number: '$line'"); |
| next; |
| } |
| elsif ($pid eq $PID) { |
| #notify($ERRORS{'DEBUG'}, 0, "ignoring line for the currently running process: $line"); |
| next; |
| } |
| elsif ($line =~ /grep -P/) { |
| #notify($ERRORS{'DEBUG'}, 0, "ignoring line containing for this command: $line"); |
| next; |
| } |
| elsif ($line =~ /sh -c/) { |
| # Ignore lines containing 'sh -c', probably indicating a duplicate process of a command run remotely |
| #notify($ERRORS{'DEBUG'}, 0, "ignoring containing 'sh -c': $line"); |
| next; |
| } |
| else { |
| #notify($ERRORS{'DEBUG'}, 0, "found matching process: $line"); |
| $processes_running->{$pid} = $process_name; |
| } |
| } |
| |
| my $process_count = scalar(keys %$processes_running); |
| if ($process_count) { |
| if (wantarray) { |
| my @process_ids = sort keys %$processes_running; |
| notify($ERRORS{'DEBUG'}, 0, "process is running on $computer_name, identifier: '$process_regex', returning array containing PIDs: @process_ids"); |
| return @process_ids; |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "process is running on $computer_name, identifier: '$process_regex', returning hash reference:\n" . format_data($processes_running)); |
| return $processes_running; |
| } |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "process is NOT running on $computer_name, identifier: '$process_regex', command: $command"); |
| return; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 is_display_manager_running |
| |
| Parameters : none |
| Returns : boolean |
| Description : Checks if a display manager (GUI) is running on the computer. |
| |
| =cut |
| |
| sub is_display_manager_running { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_name = $self->data->get_computer_short_name(); |
| |
| # Note: runlevel isn't reliable for all distros |
| # On Ubuntu, it displays 2 even if the GUI is running |
| |
| my $process_pattern; |
| |
| # CentOS "Welcome" screen |
| # 1700 /usr/bin/Xorg :9 -ac -nolisten tcp vt6 -br |
| |
| # ' 416 lightdm' |
| # '2955 lightdm --session-child 12 21' |
| $process_pattern .= '^\s*\d+\s+(kdm|lightdm)(\s|$)'; |
| |
| # Gnome |
| # 1870 /usr/sbin/gdm-binary -nodaemon |
| # 1898 /usr/libexec/gdm-simple-slave --display-id /org/gnome/DisplayManager/Display1 |
| # 1901 /usr/bin/Xorg :0 -br -verbose -audit 4 -auth /var/run/gdm/auth-for-gdm-laIZj5/database -nolisten tcp vt1 |
| # 1989 /usr/bin/gnome-session --autostart=/usr/share/gdm/autostart/LoginWindow/ |
| $process_pattern .= '|(gnome-session|gdm-binary)'; |
| |
| # ' 2891 /usr/bin/X -core :0 -seat seat0 -auth /var/run/lightdm/root/:0 -nolisten tcp vt7 -novtswitch' |
| $process_pattern .= '|bin\/X'; |
| |
| $process_pattern = "($process_pattern)"; |
| |
| my $process_info = $self->is_process_running($process_pattern); |
| if ($process_info) { |
| notify($ERRORS{'DEBUG'}, 0, "display manager is running on $computer_name:\n" . format_data($process_info)); |
| return 1; |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "display manager is not running on $computer_name"); |
| return 0; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 generate_ssh_private_key_file |
| |
| Parameters : $private_key_file_path, $type (optional), $bits (optional), $comment (optional), $passphrase, $options (optional) |
| Returns : boolean |
| Description : Calls ssh-keygen or dropbearkey to generate an SSH private key |
| file. |
| |
| =cut |
| |
| sub generate_ssh_private_key_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 ($private_key_file_path, $type, $bits, $comment, $passphrase, $options) = @_; |
| if (!$private_key_file_path) { |
| notify($ERRORS{'WARNING'}, 0, "private key file path argument was not specified"); |
| return; |
| } |
| $type = 'rsa' unless $type; |
| $passphrase = '' unless $passphrase; |
| |
| if (defined($comment)) { |
| $comment =~ s/\\*(["])/\\"$1/g; |
| } |
| |
| my $computer_name = $self->data->get_computer_short_name(); |
| |
| # Make sure the file does not already exist |
| if ($self->file_exists($private_key_file_path)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to generate SSH key, file already exists on $computer_name: $private_key_file_path"); |
| return; |
| } |
| |
| if ($self->command_exists('ssh-keygen')) { |
| if ($self->_generate_ssh_private_key_file_helper($private_key_file_path, $type, $bits, $comment, $passphrase, $options, 'ssh-keygen')) { |
| return 1; |
| } |
| } |
| if ($self->command_exists('dropbearkey')) { |
| if ($self->_generate_ssh_private_key_file_helper($private_key_file_path, $type, $bits, $comment, $passphrase, $options, 'dropbearkey')) { |
| return 1; |
| } |
| } |
| |
| if (ref($self) =~ /ManagementNode/) { |
| notify($ERRORS{'WARNING'}, 0, "failed to generate SSH key on $computer_name: $private_key_file_path"); |
| return; |
| } |
| |
| my $mn_temp_file_path = mktemp($computer_name . 'XXXXXX'); |
| notify($ERRORS{'DEBUG'}, 0, "attempting to create SSH private key file on this management node ($mn_temp_file_path) and copy it to $computer_name ($private_key_file_path)"); |
| my $result = $self->mn_os->generate_ssh_private_key_file($mn_temp_file_path, $type, $bits, $comment, $passphrase, $options); |
| if (!$result) { |
| notify($ERRORS{'WARNING'}, 0, "failed to create SSH private key file on this management node and copy it to $private_key_file_path on $computer_name"); |
| $self->mn_os->delete_file($mn_temp_file_path); |
| return; |
| } |
| |
| if (!$self->copy_file_to($mn_temp_file_path, $private_key_file_path)) { |
| notify($ERRORS{'WARNING'}, 0, "create SSH private key file on this management node but failed to copy it to $private_key_file_path on $computer_name"); |
| $self->mn_os->delete_file($mn_temp_file_path); |
| return; |
| } |
| else { |
| $self->mn_os->delete_file($mn_temp_file_path); |
| notify($ERRORS{'OK'}, 0, "created SSH private key file on this management and copied it to $private_key_file_path on $computer_name"); |
| return 1; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 _generate_ssh_private_key_file_helper |
| |
| Parameters : $private_key_file_path, $type, $bits, $comment, $passphrase, $options, $utility |
| Returns : boolean |
| Description : Calls ssh-keygen to generate an SSH private key file. |
| |
| =cut |
| |
| sub _generate_ssh_private_key_file_helper { |
| 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 ($private_key_file_path, $type, $bits, $comment, $passphrase, $options, $utility) = @_; |
| if (!defined($utility)) { |
| notify($ERRORS{'WARNING'}, 0, "utility argument was not supplied"); |
| return; |
| } |
| |
| my $computer_name = $self->data->get_computer_short_name(); |
| |
| my $command; |
| if ($utility eq 'ssh-keygen') { |
| $command = "ssh-keygen -t $type -f \"$private_key_file_path\" -N \"$passphrase\""; |
| $command .= " -b $bits" if (defined($bits) && length($bits)); |
| $command .= " $options" if (defined($options) && length($options)); |
| $command .= " -C \"$comment\"" if (defined($comment) && length($comment)); |
| } |
| elsif ($utility eq 'dropbearkey') { |
| $command = "dropbearkey -t $type -f \"$private_key_file_path\""; |
| $command .= " -s $bits" if (defined($bits) && length($bits)); |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "invalid utility argument provided: '$utility', it must either be 'ssh-keygen' or 'dropbearkey'"); |
| return; |
| } |
| |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to generate SSH key using $utility on $computer_name: $command"); |
| return; |
| } |
| elsif ($exit_status ne '0') { |
| notify($ERRORS{'WARNING'}, 0, "failed to generate SSH key using $utility on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); |
| return; |
| } |
| else { |
| notify($ERRORS{'OK'}, 0, "generated SSH key using $utility on $computer_name: $private_key_file_path, command: $command"); |
| return 1; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 generate_ssh_public_key_file |
| |
| Parameters : $private_key_file_path, $public_key_file_path, $comment (optional) |
| Returns : boolean |
| Description : Calls ssh-keygen to retrieve the corresponding SSH public key |
| from a private key file and generates a file containing the |
| public key. |
| |
| =cut |
| |
| sub generate_ssh_public_key_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 ($private_key_file_path, $public_key_file_path, $comment) = @_; |
| if (!$private_key_file_path) { |
| notify($ERRORS{'WARNING'}, 0, "private key file path argument was not specified"); |
| return; |
| } |
| if (!$public_key_file_path) { |
| notify($ERRORS{'WARNING'}, 0, "public key file path argument was not specified"); |
| return; |
| } |
| |
| my $computer_name = $self->data->get_computer_short_name(); |
| |
| # Make sure the private key file exists |
| if (!$self->file_exists($private_key_file_path)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to generate SSH public key file, private key file does not exist on $computer_name: $private_key_file_path"); |
| return; |
| } |
| |
| # Make sure the public key file does not exist |
| if ($self->file_exists($public_key_file_path)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to create SSH public key file, public key file already exists on $computer_name: $public_key_file_path"); |
| return; |
| } |
| |
| my $public_key_string = $self->get_ssh_public_key_string($private_key_file_path, $comment); |
| if (!$public_key_string) { |
| notify($ERRORS{'WARNING'}, 0, "failed to create SSH public key file: $public_key_file_path, public key string could not be retrieved from private key file: $private_key_file_path"); |
| return; |
| } |
| |
| if ($self->create_text_file($public_key_file_path, $public_key_string)) { |
| notify($ERRORS{'DEBUG'}, 0, "created SSH public key file: $public_key_file_path"); |
| return 1; |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "failed to create SSH public key file: $public_key_file_path"); |
| return; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 get_ssh_public_key_string |
| |
| Parameters : $private_key_file_path, $comment (optional) |
| Returns : boolean |
| Description : Extracts the SSH public key from a private key file. |
| |
| =cut |
| |
| sub get_ssh_public_key_string { |
| 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 ($private_key_file_path, $comment) = @_; |
| if (!$private_key_file_path) { |
| notify($ERRORS{'WARNING'}, 0, "private key file path argument was not specified"); |
| return; |
| } |
| |
| my $computer_name = $self->data->get_computer_short_name(); |
| |
| # Make sure the private key file exists |
| if (!$self->file_exists($private_key_file_path)) { |
| notify($ERRORS{'WARNING'}, 0, "unable to retrieve SSH public key, private key file does not exist on $computer_name: $private_key_file_path"); |
| return; |
| } |
| |
| my $public_key_string; |
| if ($self->command_exists('ssh-keygen')) { |
| $public_key_string = $self->_get_ssh_public_key_string_helper($private_key_file_path, 'ssh-keygen'); |
| } |
| if (!$public_key_string && $self->command_exists('dropbearkey')) { |
| $public_key_string = $self->_get_ssh_public_key_string_helper($private_key_file_path, 'dropbearkey'); |
| } |
| if ($public_key_string) { |
| #if ($comment) { |
| # $public_key_string =~ s/(ssh-[^\s]+\s[^\s=]+).*$/$1 $comment/g; |
| #} |
| return $public_key_string; |
| } |
| |
| if (ref($self) =~ /ManagementNode/) { |
| notify($ERRORS{'WARNING'}, 0, "failed to retrieve SSH public key from private key file on $computer_name: $private_key_file_path"); |
| return; |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "attempting to copy SSH private key file $private_key_file_path from $computer_name and extract the public key on this management node"); |
| } |
| |
| my ($mn_temp_file_handle, $mn_temp_file_path) = tempfile(SUFFIX => '.key', UNLINK => 1); |
| if (!$self->copy_file_from($private_key_file_path, $mn_temp_file_path)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to retrieve SSH public key from private key file on $computer_name: $private_key_file_path, failed to copy temp file to management node"); |
| $self->mn_os->delete_file($mn_temp_file_path); |
| return; |
| } |
| $self->mn_os->set_file_permissions($mn_temp_file_path, '0400'); |
| |
| $public_key_string = $self->mn_os->get_ssh_public_key_string($mn_temp_file_path, $comment); |
| $self->mn_os->delete_file($mn_temp_file_path); |
| return $public_key_string; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 _get_ssh_public_key_string_helper |
| |
| Parameters : $private_key_file_path, $utility |
| Returns : boolean |
| Description : |
| |
| =cut |
| |
| sub _get_ssh_public_key_string_helper { |
| 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 ($private_key_file_path, $utility) = @_; |
| if (!$private_key_file_path) { |
| notify($ERRORS{'WARNING'}, 0, "private key file path argument was not specified"); |
| return; |
| } |
| elsif (!$utility) { |
| notify($ERRORS{'WARNING'}, 0, "utility argument (ssh-keygen or dropbearkey) was not specified"); |
| return; |
| } |
| |
| my $computer_name = $self->data->get_computer_short_name(); |
| |
| my $command; |
| if ($utility eq 'ssh-keygen') { |
| $command = "ssh-keygen -y -f \"$private_key_file_path\""; |
| } |
| elsif ($utility eq 'dropbearkey') { |
| $command = "/bin/dropbearkey -f \"$private_key_file_path\" -y"; |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "invalid utility argument provided: '$utility', it must either be 'ssh-keygen' or 'dropbearkey'"); |
| return; |
| } |
| |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to retrieve SSH public key string using $utility from $private_key_file_path on $computer_name"); |
| return; |
| } |
| elsif ($exit_status ne 0) { |
| notify($ERRORS{'WARNING'}, 0, "failed to retrieve SSH public key string using $utility from $private_key_file_path on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); |
| return; |
| } |
| |
| my ($ssh_public_key_string) = grep(/^ssh-.*/, @$output); |
| if ($ssh_public_key_string) { |
| notify($ERRORS{'OK'}, 0, "retrieved SSH public key string using $utility from $private_key_file_path on $computer_name:\n$ssh_public_key_string"); |
| return $ssh_public_key_string; |
| } |
| else { |
| notify($ERRORS{'OK'}, 0, "failed to retrieved SSH public key string using $utility from $private_key_file_path on $computer_name, output does not contain a line beginning with 'ssh-', command:\n$command\noutput:\n" . join("\n", @$output)); |
| return; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 install_package |
| |
| Parameters : $package_name, $timeout_seconds (optional) |
| Returns : boolean |
| Description : Installs a Linux package using yum. |
| |
| =cut |
| |
| sub install_package { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my ($package_name, $timeout_seconds) = @_; |
| if (!$package_name) { |
| notify($ERRORS{'WARNING'}, 0, "package name argument was not supplied"); |
| return; |
| } |
| |
| my $computer_name = $self->data->get_computer_node_name(); |
| |
| if (!$self->command_exists('yum')) { |
| notify($ERRORS{'WARNING'}, 0, "failed to install $package_name on $computer_name, yum command is not available"); |
| return; |
| } |
| |
| $timeout_seconds = 120 unless $timeout_seconds; |
| |
| # Delete service info in case package adds a service that was previously detected as not existing |
| $self->_delete_cached_service_info(); |
| |
| my $command = "yum install -q -y $package_name"; |
| notify($ERRORS{'DEBUG'}, 0, "attempting to install $package_name using yum on $computer_name, timeout seconds: $timeout_seconds"); |
| my ($exit_status, $output) = $self->execute({ |
| 'command' => $command, |
| 'display_output' => 0, |
| 'timeout_seconds' => $timeout_seconds, |
| }); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to install $package_name using yum on $computer_name"); |
| return; |
| } |
| elsif ($exit_status ne 0) { |
| notify($ERRORS{'WARNING'}, 0, "failed to install $package_name using yum on $computer_name, exit status: $exit_status, command: '$command', output:\n" . join("\n", @$output)); |
| return 0; |
| } |
| elsif (grep(/$package_name.+already installed/, @$output)) { |
| notify($ERRORS{'DEBUG'}, 0, "$package_name is already installed on $computer_name, command: '$command', output:\n" . join("\n", @$output)); |
| return 1; |
| } |
| else { |
| notify($ERRORS{'OK'}, 0, "installed $package_name using yum on $computer_name, command: '$command', output:\n" . join("\n", @$output)); |
| return 1; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 nfs_mount_share |
| |
| Parameters : $remote_nfs_share, $local_mount_directory, $options |
| Returns : boolean |
| Description : Mounts an NFS share on the computer. The $remote_nfs_share |
| argument must be in the for used by the mount command: |
| <hostname|IP>:/path-to-share |
| |
| The $local_mount_directory argument must specify a directory. |
| This directory will be created if it does not already exist. |
| |
| The $options argument allows NFS mount options to be specified |
| such as: |
| rsize=1048576,wsize=1048576,vers=3 |
| |
| A 'retry=0' option is included in the mount command if $options |
| does not explicitly include it. This causes a single mount |
| attempt to be made rather than the default behavior of trying for |
| up to 2 minutes. This is required because the VCL code will |
| timeout before the Linux mount command gives up. As a result, VCL |
| does not receive the error message. This prevents automatic |
| corrective actions to happen such as creating the remote |
| directory. |
| |
| The $options string must be formatted correctly and is passed |
| directly to the mount command. |
| |
| =cut |
| |
| sub nfs_mount_share { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my ($remote_nfs_share, $local_mount_directory, $ignore_missing_remote_directory_error, $nfs_options, $is_retry_attempt) = @_; |
| if (!defined($remote_nfs_share)) { |
| notify($ERRORS{'WARNING'}, 0, "remote target argument was not supplied"); |
| return; |
| } |
| elsif (!defined($local_mount_directory)) { |
| notify($ERRORS{'WARNING'}, 0, "local directory path argument was not supplied"); |
| return; |
| } |
| |
| my $computer_name = $self->data->get_computer_node_name(); |
| |
| # Try to repair NFS client if 1st attempt failed |
| if ($is_retry_attempt) { |
| # Check if nfs-utils is installed, if not, try to install it |
| # Error looks like this if nfs-utils is not installed: |
| # mount: wrong fs type, bad option, bad superblock on 10.1.2.3:/nfs, |
| # missing codepage or helper program, or other error |
| # (for several filesystems (e.g. nfs, cifs) you might |
| # need a /sbin/mount.<type> helper program) |
| # In some cases useful info is found in syslog - try |
| # dmesg | tail or so |
| if (!$self->command_exists('mount.nfs')) { |
| if (ref($self) =~ /Ubuntu/) { |
| $self->install_package('nfs-common'); |
| } |
| else { |
| $self->install_package('nfs-utils'); |
| } |
| } |
| |
| # Check if the rpcbind service exists, if not, try to install it |
| # Mount may fail if rpcbind service is not installed and running: |
| # mount.nfs: rpc.statd is not running but is required for remote locking. |
| # mount.nfs: Either use '-o nolock' to keep locks local, or start statd. |
| # mount.nfs: an incorrect mount option was specified |
| $self->install_package('rpcbind') if !$self->service_exists('rpcbind'); |
| |
| # Try to start the service |
| $self->start_service('rpcbind') if $self->service_exists('rpcbind'); |
| } |
| |
| # Create the local mount point directory if it does not exist |
| my $local_mount_directory_previously_existed = $self->file_exists($local_mount_directory); |
| if (!$local_mount_directory_previously_existed && !$self->create_directory($local_mount_directory)) { |
| notify($ERRORS{'WARNING'}, 0, "unable to mount $remote_nfs_share on $computer_name, failed to create directory: $local_mount_directory"); |
| return; |
| } |
| |
| my $mount_command = "mount -t nfs $remote_nfs_share \"$local_mount_directory\" -v"; |
| if ($nfs_options) { |
| # Add retry=0 if it wasn't explicitly specified in the argument |
| if ($nfs_options =~ /retry/) { |
| $mount_command .= " -o $nfs_options"; |
| } |
| else { |
| $mount_command .= " -o retry=0,$nfs_options"; |
| } |
| } |
| else { |
| $mount_command .= " -o retry=0"; |
| } |
| |
| # Save return value if error is encountered and don't return immediately |
| # Facilitates a single call to clean up local directory just created if it didn't previously exist |
| my $return_value; |
| |
| notify($ERRORS{'DEBUG'}, 0, "attempting to mount NFS share on $computer_name: $mount_command"); |
| my ($mount_exit_status, $mount_output) = $self->execute({ |
| command => $mount_command, |
| timeout_seconds => 10, |
| max_attempts => 2, |
| }); |
| if (!defined($mount_exit_status)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to mount NFS share on $computer_name: $mount_command"); |
| $return_value = undef; |
| } |
| elsif ($mount_exit_status eq 0) { |
| notify($ERRORS{'OK'}, 0, "mounted NFS share on $computer_name: $remote_nfs_share --> $local_mount_directory"); |
| |
| # Add the share to /etc/fstab |
| $self->add_fstab_nfs_mount($remote_nfs_share, $local_mount_directory); |
| |
| return 1; |
| } |
| elsif (grep(/already mounted/, @$mount_output)) { |
| # mount.nfs: /mnt/mymountpoint is busy or already mounted |
| if ($self->is_nfs_share_mounted($remote_nfs_share, $local_mount_directory)) { |
| return 1; |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "failed to mount NFS share on $computer_name: $remote_nfs_share --> $local_mount_directory, mount command output indicates 'already mounted' but failed to verify mount in /proc/mounts, mount command: '$mount_command', exit status: $mount_exit_status, mount output:\n" . join("\n", @$mount_output)); |
| $return_value = undef; |
| } |
| } |
| elsif (grep(/(No such file or directory)/, @$mount_output)) { |
| # mount.nfs: mount(2): No such file or directory |
| # mount.nfs: mounting <hostname>:/<remote directory> failed, reason given by server: No such file or directory |
| if ($ignore_missing_remote_directory_error) { |
| notify($ERRORS{'DEBUG'}, 0, "unable to mount NFS share on $computer_name because remote directory does not exist: $remote_nfs_share, returning 0"); |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "unable to mount NFS share on $computer_name because remote directory does not exist: $remote_nfs_share, returning 0, command: '$mount_command', exit status: $mount_exit_status, output:\n" . join("\n", @$mount_output)); |
| } |
| $return_value = 0; |
| } |
| elsif (grep(/(Invalid argument|incorrect mount option|Usage:)/, @$mount_output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to mount NFS share on $computer_name: $remote_nfs_share --> $local_mount_directory, command: '$mount_command', exit status: $mount_exit_status, output:\n" . join("\n", @$mount_output)); |
| $return_value = undef; |
| } |
| elsif ($is_retry_attempt) { |
| notify($ERRORS{'WARNING'}, 0, "failed to mount NFS share on $computer_name on 2nd attempt: $remote_nfs_share --> $local_mount_directory, command: '$mount_command', exit status: $mount_exit_status, output:\n" . join("\n", @$mount_output)); |
| $return_value = undef; |
| } |
| else { |
| notify($ERRORS{'OK'}, 0, "failed to mount NFS share on $computer_name on 1st attempt: $remote_nfs_share --> $local_mount_directory, command: '$mount_command', exit status: $mount_exit_status, output:\n" . join("\n", @$mount_output)); |
| } |
| |
| # Clean up local directory if it didn't previously exist |
| if (!$local_mount_directory_previously_existed) { |
| my @local_mount_directory_files = $self->find_files($local_mount_directory, '*', 1); |
| if (@local_mount_directory_files) { |
| notify($ERRORS{'WARNING'}, 0, "local mount directory just created will NOT be deleted: $local_mount_directory, NFS mount seemed to have failed but directory is not empty:\n" . join("\n", @local_mount_directory_files)); |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "local mount directory just created will be deleted: $local_mount_directory"); |
| $self->delete_file($local_mount_directory); |
| } |
| } |
| |
| if ($is_retry_attempt) { |
| return $return_value; |
| } |
| else { |
| # Try to mount the NFS share again, set retry flag to avoid endless loop |
| return $self->nfs_mount_share($remote_nfs_share, $local_mount_directory, $ignore_missing_remote_directory_error, $nfs_options, 1); |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 nfs_unmount_share |
| |
| Parameters : $local_mount_directory |
| Returns : boolean |
| Description : Unmounts an NFS share on the computer. |
| |
| =cut |
| |
| sub nfs_unmount_share { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my ($local_mount_directory) = @_; |
| if (!defined($local_mount_directory)) { |
| notify($ERRORS{'WARNING'}, 0, "local directory path argument was not supplied"); |
| return; |
| } |
| |
| my $computer_name = $self->data->get_computer_node_name(); |
| |
| my $result; |
| |
| # Make sure lines are removed from /etc/fstab regardless of unmount result |
| (my $local_mount_directory_pattern = $local_mount_directory) =~ s/\//\\\//g; |
| $local_mount_directory_pattern = '[\s\t]' . $local_mount_directory_pattern . '[\s\t]'; |
| $self->remove_matching_fstab_lines($local_mount_directory_pattern); |
| |
| my $umount_command = "umount -v \"$local_mount_directory\""; |
| my ($umount_exit_status, $umount_output) = $self->execute({ |
| command => $umount_command, |
| timeout_seconds => 30, |
| max_attempts => 1, |
| }); |
| if (!defined($umount_exit_status)) { |
| notify($ERRORS{'CRITICAL'}, 0, "failed to execute command to umount NFS share on $computer_name: $umount_command"); |
| return; |
| } |
| elsif ($umount_exit_status eq 0 || grep(/\sumounted/, @$umount_output)) { |
| notify($ERRORS{'OK'}, 0, "unmounted NFS share on $computer_name: $local_mount_directory, output:\n" . join("\n", @$umount_output)); |
| $result = 1; |
| } |
| elsif (grep(/(not mounted|not found|Could not find)/i, @$umount_output)) { |
| # umount: /nfs-share: not found |
| # Could not find /nfs-share in mtab |
| notify($ERRORS{'OK'}, 0, "NFS share is not mounted on $computer_name: $local_mount_directory"); |
| $result = 1; |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "lazy unmount will be attempted after failing to perform normal NFS unmount on $computer_name: $local_mount_directory, command: '$umount_command', exit status: $umount_exit_status, output:\n" . join("\n", @$umount_output)); |
| my $umount_lazy_command = "umount -v -l \"$local_mount_directory\""; |
| my ($umount_lazy_exit_status, $umount_lazy_output) = $self->execute({ |
| command => $umount_lazy_command, |
| timeout_seconds => 30, |
| max_attempts => 1, |
| }); |
| |
| if ($self->is_nfs_share_mounted('.*', $local_mount_directory)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to unmount NFS share on $computer_name: $local_mount_directory, command: '$umount_command', exit status: $umount_exit_status, output:\n" . join("\n", @$umount_output)); |
| $result = 0; |
| } |
| else { |
| notify($ERRORS{'OK'}, 0, "lazy unmounted of NFS share on $computer_name: $local_mount_directory"); |
| $result = 1; |
| } |
| } |
| |
| # Clean up local directory if it didn't previously exist |
| my @local_mount_directory_files = $self->find_files($local_mount_directory, '*', 1); |
| if (@local_mount_directory_files) { |
| notify($ERRORS{'WARNING'}, 0, "local mount directory will NOT be deleted: $local_mount_directory, directory is not empty:\n" . join("\n", @local_mount_directory_files)); |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "unmounted directory is empty and will be deleted: $local_mount_directory"); |
| $self->delete_file($local_mount_directory); |
| } |
| |
| return $result; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 is_nfs_share_mounted |
| |
| Parameters : $remote_nfs_share, $local_mount_directory |
| Returns : boolean |
| Description : Checks if an NFS share is mounted on the computer matching both |
| the remote NFS share path and local mount point directory |
| arguments. |
| |
| =cut |
| |
| sub is_nfs_share_mounted { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my ($remote_nfs_share, $local_mount_directory) = @_; |
| if (!defined($remote_nfs_share)) { |
| notify($ERRORS{'WARNING'}, 0, "remote target argument was not supplied"); |
| return; |
| } |
| elsif (!defined($local_mount_directory)) { |
| notify($ERRORS{'WARNING'}, 0, "local directory path argument was not supplied"); |
| return; |
| } |
| |
| my $computer_name = $self->data->get_computer_node_name(); |
| |
| if ($self->get_nfs_mount_string($remote_nfs_share, $local_mount_directory)) { |
| notify($ERRORS{'DEBUG'}, 0, "NFS share is mounted on $computer_name: $remote_nfs_share --> $local_mount_directory"); |
| return 1; |
| } |
| else { |
| notify($ERRORS{'DEBUG'}, 0, "NFS share is NOT mounted on $computer_name: $remote_nfs_share --> $local_mount_directory"); |
| return 0; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 get_nfs_mount_strings |
| |
| Parameters : none |
| Returns : array |
| Description : Retrieves the contents of /proc/mounts and extracts all strings |
| that contain 'nfs' or 'nfs4'. An array containing the raw |
| /proc/mounts strings is returned. |
| |
| =cut |
| |
| sub get_nfs_mount_strings { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_name = $self->data->get_computer_node_name(); |
| |
| my $command = "cat /proc/mounts"; |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($exit_status)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command on $computer_name: $command"); |
| return; |
| } |
| |
| my @mount_strings; |
| for my $line (@$output) { |
| if ($line !~ /\snfs\d*\s/) { |
| next; |
| } |
| |
| push @mount_strings, $line; |
| } |
| |
| notify($ERRORS{'DEBUG'}, 0, "retrieved NFS mount strings from $computer_name:\n" . join("\n", @mount_strings)); |
| return @mount_strings; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 get_nfs_mount_string |
| |
| Parameters : $remote_nfs_share, $local_mount_directory |
| Returns : string |
| Description : Examines the contents of /proc/mounts and attempts to locate a |
| line matching the arguments. If found, the line is returned which |
| may be used in /etc/fstab. If not found, 0 is returned. Undefined |
| is returned if an error occurs. |
| |
| =cut |
| |
| sub get_nfs_mount_string { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my ($remote_nfs_share, $local_mount_directory) = @_; |
| if (!defined($remote_nfs_share)) { |
| notify($ERRORS{'WARNING'}, 0, "remote target argument was not supplied"); |
| return; |
| } |
| elsif (!defined($local_mount_directory)) { |
| notify($ERRORS{'WARNING'}, 0, "local directory path argument was not supplied"); |
| return; |
| } |
| |
| # Remove trailing slashes for consistent comparison |
| $remote_nfs_share =~ s/\/+$//; |
| $local_mount_directory =~ s/\/+$//; |
| |
| # If the NFS share or local directory contain a space, the octal value will appear in /proc/mounts |
| $remote_nfs_share =~ s/ /\\\\040/g; |
| $local_mount_directory =~ s/ /\\\\040/g; |
| |
| my $computer_name = $self->data->get_computer_node_name(); |
| |
| my $command = "cat /proc/mounts | grep ' nfs'"; |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($exit_status)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command on $computer_name: $command"); |
| return; |
| } |
| |
| for my $line (@$output) { |
| # 10.1.2.3:/share/data /tmp/data nfs4 rw,relatime,vers=4,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=10.25.10.194,minorversion=0,local_lock=none,addr=10.1.2.3 0 0 |
| if ($line =~ m|^$remote_nfs_share\/?\s+$local_mount_directory\/?\s|) { |
| notify($ERRORS{'DEBUG'}, 0, "found NFS share line in /proc/mounts on $computer_name: $remote_nfs_share --> $local_mount_directory\n$line"); |
| return $line; |
| } |
| } |
| |
| notify($ERRORS{'DEBUG'}, 0, "/proc/mounts on $computer_name does NOT contain a line matching NFS share: $remote_nfs_share --> $local_mount_directory\n" . join("\n", @$output)); |
| return 0; |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 add_fstab_nfs_mount |
| |
| Parameters : $remote_nfs_share, $local_mount_directory |
| Returns : boolean |
| Description : Adds a line to /etc/fstab for an existing NFS mount. The share |
| must be mounted prior to calling this subroutine. |
| |
| =cut |
| |
| sub add_fstab_nfs_mount { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my ($remote_nfs_share, $local_mount_directory) = @_; |
| if (!defined($remote_nfs_share)) { |
| notify($ERRORS{'WARNING'}, 0, "remote target argument was not supplied"); |
| return; |
| } |
| elsif (!defined($local_mount_directory)) { |
| notify($ERRORS{'WARNING'}, 0, "local directory path argument was not supplied"); |
| return; |
| } |
| |
| my $computer_name = $self->data->get_computer_node_name(); |
| |
| my $nfs_mount_string = $self->get_nfs_mount_string($remote_nfs_share, $local_mount_directory); |
| if (!$nfs_mount_string) { |
| notify($ERRORS{'WARNING'}, 0, "unable to add NFS mount line to /etc/fstab, NFS share is not mounted: $remote_nfs_share --> $local_mount_directory"); |
| return; |
| } |
| |
| # Add a trailing comment to identify it was added automatically |
| my $timestamp = POSIX::strftime("%Y-%m-%d %H-%M-%S", localtime); |
| $nfs_mount_string .= "\t# Added by VCL ($timestamp)"; |
| |
| # Remove existing line matching the local mount directory followed by "nfs" to avoid duplicate lines |
| $self->remove_matching_fstab_lines("$local_mount_directory nfs"); |
| |
| my @fstab_lines = $self->get_file_contents('/etc/fstab'); |
| push @fstab_lines, $nfs_mount_string; |
| my $new_fstab_contents = join("\n", @fstab_lines); |
| |
| $self->copy_file('/etc/fstab', "/tmp/fstab.$timestamp"); |
| |
| if ($self->create_text_file('/etc/fstab', $new_fstab_contents)) { |
| notify($ERRORS{'OK'}, 0, "added line to /etc/fstab on $computer_name:\n$nfs_mount_string"); |
| return 1; |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "failed to add line to /etc/fstab on $computer_name:\n$nfs_mount_string"); |
| return; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 remove_matching_fstab_lines |
| |
| Parameters : $regex_pattern |
| Returns : boolean |
| Description : Removes all lines from /etc/fstab matching the pattern. |
| |
| =cut |
| |
| sub remove_matching_fstab_lines { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my ($regex_pattern) = @_; |
| if (!defined($regex_pattern)) { |
| notify($ERRORS{'WARNING'}, 0, "pattern argument was not supplied"); |
| return; |
| } |
| |
| my $computer_name = $self->data->get_computer_node_name(); |
| |
| my $updated_fstab_contents; |
| |
| my @matching_lines; |
| my @fstab_lines = $self->get_file_contents('/etc/fstab'); |
| for my $fstab_line (@fstab_lines) { |
| (my $fstab_line_cleaned = $fstab_line) =~ s/\\040/ /g; |
| |
| if ($fstab_line =~ m|$regex_pattern| || $fstab_line_cleaned =~ m|$regex_pattern|) { |
| push @matching_lines, $fstab_line; |
| notify($ERRORS{'DEBUG'}, 0, "removing lines from /etc/fstab matching pattern: $regex_pattern\n$fstab_line"); |
| next; |
| } |
| $updated_fstab_contents .= "$fstab_line\n"; |
| } |
| |
| my $matching_line_count = scalar(@matching_lines); |
| if (!$matching_line_count) { |
| notify($ERRORS{'DEBUG'}, 0, "/etc/fstab does not contain any lines matching pattern: $regex_pattern"); |
| return 1; |
| } |
| notify($ERRORS{'DEBUG'}, 0, "removing $matching_line_count line" . ($matching_line_count == 1 ? '' : 's') . " from /etc/fstab on $computer_name:\n" . join("\n", @matching_lines)); |
| |
| # Save a backup |
| my $timestamp = POSIX::strftime("%Y-%m-%d_%H-%M-%S\n", localtime); |
| $self->copy_file('/etc/fstab', "/tmp/fstab.$timestamp"); |
| |
| return $self->create_text_file('/etc/fstab', $updated_fstab_contents); |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 update_resolv_conf |
| |
| Parameters : @public_dns_servers (optional) |
| Returns : boolean |
| Description : Updates /etc/resolv.conf on the computer. Existing nameserver |
| lines are removed and new nameserver lines are added based on the |
| public DNS servers configured for the management node. |
| |
| =cut |
| |
| sub update_resolv_conf { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_name = $self->data->get_computer_short_name(); |
| my $public_ip_configuration = $self->data->get_management_node_public_ip_configuration(); |
| my @mn_dns_servers = shift || $self->data->get_management_node_public_dns_servers(); |
| |
| my @server_request_dns_servers = $self->data->get_server_request_dns_servers(); |
| |
| my @dns_servers; |
| if (@server_request_dns_servers) { |
| @dns_servers = @server_request_dns_servers; |
| notify($ERRORS{'DEBUG'}, 0, "server request specific DNS servers will be statically set on $computer_name: " . join(", ", @dns_servers)); |
| } |
| elsif ($public_ip_configuration =~ /static/i && @mn_dns_servers) { |
| @dns_servers = @mn_dns_servers; |
| notify($ERRORS{'DEBUG'}, 0, "management node IP configuration set to $public_ip_configuration, management node DNS servers will be statically set on $computer_name: " . join(", ", @dns_servers)); |
| } |
| else { |
| notify($ERRORS{'WARNING'}, 0, "$computer_name not configured to use static DNS servers:\n" . |
| "management node IP configuration : $public_ip_configuration\n" . |
| "management node DNS servers configured : " . (@mn_dns_servers ? 'yes' : 'no') |
| ); |
| return; |
| } |
| |
| my $resolv_conf_path = "/etc/resolv.conf"; |
| |
| my @resolv_conf_lines_existing = $self->get_file_contents($resolv_conf_path); |
| my @resolv_conf_lines_new; |
| for my $line (@resolv_conf_lines_existing) { |
| if ($line =~ /\sVCL/) { |
| last; |
| } |
| elsif ($line !~ /^\s*nameserver/) { |
| push @resolv_conf_lines_new, $line; |
| } |
| } |
| |
| # Add a comment marking what was added by VCL |
| my $timestamp = POSIX::strftime("%m-%d-%Y %H:%M:%S", localtime); |
| push @resolv_conf_lines_new, "# $timestamp: The following was added by VCL"; |
| |
| # Add a nameserver line for each configured DNS server |
| for my $public_dns_server (@dns_servers) { |
| push @resolv_conf_lines_new, "nameserver $public_dns_server"; |
| } |
| |
| my $resolv_conf_contents_new = join("\n", @resolv_conf_lines_new); |
| if ($self->create_text_file($resolv_conf_path, $resolv_conf_contents_new)) { |
| notify($ERRORS{'DEBUG'}, 0, "updated $resolv_conf_path on $computer_name:\n$resolv_conf_contents_new"); |
| } |
| else { |
| return 0; |
| } |
| } |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 get_product_name |
| |
| Parameters : none |
| Returns : string |
| Description : Retrieves the name of the Linux distribution from |
| /etc/redhat-release. If this file does not exist, null is |
| returned. |
| |
| =cut |
| |
| sub get_product_name { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| return $self->{product_name} if defined($self->{product_name}); |
| |
| my $computer_name = $self->data->get_computer_short_name(); |
| |
| my $release_file_path = '/etc/redhat-release'; |
| if (!$self->file_exists($release_file_path)) { |
| notify($ERRORS{'WARNING'}, 0, "unable to determine Linux distribution name installed on $computer_name, file does not exist: $release_file_path"); |
| return; |
| } |
| |
| my @release_file_lines = $self->get_file_contents($release_file_path); |
| if (!@release_file_lines) { |
| notify($ERRORS{'WARNING'}, 0, "unable to determine Linux distribution name installed on $computer_name, failed to retrieve contents of: $release_file_path"); |
| return; |
| } |
| |
| # In case there are multiple lines, get the first one with a word character |
| my ($product_name) = grep(/\w/, @release_file_lines); |
| if (!$product_name) { |
| notify($ERRORS{'WARNING'}, 0, "unable to determine Linux distribution name installed on $computer_name, $release_file_path does not contain a line with a word character, contents:\n" . join("\n", @release_file_lines)); |
| return; |
| } |
| |
| $self->{product_name} = $product_name; |
| notify($ERRORS{'OK'}, 0, "determined Linux distribution name installed on $computer_name: '$self->{product_name}'"); |
| return $self->{product_name}; |
| } |
| |
| |
| #////////////////////////////////////////////////////////////////////////////// |
| |
| =head2 get_management_node_connected_ip_address |
| |
| Parameters : none |
| Returns : string |
| Description : Returns the management node's IP address used to connect to the |
| remote computer. |
| |
| =cut |
| |
| sub get_management_node_connected_ip_address { |
| my $self = shift; |
| if (ref($self) !~ /linux/i) { |
| notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); |
| return; |
| } |
| |
| my $computer_name = $self->data->get_computer_node_name(); |
| |
| my $command = 'echo $SSH_CONNECTION'; |
| my ($exit_status, $output) = $self->execute($command); |
| if (!defined($output)) { |
| notify($ERRORS{'WARNING'}, 0, "failed to execute command to determine IP address management node used to connect to $computer_name: $command"); |
| return; |
| } |
| elsif ($exit_status ne '0') { |
| notify($ERRORS{'WARNING'}, 0, "failed to determine IP address management node used to connect to $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); |
| return; |
| } |
| |
| # There should be a single line formatted at: |
| # <source IP> <source port> <remote IP> <remote port> |
| # Example: |
| # 10.25.7.2 43570 10.25.10.194 22 |
| for my $line (@$output) { |
| my ($source_ip) = $line =~ /^([\d\.]+)\s/; |
| if ($source_ip) { |
| notify($ERRORS{'OK'}, 0, "determined IP address management node used to connect to $computer_name: $source_ip"); |
| return $source_ip; |
| } |
| } |
| |
| notify($ERRORS{'WARNING'}, 0, "failed to determine IP address management node used to connect to $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); |
| return; |
| } |
| |
| ##///////////////////////////////////////////////////////////////////////////// |
| 1; |
| __END__ |
| |
| =head1 SEE ALSO |
| |
| L<http://cwiki.apache.org/VCL/> |
| |
| =cut |