# -*- coding: iso-8859-1 -*-
# vim: set ft=python ts=3 sw=3 expandtab:
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
#              C E D A R
#          S O L U T I O N S       "Software done right."
#           S O F T W A R E
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# Copyright (c) 2004-2008,2010,2015 Kenneth J. Pronovici.
# All rights reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License,
# Version 2, as published by the Free Software Foundation.
#
# 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.
#
# Copies of the GNU General Public License are available from
# the Free Software Foundation website, http://www.gnu.org/.
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# Author   : Kenneth J. Pronovici <pronovic@ieee.org>
# Language : Python 3 (>= 3.4)
# Project  : Cedar Backup, release 3
# Purpose  : Provides configuration-related objects.
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

########################################################################
# Module documentation
########################################################################

"""
Provides configuration-related objects.

Summary
=======

   Cedar Backup stores all of its configuration in an XML document typically
   called ``cback3.conf``.  The standard location for this document is in
   ``/etc``, but users can specify a different location if they want to.

   The ``Config`` class is a Python object representation of a Cedar Backup XML
   configuration file.  The representation is two-way: XML data can be used to
   create a ``Config`` object, and then changes to the object can be propogated
   back to disk.  A ``Config`` object can even be used to create a configuration
   file from scratch programmatically.

   The ``Config`` class is intended to be the only Python-language interface to
   Cedar Backup configuration on disk.  Cedar Backup will use the class as its
   internal representation of configuration, and applications external to Cedar
   Backup itself (such as a hypothetical third-party configuration tool written
   in Python or a third party extension module) should also use the class when
   they need to read and write configuration files.

Backwards Compatibility
=======================

   The configuration file format has changed between Cedar Backup 1.x and Cedar
   Backup 2.x.  Any Cedar Backup 1.x configuration file is also a valid Cedar
   Backup 2.x configuration file.  However, it doesn't work to go the other
   direction, as the 2.x configuration files contains additional configuration
   is not accepted by older versions of the software.

XML Configuration Structure
===========================

   A ``Config`` object can either be created "empty", or can be created based on
   XML input (either in the form of a string or read in from a file on disk).
   Generally speaking, the XML input *must* result in a ``Config`` object which
   passes the validations laid out below in the *Validation* section.

   An XML configuration file is composed of seven sections:

      - *reference*: specifies reference information about the file (author, revision, etc)
      - *extensions*: specifies mappings to Cedar Backup extensions (external code)
      - *options*: specifies global configuration options
      - *peers*: specifies the set of peers in a master's backup pool
      - *collect*: specifies configuration related to the collect action
      - *stage*: specifies configuration related to the stage action
      - *store*: specifies configuration related to the store action
      - *purge*: specifies configuration related to the purge action

   Each section is represented by an class in this module, and then the overall
   ``Config`` class is a composition of the various other classes.

   Any configuration section that is missing in the XML document (or has not
   been filled into an "empty" document) will just be set to ``None`` in the
   object representation.  The same goes for individual fields within each
   configuration section.  Keep in mind that the document might not be
   completely valid if some sections or fields aren't filled in - but that
   won't matter until validation takes place (see the *Validation* section
   below).

Unicode vs. String Data
=======================

   By default, all string data that comes out of XML documents in Python is
   unicode data (i.e. ``u"whatever"``).  This is fine for many things, but when
   it comes to filesystem paths, it can cause us some problems.  We really want
   strings to be encoded in the filesystem encoding rather than being unicode.
   So, most elements in configuration which represent filesystem paths are
   coverted to plain strings using :any:`util.encodePath`.  The main exception is
   the various ``absoluteExcludePath`` and ``relativeExcludePath`` lists.  These
   are *not* converted, because they are generally only used for filtering,
   not for filesystem operations.

Validation
==========

   There are two main levels of validation in the ``Config`` class and its
   children.  The first is field-level validation.  Field-level validation
   comes into play when a given field in an object is assigned to or updated.
   We use Python's ``property`` functionality to enforce specific validations on
   field values, and in some places we even use customized list classes to
   enforce validations on list members.  You should expect to catch a
   ``ValueError`` exception when making assignments to configuration class
   fields.

   The second level of validation is post-completion validation.  Certain
   validations don't make sense until a document is fully "complete".  We don't
   want these validations to apply all of the time, because it would make
   building up a document from scratch a real pain.  For instance, we might
   have to do things in the right order to keep from throwing exceptions, etc.

   All of these post-completion validations are encapsulated in the
   :any:`Config.validate` method.  This method can be called at any time by a
   client, and will always be called immediately after creating a ``Config``
   object from XML data and before exporting a ``Config`` object to XML.  This
   way, we get decent ease-of-use but we also don't accept or emit invalid
   configuration files.

   The :any:`Config.validate` implementation actually takes two passes to
   completely validate a configuration document.  The first pass at validation
   is to ensure that the proper sections are filled into the document.  There
   are default requirements, but the caller has the opportunity to override
   these defaults.

   The second pass at validation ensures that any filled-in section contains
   valid data.  Any section which is not set to ``None`` is validated according
   to the rules for that section (see below).

   *Reference Validations*

   No validations.

   *Extensions Validations*

   The list of actions may be either ``None`` or an empty list ``[]`` if desired.
   Each extended action must include a name, a module and a function.  Then, an
   extended action must include either an index or dependency information.
   Which one is required depends on which order mode is configured.

   *Options Validations*

   All fields must be filled in except the rsh command.  The rcp and rsh
   commands are used as default values for all remote peers.  Remote peers can
   also rely on the backup user as the default remote user name if they choose.

   *Peers Validations*

   Local peers must be completely filled in, including both name and collect
   directory.  Remote peers must also fill in the name and collect directory,
   but can leave the remote user and rcp command unset.  In this case, the
   remote user is assumed to match the backup user from the options section and
   rcp command is taken directly from the options section.

   *Collect Validations*

   The target directory must be filled in.  The collect mode, archive mode and
   ignore file are all optional.  The list of absolute paths to exclude and
   patterns to exclude may be either ``None`` or an empty list ``[]`` if desired.

   Each collect directory entry must contain an absolute path to collect, and
   then must either be able to take collect mode, archive mode and ignore file
   configuration from the parent ``CollectConfig`` object, or must set each
   value on its own.  The list of absolute paths to exclude, relative paths to
   exclude and patterns to exclude may be either ``None`` or an empty list ``[]``
   if desired.  Any list of absolute paths to exclude or patterns to exclude
   will be combined with the same list in the ``CollectConfig`` object to make
   the complete list for a given directory.

   *Stage Validations*

   The target directory must be filled in.  There must be at least one peer
   (remote or local) between the two lists of peers.  A list with no entries
   can be either ``None`` or an empty list ``[]`` if desired.

   If a set of peers is provided, this configuration completely overrides
   configuration in the peers configuration section, and the same validations
   apply.

   *Store Validations*

   The device type and drive speed are optional, and all other values are
   required (missing booleans will be set to defaults, which is OK).

   The image writer functionality in the ``writer`` module is supposed to be
   able to handle a device speed of ``None``.  Any caller which needs a "real"
   (non-``None``) value for the device type can use ``DEFAULT_DEVICE_TYPE``,
   which is guaranteed to be sensible.

   *Purge Validations*

   The list of purge directories may be either ``None`` or an empty list ``[]``
   if desired.  All purge directories must contain a path and a retain days
   value.

Module Attributes
=================

Attributes:
   DEFAULT_DEVICE_TYPE: The default device type
   DEFAULT_MEDIA_TYPE: The default media type
   VALID_DEVICE_TYPES: List of valid device types
   VALID_MEDIA_TYPES: List of valid media types
   VALID_COLLECT_MODES: List of valid collect modes
   VALID_COMPRESS_MODES: List of valid compress modes
   VALID_ARCHIVE_MODES: List of valid archive modes
   VALID_ORDER_MODES: List of valid extension order modes

:author: Kenneth J. Pronovici <pronovic@ieee.org>
"""

########################################################################
# Imported modules
########################################################################

# System modules
import os
import re
import logging
from functools import total_ordering

# Cedar Backup modules
from CedarBackup3.writers.util import validateScsiId, validateDriveSpeed
from CedarBackup3.util import UnorderedList, AbsolutePathList, ObjectTypeList, parseCommaSeparatedString
from CedarBackup3.util import RegexMatchList, RegexList, encodePath, checkUnique
from CedarBackup3.util import convertSize, displayBytes, UNIT_BYTES, UNIT_KBYTES, UNIT_MBYTES, UNIT_GBYTES
from CedarBackup3.xmlutil import isElement, readChildren, readFirstChild
from CedarBackup3.xmlutil import readStringList, readString, readInteger, readBoolean
from CedarBackup3.xmlutil import addContainerNode, addStringNode, addIntegerNode, addBooleanNode
from CedarBackup3.xmlutil import createInputDom, createOutputDom, serializeDom


########################################################################
# Module-wide constants and variables
########################################################################

logger = logging.getLogger("CedarBackup3.log.config")

DEFAULT_DEVICE_TYPE   = "cdwriter"
DEFAULT_MEDIA_TYPE    = "cdrw-74"

VALID_DEVICE_TYPES    = [ "cdwriter", "dvdwriter", ]
VALID_CD_MEDIA_TYPES  = [ "cdr-74", "cdrw-74", "cdr-80", "cdrw-80", ]
VALID_DVD_MEDIA_TYPES = [ "dvd+r", "dvd+rw", ]
VALID_MEDIA_TYPES     = VALID_CD_MEDIA_TYPES + VALID_DVD_MEDIA_TYPES
VALID_COLLECT_MODES   = [ "daily", "weekly", "incr", ]
VALID_ARCHIVE_MODES   = [ "tar", "targz", "tarbz2", ]
VALID_COMPRESS_MODES  = [ "none", "gzip", "bzip2", ]
VALID_ORDER_MODES     = [ "index", "dependency", ]
VALID_BLANK_MODES     = [ "daily", "weekly", ]
VALID_BYTE_UNITS      = [ UNIT_BYTES, UNIT_KBYTES, UNIT_MBYTES, UNIT_GBYTES, ]
VALID_FAILURE_MODES   = [ "none", "all", "daily", "weekly", ]

REWRITABLE_MEDIA_TYPES = [ "cdrw-74", "cdrw-80", "dvd+rw", ]

ACTION_NAME_REGEX     = r"^[a-z0-9]*$"


########################################################################
# ByteQuantity class definition
########################################################################

@total_ordering
class ByteQuantity(object):

   """
   Class representing a byte quantity.

   A byte quantity has both a quantity and a byte-related unit.  Units are
   maintained using the constants from util.py.  If no units are provided,
   ``UNIT_BYTES`` is assumed.

   The quantity is maintained internally as a string so that issues of
   precision can be avoided.  It really isn't possible to store a floating
   point number here while being able to losslessly translate back and forth
   between XML and object representations.  (Perhaps the Python 2.4 Decimal
   class would have been an option, but I originally wanted to stay compatible
   with Python 2.3.)

   Even though the quantity is maintained as a string, the string must be in a
   valid floating point positive number.  Technically, any floating point
   string format supported by Python is allowble.  However, it does not make
   sense to have a negative quantity of bytes in this context.

   """

   def __init__(self, quantity=None, units=None):
      """
      Constructor for the ``ByteQuantity`` class.

      Args:
         quantity: Quantity of bytes, something interpretable as a float
         units: Unit of bytes, one of VALID_BYTE_UNITS

      Raises:
         ValueError: If one of the values is invalid
      """
      self._quantity = None
      self._units = None
      self.quantity = quantity
      self.units = units

   def __repr__(self):
      """
      Official string representation for class instance.
      """
      return "ByteQuantity(%s, %s)" % (self.quantity, self.units)

   def __str__(self):
      """
      Informal string representation for class instance.
      """
      return "%s" % displayBytes(self.bytes)

   def __eq__(self, other):
      """Equals operator, implemented in terms of Python 2-style compare operator."""
      return self.__cmp__(other) == 0

   def __lt__(self, other):
      """Less-than operator, implemented in terms of Python 2-style compare operator."""
      return self.__cmp__(other) < 0

   def __gt__(self, other):
      """Greater-than operator, implemented in terms of Python 2-style compare operator."""
      return self.__cmp__(other) > 0

   def __cmp__(self, other):
      """
      Python 2-style comparison operator.
      Args:
         other: Other object to compare to
      Returns:
          -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
      """
      if other is None:
         return 1
      elif isinstance(other, ByteQuantity):
         if self.bytes != other.bytes:
            if self.bytes < other.bytes:
               return -1
            else:
               return 1
         return 0
      else:
         return self.__cmp__(ByteQuantity(other, UNIT_BYTES)) # will fail if other can't be coverted to float

   def _setQuantity(self, value):
      """
      Property target used to set the quantity
      The value must be interpretable as a float if it is not None
      Raises:
         ValueError: If the value is an empty string
         ValueError: If the value is not a valid floating point number
         ValueError: If the value is less than zero
      """
      if value is None:
         self._quantity = None
      else:
         try:
            floatValue = float(value)  # allow integer, float, string, etc.
         except:
            raise ValueError("Quantity must be interpretable as a float")
         if floatValue < 0.0:
            raise ValueError("Quantity cannot be negative.")
         self._quantity = str(value) # keep around string

   def _getQuantity(self):
      """
      Property target used to get the quantity.
      """
      return self._quantity

   def _setUnits(self, value):
      """
      Property target used to set the units value.
      If not ``None``, the units value must be one of the values in :any:`VALID_BYTE_UNITS`.
      Raises:
         ValueError: If the value is not valid
      """
      if value is None:
         self._units = UNIT_BYTES
      else:
         if value not in VALID_BYTE_UNITS:
            raise ValueError("Units value must be one of %s." % VALID_BYTE_UNITS)
         self._units = value

   def _getUnits(self):
      """
      Property target used to get the units value.
      """
      return self._units

   def _getBytes(self):
      """
      Property target used to return the byte quantity as a floating point number.
      If there is no quantity set, then a value of 0.0 is returned.
      """
      if self.quantity is not None and self.units is not None:
         return convertSize(self.quantity, self.units, UNIT_BYTES)
      return 0.0

   quantity = property(_getQuantity, _setQuantity, None, doc="Byte quantity, as a string")
   units = property(_getUnits, _setUnits, None, doc="Units for byte quantity, for instance UNIT_BYTES")
   bytes = property(_getBytes, None, None, doc="Byte quantity, as a floating point number.")


########################################################################
# ActionDependencies class definition
########################################################################

@total_ordering
class ActionDependencies(object):

   """
   Class representing dependencies associated with an extended action.

   Execution ordering for extended actions is done in one of two ways: either by using
   index values (lower index gets run first) or by having the extended action specify
   dependencies in terms of other named actions.  This class encapsulates the dependency
   information for an extended action.

   The following restrictions exist on data in this class:

      - Any action name must be a non-empty string matching ``ACTION_NAME_REGEX``

   """

   def __init__(self, beforeList=None, afterList=None):
      """
      Constructor for the ``ActionDependencies`` class.

      Args:
         beforeList: List of named actions that this action must be run before
         afterList: List of named actions that this action must be run after

      Raises:
         ValueError: If one of the values is invalid
      """
      self._beforeList = None
      self._afterList = None
      self.beforeList = beforeList
      self.afterList = afterList

   def __repr__(self):
      """
      Official string representation for class instance.
      """
      return "ActionDependencies(%s, %s)" % (self.beforeList, self.afterList)

   def __str__(self):
      """
      Informal string representation for class instance.
      """
      return self.__repr__()

   def __eq__(self, other):
      """Equals operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) == 0

   def __lt__(self, other):
      """Less-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) < 0

   def __gt__(self, other):
      """Greater-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) > 0

   def __cmp__(self, other):
      """
      Original Python 2 comparison operator.
      Args:
         other: Other object to compare to
      Returns:
          -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
      """
      if other is None:
         return 1
      if self.beforeList != other.beforeList:
         if self.beforeList < other.beforeList:
            return -1
         else:
            return 1
      if self.afterList != other.afterList:
         if self.afterList < other.afterList:
            return -1
         else:
            return 1
      return 0

   def _setBeforeList(self, value):
      """
      Property target used to set the "run before" list.
      Either the value must be ``None`` or each element must be a string matching ACTION_NAME_REGEX.
      Raises:
         ValueError: If the value does not match the regular expression
      """
      if value is None:
         self._beforeList = None
      else:
         try:
            saved = self._beforeList
            self._beforeList = RegexMatchList(ACTION_NAME_REGEX, emptyAllowed=False, prefix="Action name")
            self._beforeList.extend(value)
         except Exception as e:
            self._beforeList = saved
            raise e

   def _getBeforeList(self):
      """
      Property target used to get the "run before" list.
      """
      return self._beforeList

   def _setAfterList(self, value):
      """
      Property target used to set the "run after" list.
      Either the value must be ``None`` or each element must be a string matching ACTION_NAME_REGEX.
      Raises:
         ValueError: If the value does not match the regular expression
      """
      if value is None:
         self._afterList = None
      else:
         try:
            saved = self._afterList
            self._afterList = RegexMatchList(ACTION_NAME_REGEX, emptyAllowed=False, prefix="Action name")
            self._afterList.extend(value)
         except Exception as e:
            self._afterList = saved
            raise e

   def _getAfterList(self):
      """
      Property target used to get the "run after" list.
      """
      return self._afterList

   beforeList = property(_getBeforeList, _setBeforeList, None, "List of named actions that this action must be run before.")
   afterList = property(_getAfterList, _setAfterList, None, "List of named actions that this action must be run after.")


########################################################################
# ActionHook class definition
########################################################################

@total_ordering
class ActionHook(object):

   """
   Class representing a hook associated with an action.

   A hook associated with an action is a shell command to be executed either
   before or after a named action is executed.

   The following restrictions exist on data in this class:

      - The action name must be a non-empty string matching ``ACTION_NAME_REGEX``
      - The shell command must be a non-empty string.

   The internal ``before`` and ``after`` instance variables are always set to
   False in this parent class.

   """

   def __init__(self, action=None, command=None):
      """
      Constructor for the ``ActionHook`` class.

      Args:
         action: Action this hook is associated with
         command: Shell command to execute

      Raises:
         ValueError: If one of the values is invalid
      """
      self._action = None
      self._command = None
      self._before = False
      self._after = False
      self.action = action
      self.command = command

   def __repr__(self):
      """
      Official string representation for class instance.
      """
      return "ActionHook(%s, %s, %s, %s)" % (self.action, self.command, self.before, self.after)

   def __str__(self):
      """
      Informal string representation for class instance.
      """
      return self.__repr__()

   def __eq__(self, other):
      """Equals operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) == 0

   def __lt__(self, other):
      """Less-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) < 0

   def __gt__(self, other):
      """Greater-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) > 0

   def __cmp__(self, other):
      """
      Original Python 2 comparison operator.
      Args:
         other: Other object to compare to
      Returns:
          -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
      """
      if other is None:
         return 1
      if self.action != other.action:
         if str(self.action or "") < str(other.action or ""):
            return -1
         else:
            return 1
      if self.command != other.command:
         if str(self.command or "") < str(other.command or ""):
            return -1
         else:
            return 1
      if self.before != other.before:
         if self.before < other.before:
            return -1
         else:
            return 1
      if self.after != other.after:
         if self.after < other.after:
            return -1
         else:
            return 1
      return 0

   def _setAction(self, value):
      """
      Property target used to set the action name.
      The value must be a non-empty string if it is not ``None``.
      It must also consist only of lower-case letters and digits.
      Raises:
         ValueError: If the value is an empty string
      """
      pattern = re.compile(ACTION_NAME_REGEX)
      if value is not None:
         if len(value) < 1:
            raise ValueError("The action name must be a non-empty string.")
         if not pattern.search(value):
            raise ValueError("The action name must consist of only lower-case letters and digits.")
      self._action = value

   def _getAction(self):
      """
      Property target used to get the action name.
      """
      return self._action

   def _setCommand(self, value):
      """
      Property target used to set the command.
      The value must be a non-empty string if it is not ``None``.
      Raises:
         ValueError: If the value is an empty string
      """
      if value is not None:
         if len(value) < 1:
            raise ValueError("The command must be a non-empty string.")
      self._command = value

   def _getCommand(self):
      """
      Property target used to get the command.
      """
      return self._command

   def _getBefore(self):
      """
      Property target used to get the before flag.
      """
      return self._before

   def _getAfter(self):
      """
      Property target used to get the after flag.
      """
      return self._after

   action = property(_getAction, _setAction, None, "Action this hook is associated with.")
   command = property(_getCommand, _setCommand, None, "Shell command to execute.")
   before = property(_getBefore, None, None, "Indicates whether command should be executed before action.")
   after = property(_getAfter, None, None, "Indicates whether command should be executed after action.")

@total_ordering
class PreActionHook(ActionHook):

   """
   Class representing a pre-action hook associated with an action.

   A hook associated with an action is a shell command to be executed either
   before or after a named action is executed.  In this case, a pre-action hook
   is executed before the named action.

   The following restrictions exist on data in this class:

      - The action name must be a non-empty string consisting of lower-case letters and digits.
      - The shell command must be a non-empty string.

   The internal ``before`` instance variable is always set to True in this
   class.

   """

   def __init__(self, action=None, command=None):
      """
      Constructor for the ``PreActionHook`` class.

      Args:
         action: Action this hook is associated with
         command: Shell command to execute

      Raises:
         ValueError: If one of the values is invalid
      """
      ActionHook.__init__(self, action, command)
      self._before = True

   def __repr__(self):
      """
      Official string representation for class instance.
      """
      return "PreActionHook(%s, %s, %s, %s)" % (self.action, self.command, self.before, self.after)

@total_ordering
class PostActionHook(ActionHook):

   """
   Class representing a pre-action hook associated with an action.

   A hook associated with an action is a shell command to be executed either
   before or after a named action is executed.  In this case, a post-action hook
   is executed after the named action.

   The following restrictions exist on data in this class:

      - The action name must be a non-empty string consisting of lower-case letters and digits.
      - The shell command must be a non-empty string.

   The internal ``before`` instance variable is always set to True in this
   class.

   """

   def __init__(self, action=None, command=None):
      """
      Constructor for the ``PostActionHook`` class.

      Args:
         action: Action this hook is associated with
         command: Shell command to execute

      Raises:
         ValueError: If one of the values is invalid
      """
      ActionHook.__init__(self, action, command)
      self._after = True

   def __repr__(self):
      """
      Official string representation for class instance.
      """
      return "PostActionHook(%s, %s, %s, %s)" % (self.action, self.command, self.before, self.after)


########################################################################
# BlankBehavior class definition
########################################################################

