#!/usr/bin/perl -w
#
# Copyright 2003 Sun Microsystems, Inc.  All rights reserved.
# Use is subject to license terms.
#
#ident   "@(#)rgm_check_ccr.pl 1.2     03/12/16 SMI"
#

#
# The purpose of the script is to test if the RGM CCR tables are already
# committed to run at RGM level 2, and to detect whether is it possible to
# back-out a patch.
#
# This script checks for inter-rg dependencies, restart dependencies,
# RG affinites and new values for Failover_mode in the RGM CCR files.
#
# If it finds any of the above, it prints an error message and exits non-zero.
# Otherwise it silently exits 0.
#
# An inter-rg dependency is defined as a dependee resource listed in any of the
# resource_dependency, resource_dependency_weak, or
# resource_dependency_restart properties that does not exist in the RG
# table containing the dependent resource.
#
# New values for Failover_mode are RESTART_ONLY and LOG_ONLY.
#
# The basic alogrithm of the check follows:
# * Initialize hashtables 1, 2, and 3.
# * Initialize array illegal_R_fom (failover_mode)
# * Initialize array illegal_RT_fom (failover_mode)
# * Open the CCR directory file.
# * foreach entry in the directory
#    * If an entry in the directory is an RG table
#	* Open the RG table file
#	* Initialize hashtable 4.
#	* foreach entry in the RG table
#	    * If the entry is a resource
#		* Parse the entry to retrieve the res name and the
#			list of dependencies.
#		* Store the name/dependencies key/value pair in hashtable 4.
#		* If the resource has restart dependencies
#		    * Store the rname/dependencies key/value pair in
#			hashtable 2.
#		* If the resource has a new value for its Failover_mode
#		    * Store the resource name in array illegal_R_fom
#	    * If the entry is an RG_affinities entry, and there are affs
#			listed
#		Store the rgname/affinities key/value pair in hashtable 3.
#	* foreach name/dependencies pair in hashtable 4
#	    * foreach dependency in the list
#		* If the dependeny (resource name) is in hashtable 4
#		   * It's not an inter-rg dependency.  No problem.
#		* else
#		   * It's an inter-rg dependency.  Save the offending
#			dependency in hashtable 1 and continue processing.
#    * If an entry in the directory is a RT table
#	* Open the RT table file
#	* for each entry in the RT table
#		* if the entry is the Failover_mode property
#			* if the default value for the RT is one of the new
#			  value
#				store the name of the RT to the list
#				of illegal RT
# * If (any entries in hashtable 1)
#	* Found inter-rg dependencies.
#	* Print offenders.
#	* ret_val = 1
# * If (any entries in hashtable 2)
#	* Found restart dependencies.
#	* Print offenders.
#	* ret_val = 1
# * If (any entries in hashtable 3)
#	* Found rg affinities.
#	* Print offenders.
#	* ret_val = 1
# * If (any entries in illegal_fom array)
#	* Found at least one resource with illegal Failover_mode prop value.
#	* Print offenders.
#	* ret_val = 1
# * If (any entries in the illegal rt array)
#	* Found at least one resource type with illegal Failover_mode
#	  default value.
#	* Print offenders.
#	* ret_val = 1
# If any offenders were found, exit 1.
# Else exit 0.
#
#
# This script makes the following assumptions:
# * The CCR directory and the directory file are as defined below in the
# global constants section.
# * A file in the CCR directory is an RG table iff it has an entry in
# the directory file with the prefix defined below.
# * An entry in an RG table represents a resource iff its name begins
# with the prefix defined below.
# * A resource dependencies are listed in the following properties:
#	Resource_dependencies
#	Resource_dependencies_weak
#	Resource_dependencies_restart
# * Each property of a resource is of the form <name>=<value>, where the
# value list is comma-separated, and ends with a ;.
# * An entry in an RG table represents RG affinities iff the property name is
# "RG_Affinities".
#

use strict;
use Sun::Solaris::Utils qw(gettext);

