90-dependencies.source

From FAIWiki
Revision as of 11:39, 3 September 2010 by StefanGoetz (talk | contribs)
Jump to navigation Jump to search
#
# Implements dependencies and conflicts among FAI classes.
#
# Copyright 2010, Stefan Goetz stefan.goetz@web.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License with
# the Debian GNU/Linux distribution in file /usr/share/common-licenses/GPL-2;
# if not, write to the Free Software Foundation, Inc., 51 Franklin St,
# Fifth Floor, Boston, MA 02110-1301 USA or look on the World Wide Web
# at http://www.gnu.org/copyleft/gpl.html.
#

#
# FAI Class Dependencies
# ======================
#
# This script handles both dependency and conflict rules among FAI classes.
# To use it, drop it into the FAI class directory and put the dependency rules
# into the sub-directory dependencies (i.e. $FAI/class/dependencies).
#
# To specify that class X depends on classes D1 and D2 and D3, create a
# rule file with the name X and the contents "D1 D2 D3". Thus, if the
# current class list contains class X, then D1, D2, and D3 are added to the
# class list as well.
#
# To specify that class X conflicts with classes C1 and C2 and C3, put
# "-C1 -C2 -C3" in dependency file X. Thus, if the current class list contains
# class X, any one class of C1, C2, and C3 present in the class list is removed
# from it. If X is not in the class list, C1, C2, and C3 remain in the class
# list if present.
#
# For dependency files that are not executable, all their content is interpreted
# as class names as describe above.
# Depency files that are executable are executed and their output on stdout is
# evaluated as class names as described above. Different class names must be
# separated by at least one blank, tab, or new-line character. Note that
# dependency resolution runs in multiple passes over all rule files and
# executable rule files are executed once per pass.
#
# Chains of dependencies are supported (e.g., MOUNT_HOME depending on NFS
# depending on (NFSv3 NTP) and NFSv3 depending on PORTMAP). Note that there are
# no sanity checks in this procedure, in particular no checks for cyclic
# dependencies. Therefore, degenerate rules can lead to unstable results or to
# the resolution process not terminating.
#
# This script prints status messages on stdout and error messages on stderr.
#

#
# Changes
# =======
#
# 2010-09-03 stg: initial revision
#


#
# Test whether a class is already present int he global list of classes.
# @param CLASS_NAME the name of the class to check for.
# @return 0 if the class is present, a non-zero value otherwise
#
faidep_test_class () {
	local CLASS_NAME="${1:?}"
	grep -q "^${CLASS_NAME}$" "${FAIDEP_CLASS_FILE}"
}

#
# Add a class to the global list of classes.
# @param CLASS_NAME the name of the class to add.
# @param BEFORE_CLASS If given, add the class CLASS_NAME before the class
#  BEFORE_CLASS. This parameter is optional.
# @return 0 on success
#
faidep_add_class () {
	local CLASS_NAME="${1:?}"
	local BEFORE_CLASS="${2:-}"
	if [ "${BEFORE_CLASS}" ]; then
		sed -ri "/\b${BEFORE_CLASS}\b/ i\
${CLASS_NAME}" "${FAIDEP_CLASS_FILE}"
	else
		echo "${CLASS_NAME}" >> "${FAIDEP_CLASS_FILE}"
	fi
}

#
# Remove a class from the global list of classes.
# @param CLASS_NAME the name of the class to remove.
# @return 0 if ${CLASS_NAME} was succesfully removed, 1 if ${CLASS_NAME} was not
#  present, >1 otherwise
#
faidep_remove_class () {
	local CLASS_NAME="${1:?}"
	if faidep_test_class "${CLASS_NAME}"; then
		sed -i "/^${CLASS_NAME}$/ d" "${FAIDEP_CLASS_FILE}"
		return ${?}
	fi

	return 1
}

