From FAIWiki
Revision as of 15:03, 15 September 2010 by StefanGoetz (talk | contribs) (bugfix revision from 2010-09-15)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, 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
# 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
# 2010-09-15 stg: resolution bug: when multiple rules modify the class list in a
#  single pass so that the class list is the same before and after the pass, the
#  resolution could stop but it does not. Fix: change the condition that
#  terminates the resolution process from 'class list was modified' to 'class
#  list is different at end of pass from beginning of pass'.

# 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\
		echo "${CLASS_NAME}" >> "${FAIDEP_CLASS_FILE}"

# 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 ${?}

	return 1

# Get a fingerprint for the current class list (current with respect to
# modifications via faidep_add/remove_class(). When two fingerprints are equal,
# the class lists from which they were created can be assumed to be equal.
# @return a fingerprint on stdout.
faidep_classes_fingerprint () {
	sort -u "${FAIDEP_CLASS_FILE}" | md5sum | cut -d ' ' -f 1

# Resolve the rules in a dependency file.
# @param FILE the dependency file to evaluate.
# @return 0
faidep_resolve_file () {
	local FILE="${1:?}"
	#echo "Evaluating dependency file: '${FILE}'" >&2

	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="$(cat "${FILE}")"
		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
					faidep_remove_class "${CONFLICT}"
				# 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
					faidep_add_class "${DEPENDENCY}" "${CLASS}"

# Run a single resolution pass over the dependency files.
# @return 0
faidep_resolve_files () {
	# iterate over all dependency files
	local FILE IFS
	for FILE in $(find "${FAIDEP_DIR}" -mindepth 1 -maxdepth 1 -type f); do
		faidep_resolve_file "${FILE}"

# Run the dependency resolution until the class list is longer modified and a
# stable state is reached.
faidep_resolve_until_stable () {
	local LOOP_MAX=100 # better safe than sorry

	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

# 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)
set -u	# complain about unset variables

# where dependency information is stored

# 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.

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

# restore options