#
# Initialize the global constants
#
my $CCR_PATH = "/etc/cluster/ccr/";
my $CCR_DIR = "${CCR_PATH}directory";
my $RG_PREFIX = "rgm_rg_";
my $RT_PREFIX = "rgm_rt_";
my $R_PREFIX = "RS_";
my $PROP_PREFIX = "p.";

#
# subroutine prototypes
#
sub dbg_msg_out($);
sub check_ccr();
sub check_rg_file($$\%\%\%\@);
sub check_rt_file($$\@);

#
# List all our error strings here.  Wrap them with a call to gettext for
# proper localization.
#
my $INTER_RG_ERR = gettext("\nFound the following inter-rg resource dependencies:\n");
my $RESTART_DEP_ERR = gettext("\nFound the following resource restart dependencies:\n");
my $AFFINITIES_ERR = gettext("\nFound the following RG affinities:\n");
my $NEW_FOM_VAL_ERR = gettext("\nFound the following resources with Failover_mode property set to\neither RESTART_ONLY or LOG_ONLY:\n");
my $NEW_FOM_RT_ERR = gettext("\nFound the following resource types with a default Failover_mode property\nset to either RESTART_ONLY or LOG_ONLY:\n");
my $RECOVERY = gettext("The patch that you have attempted to remove contains the functionality to\nsupport inter-rg dependencies, restart dependencies, resource group\naffinities and new values for the Failover_mode property. Thus, the patch\nbackout will be prevented until you have removed the offending dependencies,\naffinities and/or remove the new Failover_mode values for the resources\nor resource types listed above.\n\nPlease reboot the cluster into cluster mode and remove the dependencies,\naffinities and/or reset Failover_mode using scrgadm(1M) or scsetup(1M).\n");
my $FILE_ERR = gettext("Error: failed to open %s for reading\n");
my $FILE_WARN = gettext("Warning: failed to open %s for reading\n");
my $CCR_PATH_ERR = gettext("Error: directory %s does not exist.\n");
my $USAGE = gettext("usage: %s\n");

#
# Declare the global variables
#
my $deb_out = 0;

#
# Parse the command-line, looking for the debug flag.
# We do this check brute-force.  Could use something like getopts if there
# were more command-line switches.
#
if ($#ARGV > -1) {
	if ($#ARGV != 0 || $ARGV[0] ne "-d") {
		printf(STDERR "${USAGE}", $0);
		exit(1);
	} else {
		$deb_out = 1;
	}
}

#
# Make sure the CCR directory exists
#
if (!(-e ${CCR_PATH})) {
	printf(STDERR "${CCR_PATH_ERR}", ${CCR_PATH});
	exit(1);
}

#
# Next, check the CCR.  If it returns an error, exit 1.
#
# If we later decide to check for other things, we would aggregate the
# errors from various checks and exit only after performing all checks.
#
if (check_ccr() != 0) {
	exit(1);
}

#
# All checks passed.
#
exit(0);



#
# subroutine dbg_msg_out
# --------------------
# Example: dbg_msg_out("This is a debug message.\n");
#
# Prints argument 0 if global variable $deb_out is true.
#
sub dbg_msg_out($) {
	if ($deb_out) {
		printf(STDERR "$_[0]");
	}
	return (0);
}