#
# Resolve the rules in a dependency file.
# @param FILE the dependency file to evaluate.
# @return 0 if the resolution process did not modify the class list, 1 if the
#  resolution process modified the class list, 2 on error.
#
faidep_resolve_file () {
	local FILE="${1:?}"
	#echo "Evaluating dependency file: '${FILE}'" >&2

	local IS_MODIFIED=0
	local CLASS="$(basename "${FILE}")"
	# is CLASS in $classes, i.e., do we need to resolve this dependency?
	if faidep_test_class "${CLASS}"; then
		local RULES RULE
		if [ -x "${FILE}" ]; then
			RULES="$(${FILE})"
		else
			RULES="$(cat "${FILE}")"
		fi
		for RULE in $RULES; do
			#echo "Rule: '${RULE}'" >&2
			if echo "${RULE}" | egrep -q '^-'; then
				CONFLICT="$(echo "${RULE}" | sed 's/^-//')"
				# is the conflict in the class list and needs to be removed?
				if faidep_test_class "${CONFLICT}"; then
					#echo "Removing conflicting class '${CONFLICT}'" >&2
					IS_MODIFIED=1
					faidep_remove_class "${CONFLICT}"
				fi
			else
				DEPENDENCY="${RULE}"
				# is the dependency missing from the class list and needs to be added?
				if ! faidep_test_class "${DEPENDENCY}"; then
					#echo "Adding dependency class '${DEPENDENCY}'" >&2
					IS_MODIFIED=1
					faidep_add_class "${DEPENDENCY}" "${CLASS}"
				fi
			fi
		done
	fi

	return $IS_MODIFIED
}

#
# Run a single resolution pass over the dependency files.
# @return 0 if the resolution process did not modify the class list (indicating
#  that further passes are unnecessary), 1 if the resolution process modified
#  the class list (indicating that further passes might be necessary), 2 on
#  error
#
faidep_resolve_files () {
	# iterate over all dependency files
	local FILE IS_MODIFIED IFS
	IS_MODIFIED=0
	IFS='
'
	for FILE in $(find "${FAIDEP_DIR}" -mindepth 1 -maxdepth 1 -type f); do
		faidep_resolve_file "${FILE}"
		if [ $? -eq 1 ]; then
			IS_MODIFIED=1
		fi
	done

	return $IS_MODIFIED
}

#
# Run the dependency resolution until the class list is longer modified and a
# stable state is reached.
#
faidep_resolve_until_stable () {
	# resolve dependencies as long as a class is added or removed
	local LOOP_MAX=100 # better safe than sorry
	local LOOP_COUNTER=0
	local IS_MODIFIED=1
	while LOOP_COUNTER=$(( ${LOOP_COUNTER} + 1 )) && [ ${LOOP_COUNTER} -lt ${LOOP_MAX} ]; do
		faidep_resolve_files
		case $? in
			0)
				echo "The list of FAI classes was not modified by most recent dependeny resolution pass - done."
				break
				;;
			1)
				echo "The list of FAI classes was modified by most recent dependeny resolution pass - going into another round."
				continue
				;;
			2)
				echo "Dependency resolution pass failed." 2>&1
				return 2
				;;
			*)
				echo "Unexpected return value from resolution pass." 2>&1
				return 2
		esac
	done

	if [ ${LOOP_COUNTER} -ge ${LOOP_MAX} ]; then
		echo "FAILED to fully resolve class dependencies. Either the dependency configuration in '${FAIDEP_DIR}' is extremely complex or there is a dependency/collision loop." >&2
		return 2
	fi
}


# store shell options so they can be restored at the end of the script (without
# restoring them, they would propagate to the FAI scripts and wreak havoc there)
FAIDEP_SHELL_OPTIONS="$(set +o)"
set -u	# complain about unset variables

# where dependency information is stored
FAIDEP_DIR="${FAI}/class/dependencies"

#
# There are two documented approaches in fai-class to *adding* additional classes
# 1) *.source files: list and export them in the variable ${newclasses}
# 2) non-*.source files: print them on stdout
#
# Removing a class from the list of classes is, however, not available directly
# through fai-class.
# Thus, we need to use a backdoor:
# fai-classes stores the global class list in the file ${LOGDIR}/FAI_CLASSES.
# This file is, unfortunately, not hardcoded but specified on the command line
# of fai-class. We can only hope that fai itself uses fai-class always with that
# particular file because this is what we operate on here.
#
FAIDEP_CLASS_FILE="${LOGDIR}/FAI_CLASSES"

if [ -f "${FAIDEP_CLASS_FILE}" ]; then
	#echo "Class list before dependency resolution: '$(cat ${FAIDEP_CLASS_FILE})'"
	faidep_resolve_until_stable
	#echo "Class list after dependency resolution: '$(cat ${FAIDEP_CLASS_FILE})'"
else
	echo "FAILED to resolve class dependencies. The file '${FAIDEP_CLASS_FILE}', expected to contain the global class list, does not exist!" >&2
fi

# restore options
eval "${FAIDEP_SHELL_OPTIONS}"