@total_ordering
class BlankBehavior(object):

   """
   Class representing optimized store-action media blanking behavior.

   The following restrictions exist on data in this class:

      - The blanking mode must be a one of the values in ``VALID_BLANK_MODES``
      - The blanking factor must be a positive floating point number

   """

   def __init__(self, blankMode=None, blankFactor=None):
      """
      Constructor for the ``BlankBehavior`` class.

      Args:
         blankMode: Blanking mode
         blankFactor: Blanking factor

      Raises:
         ValueError: If one of the values is invalid
      """
      self._blankMode = None
      self._blankFactor = None
      self.blankMode = blankMode
      self.blankFactor = blankFactor

   def __repr__(self):
      """
      Official string representation for class instance.
      """
      return "BlankBehavior(%s, %s)" % (self.blankMode, self.blankFactor)

   def __str__(self):
      """
      Informal string representation for class instance.
      """
      return self.__repr__()

   def __eq__(self, other):
      """Equals operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) == 0

   def __lt__(self, other):
      """Less-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) < 0

   def __gt__(self, other):
      """Greater-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) > 0

   def __cmp__(self, other):
      """
      Original Python 2 comparison operator.
      Args:
         other: Other object to compare to
      Returns:
          -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
      """
      if other is None:
         return 1
      if self.blankMode != other.blankMode:
         if str(self.blankMode or "") < str(other.blankMode or ""):
            return -1
         else:
            return 1
      if self.blankFactor != other.blankFactor:
         if float(self.blankFactor or 0.0) < float(other.blankFactor or 0.0):
            return -1
         else:
            return 1
      return 0

   def _setBlankMode(self, value):
      """
      Property target used to set the blanking mode.
      The value must be one of ``VALID_BLANK_MODES``.
      Raises:
         ValueError: If the value is not valid
      """
      if value is not None:
         if value not in VALID_BLANK_MODES:
            raise ValueError("Blanking mode must be one of %s." % VALID_BLANK_MODES)
      self._blankMode = value

   def _getBlankMode(self):
      """
      Property target used to get the blanking mode.
      """
      return self._blankMode

   def _setBlankFactor(self, value):
      """
      Property target used to set the blanking factor.
      The value must be a non-empty string if it is not ``None``.
      Raises:
         ValueError: If the value is an empty string
         ValueError: If the value is not a valid floating point number
         ValueError: If the value is less than zero
      """
      if value is not None:
         if len(value) < 1:
            raise ValueError("Blanking factor must be a non-empty string.")
         floatValue = float(value)
         if floatValue < 0.0:
            raise ValueError("Blanking factor cannot be negative.")
      self._blankFactor = value # keep around string

   def _getBlankFactor(self):
      """
      Property target used to get the blanking factor.
      """
      return self._blankFactor

   blankMode = property(_getBlankMode, _setBlankMode, None, "Blanking mode")
   blankFactor = property(_getBlankFactor, _setBlankFactor, None, "Blanking factor")


########################################################################
# ExtendedAction class definition
########################################################################

@total_ordering
class ExtendedAction(object):

   """
   Class representing an extended action.

   Essentially, an extended action needs to allow the following to happen::

      exec("from %s import %s" % (module, function))
      exec("%s(action, configPath")" % function)

   The following restrictions exist on data in this class:

      - The action name must be a non-empty string consisting of lower-case letters and digits.
      - The module must be a non-empty string and a valid Python identifier.
      - The function must be an on-empty string and a valid Python identifier.
      - If set, the index must be a positive integer.
      - If set, the dependencies attribute must be an ``ActionDependencies`` object.

   """

   def __init__(self, name=None, module=None, function=None, index=None, dependencies=None):
      """
      Constructor for the ``ExtendedAction`` class.

      Args:
         name: Name of the extended action
         module: Name of the module containing the extended action function
         function: Name of the extended action function
         index: Index of action, used for execution ordering
         dependencies: Dependencies for action, used for execution ordering

      Raises:
         ValueError: If one of the values is invalid
      """
      self._name = None
      self._module = None
      self._function = None
      self._index = None
      self._dependencies = None
      self.name = name
      self.module = module
      self.function = function
      self.index = index
      self.dependencies = dependencies

   def __repr__(self):
      """
      Official string representation for class instance.
      """
      return "ExtendedAction(%s, %s, %s, %s, %s)" % (self.name, self.module, self.function, self.index, self.dependencies)

   def __str__(self):
      """
      Informal string representation for class instance.
      """
      return self.__repr__()

   def __eq__(self, other):
      """Equals operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) == 0

   def __lt__(self, other):
      """Less-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) < 0

   def __gt__(self, other):
      """Greater-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) > 0

   def __cmp__(self, other):
      """
      Original Python 2 comparison operator.
      Args:
         other: Other object to compare to
      Returns:
          -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
      """
      if other is None:
         return 1
      if self.name != other.name:
         if str(self.name or "") < str(other.name or ""):
            return -1
         else:
            return 1
      if self.module != other.module:
         if str(self.module or "") < str(other.module or ""):
            return -1
         else:
            return 1
      if self.function != other.function:
         if str(self.function or "") < str(other.function or ""):
            return -1
         else:
            return 1
      if self.index != other.index:
         if int(self.index or 0) < int(other.index or 0):
            return -1
         else:
            return 1
      if self.dependencies != other.dependencies:
         if self.dependencies < other.dependencies:
            return -1
         else:
            return 1
      return 0

   def _setName(self, value):
      """
      Property target used to set the action name.
      The value must be a non-empty string if it is not ``None``.
      It must also consist only of lower-case letters and digits.
      Raises:
         ValueError: If the value is an empty string
      """
      pattern = re.compile(ACTION_NAME_REGEX)
      if value is not None:
         if len(value) < 1:
            raise ValueError("The action name must be a non-empty string.")
         if not pattern.search(value):
            raise ValueError("The action name must consist of only lower-case letters and digits.")
      self._name = value

   def _getName(self):
      """
      Property target used to get the action name.
      """
      return self._name

   def _setModule(self, value):
      """
      Property target used to set the module name.
      The value must be a non-empty string if it is not ``None``.
      It must also be a valid Python identifier.
      Raises:
         ValueError: If the value is an empty string
      """
      pattern = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)(\.[A-Za-z_][A-Za-z0-9_]*)*$")
      if value is not None:
         if len(value) < 1:
            raise ValueError("The module name must be a non-empty string.")
         if not pattern.search(value):
            raise ValueError("The module name must be a valid Python identifier.")
      self._module = value

   def _getModule(self):
      """
      Property target used to get the module name.
      """
      return self._module

   def _setFunction(self, value):
      """
      Property target used to set the function name.
      The value must be a non-empty string if it is not ``None``.
      It must also be a valid Python identifier.
      Raises:
         ValueError: If the value is an empty string
      """
      pattern = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
      if value is not None:
         if len(value) < 1:
            raise ValueError("The function name must be a non-empty string.")
         if not pattern.search(value):
            raise ValueError("The function name must be a valid Python identifier.")
      self._function = value

   def _getFunction(self):
      """
      Property target used to get the function name.
      """
      return self._function

   def _setIndex(self, value):
      """
      Property target used to set the action index.
      The value must be an integer >= 0.
      Raises:
         ValueError: If the value is not valid
      """
      if value is None:
         self._index = None
      else:
         try:
            value = int(value)
         except TypeError:
            raise ValueError("Action index value must be an integer >= 0.")
         if value < 0:
            raise ValueError("Action index value must be an integer >= 0.")
         self._index = value

   def _getIndex(self):
      """
      Property target used to get the action index.
      """
      return self._index

   def _setDependencies(self, value):
      """
      Property target used to set the action dependencies information.
      If not ``None``, the value must be a ``ActionDependecies`` object.
      Raises:
         ValueError: If the value is not a ``ActionDependencies`` object
      """
      if value is None:
         self._dependencies = None
      else:
         if not isinstance(value, ActionDependencies):
            raise ValueError("Value must be a ``ActionDependencies`` object.")
         self._dependencies = value

   def _getDependencies(self):
      """
      Property target used to get action dependencies information.
      """
      return self._dependencies

   name = property(_getName, _setName, None, "Name of the extended action.")
   module = property(_getModule, _setModule, None, "Name of the module containing the extended action function.")
   function = property(_getFunction, _setFunction, None, "Name of the extended action function.")
   index = property(_getIndex, _setIndex, None, "Index of action, used for execution ordering.")
   dependencies = property(_getDependencies, _setDependencies, None, "Dependencies for action, used for execution ordering.")


########################################################################
# CommandOverride class definition
########################################################################

@total_ordering
class CommandOverride(object):

   """
   Class representing a piece of Cedar Backup command override configuration.

   The following restrictions exist on data in this class:

      - The absolute path must be absolute

   *Note:* Lists within this class are "unordered" for equality comparisons.

   """

   def __init__(self, command=None, absolutePath=None):
      """
      Constructor for the ``CommandOverride`` class.

      Args:
         command: Name of command to be overridden
         absolutePath: Absolute path of the overrridden command

      Raises:
         ValueError: If one of the values is invalid
      """
      self._command = None
      self._absolutePath = None
      self.command = command
      self.absolutePath = absolutePath

   def __repr__(self):
      """
      Official string representation for class instance.
      """
      return "CommandOverride(%s, %s)" % (self.command, self.absolutePath)

   def __str__(self):
      """
      Informal string representation for class instance.
      """
      return self.__repr__()

   def __eq__(self, other):
      """Equals operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) == 0

   def __lt__(self, other):
      """Less-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) < 0

   def __gt__(self, other):
      """Greater-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) > 0

   def __cmp__(self, other):
      """
      Original Python 2 comparison operator.
      Args:
         other: Other object to compare to
      Returns:
          -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
      """
      if other is None:
         return 1
      if self.command != other.command:
         if str(self.command or "") < str(other.command or ""):
            return -1
         else:
            return 1
      if self.absolutePath != other.absolutePath:
         if str(self.absolutePath or "") < str(other.absolutePath or ""):
            return -1
         else:
            return 1
      return 0

   def _setCommand(self, value):
      """
      Property target used to set the command.
      The value must be a non-empty string if it is not ``None``.
      Raises:
         ValueError: If the value is an empty string
      """
      if value is not None:
         if len(value) < 1:
            raise ValueError("The command must be a non-empty string.")
      self._command = value

   def _getCommand(self):
      """
      Property target used to get the command.
      """
      return self._command

   def _setAbsolutePath(self, value):
      """
      Property target used to set the absolute path.
      The value must be an absolute path if it is not ``None``.
      It does not have to exist on disk at the time of assignment.
      Raises:
         ValueError: If the value is not an absolute path
         ValueError: If the value cannot be encoded properly
      """
      if value is not None:
         if not os.path.isabs(value):
            raise ValueError("Not an absolute path: [%s]" % value)
      self._absolutePath = encodePath(value)

   def _getAbsolutePath(self):
      """
      Property target used to get the absolute path.
      """
      return self._absolutePath

   command = property(_getCommand, _setCommand, None, doc="Name of command to be overridden.")
   absolutePath = property(_getAbsolutePath, _setAbsolutePath, None, doc="Absolute path of the overrridden command.")


########################################################################
# CollectFile class definition
########################################################################

@total_ordering
class CollectFile(object):

   """
   Class representing a Cedar Backup collect file.

   The following restrictions exist on data in this class:

      - Absolute paths must be absolute
      - The collect mode must be one of the values in :any:`VALID_COLLECT_MODES`.
      - The archive mode must be one of the values in :any:`VALID_ARCHIVE_MODES`.

   """

   def __init__(self, absolutePath=None, collectMode=None, archiveMode=None):
      """
      Constructor for the ``CollectFile`` class.

      Args:
         absolutePath: Absolute path of the file to collect
         collectMode: Overridden collect mode for this file
         archiveMode: Overridden archive mode for this file

      Raises:
         ValueError: If one of the values is invalid
      """
      self._absolutePath = None
      self._collectMode = None
      self._archiveMode = None
      self.absolutePath = absolutePath
      self.collectMode = collectMode
      self.archiveMode = archiveMode

   def __repr__(self):
      """
      Official string representation for class instance.
      """
      return "CollectFile(%s, %s, %s)" % (self.absolutePath, self.collectMode, self.archiveMode)

   def __str__(self):
      """
      Informal string representation for class instance.
      """
      return self.__repr__()

   def __eq__(self, other):
      """Equals operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) == 0

   def __lt__(self, other):
      """Less-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) < 0

   def __gt__(self, other):
      """Greater-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) > 0

   def __cmp__(self, other):
      """
      Original Python 2 comparison operator.
      Args:
         other: Other object to compare to
      Returns:
          -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
      """
      if other is None:
         return 1
      if self.absolutePath != other.absolutePath:
         if str(self.absolutePath or "") < str(other.absolutePath or ""):
            return -1
         else:
            return 1
      if self.collectMode != other.collectMode:
         if str(self.collectMode or "") < str(other.collectMode or ""):
            return -1
         else:
            return 1
      if self.archiveMode != other.archiveMode:
         if str(self.archiveMode or "") < str(other.archiveMode or ""):
            return -1
         else:
            return 1
      return 0

   def _setAbsolutePath(self, value):
      """
      Property target used to set the absolute path.
      The value must be an absolute path if it is not ``None``.
      It does not have to exist on disk at the time of assignment.
      Raises:
         ValueError: If the value is not an absolute path
         ValueError: If the value cannot be encoded properly
      """
      if value is not None:
         if not os.path.isabs(value):
            raise ValueError("Not an absolute path: [%s]" % value)
      self._absolutePath = encodePath(value)

   def _getAbsolutePath(self):
      """
      Property target used to get the absolute path.
      """
      return self._absolutePath

   def _setCollectMode(self, value):
      """
      Property target used to set the collect mode.
      If not ``None``, the mode must be one of the values in :any:`VALID_COLLECT_MODES`.
      Raises:
         ValueError: If the value is not valid
      """
      if value is not None:
         if value not in VALID_COLLECT_MODES:
            raise ValueError("Collect mode must be one of %s." % VALID_COLLECT_MODES)
      self._collectMode = value

   def _getCollectMode(self):
      """
      Property target used to get the collect mode.
      """
      return self._collectMode

   def _setArchiveMode(self, value):
      """
      Property target used to set the archive mode.
      If not ``None``, the mode must be one of the values in :any:`VALID_ARCHIVE_MODES`.
      Raises:
         ValueError: If the value is not valid
      """
      if value is not None:
         if value not in VALID_ARCHIVE_MODES:
            raise ValueError("Archive mode must be one of %s." % VALID_ARCHIVE_MODES)
      self._archiveMode = value

   def _getArchiveMode(self):
      """
      Property target used to get the archive mode.
      """
      return self._archiveMode

   absolutePath = property(_getAbsolutePath, _setAbsolutePath, None, doc="Absolute path of the file to collect.")
   collectMode = property(_getCollectMode, _setCollectMode, None, doc="Overridden collect mode for this file.")
   archiveMode = property(_getArchiveMode, _setArchiveMode, None, doc="Overridden archive mode for this file.")


########################################################################
# CollectDir class definition
########################################################################

@total_ordering
class CollectDir(object):

   """
   Class representing a Cedar Backup collect directory.

   The following restrictions exist on data in this class:

      - Absolute paths must be absolute
      - The collect mode must be one of the values in :any:`VALID_COLLECT_MODES`.
      - The archive mode must be one of the values in :any:`VALID_ARCHIVE_MODES`.
      - The ignore file must be a non-empty string.

   For the ``absoluteExcludePaths`` list, validation is accomplished through the
   :any:`util.AbsolutePathList` list implementation that overrides common list
   methods and transparently does the absolute path validation for us.

   *Note:* Lists within this class are "unordered" for equality comparisons.

   """

   def __init__(self, absolutePath=None, collectMode=None, archiveMode=None, ignoreFile=None,
                absoluteExcludePaths=None, relativeExcludePaths=None, excludePatterns=None,
                linkDepth=None, dereference=False, recursionLevel=None):
      """
      Constructor for the ``CollectDir`` class.

      Args:
         absolutePath: Absolute path of the directory to collect
         collectMode: Overridden collect mode for this directory
         archiveMode: Overridden archive mode for this directory
         ignoreFile: Overidden ignore file name for this directory
         linkDepth: Maximum at which soft links should be followed
         dereference: Whether to dereference links that are followed
         absoluteExcludePaths: List of absolute paths to exclude
         relativeExcludePaths: List of relative paths to exclude
         excludePatterns: List of regular expression patterns to exclude

      Raises:
         ValueError: If one of the values is invalid
      """
      self._absolutePath = None
      self._collectMode = None
      self._archiveMode = None
      self._ignoreFile = None
      self._linkDepth = None
      self._dereference = None
      self._recursionLevel = None
      self._absoluteExcludePaths = None
      self._relativeExcludePaths = None
      self._excludePatterns = None
      self.absolutePath = absolutePath
      self.collectMode = collectMode
      self.archiveMode = archiveMode
      self.ignoreFile = ignoreFile
      self.linkDepth = linkDepth
      self.dereference = dereference
      self.recursionLevel = recursionLevel
      self.absoluteExcludePaths = absoluteExcludePaths
      self.relativeExcludePaths = relativeExcludePaths
      self.excludePatterns = excludePatterns

   def __repr__(self):
      """
      Official string representation for class instance.
      """
      return "CollectDir(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)" % (self.absolutePath, self.collectMode,
                                                                     self.archiveMode, self.ignoreFile,
                                                                     self.absoluteExcludePaths,
                                                                     self.relativeExcludePaths,
                                                                     self.excludePatterns,
                                                                     self.linkDepth, self.dereference,
                                                                     self.recursionLevel)

   def __str__(self):
      """
      Informal string representation for class instance.
      """
      return self.__repr__()

   def __eq__(self, other):
      """Equals operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) == 0

   def __lt__(self, other):
      """Less-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) < 0

   def __gt__(self, other):
      """Greater-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) > 0

   def __cmp__(self, other):
      """
      Original Python 2 comparison operator.
      Lists within this class are "unordered" for equality comparisons.
      Args:
         other: Other object to compare to
      Returns:
          -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
      """
      if other is None:
         return 1
      if self.absolutePath != other.absolutePath:
         if str(self.absolutePath or "") < str(other.absolutePath or ""):
            return -1
         else:
            return 1
      if self.collectMode != other.collectMode:
         if str(self.collectMode or "") < str(other.collectMode or ""):
            return -1
         else:
            return 1
      if self.archiveMode != other.archiveMode:
         if str(self.archiveMode or "") < str(other.archiveMode or ""):
            return -1
         else:
            return 1
      if self.ignoreFile != other.ignoreFile:
         if str(self.ignoreFile or "") < str(other.ignoreFile or ""):
            return -1
         else:
            return 1
      if self.linkDepth != other.linkDepth:
         if int(self.linkDepth or 0) < int(other.linkDepth or 0):
            return -1
         else:
            return 1
      if self.dereference != other.dereference:
         if self.dereference < other.dereference:
            return -1
         else:
            return 1
      if self.recursionLevel != other.recursionLevel:
         if int(self.recursionLevel or 0) < int(other.recursionLevel or 0):
            return -1
         else:
            return 1
      if self.absoluteExcludePaths != other.absoluteExcludePaths:
         if self.absoluteExcludePaths < other.absoluteExcludePaths:
            return -1
         else:
            return 1
      if self.relativeExcludePaths != other.relativeExcludePaths:
         if self.relativeExcludePaths < other.relativeExcludePaths:
            return -1
         else:
            return 1
      if self.excludePatterns != other.excludePatterns:
         if self.excludePatterns < other.excludePatterns:
            return -1
         else:
            return 1
      return 0

   def _setAbsolutePath(self, value):
      """
      Property target used to set the absolute path.
      The value must be an absolute path if it is not ``None``.
      It does not have to exist on disk at the time of assignment.
      Raises:
         ValueError: If the value is not an absolute path
         ValueError: If the value cannot be encoded properly
      """
      if value is not None:
         if not os.path.isabs(value):
            raise ValueError("Not an absolute path: [%s]" % value)
      self._absolutePath = encodePath(value)

   def _getAbsolutePath(self):
      """
      Property target used to get the absolute path.
      """
      return self._absolutePath

   def _setCollectMode(self, value):
      """
      Property target used to set the collect mode.
      If not ``None``, the mode must be one of the values in :any:`VALID_COLLECT_MODES`.
      Raises:
         ValueError: If the value is not valid
      """
      if value is not None:
         if value not in VALID_COLLECT_MODES:
            raise ValueError("Collect mode must be one of %s." % VALID_COLLECT_MODES)
      self._collectMode = value

   def _getCollectMode(self):
      """
      Property target used to get the collect mode.
      """
      return self._collectMode

   def _setArchiveMode(self, value):
      """
      Property target used to set the archive mode.
      If not ``None``, the mode must be one of the values in :any:`VALID_ARCHIVE_MODES`.
      Raises:
         ValueError: If the value is not valid
      """
      if value is not None:
         if value not in VALID_ARCHIVE_MODES:
            raise ValueError("Archive mode must be one of %s." % VALID_ARCHIVE_MODES)
      self._archiveMode = value

   def _getArchiveMode(self):
      """
      Property target used to get the archive mode.
      """
      return self._archiveMode

   def _setIgnoreFile(self, value):
      """
      Property target used to set the ignore file.
      The value must be a non-empty string if it is not ``None``.
      Raises:
         ValueError: If the value is an empty string
      """
      if value is not None:
         if len(value) < 1:
            raise ValueError("The ignore file must be a non-empty string.")
      self._ignoreFile = value

   def _getIgnoreFile(self):
      """
      Property target used to get the ignore file.
      """
      return self._ignoreFile

   def _setLinkDepth(self, value):
      """
      Property target used to set the link depth.
      The value must be an integer >= 0.
      Raises:
         ValueError: If the value is not valid
      """
      if value is None:
         self._linkDepth = None
      else:
         try:
            value = int(value)
         except TypeError:
            raise ValueError("Link depth value must be an integer >= 0.")
         if value < 0:
            raise ValueError("Link depth value must be an integer >= 0.")
         self._linkDepth = value

   def _getLinkDepth(self):
      """
      Property target used to get the action linkDepth.
      """
      return self._linkDepth

   def _setDereference(self, value):
      """
      Property target used to set the dereference flag.
      No validations, but we normalize the value to ``True`` or ``False``.
      """
      if value:
         self._dereference = True
      else:
         self._dereference = False

   def _getDereference(self):
      """
      Property target used to get the dereference flag.
      """
      return self._dereference

   def _setRecursionLevel(self, value):
      """
      Property target used to set the recursionLevel.
      The value must be an integer.
      Raises:
         ValueError: If the value is not valid
      """
      if value is None:
         self._recursionLevel = None
      else:
         try:
            value = int(value)
         except TypeError:
            raise ValueError("Recusion level value must be an integer.")
         self._recursionLevel = value

   def _getRecursionLevel(self):
      """
      Property target used to get the action recursionLevel.
      """
      return self._recursionLevel

   def _setAbsoluteExcludePaths(self, value):
      """
      Property target used to set the absolute exclude paths list.
      Either the value must be ``None`` or each element must be an absolute path.
      Elements do not have to exist on disk at the time of assignment.
      Raises:
         ValueError: If the value is not an absolute path
      """
      if value is None:
         self._absoluteExcludePaths = None
      else:
         try:
            saved = self._absoluteExcludePaths
            self._absoluteExcludePaths = AbsolutePathList()
            self._absoluteExcludePaths.extend(value)
         except Exception as e:
            self._absoluteExcludePaths = saved
            raise e

   def _getAbsoluteExcludePaths(self):
      """
      Property target used to get the absolute exclude paths list.
      """
      return self._absoluteExcludePaths

   def _setRelativeExcludePaths(self, value):
      """
      Property target used to set the relative exclude paths list.
      Elements do not have to exist on disk at the time of assignment.
      """
      if value is None:
         self._relativeExcludePaths = None
      else:
         try:
            saved = self._relativeExcludePaths
            self._relativeExcludePaths = UnorderedList()
            self._relativeExcludePaths.extend(value)
         except Exception as e:
            self._relativeExcludePaths = saved
            raise e

   def _getRelativeExcludePaths(self):
      """
      Property target used to get the relative exclude paths list.
      """
      return self._relativeExcludePaths

   def _setExcludePatterns(self, value):
      """
      Property target used to set the exclude patterns list.
      """
      if value is None:
         self._excludePatterns = None
      else:
         try:
            saved = self._excludePatterns
            self._excludePatterns = RegexList()
            self._excludePatterns.extend(value)
         except Exception as e:
            self._excludePatterns = saved
            raise e

   def _getExcludePatterns(self):
      """
      Property target used to get the exclude patterns list.
      """
      return self._excludePatterns

   absolutePath = property(_getAbsolutePath, _setAbsolutePath, None, doc="Absolute path of the directory to collect.")
   collectMode = property(_getCollectMode, _setCollectMode, None, doc="Overridden collect mode for this directory.")
   archiveMode = property(_getArchiveMode, _setArchiveMode, None, doc="Overridden archive mode for this directory.")
   ignoreFile = property(_getIgnoreFile, _setIgnoreFile, None, doc="Overridden ignore file name for this directory.")
   linkDepth = property(_getLinkDepth, _setLinkDepth, None, doc="Maximum at which soft links should be followed.")
   dereference = property(_getDereference, _setDereference, None, doc="Whether to dereference links that are followed.")
   recursionLevel = property(_getRecursionLevel, _setRecursionLevel, None, "Recursion level to use for recursive directory collection")
   absoluteExcludePaths = property(_getAbsoluteExcludePaths, _setAbsoluteExcludePaths, None, "List of absolute paths to exclude.")
   relativeExcludePaths = property(_getRelativeExcludePaths, _setRelativeExcludePaths, None, "List of relative paths to exclude.")
   excludePatterns = property(_getExcludePatterns, _setExcludePatterns, None, "List of regular expression patterns to exclude.")


########################################################################
# PurgeDir class definition
########################################################################

@total_ordering
class PurgeDir(object):

   """
   Class representing a Cedar Backup purge directory.

   The following restrictions exist on data in this class:

      - The absolute path must be an absolute path
      - The retain days value must be an integer >= 0.

   """

   def __init__(self, absolutePath=None, retainDays=None):
      """
      Constructor for the ``PurgeDir`` class.

      Args:
         absolutePath: Absolute path of the directory to be purged
         retainDays: Number of days content within directory should be retained

      Raises:
         ValueError: If one of the values is invalid
      """
      self._absolutePath = None
      self._retainDays = None
      self.absolutePath = absolutePath
      self.retainDays = retainDays

   def __repr__(self):
      """
      Official string representation for class instance.
      """
      return "PurgeDir(%s, %s)" % (self.absolutePath, self.retainDays)

   def __str__(self):
      """
      Informal string representation for class instance.
      """
      return self.__repr__()

   def __eq__(self, other):
      """Equals operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) == 0

   def __lt__(self, other):
      """Less-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) < 0

   def __gt__(self, other):
      """Greater-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) > 0

   def __cmp__(self, other):
      """
      Original Python 2 comparison operator.
      Args:
         other: Other object to compare to
      Returns:
          -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
      """
      if other is None:
         return 1
      if self.absolutePath != other.absolutePath:
         if str(self.absolutePath or "") < str(other.absolutePath or ""):
            return -1
         else:
            return 1
      if self.retainDays != other.retainDays:
         if int(self.retainDays or 0) < int(other.retainDays or 0):
            return -1
         else:
            return 1
      return 0

   def _setAbsolutePath(self, value):
      """
      Property target used to set the absolute path.
      The value must be an absolute path if it is not ``None``.
      It does not have to exist on disk at the time of assignment.
      Raises:
         ValueError: If the value is not an absolute path
         ValueError: If the value cannot be encoded properly
      """
      if value is not None:
         if not os.path.isabs(value):
            raise ValueError("Absolute path must, er, be an absolute path.")
      self._absolutePath = encodePath(value)

   def _getAbsolutePath(self):
      """
      Property target used to get the absolute path.
      """
      return self._absolutePath

   def _setRetainDays(self, value):
      """
      Property target used to set the retain days value.
      The value must be an integer >= 0.
      Raises:
         ValueError: If the value is not valid
      """
      if value is None:
         self._retainDays = None
      else:
         try:
            value = int(value)
         except TypeError:
            raise ValueError("Retain days value must be an integer >= 0.")
         if value < 0:
            raise ValueError("Retain days value must be an integer >= 0.")
         self._retainDays = value

   def _getRetainDays(self):
      """
      Property target used to get the absolute path.
      """
      return self._retainDays

   absolutePath = property(_getAbsolutePath, _setAbsolutePath, None, "Absolute path of directory to purge.")
   retainDays = property(_getRetainDays, _setRetainDays, None, "Number of days content within directory should be retained.")