#
# subroutine check_ccr
# -------------------------------
# Example:
# if (check_ccr()) {
#	# it found inter-rg deps, rg affinities, restart deps or
#	# illegal failover_mode values
# } else {
#	# no offending entries in the CCR
#
# Takes no paramaters and returns non-zero if it finds inter-rg dependencies,
# restart dependencies, RG affinities or illegal Failover_mode values. 
# Returns 0 if it does not find any.
#
# As a side effect, writes messages to STDERR if it returns non-zero.
#
sub check_ccr() {
	#
	# Declare our local variables
	#
	my ($ccr_filename, $rg_filename, $rt_filename, $r_name, $rg_name);
	my ($rt_name, $deps);
	my %inter_rg_deps = ();
	my %restart_deps = ();
	my %rg_affinities = ();
	my @illegal_r_fom = ();
	my @illegal_rt_fom= ();

	my $ret_val = 0;

	#
	# Open the directory file
	#
	if (!open(DIRECTORY, "${CCR_DIR}")) {
		printf(STDERR "${FILE_ERR}", ${CCR_DIR});
		return(1);
	}

	dbg_msg_out("After opening directory\n");

	#
	# Iterate over the list of CCR tables in the directory
	#
	while ($ccr_filename = <DIRECTORY>) {
		#
		# get rid of the newline
		#
		chop($ccr_filename);
		dbg_msg_out("Found entry $ccr_filename\n");

		#
		# Check if the filename starts with the RG_PREFIX.  If so,
		# it's an RG file.  We need to open up the file and check
		# it for inter-rg dependencies and restart dependencies.
		#
		if ($ccr_filename =~ /^${RG_PREFIX}(.*)/) {
			$rg_name = $1;

			#
			# construct the absolute path
			#
			$rg_filename = "${CCR_PATH}${ccr_filename}";

			#
			# If check_rg_file finds any offending resources or
			# resource groups, it will record them in the
			# inter_rg_deps, restart_deps, or rg_affinities tables
			# (which we pass by reference via the prototype
			# template, which converts the value args to
			# references).
			# If any resource in the RG uses an illegal
			# Failover_mode value, the resource is added to the
			# illegal_r_fom list (also passed by reference).
			#
			check_rg_file($rg_filename, $rg_name, %inter_rg_deps,
			    %restart_deps, %rg_affinities, @illegal_r_fom);
		}

		#
		# Check if filename is a resource type file. We need to check
		# if the RT defines a default value for the Failover_mode
		# property and if so, check if the default is legal
		#
		if ($ccr_filename =~ /^${RT_PREFIX}(.*)/) {
			$rt_name = $1;

			#
			# construct the absolute path
			#
			$rt_filename = "${CCR_PATH}${ccr_filename}";

			#
			# If the RT uses an illegal default value for
			# Failover_mode, check_rt_file adds the name of the RT
			# to the illegal_rt_fom list
			#
			check_rt_file($rt_filename, $rt_name, @illegal_rt_fom);
		}


	}

	close(DIRECTORY);

	#
	# We've saved the list of offending resources from each resource in
	# the inter_rg_deps and restart_deps tables, and the list of offending
	# rgs in the rg_affinities table.  If there are any entries in the
	# tables, we have found some inter-rg dependencies, restart
	# dependencies, or rg_affinities.  Print the appropriate error
	# messages and return 1.
	#
	# The illegal__r_fom array contains the name of the resources with
	# an illegal value for the Failover_mode property. The illegal_rt_fom
	# array contains the name of the RT using an illegal default value for
	# the Failover_mode property. Both those arrays should be empty. If
	# not, print the appropriate error message and return 1.
	#
	# If there are no entries in the tables, than we have not found any
	# inter-rg dependencies, restart dependencies, rg_affinities or illegal
	# value for Failover_mode, so we can return 0.
	#
	if (scalar(%inter_rg_deps)) {
		printf(STDERR "${INTER_RG_ERR}\n");
		while (($r_name, $deps) = each(%inter_rg_deps)) {
			printf(STDERR gettext("\tResource %s depends on %s\n"),
			    $r_name, $deps);
		}

		$ret_val = 1;
	}

	if (scalar(%restart_deps)) {
		printf(STDERR "${RESTART_DEP_ERR}\n");
		while (($r_name, $deps) = each(%restart_deps)) {
			printf(STDERR gettext("\tResource %s depends on %s\n"),
			    $r_name, $deps);
		}

		$ret_val = 1;
	}

	if (scalar(%rg_affinities)) {
		printf(STDERR "${AFFINITIES_ERR}\n");
		while (($r_name, $deps) = each(%rg_affinities)) {
			printf(STDERR gettext(
			    "\tResource Group %s has affinities for %s\n"),
			    $r_name, $deps);
		}

		$ret_val = 1;
	}

	if (scalar(@illegal_r_fom)) {
		printf(STDERR "${NEW_FOM_VAL_ERR}");
		foreach $r_name (@illegal_r_fom) {
			  printf(STDERR "\t$r_name\n");
		}

		$ret_val = 1;
	}

	if (scalar(@illegal_rt_fom)) {
		printf(STDERR "${NEW_FOM_RT_ERR}");
		foreach $rt_name (@illegal_rt_fom) {
			  printf(STDERR "\t$rt_name\n");
		}

		$ret_val = 1;
	}

	if ($ret_val > 0) {
		printf(STDERR "\n${RECOVERY}");
	}

	return($ret_val);
}


