CedarBackup2.peer

Package CedarBackup2 :: Module peer |
[hide private]
[frames] | no frames]

# Source Code for Module CedarBackup2.peer

       1  # -*- coding: iso-8859-1 -*-
       2  # vim: set ft=python ts=3 sw=3 expandtab:
       3  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
       4  #
       5  #              C E D A R
       6  #          S O L U T I O N S       "Software done right."
       7  #           S O F T W A R E
       8  #
       9  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
      10  #
      11  # Copyright (c) 2004-2008,2010 Kenneth J. Pronovici.
      12  # All rights reserved.
      13  #
      14  # This program is free software; you can redistribute it and/or
      15  # modify it under the terms of the GNU General Public License,
      16  # Version 2, as published by the Free Software Foundation.
      17  #
      18  # This program is distributed in the hope that it will be useful,
      19  # but WITHOUT ANY WARRANTY; without even the implied warranty of
      20  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
      21  #
      22  # Copies of the GNU General Public License are available from
      23  # the Free Software Foundation website, http://www.gnu.org/.
      24  #
      25  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
      26  #
      27  # Author   : Kenneth J. Pronovici <pronovic@ieee.org>
      28  # Language : Python 2 (>= 2.7)
      29  # Project  : Cedar Backup, release 2
      30  # Purpose  : Provides backup peer-related objects.
      31  #
      32  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
      33
      34  ########################################################################
      35  # Module documentation
      36  ########################################################################
      37
      38  """
      39  Provides backup peer-related objects and utility functions.
      40
      41  @sort: LocalPeer, RemotePeer
      42
      43  @var DEF_COLLECT_INDICATOR: Name of the default collect indicator file.
      44  @var DEF_STAGE_INDICATOR: Name of the default stage indicator file.
      45
      46  @author: Kenneth J. Pronovici <pronovic@ieee.org>
      47  """
      48
      49
      50  ########################################################################
      51  # Imported modules
      52  ########################################################################
      53
      54  # System modules
      55  import os
      56  import logging
      57  import shutil
      58
      59  # Cedar Backup modules
      60  from CedarBackup2.filesystem import FilesystemList
      61  from CedarBackup2.util import resolveCommand, executeCommand, isRunningAsRoot
      62  from CedarBackup2.util import splitCommandLine, encodePath
      63  from CedarBackup2.config import VALID_FAILURE_MODES
      64
      65
      66  ########################################################################
      67  # Module-wide constants and variables
      68  ########################################################################
      69
      70  logger                  = logging.getLogger("CedarBackup2.log.peer")
      71
      72  DEF_RCP_COMMAND         = [ "/usr/bin/scp", "-B", "-q", "-C" ]
      73  DEF_RSH_COMMAND         = [ "/usr/bin/ssh", ]
      74  DEF_CBACK_COMMAND       = "/usr/bin/cback"
      75
      76  DEF_COLLECT_INDICATOR   = "cback.collect"
      77  DEF_STAGE_INDICATOR     = "cback.stage"
      78
      79  SU_COMMAND              = [ "su" ]
      80
      81
      82  ########################################################################
      83  # LocalPeer class definition
      84  ########################################################################
      85
      86 -class LocalPeer(object):
      87
      88     ######################
      89     # Class documentation
      90     ######################
      91
      92     """
      93     Backup peer representing a local peer in a backup pool.
      94
      95     This is a class representing a local (non-network) peer in a backup pool.
      96     Local peers are backed up by simple filesystem copy operations.  A local
      97     peer has associated with it a name (typically, but not necessarily, a
      98     hostname) and a collect directory.
      99
     100     The public methods other than the constructor are part of a "backup peer"
     101     interface shared with the C{RemotePeer} class.
     102
     103     @sort: __init__, stagePeer, checkCollectIndicator, writeStageIndicator,
     104            _copyLocalDir, _copyLocalFile, name, collectDir
     105     """
     106
     107     ##############
     108     # Constructor
     109     ##############
     110
     111 -   def __init__(self, name, collectDir, ignoreFailureMode=None):
     112        """
     113        Initializes a local backup peer.
     114
     115        Note that the collect directory must be an absolute path, but does not
     116        have to exist when the object is instantiated.  We do a lazy validation
     117        on this value since we could (potentially) be creating peer objects
     118        before an ongoing backup completed.
     119
     120        @param name: Name of the backup peer
     121        @type name: String, typically a hostname
     122
     123        @param collectDir: Path to the peer's collect directory
     124        @type collectDir: String representing an absolute local path on disk
     125
     126        @param ignoreFailureMode: Ignore failure mode for this peer
     127        @type ignoreFailureMode: One of VALID_FAILURE_MODES
     128
     129        @raise ValueError: If the name is empty.
     130        @raise ValueError: If collect directory is not an absolute path.
     131        """
     132        self._name = None
     133        self._collectDir = None
     134        self._ignoreFailureMode = None
     135        self.name = name
     136        self.collectDir = collectDir
     137        self.ignoreFailureMode = ignoreFailureMode
     138
     139
     140     #############
     141     # Properties
     142     #############
     143
     144 -   def _setName(self, value):
     145        """
     146        Property target used to set the peer name.
     147        The value must be a non-empty string and cannot be C{None}.
     148        @raise ValueError: If the value is an empty string or C{None}.
     149        """
     150        if value is None or len(value) < 1:
     151           raise ValueError("Peer name must be a non-empty string.")
     152        self._name = value
     153
     154 -   def _getName(self):
     155        """
     156        Property target used to get the peer name.
     157        """
     158        return self._name
     159
     160 -   def _setCollectDir(self, value):
     161        """
     162        Property target used to set the collect directory.
     163        The value must be an absolute path and cannot be C{None}.
     164        It does not have to exist on disk at the time of assignment.
     165        @raise ValueError: If the value is C{None} or is not an absolute path.
     166        @raise ValueError: If a path cannot be encoded properly.
     167        """
     168        if value is None or not os.path.isabs(value):
     169           raise ValueError("Collect directory must be an absolute path.")
     170        self._collectDir = encodePath(value)
     171
     172 -   def _getCollectDir(self):
     173        """
     174        Property target used to get the collect directory.
     175        """
     176        return self._collectDir
     177
     178 -   def _setIgnoreFailureMode(self, value):
     179        """
     180        Property target used to set the ignoreFailure mode.
     181        If not C{None}, the mode must be one of the values in L{VALID_FAILURE_MODES}.
     182        @raise ValueError: If the value is not valid.
     183        """
     184        if value is not None:
     185           if value not in VALID_FAILURE_MODES:
     186              raise ValueError("Ignore failure mode must be one of %s." % VALID_FAILURE_MODES)
     187        self._ignoreFailureMode = value
     188
     189 -   def _getIgnoreFailureMode(self):
     190        """
     191        Property target used to get the ignoreFailure mode.
     192        """
     193        return self._ignoreFailureMode
     194
     195     name = property(_getName, _setName, None, "Name of the peer.")
     196     collectDir = property(_getCollectDir, _setCollectDir, None, "Path to the peer's collect directory (an absolute local path).")
     197     ignoreFailureMode = property(_getIgnoreFailureMode, _setIgnoreFailureMode, None, "Ignore failure mode for peer.")
     198
     199
     200     #################
     201     # Public methods
     202     #################
     203
     204 -   def stagePeer(self, targetDir, ownership=None, permissions=None):
     205        """
     206        Stages data from the peer into the indicated local target directory.
     207
     208        The collect and target directories must both already exist before this
     209        method is called.  If passed in, ownership and permissions will be
     210        applied to the files that are copied.
     211
     212        @note: The caller is responsible for checking that the indicator exists,
     213        if they care.  This function only stages the files within the directory.
     214
     215        @note: If you have user/group as strings, call the L{util.getUidGid} function
     216        to get the associated uid/gid as an ownership tuple.
     217
     218        @param targetDir: Target directory to write data into
     219        @type targetDir: String representing a directory on disk
     220
     221        @param ownership: Owner and group that the staged files should have
     222        @type ownership: Tuple of numeric ids C{(uid, gid)}
     223
     224        @param permissions: Permissions that the staged files should have
     225        @type permissions: UNIX permissions mode, specified in octal (i.e. C{0640}).
     226
     227        @return: Number of files copied from the source directory to the target directory.
     228
     229        @raise ValueError: If collect directory is not a directory or does not exist
     230        @raise ValueError: If target directory is not a directory, does not exist or is not absolute.
     231        @raise ValueError: If a path cannot be encoded properly.
     232        @raise IOError: If there were no files to stage (i.e. the directory was empty)
     233        @raise IOError: If there is an IO error copying a file.
     234        @raise OSError: If there is an OS error copying or changing permissions on a file
     235        """
     236        targetDir = encodePath(targetDir)
     237        if not os.path.isabs(targetDir):
     238           logger.debug("Target directory [%s] not an absolute path.", targetDir)
     239           raise ValueError("Target directory must be an absolute path.")
     240        if not os.path.exists(self.collectDir) or not os.path.isdir(self.collectDir):
     241           logger.debug("Collect directory [%s] is not a directory or does not exist on disk.", self.collectDir)
     242           raise ValueError("Collect directory is not a directory or does not exist on disk.")
     243        if not os.path.exists(targetDir) or not os.path.isdir(targetDir):
     244           logger.debug("Target directory [%s] is not a directory or does not exist on disk.", targetDir)
     245           raise ValueError("Target directory is not a directory or does not exist on disk.")
     246        count = LocalPeer._copyLocalDir(self.collectDir, targetDir, ownership, permissions)
     247        if count == 0:
     248           raise IOError("Did not copy any files from local peer.")
     249        return count
     250
     251 -   def checkCollectIndicator(self, collectIndicator=None):
     252        """
     253        Checks the collect indicator in the peer's staging directory.
     254
     255        When a peer has completed collecting its backup files, it will write an
     256        empty indicator file into its collect directory.  This method checks to
     257        see whether that indicator has been written.  We're "stupid" here - if
     258        the collect directory doesn't exist, you'll naturally get back C{False}.
     259
     260        If you need to, you can override the name of the collect indicator file
     261        by passing in a different name.
     262
     263        @param collectIndicator: Name of the collect indicator file to check
     264        @type collectIndicator: String representing name of a file in the collect directory
     265
     266        @return: Boolean true/false depending on whether the indicator exists.
     267        @raise ValueError: If a path cannot be encoded properly.
     268        """
     269        collectIndicator = encodePath(collectIndicator)
     270        if collectIndicator is None:
     271           return os.path.exists(os.path.join(self.collectDir, DEF_COLLECT_INDICATOR))
     272        else:
     273           return os.path.exists(os.path.join(self.collectDir, collectIndicator))
     274
     275 -   def writeStageIndicator(self, stageIndicator=None, ownership=None, permissions=None):
     276        """
     277        Writes the stage indicator in the peer's staging directory.
     278
     279        When the master has completed collecting its backup files, it will write
     280        an empty indicator file into the peer's collect directory.  The presence
     281        of this file implies that the staging process is complete.
     282
     283        If you need to, you can override the name of the stage indicator file by
     284        passing in a different name.
     285
     286        @note: If you have user/group as strings, call the L{util.getUidGid}
     287        function to get the associated uid/gid as an ownership tuple.
     288
     289        @param stageIndicator: Name of the indicator file to write
     290        @type stageIndicator: String representing name of a file in the collect directory
     291
     292        @param ownership: Owner and group that the indicator file should have
     293        @type ownership: Tuple of numeric ids C{(uid, gid)}
     294
     295        @param permissions: Permissions that the indicator file should have
     296        @type permissions: UNIX permissions mode, specified in octal (i.e. C{0640}).
     297
     298        @raise ValueError: If collect directory is not a directory or does not exist
     299        @raise ValueError: If a path cannot be encoded properly.
     300        @raise IOError: If there is an IO error creating the file.
     301        @raise OSError: If there is an OS error creating or changing permissions on the file
     302        """
     303        stageIndicator = encodePath(stageIndicator)
     304        if not os.path.exists(self.collectDir) or not os.path.isdir(self.collectDir):
     305           logger.debug("Collect directory [%s] is not a directory or does not exist on disk.", self.collectDir)
     306           raise ValueError("Collect directory is not a directory or does not exist on disk.")
     307        if stageIndicator is None:
     308           fileName = os.path.join(self.collectDir, DEF_STAGE_INDICATOR)
     309        else:
     310           fileName = os.path.join(self.collectDir, stageIndicator)
     311        LocalPeer._copyLocalFile(None, fileName, ownership, permissions)    # None for sourceFile results in an empty target
     312
     313
     314     ##################
     315     # Private methods
     316     ##################
     317
     318     @staticmethod
     319 -   def _copyLocalDir(sourceDir, targetDir, ownership=None, permissions=None):
     320        """
     321        Copies files from the source directory to the target directory.
     322
     323        This function is not recursive.  Only the files in the directory will be
     324        copied.   Ownership and permissions will be left at their default values
     325        if new values are not specified.  The source and target directories are
     326        allowed to be soft links to a directory, but besides that soft links are
     327        ignored.
     328
     329        @note: If you have user/group as strings, call the L{util.getUidGid}
     330        function to get the associated uid/gid as an ownership tuple.
     331
     332        @param sourceDir: Source directory
     333        @type sourceDir: String representing a directory on disk
     334
     335        @param targetDir: Target directory
     336        @type targetDir: String representing a directory on disk
     337
     338        @param ownership: Owner and group that the copied files should have
     339        @type ownership: Tuple of numeric ids C{(uid, gid)}
     340
     341        @param permissions: Permissions that the staged files should have
     342        @type permissions: UNIX permissions mode, specified in octal (i.e. C{0640}).
     343
     344        @return: Number of files copied from the source directory to the target directory.
     345
     346        @raise ValueError: If source or target is not a directory or does not exist.
     347        @raise ValueError: If a path cannot be encoded properly.
     348        @raise IOError: If there is an IO error copying the files.
     349        @raise OSError: If there is an OS error copying or changing permissions on a files
     350        """
     351        filesCopied = 0
     352        sourceDir = encodePath(sourceDir)
     353        targetDir = encodePath(targetDir)
     354        for fileName in os.listdir(sourceDir):
     355           sourceFile = os.path.join(sourceDir, fileName)
     356           targetFile = os.path.join(targetDir, fileName)
     357           LocalPeer._copyLocalFile(sourceFile, targetFile, ownership, permissions)
     358           filesCopied += 1
     359        return filesCopied
     360
     361     @staticmethod
     362 -   def _copyLocalFile(sourceFile=None, targetFile=None, ownership=None, permissions=None, overwrite=True):
     363        """
     364        Copies a source file to a target file.
     365
     366        If the source file is C{None} then the target file will be created or
     367        overwritten as an empty file.  If the target file is C{None}, this method
     368        is a no-op.  Attempting to copy a soft link or a directory will result in
     369        an exception.
     370
     371        @note: If you have user/group as strings, call the L{util.getUidGid}
     372        function to get the associated uid/gid as an ownership tuple.
     373
     374        @note: We will not overwrite a target file that exists when this method
     375        is invoked.  If the target already exists, we'll raise an exception.
     376
     377        @param sourceFile: Source file to copy
     378        @type sourceFile: String representing a file on disk, as an absolute path
     379
     380        @param targetFile: Target file to create
     381        @type targetFile: String representing a file on disk, as an absolute path
     382
     383        @param ownership: Owner and group that the copied should have
     384        @type ownership: Tuple of numeric ids C{(uid, gid)}
     385
     386        @param permissions: Permissions that the staged files should have
     387        @type permissions: UNIX permissions mode, specified in octal (i.e. C{0640}).
     388
     389        @param overwrite: Indicates whether it's OK to overwrite the target file.
     390        @type overwrite: Boolean true/false.
     391
     392        @raise ValueError: If the passed-in source file is not a regular file.
     393        @raise ValueError: If a path cannot be encoded properly.
     394        @raise IOError: If the target file already exists.
     395        @raise IOError: If there is an IO error copying the file
     396        @raise OSError: If there is an OS error copying or changing permissions on a file
     397        """
     398        targetFile = encodePath(targetFile)
     399        sourceFile = encodePath(sourceFile)
     400        if targetFile is None:
     401           return
     402        if not overwrite:
     403           if os.path.exists(targetFile):
     404              raise IOError("Target file [%s] already exists." % targetFile)
     405        if sourceFile is None:
     406           open(targetFile, "w").write("")
     407        else:
     408           if os.path.isfile(sourceFile) and not os.path.islink(sourceFile):
     409              shutil.copy(sourceFile, targetFile)
     410           else:
     411              logger.debug("Source [%s] is not a regular file.", sourceFile)
     412              raise ValueError("Source is not a regular file.")
     413        if ownership is not None:
     414           os.chown(targetFile, ownership[0], ownership[1])
     415        if permissions is not None:
     416           os.chmod(targetFile, permissions)
     417
     418
     419  ########################################################################
     420  # RemotePeer class definition
     421  ########################################################################
     422
     423 -class RemotePeer(object):
     424
     425     ######################
     426     # Class documentation
     427     ######################
     428
     429     """
     430     Backup peer representing a remote peer in a backup pool.
     431
     432     This is a class representing a remote (networked) peer in a backup pool.
     433     Remote peers are backed up using an rcp-compatible copy command.  A remote
     434     peer has associated with it a name (which must be a valid hostname), a
     435     collect directory, a working directory and a copy method (an rcp-compatible
     436     command).
     437
     438     You can also set an optional local user value.  This username will be used
     439     as the local user for any remote copies that are required.  It can only be
     440     used if the root user is executing the backup.  The root user will C{su} to
     441     the local user and execute the remote copies as that user.
     442
     443     The copy method is associated with the peer and not with the actual request
     444     to copy, because we can envision that each remote host might have a
     445     different connect method.
     446
     447     The public methods other than the constructor are part of a "backup peer"
     448     interface shared with the C{LocalPeer} class.
     449
     450     @sort: __init__, stagePeer, checkCollectIndicator, writeStageIndicator,
     451            executeRemoteCommand, executeManagedAction, _getDirContents,
     452            _copyRemoteDir, _copyRemoteFile, _pushLocalFile, name, collectDir,
     453            remoteUser, rcpCommand, rshCommand, cbackCommand
     454     """
     455
     456     ##############
     457     # Constructor
     458     ##############
     459
     460 -   def __init__(self, name=None, collectDir=None, workingDir=None, remoteUser=None,
     461                  rcpCommand=None, localUser=None, rshCommand=None, cbackCommand=None,
     462                  ignoreFailureMode=None):
     463        """
     464        Initializes a remote backup peer.
     465
     466        @note: If provided, each command will eventually be parsed into a list of
     467        strings suitable for passing to C{util.executeCommand} in order to avoid
     468        security holes related to shell interpolation.   This parsing will be
     469        done by the L{util.splitCommandLine} function.  See the documentation for
     470        that function for some important notes about its limitations.
     471
     472        @param name: Name of the backup peer
     473        @type name: String, must be a valid DNS hostname
     474
     475        @param collectDir: Path to the peer's collect directory
     476        @type collectDir: String representing an absolute path on the remote peer
     477
     478        @param workingDir: Working directory that can be used to create temporary files, etc.
     479        @type workingDir: String representing an absolute path on the current host.
     480
     481        @param remoteUser: Name of the Cedar Backup user on the remote peer
     482        @type remoteUser: String representing a username, valid via remote shell to the peer
     483
     484        @param localUser: Name of the Cedar Backup user on the current host
     485        @type localUser: String representing a username, valid on the current host
     486
     487        @param rcpCommand: An rcp-compatible copy command to use for copying files from the peer
     488        @type rcpCommand: String representing a system command including required arguments
     489
     490        @param rshCommand: An rsh-compatible copy command to use for remote shells to the peer
     491        @type rshCommand: String representing a system command including required arguments
     492
     493        @param cbackCommand: A chack-compatible command to use for executing managed actions
     494        @type cbackCommand: String representing a system command including required arguments
     495
     496        @param ignoreFailureMode: Ignore failure mode for this peer
     497        @type ignoreFailureMode: One of VALID_FAILURE_MODES
     498
     499        @raise ValueError: If collect directory is not an absolute path
     500        """
     501        self._name = None
     502        self._collectDir = None
     503        self._workingDir = None
     504        self._remoteUser = None
     505        self._localUser = None
     506        self._rcpCommand = None
     507        self._rcpCommandList = None
     508        self._rshCommand = None
     509        self._rshCommandList = None
     510        self._cbackCommand = None
     511        self._ignoreFailureMode = None
     512        self.name = name
     513        self.collectDir = collectDir
     514        self.workingDir = workingDir
     515        self.remoteUser = remoteUser
     516        self.localUser = localUser
     517        self.rcpCommand = rcpCommand
     518        self.rshCommand = rshCommand
     519        self.cbackCommand = cbackCommand
     520        self.ignoreFailureMode = ignoreFailureMode
     521
     522
     523     #############
     524     # Properties
     525     #############
     526
     527 -   def _setName(self, value):
     528        """
     529        Property target used to set the peer name.
     530        The value must be a non-empty string and cannot be C{None}.
     531        @raise ValueError: If the value is an empty string or C{None}.
     532        """
     533        if value is None or len(value) < 1:
     534           raise ValueError("Peer name must be a non-empty string.")
     535        self._name = value
     536
     537 -   def _getName(self):
     538        """
     539        Property target used to get the peer name.
     540        """
     541        return self._name
     542
     543 -   def _setCollectDir(self, value):
     544        """
     545        Property target used to set the collect directory.
     546        The value must be an absolute path and cannot be C{None}.
     547        It does not have to exist on disk at the time of assignment.
     548        @raise ValueError: If the value is C{None} or is not an absolute path.
     549        @raise ValueError: If the value cannot be encoded properly.
     550        """
     551        if value is not None:
     552           if not os.path.isabs(value):
     553              raise ValueError("Collect directory must be an absolute path.")
     554        self._collectDir = encodePath(value)
     555
     556 -   def _getCollectDir(self):
     557        """
     558        Property target used to get the collect directory.
     559        """
     560        return self._collectDir
     561
     562 -   def _setWorkingDir(self, value):
     563        """
     564        Property target used to set the working directory.
     565        The value must be an absolute path and cannot be C{None}.
     566        @raise ValueError: If the value is C{None} or is not an absolute path.
     567        @raise ValueError: If the value cannot be encoded properly.
     568        """
     569        if value is not None:
     570           if not os.path.isabs(value):
     571              raise ValueError("Working directory must be an absolute path.")
     572        self._workingDir = encodePath(value)
     573
     574 -   def _getWorkingDir(self):
     575        """
     576        Property target used to get the working directory.
     577        """
     578        return self._workingDir
     579
     580 -   def _setRemoteUser(self, value):
     581        """
     582        Property target used to set the remote user.
     583        The value must be a non-empty string and cannot be C{None}.
     584        @raise ValueError: If the value is an empty string or C{None}.
     585        """
     586        if value is None or len(value) < 1:
     587           raise ValueError("Peer remote user must be a non-empty string.")
     588        self._remoteUser = value
     589
     590 -   def _getRemoteUser(self):
     591        """
     592        Property target used to get the remote user.
     593        """
     594        return self._remoteUser
     595
     596 -   def _setLocalUser(self, value):
     597        """
     598        Property target used to set the local user.
     599        The value must be a non-empty string if it is not C{None}.
     600        @raise ValueError: If the value is an empty string.
     601        """
     602        if value is not None:
     603           if len(value) < 1:
     604              raise ValueError("Peer local user must be a non-empty string.")
     605        self._localUser = value
     606
     607 -   def _getLocalUser(self):
     608        """
     609        Property target used to get the local user.
     610        """
     611        return self._localUser
     612
     613 -   def _setRcpCommand(self, value):
     614        """
     615        Property target to set the rcp command.
     616
     617        The value must be a non-empty string or C{None}.  Its value is stored in
     618        the two forms: "raw" as provided by the client, and "parsed" into a list
     619        suitable for being passed to L{util.executeCommand} via
     620        L{util.splitCommandLine}.
     621
     622        However, all the caller will ever see via the property is the actual
     623        value they set (which includes seeing C{None}, even if we translate that
     624        internally to C{DEF_RCP_COMMAND}).  Internally, we should always use
     625        C{self._rcpCommandList} if we want the actual command list.
     626
     627        @raise ValueError: If the value is an empty string.
     628        """
     629        if value is None:
     630           self._rcpCommand = None
     631           self._rcpCommandList = DEF_RCP_COMMAND
     632        else:
     633           if len(value) >= 1:
     634              self._rcpCommand = value
     635              self._rcpCommandList = splitCommandLine(self._rcpCommand)
     636           else:
     637              raise ValueError("The rcp command must be a non-empty string.")
     638
     639 -   def _getRcpCommand(self):
     640        """
     641        Property target used to get the rcp command.
     642        """
     643        return self._rcpCommand
     644
     645 -   def _setRshCommand(self, value):
     646        """
     647        Property target to set the rsh command.
     648
     649        The value must be a non-empty string or C{None}.  Its value is stored in
     650        the two forms: "raw" as provided by the client, and "parsed" into a list
     651        suitable for being passed to L{util.executeCommand} via
     652        L{util.splitCommandLine}.
     653
     654        However, all the caller will ever see via the property is the actual
     655        value they set (which includes seeing C{None}, even if we translate that
     656        internally to C{DEF_RSH_COMMAND}).  Internally, we should always use
     657        C{self._rshCommandList} if we want the actual command list.
     658
     659        @raise ValueError: If the value is an empty string.
     660        """
     661        if value is None:
     662           self._rshCommand = None
     663           self._rshCommandList = DEF_RSH_COMMAND
     664        else:
     665           if len(value) >= 1:
     666              self._rshCommand = value
     667              self._rshCommandList = splitCommandLine(self._rshCommand)
     668           else:
     669              raise ValueError("The rsh command must be a non-empty string.")
     670
     671 -   def _getRshCommand(self):
     672        """
     673        Property target used to get the rsh command.
     674        """
     675        return self._rshCommand
     676
     677 -   def _setCbackCommand(self, value):
     678        """
     679        Property target to set the cback command.
     680
     681        The value must be a non-empty string or C{None}.  Unlike the other
     682        command, this value is only stored in the "raw" form provided by the
     683        client.
     684
     685        @raise ValueError: If the value is an empty string.
     686        """
     687        if value is None:
     688           self._cbackCommand = None
     689        else:
     690           if len(value) >= 1:
     691              self._cbackCommand = value
     692           else:
     693              raise ValueError("The cback command must be a non-empty string.")
     694
     695 -   def _getCbackCommand(self):
     696        """
     697        Property target used to get the cback command.
     698        """
     699        return self._cbackCommand
     700
     701 -   def _setIgnoreFailureMode(self, value):
     702        """
     703        Property target used to set the ignoreFailure mode.
     704        If not C{None}, the mode must be one of the values in L{VALID_FAILURE_MODES}.
     705        @raise ValueError: If the value is not valid.
     706        """
     707        if value is not None:
     708           if value not in VALID_FAILURE_MODES:
     709              raise ValueError("Ignore failure mode must be one of %s." % VALID_FAILURE_MODES)
     710        self._ignoreFailureMode = value
     711
     712 -   def _getIgnoreFailureMode(self):
     713        """
     714        Property target used to get the ignoreFailure mode.
     715        """
     716        return self._ignoreFailureMode
     717
     718     name = property(_getName, _setName, None, "Name of the peer (a valid DNS hostname).")
     719     collectDir = property(_getCollectDir, _setCollectDir, None, "Path to the peer's collect directory (an absolute local path).")
     720     workingDir = property(_getWorkingDir, _setWorkingDir, None, "Path to the peer's working directory (an absolute local path).")
     721     remoteUser = property(_getRemoteUser, _setRemoteUser, None, "Name of the Cedar Backup user on the remote peer.")
     722     localUser = property(_getLocalUser, _setLocalUser, None, "Name of the Cedar Backup user on the current host.")
     723     rcpCommand = property(_getRcpCommand, _setRcpCommand, None, "An rcp-compatible copy command to use for copying files.")
     724     rshCommand = property(_getRshCommand, _setRshCommand, None, "An rsh-compatible command to use for remote shells to the peer.")
     725     cbackCommand = property(_getCbackCommand, _setCbackCommand, None, "A chack-compatible command to use for executing managed actions.")
     726     ignoreFailureMode = property(_getIgnoreFailureMode, _setIgnoreFailureMode, None, "Ignore failure mode for peer.")
     727
     728
     729     #################
     730     # Public methods
     731     #################
     732
     733 -   def stagePeer(self, targetDir, ownership=None, permissions=None):
     734        """
     735        Stages data from the peer into the indicated local target directory.
     736
     737        The target directory must already exist before this method is called.  If
     738        passed in, ownership and permissions will be applied to the files that
     739        are copied.
     740
     741        @note: The returned count of copied files might be inaccurate if some of
     742        the copied files already existed in the staging directory prior to the
     743        copy taking place.  We don't clear the staging directory first, because
     744        some extension might also be using it.
     745
     746        @note: If you have user/group as strings, call the L{util.getUidGid} function
     747        to get the associated uid/gid as an ownership tuple.
     748
     749        @note: Unlike the local peer version of this method, an I/O error might
     750        or might not be raised if the directory is empty.  Since we're using a
     751        remote copy method, we just don't have the fine-grained control over our
     752        exceptions that's available when we can look directly at the filesystem,
     753        and we can't control whether the remote copy method thinks an empty
     754        directory is an error.
     755
     756        @param targetDir: Target directory to write data into
     757        @type targetDir: String representing a directory on disk
     758
     759        @param ownership: Owner and group that the staged files should have
     760        @type ownership: Tuple of numeric ids C{(uid, gid)}
     761
     762        @param permissions: Permissions that the staged files should have
     763        @type permissions: UNIX permissions mode, specified in octal (i.e. C{0640}).
     764
     765        @return: Number of files copied from the source directory to the target directory.
     766
     767        @raise ValueError: If target directory is not a directory, does not exist or is not absolute.
     768        @raise ValueError: If a path cannot be encoded properly.
     769        @raise IOError: If there were no files to stage (i.e. the directory was empty)
     770        @raise IOError: If there is an IO error copying a file.
     771        @raise OSError: If there is an OS error copying or changing permissions on a file
     772        """
     773        targetDir = encodePath(targetDir)
     774        if not os.path.isabs(targetDir):
     775           logger.debug("Target directory [%s] not an absolute path.", targetDir)
     776           raise ValueError("Target directory must be an absolute path.")
     777        if not os.path.exists(targetDir) or not os.path.isdir(targetDir):
     778           logger.debug("Target directory [%s] is not a directory or does not exist on disk.", targetDir)
     779           raise ValueError("Target directory is not a directory or does not exist on disk.")
     780        count = RemotePeer._copyRemoteDir(self.remoteUser, self.localUser, self.name,
     781                                          self._rcpCommand, self._rcpCommandList,
     782                                          self.collectDir, targetDir,
     783                                          ownership, permissions)
     784        if count == 0:
     785           raise IOError("Did not copy any files from local peer.")
     786        return count
     787
     788 -   def checkCollectIndicator(self, collectIndicator=None):
     789        """
     790        Checks the collect indicator in the peer's staging directory.
     791
     792        When a peer has completed collecting its backup files, it will write an
     793        empty indicator file into its collect directory.  This method checks to
     794        see whether that indicator has been written.  If the remote copy command
     795        fails, we return C{False} as if the file weren't there.
     796
     797        If you need to, you can override the name of the collect indicator file
     798        by passing in a different name.
     799
     800        @note: Apparently, we can't count on all rcp-compatible implementations
     801        to return sensible errors for some error conditions.  As an example, the
     802        C{scp} command in Debian 'woody' returns a zero (normal) status even when
     803        it can't find a host or if the login or path is invalid.  Because of
     804        this, the implementation of this method is rather convoluted.
     805
     806        @param collectIndicator: Name of the collect indicator file to check
     807        @type collectIndicator: String representing name of a file in the collect directory
     808
     809        @return: Boolean true/false depending on whether the indicator exists.
     810        @raise ValueError: If a path cannot be encoded properly.
     811        """
     812        try:
     813           if collectIndicator is None:
     814              sourceFile = os.path.join(self.collectDir, DEF_COLLECT_INDICATOR)
     815              targetFile = os.path.join(self.workingDir, DEF_COLLECT_INDICATOR)
     816           else:
     817              collectIndicator = encodePath(collectIndicator)
     818              sourceFile = os.path.join(self.collectDir, collectIndicator)
     819              targetFile = os.path.join(self.workingDir, collectIndicator)
     820           logger.debug("Fetch remote [%s] into [%s].", sourceFile, targetFile)
     821           if os.path.exists(targetFile):
     822              try:
     823                 os.remove(targetFile)
     824              except:
     825                 raise Exception("Error: collect indicator [%s] already exists!" % targetFile)
     826           try:
     827              RemotePeer._copyRemoteFile(self.remoteUser, self.localUser, self.name,
     828                                         self._rcpCommand, self._rcpCommandList,
     829                                         sourceFile, targetFile,
     830                                         overwrite=False)
     831              if os.path.exists(targetFile):
     832                 return True
     833              else:
     834                 return False
     835           except Exception, e:
     836              logger.info("Failed looking for collect indicator: %s", e)
     837              return False
     838        finally:
     839           if os.path.exists(targetFile):
     840              try:
     841                 os.remove(targetFile)
     842              except: pass
     843
     844 -   def writeStageIndicator(self, stageIndicator=None):
     845        """
     846        Writes the stage indicator in the peer's staging directory.
     847
     848        When the master has completed collecting its backup files, it will write
     849        an empty indicator file into the peer's collect directory.  The presence
     850        of this file implies that the staging process is complete.
     851
     852        If you need to, you can override the name of the stage indicator file by
     853        passing in a different name.
     854
     855        @note: If you have user/group as strings, call the L{util.getUidGid} function
     856        to get the associated uid/gid as an ownership tuple.
     857
     858        @param stageIndicator: Name of the indicator file to write
     859        @type stageIndicator: String representing name of a file in the collect directory
     860
     861        @raise ValueError: If a path cannot be encoded properly.
     862        @raise IOError: If there is an IO error creating the file.
     863        @raise OSError: If there is an OS error creating or changing permissions on the file
     864        """
     865        stageIndicator = encodePath(stageIndicator)
     866        if stageIndicator is None:
     867           sourceFile = os.path.join(self.workingDir, DEF_STAGE_INDICATOR)
     868           targetFile = os.path.join(self.collectDir, DEF_STAGE_INDICATOR)
     869        else:
     870           sourceFile = os.path.join(self.workingDir, DEF_STAGE_INDICATOR)
     871           targetFile = os.path.join(self.collectDir, stageIndicator)
     872        try:
     873           if not os.path.exists(sourceFile):
     874              open(sourceFile, "w").write("")
     875           RemotePeer._pushLocalFile(self.remoteUser, self.localUser, self.name,
     876                                     self._rcpCommand, self._rcpCommandList,
     877                                     sourceFile, targetFile)
     878        finally:
     879           if os.path.exists(sourceFile):
     880              try:
     881                 os.remove(sourceFile)
     882              except: pass
     883
     884 -   def executeRemoteCommand(self, command):
     885        """
     886        Executes a command on the peer via remote shell.
     887
     888        @param command: Command to execute
     889        @type command: String command-line suitable for use with rsh.
     890
     891        @raise IOError: If there is an error executing the command on the remote peer.
     892        """
     893        RemotePeer._executeRemoteCommand(self.remoteUser, self.localUser,
     894                                         self.name, self._rshCommand,
     895                                         self._rshCommandList, command)
     896
     897 -   def executeManagedAction(self, action, fullBackup):
     898        """
     899        Executes a managed action on this peer.
     900
     901        @param action: Name of the action to execute.
     902        @param fullBackup: Whether a full backup should be executed.
     903
     904        @raise IOError: If there is an error executing the action on the remote peer.
     905        """
     906        try:
     907           command = RemotePeer._buildCbackCommand(self.cbackCommand, action, fullBackup)
     908           self.executeRemoteCommand(command)
     909        except IOError, e:
     910           logger.info(e)
     911           raise IOError("Failed to execute action [%s] on managed client [%s]." % (action, self.name))
     912
     913
     914     ##################
     915     # Private methods
     916     ##################
     917
     918     @staticmethod
     919 -   def _getDirContents(path):
     920        """
     921        Returns the contents of a directory in terms of a Set.
     922
     923        The directory's contents are read as a L{FilesystemList} containing only
     924        files, and then the list is converted into a set object for later use.
     925
     926        @param path: Directory path to get contents for
     927        @type path: String representing a path on disk
     928
     929        @return: Set of files in the directory
     930        @raise ValueError: If path is not a directory or does not exist.
     931        """
     932        contents = FilesystemList()
     933        contents.excludeDirs = True
     934        contents.excludeLinks = True
     935        contents.addDirContents(path)
     936        try:
     937           return set(contents)
     938        except:
     939           import sets
     940           return sets.Set(contents)
     941
     942     @staticmethod
     943 -   def _copyRemoteDir(remoteUser, localUser, remoteHost, rcpCommand, rcpCommandList,
     944                        sourceDir, targetDir, ownership=None, permissions=None):
     945        """
     946        Copies files from the source directory to the target directory.
     947
     948        This function is not recursive.  Only the files in the directory will be
     949        copied.   Ownership and permissions will be left at their default values
     950        if new values are not specified.  Behavior when copying soft links from
     951        the collect directory is dependent on the behavior of the specified rcp
     952        command.
     953
     954        @note: The returned count of copied files might be inaccurate if some of
     955        the copied files already existed in the staging directory prior to the
     956        copy taking place.  We don't clear the staging directory first, because
     957        some extension might also be using it.
     958
     959        @note: If you have user/group as strings, call the L{util.getUidGid} function
     960        to get the associated uid/gid as an ownership tuple.
     961
     962        @note: We don't have a good way of knowing exactly what files we copied
     963        down from the remote peer, unless we want to parse the output of the rcp
     964        command (ugh).  We could change permissions on everything in the target
     965        directory, but that's kind of ugly too.  Instead, we use Python's set
     966        functionality to figure out what files were added while we executed the
     967        rcp command.  This isn't perfect - for instance, it's not correct if
     968        someone else is messing with the directory at the same time we're doing
     969        the remote copy - but it's about as good as we're going to get.
     970
     971        @note: Apparently, we can't count on all rcp-compatible implementations
     972        to return sensible errors for some error conditions.  As an example, the
     973        C{scp} command in Debian 'woody' returns a zero (normal) status even
     974        when it can't find a host or if the login or path is invalid.  We try
     975        to work around this by issuing C{IOError} if we don't copy any files from
     976        the remote host.
     977
     978        @param remoteUser: Name of the Cedar Backup user on the remote peer
     979        @type remoteUser: String representing a username, valid via the copy command
     980
     981        @param localUser: Name of the Cedar Backup user on the current host
     982        @type localUser: String representing a username, valid on the current host
     983
     984        @param remoteHost: Hostname of the remote peer
     985        @type remoteHost: String representing a hostname, accessible via the copy command
     986
     987        @param rcpCommand: An rcp-compatible copy command to use for copying files from the peer
     988        @type rcpCommand: String representing a system command including required arguments
     989
     990        @param rcpCommandList: An rcp-compatible copy command to use for copying files
     991        @type rcpCommandList: Command as a list to be passed to L{util.executeCommand}
     992
     993        @param sourceDir: Source directory
     994        @type sourceDir: String representing a directory on disk
     995
     996        @param targetDir: Target directory
     997        @type targetDir: String representing a directory on disk
     998
     999        @param ownership: Owner and group that the copied files should have
    1000        @type ownership: Tuple of numeric ids C{(uid, gid)}
    1001
    1002        @param permissions: Permissions that the staged files should have
    1003        @type permissions: UNIX permissions mode, specified in octal (i.e. C{0640}).
    1004
    1005        @return: Number of files copied from the source directory to the target directory.
    1006
    1007        @raise ValueError: If source or target is not a directory or does not exist.
    1008        @raise IOError: If there is an IO error copying the files.
    1009        """
    1010        beforeSet = RemotePeer._getDirContents(targetDir)
    1011        if localUser is not None:
    1012           try:
    1013              if not isRunningAsRoot():
    1014                 raise IOError("Only root can remote copy as another user.")
    1015           except AttributeError: pass
    1016           actualCommand = "%s %s@%s:%s/* %s" % (rcpCommand, remoteUser, remoteHost, sourceDir, targetDir)
    1017           command = resolveCommand(SU_COMMAND)
    1018           result = executeCommand(command, [localUser, "-c", actualCommand])[0]
    1019           if result != 0:
    1020              raise IOError("Error (%d) copying files from remote host as local user [%s]." % (result, localUser))
    1021        else:
    1022           copySource = "%s@%s:%s/*" % (remoteUser, remoteHost, sourceDir)
    1023           command = resolveCommand(rcpCommandList)
    1024           result = executeCommand(command, [copySource, targetDir])[0]
    1025           if result != 0:
    1026              raise IOError("Error (%d) copying files from remote host." % result)
    1027        afterSet = RemotePeer._getDirContents(targetDir)
    1028        if len(afterSet) == 0:
    1029           raise IOError("Did not copy any files from remote peer.")
    1030        differenceSet = afterSet.difference(beforeSet)  # files we added as part of copy
    1031        if len(differenceSet) == 0:
    1032           raise IOError("Apparently did not copy any new files from remote peer.")
    1033        for targetFile in differenceSet:
    1034           if ownership is not None:
    1035              os.chown(targetFile, ownership[0], ownership[1])
    1036           if permissions is not None:
    1037              os.chmod(targetFile, permissions)
    1038        return len(differenceSet)
    1039
    1040     @staticmethod
    1041 -   def _copyRemoteFile(remoteUser, localUser, remoteHost,
    1042                         rcpCommand, rcpCommandList,
    1043                         sourceFile, targetFile, ownership=None,
    1044                         permissions=None, overwrite=True):
    1045        """
    1046        Copies a remote source file to a target file.
    1047
    1048        @note: Internally, we have to go through and escape any spaces in the
    1049        source path with double-backslash, otherwise things get screwed up.   It
    1050        doesn't seem to be required in the target path. I hope this is portable
    1051        to various different rcp methods, but I guess it might not be (all I have
    1052        to test with is OpenSSH).
    1053
    1054        @note: If you have user/group as strings, call the L{util.getUidGid} function
    1055        to get the associated uid/gid as an ownership tuple.
    1056
    1057        @note: We will not overwrite a target file that exists when this method
    1058        is invoked.  If the target already exists, we'll raise an exception.
    1059
    1060        @note: Apparently, we can't count on all rcp-compatible implementations
    1061        to return sensible errors for some error conditions.  As an example, the
    1062        C{scp} command in Debian 'woody' returns a zero (normal) status even when
    1063        it can't find a host or if the login or path is invalid.  We try to work
    1064        around this by issuing C{IOError} the target file does not exist when
    1065        we're done.
    1066
    1067        @param remoteUser: Name of the Cedar Backup user on the remote peer
    1068        @type remoteUser: String representing a username, valid via the copy command
    1069
    1070        @param remoteHost: Hostname of the remote peer
    1071        @type remoteHost: String representing a hostname, accessible via the copy command
    1072
    1073        @param localUser: Name of the Cedar Backup user on the current host
    1074        @type localUser: String representing a username, valid on the current host
    1075
    1076        @param rcpCommand: An rcp-compatible copy command to use for copying files from the peer
    1077        @type rcpCommand: String representing a system command including required arguments
    1078
    1079        @param rcpCommandList: An rcp-compatible copy command to use for copying files
    1080        @type rcpCommandList: Command as a list to be passed to L{util.executeCommand}
    1081
    1082        @param sourceFile: Source file to copy
    1083        @type sourceFile: String representing a file on disk, as an absolute path
    1084
    1085        @param targetFile: Target file to create
    1086        @type targetFile: String representing a file on disk, as an absolute path
    1087
    1088        @param ownership: Owner and group that the copied should have
    1089        @type ownership: Tuple of numeric ids C{(uid, gid)}
    1090
    1091        @param permissions: Permissions that the staged files should have
    1092        @type permissions: UNIX permissions mode, specified in octal (i.e. C{0640}).
    1093
    1094        @param overwrite: Indicates whether it's OK to overwrite the target file.
    1095        @type overwrite: Boolean true/false.
    1096
    1097        @raise IOError: If the target file already exists.
    1098        @raise IOError: If there is an IO error copying the file
    1099        @raise OSError: If there is an OS error changing permissions on the file
    1100        """
    1101        if not overwrite:
    1102           if os.path.exists(targetFile):
    1103              raise IOError("Target file [%s] already exists." % targetFile)
    1104        if localUser is not None:
    1105           try:
    1106              if not isRunningAsRoot():
    1107                 raise IOError("Only root can remote copy as another user.")
    1108           except AttributeError: pass
    1109           actualCommand = "%s %s@%s:%s %s" % (rcpCommand, remoteUser, remoteHost, sourceFile.replace(" ", "\\ "), targetFile)
    1110           command = resolveCommand(SU_COMMAND)
    1111           result = executeCommand(command, [localUser, "-c", actualCommand])[0]
    1112           if result != 0:
    1113              raise IOError("Error (%d) copying [%s] from remote host as local user [%s]." % (result, sourceFile, localUser))
    1114        else:
    1115           copySource = "%s@%s:%s" % (remoteUser, remoteHost, sourceFile.replace(" ", "\\ "))
    1116           command = resolveCommand(rcpCommandList)
    1117           result = executeCommand(command, [copySource, targetFile])[0]
    1118           if result != 0:
    1119              raise IOError("Error (%d) copying [%s] from remote host." % (result, sourceFile))
    1120        if not os.path.exists(targetFile):
    1121           raise IOError("Apparently unable to copy file from remote host.")
    1122        if ownership is not None:
    1123           os.chown(targetFile, ownership[0], ownership[1])
    1124        if permissions is not None:
    1125           os.chmod(targetFile, permissions)
    1126
    1127     @staticmethod
    1128 -   def _pushLocalFile(remoteUser, localUser, remoteHost,
    1129                        rcpCommand, rcpCommandList,
    1130                        sourceFile, targetFile, overwrite=True):
    1131        """
    1132        Copies a local source file to a remote host.
    1133
    1134        @note: We will not overwrite a target file that exists when this method
    1135        is invoked.  If the target already exists, we'll raise an exception.
    1136
    1137        @note: Internally, we have to go through and escape any spaces in the
    1138        source and target paths with double-backslash, otherwise things get
    1139        screwed up.  I hope this is portable to various different rcp methods,
    1140        but I guess it might not be (all I have to test with is OpenSSH).
    1141
    1142        @note: If you have user/group as strings, call the L{util.getUidGid} function
    1143        to get the associated uid/gid as an ownership tuple.
    1144
    1145        @param remoteUser: Name of the Cedar Backup user on the remote peer
    1146        @type remoteUser: String representing a username, valid via the copy command
    1147
    1148        @param localUser: Name of the Cedar Backup user on the current host
    1149        @type localUser: String representing a username, valid on the current host
    1150
    1151        @param remoteHost: Hostname of the remote peer
    1152        @type remoteHost: String representing a hostname, accessible via the copy command
    1153
    1154        @param rcpCommand: An rcp-compatible copy command to use for copying files from the peer
    1155        @type rcpCommand: String representing a system command including required arguments
    1156
    1157        @param rcpCommandList: An rcp-compatible copy command to use for copying files
    1158        @type rcpCommandList: Command as a list to be passed to L{util.executeCommand}
    1159
    1160        @param sourceFile: Source file to copy
    1161        @type sourceFile: String representing a file on disk, as an absolute path
    1162
    1163        @param targetFile: Target file to create
    1164        @type targetFile: String representing a file on disk, as an absolute path
    1165
    1166        @param overwrite: Indicates whether it's OK to overwrite the target file.
    1167        @type overwrite: Boolean true/false.
    1168
    1169        @raise IOError: If there is an IO error copying the file
    1170        @raise OSError: If there is an OS error changing permissions on the file
    1171        """
    1172        if not overwrite:
    1173           if os.path.exists(targetFile):
    1174              raise IOError("Target file [%s] already exists." % targetFile)
    1175        if localUser is not None:
    1176           try:
    1177              if not isRunningAsRoot():
    1178                 raise IOError("Only root can remote copy as another user.")
    1179           except AttributeError: pass
    1180           actualCommand = '%s "%s" "%s@%s:%s"' % (rcpCommand, sourceFile, remoteUser, remoteHost, targetFile)
    1181           command = resolveCommand(SU_COMMAND)
    1182           result = executeCommand(command, [localUser, "-c", actualCommand])[0]
    1183           if result != 0:
    1184              raise IOError("Error (%d) copying [%s] to remote host as local user [%s]." % (result, sourceFile, localUser))
    1185        else:
    1186           copyTarget = "%s@%s:%s" % (remoteUser, remoteHost, targetFile.replace(" ", "\\ "))
    1187           command = resolveCommand(rcpCommandList)
    1188           result = executeCommand(command, [sourceFile.replace(" ", "\\ "), copyTarget])[0]
    1189           if result != 0:
    1190              raise IOError("Error (%d) copying [%s] to remote host." % (result, sourceFile))
    1191
    1192     @staticmethod
    1193 -   def _executeRemoteCommand(remoteUser, localUser, remoteHost, rshCommand, rshCommandList, remoteCommand):
    1194        """
    1195        Executes a command on the peer via remote shell.
    1196
    1197        @param remoteUser: Name of the Cedar Backup user on the remote peer
    1198        @type remoteUser: String representing a username, valid on the remote host
    1199
    1200        @param localUser: Name of the Cedar Backup user on the current host
    1201        @type localUser: String representing a username, valid on the current host
    1202
    1203        @param remoteHost: Hostname of the remote peer
    1204        @type remoteHost: String representing a hostname, accessible via the copy command
    1205
    1206        @param rshCommand: An rsh-compatible copy command to use for remote shells to the peer
    1207        @type rshCommand: String representing a system command including required arguments
    1208
    1209        @param rshCommandList: An rsh-compatible copy command to use for remote shells to the peer
    1210        @type rshCommandList: Command as a list to be passed to L{util.executeCommand}
    1211
    1212        @param remoteCommand: The command to be executed on the remote host
    1213        @type remoteCommand: String command-line, with no special shell characters ($, <, etc.)
    1214
    1215        @raise IOError: If there is an error executing the remote command
    1216        """
    1217        actualCommand = "%s %s@%s '%s'" % (rshCommand, remoteUser, remoteHost, remoteCommand)
    1218        if localUser is not None:
    1219           try:
    1220              if not isRunningAsRoot():
    1221                 raise IOError("Only root can remote shell as another user.")
    1222           except AttributeError: pass
    1223           command = resolveCommand(SU_COMMAND)
    1224           result = executeCommand(command, [localUser, "-c", actualCommand])[0]
    1225           if result != 0:
    1226              raise IOError("Command failed [su -c %s \"%s\"]" % (localUser, actualCommand))
    1227        else:
    1228           command = resolveCommand(rshCommandList)
    1229           result = executeCommand(command, ["%s@%s" % (remoteUser, remoteHost), "%s" % remoteCommand])[0]
    1230           if result != 0:
    1231              raise IOError("Command failed [%s]" % (actualCommand))
    1232
    1233     @staticmethod
    1234 -   def _buildCbackCommand(cbackCommand, action, fullBackup):
    1235        """
    1236        Builds a Cedar Backup command line for the named action.
    1237
    1238        @note: If the cback command is None, then DEF_CBACK_COMMAND is used.
    1239
    1240        @param cbackCommand: cback command to execute, including required options
    1241        @param action: Name of the action to execute.
    1242        @param fullBackup: Whether a full backup should be executed.
    1243
    1244        @return: String suitable for passing to L{_executeRemoteCommand} as remoteCommand.
    1245        @raise ValueError: If action is None.
    1246        """
    1247        if action is None:
    1248           raise ValueError("Action cannot be None.")
    1249        if cbackCommand is None:
    1250           cbackCommand = DEF_CBACK_COMMAND
    1251        if fullBackup:
    1252           return "%s --full %s" % (cbackCommand, action)
    1253        else:
    1254           return "%s %s" % (cbackCommand, action)
    1255