########################################################################
# LocalPeer class definition
########################################################################

@total_ordering
class LocalPeer(object):

   """
   Class representing a Cedar Backup peer.

   The following restrictions exist on data in this class:

      - The peer name must be a non-empty string.
      - The collect directory must be an absolute path.
      - The ignore failure mode must be one of the values in ``VALID_FAILURE_MODES``.

   """

   def __init__(self, name=None, collectDir=None, ignoreFailureMode=None):
      """
      Constructor for the ``LocalPeer`` class.

      Args:
         name: Name of the peer, typically a valid hostname
         collectDir: Collect directory to stage files from on peer
         ignoreFailureMode: Ignore failure mode for peer

      Raises:
         ValueError: If one of the values is invalid
      """
      self._name = None
      self._collectDir = None
      self._ignoreFailureMode = None
      self.name = name
      self.collectDir = collectDir
      self.ignoreFailureMode = ignoreFailureMode

   def __repr__(self):
      """
      Official string representation for class instance.
      """
      return "LocalPeer(%s, %s, %s)" % (self.name, self.collectDir, self.ignoreFailureMode)

   def __str__(self):
      """
      Informal string representation for class instance.
      """
      return self.__repr__()

   def __eq__(self, other):
      """Equals operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) == 0

   def __lt__(self, other):
      """Less-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) < 0

   def __gt__(self, other):
      """Greater-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) > 0

   def __cmp__(self, other):
      """
      Original Python 2 comparison operator.
      Args:
         other: Other object to compare to
      Returns:
          -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
      """
      if other is None:
         return 1
      if self.name != other.name:
         if str(self.name or "") < str(other.name or ""):
            return -1
         else:
            return 1
      if self.collectDir != other.collectDir:
         if str(self.collectDir or "") < str(other.collectDir or ""):
            return -1
         else:
            return 1
      if self.ignoreFailureMode != other.ignoreFailureMode:
         if str(self.ignoreFailureMode or "") < str(other.ignoreFailureMode or ""):
            return -1
         else:
            return 1
      return 0

   def _setName(self, value):
      """
      Property target used to set the peer name.
      The value must be a non-empty string if it is not ``None``.
      Raises:
         ValueError: If the value is an empty string
      """
      if value is not None:
         if len(value) < 1:
            raise ValueError("The peer name must be a non-empty string.")
      self._name = value

   def _getName(self):
      """
      Property target used to get the peer name.
      """
      return self._name

   def _setCollectDir(self, value):
      """
      Property target used to set the collect directory.
      The value must be an absolute path if it is not ``None``.
      It does not have to exist on disk at the time of assignment.
      Raises:
         ValueError: If the value is not an absolute path
         ValueError: If the value cannot be encoded properly
      """
      if value is not None:
         if not os.path.isabs(value):
            raise ValueError("Collect directory must be an absolute path.")
      self._collectDir = encodePath(value)

   def _getCollectDir(self):
      """
      Property target used to get the collect directory.
      """
      return self._collectDir

   def _setIgnoreFailureMode(self, value):
      """
      Property target used to set the ignoreFailure mode.
      If not ``None``, the mode must be one of the values in ``VALID_FAILURE_MODES``.
      Raises:
         ValueError: If the value is not valid
      """
      if value is not None:
         if value not in VALID_FAILURE_MODES:
            raise ValueError("Ignore failure mode must be one of %s." % VALID_FAILURE_MODES)
      self._ignoreFailureMode = value

   def _getIgnoreFailureMode(self):
      """
      Property target used to get the ignoreFailure mode.
      """
      return self._ignoreFailureMode

   name = property(_getName, _setName, None, "Name of the peer, typically a valid hostname.")
   collectDir = property(_getCollectDir, _setCollectDir, None, "Collect directory to stage files from on peer.")
   ignoreFailureMode = property(_getIgnoreFailureMode, _setIgnoreFailureMode, None, "Ignore failure mode for peer.")


########################################################################
# RemotePeer class definition
########################################################################

@total_ordering
class RemotePeer(object):

   """
   Class representing a Cedar Backup peer.

   The following restrictions exist on data in this class:

      - The peer name must be a non-empty string.
      - The collect directory must be an absolute path.
      - The remote user must be a non-empty string.
      - The rcp command must be a non-empty string.
      - The rsh command must be a non-empty string.
      - The cback command must be a non-empty string.
      - Any managed action name must be a non-empty string matching ``ACTION_NAME_REGEX``
      - The ignore failure mode must be one of the values in ``VALID_FAILURE_MODES``.

   """

   def __init__(self, name=None, collectDir=None, remoteUser=None,
                rcpCommand=None, rshCommand=None, cbackCommand=None,
                managed=False, managedActions=None, ignoreFailureMode=None):
      """
      Constructor for the ``RemotePeer`` class.

      Args:
         name: Name of the peer, must be a valid hostname
         collectDir: Collect directory to stage files from on peer
         remoteUser: Name of backup user on remote peer
         rcpCommand: Overridden rcp-compatible copy command for peer
         rshCommand: Overridden rsh-compatible remote shell command for peer
         cbackCommand: Overridden cback-compatible command to use on remote peer
         managed: Indicates whether this is a managed peer
         managedActions: Overridden set of actions that are managed on the peer
         ignoreFailureMode: Ignore failure mode for peer

      Raises:
         ValueError: If one of the values is invalid
      """
      self._name = None
      self._collectDir = None
      self._remoteUser = None
      self._rcpCommand = None
      self._rshCommand = None
      self._cbackCommand = None
      self._managed = None
      self._managedActions = None
      self._ignoreFailureMode = None
      self.name = name
      self.collectDir = collectDir
      self.remoteUser = remoteUser
      self.rcpCommand = rcpCommand
      self.rshCommand = rshCommand
      self.cbackCommand = cbackCommand
      self.managed = managed
      self.managedActions = managedActions
      self.ignoreFailureMode = ignoreFailureMode

   def __repr__(self):
      """
      Official string representation for class instance.
      """
      return "RemotePeer(%s, %s, %s, %s, %s, %s, %s, %s, %s)" % (self.name, self.collectDir, self.remoteUser,
                                                                 self.rcpCommand, self.rshCommand, self.cbackCommand,
                                                                 self.managed, self.managedActions, self.ignoreFailureMode)

   def __str__(self):
      """
      Informal string representation for class instance.
      """
      return self.__repr__()

   def __eq__(self, other):
      """Equals operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) == 0

   def __lt__(self, other):
      """Less-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) < 0

   def __gt__(self, other):
      """Greater-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) > 0

   def __cmp__(self, other):
      """
      Original Python 2 comparison operator.
      Args:
         other: Other object to compare to
      Returns:
          -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
      """
      if other is None:
         return 1
      if self.name != other.name:
         if str(self.name or "") < str(other.name or ""):
            return -1
         else:
            return 1
      if self.collectDir != other.collectDir:
         if str(self.collectDir or "") < str(other.collectDir or ""):
            return -1
         else:
            return 1
      if self.remoteUser != other.remoteUser:
         if str(self.remoteUser or "") < str(other.remoteUser or ""):
            return -1
         else:
            return 1
      if self.rcpCommand != other.rcpCommand:
         if str(self.rcpCommand or "") < str(other.rcpCommand or ""):
            return -1
         else:
            return 1
      if self.rshCommand != other.rshCommand:
         if str(self.rshCommand or "") < str(other.rshCommand or ""):
            return -1
         else:
            return 1
      if self.cbackCommand != other.cbackCommand:
         if str(self.cbackCommand or "") < str(other.cbackCommand or ""):
            return -1
         else:
            return 1
      if self.managed != other.managed:
         if str(self.managed or "") < str(other.managed or ""):
            return -1
         else:
            return 1
      if self.managedActions != other.managedActions:
         if self.managedActions < other.managedActions:
            return -1
         else:
            return 1
      if self.ignoreFailureMode != other.ignoreFailureMode:
         if str(self.ignoreFailureMode or "") < str(other.ignoreFailureMode or ""):
            return -1
         else:
            return 1
      return 0

   def _setName(self, value):
      """
      Property target used to set the peer name.
      The value must be a non-empty string if it is not ``None``.
      Raises:
         ValueError: If the value is an empty string
      """
      if value is not None:
         if len(value) < 1:
            raise ValueError("The peer name must be a non-empty string.")
      self._name = value

   def _getName(self):
      """
      Property target used to get the peer name.
      """
      return self._name

   def _setCollectDir(self, value):
      """
      Property target used to set the collect directory.
      The value must be an absolute path if it is not ``None``.
      It does not have to exist on disk at the time of assignment.
      Raises:
         ValueError: If the value is not an absolute path
         ValueError: If the value cannot be encoded properly
      """
      if value is not None:
         if not os.path.isabs(value):
            raise ValueError("Collect directory must be an absolute path.")
      self._collectDir = encodePath(value)

   def _getCollectDir(self):
      """
      Property target used to get the collect directory.
      """
      return self._collectDir

   def _setRemoteUser(self, value):
      """
      Property target used to set the remote user.
      The value must be a non-empty string if it is not ``None``.
      Raises:
         ValueError: If the value is an empty string
      """
      if value is not None:
         if len(value) < 1:
            raise ValueError("The remote user must be a non-empty string.")
      self._remoteUser = value

   def _getRemoteUser(self):
      """
      Property target used to get the remote user.
      """
      return self._remoteUser

   def _setRcpCommand(self, value):
      """
      Property target used to set the rcp command.
      The value must be a non-empty string if it is not ``None``.
      Raises:
         ValueError: If the value is an empty string
      """
      if value is not None:
         if len(value) < 1:
            raise ValueError("The rcp command must be a non-empty string.")
      self._rcpCommand = value

   def _getRcpCommand(self):
      """
      Property target used to get the rcp command.
      """
      return self._rcpCommand

   def _setRshCommand(self, value):
      """
      Property target used to set the rsh command.
      The value must be a non-empty string if it is not ``None``.
      Raises:
         ValueError: If the value is an empty string
      """
      if value is not None:
         if len(value) < 1:
            raise ValueError("The rsh command must be a non-empty string.")
      self._rshCommand = value

   def _getRshCommand(self):
      """
      Property target used to get the rsh command.
      """
      return self._rshCommand

   def _setCbackCommand(self, value):
      """
      Property target used to set the cback command.
      The value must be a non-empty string if it is not ``None``.
      Raises:
         ValueError: If the value is an empty string
      """
      if value is not None:
         if len(value) < 1:
            raise ValueError("The cback command must be a non-empty string.")
      self._cbackCommand = value

   def _getCbackCommand(self):
      """
      Property target used to get the cback command.
      """
      return self._cbackCommand

   def _setManaged(self, value):
      """
      Property target used to set the managed flag.
      No validations, but we normalize the value to ``True`` or ``False``.
      """
      if value:
         self._managed = True
      else:
         self._managed = False

   def _getManaged(self):
      """
      Property target used to get the managed flag.
      """
      return self._managed

   def _setManagedActions(self, value):
      """
      Property target used to set the managed actions list.
      Elements do not have to exist on disk at the time of assignment.
      """
      if value is None:
         self._managedActions = None
      else:
         try:
            saved = self._managedActions
            self._managedActions = RegexMatchList(ACTION_NAME_REGEX, emptyAllowed=False, prefix="Action name")
            self._managedActions.extend(value)
         except Exception as e:
            self._managedActions = saved
            raise e

   def _getManagedActions(self):
      """
      Property target used to get the managed actions list.
      """
      return self._managedActions

   def _setIgnoreFailureMode(self, value):
      """
      Property target used to set the ignoreFailure mode.
      If not ``None``, the mode must be one of the values in ``VALID_FAILURE_MODES``.
      Raises:
         ValueError: If the value is not valid
      """
      if value is not None:
         if value not in VALID_FAILURE_MODES:
            raise ValueError("Ignore failure mode must be one of %s." % VALID_FAILURE_MODES)
      self._ignoreFailureMode = value

   def _getIgnoreFailureMode(self):
      """
      Property target used to get the ignoreFailure mode.
      """
      return self._ignoreFailureMode

   name = property(_getName, _setName, None, "Name of the peer, must be a valid hostname.")
   collectDir = property(_getCollectDir, _setCollectDir, None, "Collect directory to stage files from on peer.")
   remoteUser = property(_getRemoteUser, _setRemoteUser, None, "Name of backup user on remote peer.")
   rcpCommand = property(_getRcpCommand, _setRcpCommand, None, "Overridden rcp-compatible copy command for peer.")
   rshCommand = property(_getRshCommand, _setRshCommand, None, "Overridden rsh-compatible remote shell command for peer.")
   cbackCommand = property(_getCbackCommand, _setCbackCommand, None, "Overridden cback-compatible command to use on remote peer.")
   managed = property(_getManaged, _setManaged, None, "Indicates whether this is a managed peer.")
   managedActions = property(_getManagedActions, _setManagedActions, None, "Overridden set of actions that are managed on the peer.")
   ignoreFailureMode = property(_getIgnoreFailureMode, _setIgnoreFailureMode, None, "Ignore failure mode for peer.")


########################################################################
# ReferenceConfig class definition
########################################################################

@total_ordering
class ReferenceConfig(object):

   """
   Class representing a Cedar Backup reference configuration.

   The reference information is just used for saving off metadata about
   configuration and exists mostly for backwards-compatibility with Cedar
   Backup 1.x.

   """

   def __init__(self, author=None, revision=None, description=None, generator=None):
      """
      Constructor for the ``ReferenceConfig`` class.

      Args:
         author: Author of the configuration file
         revision: Revision of the configuration file
         description: Description of the configuration file
         generator: Tool that generated the configuration file
      """
      self._author = None
      self._revision = None
      self._description = None
      self._generator = None
      self.author = author
      self.revision = revision
      self.description = description
      self.generator = generator

   def __repr__(self):
      """
      Official string representation for class instance.
      """
      return "ReferenceConfig(%s, %s, %s, %s)" % (self.author, self.revision, self.description, self.generator)

   def __str__(self):
      """
      Informal string representation for class instance.
      """
      return self.__repr__()

   def __eq__(self, other):
      """Equals operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) == 0

   def __lt__(self, other):
      """Less-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) < 0

   def __gt__(self, other):
      """Greater-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) > 0

   def __cmp__(self, other):
      """
      Original Python 2 comparison operator.
      Args:
         other: Other object to compare to
      Returns:
          -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
      """
      if other is None:
         return 1
      if self.author != other.author:
         if str(self.author or "") < str(other.author or ""):
            return -1
         else:
            return 1
      if self.revision != other.revision:
         if str(self.revision or "") < str(other.revision or ""):
            return -1
         else:
            return 1
      if self.description != other.description:
         if str(self.description or "") < str(other.description or ""):
            return -1
         else:
            return 1
      if self.generator != other.generator:
         if str(self.generator or "") < str(other.generator or ""):
            return -1
         else:
            return 1
      return 0

   def _setAuthor(self, value):
      """
      Property target used to set the author value.
      No validations.
      """
      self._author = value

   def _getAuthor(self):
      """
      Property target used to get the author value.
      """
      return self._author

   def _setRevision(self, value):
      """
      Property target used to set the revision value.
      No validations.
      """
      self._revision = value

   def _getRevision(self):
      """
      Property target used to get the revision value.
      """
      return self._revision

   def _setDescription(self, value):
      """
      Property target used to set the description value.
      No validations.
      """
      self._description = value

   def _getDescription(self):
      """
      Property target used to get the description value.
      """
      return self._description

   def _setGenerator(self, value):
      """
      Property target used to set the generator value.
      No validations.
      """
      self._generator = value

   def _getGenerator(self):
      """
      Property target used to get the generator value.
      """
      return self._generator

   author = property(_getAuthor, _setAuthor, None, "Author of the configuration file.")
   revision = property(_getRevision, _setRevision, None, "Revision of the configuration file.")
   description = property(_getDescription, _setDescription, None, "Description of the configuration file.")
   generator = property(_getGenerator, _setGenerator, None, "Tool that generated the configuration file.")


########################################################################
# ExtensionsConfig class definition
########################################################################

@total_ordering
class ExtensionsConfig(object):

   """
   Class representing Cedar Backup extensions configuration.

   Extensions configuration is used to specify "extended actions" implemented
   by code external to Cedar Backup.  For instance, a hypothetical third party
   might write extension code to collect database repository data.  If they
   write a properly-formatted extension function, they can use the extension
   configuration to map a command-line Cedar Backup action (i.e. "database")
   to their function.

   The following restrictions exist on data in this class:

      - If set, the order mode must be one of the values in ``VALID_ORDER_MODES``
      - The actions list must be a list of ``ExtendedAction`` objects.

   """

   def __init__(self, actions=None, orderMode=None):
      """
      Constructor for the ``ExtensionsConfig`` class.
      Args:
         actions: List of extended actions
      """
      self._orderMode = None
      self._actions = None
      self.orderMode = orderMode
      self.actions = actions

   def __repr__(self):
      """
      Official string representation for class instance.
      """
      return "ExtensionsConfig(%s, %s)" % (self.orderMode, self.actions)

   def __str__(self):
      """
      Informal string representation for class instance.
      """
      return self.__repr__()

   def __eq__(self, other):
      """Equals operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) == 0

   def __lt__(self, other):
      """Less-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) < 0

   def __gt__(self, other):
      """Greater-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) > 0

   def __cmp__(self, other):
      """
      Original Python 2 comparison operator.
      Args:
         other: Other object to compare to
      Returns:
          -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
      """
      if other is None:
         return 1
      if self.orderMode != other.orderMode:
         if str(self.orderMode or "") < str(other.orderMode or ""):
            return -1
         else:
            return 1
      if self.actions != other.actions:
         if self.actions < other.actions:
            return -1
         else:
            return 1
      return 0

   def _setOrderMode(self, value):
      """
      Property target used to set the order mode.
      The value must be one of :any:`VALID_ORDER_MODES`.
      Raises:
         ValueError: If the value is not valid
      """
      if value is not None:
         if value not in VALID_ORDER_MODES:
            raise ValueError("Order mode must be one of %s." % VALID_ORDER_MODES)
      self._orderMode = value

   def _getOrderMode(self):
      """
      Property target used to get the order mode.
      """
      return self._orderMode

   def _setActions(self, value):
      """
      Property target used to set the actions list.
      Either the value must be ``None`` or each element must be an ``ExtendedAction``.
      Raises:
         ValueError: If the value is not a ``ExtendedAction``
      """
      if value is None:
         self._actions = None
      else:
         try:
            saved = self._actions
            self._actions = ObjectTypeList(ExtendedAction, "ExtendedAction")
            self._actions.extend(value)
         except Exception as e:
            self._actions = saved
            raise e

   def _getActions(self):
      """
      Property target used to get the actions list.
      """
      return self._actions

   orderMode = property(_getOrderMode, _setOrderMode, None, "Order mode for extensions, to control execution ordering.")
   actions = property(_getActions, _setActions, None, "List of extended actions.")


########################################################################
# OptionsConfig class definition
########################################################################