#
# subroutine check_rg_file
# -----------------------
# Example:
# 	check_rg_file($filename, $rgname, \%hash1, \%hash2, \%hash3,
#		      \@illegal_r_fom);
#
# Takes filename and rgname by value and three hastables and one
# array by reference.
#
# Opens the filename, assuming that it represent an RG table.
# Checks whether any of the resources in the table have inter-rg
# dependencies.  If so, adds an entry to hash1 in the form:
#	res_name = dep1,dep2,...
# where res_name is the name of the resource in the RG, and the list
# of dependencies are all its inter-rg dependencies.
#
# Checks also if any of the resources in the RG table have restart
# dependencies.  If so, adds an entry to hash2 of the form:
#	res_name = dep1,dep2,...
#
# Checks also if the RG table has any rg_affinities.  If so, adds an entry
# to hash3 of the form:
#	rg_name = aff1,aff2,...
#
# Check also if any of the resources in the RG table have an illegal value
# for the Failover_mode property. If so, adds it to the res_fom array
#
# Returns the empty list.
#
sub check_rg_file($$\%\%\%\@) {
	#
	# Save the arguments.
	#
	my $filename = $_[0];
	my $rg_name = $_[1];
	my $inter_rgs = $_[2];
	my $res_deps = $_[3];
	my $rg_affs = $_[4];
	my $res_fom = $_[5];

	#
	# Declare local vars.
	#
	my ($r_name, $rg_entry, $res, $affs_str);
	my $rdeps = "";
	my $rdepsw = "";
	my $rdepsr = "";
	my $rfom = "";
	my $deps = "";
	my %res_tab;
	my @deps_list;

	dbg_msg_out("In check_rg_file: filename=$filename\n");

	#
	# Open the RG CCR file
	# If it fails to open, print a warning message, but
	# just skip this file.
	#
	if (!open(RGFILE, "$filename")) {
		printf(STDERR "${FILE_WARN}", $filename);
		return(());
	}

	#
	# Read each line out of the RG CCR file
	#
	while ($rg_entry = <RGFILE>) {
		dbg_msg_out("\tFound entry $rg_entry\n");

		#
		# Check if the entry is a resource.
		#
		if ($rg_entry =~ /^${R_PREFIX}(.*?)\t/) {
			$r_name = $1;

			dbg_msg_out("Found Resource $r_name: ");

			#
			# Pull out the values for each of the dependency
			# types.
			#
			if ($rg_entry =~ /Resource_dependencies=(.*?);/) {
				$rdeps = $1;
				dbg_msg_out("rdeps = $rdeps");
			}

			if ($rg_entry =~ /Resource_dependencies_weak=(.*?);/) {
				$rdepsw = $1;
				dbg_msg_out(", rdepsw = $rdepsw");
			}

			if ($rg_entry =~
			    /Resource_dependencies_restart=(.*?);/) {
				$rdepsr = $1;
				dbg_msg_out(", rdepsr = $rdepsr");
			}

			#
			# Pull out the Failover_mode property value
			#
			if ($rg_entry =~ /Failover_mode=(.*?);/) {
				$rfom = $1;
				dbg_msg_out(", rfom = $rfom");
			}

			dbg_msg_out("\n\n");

			#
			# Gather all the three lists of dependencies together
			# into one big list.  We don't care anymore about the
			# distinction between the types of dependencies.
			#
			# We have to convert the strings of dependencies
			# to lists, then back into one big string, in order
			# to make sure we handle empty dependency lists
			# properly.  That is, we can't just combine the three
			# lists, separated by commas, because one or more of
			# the lists might be empty, so we'd end up with
			# extra commas.
			#
			$deps = join(',', split(/,/, $rdeps),
			    split(/,/, $rdepsw), split(/,/, $rdepsr));

			dbg_msg_out("All deps: $deps\n");


			#
			# Add this resource and its list of resources to
			# our resource table.
			#
			$res_tab{"$r_name"} = $deps;

			#
			# Check for restart dependencies, which we store
			# in the res_deps table.
			#
			if ($rdepsr ne "") {
				dbg_msg_out(
				    "Storing rdeps: $r_name=$rdepsr\n");
				$$res_deps{"$r_name"} = $rdepsr;
			}

			#
			# Check for new values for Failover_mode. Add resource
			# to res_fom array
			if (($rfom eq "RESTART_ONLY") ||
			    ($rfom eq "LOG_ONLY")) {
				dbg_msg_out(
			     "Resource $r_name has new Failover_mode value\n");
				push(@$res_fom, $r_name);
			}
		#
		# Check if the entry is for RG affinities.  The regex
		# should only match if there are actually affinities.
		#
		} elsif ($rg_entry =~ /^RG_affinities\t(.+)/) {
			$affs_str = $1;

			#
			# We matched, so add the affinities to our table.
			#
			$$rg_affs{"$rg_name"} = $affs_str;
		}
	}
	close(RGFILE);

	#
	# Now that we're done processing the file, it's time to check
	# that all the dependencies listed in res_tab refer to resources in
	# the same resource group.  To do that, we iterate over each element
	# in the resource table.  The value of each element is a string
	# containing all the resource dependencies of that resource.  We then
	# iterate through each element of the resource dependencies string,
	# looking up each resource in the resource table to make sure that it
	# is a resource in this resource group.  Any resources that we don't
	# find we add to our offending_res list.  After we have processed
	# each resource, if there is anything in offending_res, add an entry
	# for that resource in the inter_rgs table.
	#
	while (($r_name, $deps) = each(%res_tab)) {
		my @offending_res;

		dbg_msg_out("iterating: name=$r_name, deps=$deps\n");

		foreach $res (split(/,/, $deps)) {
			dbg_msg_out("dep = $res\n");
			if (!defined($res_tab{$res})) {
				dbg_msg_out("Found offending res: $res\n");
				push (@offending_res, $res);
			}
		}

		#
		# If there were any inter-rg deps, save them in the inter_rgs
		# table (to which we have a reference, so we must
		# dereference with $$).
		#
		if (scalar(@offending_res)) {
			dbg_msg_out("Adding offenders for $r_name\n");
			$$inter_rgs{$r_name} = join(",", @offending_res);
		}
	}

	return (());
}


