blob: a74d74b0d801f835350d884e30e7e18f8215a5e3 [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::Ubuntu.pm - VCL module to support Ubuntu operating systems
=head1 DESCRIPTION
This module provides VCL support for Ubuntu operating systems.
=cut
###############################################################################
package VCL::Module::OS::Linux::Ubuntu;
# Specify the lib path using FindBin
use FindBin;
use lib "$FindBin::Bin/../../../..";
# Configure inheritance
use base qw(VCL::Module::OS::Linux);
# Specify the version of this module
our $VERSION = '2.5.1';
# Specify the version of Perl to use
use 5.008000;
use strict;
use warnings;
use diagnostics;
no warnings 'redefine';
use VCL::utils;
###############################################################################
=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/Ubuntu
=cut
our $SOURCE_CONFIGURATION_DIRECTORY = "$TOOLS/Ubuntu";
=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 = [
'/etc/network/interfaces.20*', # Delete backups VCL makes of /etc/network/interfaces
];
###############################################################################
=head1 OBJECT METHODS
=cut
#//////////////////////////////////////////////////////////////////////////////
=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 $username:$password | chpasswd";
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 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}
|--%{eth0}{default_gateway} '10.10.4.1'
|--%{eth0}{ip_address}
|--{eth0}{ip_address}{10.10.4.3} = '255.255.240.0'
|--{eth0}{name} = 'eth0'
|--{eth0}{physical_address} = '00:50:56:08:00:f8'
=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;
# Delete previously retrieved data if $no_cache was specified
delete $self->{network_configuration} if $no_cache;
# Check if the network configuration has already been retrieved and saved in this object
return $self->{network_configuration} if ($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;
}
# 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
if ($ifconfig_line =~ /^([^\s]+).*Link/) {
$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:]+)/) {
$network_configuration->{$interface_name}{physical_address} = lc($1);
}
# 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;
}
}
# 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})) {
notify($ERRORS{'WARNING'}, 0, "multiple default gateway 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");
}
}
$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 enable_dhcp
Parameters : none
Returns : boolean
Description : Configures /etc/network/interfaces file so that DHCP is enabled
for the public interface.
=cut
sub enable_dhcp {
my $self = shift;
if (ref($self) !~ /VCL::Module/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $computer_name = $self->data->get_computer_short_name();
my $private_interface_name = $self->get_private_interface_name();
my $public_interface_name = $self->get_public_interface_name();
# Get the current interfaces file contents
my $interfaces_file_path = '/etc/network/interfaces';
my @interfaces_lines_original = $self->get_file_contents($interfaces_file_path);
if (!@interfaces_lines_original) {
notify($ERRORS{'WARNING'}, 0, "failed to enable DHCP on $computer_name, contents of $interfaces_file_path could not be retrieved");
return;
}
my $interfaces_contents_original = join("\n", @interfaces_lines_original);
# Make a backup of the file
my $timestamp = POSIX::strftime("%Y-%m-%d_%H-%M-%S", localtime);
$self->copy_file($interfaces_file_path, "$interfaces_file_path.$timestamp");
my @stanza_types = (
'iface',
'mapping',
'auto',
'allow-',
'source',
);
my @interfaces_lines_new;
my $in_iface_stanza = 0;
my $iface_stanza_type;
for my $line (@interfaces_lines_original) {
# Never add hwaddress lines
if ($line =~ /^\s*(hwaddress)/) {
notify($ERRORS{'DEBUG'}, 0, "not including hwaddress line: $line");
next;
}
if ($line =~ /^\s*iface\s+($private_interface_name|$public_interface_name)\s+(\w+)/) {
my $matching_interface_name = $1;
my $address_family = $2;
$in_iface_stanza = 1;
$iface_stanza_type = ($matching_interface_name eq $private_interface_name ? 'private' : 'public');
notify($ERRORS{'DEBUG'}, 0, "found beginning of $iface_stanza_type iface stanza: $line");
push @interfaces_lines_new, "iface $matching_interface_name $address_family dhcp";
}
elsif ($in_iface_stanza) {
my ($stanza_type) = grep { $line =~ /^\s*$_/ } @stanza_types;
if ($stanza_type) {
$in_iface_stanza = 0;
notify($ERRORS{'DEBUG'}, 0, "found end of $iface_stanza_type iface stanza, line begins new stanza: $line");
# Add line which begins next stanza
push @interfaces_lines_new, $line;
}
else {
# Check if line should be added or ignored
if ($line =~ /^\s*(address|netmask|broadcast|gateway|pointopoint)/) {
my $match = $1;
notify($ERRORS{'DEBUG'}, 0, "not including '$match' line from $iface_stanza_type iface stanza: $line");
}
else {
notify($ERRORS{'DEBUG'}, 0, "including line from $iface_stanza_type iface stanza: $line");
push @interfaces_lines_new, $line;
}
}
}
else {
notify($ERRORS{'DEBUG'}, 0, "line is not part of public or private iface stanza: $line");
push @interfaces_lines_new, $line;
}
}
my $interfaces_contents_new = join("\n", @interfaces_lines_new);
# Check if the interfaces content changed, update file if necessary
if ($interfaces_contents_new eq $interfaces_contents_original) {
notify($ERRORS{'OK'}, 0, "update of $interfaces_file_path on $computer_name not necessary, $interfaces_file_path not changed:\n$interfaces_contents_new");
}
elsif ($self->create_text_file($interfaces_file_path, $interfaces_contents_new)) {
notify($ERRORS{'OK'}, 0, "updated $interfaces_file_path to enable public DHCP on $computer_name\n" .
"original:\n$interfaces_contents_original\n" .
"---\n" .
"current:\n$interfaces_contents_new"
);
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to update $interfaces_file_path to enable public DHCP on $computer_name");
return;
}
delete $self->{network_configuration};
notify($ERRORS{'DEBUG'}, 0, "enabled public DHCP on $computer_name");
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=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) !~ /ubuntu/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 $public_ip_configuration = $self->data->get_management_node_public_ip_configuration();
my $public_ip_address = $self->data->get_computer_public_ip_address();
my $public_subnet_mask = $self->data->get_management_node_public_subnet_mask();
my @public_dns_servers = $self->data->get_management_node_public_dns_servers();
my $public_default_gateway = $self->get_correct_default_gateway();
my $server_request_fixed_ip = $self->data->get_server_request_fixed_ip();
if ($server_request_fixed_ip) {
$public_ip_address = $server_request_fixed_ip;
$public_subnet_mask = $self->data->get_server_request_netmask();
$public_default_gateway = $self->data->get_server_request_router();
@public_dns_servers = $self->data->get_server_request_dns_servers();
if (!$public_subnet_mask) {
notify($ERRORS{'WARNING'}, 0, "unable to set static public IP address to $public_ip_address on $computer_name, server request fixed IP is set but server request subnet mask could not be retrieved");
return;
}
elsif (!@public_dns_servers) {
notify($ERRORS{'WARNING'}, 0, "unable to set static public IP address to $public_ip_address on $computer_name, server request fixed IP is set but server request DNS servers could not be retrieved");
return;
}
}
else {
if ($public_ip_configuration !~ /static/i) {
notify($ERRORS{'WARNING'}, 0, "unable to set static public IP address to $public_ip_address on $computer_name, management node's IP configuration is set to $public_ip_configuration");
return;
}
}
# Get 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 to $public_ip_address on $computer_name, failed to determine public interface name");
return;
}
# Stop the interface in case it is already assigned the static IP otherwise ping will respond
$self->stop_network_interface($public_interface_name);
# Attempt to ping the public IP address to make sure it's available
if (_pingnode($public_ip_address)) {
notify($ERRORS{'CRITICAL'}, 0, "failed to set static public IP address to $public_ip_address on $computer_name, IP address is pingable");
return;
}
# Get the current interfaces file contents
my $interfaces_file_path = '/etc/network/interfaces';
my @interfaces_lines_original = $self->get_file_contents($interfaces_file_path);
if (!@interfaces_lines_original) {
notify($ERRORS{'WARNING'}, 0, "failed to set static public IP address to $public_ip_address on $computer_name, $interfaces_file_path contents could not be retrieved");
return;
}
my $interfaces_contents_original = join("\n", @interfaces_lines_original);
notify($ERRORS{'DEBUG'}, 0, "retreived contents of '$interfaces_file_path' from $computer_name:\n$interfaces_contents_original");
# Make a backup of the file
my $timestamp = POSIX::strftime("%Y-%m-%d_%H-%M-%S", localtime);
$self->copy_file($interfaces_file_path, "$interfaces_file_path.$timestamp");
# Examples:
# auto eth0
# iface eth0 inet dhcp
# auto br1
# iface br1 inet dhcp
# bridge_ports eth1
# bridge_stp off
# bridge_fd 0
# iface eth1 inet static
# address 192.168.1.1
# netmask 255.255.255.0
my @stanza_types = (
'iface',
'mapping',
'auto',
'allow-',
'source',
);
my @interfaces_lines_new;
my $in_public_iface_stanza = 0;
my @lines_to_add = (
" address $public_ip_address",
" netmask $public_subnet_mask",
" gateway $public_default_gateway",
);
for my $line (@interfaces_lines_original) {
if ($line =~ /^\s*iface\s+$public_interface_name\s+(\w+)/) {
my $address_family = $1;
$in_public_iface_stanza = 1;
notify($ERRORS{'DEBUG'}, 0, "found beginning of public iface stanza: $line");
push @interfaces_lines_new, "iface $public_interface_name $address_family static";
# Add static IP information
push @interfaces_lines_new, @lines_to_add;
notify($ERRORS{'DEBUG'}, 0, "adding lines:\n" . join("\n", @lines_to_add));
}
elsif ($in_public_iface_stanza) {
my ($stanza_type) = grep { $line =~ /^\s*$_/ } @stanza_types;
if ($stanza_type) {
$in_public_iface_stanza = 0;
notify($ERRORS{'DEBUG'}, 0, "found end of public iface stanza, line begins new stanza: $line");
# Add line which begins next stanza
push @interfaces_lines_new, $line;
}
else {
notify($ERRORS{'DEBUG'}, 0, "line in public iface stanza: $line");
# Check if line should be added or ignored
if ($line =~ /^\s*(bridge|bond|vlan)/) {
my $match = $1;
notify($ERRORS{'DEBUG'}, 0, "including '$match' line from public iface stanza: $line");
push @interfaces_lines_new, $line;
}
else {
notify($ERRORS{'DEBUG'}, 0, "not including line from public iface stanza: $line");
}
}
}
else {
notify($ERRORS{'DEBUG'}, 0, "line is not part of public iface stanza: $line");
push @interfaces_lines_new, $line;
}
}
my $interfaces_contents_new = join("\n", @interfaces_lines_new);
# Check if the interfaces content changed, update file if necessary
if ($interfaces_contents_new eq $interfaces_contents_original) {
notify($ERRORS{'OK'}, 0, "update of $interfaces_file_path on $computer_name not necessary, $interfaces_file_path not changed:\n$interfaces_contents_new");
}
elsif ($self->create_text_file($interfaces_file_path, $interfaces_contents_new)) {
notify($ERRORS{'OK'}, 0, "updated $interfaces_file_path to set static public IP address to $public_ip_address on $computer_name\n" .
"original:\n$interfaces_contents_original\n" .
"---\n" .
"new:\n$interfaces_contents_new"
);
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to update $interfaces_file_path to set static public IP address to $public_ip_address on $computer_name");
return;
}
# Restart the public interface
if (!$self->restart_network_interface($public_interface_name)) {
notify($ERRORS{'WARNING'}, 0, "failed to set static public IP address to $public_ip_address on $computer_name, failed to restart public interface $public_interface_name");
return;
}
# Set the default gateway
if (!$self->set_static_default_gateway()) {
notify($ERRORS{'WARNING'}, 0, "failed to set static public IP address to $public_ip_address on $computer_name, failed to set the default gateway");
return;
}
# Update resolv.conf
if (!$self->update_resolv_conf()) {
notify($ERRORS{'WARNING'}, 0, "failed to set static public IP address to $public_ip_address on $computer_name, failed to update resolv.conf");
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{'DEBUG'}, 0, "set static public IP address to $public_ip_address on $computer_name");
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 activate_interfaces
Parameters :
Returns :
Description :
=cut
sub activate_interfaces {
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=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;
}
# Notes (ARK): Ubuntu 14+ seems to have issues hibernating. The machine's
# console may turn into a black screen with a blinking cursor if the GUI
# isn't running and SSH access may become unavailable. I haven't found a way
# to recover from this when it happens without a hard reset.
my $computer_name = $self->data->get_computer_node_name();
# Make sure pm-hibernate command exists
if (!$self->command_exists('pm-hibernate')) {
if (!$self->install_package('pm-utils')) {
notify($ERRORS{'WARNING'}, 0, "failed to hibernate $computer_name, pm-hibernate command does not exist and pm-utils could not be installed");
return;
}
}
# Ubuntu seems to have problems hibernating if a display manager isn't running
# If it is not running, attempt to install and start lightdm
if (!$self->is_display_manager_running()) {
#if (!$self->install_package('xfce4')) {
# notify($ERRORS{'WARNING'}, 0, "hibernation of $computer_name not attempted, display manager/GUI is not running, failed to install xfce4");
# return;
#}
if (!$self->install_package('lightdm')) {
notify($ERRORS{'WARNING'}, 0, "hibernation of $computer_name not attempted, display manager/GUI is not running, failed to install xfce4");
return;
}
if (!$self->start_service('lightdm')) {
notify($ERRORS{'WARNING'}, 0, "hibernation of $computer_name not attempted, display manager/GUI is not running, failed to start lightdm service");
return;
}
if (!$self->is_display_manager_running()) {
notify($ERRORS{'WARNING'}, 0, "hibernation of $computer_name not attempted, unable to verify display manager/GUI is running, hibernate may fail to shut down the computer unless GUI is running");
return;
}
}
# Delete old log files
$self->delete_file('/var/log/pm-*');
# Try to determine if NetworkManager or network service is being used
my $network_service_name = 'network';
if ($self->service_exists('network-manager')) {
$network_service_name = 'network-manager';
}
my $private_interface_name = $self->get_private_interface_name() || 'eth0';
my $public_interface_name = $self->get_public_interface_name() || 'eth1';
# Some versions of Ubuntu fail to respond after resuming from hibernation
# Networking is up but not responding
# Add script to restart networking service
my $fix_network_script_path = '/etc/pm/sleep.d/50_restart_networking';
my $fix_network_log_path = '/var/log/50_restart_networking.log';
$self->delete_file($fix_network_log_path);
my $fix_network_script_contents = <<"EOF";
#!/bin/sh
echo >> /var/log/50_restart_networking.log
date -R >> /var/log/50_restart_networking.log
echo "\$1: begin" >> /var/log/50_restart_networking.log
case "\$1" in
hibernate)
ifdown $private_interface_name 2>&1 >> /var/log/50_restart_networking.log
ifdown $public_interface_name 2>&1 >> /var/log/50_restart_networking.log
initctl stop $network_service_name 2>&1 >> /var/log/50_restart_networking.log
modprobe -r vmxnet3 2>&1 >> /var/log/50_restart_networking.log
;;
thaw)
modprobe vmxnet3 2>&1 >> /var/log/50_restart_networking.log
initctl restart $network_service_name 2>&1 >> /var/log/50_restart_networking.log
ifup $private_interface_name 2>&1 >> /var/log/50_restart_networking.log
ifup $public_interface_name 2>&1 >> /var/log/50_restart_networking.log
;;
esac
echo "\$1: done" >> $fix_network_log_path
date -R >> /var/log/50_restart_networking.log
EOF
if (!$self->create_text_file($fix_network_script_path, $fix_network_script_contents)) {
notify($ERRORS{'WARNING'}, 0, "hibernate not attempted, failed to create $fix_network_script_path on $computer_name in order to prevent networking problems after computer is powered back on");
return;
}
if (!$self->set_file_permissions($fix_network_script_path, '755')) {
notify($ERRORS{'WARNING'}, 0, "hibernate not attempted, failed to set file permissions on $fix_network_script_path on $computer_name, networking problems may occur after computer is powered back on");
return;
}
# Make sure the grubenv recordfail flag is not set
if (!$self->unset_grubenv_recordfail()) {
notify($ERRORS{'WARNING'}, 0, "hibernate not attempted, failed to unset grubenv recordfail flag, computer may hang on grub boot screen after it is powered back on");
return;
}
my $command = 'pm-hibernate';
#$command .= ' --quirk-dpms-on' if ($computer_name =~ /32$/);
#$command .= ' --quirk-dpms-suspend' if ($computer_name =~ /33$/);
#$command .= ' --quirk-radeon-off' if ($computer_name =~ /34$/);
#$command .= ' --quirk-s3-bios' if ($computer_name =~ /35$/);
#$command .= ' --quirk-s3-mode' if ($computer_name =~ /36$/);
#$command .= ' --quirk-vbe-post' if ($computer_name =~ /37$/);
#$command .= ' --quirk-vbemode-restore' if ($computer_name =~ /38$/);
#$command .= ' --quirk-vbestate-restore' if ($computer_name =~ /39$/);
#$command .= ' --quirk-vga-mode-3' if ($computer_name =~ /40$/);
#$command .= ' --quirk-save-pci' if ($computer_name =~ /41$/);
#$command .= ' --store-quirks-as-lkw' if ($computer_name =~ /42$/);
$command .= ' &';
my ($exit_status, $output) = $self->execute($command);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command to hibernate $computer_name");
return;
}
elsif ($exit_status eq 0) {
notify($ERRORS{'OK'}, 0, "executed command to hibernate $computer_name: $command" . (scalar(@$output) ? "\noutput:\n" . join("\n", @$output) : ''));
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to hibernate $computer_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_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_name never powered off after executing hibernate command: $command");
return;
}
else {
notify($ERRORS{'DEBUG'}, 0, "$computer_name powered off after executing hibernate command");
return 1;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 grubenv_unset_recordfail
Parameters : none
Returns : boolean
Description : Unsets the grub "recordfail" flag. If this is set, the computer
may hang at the grub boot screen when rebooted.
=cut
sub unset_grubenv_recordfail {
my $self = shift;
if (ref($self) !~ /linux/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $computer_name = $self->data->get_computer_node_name();
if (!$self->command_exists('grub-editenv')) {
return 1;
}
my $command = "grub-editenv /boot/grub/grubenv unset recordfail";
my ($exit_status, $output) = $self->execute($command);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command to unset grubenv recordfail on $computer_name");
return;
}
elsif ($exit_status eq 0) {
notify($ERRORS{'OK'}, 0, "unset grubenv recordfail on $computer_name, command: '$command'" . (scalar(@$output) ? "\noutput:\n" . join("\n", @$output) : ''));
return 1;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to unset grubenv recordfail on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 install_package
Parameters : $package_name
Returns : boolean
Description : Installs a Linux package using apt-get.
=cut
sub install_package {
my $self = shift;
if (ref($self) !~ /linux/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my ($package_name) = @_;
if (!$package_name) {
notify($ERRORS{'WARNING'}, 0, "package name argument was not supplied");
return;
}
my $computer_name = $self->data->get_computer_node_name();
# Delete service info in case package adds a service that was previously detected as not existing
$self->_delete_cached_service_info();
# Run apt-get update before installing package - only do this once
$self->apt_get_update();
# Some packages are known to cause debconf database errors
# Check if package being installed will also install/update a package with known problems
# Attempt to fix the debconf database if any are found
my @simulate_lines = $self->simulate_install_package($package_name);
if (@simulate_lines) {
my @problematic_packages = grep { $_ =~ /(dictionaries-common)/; $_ = $1; } @simulate_lines;
if (@problematic_packages) {
@problematic_packages = remove_array_duplicates(@problematic_packages);
notify($ERRORS{'DEBUG'}, 0, "installing $package_name requires the following packages to be installed which are known to have problems with the debconf database, attempting to fix the debconf database first:\n" . join("\n", @problematic_packages));
for my $problematic_package (@problematic_packages) {
$self->fix_debconf_db();
$self->_install_package_helper($problematic_package);
}
$self->fix_debconf_db();
}
}
my $attempt = 0;
my $attempt_limit = 2;
for (my $attempt = 1; $attempt <= $attempt_limit; $attempt++) {
my $attempt_string = ($attempt > 1 ? "attempt $attempt/$attempt_limit: " : '');
if ($self->_install_package_helper($package_name, $attempt_string)) {
return 1;
}
}
notify($ERRORS{'WARNING'}, 0, "failed to install $package_name on $computer_name, made $attempt_limit attempts");
return;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 _install_package_helper
Parameters : $package_name, $attempt_string (optional)
Returns : boolean
Description : Helper subroutine to install_package. Executes command to
installs a Linux package using apt-get.
=cut
sub _install_package_helper {
my $self = shift;
if (ref($self) !~ /linux/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my ($package_name, $attempt_string) = @_;
if (!$package_name) {
notify($ERRORS{'WARNING'}, 0, "package name argument was not supplied");
return;
}
$attempt_string = '' unless defined($attempt_string);
my $computer_name = $self->data->get_computer_node_name();
my $command = "apt-get -qq -y install $package_name";
notify($ERRORS{'DEBUG'}, 0, $attempt_string . "installing package on $computer_name: $package_name");
my ($exit_status, $output) = $self->execute($command, 0, 300);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, $attempt_string . "failed to execute command to install $package_name on $computer_name");
return;
}
elsif ($exit_status eq 0) {
if (grep(/$package_name is already/, @$output)) {
notify($ERRORS{'OK'}, 0, $attempt_string . "$package_name is already installed on $computer_name");
}
else {
notify($ERRORS{'OK'}, 0, $attempt_string . "installed $package_name on $computer_name");
}
return 1;
}
else {
notify($ERRORS{'WARNING'}, 0, $attempt_string . "failed to install $package_name on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
return 0;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 simulate_install_package
Parameters : $package_name
Returns : array
Description : Simulates the installation of a Linux package using apt-get.
Returns the output lines as an array.
=cut
sub simulate_install_package {
my $self = shift;
if (ref($self) !~ /linux/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my ($package_name) = @_;
if (!$package_name) {
notify($ERRORS{'WARNING'}, 0, "package name argument was not supplied");
return;
}
my $computer_name = $self->data->get_computer_node_name();
my $command = "apt-get -s install $package_name";
notify($ERRORS{'DEBUG'}, 0, "attempting to simulate the installation of $package_name on $computer_name");
my ($exit_status, $output) = $self->execute($command, 0, 300);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command to simulate the installation of $package_name on $computer_name");
return;
}
elsif ($exit_status eq 0) {
#notify($ERRORS{'DEBUG'}, 0, "simulated the installation of $package_name on $computer_name, output:\n" . join("\n", @$output));
return @$output;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to simulate the installation of $package_name on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 apt_get_update
Parameters : $force (optional)
Returns : boolean
Description : Runs 'apt-get update' to resynchronize package index files from
their sources. By default, this will only be executed once. The
$force argument will cause apt-get update to be executed even if
it was previously executed.
=cut
sub apt_get_update {
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 ($force) = @_;
return 1 if (!$force && $self->{apt_get_update});
my $computer_name = $self->data->get_computer_node_name();
# Clear out the files under lists to try to avoid these errors:
# W: Failed to fetch http://us.archive.ubuntu.com/ubuntu/dists/trusty-updates/universe/i18n/Translation-en Hash Sum mismatch
# E: Some index files failed to download. They have been ignored, or old ones used instead.
$self->delete_file('/var/lib/apt/lists/*');
notify($ERRORS{'DEBUG'}, 0, "executing 'apt-get update' on $computer_name");
my $command = "apt-get -qq update";
my ($exit_status, $output) = $self->execute($command, 0, 300);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute 'apt-get update' on $computer_name");
return;
}
elsif ($exit_status eq 0) {
notify($ERRORS{'OK'}, 0, "executed 'apt-get update' on $computer_name");
$self->{apt_get_update} = 1;
return 1;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to execute 'apt-get update' on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 fix_debconf_db
Parameters : none
Returns : boolean
Description : Executes /usr/share/debconf/fix_db.pl to attempt to fix problems
installing packages via apt-get.
=cut
sub fix_debconf_db {
my $self = shift;
if (ref($self) !~ /linux/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $computer_name = $self->data->get_computer_node_name();
# Setting up dictionaries-common (1.20.5) ...
# debconf: unable to initialize frontend: Dialog
# debconf: (TERM is not set, so the dialog frontend is not usable.)
# debconf: falling back to frontend: Readline
# debconf: unable to initialize frontend: Readline
# debconf: (This frontend requires a controlling tty.)
# debconf: falling back to frontend: Teletype
# update-default-wordlist: Question empty but elements installed for class "wordlist"
# dictionaries-common/default-wordlist: return code: "0", value: ""
# Choices: , Manual symlink setting
# shared/packages-wordlist: return code: "10" owners/error: "shared/packages-wordlist doesn't exist"
# Installed elements: english (Webster's Second International English wordlist)
# Please see "/usr/share/doc/dictionaries-common/README.problems", section
# "Debconf database corruption" for recovery info.
# update-default-wordlist: Selected wordlist ""
# does not correspond to any installed package in the system
# and no alternative wordlist could be selected.
# dpkg: error processing package dictionaries-common (--configure):
# subprocess installed post-installation script returned error exit status 255
my $command = "/usr/share/debconf/fix_db.pl";
my $attempt = 0;
my $attempt_limit = 5;
while ($attempt < $attempt_limit) {
$attempt++;
my ($exit_status, $output) = $self->execute($command, 0, 60);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command to attempt to fix debconf database on $computer_name: $command");
return;
}
# This command occasionally needs to be run multiple times to fix all problems
# If output contains a line such as the following, run it again:
# debconf: template "base-passwd/user-change-uid" has no owners; removing it.
if ($exit_status == 0) {
my @lines = grep(/^debconf: /, @$output);
my $line_count = scalar(@lines);
if ($line_count) {
notify($ERRORS{'DEBUG'}, 0, "attempt $attempt/$attempt_limit: executed command to fix debconf database on $computer_name, $line_count problems were detected and/or fixed, another attempt will be made");
next;
}
else {
notify($ERRORS{'DEBUG'}, 0, "attempt $attempt/$attempt_limit: no debconf database problems were detected on $computer_name");
return 1;
}
}
else {
notify($ERRORS{'WARNING'}, 0, "attempt $attempt/$attempt_limit: failed to execute command to fix debconf database on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
return;
}
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_product_name
Parameters : none
Returns : string
Description : Retrieves the name of the Ubuntu distribution from
'lsb_release --description'.
=cut
sub get_product_name {
my $self = shift;
if (ref($self) !~ /linux/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
return $self->{product_name} if defined($self->{product_name});
my $computer_name = $self->data->get_computer_short_name();
my $command = 'lsb_release --description';
my ($exit_status, $output) = $self->execute($command);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command to determine Ubuntu distribution name installed on $computer_name: $command");
return;
}
elsif ($exit_status ne '0') {
notify($ERRORS{'WARNING'}, 0, "failed to determine Ubuntu distribution name installed on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
return 0;
}
# Line should be in the form:
# Description: Ubuntu 14.04.2 LTS
my ($product_name_line) = grep(/(Description|Ubuntu)/i, @$output);
if (!$product_name_line) {
notify($ERRORS{'WARNING'}, 0, "unable to determine Ubuntu distribution name installed on $computer_name, output does not contain a line with 'Description' or 'Ubuntu':\n" . join("\n", @$output));
return;
}
# Remove Description: from line
$product_name_line =~ s/.*Description:\s*//g;
$self->{product_name} = $product_name_line;
notify($ERRORS{'OK'}, 0, "determined Ubuntu distribution name installed on $computer_name: '$self->{product_name}'");
return $self->{product_name};
}
#//////////////////////////////////////////////////////////////////////////////
1;
__END__
=head1 SEE ALSO
L<http://cwiki.apache.org/VCL/>
=cut