@total_ordering
class OptionsConfig(object):

   """
   Class representing a Cedar Backup global options configuration.

   The options section is used to store global configuration options and
   defaults that can be applied to other sections.

   The following restrictions exist on data in this class:

      - The working directory must be an absolute path.
      - The starting day must be a day of the week in English, i.e. ``"monday"``, ``"tuesday"``, etc.
      - All of the other values must be non-empty strings if they are set to something other than ``None``.
      - The overrides list must be a list of ``CommandOverride`` objects.
      - The hooks list must be a list of ``ActionHook`` objects.
      - The cback command must be a non-empty string.
      - Any managed action name must be a non-empty string matching ``ACTION_NAME_REGEX``

   """

   def __init__(self, startingDay=None, workingDir=None, backupUser=None,
                backupGroup=None, rcpCommand=None, overrides=None,
                hooks=None, rshCommand=None, cbackCommand=None,
                managedActions=None):
      """
      Constructor for the ``OptionsConfig`` class.

      Args:
         startingDay: Day that starts the week
         workingDir: Working (temporary) directory to use for backups
         backupUser: Effective user that backups should run as
         backupGroup: Effective group that backups should run as
         rcpCommand: Default rcp-compatible copy command for staging
         rshCommand: Default rsh-compatible command to use for remote shells
         cbackCommand: Default cback-compatible command to use on managed remote peers
         overrides: List of configured command path overrides, if any
         hooks: List of configured pre- and post-action hooks
         managedActions: Default set of actions that are managed on remote peers

      Raises:
         ValueError: If one of the values is invalid
      """
      self._startingDay = None
      self._workingDir = None
      self._backupUser = None
      self._backupGroup = None
      self._rcpCommand = None
      self._rshCommand = None
      self._cbackCommand = None
      self._overrides = None
      self._hooks = None
      self._managedActions = None
      self.startingDay = startingDay
      self.workingDir = workingDir
      self.backupUser = backupUser
      self.backupGroup = backupGroup
      self.rcpCommand = rcpCommand
      self.rshCommand = rshCommand
      self.cbackCommand = cbackCommand
      self.overrides = overrides
      self.hooks = hooks
      self.managedActions = managedActions

   def __repr__(self):
      """
      Official string representation for class instance.
      """
      return "OptionsConfig(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)" % (self.startingDay, self.workingDir,
                                                                        self.backupUser, self.backupGroup,
                                                                        self.rcpCommand, self.overrides,
                                                                        self.hooks, self.rshCommand,
                                                                        self.cbackCommand, self.managedActions)

   def __str__(self):
      """
      Informal string representation for class instance.
      """
      return self.__repr__()

   def __eq__(self, other):
      """Equals operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) == 0

   def __lt__(self, other):
      """Less-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) < 0

   def __gt__(self, other):
      """Greater-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) > 0

   def __cmp__(self, other):
      """
      Original Python 2 comparison operator.
      Args:
         other: Other object to compare to
      Returns:
          -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
      """
      if other is None:
         return 1
      if self.startingDay != other.startingDay:
         if str(self.startingDay or "") < str(other.startingDay or ""):
            return -1
         else:
            return 1
      if self.workingDir != other.workingDir:
         if str(self.workingDir or "") < str(other.workingDir or ""):
            return -1
         else:
            return 1
      if self.backupUser != other.backupUser:
         if str(self.backupUser or "") < str(other.backupUser or ""):
            return -1
         else:
            return 1
      if self.backupGroup != other.backupGroup:
         if str(self.backupGroup or "") < str(other.backupGroup or ""):
            return -1
         else:
            return 1
      if self.rcpCommand != other.rcpCommand:
         if str(self.rcpCommand or "") < str(other.rcpCommand or ""):
            return -1
         else:
            return 1
      if self.rshCommand != other.rshCommand:
         if str(self.rshCommand or "") < str(other.rshCommand or ""):
            return -1
         else:
            return 1
      if self.cbackCommand != other.cbackCommand:
         if str(self.cbackCommand or "") < str(other.cbackCommand or ""):
            return -1
         else:
            return 1
      if self.overrides != other.overrides:
         if self.overrides < other.overrides:
            return -1
         else:
            return 1
      if self.hooks != other.hooks:
         if self.hooks < other.hooks:
            return -1
         else:
            return 1
      if self.managedActions != other.managedActions:
         if self.managedActions < other.managedActions:
            return -1
         else:
            return 1
      return 0

   def addOverride(self, command, absolutePath):
      """
      If no override currently exists for the command, add one.
      Args:
         command: Name of command to be overridden
         absolutePath: Absolute path of the overrridden command
      """
      override = CommandOverride(command, absolutePath)
      if self.overrides is None:
         self.overrides = [ override, ]
      else:
         exists = False
         for obj in self.overrides:
            if obj.command == override.command:
               exists = True
               break
         if not exists:
            self.overrides.append(override)

   def replaceOverride(self, command, absolutePath):
      """
      If override currently exists for the command, replace it; otherwise add it.
      Args:
         command: Name of command to be overridden
         absolutePath: Absolute path of the overrridden command
      """
      override = CommandOverride(command, absolutePath)
      if self.overrides is None:
         self.overrides = [ override, ]
      else:
         exists = False
         for obj in self.overrides:
            if obj.command == override.command:
               exists = True
               obj.absolutePath = override.absolutePath
               break
         if not exists:
            self.overrides.append(override)

   def _setStartingDay(self, value):
      """
      Property target used to set the starting day.
      If it is not ``None``, the value must be a valid English day of the week,
      one of ``"monday"``, ``"tuesday"``, ``"wednesday"``, etc.
      Raises:
         ValueError: If the value is not a valid day of the week
      """
      if value is not None:
         if value not in ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday", ]:
            raise ValueError("Starting day must be an English day of the week, i.e. \"monday\".")
      self._startingDay = value

   def _getStartingDay(self):
      """
      Property target used to get the starting day.
      """
      return self._startingDay

   def _setWorkingDir(self, value):
      """
      Property target used to set the working directory.
      The value must be an absolute path if it is not ``None``.
      It does not have to exist on disk at the time of assignment.
      Raises:
         ValueError: If the value is not an absolute path
         ValueError: If the value cannot be encoded properly
      """
      if value is not None:
         if not os.path.isabs(value):
            raise ValueError("Working directory must be an absolute path.")
      self._workingDir = encodePath(value)

   def _getWorkingDir(self):
      """
      Property target used to get the working directory.
      """
      return self._workingDir

   def _setBackupUser(self, value):
      """
      Property target used to set the backup user.
      The value must be a non-empty string if it is not ``None``.
      Raises:
         ValueError: If the value is an empty string
      """
      if value is not None:
         if len(value) < 1:
            raise ValueError("Backup user must be a non-empty string.")
      self._backupUser = value

   def _getBackupUser(self):
      """
      Property target used to get the backup user.
      """
      return self._backupUser

   def _setBackupGroup(self, value):
      """
      Property target used to set the backup group.
      The value must be a non-empty string if it is not ``None``.
      Raises:
         ValueError: If the value is an empty string
      """
      if value is not None:
         if len(value) < 1:
            raise ValueError("Backup group must be a non-empty string.")
      self._backupGroup = value

   def _getBackupGroup(self):
      """
      Property target used to get the backup group.
      """
      return self._backupGroup

   def _setRcpCommand(self, value):
      """
      Property target used to set the rcp command.
      The value must be a non-empty string if it is not ``None``.
      Raises:
         ValueError: If the value is an empty string
      """
      if value is not None:
         if len(value) < 1:
            raise ValueError("The rcp command must be a non-empty string.")
      self._rcpCommand = value

   def _getRcpCommand(self):
      """
      Property target used to get the rcp command.
      """
      return self._rcpCommand

   def _setRshCommand(self, value):
      """
      Property target used to set the rsh command.
      The value must be a non-empty string if it is not ``None``.
      Raises:
         ValueError: If the value is an empty string
      """
      if value is not None:
         if len(value) < 1:
            raise ValueError("The rsh command must be a non-empty string.")
      self._rshCommand = value

   def _getRshCommand(self):
      """
      Property target used to get the rsh command.
      """
      return self._rshCommand

   def _setCbackCommand(self, value):
      """
      Property target used to set the cback command.
      The value must be a non-empty string if it is not ``None``.
      Raises:
         ValueError: If the value is an empty string
      """
      if value is not None:
         if len(value) < 1:
            raise ValueError("The cback command must be a non-empty string.")
      self._cbackCommand = value

   def _getCbackCommand(self):
      """
      Property target used to get the cback command.
      """
      return self._cbackCommand

   def _setOverrides(self, value):
      """
      Property target used to set the command path overrides list.
      Either the value must be ``None`` or each element must be a ``CommandOverride``.
      Raises:
         ValueError: If the value is not a ``CommandOverride``
      """
      if value is None:
         self._overrides = None
      else:
         try:
            saved = self._overrides
            self._overrides = ObjectTypeList(CommandOverride, "CommandOverride")
            self._overrides.extend(value)
         except Exception as e:
            self._overrides = saved
            raise e

   def _getOverrides(self):
      """
      Property target used to get the command path overrides list.
      """
      return self._overrides

   def _setHooks(self, value):
      """
      Property target used to set the pre- and post-action hooks list.
      Either the value must be ``None`` or each element must be an ``ActionHook``.
      Raises:
         ValueError: If the value is not a ``CommandOverride``
      """
      if value is None:
         self._hooks = None
      else:
         try:
            saved = self._hooks
            self._hooks = ObjectTypeList(ActionHook, "ActionHook")
            self._hooks.extend(value)
         except Exception as e:
            self._hooks = saved
            raise e

   def _getHooks(self):
      """
      Property target used to get the command path hooks list.
      """
      return self._hooks

   def _setManagedActions(self, value):
      """
      Property target used to set the managed actions list.
      Elements do not have to exist on disk at the time of assignment.
      """
      if value is None:
         self._managedActions = None
      else:
         try:
            saved = self._managedActions
            self._managedActions = RegexMatchList(ACTION_NAME_REGEX, emptyAllowed=False, prefix="Action name")
            self._managedActions.extend(value)
         except Exception as e:
            self._managedActions = saved
            raise e

   def _getManagedActions(self):
      """
      Property target used to get the managed actions list.
      """
      return self._managedActions

   startingDay = property(_getStartingDay, _setStartingDay, None, "Day that starts the week.")
   workingDir = property(_getWorkingDir, _setWorkingDir, None, "Working (temporary) directory to use for backups.")
   backupUser = property(_getBackupUser, _setBackupUser, None, "Effective user that backups should run as.")
   backupGroup = property(_getBackupGroup, _setBackupGroup, None, "Effective group that backups should run as.")
   rcpCommand = property(_getRcpCommand, _setRcpCommand, None, "Default rcp-compatible copy command for staging.")
   rshCommand = property(_getRshCommand, _setRshCommand, None, "Default rsh-compatible command to use for remote shells.")
   cbackCommand = property(_getCbackCommand, _setCbackCommand, None, "Default cback-compatible command to use on managed remote peers.")
   overrides = property(_getOverrides, _setOverrides, None, "List of configured command path overrides, if any.")
   hooks = property(_getHooks, _setHooks, None, "List of configured pre- and post-action hooks.")
   managedActions = property(_getManagedActions, _setManagedActions, None, "Default set of actions that are managed on remote peers.")


########################################################################
# PeersConfig class definition
########################################################################

@total_ordering
class PeersConfig(object):

   """
   Class representing Cedar Backup global peer configuration.

   This section contains a list of local and remote peers in a master's backup
   pool.  The section is optional.  If a master does not define this section,
   then all peers are unmanaged, and the stage configuration section must
   explicitly list any peer that is to be staged.  If this section is
   configured, then peers may be managed or unmanaged, and the stage section
   peer configuration (if any) completely overrides this configuration.

   The following restrictions exist on data in this class:

      - The list of local peers must contain only ``LocalPeer`` objects
      - The list of remote peers must contain only ``RemotePeer`` objects

   *Note:* Lists within this class are "unordered" for equality comparisons.

   """

   def __init__(self, localPeers=None, remotePeers=None):
      """
      Constructor for the ``PeersConfig`` class.

      Args:
         localPeers: List of local peers
         remotePeers: List of remote peers

      Raises:
         ValueError: If one of the values is invalid
      """
      self._localPeers = None
      self._remotePeers = None
      self.localPeers = localPeers
      self.remotePeers = remotePeers

   def __repr__(self):
      """
      Official string representation for class instance.
      """
      return "PeersConfig(%s, %s)" % (self.localPeers, self.remotePeers)

   def __str__(self):
      """
      Informal string representation for class instance.
      """
      return self.__repr__()

   def __eq__(self, other):
      """Equals operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) == 0

   def __lt__(self, other):
      """Less-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) < 0

   def __gt__(self, other):
      """Greater-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) > 0

   def __cmp__(self, other):
      """
      Original Python 2 comparison operator.
      Lists within this class are "unordered" for equality comparisons.
      Args:
         other: Other object to compare to
      Returns:
          -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
      """
      if other is None:
         return 1
      if self.localPeers != other.localPeers:
         if self.localPeers < other.localPeers:
            return -1
         else:
            return 1
      if self.remotePeers != other.remotePeers:
         if self.remotePeers < other.remotePeers:
            return -1
         else:
            return 1
      return 0

   def hasPeers(self):
      """
      Indicates whether any peers are filled into this object.
      Returns:
          Boolean true if any local or remote peers are filled in, false otherwise
      """
      return ((self.localPeers is not None and len(self.localPeers) > 0) or
              (self.remotePeers is not None and len(self.remotePeers) > 0))

   def _setLocalPeers(self, value):
      """
      Property target used to set the local peers list.
      Either the value must be ``None`` or each element must be a ``LocalPeer``.
      Raises:
         ValueError: If the value is not an absolute path
      """
      if value is None:
         self._localPeers = None
      else:
         try:
            saved = self._localPeers
            self._localPeers = ObjectTypeList(LocalPeer, "LocalPeer")
            self._localPeers.extend(value)
         except Exception as e:
            self._localPeers = saved
            raise e

   def _getLocalPeers(self):
      """
      Property target used to get the local peers list.
      """
      return self._localPeers

   def _setRemotePeers(self, value):
      """
      Property target used to set the remote peers list.
      Either the value must be ``None`` or each element must be a ``RemotePeer``.
      Raises:
         ValueError: If the value is not a ``RemotePeer``
      """
      if value is None:
         self._remotePeers = None
      else:
         try:
            saved = self._remotePeers
            self._remotePeers = ObjectTypeList(RemotePeer, "RemotePeer")
            self._remotePeers.extend(value)
         except Exception as e:
            self._remotePeers = saved
            raise e

   def _getRemotePeers(self):
      """
      Property target used to get the remote peers list.
      """
      return self._remotePeers

   localPeers = property(_getLocalPeers, _setLocalPeers, None, "List of local peers.")
   remotePeers = property(_getRemotePeers, _setRemotePeers, None, "List of remote peers.")


########################################################################
# CollectConfig class definition
########################################################################

@total_ordering
class CollectConfig(object):

   """
   Class representing a Cedar Backup collect configuration.

   The following restrictions exist on data in this class:

      - The target directory must be an absolute path.
      - The collect mode must be one of the values in :any:`VALID_COLLECT_MODES`.
      - The archive mode must be one of the values in :any:`VALID_ARCHIVE_MODES`.
      - The ignore file must be a non-empty string.
      - Each of the paths in ``absoluteExcludePaths`` must be an absolute path
      - The collect file list must be a list of ``CollectFile`` objects.
      - The collect directory list must be a list of ``CollectDir`` objects.

   For the ``absoluteExcludePaths`` list, validation is accomplished through the
   :any:`util.AbsolutePathList` list implementation that overrides common list
   methods and transparently does the absolute path validation for us.

   For the ``collectFiles`` and ``collectDirs`` list, validation is accomplished
   through the :any:`util.ObjectTypeList` list implementation that overrides common
   list methods and transparently ensures that each element has an appropriate
   type.

   *Note:* Lists within this class are "unordered" for equality comparisons.

   """

   def __init__(self, targetDir=None, collectMode=None, archiveMode=None, ignoreFile=None,
                absoluteExcludePaths=None, excludePatterns=None, collectFiles=None,
                collectDirs=None):
      """
      Constructor for the ``CollectConfig`` class.

      Args:
         targetDir: Directory to collect files into
         collectMode: Default collect mode
         archiveMode: Default archive mode for collect files
         ignoreFile: Default ignore file name
         absoluteExcludePaths: List of absolute paths to exclude
         excludePatterns: List of regular expression patterns to exclude
         collectFiles: List of collect files
         collectDirs: List of collect directories

      Raises:
         ValueError: If one of the values is invalid
      """
      self._targetDir = None
      self._collectMode = None
      self._archiveMode = None
      self._ignoreFile = None
      self._absoluteExcludePaths = None
      self._excludePatterns = None
      self._collectFiles = None
      self._collectDirs = None
      self.targetDir = targetDir
      self.collectMode = collectMode
      self.archiveMode = archiveMode
      self.ignoreFile = ignoreFile
      self.absoluteExcludePaths = absoluteExcludePaths
      self.excludePatterns = excludePatterns
      self.collectFiles = collectFiles
      self.collectDirs = collectDirs

   def __repr__(self):
      """
      Official string representation for class instance.
      """
      return "CollectConfig(%s, %s, %s, %s, %s, %s, %s, %s)" % (self.targetDir, self.collectMode, self.archiveMode,
                                                                self.ignoreFile, self.absoluteExcludePaths,
                                                                self.excludePatterns, self.collectFiles, self.collectDirs)

   def __str__(self):
      """
      Informal string representation for class instance.
      """
      return self.__repr__()

   def __eq__(self, other):
      """Equals operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) == 0

   def __lt__(self, other):
      """Less-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) < 0

   def __gt__(self, other):
      """Greater-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) > 0

   def __cmp__(self, other):
      """
      Original Python 2 comparison operator.
      Lists within this class are "unordered" for equality comparisons.
      Args:
         other: Other object to compare to
      Returns:
          -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
      """
      if other is None:
         return 1
      if self.targetDir != other.targetDir:
         if str(self.targetDir or "") < str(other.targetDir or ""):
            return -1
         else:
            return 1
      if self.collectMode != other.collectMode:
         if str(self.collectMode or "") < str(other.collectMode or ""):
            return -1
         else:
            return 1
      if self.archiveMode != other.archiveMode:
         if str(self.archiveMode or "") < str(other.archiveMode or ""):
            return -1
         else:
            return 1
      if self.ignoreFile != other.ignoreFile:
         if str(self.ignoreFile or "") < str(other.ignoreFile or ""):
            return -1
         else:
            return 1
      if self.absoluteExcludePaths != other.absoluteExcludePaths:
         if self.absoluteExcludePaths < other.absoluteExcludePaths:
            return -1
         else:
            return 1
      if self.excludePatterns != other.excludePatterns:
         if self.excludePatterns < other.excludePatterns:
            return -1
         else:
            return 1
      if self.collectFiles != other.collectFiles:
         if self.collectFiles < other.collectFiles:
            return -1
         else:
            return 1
      if self.collectDirs != other.collectDirs:
         if self.collectDirs < other.collectDirs:
            return -1
         else:
            return 1
      return 0

   def _setTargetDir(self, value):
      """
      Property target used to set the target directory.
      The value must be an absolute path if it is not ``None``.
      It does not have to exist on disk at the time of assignment.
      Raises:
         ValueError: If the value is not an absolute path
         ValueError: If the value cannot be encoded properly
      """
      if value is not None:
         if not os.path.isabs(value):
            raise ValueError("Target directory must be an absolute path.")
      self._targetDir = encodePath(value)

   def _getTargetDir(self):
      """
      Property target used to get the target directory.
      """
      return self._targetDir

   def _setCollectMode(self, value):
      """
      Property target used to set the collect mode.
      If not ``None``, the mode must be one of :any:`VALID_COLLECT_MODES`.
      Raises:
         ValueError: If the value is not valid
      """
      if value is not None:
         if value not in VALID_COLLECT_MODES:
            raise ValueError("Collect mode must be one of %s." % VALID_COLLECT_MODES)
      self._collectMode = value

   def _getCollectMode(self):
      """
      Property target used to get the collect mode.
      """
      return self._collectMode

   def _setArchiveMode(self, value):
      """
      Property target used to set the archive mode.
      If not ``None``, the mode must be one of :any:`VALID_ARCHIVE_MODES`.
      Raises:
         ValueError: If the value is not valid
      """
      if value is not None:
         if value not in VALID_ARCHIVE_MODES:
            raise ValueError("Archive mode must be one of %s." % VALID_ARCHIVE_MODES)
      self._archiveMode = value

   def _getArchiveMode(self):
      """
      Property target used to get the archive mode.
      """
      return self._archiveMode

   def _setIgnoreFile(self, value):
      """
      Property target used to set the ignore file.
      The value must be a non-empty string if it is not ``None``.
      Raises:
         ValueError: If the value is an empty string
         ValueError: If the value cannot be encoded properly
      """
      if value is not None:
         if len(value) < 1:
            raise ValueError("The ignore file must be a non-empty string.")
      self._ignoreFile = encodePath(value)

   def _getIgnoreFile(self):
      """
      Property target used to get the ignore file.
      """
      return self._ignoreFile

   def _setAbsoluteExcludePaths(self, value):
      """
      Property target used to set the absolute exclude paths list.
      Either the value must be ``None`` or each element must be an absolute path.
      Elements do not have to exist on disk at the time of assignment.
      Raises:
         ValueError: If the value is not an absolute path
      """
      if value is None:
         self._absoluteExcludePaths = None
      else:
         try:
            saved = self._absoluteExcludePaths
            self._absoluteExcludePaths = AbsolutePathList()
            self._absoluteExcludePaths.extend(value)
         except Exception as e:
            self._absoluteExcludePaths = saved
            raise e

   def _getAbsoluteExcludePaths(self):
      """
      Property target used to get the absolute exclude paths list.
      """
      return self._absoluteExcludePaths

   def _setExcludePatterns(self, value):
      """
      Property target used to set the exclude patterns list.
      """
      if value is None:
         self._excludePatterns = None
      else:
         try:
            saved = self._excludePatterns
            self._excludePatterns = RegexList()
            self._excludePatterns.extend(value)
         except Exception as e:
            self._excludePatterns = saved
            raise e

   def _getExcludePatterns(self):
      """
      Property target used to get the exclude patterns list.
      """
      return self._excludePatterns

   def _setCollectFiles(self, value):
      """
      Property target used to set the collect files list.
      Either the value must be ``None`` or each element must be a ``CollectFile``.
      Raises:
         ValueError: If the value is not a ``CollectFile``
      """
      if value is None:
         self._collectFiles = None
      else:
         try:
            saved = self._collectFiles
            self._collectFiles = ObjectTypeList(CollectFile, "CollectFile")
            self._collectFiles.extend(value)
         except Exception as e:
            self._collectFiles = saved
            raise e

   def _getCollectFiles(self):
      """
      Property target used to get the collect files list.
      """
      return self._collectFiles

   def _setCollectDirs(self, value):
      """
      Property target used to set the collect dirs list.
      Either the value must be ``None`` or each element must be a ``CollectDir``.
      Raises:
         ValueError: If the value is not a ``CollectDir``
      """
      if value is None:
         self._collectDirs = None
      else:
         try:
            saved = self._collectDirs
            self._collectDirs = ObjectTypeList(CollectDir, "CollectDir")
            self._collectDirs.extend(value)
         except Exception as e:
            self._collectDirs = saved
            raise e

   def _getCollectDirs(self):
      """
      Property target used to get the collect dirs list.
      """
      return self._collectDirs

   targetDir = property(_getTargetDir, _setTargetDir, None, "Directory to collect files into.")
   collectMode = property(_getCollectMode, _setCollectMode, None, "Default collect mode.")
   archiveMode = property(_getArchiveMode, _setArchiveMode, None, "Default archive mode for collect files.")
   ignoreFile = property(_getIgnoreFile, _setIgnoreFile, None, "Default ignore file name.")
   absoluteExcludePaths = property(_getAbsoluteExcludePaths, _setAbsoluteExcludePaths, None, "List of absolute paths to exclude.")
   excludePatterns = property(_getExcludePatterns, _setExcludePatterns, None, "List of regular expressions patterns to exclude.")
   collectFiles = property(_getCollectFiles, _setCollectFiles, None, "List of collect files.")
   collectDirs = property(_getCollectDirs, _setCollectDirs, None, "List of collect directories.")


########################################################################
# StageConfig class definition
########################################################################

@total_ordering
class StageConfig(object):

   """
   Class representing a Cedar Backup stage configuration.

   The following restrictions exist on data in this class:

      - The target directory must be an absolute path
      - The list of local peers must contain only ``LocalPeer`` objects
      - The list of remote peers must contain only ``RemotePeer`` objects

   *Note:* Lists within this class are "unordered" for equality comparisons.

   """

   def __init__(self, targetDir=None, localPeers=None, remotePeers=None):
      """
      Constructor for the ``StageConfig`` class.

      Args:
         targetDir: Directory to stage files into, by peer name
         localPeers: List of local peers
         remotePeers: List of remote peers

      Raises:
         ValueError: If one of the values is invalid
      """
      self._targetDir = None
      self._localPeers = None
      self._remotePeers = None
      self.targetDir = targetDir
      self.localPeers = localPeers
      self.remotePeers = remotePeers

   def __repr__(self):
      """
      Official string representation for class instance.
      """
      return "StageConfig(%s, %s, %s)" % (self.targetDir, self.localPeers, self.remotePeers)

   def __str__(self):
      """
      Informal string representation for class instance.
      """
      return self.__repr__()

   def __eq__(self, other):
      """Equals operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) == 0

   def __lt__(self, other):
      """Less-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) < 0

   def __gt__(self, other):
      """Greater-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) > 0

   def __cmp__(self, other):
      """
      Original Python 2 comparison operator.
      Lists within this class are "unordered" for equality comparisons.
      Args:
         other: Other object to compare to
      Returns:
          -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
      """
      if other is None:
         return 1
      if self.targetDir != other.targetDir:
         if str(self.targetDir or "") < str(other.targetDir or ""):
            return -1
         else:
            return 1
      if self.localPeers != other.localPeers:
         if self.localPeers < other.localPeers:
            return -1
         else:
            return 1
      if self.remotePeers != other.remotePeers:
         if self.remotePeers < other.remotePeers:
            return -1
         else:
            return 1
      return 0

   def hasPeers(self):
      """
      Indicates whether any peers are filled into this object.
      Returns:
          Boolean true if any local or remote peers are filled in, false otherwise
      """
      return ((self.localPeers is not None and len(self.localPeers) > 0) or
              (self.remotePeers is not None and len(self.remotePeers) > 0))

   def _setTargetDir(self, value):
      """
      Property target used to set the target directory.
      The value must be an absolute path if it is not ``None``.
      It does not have to exist on disk at the time of assignment.
      Raises:
         ValueError: If the value is not an absolute path
         ValueError: If the value cannot be encoded properly
      """
      if value is not None:
         if not os.path.isabs(value):
            raise ValueError("Target directory must be an absolute path.")
      self._targetDir = encodePath(value)

   def _getTargetDir(self):
      """
      Property target used to get the target directory.
      """
      return self._targetDir

   def _setLocalPeers(self, value):
      """
      Property target used to set the local peers list.
      Either the value must be ``None`` or each element must be a ``LocalPeer``.
      Raises:
         ValueError: If the value is not an absolute path
      """
      if value is None:
         self._localPeers = None
      else:
         try:
            saved = self._localPeers
            self._localPeers = ObjectTypeList(LocalPeer, "LocalPeer")
            self._localPeers.extend(value)
         except Exception as e:
            self._localPeers = saved
            raise e

   def _getLocalPeers(self):
      """
      Property target used to get the local peers list.
      """
      return self._localPeers

   def _setRemotePeers(self, value):
      """
      Property target used to set the remote peers list.
      Either the value must be ``None`` or each element must be a ``RemotePeer``.
      Raises:
         ValueError: If the value is not a ``RemotePeer``
      """
      if value is None:
         self._remotePeers = None
      else:
         try:
            saved = self._remotePeers
            self._remotePeers = ObjectTypeList(RemotePeer, "RemotePeer")
            self._remotePeers.extend(value)
         except Exception as e:
            self._remotePeers = saved
            raise e

   def _getRemotePeers(self):
      """
      Property target used to get the remote peers list.
      """
      return self._remotePeers

   targetDir = property(_getTargetDir, _setTargetDir, None, "Directory to stage files into, by peer name.")
   localPeers = property(_getLocalPeers, _setLocalPeers, None, "List of local peers.")
   remotePeers = property(_getRemotePeers, _setRemotePeers, None, "List of remote peers.")