#
# subroutine check_rt_file
# ------------------------
# Example:
# 	check_rt_file($filename, $rtname, \@illegal_rt_fom);
#
# Takes filename and rgtname by value and one array by reference.
#
# Checks whether the Failover_mode system property is defined
# for the resource type. If it is, make sure the default value
# (if any) is not one of the newly supported values RESTART_ONLY
# or LOG_ONLY
#
# Returns the empty list.
#
sub check_rt_file($$\@) {
	#
	# Save the arguments.
	#
	my $filename = $_[0];
	my $rt_name = $_[1];
	my $rt_list = $_[2];

	#
	# Declare local vars.
	#
	my ($rt_entry, $property, $fom);


	dbg_msg_out("In check_rt_file: filename=$filename\n");

	#
	# Open the RT CCR file
	# If it fails to open, print a warning message, but
	# just skip this file.
	#
	if (!open(RTFILE, "$filename")) {
		printf(STDERR "${FILE_WARN}", $filename);
		return(());
	}

	#
	# Read each line out of the RT CCR file
	#
	while ($rt_entry = <RTFILE>) {

		#
		# Check if the entry is the Failover_mode property
		#
		if ($rt_entry =~ /^${PROP_PREFIX}Failover_mode(.*?)\n/) {

			$property = $1;

			#
			# look for the default value to see if it matches
			# one of the new possible values
			#
			if ($property =~ /Default=(.*?);/) {

			    $fom = $1;

			    if (($fom eq "RESTART_ONLY") ||
				($fom eq "LOG_ONLY")) {
				push(@$rt_list, $rt_name);
			    }
			}
		}
	}
	close(RTFILE);


	return (());
}
