#!/usr/bin/perl
#
# This is the resource agent used to manage servers on the Anvil! Intelligent Availability platform.
#
# License: GNU General Public License (GPL) v2+
# (c) 1997-2018 - Alteeve's Niche! Inc.
#
# WARNING: This is a pretty purpose-specific resource agent. No effort was made to test this on an rgmanager
# cluster or on any configuration outside how the Anvil! m3 uses it. If you plan to adapt it to
# another purpose, let us know and we'll try to help.
#
# Based on: https://github.com/ClusterLabs/resource-agents/blob/master/doc/dev-guides/ra-dev-guide.asc
#
# Error types from pacemaker's perspective;
#
# - Soft Error - Unless specifically configured otherwise, pacemaker will attempt to recover a resource
# in-place - usually by restarting the resource on the same node.
# - Hard Error - Unless specifically configured otherwise, pacemaker will attempt to recover a resource
# which failed with this error by restarting the resource on a different node.
# - Fatal Error - This is a cluster-wide error, it would make no sense to recover such a resource on a
# different node, let alone in-place. When a resource fails with this error, Pacemaker will
# attempt to shut down the resource, and wait for administrator intervention.
#
# Exit codes;
# 0 - OCF_SUCCESS
# - The action completed successfully. This is the expected return code for any successful start, stop,
# migrate_to, meta_data, help, and usage action.
# - For monitor, however, a modified convention applies:
# - If the server is running we return, OCF_SUCCESS. If not running and gracefully stopped or migrated
# off, return OCF_NOT_RUNNING.
#
# 1 - OCF_ERR_GENERIC
# - The action returned a generic error. This is used only when none of the more specific error codes,
# defined below, accurately describes the problem.
# - Pacemaker interprets this exit code as a soft error.
#
# 2 - OCF_ERR_ARGS
# - The resource’s configuration is not valid on this machine. This can happen if the serve fails to boot
# because of a missing bridge, for example.
#
# 3 - OCF_ERR_UNIMPLEMENTED
# - The resource agent was instructed to execute an action that we do not implement.
# - Not all resource agent actions are mandatory. We don't implement 'promote' or 'demote'. We do implement
# 'migrate_to', 'migrate_from', and 'notify'. If we're misconfigured as a master/slave resource, for
# example, then will alert the user about this misconfiguration by returning OCF_ERR_UNIMPLEMENTED.
#
# 4 - OCF_ERR_PERM
# - The action failed due to insufficient permissions. This may be due to a node not being able to open a
# definition file or resource config.
# - Pacemaker interprets this exit code as a hard error.
#
# 5 - OCF_ERR_INSTALLED
# - The action failed because a required component is missing on the node where the action was executed.
# This may be due to a required binary not being executable, or a the DRBD resource config file not
# existing.
# - Pacemaker interprets this exit code as a hard error.
#
# 6 - OCF_ERR_CONFIGURED
# - The action failed because the user misconfigured the resource in pacemaker. For example, the user may
# have configured an alphanumeric string for a parameter that really should be an integer.
# - Pacemaker interprets this exit code as a fatal error.
#
# 7 - OCF_NOT_RUNNING
# - The resource was found not to be running. This is an exit code that may be returned by the monitor
# action exclusively. Note that this implies that the resource has either gracefully shut down, or has
# never been started.
#
# 8 - OCF_RUNNING_MASTER
# 9 - OCF_FAILED_MASTER
# - These OCF exit codes are not used here.
#
# NOTE: We don't use Anvil::Tools to keep overhead low and to keep this agent independent as possible.
use strict;
use warnings;
use XML::Simple;
use Math::BigInt;
use Data::Dumper;
# Turn off buffering so that the pinwheel will display while waiting for the SSH call(s) to complete.
$| = 1;
my $THIS_FILE = ($0 =~ /^.*\/(.*)$/)[0];
my $running_directory = ($0 =~ /^(.*?)\/$THIS_FILE$/)[0];
if (($running_directory =~ /^\./) && ($ENV{PWD}))
{
$running_directory =~ s/^\./$ENV{PWD}/;
}
my $conf = {
'log' => {
facility => "local0",
level => 2,
line_numbers => 1,
tag => "ocf:alteeve:".$THIS_FILE,
},
# If a program isn't at the defined path, $ENV{PATH} will be searched.
path => {
config => {
definition => "/mnt/anvil/definitions/#!NAME!#.xml",
},
exe => {
brctl => "/usr/sbin/brctl",
cibadmin => "/usr/sbin/cibadmin",
crm_error => "/usr/sbin/crm_error",
drbdadm => "/usr/sbin/drbdadm",
echo => "/usr/bin/echo",
free => "/usr/bin/free",
getent => "/usr/bin/getent",
logger => "/usr/bin/logger",
stonith_admin => "/usr/sbin/stonith_admin",
virsh => "/usr/bin/virsh",
},
},
environment => {
# This is the name of the server we're managing. # Example values:
OCF_RESKEY_name => defined $ENV{OCF_RESKEY_name} ? $ENV{OCF_RESKEY_name} : "", # srv01-c7
# This is our node name
OCF_RESKEY_CRM_meta_on_node => defined $ENV{OCF_RESKEY_CRM_meta_on_node} ? $ENV{OCF_RESKEY_CRM_meta_on_node} : "", # m3-a02n01.alteeve.com
# This says "UUID", but it's the node ID.
OCF_RESKEY_CRM_meta_on_node_uuid => defined $ENV{OCF_RESKEY_CRM_meta_on_node_uuid} ? $ENV{OCF_RESKEY_CRM_meta_on_node_uuid} : "", # 1
# This is the timeout for the called action in millisecond.
OCF_RESKEY_CRM_meta_timeout => defined $ENV{OCF_RESKEY_CRM_meta_timeout} ? $ENV{OCF_RESKEY_CRM_meta_timeout} : "", # 20000
# If this is set, we'll bump our log level as well.
PCMK_debug => defined $ENV{PCMK_debug} ? $ENV{PCMK_debug} : "", # 0
# These are other variables that are set, but we don't currently care about them
OCF_EXIT_REASON_PREFIX => defined $ENV{OCF_EXIT_REASON_PREFIX} ? $ENV{OCF_EXIT_REASON_PREFIX} : "", # ocf-exit-reason:
OCF_RA_VERSION_MAJOR => defined $ENV{OCF_RA_VERSION_MAJOR} ? $ENV{OCF_RA_VERSION_MAJOR} : "", # 1
OCF_RA_VERSION_MINOR => defined $ENV{OCF_RA_VERSION_MINOR} ? $ENV{OCF_RA_VERSION_MINOR} : "", # 0
OCF_RESKEY_crm_feature_set => defined $ENV{OCF_RESKEY_crm_feature_set} ? $ENV{OCF_RESKEY_crm_feature_set} : "", # 3.0.12
OCF_RESOURCE_INSTANCE => defined $ENV{OCF_RESOURCE_INSTANCE} ? $ENV{OCF_RESOURCE_INSTANCE} : "", # srv01-c7
OCF_RESOURCE_PROVIDER => defined $ENV{OCF_RESOURCE_PROVIDER} ? $ENV{OCF_RESOURCE_PROVIDER} : "", # alteeve
OCF_RESOURCE_TYPE => defined $ENV{OCF_RESOURCE_TYPE} ? $ENV{OCF_RESOURCE_TYPE} : "", # server
OCF_ROOT => defined $ENV{OCF_ROOT} ? $ENV{OCF_ROOT} : "", # /usr/lib/ocf
},
};
# If pacemaker is in debug, so are we,
if ($conf->{environment}{PCMK_debug})
{
$conf->{'log'}{level} = 3;
}
# Find executables.
find_executables($conf);
# Get any command line switches.
get_switches($conf);
### TEST: to be removed later
if ($conf->{switches}{test})
{
$conf->{environment}{OCF_RESKEY_name} = "srv01-c7";
}
# Something for the logs
to_log($conf, {message => "ocf:alteeve:server invoked.", 'line' => __LINE__, level => 2});
# This is for debugging.
show_environment($conf, 2);
### What are we being asked to do?
# start - Starts the resource.
# stop - Shuts down the resource.
# monitor - (status aliases here) Queries the resource for its state.
# meta-data - Dumps the resource agent metadata.
# promote - Turns a resource into the Master role (Master/Slave resources only).
# demote - Turns a resource into the Slave role (Master/Slave resources only).
# migrate_to - migration target
# migrate_from - Implement live migration of resources.
# validate-all - Validates a resource’s configuration.
# help - (usage maps here) Displays a usage message when the resource agent is invoked from the command line, rather than by the cluster manager.
# notify - Inform resource about changes in state of other clones.
if ($conf->{switches}{start})
{
# Start the server
start_server($conf);
}
elsif ($conf->{switches}{stop})
{
# Stop the server
stop_server($conf);
}
elsif (($conf->{switches}{monitor}) or ($conf->{switches}{status}))
{
# Report the status of the server.
server_status($conf);
}
elsif (($conf->{switches}{metadaata}) or ($conf->{switches}{'meta-data'}))
{
show_metadata($conf);
}
elsif ($conf->{switches}{promote})
{
# We don't support this, so we return OCF_ERR_UNIMPLEMENTED (3)
to_log($conf, {message => "We were asked to promote: [".$conf->{environment}{OCF_RESKEY_name}."], which makes no sense and is not supported. Ignoreing.", 'line' => __LINE__, level => 0, priority => "err"});
exit(3);
}
elsif ($conf->{switches}{demote})
{
# We don't support this, so we return OCF_ERR_UNIMPLEMENTED (3)
to_log($conf, {message => "We were asked to demote: [".$conf->{environment}{OCF_RESKEY_name}."], which makes no sense and is not supported. Ignoreing.", 'line' => __LINE__, level => 0, priority => "err"});
exit(3);
}
elsif (($conf->{switches}{migrate_to}) && ($conf->{switches}{migrate_from}))
{
# We don't support this, so we return OCF_ERR_UNIMPLEMENTED (3)
migrate_server($conf);
}
elsif ($conf->{switches}{'validate-all'})
{
# Validate our local config and setup.
validate($conf);
exit(0);
}
elsif (($conf->{switches}{help}) && ($conf->{switches}{usage}))
{
# Show the usage information
show_usage($conf);
exit(0);
}
elsif ($conf->{switches}{notify})
{
# We don't implement this
to_log($conf, {message => "We were asked to notify, but this is not a promotable (we're stateless) agent. Ignoring.", 'line' => __LINE__, level => 0, priority => "warn"});
exit(3);
}
else
{
# We were called in some unexpected way. Log an error, show usage and exit.
to_log($conf, {message => "We were invoked with an unexpected (or no) command. Environment variables and arguments below.", 'line' => __LINE__, level => 0, priority => "warn"});
show_environment($conf, 0);
exit(1);
}
# If we hit here, something very wrong happened.
exit(255);
#############################################################################################################
# Functions #
#############################################################################################################
# This boots the server if possible.
sub start_server
{
my ($conf) = @_;
# Start procedure;
# 1. Read the XML definition file and find the backing storage and bridges. Soft error if read fails.
# 2. Make sure the name matches.
# 3. Make sure we have enough free RAM.
# 4. Make sure the emulator exists (can be an issue after migrating from an different gen Anvil!).
# 5.1. Make sure optical drives with mounted data have the disk present. Soft error if not.
# 5.2. Find any backing DRBD devices
# 6. For each DRBD device;
# 6.1. Make sure the backing LV is ACTIVE. Soft error if not.
# 6.2. Check if the drbd resource is up. If not, up it.
# 6.3. Make sure the backing disk is UpToDate. Soft error if not.
# 6.4. Make sure the backing device is 'Connected' or 'Connecting'. Call a connect if not.
# 7. Make sure all bridges exist and soft error if not.
# 8. Start the server.
to_log($conf, {message => "We've been asked to start the server: [".$conf->{environment}{OCF_RESKEY_name}."]..", 'line' => __LINE__, level => 2});
validate_all($conf);
exit(0);
}
# This shuts down the server if possible.
sub stop_server
{
my ($conf) = @_;
exit(0);
}
# This checks the status of the server.
sub server_status
{
my ($conf) = @_;
# If the named server is running, return OCF_SUCCESS (0), otherwise OCF_NOT_RUNNING (7). If the
# server is failed, return OCF_ERR_GENERIC (1).
my $state = "";
my $server = $conf->{environment}{OCF_RESKEY_name};
### NOTE: When pacemaker is first starting, virsh won't be up right away. So if we get a return code
### of '1', we'll try again up to 50% of 'environment::OCF_RESKEY_CRM_meta_timeout'.
if (not $conf->{environment}{OCF_RESKEY_CRM_meta_timeout})
{
# Set a sane default of 20 seconds.
$conf->{environment}{OCF_RESKEY_CRM_meta_timeout} = 20000;
to_log($conf, {message => "The environment variable 'OCF_RESKEY_CRM_meta_timeout' was not set, so setting it to: [".$conf->{environment}{OCF_RESKEY_CRM_meta_timeout}."].", 'line' => __LINE__, level => 1, priority => "warn"});
}
my $return_code = undef;
my $output = "";
my $current_time = time;
my $timeout = $current_time + int(($conf->{environment}{OCF_RESKEY_CRM_meta_timeout} /= 1000) / 2);
my $waiting = 1;
to_log($conf, {message => "current_time: [$current_time], timeout: [$timeout].", 'line' => __LINE__, level => 2});
while($waiting)
{
# Make the call
($return_code, $output) = shell_call($conf, $conf->{path}{exe}{virsh}." list");
to_log($conf, {message => "return_code: [$return_code].", 'line' => __LINE__, level => 2});
if (not $return_code)
{
$waiting = 0;
to_log($conf, {message => "waiting: [$waiting].", 'line' => __LINE__, level => 2});
}
elsif (time > $timeout)
{
# We've waited long enough.
$waiting = 0;
to_log($conf, {message => "The 'virsh' call exited with the return code: [$return_code]. The 'libvirtd' may have failed to start. We won't wait any longer.", 'line' => __LINE__, level => 1, priority => "warn"});
}
else
{
to_log($conf, {message => "The 'virsh' call exited with the return code: [$return_code]. The 'libvirtd' service might be starting, so we will check again shortly.", 'line' => __LINE__, level => 2});
sleep 2;
}
}
# If I got a non-zero return code, something went wrong with the virsh call.
if ($return_code)
{
to_log($conf, {message => "It would appear that libvirtd is not operating (or not operating correctly). Expected the return code '0' but got: [$return_code].", 'line' => __LINE__, level => 0, priority => "err"});
if ($output)
{
to_log($conf, {message => "Output of: [".$conf->{path}{exe}{virsh}." list] follows;", 'line' => __LINE__, level => 0, priority => "err"});
to_log($conf, {message => "Output: [$output]", 'line' => __LINE__, level => 0, priority => "err"});
}
exit(1);
}
# If we're still alive, process the output
foreach my $line (split/\n/, $output)
{
$line =~ s/^\s+//;
$line =~ s/\s+$//;
$line =~ s/\s+/ /g;
if ($line =~ /^(\d+) $server (.*)$/)
{
$state = $2;
to_log($conf, {message => "server: [$server], state: [$state]", 'line' => __LINE__, level => 2});
last;
}
}
# If there is a state, see what the state is.
if ($state)
{
### What is the state?
## States we return OCF_SUCCESS (0).
# running - The domain is currently running on a CPU
# paused - The domain has been paused, usually occurring through the administrator
# running virsh suspend. When in a paused state the domain will still consume
# allocated resources like memory, but will not be eligible for scheduling by
# the hypervisor.
# pmsuspended - The domain has been suspended by guest power management, e.g. entered into s3
# state.
# in shutdown - The domain is in the process of shutting down, i.e. the guest operating
# system has been notified and should be in the process of stopping its
# operations gracefully.
## States we'll return OCF_NOT_RUNNING (7).
# shut off - The domain is not running. Usually this indicates the domain has been shut
# down completely, or has not been started.
## States we'll return OCF_ERR_GENERIC (1).
# idle - The domain is idle, and not running or runnable. This can be caused because
# the domain is waiting on IO (a traditional wait state) or has gone to sleep
# because there was nothing else for it to do.
# crashed - The domain has crashed, which is always a violent ending. Usually this state
# can only occur if the domain has been configured not to restart on crash.
if (($state eq "running") or ($state eq "paused") or ($state eq "pmsuspended") or ($state eq "in shutdown"))
{
to_log($conf, {message => "The server: [$server] is: [$state], which is OK.", 'line' => __LINE__, level => 1});
exit(0);
}
elsif ($state eq "shut off")
{
to_log($conf, {message => "The server: [$server] is: [$state].", 'line' => __LINE__, level => 1});
exit(7);
}
elsif (($state eq "idle") or ($state eq "crashed"))
{
to_log($conf, {message => "The server: [$server] is in a bad state: [$state]!", 'line' => __LINE__, level => 0, priority => "err"});
exit(1);
}
else
{
# WTF?
to_log($conf, {message => "The server: [$server] is in an unexpected state: [$state]!", 'line' => __LINE__, level => 0, priority => "err"});
exit(1);
}
}
else
{
# Not running. Exit with OCF_NOT_RUNNING
to_log($conf, {message => "The server: [$server] is not running on this node.", 'line' => __LINE__, level => 1});
exit(7);
}
exit(0);
}
# Migrate the server
sub migrate_server
{
my ($conf) = @_;
# If we were given 'migrate_to', then just verify that the node name makes sense. If we were given
# 'migrate_from', we need to find the peer.
# Return failed until this is actually implemented.
exit(1);
}
# Validation checks that we have the definition XML, resource config and that needed apps are installed.
sub validate_all
{
my ($conf) = @_;
to_log($conf, {message => "Running validation tests...", 'line' => __LINE__, level => 2});
# Read in the server's definition file (if found and readable).
read_server_definition($conf);
to_log($conf, {message => "- Server definition was read.", 'line' => __LINE__, level => 2});
# Does the internal server name match?
validate_name($conf);
to_log($conf, {message => "- Server name is valid.", 'line' => __LINE__, level => 2});
# Make sure the emulator it wants is the one we have.
validate_emulator($conf);
to_log($conf, {message => "- Eumlator is valid.", 'line' => __LINE__, level => 2});
# These tests are only needed if we're about to boot the server
if ($conf->{switches}{start})
{
# Check that we have enough RAM.
validate_ram($conf);
to_log($conf, {message => "- Sufficient RAM is available.", 'line' => __LINE__, level => 2});
}
# Validate storage (Disks and optical media)
validate_storage($conf);
to_log($conf, {message => "- Storage is valid and ready.", 'line' => __LINE__, level => 2});
# Validate bridges
validate_bridges($conf);
to_log($conf, {message => "- Network bridge(s) are available.", 'line' => __LINE__, level => 2});
exit(0);
}
# This ensures that the bridges the server connects to exist on this node.
sub validate_bridges
{
my ($conf) = @_;
# Find the Optical drives and DRBD devices.
foreach my $device_ref (@{$conf->{server}{definition_xml}->{devices}})
{
foreach my $interface_ref (@{$device_ref->{interface}})
{
foreach my $source_ref (@{$interface_ref->{source}})
{
my $bridge = $source_ref->{bridge};
$conf->{server}{bridges}{$bridge} = 1;
to_log($conf, {message => "server::bridges::${bridge}: [".$conf->{server}{bridges}{$bridge}."].", 'line' => __LINE__, level => 3});
}
}
}
# Get a list of available bridges.
my ($return_code, $output) = shell_call($conf, $conf->{path}{exe}{brctl}." show");
my $first_line = 1;
foreach my $line (split/\n/, $output)
{
to_log($conf, {message => "line: [$line].", 'line' => __LINE__, level => 3});
if ($first_line)
{
# We skip the first line instead of parse the string to avoid getting caught by
# translations.
$first_line = 0;
}
elsif ($line =~ /^(\S+)\s/)
{
my $bridge = $1;
$conf->{'local'}{bridge}{$bridge} = 1;
to_log($conf, {message => "local::bridge::${bridge}: [$bridge].", 'line' => __LINE__, level => 3});
}
}
# Verify bridges now
foreach my $bridge (sort {$a cmp $b} keys %{$conf->{server}{bridges}})
{
if ($conf->{'local'}{bridge}{$bridge})
{
to_log($conf, {message => "The bridge: [$bridge] is available for this server.", 'line' => __LINE__, level => 2});
}
else
{
# Missing bridge.
to_log($conf, {message => "The server wants to connect to the bridge: [$bridge] which we do not have on this node.", 'line' => __LINE__, level => 0, priority => "err"});
exit(5);
}
}
return(0);
}
# This looks up the disks and optical media connected to this server.
sub validate_storage
{
my ($conf) = @_;
# Find the bridge(s) this server uses.
foreach my $device_ref (@{$conf->{server}{definition_xml}->{devices}})
{
foreach my $disk_ref (@{$device_ref->{disk}})
{
my $type = $disk_ref->{device};
to_log($conf, {message => "type: [$type].", 'line' => __LINE__, level => 2});
if ($type eq "disk")
{
foreach my $source_ref (@{$disk_ref->{source}})
{
my $disk = $source_ref->{dev};
$conf->{server}{disks}{$disk} = 1;
to_log($conf, {message => "server::disks::${disk}: [".$conf->{server}{disks}{$disk}."].", 'line' => __LINE__, level => 2});
}
}
elsif ($type eq "cdrom")
{
foreach my $source_ref (@{$disk_ref->{source}})
{
my $file = $source_ref->{file};
$conf->{server}{optical}{$file} = 1;
to_log($conf, {message => "server::optical::${file}: [".$conf->{server}{optical}{$file}."].", 'line' => __LINE__, level => 2});
}
}
}
}
# Verify DRBD devices now
foreach my $disk (sort {$a cmp $b} keys %{$conf->{server}{disks}})
{
to_log($conf, {message => "Checking that the DRBD device: [$disk] is ready.", 'line' => __LINE__, level => 2});
}
# Verify optical disks now
foreach my $file (sort {$a cmp $b} keys %{$conf->{server}{optical}})
{
to_log($conf, {message => "Checking that the optical disc image: [$file] exists.", 'line' => __LINE__, level => 2});
# If the file doesn't exist, exit with OCF_ERR_INSTALLED (5). If we can't read it, exit with
# OCF_ERR_PERM (4).
if (not -e $file)
{
}
elsif (not -r $file)
{
}
else
{
# We're OK.
}
}
return(0);
}
# This verifies that the requested emulator exists and can be used.
sub validate_emulator
{
my ($conf) = @_;
# What emulator is this using?
my $emulator = $conf->{server}{definition_xml}->{devices}->[0]->{emulator}->[0];
to_log($conf, {message => "emulator: [$emulator]", 'line' => __LINE__, level => 2});
if (not -e $emulator)
{
# It doesn't exist. Exit with OCF_ERR_INSTALLED (5).
to_log($conf, {message => "The server wants to use the emulator: [$emulator] which doesn't exist on this node. Was this server migrated from a different generation Anvil! system? Please update '...' in the server's definition file: [".$conf->{server}{definition_file}."].", 'line' => __LINE__, level => 0, priority => "err"});
exit(5);
}
if (not -x $emulator)
{
# We can't execute it. Exit with OCF_ERR_PERM (4).
to_log($conf, {message => "The server wants to use the emulator: [$emulator] which exists, but we can't run. Please check permissions and for SELinux denials.", 'line' => __LINE__, level => 0, priority => "err"});
exit(4);
}
return(0);
}
# This makes sure the name we see in the definition file matches what we expect.
sub validate_name
{
my ($conf) = @_;
my $server = $conf->{environment}{OCF_RESKEY_name};
if ($server ne $conf->{server}{definition_xml}->{name}->[0])
{
to_log($conf, {message => "The configured server name: [$server] does not match the name of the server in the definition file: [".$conf->{server}{definition_xml}->{name}."]!", 'line' => __LINE__, level => 0, priority => "err"});
exit(1);
}
return(0);
}
# This checks that there is enough RAM to run this server.
sub validate_ram
{
my ($conf) = @_;
# How mcuh RAM does the server need?
my $server_ram_value = $conf->{server}{definition_xml}->{memory}->[0]->{content};
my $server_ram_units = $conf->{server}{definition_xml}->{memory}->[0]->{unit};
to_log($conf, {message => "server_ram_value: [$server_ram_value], server_ram_units: [$server_ram_units].", 'line' => __LINE__, level => 2});
# Convert to bytes
my $server_ram_bytes = $server_ram_value;
if ($server_ram_units =~ /^k/i) { $server_ram_bytes = ($server_ram_value * (2 ** 10)); }
elsif ($server_ram_units =~ /^m/i) { $server_ram_bytes = ($server_ram_value * (2 ** 20)); }
elsif ($server_ram_units =~ /^g/i) { $server_ram_bytes = ($server_ram_value * (2 ** 30)); }
elsif ($server_ram_units =~ /^t/i) { $server_ram_bytes = ($server_ram_value * (2 ** 40)); }
elsif ($server_ram_units =~ /^p/i) { $server_ram_bytes = Math::BigInt->new('2')->bpow('50')->bmul($server_ram_value); }
elsif ($server_ram_units =~ /^e/i) { $server_ram_bytes = Math::BigInt->new('2')->bpow('60')->bmul($server_ram_value); }
elsif ($server_ram_units =~ /^z/i) { $server_ram_bytes = Math::BigInt->new('2')->bpow('70')->bmul($server_ram_value); }
elsif ($server_ram_units =~ /^y/i) { $server_ram_bytes = Math::BigInt->new('2')->bpow('80')->bmul($server_ram_value); }
to_log($conf, {message => "server_ram_bytes: [$server_ram_bytes].", 'line' => __LINE__, level => 3});
# How much RAM do we have available?
my $available = 0;
my ($free_rc, $free_output) = shell_call($conf, $conf->{path}{exe}{free}." --bytes");
foreach my $line (split/\n/, $free_output)
{
to_log($conf, {message => "line: [$line].", 'line' => __LINE__, level => 3});
if ($line =~ /Mem:\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$/)
{
my $total = $1;
my $used = $2;
my $free = $3;
my $shared = $4;
my $cache = $5;
$available = $6;
to_log($conf, {message => "total: [$total], used: [$used], free: [$free], shared: [$shared], cache: [$cache], available: [$available]", 'line' => __LINE__, level => 3});
}
}
to_log($conf, {message => "server_ram_bytes: [".comma($conf, $server_ram_bytes)." bytes].", 'line' => __LINE__, level => 2});
to_log($conf, {message => "available: ...... [".comma($conf, $available)." bytes].", 'line' => __LINE__, level => 2});
if ($server_ram_bytes > $available)
{
# Not enough free memory.
to_log($conf, {message => "The configured server name: [".$conf->{environment}{OCF_RESKEY_name}."] needs: [".comma($conf, $server_ram_bytes)." bytes] of RAM, but only: [".comma($conf, $available)." bytes] are available!", 'line' => __LINE__, level => 0, priority => "err"});
exit(1);
}
return(0);
}
# This reads the XML definition data into an XML data hash.
sub read_server_definition
{
my ($conf) = @_;
my $server = $conf->{environment}{OCF_RESKEY_name};
my $definition_file = $conf->{path}{config}{definition};
$definition_file =~ s/#!NAME!#/$server/;
my $server_xml = "";
to_log($conf, {message => "server: [$server], definition_file: [$definition_file]", 'line' => __LINE__, level => 3});
# If the file doesn't exist, return OCF_ERR_INSTALLED (5). If the file exists but we can't read it,
# return OCF_ERR_PERM (4).
if (not -e $definition_file)
{
to_log($conf, {message => "The definition file: [$definition_file] for the server: [$server] does not exist here!", 'line' => __LINE__, level => 0, priority => "err"});
exit(5);
}
elsif (not -r $definition_file)
{
to_log($conf, {message => "The definition file: [$definition_file] for the server: [$server] can not be read!", 'line' => __LINE__, level => 0, priority => "err"});
exit(4);
}
# Still alive? Read it in.
my ($definition_xml) = read_file($conf, $definition_file);
to_log($conf, {message => "definition_xml: [$definition_xml]", 'line' => __LINE__, level => 3});
my $xml = XML::Simple->new();
eval { $server_xml = $xml->XMLin($definition_xml, KeyAttr => {}, ForceArray => 1) };
if ($@)
{
chomp $@;
my $error = "[ Error ] - The was a problem parsing: [$definition_file]. The error was:\n";
$error .= "===========================================================\n";
$error .= $@."\n";
$error .= "===========================================================\n";
to_log($conf, {message => $error, 'line' => __LINE__, level => 0, priority => "err"});
exit(1);
}
$conf->{server}{definition_xml} = $server_xml;
$conf->{server}{definition_file} = $definition_file;
return(0);
}
# This reads in a file and returns the contents as a single string variable.
sub read_file
{
my ($conf, $file) = @_;
my $body = "";
open (my $file_handle, "<".$file) or to_log($conf, {message => "Failed to read: [$file]. The error was: $!", 'line' => __LINE__, level => 0, priority => "err", exit_code => 1});
while(<$file_handle>)
{
# This should not generate output.
chomp;
my $line = $_;
to_log($conf, {message => "Output: [$line]", 'line' => __LINE__, level => 3});
$body .= $line."\n";
}
close $file_handle;
to_log($conf, {message => "body: [$body]", 'line' => __LINE__, level => 3});
return($body);
}
# This makes a system call and returns the return code and the output as string variable.
sub shell_call
{
my ($conf, $shell_call) = @_;
$shell_call .= " 2>&1; ".$conf->{path}{exe}{echo}." return_code:\$?";
my $return_code = 9999;
my $output = "";
to_log($conf, {message => "Calling: [$shell_call]", 'line' => __LINE__, level => 2});
open (my $file_handle, $shell_call." 2>&1 |") or to_log($conf, {message => "Failed to call: [".$shell_call."]. The error was: $!", 'line' => __LINE__, level => 0, priority => "err", exit_code => 1});
while(<$file_handle>)
{
# This should not generate output.
chomp;
my $line = $_;
to_log($conf, {message => "line: [$line]", 'line' => __LINE__, level => 3});
if ($line =~ /^return_code:(\d+)$/)
{
$return_code = $1;
to_log($conf, {message => "return_code: [$return_code]", 'line' => __LINE__, level => 3});
next;
}
$output .= $line."\n";
to_log($conf, {message => "Output: [$line]", 'line' => __LINE__, level => 3});
}
close $file_handle;
to_log($conf, {message => "return_code: [$return_code], output: [$output]", 'line' => __LINE__, level => 3});
return($return_code, $output);
}
# This logs the details of this call.
sub show_environment
{
my ($conf, $level) = @_;
foreach my $key (sort {$a cmp $b} keys %{$conf->{switches}})
{
next if $key eq "raw";
next if $conf->{switches}{$key} eq "";
to_log($conf, {message => "Command line switch: [$key] -> [".$conf->{switches}{$key}."]", 'line' => __LINE__, level => $level});
}
foreach my $key (sort {$a cmp $b} keys %{$conf->{environment}})
{
next if $conf->{environment}{$key} eq "";
to_log($conf, {message => "OCF Environment variable: [$key] -> [".$conf->{environment}{$key}."]", 'line' => __LINE__, level => $level});
}
foreach my $key (sort {$a cmp $b} keys %ENV)
{
next if exists $conf->{environment}{$key};
to_log($conf, {message => "System Environment variable: [$key] -> [".$ENV{$key}."]", 'line' => __LINE__, level => ($level + 1)});
}
return(0);
}
# This just prints a quick usage message for now.
sub show_usage
{
my ($conf) = @_;
print "TODO: How to use this...\n";
exit(0);
}
# This prints out the metadata and exits.
sub show_metadata
{
my ($conf) = @_;
# This is a pretty simple agent, by design. We only take a server name for now.
print '
0.1
This resource agent manages KVM+qemu virtual servers on an Anvil! m3 Intelligent Availability(tm) system.
It manages underlying components like DRBD 9 storage resources, brodge connections and so forth.
Anvil! m3 server resource agent
This is the name of the server as reported by virsh.
Server name
';
exit(0);
}
# This gathers command line switches and stores them in 'swithes::'.
sub get_switches
{
my ($conf) = @_;
my $last_argument = "";
$conf->{switches}{raw} = "";
foreach my $argument (@ARGV)
{
to_log($conf, {message => "argument: [$argument]", 'line' => __LINE__, level => 3});
if ($last_argument eq "raw")
{
# Don't process anything.
$conf->{switches}{raw} .= " ".$argument;
to_log($conf, {message => "switches::raw: [".$conf->{switches}{raw}."]", 'line' => __LINE__, level => 3});
}
elsif ($argument =~ /^-/)
{
# If the argument is just '--', appeand everything after it to 'raw'.
if ($argument eq "--")
{
$last_argument = "raw";
$conf->{switches}{raw} = "";
to_log($conf, {message => "switches::raw: [".$conf->{switches}{raw}."]", 'line' => __LINE__, level => 3});
}
else
{
($last_argument) = ($argument =~ /^-{1,2}(.*)/)[0];
to_log($conf, {message => "last_argument: [$last_argument]", 'line' => __LINE__, level => 3});
if ($last_argument =~ /=/)
{
# Break up the variable/value.
($last_argument, my $value) = (split /=/, $last_argument, 2);
$conf->{switches}{$last_argument} = $value;
to_log($conf, {message => "switches::${last_argument}: [".$conf->{switches}{$last_argument}."]", 'line' => __LINE__, level => 3});
}
else
{
$conf->{switches}{$last_argument} = "#!SET!#";
to_log($conf, {message => "switches::${last_argument}: [".$conf->{switches}{$last_argument}."]", 'line' => __LINE__, level => 3});
}
}
}
else
{
if ($last_argument)
{
$conf->{switches}{$last_argument} = $argument;
to_log($conf, {message => "switches::${last_argument}: [".$conf->{switches}{$last_argument}."]", 'line' => __LINE__, level => 3});
$last_argument = "";
to_log($conf, {message => "last_argument: [$last_argument]", 'line' => __LINE__, level => 3});
}
else
{
# Got a value without an argument. That's OK.
$conf->{switches}{$argument} = "#!SET!#";
to_log($conf, {message => "switches::${argument}: [".$conf->{switches}{$argument}."]", 'line' => __LINE__, level => 3});
}
}
}
# Clean up the initial space added to 'raw'.
to_log($conf, {message => "switches::raw: [".$conf->{switches}{raw}."]", 'line' => __LINE__, level => 3});
if ($conf->{switches}{raw})
{
$conf->{switches}{raw} =~ s/^ //;
to_log($conf, {message => "switches::raw: [".$conf->{switches}{raw}."]", 'line' => __LINE__, level => 3});
}
return(0);
}
# This adds commas to long numbers.
sub comma
{
my ($conf, $number) = @_;
return undef if not defined $number;
# Strip out any existing commas.
$number =~ s/,//g;
# Record and remove the sign, if present.
my $sign = "";
if ($number =~ /^\+/)
{
$number =~ s/^\+//g;
$sign = "+";
}
elsif ($number =~ /^\-/)
{
$number =~ s/^\-//g;
$sign = "-";
}
# Split on the left-most period.
my ($whole, $decimal) = split/\./, $number, 2;
$whole = "" if not defined $whole;
$decimal = "" if not defined $decimal;
# Now die if either number has a non-digit character in it.
if (($whole =~ /\D/) or ($decimal =~ /\D/))
{
to_log($conf, {message => "We were asked to insert commas into a dumber that is not actually a number: [$number]. This is likely a symptom of a larger problem.", 'line' => __LINE__, level => 0, priority => "err"});
exit(1);
}
local($_) = $whole ? $whole : "";
1 while s/^(-?\d+)(\d{3})/$1,$2/;
$whole = $_;
my $return = $decimal ? $whole.".".$decimal : $whole;
if ($sign)
{
$return = $sign.$return;
}
return ($return);
}
# Log file entries
sub to_log
{
my ($conf, $parameters) = @_;
my $facility = defined $parameters->{facility} ? $parameters->{facility} : $conf->{'log'}{facility};
my $level = defined $parameters->{level} ? $parameters->{level} : 1;
my $line = defined $parameters->{'line'} ? $parameters->{'line'} : 0;
my $message = defined $parameters->{message} ? $parameters->{message} : "";
my $priority = defined $parameters->{priority} ? $parameters->{priority} : "";
my $exit_code = defined $parameters->{exit_code} ? $parameters->{exit_code} : "";
# Leave if we don't care about this message
return if $level > $conf->{'log'}{level};
return if not $message;
# Build the message. We log the line
if (($conf->{'log'}{line_numbers}) && ($line))
{
$message = $line."; ".$message;
}
my $priority_string = $facility;
if ($priority)
{
$priority_string .= ".".$priority;
}
elsif ($level eq "0")
{
$priority_string .= ".notice";
}
elsif (($level eq "1") or ($level eq "2"))
{
$priority_string .= ".info";
}
else
{
$priority_string .= ".debug";
}
# Clean up the string for bash
$message =~ s/"/\\\"/gs;
#$message =~ s/\(/\\\(/gs;
my $shell_call = $conf->{path}{exe}{logger}." --priority ".$priority_string." --tag ".$conf->{'log'}{tag}." -- \"".$message."\"";
open (my $file_handle, $shell_call." 2>&1 |") or die "Failed to call: [".$shell_call."]. The error was: $!\n";
while(<$file_handle>)
{
# This should not generate output.
chomp;
my $line = $_;
print "Unexpected logging output: [".$line."]\n";
}
close $file_handle;
if ($exit_code =~ /^\d+$/)
{
exit($exit_code);
}
return(0);
}
# This checks the given paths and, if something isn't found, it searches PATH trying to find it.
sub find_executables
{
my ($conf) = @_;
# Variables.
my $check = "";
my $bad = 0;
# Log entries can only happen if I've found 'logger', so an extra check will be made on 'to_log'
# calls.
my @dirs = split/:/, $ENV{PATH};
foreach my $exe (sort {$b cmp $a} keys %{$conf->{path}{exe}})
{
if ( not -e $conf->{path}{exe}{$exe} )
{
to_log($conf, {message => "The program: [$exe] is not at: [".$conf->{path}{exe}{$exe}."]. Looking for it now...", 'line' => __LINE__, level => 1});
foreach my $path (@dirs)
{
$check = "$path/$exe";
$check =~ s/\/\//\//g;
to_log($conf, {message => "Checking: [$check]", 'line' => __LINE__, level => 2});
if ( -e $check )
{
if (-e $conf->{path}{exe}{logger})
{
to_log($conf, {message => "Found it! Changed path for: [$exe] from: [".$conf->{path}{exe}{$exe}."] to: [$check]", 'line' => __LINE__, level => 1});
}
else
{
warn "DEBUG: Found it! Changed path for: [$exe] from: [".$conf->{path}{exe}{$exe}."] to: [$check]\n";
}
$conf->{path}{exe}{$exe} = $check;
}
else
{
to_log($conf, {message => "Not found.", 'line' => __LINE__, level => 2});
}
}
}
else
{
to_log($conf, {message => "Found!", 'line' => __LINE__, level => 3});
next;
}
# Make sure it exists now.
to_log($conf, {message => "Checking again if: [$exe] is at: [".$conf->{path}{exe}{$exe}."].", 'line' => __LINE__, level => 3});
if (not -e $conf->{path}{exe}{$exe})
{
$bad = 1;
if (-e $conf->{path}{exe}{logger})
{
to_log($conf, {message => "Failed to find executable: [$exe]. Unable to proceed.", 'line' => __LINE__, level => 0});
}
else
{
warn "Failed to find executable: [$exe]. Unable to proceed.\n";
}
}
}
if ($bad)
{
exit(1);
}
return(0);
}