########################################################################
# StoreConfig class definition
########################################################################

@total_ordering
class StoreConfig(object):

   """
   Class representing a Cedar Backup store configuration.

   The following restrictions exist on data in this class:

      - The source directory must be an absolute path.
      - The media type must be one of the values in :any:`VALID_MEDIA_TYPES`.
      - The device type must be one of the values in :any:`VALID_DEVICE_TYPES`.
      - The device path must be an absolute path.
      - The SCSI id, if provided, must be in the form specified by :any:`validateScsiId`.
      - The drive speed must be an integer >= 1
      - The blanking behavior must be a ``BlankBehavior`` object
      - The refresh media delay must be an integer >= 0
      - The eject delay must be an integer >= 0

   Note that although the blanking factor must be a positive floating point
   number, it is stored as a string. This is done so that we can losslessly go
   back and forth between XML and object representations of configuration.

   """

   def __init__(self, sourceDir=None, mediaType=None, deviceType=None,
                devicePath=None, deviceScsiId=None, driveSpeed=None,
                checkData=False, warnMidnite=False, noEject=False,
                checkMedia=False, blankBehavior=None, refreshMediaDelay=None,
                ejectDelay=None):
      """
      Constructor for the ``StoreConfig`` class.

      Args:
         sourceDir: Directory whose contents should be written to media
         mediaType: Type of the media (see notes above)
         deviceType: Type of the device (optional, see notes above)
         devicePath: Filesystem device name for writer device, i.e. ``/dev/cdrw``
         deviceScsiId: SCSI id for writer device, i.e. ``[<method>:]scsibus,target,lun``
         driveSpeed: Speed of the drive, i.e. ``2`` for 2x drive, etc
         checkData: Whether resulting image should be validated
         checkMedia: Whether media should be checked before being written to
         warnMidnite: Whether to generate warnings for crossing midnite
         noEject: Indicates that the writer device should not be ejected
         blankBehavior: Controls optimized blanking behavior
         refreshMediaDelay: Delay, in seconds, to add after refreshing media
         ejectDelay: Delay, in seconds, to add after ejecting media before closing the tray

      Raises:
         ValueError: If one of the values is invalid
      """
      self._sourceDir = None
      self._mediaType = None
      self._deviceType = None
      self._devicePath = None
      self._deviceScsiId = None
      self._driveSpeed = None
      self._checkData = None
      self._checkMedia = None
      self._warnMidnite = None
      self._noEject = None
      self._blankBehavior = None
      self._refreshMediaDelay = None
      self._ejectDelay = None
      self.sourceDir = sourceDir
      self.mediaType = mediaType
      self.deviceType = deviceType
      self.devicePath = devicePath
      self.deviceScsiId = deviceScsiId
      self.driveSpeed = driveSpeed
      self.checkData = checkData
      self.checkMedia = checkMedia
      self.warnMidnite = warnMidnite
      self.noEject = noEject
      self.blankBehavior = blankBehavior
      self.refreshMediaDelay = refreshMediaDelay
      self.ejectDelay = ejectDelay

   def __repr__(self):
      """
      Official string representation for class instance.
      """
      return "StoreConfig(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)" % (
          self.sourceDir, self.mediaType, self.deviceType,
          self.devicePath, self.deviceScsiId, self.driveSpeed,
          self.checkData, self.warnMidnite, self.noEject,
          self.checkMedia, self.blankBehavior, self.refreshMediaDelay,
          self.ejectDelay)

   def __str__(self):
      """
      Informal string representation for class instance.
      """
      return self.__repr__()

   def __eq__(self, other):
      """Equals operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) == 0

   def __lt__(self, other):
      """Less-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) < 0

   def __gt__(self, other):
      """Greater-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) > 0

   def __cmp__(self, other):
      """
      Original Python 2 comparison operator.
      Args:
         other: Other object to compare to
      Returns:
          -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
      """
      if other is None:
         return 1
      if self.sourceDir != other.sourceDir:
         if str(self.sourceDir or "") < str(other.sourceDir or ""):
            return -1
         else:
            return 1
      if self.mediaType != other.mediaType:
         if str(self.mediaType or "") < str(other.mediaType or ""):
            return -1
         else:
            return 1
      if self.deviceType != other.deviceType:
         if str(self.deviceType or "") < str(other.deviceType or ""):
            return -1
         else:
            return 1
      if self.devicePath != other.devicePath:
         if str(self.devicePath or "") < str(other.devicePath or ""):
            return -1
         else:
            return 1
      if self.deviceScsiId != other.deviceScsiId:
         if str(self.deviceScsiId or "") < str(other.deviceScsiId or ""):
            return -1
         else:
            return 1
      if self.driveSpeed != other.driveSpeed:
         if str(self.driveSpeed or "") < str(other.driveSpeed or ""):
            return -1
         else:
            return 1
      if self.checkData != other.checkData:
         if self.checkData < other.checkData:
            return -1
         else:
            return 1
      if self.checkMedia != other.checkMedia:
         if self.checkMedia < other.checkMedia:
            return -1
         else:
            return 1
      if self.warnMidnite != other.warnMidnite:
         if self.warnMidnite < other.warnMidnite:
            return -1
         else:
            return 1
      if self.noEject != other.noEject:
         if self.noEject < other.noEject:
            return -1
         else:
            return 1
      if self.blankBehavior != other.blankBehavior:
         if str(self.blankBehavior or "") < str(other.blankBehavior or ""):
            return -1
         else:
            return 1
      if self.refreshMediaDelay != other.refreshMediaDelay:
         if int(self.refreshMediaDelay or 0) < int(other.refreshMediaDelay or 0):
            return -1
         else:
            return 1
      if self.ejectDelay != other.ejectDelay:
         if int(self.ejectDelay or 0) < int(other.ejectDelay or 0):
            return -1
         else:
            return 1
      return 0

   def _setSourceDir(self, value):
      """
      Property target used to set the source directory.
      The value must be an absolute path if it is not ``None``.
      It does not have to exist on disk at the time of assignment.
      Raises:
         ValueError: If the value is not an absolute path
         ValueError: If the value cannot be encoded properly
      """
      if value is not None:
         if not os.path.isabs(value):
            raise ValueError("Source directory must be an absolute path.")
      self._sourceDir = encodePath(value)

   def _getSourceDir(self):
      """
      Property target used to get the source directory.
      """
      return self._sourceDir

   def _setMediaType(self, value):
      """
      Property target used to set the media type.
      The value must be one of :any:`VALID_MEDIA_TYPES`.
      Raises:
         ValueError: If the value is not valid
      """
      if value is not None:
         if value not in VALID_MEDIA_TYPES:
            raise ValueError("Media type must be one of %s." % VALID_MEDIA_TYPES)
      self._mediaType = value

   def _getMediaType(self):
      """
      Property target used to get the media type.
      """
      return self._mediaType

   def _setDeviceType(self, value):
      """
      Property target used to set the device type.
      The value must be one of :any:`VALID_DEVICE_TYPES`.
      Raises:
         ValueError: If the value is not valid
      """
      if value is not None:
         if value not in VALID_DEVICE_TYPES:
            raise ValueError("Device type must be one of %s." % VALID_DEVICE_TYPES)
      self._deviceType = value

   def _getDeviceType(self):
      """
      Property target used to get the device type.
      """
      return self._deviceType

   def _setDevicePath(self, value):
      """
      Property target used to set the device path.
      The value must be an absolute path if it is not ``None``.
      It does not have to exist on disk at the time of assignment.
      Raises:
         ValueError: If the value is not an absolute path
         ValueError: If the value cannot be encoded properly
      """
      if value is not None:
         if not os.path.isabs(value):
            raise ValueError("Device path must be an absolute path.")
      self._devicePath = encodePath(value)

   def _getDevicePath(self):
      """
      Property target used to get the device path.
      """
      return self._devicePath

   def _setDeviceScsiId(self, value):
      """
      Property target used to set the SCSI id
      The SCSI id must be valid per :any:`validateScsiId`.
      Raises:
         ValueError: If the value is not valid
      """
      if value is None:
         self._deviceScsiId = None
      else:
         self._deviceScsiId = validateScsiId(value)

   def _getDeviceScsiId(self):
      """
      Property target used to get the SCSI id.
      """
      return self._deviceScsiId

   def _setDriveSpeed(self, value):
      """
      Property target used to set the drive speed.
      The drive speed must be valid per :any:`validateDriveSpeed`.
      Raises:
         ValueError: If the value is not valid
      """
      self._driveSpeed = validateDriveSpeed(value)

   def _getDriveSpeed(self):
      """
      Property target used to get the drive speed.
      """
      return self._driveSpeed

   def _setCheckData(self, value):
      """
      Property target used to set the check data flag.
      No validations, but we normalize the value to ``True`` or ``False``.
      """
      if value:
         self._checkData = True
      else:
         self._checkData = False

   def _getCheckData(self):
      """
      Property target used to get the check data flag.
      """
      return self._checkData

   def _setCheckMedia(self, value):
      """
      Property target used to set the check media flag.
      No validations, but we normalize the value to ``True`` or ``False``.
      """
      if value:
         self._checkMedia = True
      else:
         self._checkMedia = False

   def _getCheckMedia(self):
      """
      Property target used to get the check media flag.
      """
      return self._checkMedia

   def _setWarnMidnite(self, value):
      """
      Property target used to set the midnite warning flag.
      No validations, but we normalize the value to ``True`` or ``False``.
      """
      if value:
         self._warnMidnite = True
      else:
         self._warnMidnite = False

   def _getWarnMidnite(self):
      """
      Property target used to get the midnite warning flag.
      """
      return self._warnMidnite

   def _setNoEject(self, value):
      """
      Property target used to set the no-eject flag.
      No validations, but we normalize the value to ``True`` or ``False``.
      """
      if value:
         self._noEject = True
      else:
         self._noEject = False

   def _getNoEject(self):
      """
      Property target used to get the no-eject flag.
      """
      return self._noEject

   def _setBlankBehavior(self, value):
      """
      Property target used to set blanking behavior configuration.
      If not ``None``, the value must be a ``BlankBehavior`` object.
      Raises:
         ValueError: If the value is not a ``BlankBehavior``
      """
      if value is None:
         self._blankBehavior = None
      else:
         if not isinstance(value, BlankBehavior):
            raise ValueError("Value must be a ``BlankBehavior`` object.")
         self._blankBehavior = value

   def _getBlankBehavior(self):
      """
      Property target used to get the blanking behavior configuration.
      """
      return self._blankBehavior

   def _setRefreshMediaDelay(self, value):
      """
      Property target used to set the refreshMediaDelay.
      The value must be an integer >= 0.
      Raises:
         ValueError: If the value is not valid
      """
      if value is None:
         self._refreshMediaDelay = None
      else:
         try:
            value = int(value)
         except TypeError:
            raise ValueError("Action refreshMediaDelay value must be an integer >= 0.")
         if value < 0:
            raise ValueError("Action refreshMediaDelay value must be an integer >= 0.")
         if value == 0:
            value = None  # normalize this out, since it's the default
         self._refreshMediaDelay = value

   def _getRefreshMediaDelay(self):
      """
      Property target used to get the action refreshMediaDelay.
      """
      return self._refreshMediaDelay

   def _setEjectDelay(self, value):
      """
      Property target used to set the ejectDelay.
      The value must be an integer >= 0.
      Raises:
         ValueError: If the value is not valid
      """
      if value is None:
         self._ejectDelay = None
      else:
         try:
            value = int(value)
         except TypeError:
            raise ValueError("Action ejectDelay value must be an integer >= 0.")
         if value < 0:
            raise ValueError("Action ejectDelay value must be an integer >= 0.")
         if value == 0:
            value = None  # normalize this out, since it's the default
         self._ejectDelay = value

   def _getEjectDelay(self):
      """
      Property target used to get the action ejectDelay.
      """
      return self._ejectDelay

   sourceDir = property(_getSourceDir, _setSourceDir, None, "Directory whose contents should be written to media.")
   mediaType = property(_getMediaType, _setMediaType, None, "Type of the media (see notes above).")
   deviceType = property(_getDeviceType, _setDeviceType, None, "Type of the device (optional, see notes above).")
   devicePath = property(_getDevicePath, _setDevicePath, None, "Filesystem device name for writer device.")
   deviceScsiId = property(_getDeviceScsiId, _setDeviceScsiId, None, "SCSI id for writer device (optional, see notes above).")
   driveSpeed = property(_getDriveSpeed, _setDriveSpeed, None, "Speed of the drive.")
   checkData = property(_getCheckData, _setCheckData, None, "Whether resulting image should be validated.")
   checkMedia = property(_getCheckMedia, _setCheckMedia, None, "Whether media should be checked before being written to.")
   warnMidnite = property(_getWarnMidnite, _setWarnMidnite, None, "Whether to generate warnings for crossing midnite.")
   noEject = property(_getNoEject, _setNoEject, None, "Indicates that the writer device should not be ejected.")
   blankBehavior = property(_getBlankBehavior, _setBlankBehavior, None, "Controls optimized blanking behavior.")
   refreshMediaDelay = property(_getRefreshMediaDelay, _setRefreshMediaDelay, None, "Delay, in seconds, to add after refreshing media.")
   ejectDelay = property(_getEjectDelay, _setEjectDelay, None, "Delay, in seconds, to add after ejecting media before closing the tray")


########################################################################
# PurgeConfig class definition
########################################################################

@total_ordering
class PurgeConfig(object):

   """
   Class representing a Cedar Backup purge configuration.

   The following restrictions exist on data in this class:

      - The purge directory list must be a list of ``PurgeDir`` objects.

   For the ``purgeDirs`` list, validation is accomplished through the
   :any:`util.ObjectTypeList` list implementation that overrides common list
   methods and transparently ensures that each element is a ``PurgeDir``.

   *Note:* Lists within this class are "unordered" for equality comparisons.

   """

   def __init__(self, purgeDirs=None):
      """
      Constructor for the ``Purge`` class.
      Args:
         purgeDirs: List of purge directories
      Raises:
         ValueError: If one of the values is invalid
      """
      self._purgeDirs = None
      self.purgeDirs = purgeDirs

   def __repr__(self):
      """
      Official string representation for class instance.
      """
      return "PurgeConfig(%s)" % self.purgeDirs

   def __str__(self):
      """
      Informal string representation for class instance.
      """
      return self.__repr__()

   def __eq__(self, other):
      """Equals operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) == 0

   def __lt__(self, other):
      """Less-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) < 0

   def __gt__(self, other):
      """Greater-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) > 0

   def __cmp__(self, other):
      """
      Original Python 2 comparison operator.
      Lists within this class are "unordered" for equality comparisons.
      Args:
         other: Other object to compare to
      Returns:
          -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
      """
      if other is None:
         return 1
      if self.purgeDirs != other.purgeDirs:
         if self.purgeDirs < other.purgeDirs:
            return -1
         else:
            return 1
      return 0

   def _setPurgeDirs(self, value):
      """
      Property target used to set the purge dirs list.
      Either the value must be ``None`` or each element must be a ``PurgeDir``.
      Raises:
         ValueError: If the value is not a ``PurgeDir``
      """
      if value is None:
         self._purgeDirs = None
      else:
         try:
            saved = self._purgeDirs
            self._purgeDirs = ObjectTypeList(PurgeDir, "PurgeDir")
            self._purgeDirs.extend(value)
         except Exception as e:
            self._purgeDirs = saved
            raise e

   def _getPurgeDirs(self):
      """
      Property target used to get the purge dirs list.
      """
      return self._purgeDirs

   purgeDirs = property(_getPurgeDirs, _setPurgeDirs, None, "List of directories to purge.")


########################################################################
# Config class definition
########################################################################

