blob: 8ddc6848f657f13d32701dfd36d3c7e432fa7d37 [file] [log] [blame]
#!/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