@total_ordering
class Config(object):

   ######################
   # Class documentation
   ######################

   """
   Class representing a Cedar Backup XML configuration document.

   The ``Config`` class is a Python object representation of a Cedar Backup XML
   configuration file.  It is intended to be the only Python-language interface
   to Cedar Backup configuration on disk for both Cedar Backup itself and for
   external applications.

   The object representation is two-way: XML data can be used to create a
   ``Config`` object, and then changes to the object can be propogated back to
   disk.  A ``Config`` object can even be used to create a configuration file
   from scratch programmatically.

   This class and the classes it is composed from often use Python's
   ``property`` construct to validate input and limit access to values.  Some
   validations can only be done once a document is considered "complete"
   (see module notes for more details).

   Assignments to the various instance variables must match the expected
   type, i.e. ``reference`` must be a ``ReferenceConfig``.  The internal check
   uses the built-in ``isinstance`` function, so it should be OK to use
   subclasses if you want to.

   If an instance variable is not set, its value will be ``None``.  When an
   object is initialized without using an XML document, all of the values
   will be ``None``.  Even when an object is initialized using XML, some of
   the values might be ``None`` because not every section is required.

   *Note:* Lists within this class are "unordered" for equality comparisons.

   """

   ##############
   # Constructor
   ##############

   def __init__(self, xmlData=None, xmlPath=None, validate=True):
      """
      Initializes a configuration object.

      If you initialize the object without passing either ``xmlData`` or
      ``xmlPath``, then configuration will be empty and will be invalid until it
      is filled in properly.

      No reference to the original XML data or original path is saved off by
      this class.  Once the data has been parsed (successfully or not) this
      original information is discarded.

      Unless the ``validate`` argument is ``False``, the :any:`Config.validate`
      method will be called (with its default arguments) against configuration
      after successfully parsing any passed-in XML.  Keep in mind that even if
      ``validate`` is ``False``, it might not be possible to parse the passed-in
      XML document if lower-level validations fail.

      *Note:* It is strongly suggested that the ``validate`` option always be set
      to ``True`` (the default) unless there is a specific need to read in
      invalid configuration from disk.

      Args:
         xmlData (String data): XML data representing configuration
         xmlPath (Absolute path to a file on disk): Path to an XML file on disk
         validate (Boolean true/false): Validate the document after parsing it
      Raises:
         ValueError: If both ``xmlData`` and ``xmlPath`` are passed-in
         ValueError: If the XML data in ``xmlData`` or ``xmlPath`` cannot be parsed
         ValueError: If the parsed configuration document is not valid
      """
      self._reference = None
      self._extensions = None
      self._options = None
      self._peers = None
      self._collect = None
      self._stage = None
      self._store = None
      self._purge = None
      self.reference = None
      self.extensions = None
      self.options = None
      self.peers = None
      self.collect = None
      self.stage = None
      self.store = None
      self.purge = None
      if xmlData is not None and xmlPath is not None:
         raise ValueError("Use either xmlData or xmlPath, but not both.")
      if xmlData is not None:
         self._parseXmlData(xmlData)
         if validate:
            self.validate()
      elif xmlPath is not None:
         with open(xmlPath) as f:
            xmlData = f.read()
         self._parseXmlData(xmlData)
         if validate:
            self.validate()


   #########################
   # String representations
   #########################

   def __repr__(self):
      """
      Official string representation for class instance.
      """
      return "Config(%s, %s, %s, %s, %s, %s, %s, %s)" % (self.reference, self.extensions, self.options,
                                                         self.peers, self.collect, self.stage, self.store,
                                                         self.purge)

   def __str__(self):
      """
      Informal string representation for class instance.
      """
      return self.__repr__()


   #############################
   # Standard comparison method
   #############################

   def __eq__(self, other):
      """Equals operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) == 0

   def __lt__(self, other):
      """Less-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) < 0

   def __gt__(self, other):
      """Greater-than operator, implemented in terms of original Python 2 compare operator."""
      return self.__cmp__(other) > 0

   def __cmp__(self, other):
      """
      Original Python 2 comparison operator.
      Lists within this class are "unordered" for equality comparisons.
      Args:
         other: Other object to compare to
      Returns:
          -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
      """
      if other is None:
         return 1
      if self.reference != other.reference:
         if self.reference < other.reference:
            return -1
         else:
            return 1
      if self.extensions != other.extensions:
         if self.extensions < other.extensions:
            return -1
         else:
            return 1
      if self.options != other.options:
         if self.options < other.options:
            return -1
         else:
            return 1
      if self.peers != other.peers:
         if self.peers < other.peers:
            return -1
         else:
            return 1
      if self.collect != other.collect:
         if self.collect < other.collect:
            return -1
         else:
            return 1
      if self.stage != other.stage:
         if self.stage < other.stage:
            return -1
         else:
            return 1
      if self.store != other.store:
         if self.store < other.store:
            return -1
         else:
            return 1
      if self.purge != other.purge:
         if self.purge < other.purge:
            return -1
         else:
            return 1
      return 0


   #############
   # Properties
   #############

   def _setReference(self, value):
      """
      Property target used to set the reference configuration value.
      If not ``None``, the value must be a ``ReferenceConfig`` object.
      Raises:
         ValueError: If the value is not a ``ReferenceConfig``
      """
      if value is None:
         self._reference = None
      else:
         if not isinstance(value, ReferenceConfig):
            raise ValueError("Value must be a ``ReferenceConfig`` object.")
         self._reference = value

   def _getReference(self):
      """
      Property target used to get the reference configuration value.
      """
      return self._reference

   def _setExtensions(self, value):
      """
      Property target used to set the extensions configuration value.
      If not ``None``, the value must be a ``ExtensionsConfig`` object.
      Raises:
         ValueError: If the value is not a ``ExtensionsConfig``
      """
      if value is None:
         self._extensions = None
      else:
         if not isinstance(value, ExtensionsConfig):
            raise ValueError("Value must be a ``ExtensionsConfig`` object.")
         self._extensions = value

   def _getExtensions(self):
      """
      Property target used to get the extensions configuration value.
      """
      return self._extensions

   def _setOptions(self, value):
      """
      Property target used to set the options configuration value.
      If not ``None``, the value must be an ``OptionsConfig`` object.
      Raises:
         ValueError: If the value is not a ``OptionsConfig``
      """
      if value is None:
         self._options = None
      else:
         if not isinstance(value, OptionsConfig):
            raise ValueError("Value must be a ``OptionsConfig`` object.")
         self._options = value

   def _getOptions(self):
      """
      Property target used to get the options configuration value.
      """
      return self._options

   def _setPeers(self, value):
      """
      Property target used to set the peers configuration value.
      If not ``None``, the value must be an ``PeersConfig`` object.
      Raises:
         ValueError: If the value is not a ``PeersConfig``
      """
      if value is None:
         self._peers = None
      else:
         if not isinstance(value, PeersConfig):
            raise ValueError("Value must be a ``PeersConfig`` object.")
         self._peers = value

   def _getPeers(self):
      """
      Property target used to get the peers configuration value.
      """
      return self._peers

   def _setCollect(self, value):
      """
      Property target used to set the collect configuration value.
      If not ``None``, the value must be a ``CollectConfig`` object.
      Raises:
         ValueError: If the value is not a ``CollectConfig``
      """
      if value is None:
         self._collect = None
      else:
         if not isinstance(value, CollectConfig):
            raise ValueError("Value must be a ``CollectConfig`` object.")
         self._collect = value

   def _getCollect(self):
      """
      Property target used to get the collect configuration value.
      """
      return self._collect

   def _setStage(self, value):
      """
      Property target used to set the stage configuration value.
      If not ``None``, the value must be a ``StageConfig`` object.
      Raises:
         ValueError: If the value is not a ``StageConfig``
      """
      if value is None:
         self._stage = None
      else:
         if not isinstance(value, StageConfig):
            raise ValueError("Value must be a ``StageConfig`` object.")
         self._stage = value

   def _getStage(self):
      """
      Property target used to get the stage configuration value.
      """
      return self._stage

   def _setStore(self, value):
      """
      Property target used to set the store configuration value.
      If not ``None``, the value must be a ``StoreConfig`` object.
      Raises:
         ValueError: If the value is not a ``StoreConfig``
      """
      if value is None:
         self._store = None
      else:
         if not isinstance(value, StoreConfig):
            raise ValueError("Value must be a ``StoreConfig`` object.")
         self._store = value

   def _getStore(self):
      """
      Property target used to get the store configuration value.
      """
      return self._store

   def _setPurge(self, value):
      """
      Property target used to set the purge configuration value.
      If not ``None``, the value must be a ``PurgeConfig`` object.
      Raises:
         ValueError: If the value is not a ``PurgeConfig``
      """
      if value is None:
         self._purge = None
      else:
         if not isinstance(value, PurgeConfig):
            raise ValueError("Value must be a ``PurgeConfig`` object.")
         self._purge = value

   def _getPurge(self):
      """
      Property target used to get the purge configuration value.
      """
      return self._purge

   reference = property(_getReference, _setReference, None, "Reference configuration in terms of a ``ReferenceConfig`` object.")
   extensions = property(_getExtensions, _setExtensions, None, "Extensions configuration in terms of a ``ExtensionsConfig`` object.")
   options = property(_getOptions, _setOptions, None, "Options configuration in terms of a ``OptionsConfig`` object.")
   peers = property(_getPeers, _setPeers, None, "Peers configuration in terms of a ``PeersConfig`` object.")
   collect = property(_getCollect, _setCollect, None, "Collect configuration in terms of a ``CollectConfig`` object.")
   stage = property(_getStage, _setStage, None, "Stage configuration in terms of a ``StageConfig`` object.")
   store = property(_getStore, _setStore, None, "Store configuration in terms of a ``StoreConfig`` object.")
   purge = property(_getPurge, _setPurge, None, "Purge configuration in terms of a ``PurgeConfig`` object.")


   #################
   # Public methods
   #################

   def extractXml(self, xmlPath=None, validate=True):
      """
      Extracts configuration into an XML document.

      If ``xmlPath`` is not provided, then the XML document will be returned as
      a string.  If ``xmlPath`` is provided, then the XML document will be written
      to the file and ``None`` will be returned.

      Unless the ``validate`` parameter is ``False``, the :any:`Config.validate`
      method will be called (with its default arguments) against the
      configuration before extracting the XML.  If configuration is not valid,
      then an XML document will not be extracted.

      *Note:* It is strongly suggested that the ``validate`` option always be set
      to ``True`` (the default) unless there is a specific need to write an
      invalid configuration file to disk.

      Args:
         xmlPath (Absolute path to a file): Path to an XML file to create on disk
         validate (Boolean true/false): Validate the document before extracting it
      Returns:
          XML string data or ``None`` as described above

      Raises:
         ValueError: If configuration within the object is not valid
         IOError: If there is an error writing to the file
         OSError: If there is an error writing to the file
      """
      if validate:
         self.validate()
      xmlData = self._extractXml()
      if xmlPath is not None:
         with open(xmlPath, "w") as f:
            f.write(xmlData)
         return None
      else:
         return xmlData

   def validate(self, requireOneAction=True, requireReference=False, requireExtensions=False, requireOptions=True,
                requireCollect=False, requireStage=False, requireStore=False, requirePurge=False, requirePeers=False):
      """
      Validates configuration represented by the object.

      This method encapsulates all of the validations that should apply to a
      fully "complete" document but are not already taken care of by earlier
      validations.  It also provides some extra convenience functionality which
      might be useful to some people.  The process of validation is laid out in
      the *Validation* section in the class notes (above).

      Args:
         requireOneAction: Require at least one of the collect, stage, store or purge sections
         requireReference: Require the reference section
         requireExtensions: Require the extensions section
         requireOptions: Require the options section
         requirePeers: Require the peers section
         requireCollect: Require the collect section
         requireStage: Require the stage section
         requireStore: Require the store section
         requirePurge: Require the purge section

      Raises:
         ValueError: If one of the validations fails
      """
      if requireOneAction and (self.collect, self.stage, self.store, self.purge) == (None, None, None, None):
         raise ValueError("At least one of the collect, stage, store and purge sections is required.")
      if requireReference and self.reference is None:
         raise ValueError("The reference is section is required.")
      if requireExtensions and self.extensions is None:
         raise ValueError("The extensions is section is required.")
      if requireOptions and self.options is None:
         raise ValueError("The options is section is required.")
      if requirePeers and self.peers is None:
         raise ValueError("The peers is section is required.")
      if requireCollect and self.collect is None:
         raise ValueError("The collect is section is required.")
      if requireStage and self.stage is None:
         raise ValueError("The stage is section is required.")
      if requireStore and self.store is None:
         raise ValueError("The store is section is required.")
      if requirePurge and self.purge is None:
         raise ValueError("The purge is section is required.")
      self._validateContents()


   #####################################
   # High-level methods for parsing XML
   #####################################

   def _parseXmlData(self, xmlData):
      """
      Internal method to parse an XML string into the object.

      This method parses the XML document into a DOM tree (``xmlDom``) and then
      calls individual static methods to parse each of the individual
      configuration sections.

      Most of the validation we do here has to do with whether the document can
      be parsed and whether any values which exist are valid.  We don't do much
      validation as to whether required elements actually exist unless we have
      to to make sense of the document (instead, that's the job of the
      :any:`validate` method).

      Args:
         xmlData (String data): XML data to be parsed
      Raises:
         ValueError: If the XML cannot be successfully parsed
      """
      (xmlDom, parentNode) = createInputDom(xmlData)
      self._reference = Config._parseReference(parentNode)
      self._extensions = Config._parseExtensions(parentNode)
      self._options = Config._parseOptions(parentNode)
      self._peers = Config._parsePeers(parentNode)
      self._collect = Config._parseCollect(parentNode)
      self._stage = Config._parseStage(parentNode)
      self._store = Config._parseStore(parentNode)
      self._purge = Config._parsePurge(parentNode)

   @staticmethod
   def _parseReference(parentNode):
      """
      Parses a reference configuration section.

      We read the following fields::

         author         //cb_config/reference/author
         revision       //cb_config/reference/revision
         description    //cb_config/reference/description
         generator      //cb_config/reference/generator

      Args:
         parentNode: Parent node to search beneath

      Returns:
          ``ReferenceConfig`` object or ``None`` if the section does not exist
      Raises:
         ValueError: If some filled-in value is invalid
      """
      reference = None
      sectionNode = readFirstChild(parentNode, "reference")
      if sectionNode is not None:
         reference = ReferenceConfig()
         reference.author = readString(sectionNode, "author")
         reference.revision = readString(sectionNode, "revision")
         reference.description = readString(sectionNode, "description")
         reference.generator = readString(sectionNode, "generator")
      return reference

   @staticmethod
   def _parseExtensions(parentNode):
      """
      Parses an extensions configuration section.

      We read the following fields::

         orderMode            //cb_config/extensions/order_mode

      We also read groups of the following items, one list element per item::

         name                 //cb_config/extensions/action/name
         module               //cb_config/extensions/action/module
         function             //cb_config/extensions/action/function
         index                //cb_config/extensions/action/index
         dependencies         //cb_config/extensions/action/depends

      The extended actions are parsed by :any:`_parseExtendedActions`.

      Args:
         parentNode: Parent node to search beneath

      Returns:
          ``ExtensionsConfig`` object or ``None`` if the section does not exist
      Raises:
         ValueError: If some filled-in value is invalid
      """
      extensions = None
      sectionNode = readFirstChild(parentNode, "extensions")
      if sectionNode is not None:
         extensions = ExtensionsConfig()
         extensions.orderMode = readString(sectionNode, "order_mode")
         extensions.actions = Config._parseExtendedActions(sectionNode)
      return extensions

   @staticmethod
   def _parseOptions(parentNode):
      """
      Parses a options configuration section.

      We read the following fields::

         startingDay    //cb_config/options/starting_day
         workingDir     //cb_config/options/working_dir
         backupUser     //cb_config/options/backup_user
         backupGroup    //cb_config/options/backup_group
         rcpCommand     //cb_config/options/rcp_command
         rshCommand     //cb_config/options/rsh_command
         cbackCommand   //cb_config/options/cback_command
         managedActions //cb_config/options/managed_actions

      The list of managed actions is a comma-separated list of action names.

      We also read groups of the following items, one list element per
      item::

         overrides      //cb_config/options/override
         hooks          //cb_config/options/hook

      The overrides are parsed by :any:`_parseOverrides` and the hooks are parsed
      by :any:`_parseHooks`.

      Args:
         parentNode: Parent node to search beneath

      Returns:
          ``OptionsConfig`` object or ``None`` if the section does not exist
      Raises:
         ValueError: If some filled-in value is invalid
      """
      options = None
      sectionNode = readFirstChild(parentNode, "options")
      if sectionNode is not None:
         options = OptionsConfig()
         options.startingDay = readString(sectionNode, "starting_day")
         options.workingDir = readString(sectionNode, "working_dir")
         options.backupUser = readString(sectionNode, "backup_user")
         options.backupGroup = readString(sectionNode, "backup_group")
         options.rcpCommand = readString(sectionNode, "rcp_command")
         options.rshCommand = readString(sectionNode, "rsh_command")
         options.cbackCommand = readString(sectionNode, "cback_command")
         options.overrides = Config._parseOverrides(sectionNode)
         options.hooks = Config._parseHooks(sectionNode)
         managedActions = readString(sectionNode, "managed_actions")
         options.managedActions = parseCommaSeparatedString(managedActions)
      return options

   @staticmethod
   def _parsePeers(parentNode):
      """
      Parses a peers configuration section.

      We read groups of the following items, one list element per
      item::

         localPeers     //cb_config/stage/peer
         remotePeers    //cb_config/stage/peer

      The individual peer entries are parsed by :any:`_parsePeerList`.

      Args:
         parentNode: Parent node to search beneath

      Returns:
          ``StageConfig`` object or ``None`` if the section does not exist
      Raises:
         ValueError: If some filled-in value is invalid
      """
      peers = None
      sectionNode = readFirstChild(parentNode, "peers")
      if sectionNode is not None:
         peers = PeersConfig()
         (peers.localPeers, peers.remotePeers) = Config._parsePeerList(sectionNode)
      return peers

   @staticmethod
   def _parseCollect(parentNode):
      """
      Parses a collect configuration section.

      We read the following individual fields::

         targetDir            //cb_config/collect/collect_dir
         collectMode          //cb_config/collect/collect_mode
         archiveMode          //cb_config/collect/archive_mode
         ignoreFile           //cb_config/collect/ignore_file

      We also read groups of the following items, one list element per
      item::

         absoluteExcludePaths //cb_config/collect/exclude/abs_path
         excludePatterns      //cb_config/collect/exclude/pattern
         collectFiles         //cb_config/collect/file
         collectDirs          //cb_config/collect/dir

      The exclusions are parsed by :any:`_parseExclusions`, the collect files are
      parsed by :any:`_parseCollectFiles`, and the directories are parsed by
      :any:`_parseCollectDirs`.

      Args:
         parentNode: Parent node to search beneath

      Returns:
          ``CollectConfig`` object or ``None`` if the section does not exist
      Raises:
         ValueError: If some filled-in value is invalid
      """
      collect = None
      sectionNode = readFirstChild(parentNode, "collect")
      if sectionNode is not None:
         collect = CollectConfig()
         collect.targetDir = readString(sectionNode, "collect_dir")
         collect.collectMode = readString(sectionNode, "collect_mode")
         collect.archiveMode = readString(sectionNode, "archive_mode")
         collect.ignoreFile = readString(sectionNode, "ignore_file")
         (collect.absoluteExcludePaths, unused, collect.excludePatterns) = Config._parseExclusions(sectionNode)
         collect.collectFiles = Config._parseCollectFiles(sectionNode)
         collect.collectDirs = Config._parseCollectDirs(sectionNode)
      return collect

   @staticmethod
   def _parseStage(parentNode):
      """
      Parses a stage configuration section.

      We read the following individual fields::

         targetDir      //cb_config/stage/staging_dir

      We also read groups of the following items, one list element per
      item::

         localPeers     //cb_config/stage/peer
         remotePeers    //cb_config/stage/peer

      The individual peer entries are parsed by :any:`_parsePeerList`.

      Args:
         parentNode: Parent node to search beneath

      Returns:
          ``StageConfig`` object or ``None`` if the section does not exist
      Raises:
         ValueError: If some filled-in value is invalid
      """
      stage = None
      sectionNode = readFirstChild(parentNode, "stage")
      if sectionNode is not None:
         stage = StageConfig()
         stage.targetDir = readString(sectionNode, "staging_dir")
         (stage.localPeers, stage.remotePeers) = Config._parsePeerList(sectionNode)
      return stage

   @staticmethod
   def _parseStore(parentNode):
      """
      Parses a store configuration section.

      We read the following fields::

         sourceDir         //cb_config/store/source_dir
         mediaType         //cb_config/store/media_type
         deviceType        //cb_config/store/device_type
         devicePath        //cb_config/store/target_device
         deviceScsiId      //cb_config/store/target_scsi_id
         driveSpeed        //cb_config/store/drive_speed
         checkData         //cb_config/store/check_data
         checkMedia        //cb_config/store/check_media
         warnMidnite       //cb_config/store/warn_midnite
         noEject           //cb_config/store/no_eject

      Blanking behavior configuration is parsed by the ``_parseBlankBehavior``
      method.

      Args:
         parentNode: Parent node to search beneath

      Returns:
          ``StoreConfig`` object or ``None`` if the section does not exist
      Raises:
         ValueError: If some filled-in value is invalid
      """
      store = None
      sectionNode = readFirstChild(parentNode, "store")
      if sectionNode is not None:
         store = StoreConfig()
         store.sourceDir = readString(sectionNode,  "source_dir")
         store.mediaType = readString(sectionNode,  "media_type")
         store.deviceType = readString(sectionNode,  "device_type")
         store.devicePath = readString(sectionNode,  "target_device")
         store.deviceScsiId = readString(sectionNode,  "target_scsi_id")
         store.driveSpeed = readInteger(sectionNode, "drive_speed")
         store.checkData = readBoolean(sectionNode, "check_data")
         store.checkMedia = readBoolean(sectionNode, "check_media")
         store.warnMidnite = readBoolean(sectionNode, "warn_midnite")
         store.noEject = readBoolean(sectionNode, "no_eject")
         store.blankBehavior = Config._parseBlankBehavior(sectionNode)
         store.refreshMediaDelay = readInteger(sectionNode, "refresh_media_delay")
         store.ejectDelay = readInteger(sectionNode, "eject_delay")
      return store

   @staticmethod
   def _parsePurge(parentNode):
      """
      Parses a purge configuration section.

      We read groups of the following items, one list element per
      item::

         purgeDirs     //cb_config/purge/dir

      The individual directory entries are parsed by :any:`_parsePurgeDirs`.

      Args:
         parentNode: Parent node to search beneath

      Returns:
          ``PurgeConfig`` object or ``None`` if the section does not exist
      Raises:
         ValueError: If some filled-in value is invalid
      """
      purge = None
      sectionNode = readFirstChild(parentNode, "purge")
      if sectionNode is not None:
         purge = PurgeConfig()
         purge.purgeDirs = Config._parsePurgeDirs(sectionNode)
      return purge

   @staticmethod
   def _parseExtendedActions(parentNode):
      """
      Reads extended actions data from immediately beneath the parent.

      We read the following individual fields from each extended action::

         name           name
         module         module
         function       function
         index          index
         dependencies   depends

      Dependency information is parsed by the ``_parseDependencies`` method.

      Args:
         parentNode: Parent node to search beneath

      Returns:
          List of extended actions
      Raises:
         ValueError: If the data at the location can't be read
      """
      lst = []
      for entry in readChildren(parentNode, "action"):
         if isElement(entry):
            action = ExtendedAction()
            action.name = readString(entry, "name")
            action.module = readString(entry, "module")
            action.function = readString(entry, "function")
            action.index = readInteger(entry, "index")
            action.dependencies = Config._parseDependencies(entry)
            lst.append(action)
      if lst == []:
         lst = None
      return lst

   @staticmethod
   def _parseExclusions(parentNode):
      """
      Reads exclusions data from immediately beneath the parent.

      We read groups of the following items, one list element per item::

         absolute    exclude/abs_path
         relative    exclude/rel_path
         patterns    exclude/pattern

      If there are none of some pattern (i.e. no relative path items) then
      ``None`` will be returned for that item in the tuple.

      This method can be used to parse exclusions on both the collect
      configuration level and on the collect directory level within collect
      configuration.

      Args:
         parentNode: Parent node to search beneath

      Returns:
          Tuple of (absolute, relative, patterns) exclusions
      """
      sectionNode = readFirstChild(parentNode, "exclude")
      if sectionNode is None:
         return (None, None, None)
      else:
         absolute = readStringList(sectionNode, "abs_path")
         relative = readStringList(sectionNode, "rel_path")
         patterns = readStringList(sectionNode, "pattern")
         return (absolute, relative, patterns)

   @staticmethod
   def _parseOverrides(parentNode):
      """
      Reads a list of ``CommandOverride`` objects from immediately beneath the parent.

      We read the following individual fields::

         command                 command
         absolutePath            abs_path

      Args:
         parentNode: Parent node to search beneath

      Returns:
          List of ``CommandOverride`` objects or ``None`` if none are found
      Raises:
         ValueError: If some filled-in value is invalid
      """
      lst = []
      for entry in readChildren(parentNode, "override"):
         if isElement(entry):
            override = CommandOverride()
            override.command = readString(entry, "command")
            override.absolutePath = readString(entry, "abs_path")
            lst.append(override)
      if lst == []:
         lst = None
      return lst

   @staticmethod
   def _parseHooks(parentNode):
      """
      Reads a list of ``ActionHook`` objects from immediately beneath the parent.

      We read the following individual fields::

         action                  action
         command                 command

      Args:
         parentNode: Parent node to search beneath

      Returns:
          List of ``ActionHook`` objects or ``None`` if none are found
      Raises:
         ValueError: If some filled-in value is invalid
      """
      lst = []
      for entry in readChildren(parentNode, "pre_action_hook"):
         if isElement(entry):
            hook = PreActionHook()
            hook.action = readString(entry, "action")
            hook.command = readString(entry, "command")
            lst.append(hook)
      for entry in readChildren(parentNode, "post_action_hook"):
         if isElement(entry):
            hook = PostActionHook()
            hook.action = readString(entry, "action")
            hook.command = readString(entry, "command")
            lst.append(hook)
      if lst == []:
         lst = None
      return lst

   @staticmethod
   def _parseCollectFiles(parentNode):
      """
      Reads a list of ``CollectFile`` objects from immediately beneath the parent.

      We read the following individual fields::

         absolutePath            abs_path
         collectMode             mode *or* collect_mode
         archiveMode             archive_mode

      The collect mode is a special case.  Just a ``mode`` tag is accepted, but
      we prefer ``collect_mode`` for consistency with the rest of the config
      file and to avoid confusion with the archive mode.  If both are provided,
      only ``mode`` will be used.

      Args:
         parentNode: Parent node to search beneath

      Returns:
          List of ``CollectFile`` objects or ``None`` if none are found
      Raises:
         ValueError: If some filled-in value is invalid
      """
      lst = []
      for entry in readChildren(parentNode, "file"):
         if isElement(entry):
            cfile = CollectFile()
            cfile.absolutePath = readString(entry, "abs_path")
            cfile.collectMode = readString(entry, "mode")
            if cfile.collectMode is None:
               cfile.collectMode = readString(entry, "collect_mode")
            cfile.archiveMode = readString(entry, "archive_mode")
            lst.append(cfile)
      if lst == []:
         lst = None
      return lst

   @staticmethod
   def _parseCollectDirs(parentNode):
      """
      Reads a list of ``CollectDir`` objects from immediately beneath the parent.

      We read the following individual fields::

         absolutePath            abs_path
         collectMode             mode *or* collect_mode
         archiveMode             archive_mode
         ignoreFile              ignore_file
         linkDepth               link_depth
         dereference             dereference
         recursionLevel          recursion_level

      The collect mode is a special case.  Just a ``mode`` tag is accepted for
      backwards compatibility, but we prefer ``collect_mode`` for consistency
      with the rest of the config file and to avoid confusion with the archive
      mode.  If both are provided, only ``mode`` will be used.

      We also read groups of the following items, one list element per
      item::

         absoluteExcludePaths    exclude/abs_path
         relativeExcludePaths    exclude/rel_path
         excludePatterns         exclude/pattern

      The exclusions are parsed by :any:`_parseExclusions`.

      Args:
         parentNode: Parent node to search beneath

      Returns:
          List of ``CollectDir`` objects or ``None`` if none are found
      Raises:
         ValueError: If some filled-in value is invalid
      """
      lst = []
      for entry in readChildren(parentNode, "dir"):
         if isElement(entry):
            cdir = CollectDir()
            cdir.absolutePath = readString(entry, "abs_path")
            cdir.collectMode = readString(entry, "mode")
            if cdir.collectMode is None:
               cdir.collectMode = readString(entry, "collect_mode")
            cdir.archiveMode = readString(entry, "archive_mode")
            cdir.ignoreFile = readString(entry, "ignore_file")
            cdir.linkDepth = readInteger(entry, "link_depth")
            cdir.dereference = readBoolean(entry, "dereference")
            cdir.recursionLevel = readInteger(entry, "recursion_level")
            (cdir.absoluteExcludePaths, cdir.relativeExcludePaths, cdir.excludePatterns) = Config._parseExclusions(entry)
            lst.append(cdir)
      if lst == []:
         lst = None
      return lst

   @staticmethod
   def _parsePurgeDirs(parentNode):
      """
      Reads a list of ``PurgeDir`` objects from immediately beneath the parent.

      We read the following individual fields::

         absolutePath            <baseExpr>/abs_path
         retainDays              <baseExpr>/retain_days

      Args:
         parentNode: Parent node to search beneath

      Returns:
          List of ``PurgeDir`` objects or ``None`` if none are found
      Raises:
         ValueError: If the data at the location can't be read
      """
      lst = []
      for entry in readChildren(parentNode, "dir"):
         if isElement(entry):
            cdir = PurgeDir()
            cdir.absolutePath = readString(entry, "abs_path")
            cdir.retainDays = readInteger(entry, "retain_days")
            lst.append(cdir)
      if lst == []:
         lst = None
      return lst

   @staticmethod
   def _parsePeerList(parentNode):
      """
      Reads remote and local peer data from immediately beneath the parent.

      We read the following individual fields for both remote
      and local peers::

         name        name
         collectDir  collect_dir

      We also read the following individual fields for remote peers
      only::

         remoteUser     backup_user
         rcpCommand     rcp_command
         rshCommand     rsh_command
         cbackCommand   cback_command
         managed        managed
         managedActions managed_actions

      Additionally, the value in the ``type`` field is used to determine whether
      this entry is a remote peer.  If the type is ``"remote"``, it's a remote
      peer, and if the type is ``"local"``, it's a remote peer.

      If there are none of one type of peer (i.e. no local peers) then ``None``
      will be returned for that item in the tuple.

      Args:
         parentNode: Parent node to search beneath

      Returns:
          Tuple of (local, remote) peer lists
      Raises:
         ValueError: If the data at the location can't be read
      """
      localPeers = []
      remotePeers = []
      for entry in readChildren(parentNode, "peer"):
         if isElement(entry):
            peerType = readString(entry, "type")
            if peerType == "local":
               localPeer = LocalPeer()
               localPeer.name = readString(entry, "name")
               localPeer.collectDir = readString(entry, "collect_dir")
               localPeer.ignoreFailureMode = readString(entry, "ignore_failures")
               localPeers.append(localPeer)
            elif peerType == "remote":
               remotePeer = RemotePeer()
               remotePeer.name = readString(entry, "name")
               remotePeer.collectDir = readString(entry, "collect_dir")
               remotePeer.remoteUser = readString(entry, "backup_user")
               remotePeer.rcpCommand = readString(entry, "rcp_command")
               remotePeer.rshCommand = readString(entry, "rsh_command")
               remotePeer.cbackCommand = readString(entry, "cback_command")
               remotePeer.ignoreFailureMode = readString(entry, "ignore_failures")
               remotePeer.managed = readBoolean(entry, "managed")
               managedActions = readString(entry, "managed_actions")
               remotePeer.managedActions = parseCommaSeparatedString(managedActions)
               remotePeers.append(remotePeer)
      if localPeers == []:
         localPeers = None
      if remotePeers == []:
         remotePeers = None
      return (localPeers, remotePeers)

   @staticmethod
   def _parseDependencies(parentNode):
      """
      Reads extended action dependency information from a parent node.

      We read the following individual fields::

         runBefore   depends/run_before
         runAfter    depends/run_after

      Each of these fields is a comma-separated list of action names.

      The result is placed into an ``ActionDependencies`` object.

      If the dependencies parent node does not exist, ``None`` will be returned.
      Otherwise, an ``ActionDependencies`` object will always be created, even
      if it does not contain any actual dependencies in it.

      Args:
         parentNode: Parent node to search beneath

      Returns:
          ``ActionDependencies`` object or ``None``
      Raises:
         ValueError: If the data at the location can't be read
      """
      sectionNode = readFirstChild(parentNode, "depends")
      if sectionNode is None:
         return None
      else:
         runBefore = readString(sectionNode, "run_before")
         runAfter = readString(sectionNode, "run_after")
         beforeList = parseCommaSeparatedString(runBefore)
         afterList = parseCommaSeparatedString(runAfter)
         return ActionDependencies(beforeList, afterList)

   @staticmethod
   def _parseBlankBehavior(parentNode):
      """
      Reads a single ``BlankBehavior`` object from immediately beneath the parent.

      We read the following individual fields::

         blankMode     blank_behavior/mode
         blankFactor   blank_behavior/factor

      Args:
         parentNode: Parent node to search beneath

      Returns:
          ``BlankBehavior`` object or ``None`` if none if the section is not found
      Raises:
         ValueError: If some filled-in value is invalid
      """
      blankBehavior = None
      sectionNode = readFirstChild(parentNode, "blank_behavior")
      if sectionNode is not None:
         blankBehavior = BlankBehavior()
         blankBehavior.blankMode = readString(sectionNode, "mode")
         blankBehavior.blankFactor = readString(sectionNode, "factor")
      return blankBehavior


   ########################################
   # High-level methods for generating XML
   ########################################

   def _extractXml(self):
      """
      Internal method to extract configuration into an XML string.

      This method assumes that the internal :any:`validate` method has been called
      prior to extracting the XML, if the caller cares.  No validation will be
      done internally.

      As a general rule, fields that are set to ``None`` will be extracted into
      the document as empty tags.  The same goes for container tags that are
      filled based on lists - if the list is empty or ``None``, the container
      tag will be empty.
      """
      (xmlDom, parentNode) = createOutputDom()
      Config._addReference(xmlDom, parentNode, self.reference)
      Config._addExtensions(xmlDom, parentNode, self.extensions)
      Config._addOptions(xmlDom, parentNode, self.options)
      Config._addPeers(xmlDom, parentNode, self.peers)
      Config._addCollect(xmlDom, parentNode, self.collect)
      Config._addStage(xmlDom, parentNode, self.stage)
      Config._addStore(xmlDom, parentNode, self.store)
      Config._addPurge(xmlDom, parentNode, self.purge)
      xmlData = serializeDom(xmlDom)
      xmlDom.unlink()
      return xmlData

   @staticmethod
   def _addReference(xmlDom, parentNode, referenceConfig):
      """
      Adds a <reference> configuration section as the next child of a parent.

      We add the following fields to the document::

         author         //cb_config/reference/author
         revision       //cb_config/reference/revision
         description    //cb_config/reference/description
         generator      //cb_config/reference/generator

      If ``referenceConfig`` is ``None``, then no container will be added.

      Args:
         xmlDom: DOM tree as from :any:`createOutputDom`
         parentNode: Parent that the section should be appended to
         referenceConfig: Reference configuration section to be added to the document
      """
      if referenceConfig is not None:
         sectionNode = addContainerNode(xmlDom, parentNode, "reference")
         addStringNode(xmlDom, sectionNode, "author", referenceConfig.author)
         addStringNode(xmlDom, sectionNode, "revision", referenceConfig.revision)
         addStringNode(xmlDom, sectionNode, "description", referenceConfig.description)
         addStringNode(xmlDom, sectionNode, "generator", referenceConfig.generator)

   @staticmethod
   def _addExtensions(xmlDom, parentNode, extensionsConfig):
      """
      Adds an <extensions> configuration section as the next child of a parent.

      We add the following fields to the document::

         order_mode     //cb_config/extensions/order_mode

      We also add groups of the following items, one list element per item::

         actions        //cb_config/extensions/action

      The extended action entries are added by :any:`_addExtendedAction`.

      If ``extensionsConfig`` is ``None``, then no container will be added.

      Args:
         xmlDom: DOM tree as from :any:`createOutputDom`
         parentNode: Parent that the section should be appended to
         extensionsConfig: Extensions configuration section to be added to the document
      """
      if extensionsConfig is not None:
         sectionNode = addContainerNode(xmlDom, parentNode, "extensions")
         addStringNode(xmlDom, sectionNode, "order_mode", extensionsConfig.orderMode)
         if extensionsConfig.actions is not None:
            for action in extensionsConfig.actions:
               Config._addExtendedAction(xmlDom, sectionNode, action)

   @staticmethod
   def _addOptions(xmlDom, parentNode, optionsConfig):
      """
      Adds a <options> configuration section as the next child of a parent.

      We add the following fields to the document::

         startingDay    //cb_config/options/starting_day
         workingDir     //cb_config/options/working_dir
         backupUser     //cb_config/options/backup_user
         backupGroup    //cb_config/options/backup_group
         rcpCommand     //cb_config/options/rcp_command
         rshCommand     //cb_config/options/rsh_command
         cbackCommand   //cb_config/options/cback_command
         managedActions //cb_config/options/managed_actions

      We also add groups of the following items, one list element per
      item::

         overrides      //cb_config/options/override
         hooks          //cb_config/options/pre_action_hook
         hooks          //cb_config/options/post_action_hook

      The individual override items are added by :any:`_addOverride`.  The
      individual hook items are added by :any:`_addHook`.

      If ``optionsConfig`` is ``None``, then no container will be added.

      Args:
         xmlDom: DOM tree as from :any:`createOutputDom`
         parentNode: Parent that the section should be appended to
         optionsConfig: Options configuration section to be added to the document
      """
      if optionsConfig is not None:
         sectionNode = addContainerNode(xmlDom, parentNode, "options")
         addStringNode(xmlDom, sectionNode, "starting_day", optionsConfig.startingDay)
         addStringNode(xmlDom, sectionNode, "working_dir", optionsConfig.workingDir)
         addStringNode(xmlDom, sectionNode, "backup_user", optionsConfig.backupUser)
         addStringNode(xmlDom, sectionNode, "backup_group", optionsConfig.backupGroup)
         addStringNode(xmlDom, sectionNode, "rcp_command", optionsConfig.rcpCommand)
         addStringNode(xmlDom, sectionNode, "rsh_command", optionsConfig.rshCommand)
         addStringNode(xmlDom, sectionNode, "cback_command", optionsConfig.cbackCommand)
         managedActions = Config._buildCommaSeparatedString(optionsConfig.managedActions)
         addStringNode(xmlDom, sectionNode, "managed_actions", managedActions)
         if optionsConfig.overrides is not None:
            for override in optionsConfig.overrides:
               Config._addOverride(xmlDom, sectionNode, override)
         if optionsConfig.hooks is not None:
            for hook in optionsConfig.hooks:
               Config._addHook(xmlDom, sectionNode, hook)

   @staticmethod
   def _addPeers(xmlDom, parentNode, peersConfig):
      """
      Adds a <peers> configuration section as the next child of a parent.

      We add groups of the following items, one list element per
      item::

         localPeers     //cb_config/peers/peer
         remotePeers    //cb_config/peers/peer

      The individual local and remote peer entries are added by
      :any:`_addLocalPeer` and :any:`_addRemotePeer`, respectively.

      If ``peersConfig`` is ``None``, then no container will be added.

      Args:
         xmlDom: DOM tree as from :any:`createOutputDom`
         parentNode: Parent that the section should be appended to
         peersConfig: Peers configuration section to be added to the document
      """
      if peersConfig is not None:
         sectionNode = addContainerNode(xmlDom, parentNode, "peers")
         if peersConfig.localPeers is not None:
            for localPeer in peersConfig.localPeers:
               Config._addLocalPeer(xmlDom, sectionNode, localPeer)
         if peersConfig.remotePeers is not None:
            for remotePeer in peersConfig.remotePeers:
               Config._addRemotePeer(xmlDom, sectionNode, remotePeer)

   @staticmethod
   def _addCollect(xmlDom, parentNode, collectConfig):
      """
      Adds a <collect> configuration section as the next child of a parent.

      We add the following fields to the document::

         targetDir            //cb_config/collect/collect_dir
         collectMode          //cb_config/collect/collect_mode
         archiveMode          //cb_config/collect/archive_mode
         ignoreFile           //cb_config/collect/ignore_file

      We also add groups of the following items, one list element per
      item::

         absoluteExcludePaths //cb_config/collect/exclude/abs_path
         excludePatterns      //cb_config/collect/exclude/pattern
         collectFiles         //cb_config/collect/file
         collectDirs          //cb_config/collect/dir

      The individual collect files are added by :any:`_addCollectFile` and
      individual collect directories are added by :any:`_addCollectDir`.

      If ``collectConfig`` is ``None``, then no container will be added.

      Args:
         xmlDom: DOM tree as from :any:`createOutputDom`
         parentNode: Parent that the section should be appended to
         collectConfig: Collect configuration section to be added to the document
      """
      if collectConfig is not None:
         sectionNode = addContainerNode(xmlDom, parentNode, "collect")
         addStringNode(xmlDom, sectionNode, "collect_dir", collectConfig.targetDir)
         addStringNode(xmlDom, sectionNode, "collect_mode", collectConfig.collectMode)
         addStringNode(xmlDom, sectionNode, "archive_mode", collectConfig.archiveMode)
         addStringNode(xmlDom, sectionNode, "ignore_file", collectConfig.ignoreFile)
         if ((collectConfig.absoluteExcludePaths is not None and collectConfig.absoluteExcludePaths != []) or
             (collectConfig.excludePatterns is not None and collectConfig.excludePatterns != [])):
            excludeNode = addContainerNode(xmlDom, sectionNode, "exclude")
            if collectConfig.absoluteExcludePaths is not None:
               for absolutePath in collectConfig.absoluteExcludePaths:
                  addStringNode(xmlDom, excludeNode, "abs_path", absolutePath)
            if collectConfig.excludePatterns is not None:
               for pattern in collectConfig.excludePatterns:
                  addStringNode(xmlDom, excludeNode, "pattern", pattern)
         if collectConfig.collectFiles is not None:
            for collectFile in collectConfig.collectFiles:
               Config._addCollectFile(xmlDom, sectionNode, collectFile)
         if collectConfig.collectDirs is not None:
            for collectDir in collectConfig.collectDirs:
               Config._addCollectDir(xmlDom, sectionNode, collectDir)

   @staticmethod
   def _addStage(xmlDom, parentNode, stageConfig):
      """
      Adds a <stage> configuration section as the next child of a parent.

      We add the following fields to the document::

         targetDir      //cb_config/stage/staging_dir

      We also add groups of the following items, one list element per
      item::

         localPeers     //cb_config/stage/peer
         remotePeers    //cb_config/stage/peer

      The individual local and remote peer entries are added by
      :any:`_addLocalPeer` and :any:`_addRemotePeer`, respectively.

      If ``stageConfig`` is ``None``, then no container will be added.

      Args:
         xmlDom: DOM tree as from :any:`createOutputDom`
         parentNode: Parent that the section should be appended to
         stageConfig: Stage configuration section to be added to the document
      """
      if stageConfig is not None:
         sectionNode = addContainerNode(xmlDom, parentNode, "stage")
         addStringNode(xmlDom, sectionNode, "staging_dir", stageConfig.targetDir)
         if stageConfig.localPeers is not None:
            for localPeer in stageConfig.localPeers:
               Config._addLocalPeer(xmlDom, sectionNode, localPeer)
         if stageConfig.remotePeers is not None:
            for remotePeer in stageConfig.remotePeers:
               Config._addRemotePeer(xmlDom, sectionNode, remotePeer)

   @staticmethod
   def _addStore(xmlDom, parentNode, storeConfig):
      """
      Adds a <store> configuration section as the next child of a parent.

      We add the following fields to the document::

         sourceDir         //cb_config/store/source_dir
         mediaType         //cb_config/store/media_type
         deviceType        //cb_config/store/device_type
         devicePath        //cb_config/store/target_device
         deviceScsiId      //cb_config/store/target_scsi_id
         driveSpeed        //cb_config/store/drive_speed
         checkData         //cb_config/store/check_data
         checkMedia        //cb_config/store/check_media
         warnMidnite       //cb_config/store/warn_midnite
         noEject           //cb_config/store/no_eject
         refreshMediaDelay //cb_config/store/refresh_media_delay
         ejectDelay        //cb_config/store/eject_delay

      Blanking behavior configuration is added by the :any:`_addBlankBehavior`
      method.

      If ``storeConfig`` is ``None``, then no container will be added.

      Args:
         xmlDom: DOM tree as from :any:`createOutputDom`
         parentNode: Parent that the section should be appended to
         storeConfig: Store configuration section to be added to the document
      """
      if storeConfig is not None:
         sectionNode = addContainerNode(xmlDom, parentNode, "store")
         addStringNode(xmlDom, sectionNode, "source_dir", storeConfig.sourceDir)
         addStringNode(xmlDom, sectionNode, "media_type", storeConfig.mediaType)
         addStringNode(xmlDom, sectionNode, "device_type", storeConfig.deviceType)
         addStringNode(xmlDom, sectionNode, "target_device", storeConfig.devicePath)
         addStringNode(xmlDom, sectionNode, "target_scsi_id", storeConfig.deviceScsiId)
         addIntegerNode(xmlDom, sectionNode, "drive_speed", storeConfig.driveSpeed)
         addBooleanNode(xmlDom, sectionNode, "check_data", storeConfig.checkData)
         addBooleanNode(xmlDom, sectionNode, "check_media", storeConfig.checkMedia)
         addBooleanNode(xmlDom, sectionNode, "warn_midnite", storeConfig.warnMidnite)
         addBooleanNode(xmlDom, sectionNode, "no_eject", storeConfig.noEject)
         addIntegerNode(xmlDom, sectionNode, "refresh_media_delay", storeConfig.refreshMediaDelay)
         addIntegerNode(xmlDom, sectionNode, "eject_delay", storeConfig.ejectDelay)
         Config._addBlankBehavior(xmlDom, sectionNode, storeConfig.blankBehavior)

   @staticmethod
   def _addPurge(xmlDom, parentNode, purgeConfig):
      """
      Adds a <purge> configuration section as the next child of a parent.

      We add the following fields to the document::

         purgeDirs     //cb_config/purge/dir

      The individual directory entries are added by :any:`_addPurgeDir`.

      If ``purgeConfig`` is ``None``, then no container will be added.

      Args:
         xmlDom: DOM tree as from :any:`createOutputDom`
         parentNode: Parent that the section should be appended to
         purgeConfig: Purge configuration section to be added to the document
      """
      if purgeConfig is not None:
         sectionNode = addContainerNode(xmlDom, parentNode, "purge")
         if purgeConfig.purgeDirs is not None:
            for purgeDir in purgeConfig.purgeDirs:
               Config._addPurgeDir(xmlDom, sectionNode, purgeDir)

   @staticmethod
   def _addExtendedAction(xmlDom, parentNode, action):
      """
      Adds an extended action container as the next child of a parent.

      We add the following fields to the document::

         name           action/name
         module         action/module
         function       action/function
         index          action/index
         dependencies   action/depends

      Dependencies are added by the :any:`_addDependencies` method.

      The <action> node itself is created as the next child of the parent node.
      This method only adds one action node.  The parent must loop for each action
      in the ``ExtensionsConfig`` object.

      If ``action`` is ``None``, this method call will be a no-op.

      Args:
         xmlDom: DOM tree as from :any:`createOutputDom`
         parentNode: Parent that the section should be appended to
         action: Purge directory to be added to the document
      """
      if action is not None:
         sectionNode = addContainerNode(xmlDom, parentNode, "action")
         addStringNode(xmlDom, sectionNode, "name", action.name)
         addStringNode(xmlDom, sectionNode, "module", action.module)
         addStringNode(xmlDom, sectionNode, "function", action.function)
         addIntegerNode(xmlDom, sectionNode, "index", action.index)
         Config._addDependencies(xmlDom, sectionNode, action.dependencies)

   @staticmethod
   def _addOverride(xmlDom, parentNode, override):
      """
      Adds a command override container as the next child of a parent.

      We add the following fields to the document::

         command                 override/command
         absolutePath            override/abs_path

      The <override> node itself is created as the next child of the parent
      node.  This method only adds one override node.  The parent must loop for
      each override in the ``OptionsConfig`` object.

      If ``override`` is ``None``, this method call will be a no-op.

      Args:
         xmlDom: DOM tree as from :any:`createOutputDom`
         parentNode: Parent that the section should be appended to
         override: Command override to be added to the document
      """
      if override is not None:
         sectionNode = addContainerNode(xmlDom, parentNode, "override")
         addStringNode(xmlDom, sectionNode, "command", override.command)
         addStringNode(xmlDom, sectionNode, "abs_path", override.absolutePath)

   @staticmethod
   def _addHook(xmlDom, parentNode, hook):
      """
      Adds an action hook container as the next child of a parent.

      The behavior varies depending on the value of the ``before`` and ``after``
      flags on the hook.  If the ``before`` flag is set, it's a pre-action hook,
      and we'll add the following fields::

         action                  pre_action_hook/action
         command                 pre_action_hook/command

      If the ``after`` flag is set, it's a post-action hook, and we'll add the
      following fields::

         action                  post_action_hook/action
         command                 post_action_hook/command

      The <pre_action_hook> or <post_action_hook> node itself is created as the
      next child of the parent node.  This method only adds one hook node.  The
      parent must loop for each hook in the ``OptionsConfig`` object.

      If ``hook`` is ``None``, this method call will be a no-op.

      Args:
         xmlDom: DOM tree as from :any:`createOutputDom`
         parentNode: Parent that the section should be appended to
         hook: Command hook to be added to the document
      """
      if hook is not None:
         if hook.before:
            sectionNode = addContainerNode(xmlDom, parentNode, "pre_action_hook")
         else:
            sectionNode = addContainerNode(xmlDom, parentNode, "post_action_hook")
         addStringNode(xmlDom, sectionNode, "action", hook.action)
         addStringNode(xmlDom, sectionNode, "command", hook.command)

   @staticmethod
   def _addCollectFile(xmlDom, parentNode, collectFile):
      """
      Adds a collect file container as the next child of a parent.

      We add the following fields to the document::

         absolutePath            dir/abs_path
         collectMode             dir/collect_mode
         archiveMode             dir/archive_mode

      Note that for consistency with collect directory handling we'll only emit
      the preferred ``collect_mode`` tag.

      The <file> node itself is created as the next child of the parent node.
      This method only adds one collect file node.  The parent must loop
      for each collect file in the ``CollectConfig`` object.

      If ``collectFile`` is ``None``, this method call will be a no-op.

      Args:
         xmlDom: DOM tree as from :any:`createOutputDom`
         parentNode: Parent that the section should be appended to
         collectFile: Collect file to be added to the document
      """
      if collectFile is not None:
         sectionNode = addContainerNode(xmlDom, parentNode, "file")
         addStringNode(xmlDom, sectionNode, "abs_path", collectFile.absolutePath)
         addStringNode(xmlDom, sectionNode, "collect_mode", collectFile.collectMode)
         addStringNode(xmlDom, sectionNode, "archive_mode", collectFile.archiveMode)

   @staticmethod
   def _addCollectDir(xmlDom, parentNode, collectDir):
      """
      Adds a collect directory container as the next child of a parent.

      We add the following fields to the document::

         absolutePath            dir/abs_path
         collectMode             dir/collect_mode
         archiveMode             dir/archive_mode
         ignoreFile              dir/ignore_file
         linkDepth               dir/link_depth
         dereference             dir/dereference
         recursionLevel          dir/recursion_level

      Note that an original XML document might have listed the collect mode
      using the ``mode`` tag, since we accept both ``collect_mode`` and ``mode``.
      However, here we'll only emit the preferred ``collect_mode`` tag.

      We also add groups of the following items, one list element per item::

         absoluteExcludePaths    dir/exclude/abs_path
         relativeExcludePaths    dir/exclude/rel_path
         excludePatterns         dir/exclude/pattern

      The <dir> node itself is created as the next child of the parent node.
      This method only adds one collect directory node.  The parent must loop
      for each collect directory in the ``CollectConfig`` object.

      If ``collectDir`` is ``None``, this method call will be a no-op.

      Args:
         xmlDom: DOM tree as from :any:`createOutputDom`
         parentNode: Parent that the section should be appended to
         collectDir: Collect directory to be added to the document
      """
      if collectDir is not None:
         sectionNode = addContainerNode(xmlDom, parentNode, "dir")
         addStringNode(xmlDom, sectionNode, "abs_path", collectDir.absolutePath)
         addStringNode(xmlDom, sectionNode, "collect_mode", collectDir.collectMode)
         addStringNode(xmlDom, sectionNode, "archive_mode", collectDir.archiveMode)
         addStringNode(xmlDom, sectionNode, "ignore_file", collectDir.ignoreFile)
         addIntegerNode(xmlDom, sectionNode, "link_depth", collectDir.linkDepth)
         addBooleanNode(xmlDom, sectionNode, "dereference", collectDir.dereference)
         addIntegerNode(xmlDom, sectionNode, "recursion_level", collectDir.recursionLevel)
         if ((collectDir.absoluteExcludePaths is not None and collectDir.absoluteExcludePaths != []) or
             (collectDir.relativeExcludePaths is not None and collectDir.relativeExcludePaths != []) or
             (collectDir.excludePatterns is not None and collectDir.excludePatterns != [])):
            excludeNode = addContainerNode(xmlDom, sectionNode, "exclude")
            if collectDir.absoluteExcludePaths is not None:
               for absolutePath in collectDir.absoluteExcludePaths:
                  addStringNode(xmlDom, excludeNode, "abs_path", absolutePath)
            if collectDir.relativeExcludePaths is not None:
               for relativePath in collectDir.relativeExcludePaths:
                  addStringNode(xmlDom, excludeNode, "rel_path", relativePath)
            if collectDir.excludePatterns is not None:
               for pattern in collectDir.excludePatterns:
                  addStringNode(xmlDom, excludeNode, "pattern", pattern)

   @staticmethod
   def _addLocalPeer(xmlDom, parentNode, localPeer):
      """
      Adds a local peer container as the next child of a parent.

      We add the following fields to the document::

         name                peer/name
         collectDir          peer/collect_dir
         ignoreFailureMode   peer/ignore_failures

      Additionally, ``peer/type`` is filled in with ``"local"``, since this is a
      local peer.

      The <peer> node itself is created as the next child of the parent node.
      This method only adds one peer node.  The parent must loop for each peer
      in the ``StageConfig`` object.

      If ``localPeer`` is ``None``, this method call will be a no-op.

      Args:
         xmlDom: DOM tree as from :any:`createOutputDom`
         parentNode: Parent that the section should be appended to
         localPeer: Purge directory to be added to the document
      """
      if localPeer is not None:
         sectionNode = addContainerNode(xmlDom, parentNode, "peer")
         addStringNode(xmlDom, sectionNode, "name", localPeer.name)
         addStringNode(xmlDom, sectionNode, "type", "local")
         addStringNode(xmlDom, sectionNode, "collect_dir", localPeer.collectDir)
         addStringNode(xmlDom, sectionNode, "ignore_failures", localPeer.ignoreFailureMode)

   @staticmethod
   def _addRemotePeer(xmlDom, parentNode, remotePeer):
      """
      Adds a remote peer container as the next child of a parent.

      We add the following fields to the document::

         name                peer/name
         collectDir          peer/collect_dir
         remoteUser          peer/backup_user
         rcpCommand          peer/rcp_command
         rcpCommand          peer/rcp_command
         rshCommand          peer/rsh_command
         cbackCommand        peer/cback_command
         ignoreFailureMode   peer/ignore_failures
         managed             peer/managed
         managedActions      peer/managed_actions

      Additionally, ``peer/type`` is filled in with ``"remote"``, since this is a
      remote peer.

      The <peer> node itself is created as the next child of the parent node.
      This method only adds one peer node.  The parent must loop for each peer
      in the ``StageConfig`` object.

      If ``remotePeer`` is ``None``, this method call will be a no-op.

      Args:
         xmlDom: DOM tree as from :any:`createOutputDom`
         parentNode: Parent that the section should be appended to
         remotePeer: Purge directory to be added to the document
      """
      if remotePeer is not None:
         sectionNode = addContainerNode(xmlDom, parentNode, "peer")
         addStringNode(xmlDom, sectionNode, "name", remotePeer.name)
         addStringNode(xmlDom, sectionNode, "type", "remote")
         addStringNode(xmlDom, sectionNode, "collect_dir", remotePeer.collectDir)
         addStringNode(xmlDom, sectionNode, "backup_user", remotePeer.remoteUser)
         addStringNode(xmlDom, sectionNode, "rcp_command", remotePeer.rcpCommand)
         addStringNode(xmlDom, sectionNode, "rsh_command", remotePeer.rshCommand)
         addStringNode(xmlDom, sectionNode, "cback_command", remotePeer.cbackCommand)
         addStringNode(xmlDom, sectionNode, "ignore_failures", remotePeer.ignoreFailureMode)
         addBooleanNode(xmlDom, sectionNode, "managed", remotePeer.managed)
         managedActions = Config._buildCommaSeparatedString(remotePeer.managedActions)
         addStringNode(xmlDom, sectionNode, "managed_actions", managedActions)

   @staticmethod
   def _addPurgeDir(xmlDom, parentNode, purgeDir):
      """
      Adds a purge directory container as the next child of a parent.

      We add the following fields to the document::

         absolutePath            dir/abs_path
         retainDays              dir/retain_days

      The <dir> node itself is created as the next child of the parent node.
      This method only adds one purge directory node.  The parent must loop for
      each purge directory in the ``PurgeConfig`` object.

      If ``purgeDir`` is ``None``, this method call will be a no-op.

      Args:
         xmlDom: DOM tree as from :any:`createOutputDom`
         parentNode: Parent that the section should be appended to
         purgeDir: Purge directory to be added to the document
      """
      if purgeDir is not None:
         sectionNode = addContainerNode(xmlDom, parentNode, "dir")
         addStringNode(xmlDom, sectionNode, "abs_path", purgeDir.absolutePath)
         addIntegerNode(xmlDom, sectionNode, "retain_days", purgeDir.retainDays)

   @staticmethod
   def _addDependencies(xmlDom, parentNode, dependencies):
      """
      Adds a extended action dependencies to parent node.

      We add the following fields to the document::

         runBefore      depends/run_before
         runAfter       depends/run_after

      If ``dependencies`` is ``None``, this method call will be a no-op.

      Args:
         xmlDom: DOM tree as from :any:`createOutputDom`
         parentNode: Parent that the section should be appended to
         dependencies: ``ActionDependencies`` object to be added to the document
      """
      if dependencies is not None:
         sectionNode = addContainerNode(xmlDom, parentNode, "depends")
         runBefore = Config._buildCommaSeparatedString(dependencies.beforeList)
         runAfter = Config._buildCommaSeparatedString(dependencies.afterList)
         addStringNode(xmlDom, sectionNode, "run_before", runBefore)
         addStringNode(xmlDom, sectionNode, "run_after", runAfter)

   @staticmethod
   def _buildCommaSeparatedString(valueList):
      """
      Creates a comma-separated string from a list of values.

      As a special case, if ``valueList`` is ``None``, then ``None`` will be
      returned.

      Args:
         valueList: List of values to be placed into a string

      Returns:
          Values from valueList as a comma-separated string
      """
      if valueList is None:
         return None
      return ",".join(valueList)

   @staticmethod
   def _addBlankBehavior(xmlDom, parentNode, blankBehavior):
      """
      Adds a blanking behavior container as the next child of a parent.

      We add the following fields to the document::

         blankMode    blank_behavior/mode
         blankFactor  blank_behavior/factor

      The <blank_behavior> node itself is created as the next child of the
      parent node.

      If ``blankBehavior`` is ``None``, this method call will be a no-op.

      Args:
         xmlDom: DOM tree as from :any:`createOutputDom`
         parentNode: Parent that the section should be appended to
         blankBehavior: Blanking behavior to be added to the document
      """
      if blankBehavior is not None:
         sectionNode = addContainerNode(xmlDom, parentNode, "blank_behavior")
         addStringNode(xmlDom, sectionNode, "mode", blankBehavior.blankMode)
         addStringNode(xmlDom, sectionNode, "factor", blankBehavior.blankFactor)


   #################################################
   # High-level methods used for validating content
   #################################################

   def _validateContents(self):
      """
      Validates configuration contents per rules discussed in module
      documentation.

      This is the second pass at validation.  It ensures that any filled-in
      section contains valid data.  Any sections which is not set to ``None`` is
      validated per the rules for that section, laid out in the module
      documentation (above).

      Raises:
         ValueError: If configuration is invalid
      """
      self._validateReference()
      self._validateExtensions()
      self._validateOptions()
      self._validatePeers()
      self._validateCollect()
      self._validateStage()
      self._validateStore()
      self._validatePurge()

   def _validateReference(self):
      """
      Validates reference configuration.
      There are currently no reference-related validations.
      Raises:
         ValueError: If reference configuration is invalid
      """
      pass

   def _validateExtensions(self):
      """
      Validates extensions configuration.

      The list of actions may be either ``None`` or an empty list ``[]`` if
      desired.  Each extended action must include a name, a module, and a
      function.

      Then, if the order mode is None or "index", an index is required; and if
      the order mode is "dependency", dependency information is required.

      Raises:
         ValueError: If reference configuration is invalid
      """
      if self.extensions is not None:
         if self.extensions.actions is not None:
            names = []
            for action in self.extensions.actions:
               if action.name is None:
                  raise ValueError("Each extended action must set a name.")
               names.append(action.name)
               if action.module is None:
                  raise ValueError("Each extended action must set a module.")
               if action.function is None:
                  raise ValueError("Each extended action must set a function.")
               if self.extensions.orderMode is None or self.extensions.orderMode == "index":
                  if action.index is None:
                     raise ValueError("Each extended action must set an index, based on order mode.")
               elif self.extensions.orderMode == "dependency":
                  if action.dependencies is None:
                     raise ValueError("Each extended action must set dependency information, based on order mode.")
            checkUnique("Duplicate extension names exist:", names)

   def _validateOptions(self):
      """
      Validates options configuration.

      All fields must be filled in except the rsh command.  The rcp and rsh
      commands are used as default values for all remote peers.  Remote peers
      can also rely on the backup user as the default remote user name if they
      choose.

      Raises:
         ValueError: If reference configuration is invalid
      """
      if self.options is not None:
         if self.options.startingDay is None:
            raise ValueError("Options section starting day must be filled in.")
         if self.options.workingDir is None:
            raise ValueError("Options section working directory must be filled in.")
         if self.options.backupUser is None:
            raise ValueError("Options section backup user must be filled in.")
         if self.options.backupGroup is None:
            raise ValueError("Options section backup group must be filled in.")
         if self.options.rcpCommand is None:
            raise ValueError("Options section remote copy command must be filled in.")

   def _validatePeers(self):
      """
      Validates peers configuration per rules in :any:`_validatePeerList`.
      Raises:
         ValueError: If peers configuration is invalid
      """
      if self.peers is not None:
         self._validatePeerList(self.peers.localPeers, self.peers.remotePeers)

   def _validateCollect(self):
      """
      Validates collect configuration.

      The target directory must be filled in.  The collect mode, archive mode,
      ignore file, and recursion level are all optional.  The list of absolute
      paths to exclude and patterns to exclude may be either ``None`` or an
      empty list ``[]`` if desired.

      Each collect directory entry must contain an absolute path to collect,
      and then must either be able to take collect mode, archive mode and
      ignore file configuration from the parent ``CollectConfig`` object, or
      must set each value on its own.  The list of absolute paths to exclude,
      relative paths to exclude and patterns to exclude may be either ``None``
      or an empty list ``[]`` if desired.  Any list of absolute paths to exclude
      or patterns to exclude will be combined with the same list in the
      ``CollectConfig`` object to make the complete list for a given directory.

      Raises:
         ValueError: If collect configuration is invalid
      """
      if self.collect is not None:
         if self.collect.targetDir is None:
            raise ValueError("Collect section target directory must be filled in.")
         if self.collect.collectFiles is not None:
            for collectFile in self.collect.collectFiles:
               if collectFile.absolutePath is None:
                  raise ValueError("Each collect file must set an absolute path.")
               if self.collect.collectMode is None and collectFile.collectMode is None:
                  raise ValueError("Collect mode must either be set in parent collect section or individual collect file.")
               if self.collect.archiveMode is None and collectFile.archiveMode is None:
                  raise ValueError("Archive mode must either be set in parent collect section or individual collect file.")
         if self.collect.collectDirs is not None:
            for collectDir in self.collect.collectDirs:
               if collectDir.absolutePath is None:
                  raise ValueError("Each collect directory must set an absolute path.")
               if self.collect.collectMode is None and collectDir.collectMode is None:
                  raise ValueError("Collect mode must either be set in parent collect section or individual collect directory.")
               if self.collect.archiveMode is None and collectDir.archiveMode is None:
                  raise ValueError("Archive mode must either be set in parent collect section or individual collect directory.")
               if self.collect.ignoreFile is None and collectDir.ignoreFile is None:
                  raise ValueError("Ignore file must either be set in parent collect section or individual collect directory.")
               if (collectDir.linkDepth is None or collectDir.linkDepth < 1) and collectDir.dereference:
                  raise ValueError("Dereference flag is only valid when a non-zero link depth is in use.")

   def _validateStage(self):
      """
      Validates stage configuration.

      The target directory must be filled in, and the peers are
      also validated.

      Peers are only required in this section if the peers configuration
      section is not filled in.  However, if any peers are filled in
      here, they override the peers configuration and must meet the
      validation criteria in :any:`_validatePeerList`.

      Raises:
         ValueError: If stage configuration is invalid
      """
      if self.stage is not None:
         if self.stage.targetDir is None:
            raise ValueError("Stage section target directory must be filled in.")
         if self.peers is None:
            # In this case, stage configuration is our only configuration and must be valid.
            self._validatePeerList(self.stage.localPeers, self.stage.remotePeers)
         else:
            # In this case, peers configuration is the default and stage configuration overrides.
            # Validation is only needed if it's stage configuration is actually filled in.
            if self.stage.hasPeers():
               self._validatePeerList(self.stage.localPeers, self.stage.remotePeers)

   def _validateStore(self):
      """
      Validates store configuration.

      The device type, drive speed, and blanking behavior are optional.  All
      other values are required. Missing booleans will be set to defaults.

      If blanking behavior is provided, then both a blanking mode and a
      blanking factor are required.

      The image writer functionality in the ``writer`` module is supposed to be
      able to handle a device speed of ``None``.

      Any caller which needs a "real" (non-``None``) value for the device type
      can use ``DEFAULT_DEVICE_TYPE``, which is guaranteed to be sensible.

      This is also where we make sure that the media type -- which is already a
      valid type -- matches up properly with the device type.

      Raises:
         ValueError: If store configuration is invalid
      """
      if self.store is not None:
         if self.store.sourceDir is None:
            raise ValueError("Store section source directory must be filled in.")
         if self.store.mediaType is None:
            raise ValueError("Store section media type must be filled in.")
         if self.store.devicePath is None:
            raise ValueError("Store section device path must be filled in.")
         if self.store.deviceType is None or self.store.deviceType == "cdwriter":
            if self.store.mediaType not in VALID_CD_MEDIA_TYPES:
               raise ValueError("Media type must match device type.")
         elif self.store.deviceType == "dvdwriter":
            if self.store.mediaType not in VALID_DVD_MEDIA_TYPES:
               raise ValueError("Media type must match device type.")
         if self.store.blankBehavior is not None:
            if self.store.blankBehavior.blankMode is None and self.store.blankBehavior.blankFactor is None:
               raise ValueError("If blanking behavior is provided, all values must be filled in.")

   def _validatePurge(self):
      """
      Validates purge configuration.

      The list of purge directories may be either ``None`` or an empty list
      ``[]`` if desired.  All purge directories must contain a path and a retain
      days value.

      Raises:
         ValueError: If purge configuration is invalid
      """
      if self.purge is not None:
         if self.purge.purgeDirs is not None:
            for purgeDir in self.purge.purgeDirs:
               if purgeDir.absolutePath is None:
                  raise ValueError("Each purge directory must set an absolute path.")
               if purgeDir.retainDays is None:
                  raise ValueError("Each purge directory must set a retain days value.")

   def _validatePeerList(self, localPeers, remotePeers):
      """
      Validates the set of local and remote peers.

      Local peers must be completely filled in, including both name and collect
      directory.  Remote peers must also fill in the name and collect
      directory, but can leave the remote user and rcp command unset.  In this
      case, the remote user is assumed to match the backup user from the
      options section and rcp command is taken directly from the options
      section.

      Args:
         localPeers: List of local peers
         remotePeers: List of remote peers

      Raises:
         ValueError: If stage configuration is invalid
      """
      if localPeers is None and remotePeers is None:
         raise ValueError("Peer list must contain at least one backup peer.")
      if localPeers is None and remotePeers is not None:
         if len(remotePeers) < 1:
            raise ValueError("Peer list must contain at least one backup peer.")
      elif localPeers is not None and remotePeers is None:
         if len(localPeers) < 1:
            raise ValueError("Peer list must contain at least one backup peer.")
      elif localPeers is not None and remotePeers is not None:
         if len(localPeers) + len(remotePeers) < 1:
            raise ValueError("Peer list must contain at least one backup peer.")
      names = []
      if localPeers is not None:
         for localPeer in localPeers:
            if localPeer.name is None:
               raise ValueError("Local peers must set a name.")
            names.append(localPeer.name)
            if localPeer.collectDir is None:
               raise ValueError("Local peers must set a collect directory.")
      if remotePeers is not None:
         for remotePeer in remotePeers:
            if remotePeer.name is None:
               raise ValueError("Remote peers must set a name.")
            names.append(remotePeer.name)
            if remotePeer.collectDir is None:
               raise ValueError("Remote peers must set a collect directory.")
            if (self.options is None or self.options.backupUser is None) and remotePeer.remoteUser is None:
               raise ValueError("Remote user must either be set in options section or individual remote peer.")
            if (self.options is None or self.options.rcpCommand is None) and remotePeer.rcpCommand is None:
               raise ValueError("Remote copy command must either be set in options section or individual remote peer.")
            if remotePeer.managed:
               if (self.options is None or self.options.rshCommand is None) and remotePeer.rshCommand is None:
                  raise ValueError("Remote shell command must either be set in options section or individual remote peer.")
               if (self.options is None or self.options.cbackCommand is None) and remotePeer.cbackCommand is None:
                  raise ValueError("Remote cback command must either be set in options section or individual remote peer.")
               if ((self.options is None or self.options.managedActions is None or len(self.options.managedActions) < 1)
                   and (remotePeer.managedActions is None or len(remotePeer.managedActions) < 1)):
                  raise ValueError("Managed actions list must be set in options section or individual remote peer.")
      checkUnique("Duplicate peer names exist:", names)


########################################################################
# General utility functions
########################################################################

def readByteQuantity(parent, name):
   """
   Read a byte size value from an XML document.

   A byte size value is an interpreted string value.  If the string value
   ends with "MB" or "GB", then the string before that is interpreted as
   megabytes or gigabytes.  Otherwise, it is intepreted as bytes.

   Args:
      parent: Parent node to search beneath
      name: Name of node to search for

   Returns:
       ByteQuantity parsed from XML document
   """
   data = readString(parent, name)
   if data is None:
      return None
   data = data.strip()
   if data.endswith("KB"):
      quantity = data[0:data.rfind("KB")].strip()
      units = UNIT_KBYTES
   elif data.endswith("MB"):
      quantity = data[0:data.rfind("MB")].strip()
      units = UNIT_MBYTES
   elif data.endswith("GB"):
      quantity = data[0:data.rfind("GB")].strip()
      units = UNIT_GBYTES
   else:
      quantity = data.strip()
      units = UNIT_BYTES
   return ByteQuantity(quantity, units)

def addByteQuantityNode(xmlDom, parentNode, nodeName, byteQuantity):
   """
   Adds a text node as the next child of a parent, to contain a byte size.

   If the ``byteQuantity`` is None, then the node will be created, but will
   be empty (i.e. will contain no text node child).

   The size in bytes will be normalized.  If it is larger than 1.0 GB, it will
   be shown in GB ("1.0 GB").  If it is larger than 1.0 MB ("1.0 MB"), it will
   be shown in MB.  Otherwise, it will be shown in bytes ("423413").

   Args:
      xmlDom: DOM tree as from ``impl.createDocument()``
      parentNode: Parent node to create child for
      nodeName: Name of the new container node
      byteQuantity: ByteQuantity object to put into the XML document

   Returns:
       Reference to the newly-created node
   """
   if byteQuantity is None:
      byteString = None
   elif byteQuantity.units == UNIT_KBYTES:
      byteString = "%s KB" % byteQuantity.quantity
   elif byteQuantity.units == UNIT_MBYTES:
      byteString = "%s MB" % byteQuantity.quantity
   elif byteQuantity.units == UNIT_GBYTES:
      byteString = "%s GB" % byteQuantity.quantity
   else:
      byteString = byteQuantity.quantity
   return addStringNode(xmlDom, parentNode, nodeName, byteString)
