CedarBackup3.peer

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

# Source Code for Module CedarBackup3.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,2015 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 3 (>= 3.4)
      29  # Project  : Cedar Backup, release 3
      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 CedarBackup3.filesystem import FilesystemList
      61  from CedarBackup3.util import resolveCommand, executeCommand, isRunningAsRoot
      62  from CedarBackup3.util import splitCommandLine, encodePath
      63  from CedarBackup3.config import VALID_FAILURE_MODES
      64
      65
      66  ########################################################################
      67  # Module-wide constants and variables
      68  ########################################################################
      69
      70  logger                  = logging.getLogger("CedarBackup3.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/cback3"
      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           with open(targetFile, "w") as f:
     407              f.write("")
     408        else:
     409           if os.path.isfile(sourceFile) and not os.path.islink(sourceFile):
     410              shutil.copy(sourceFile, targetFile)
     411           else:
     412              logger.debug("Source [%s] is not a regular file.", sourceFile)
     413              raise ValueError("Source is not a regular file.")
     414        if ownership is not None:
     415           os.chown(targetFile, ownership[0], ownership[1])
     416        if permissions is not None:
     417           os.chmod(targetFile, permissions)
     418
     419
     420  ########################################################################
     421  # RemotePeer class definition
     422  ########################################################################
     423
     424 -class RemotePeer(object):
     425
     426     ######################
     427     # Class documentation
     428     ######################
     429
     430     """
     431     Backup peer representing a remote peer in a backup pool.
     432
     433     This is a class representing a remote (networked) peer in a backup pool.
     434     Remote peers are backed up using an rcp-compatible copy command.  A remote
     435     peer has associated with it a name (which must be a valid hostname), a
     436     collect directory, a working directory and a copy method (an rcp-compatible
     437     command).
     438
     439     You can also set an optional local user value.  This username will be used
     440     as the local user for any remote copies that are required.  It can only be
     441     used if the root user is executing the backup.  The root user will C{su} to
     442     the local user and execute the remote copies as that user.
     443
     444     The copy method is associated with the peer and not with the actual request
     445     to copy, because we can envision that each remote host might have a
     446     different connect method.
     447
     448     The public methods other than the constructor are part of a "backup peer"
     449     interface shared with the C{LocalPeer} class.
     450
     451     @sort: __init__, stagePeer, checkCollectIndicator, writeStageIndicator,
     452            executeRemoteCommand, executeManagedAction, _getDirContents,
     453            _copyRemoteDir, _copyRemoteFile, _pushLocalFile, name, collectDir,
     454            remoteUser, rcpCommand, rshCommand, cbackCommand
     455     """
     456
     457     ##############
     458     # Constructor
     459     ##############
     460
     461 -   def __init__(self, name=None, collectDir=None, workingDir=None, remoteUser=None,
     462                  rcpCommand=None, localUser=None, rshCommand=None, cbackCommand=None,
     463                  ignoreFailureMode=None):
     464        """
     465        Initializes a remote backup peer.
     466
     467        @note: If provided, each command will eventually be parsed into a list of
     468        strings suitable for passing to C{util.executeCommand} in order to avoid
     469        security holes related to shell interpolation.   This parsing will be
     470        done by the L{util.splitCommandLine} function.  See the documentation for
     471        that function for some important notes about its limitations.
     472
     473        @param name: Name of the backup peer
     474        @type name: String, must be a valid DNS hostname
     475
     476        @param collectDir: Path to the peer's collect directory
     477        @type collectDir: String representing an absolute path on the remote peer
     478
     479        @param workingDir: Working directory that can be used to create temporary files, etc.
     480        @type workingDir: String representing an absolute path on the current host.
     481
     482        @param remoteUser: Name of the Cedar Backup user on the remote peer
     483        @type remoteUser: String representing a username, valid via remote shell to the peer
     484
     485        @param localUser: Name of the Cedar Backup user on the current host
     486        @type localUser: String representing a username, valid on the current host
     487
     488        @param rcpCommand: An rcp-compatible copy command to use for copying files from the peer
     489        @type rcpCommand: String representing a system command including required arguments
     490
     491        @param rshCommand: An rsh-compatible copy command to use for remote shells to the peer
     492        @type rshCommand: String representing a system command including required arguments
     493
     494        @param cbackCommand: A chack-compatible command to use for executing managed actions
     495        @type cbackCommand: String representing a system command including required arguments
     496
     497        @param ignoreFailureMode: Ignore failure mode for this peer
     498        @type ignoreFailureMode: One of VALID_FAILURE_MODES
     499
     500        @raise ValueError: If collect directory is not an absolute path
     501        """
     502        self._name = None
     503        self._collectDir = None
     504        self._workingDir = None
     505        self._remoteUser = None
     506        self._localUser = None
     507        self._rcpCommand = None
     508        self._rcpCommandList = None
     509        self._rshCommand = None
     510        self._rshCommandList = None
     511        self._cbackCommand = None
     512        self._ignoreFailureMode = None
     513        self.name = name
     514        self.collectDir = collectDir
     515        self.workingDir = workingDir
     516        self.remoteUser = remoteUser
     517        self.localUser = localUser
     518        self.rcpCommand = rcpCommand
     519        self.rshCommand = rshCommand
     520        self.cbackCommand = cbackCommand
     521        self.ignoreFailureMode = ignoreFailureMode
     522
     523
     524     #############
     525     # Properties
     526     #############
     527
     528 -   def _setName(self, value):
     529        """
     530        Property target used to set the peer name.
     531        The value must be a non-empty string and cannot be C{None}.
     532        @raise ValueError: If the value is an empty string or C{None}.
     533        """
     534        if value is None or len(value) < 1:
     535           raise ValueError("Peer name must be a non-empty string.")
     536        self._name = value
     537
     538 -   def _getName(self):
     539        """
     540        Property target used to get the peer name.
     541        """
     542        return self._name
     543
     544 -   def _setCollectDir(self, value):
     545        """
     546        Property target used to set the collect directory.
     547        The value must be an absolute path and cannot be C{None}.
     548        It does not have to exist on disk at the time of assignment.
     549        @raise ValueError: If the value is C{None} or is not an absolute path.
     550        @raise ValueError: If the value cannot be encoded properly.
     551        """
     552        if value is not None:
     553           if not os.path.isabs(value):
     554              raise ValueError("Collect directory must be an absolute path.")
     555        self._collectDir = encodePath(value)
     556
     557 -   def _getCollectDir(self):
     558        """
     559        Property target used to get the collect directory.
     560        """
     561        return self._collectDir
     562
     563 -   def _setWorkingDir(self, value):
     564        """
     565        Property target used to set the working directory.
     566        The value must be an absolute path and cannot be C{None}.
     567        @raise ValueError: If the value is C{None} or is not an absolute path.
     568        @raise ValueError: If the value cannot be encoded properly.
     569        """
     570        if value is not None:
     571           if not os.path.isabs(value):
     572              raise ValueError("Working directory must be an absolute path.")
     573        self._workingDir = encodePath(value)
     574
     575 -   def _getWorkingDir(self):
     576        """
     577        Property target used to get the working directory.
     578        """
     579        return self._workingDir
     580
     581 -   def _setRemoteUser(self, value):
     582        """
     583        Property target used to set the remote user.
     584        The value must be a non-empty string and cannot be C{None}.
     585        @raise ValueError: If the value is an empty string or C{None}.
     586        """
     587        if value is None or len(value) < 1:
     588           raise ValueError("Peer remote user must be a non-empty string.")
     589        self._remoteUser = value
     590
     591 -   def _getRemoteUser(self):
     592        """
     593        Property target used to get the remote user.
     594        """
     595        return self._remoteUser
     596
     597 -   def _setLocalUser(self, value):
     598        """
     599        Property target used to set the local user.
     600        The value must be a non-empty string if it is not C{None}.
     601        @raise ValueError: If the value is an empty string.
     602        """
     603        if value is not None:
     604           if len(value) < 1:
     605              raise ValueError("Peer local user must be a non-empty string.")
     606        self._localUser = value
     607
     608 -   def _getLocalUser(self):
     609        """
     610        Property target used to get the local user.
     611        """
     612        return self._localUser
     613
     614 -   def _setRcpCommand(self, value):
     615        """
     616        Property target to set the rcp command.
     617
     618        The value must be a non-empty string or C{None}.  Its value is stored in
     619        the two forms: "raw" as provided by the client, and "parsed" into a list
     620        suitable for being passed to L{util.executeCommand} via
     621        L{util.splitCommandLine}.
     622
     623        However, all the caller will ever see via the property is the actual
     624        value they set (which includes seeing C{None}, even if we translate that
     625        internally to C{DEF_RCP_COMMAND}).  Internally, we should always use
     626        C{self._rcpCommandList} if we want the actual command list.
     627
     628        @raise ValueError: If the value is an empty string.
     629        """
     630        if value is None:
     631           self._rcpCommand = None
     632           self._rcpCommandList = DEF_RCP_COMMAND
     633        else:
     634           if len(value) >= 1:
     635              self._rcpCommand = value
     636              self._rcpCommandList = splitCommandLine(self._rcpCommand)
     637           else:
     638              raise ValueError("The rcp command must be a non-empty string.")
     639
     640 -   def _getRcpCommand(self):
     641        """
     642        Property target used to get the rcp command.
     643        """
     644        return self._rcpCommand
     645
     646 -   def _setRshCommand(self, value):
     647        """
     648        Property target to set the rsh command.
     649
     650        The value must be a non-empty string or C{None}.  Its value is stored in
     651        the two forms: "raw" as provided by the client, and "parsed" into a list
     652        suitable for being passed to L{util.executeCommand} via
     653        L{util.splitCommandLine}.
     654
     655        However, all the caller will ever see via the property is the actual
     656        value they set (which includes seeing C{None}, even if we translate that
     657        internally to C{DEF_RSH_COMMAND}).  Internally, we should always use
     658        C{self._rshCommandList} if we want the actual command list.
     659
     660        @raise ValueError: If the value is an empty string.
     661        """
     662        if value is None:
     663           self._rshCommand = None
     664           self._rshCommandList = DEF_RSH_COMMAND
     665        else:
     666           if len(value) >= 1:
     667              self._rshCommand = value
     668              self._rshCommandList = splitCommandLine(self._rshCommand)
     669           else:
     670              raise ValueError("The rsh command must be a non-empty string.")
     671
     672 -   def _getRshCommand(self):
     673        """
     674        Property target used to get the rsh command.
     675        """
     676        return self._rshCommand
     677
     678 -   def _setCbackCommand(self, value):
     679        """
     680        Property target to set the cback command.
     681
     682        The value must be a non-empty string or C{None}.  Unlike the other
     683        command, this value is only stored in the "raw" form provided by the
     684        client.
     685
     686        @raise ValueError: If the value is an empty string.
     687        """
     688        if value is None:
     689           self._cbackCommand = None
     690        else:
     691           if len(value) >= 1:
     692              self._cbackCommand = value
     693           else:
     694              raise ValueError("The cback command must be a non-empty string.")
     695
     696 -   def _getCbackCommand(self):
     697        """
     698        Property target used to get the cback command.
     699        """
     700        return self._cbackCommand
     701
     702 -   def _setIgnoreFailureMode(self, value):
     703        """
     704        Property target used to set the ignoreFailure mode.
     705        If not C{None}, the mode must be one of the values in L{VALID_FAILURE_MODES}.
     706        @raise ValueError: If the value is not valid.
     707        """
     708        if value is not None:
     709           if value not in VALID_FAILURE_MODES:
     710              raise ValueError("Ignore failure mode must be one of %s." % VALID_FAILURE_MODES)
     711        self._ignoreFailureMode = value
     712
     713 -   def _getIgnoreFailureMode(self):
     714        """
     715        Property target used to get the ignoreFailure mode.
     716        """
     717        return self._ignoreFailureMode
     718
     719     name = property(_getName, _setName, None, "Name of the peer (a valid DNS hostname).")
     720     collectDir = property(_getCollectDir, _setCollectDir, None, "Path to the peer's collect directory (an absolute local path).")
     721     workingDir = property(_getWorkingDir, _setWorkingDir, None, "Path to the peer's working directory (an absolute local path).")
     722     remoteUser = property(_getRemoteUser, _setRemoteUser, None, "Name of the Cedar Backup user on the remote peer.")
     723     localUser = property(_getLocalUser, _setLocalUser, None, "Name of the Cedar Backup user on the current host.")
     724     rcpCommand = property(_getRcpCommand, _setRcpCommand, None, "An rcp-compatible copy command to use for copying files.")
     725     rshCommand = property(_getRshCommand, _setRshCommand, None, "An rsh-compatible command to use for remote shells to the peer.")
     726     cbackCommand = property(_getCbackCommand, _setCbackCommand, None, "A chack-compatible command to use for executing managed actions.")
     727     ignoreFailureMode = property(_getIgnoreFailureMode, _setIgnoreFailureMode, None, "Ignore failure mode for peer.")
     728
     729
     730     #################
     731     # Public methods
     732     #################
     733
     734 -   def stagePeer(self, targetDir, ownership=None, permissions=None):
     735        """
     736        Stages data from the peer into the indicated local target directory.
     737
     738        The target directory must already exist before this method is called.  If
     739        passed in, ownership and permissions will be applied to the files that
     740        are copied.
     741
     742        @note: The returned count of copied files might be inaccurate if some of
     743        the copied files already existed in the staging directory prior to the
     744        copy taking place.  We don't clear the staging directory first, because
     745        some extension might also be using it.
     746
     747        @note: If you have user/group as strings, call the L{util.getUidGid} function
     748        to get the associated uid/gid as an ownership tuple.
     749
     750        @note: Unlike the local peer version of this method, an I/O error might
     751        or might not be raised if the directory is empty.  Since we're using a
     752        remote copy method, we just don't have the fine-grained control over our
     753        exceptions that's available when we can look directly at the filesystem,
     754        and we can't control whether the remote copy method thinks an empty
     755        directory is an error.
     756
     757        @param targetDir: Target directory to write data into
     758        @type targetDir: String representing a directory on disk
     759
     760        @param ownership: Owner and group that the staged files should have
     761        @type ownership: Tuple of numeric ids C{(uid, gid)}
     762
     763        @param permissions: Permissions that the staged files should have
     764        @type permissions: UNIX permissions mode, specified in octal (i.e. C{0640}).
     765
     766        @return: Number of files copied from the source directory to the target directory.
     767
     768        @raise ValueError: If target directory is not a directory, does not exist or is not absolute.
     769        @raise ValueError: If a path cannot be encoded properly.
     770        @raise IOError: If there were no files to stage (i.e. the directory was empty)
     771        @raise IOError: If there is an IO error copying a file.
     772        @raise OSError: If there is an OS error copying or changing permissions on a file
     773        """
     774        targetDir = encodePath(targetDir)
     775        if not os.path.isabs(targetDir):
     776           logger.debug("Target directory [%s] not an absolute path.", targetDir)
     777           raise ValueError("Target directory must be an absolute path.")
     778        if not os.path.exists(targetDir) or not os.path.isdir(targetDir):
     779           logger.debug("Target directory [%s] is not a directory or does not exist on disk.", targetDir)
     780           raise ValueError("Target directory is not a directory or does not exist on disk.")
     781        count = RemotePeer._copyRemoteDir(self.remoteUser, self.localUser, self.name,
     782                                          self._rcpCommand, self._rcpCommandList,
     783                                          self.collectDir, targetDir,
     784                                          ownership, permissions)
     785        if count == 0:
     786           raise IOError("Did not copy any files from local peer.")
     787        return count
     788
     789 -   def checkCollectIndicator(self, collectIndicator=None):
     790        """
     791        Checks the collect indicator in the peer's staging directory.
     792
     793        When a peer has completed collecting its backup files, it will write an
     794        empty indicator file into its collect directory.  This method checks to
     795        see whether that indicator has been written.  If the remote copy command
     796        fails, we return C{False} as if the file weren't there.
     797
     798        If you need to, you can override the name of the collect indicator file
     799        by passing in a different name.
     800
     801        @note: Apparently, we can't count on all rcp-compatible implementations
     802        to return sensible errors for some error conditions.  As an example, the
     803        C{scp} command in Debian 'woody' returns a zero (normal) status even when
     804        it can't find a host or if the login or path is invalid.  Because of
     805        this, the implementation of this method is rather convoluted.
     806
     807        @param collectIndicator: Name of the collect indicator file to check
     808        @type collectIndicator: String representing name of a file in the collect directory
     809
     810        @return: Boolean true/false depending on whether the indicator exists.
     811        @raise ValueError: If a path cannot be encoded properly.
     812        """
     813        try:
     814           if collectIndicator is None:
     815              sourceFile = os.path.join(self.collectDir, DEF_COLLECT_INDICATOR)
     816              targetFile = os.path.join(self.workingDir, DEF_COLLECT_INDICATOR)
     817           else:
     818              collectIndicator = encodePath(collectIndicator)
     819              sourceFile = os.path.join(self.collectDir, collectIndicator)
     820              targetFile = os.path.join(self.workingDir, collectIndicator)
     821           logger.debug("Fetch remote [%s] into [%s].", sourceFile, targetFile)
     822           if os.path.exists(targetFile):
     823              try:
     824                 os.remove(targetFile)
     825              except:
     826                 raise Exception("Error: collect indicator [%s] already exists!" % targetFile)
     827           try:
     828              RemotePeer._copyRemoteFile(self.remoteUser, self.localUser, self.name,
     829                                         self._rcpCommand, self._rcpCommandList,
     830                                         sourceFile, targetFile,
     831                                         overwrite=False)
     832              if os.path.exists(targetFile):
     833                 return True
     834              else:
     835                 return False
     836           except Exception as e:
     837              logger.info("Failed looking for collect indicator: %s", e)
     838              return False
     839        finally:
     840           if os.path.exists(targetFile):
     841              try:
     842                 os.remove(targetFile)
     843              except: pass
     844
     845 -   def writeStageIndicator(self, stageIndicator=None):
     846        """
     847        Writes the stage indicator in the peer's staging directory.
     848
     849        When the master has completed collecting its backup files, it will write
     850        an empty indicator file into the peer's collect directory.  The presence
     851        of this file implies that the staging process is complete.
     852
     853        If you need to, you can override the name of the stage indicator file by
     854        passing in a different name.
     855
     856        @note: If you have user/group as strings, call the L{util.getUidGid} function
     857        to get the associated uid/gid as an ownership tuple.
     858
     859        @param stageIndicator: Name of the indicator file to write
     860        @type stageIndicator: String representing name of a file in the collect directory
     861
     862        @raise ValueError: If a path cannot be encoded properly.
     863        @raise IOError: If there is an IO error creating the file.
     864        @raise OSError: If there is an OS error creating or changing permissions on the file
     865        """
     866        stageIndicator = encodePath(stageIndicator)
     867        if stageIndicator is None:
     868           sourceFile = os.path.join(self.workingDir, DEF_STAGE_INDICATOR)
     869           targetFile = os.path.join(self.collectDir, DEF_STAGE_INDICATOR)
     870        else:
     871           sourceFile = os.path.join(self.workingDir, DEF_STAGE_INDICATOR)
     872           targetFile = os.path.join(self.collectDir, stageIndicator)
     873        try:
     874           if not os.path.exists(sourceFile):
     875              with open(sourceFile, "w") as f:
     876                 f.write("")
     877           RemotePeer._pushLocalFile(self.remoteUser, self.localUser, self.name,
     878                                     self._rcpCommand, self._rcpCommandList,
     879                                     sourceFile, targetFile)
     880        finally:
     881           if os.path.exists(sourceFile):
     882              try:
     883                 os.remove(sourceFile)
     884              except: pass
     885
     886 -   def executeRemoteCommand(self, command):
     887        """
     888        Executes a command on the peer via remote shell.
     889
     890        @param command: Command to execute
     891        @type command: String command-line suitable for use with rsh.
     892
     893        @raise IOError: If there is an error executing the command on the remote peer.
     894        """
     895        RemotePeer._executeRemoteCommand(self.remoteUser, self.localUser,
     896                                         self.name, self._rshCommand,
     897                                         self._rshCommandList, command)
     898
     899 -   def executeManagedAction(self, action, fullBackup):
     900        """
     901        Executes a managed action on this peer.
     902
     903        @param action: Name of the action to execute.
     904        @param fullBackup: Whether a full backup should be executed.
     905
     906        @raise IOError: If there is an error executing the action on the remote peer.
     907        """
     908        try:
     909           command = RemotePeer._buildCbackCommand(self.cbackCommand, action, fullBackup)
     910           self.executeRemoteCommand(command)
     911        except IOError as e:
     912           logger.info(e)
     913           raise IOError("Failed to execute action [%s] on managed client [%s]." % (action, self.name))
     914
     915
     916     ##################
     917     # Private methods
     918     ##################
     919
     920     @staticmethod
     921 -   def _getDirContents(path):
     922        """
     923        Returns the contents of a directory in terms of a Set.
     924
     925        The directory's contents are read as a L{FilesystemList} containing only
     926        files, and then the list is converted into a set object for later use.
     927
     928        @param path: Directory path to get contents for
     929        @type path: String representing a path on disk
     930
     931        @return: Set of files in the directory
     932        @raise ValueError: If path is not a directory or does not exist.
     933        """
     934        contents = FilesystemList()
     935        contents.excludeDirs = True
     936        contents.excludeLinks = True
     937        contents.addDirContents(path)
     938        return set(contents)
     939
     940     @staticmethod
     941 -   def _copyRemoteDir(remoteUser, localUser, remoteHost, rcpCommand, rcpCommandList,
     942                        sourceDir, targetDir, ownership=None, permissions=None):
     943        """
     944        Copies files from the source directory to the target directory.
     945
     946        This function is not recursive.  Only the files in the directory will be
     947        copied.   Ownership and permissions will be left at their default values
     948        if new values are not specified.  Behavior when copying soft links from
     949        the collect directory is dependent on the behavior of the specified rcp
     950        command.
     951
     952        @note: The returned count of copied files might be inaccurate if some of
     953        the copied files already existed in the staging directory prior to the
     954        copy taking place.  We don't clear the staging directory first, because
     955        some extension might also be using it.
     956
     957        @note: If you have user/group as strings, call the L{util.getUidGid} function
     958        to get the associated uid/gid as an ownership tuple.
     959
     960        @note: We don't have a good way of knowing exactly what files we copied
     961        down from the remote peer, unless we want to parse the output of the rcp
     962        command (ugh).  We could change permissions on everything in the target
     963        directory, but that's kind of ugly too.  Instead, we use Python's set
     964        functionality to figure out what files were added while we executed the
     965        rcp command.  This isn't perfect - for instance, it's not correct if
     966        someone else is messing with the directory at the same time we're doing
     967        the remote copy - but it's about as good as we're going to get.
     968
     969        @note: Apparently, we can't count on all rcp-compatible implementations
     970        to return sensible errors for some error conditions.  As an example, the
     971        C{scp} command in Debian 'woody' returns a zero (normal) status even
     972        when it can't find a host or if the login or path is invalid.  We try
     973        to work around this by issuing C{IOError} if we don't copy any files from
     974        the remote host.
     975
     976        @param remoteUser: Name of the Cedar Backup user on the remote peer
     977        @type remoteUser: String representing a username, valid via the copy command
     978
     979        @param localUser: Name of the Cedar Backup user on the current host
     980        @type localUser: String representing a username, valid on the current host
     981
     982        @param remoteHost: Hostname of the remote peer
     983        @type remoteHost: String representing a hostname, accessible via the copy command
     984
     985        @param rcpCommand: An rcp-compatible copy command to use for copying files from the peer
     986        @type rcpCommand: String representing a system command including required arguments
     987
     988        @param rcpCommandList: An rcp-compatible copy command to use for copying files
     989        @type rcpCommandList: Command as a list to be passed to L{util.executeCommand}
     990
     991        @param sourceDir: Source directory
     992        @type sourceDir: String representing a directory on disk
     993
     994        @param targetDir: Target directory
     995        @type targetDir: String representing a directory on disk
     996
     997        @param ownership: Owner and group that the copied files should have
     998        @type ownership: Tuple of numeric ids C{(uid, gid)}
     999
    1000        @param permissions: Permissions that the staged files should have
    1001        @type permissions: UNIX permissions mode, specified in octal (i.e. C{0640}).
    1002
    1003        @return: Number of files copied from the source directory to the target directory.
    1004
    1005        @raise ValueError: If source or target is not a directory or does not exist.
    1006        @raise IOError: If there is an IO error copying the files.
    1007        """
    1008        beforeSet = RemotePeer._getDirContents(targetDir)
    1009        if localUser is not None:
    1010           try:
    1011              if not isRunningAsRoot():
    1012                 raise IOError("Only root can remote copy as another user.")
    1013           except AttributeError: pass
    1014           actualCommand = "%s %s@%s:%s/* %s" % (rcpCommand, remoteUser, remoteHost, sourceDir, targetDir)
    1015           command = resolveCommand(SU_COMMAND)
    1016           result = executeCommand(command, [localUser, "-c", actualCommand])[0]
    1017           if result != 0:
    1018              raise IOError("Error (%d) copying files from remote host as local user [%s]." % (result, localUser))
    1019        else:
    1020           copySource = "%s@%s:%s/*" % (remoteUser, remoteHost, sourceDir)
    1021           command = resolveCommand(rcpCommandList)
    1022           result = executeCommand(command, [copySource, targetDir])[0]
    1023           if result != 0:
    1024              raise IOError("Error (%d) copying files from remote host." % result)
    1025        afterSet = RemotePeer._getDirContents(targetDir)
    1026        if len(afterSet) == 0:
    1027           raise IOError("Did not copy any files from remote peer.")
    1028        differenceSet = afterSet.difference(beforeSet)  # files we added as part of copy
    1029        if len(differenceSet) == 0:
    1030           raise IOError("Apparently did not copy any new files from remote peer.")
    1031        for targetFile in differenceSet:
    1032           if ownership is not None:
    1033              os.chown(targetFile, ownership[0], ownership[1])
    1034           if permissions is not None:
    1035              os.chmod(targetFile, permissions)
    1036        return len(differenceSet)
    1037
    1038     @staticmethod
    1039 -   def _copyRemoteFile(remoteUser, localUser, remoteHost,
    1040                         rcpCommand, rcpCommandList,
    1041                         sourceFile, targetFile, ownership=None,
    1042                         permissions=None, overwrite=True):
    1043        """
    1044        Copies a remote source file to a target file.
    1045
    1046        @note: Internally, we have to go through and escape any spaces in the
    1047        source path with double-backslash, otherwise things get screwed up.   It
    1048        doesn't seem to be required in the target path. I hope this is portable
    1049        to various different rcp methods, but I guess it might not be (all I have
    1050        to test with is OpenSSH).
    1051
    1052        @note: If you have user/group as strings, call the L{util.getUidGid} function
    1053        to get the associated uid/gid as an ownership tuple.
    1054
    1055        @note: We will not overwrite a target file that exists when this method
    1056        is invoked.  If the target already exists, we'll raise an exception.
    1057
    1058        @note: Apparently, we can't count on all rcp-compatible implementations
    1059        to return sensible errors for some error conditions.  As an example, the
    1060        C{scp} command in Debian 'woody' returns a zero (normal) status even when
    1061        it can't find a host or if the login or path is invalid.  We try to work
    1062        around this by issuing C{IOError} the target file does not exist when
    1063        we're done.
    1064
    1065        @param remoteUser: Name of the Cedar Backup user on the remote peer
    1066        @type remoteUser: String representing a username, valid via the copy command
    1067
    1068        @param remoteHost: Hostname of the remote peer
    1069        @type remoteHost: String representing a hostname, accessible via the copy command
    1070
    1071        @param localUser: Name of the Cedar Backup user on the current host
    1072        @type localUser: String representing a username, valid on the current host
    1073
    1074        @param rcpCommand: An rcp-compatible copy command to use for copying files from the peer
    1075        @type rcpCommand: String representing a system command including required arguments
    1076
    1077        @param rcpCommandList: An rcp-compatible copy command to use for copying files
    1078        @type rcpCommandList: Command as a list to be passed to L{util.executeCommand}
    1079
    1080        @param sourceFile: Source file to copy
    1081        @type sourceFile: String representing a file on disk, as an absolute path
    1082
    1083        @param targetFile: Target file to create
    1084        @type targetFile: String representing a file on disk, as an absolute path
    1085
    1086        @param ownership: Owner and group that the copied should have
    1087        @type ownership: Tuple of numeric ids C{(uid, gid)}
    1088
    1089        @param permissions: Permissions that the staged files should have
    1090        @type permissions: UNIX permissions mode, specified in octal (i.e. C{0640}).
    1091
    1092        @param overwrite: Indicates whether it's OK to overwrite the target file.
    1093        @type overwrite: Boolean true/false.
    1094
    1095        @raise IOError: If the target file already exists.
    1096        @raise IOError: If there is an IO error copying the file
    1097        @raise OSError: If there is an OS error changing permissions on the file
    1098        """
    1099        if not overwrite:
    1100           if os.path.exists(targetFile):
    1101              raise IOError("Target file [%s] already exists." % targetFile)
    1102        if localUser is not None:
    1103           try:
    1104              if not isRunningAsRoot():
    1105                 raise IOError("Only root can remote copy as another user.")
    1106           except AttributeError: pass
    1107           actualCommand = "%s %s@%s:%s %s" % (rcpCommand, remoteUser, remoteHost, sourceFile.replace(" ", "\\ "), targetFile)
    1108           command = resolveCommand(SU_COMMAND)
    1109           result = executeCommand(command, [localUser, "-c", actualCommand])[0]
    1110           if result != 0:
    1111              raise IOError("Error (%d) copying [%s] from remote host as local user [%s]." % (result, sourceFile, localUser))
    1112        else:
    1113           copySource = "%s@%s:%s" % (remoteUser, remoteHost, sourceFile.replace(" ", "\\ "))
    1114           command = resolveCommand(rcpCommandList)
    1115           result = executeCommand(command, [copySource, targetFile])[0]
    1116           if result != 0:
    1117              raise IOError("Error (%d) copying [%s] from remote host." % (result, sourceFile))
    1118        if not os.path.exists(targetFile):
    1119           raise IOError("Apparently unable to copy file from remote host.")
    1120        if ownership is not None:
    1121           os.chown(targetFile, ownership[0], ownership[1])
    1122        if permissions is not None:
    1123           os.chmod(targetFile, permissions)
    1124
    1125     @staticmethod
    1126 -   def _pushLocalFile(remoteUser, localUser, remoteHost,
    1127                        rcpCommand, rcpCommandList,
    1128                        sourceFile, targetFile, overwrite=True):
    1129        """
    1130        Copies a local source file to a remote host.
    1131
    1132        @note: We will not overwrite a target file that exists when this method
    1133        is invoked.  If the target already exists, we'll raise an exception.
    1134
    1135        @note: Internally, we have to go through and escape any spaces in the
    1136        source and target paths with double-backslash, otherwise things get
    1137        screwed up.  I hope this is portable to various different rcp methods,
    1138        but I guess it might not be (all I have to test with is OpenSSH).
    1139
    1140        @note: If you have user/group as strings, call the L{util.getUidGid} function
    1141        to get the associated uid/gid as an ownership tuple.
    1142
    1143        @param remoteUser: Name of the Cedar Backup user on the remote peer
    1144        @type remoteUser: String representing a username, valid via the copy command
    1145
    1146        @param localUser: Name of the Cedar Backup user on the current host
    1147        @type localUser: String representing a username, valid on the current host
    1148
    1149        @param remoteHost: Hostname of the remote peer
    1150        @type remoteHost: String representing a hostname, accessible via the copy command
    1151
    1152        @param rcpCommand: An rcp-compatible copy command to use for copying files from the peer
    1153        @type rcpCommand: String representing a system command including required arguments
    1154
    1155        @param rcpCommandList: An rcp-compatible copy command to use for copying files
    1156        @type rcpCommandList: Command as a list to be passed to L{util.executeCommand}
    1157
    1158        @param sourceFile: Source file to copy
    1159        @type sourceFile: String representing a file on disk, as an absolute path
    1160
    1161        @param targetFile: Target file to create
    1162        @type targetFile: String representing a file on disk, as an absolute path
    1163
    1164        @param overwrite: Indicates whether it's OK to overwrite the target file.
    1165        @type overwrite: Boolean true/false.
    1166
    1167        @raise IOError: If there is an IO error copying the file
    1168        @raise OSError: If there is an OS error changing permissions on the file
    1169        """
    1170        if not overwrite:
    1171           if os.path.exists(targetFile):
    1172              raise IOError("Target file [%s] already exists." % targetFile)
    1173        if localUser is not None:
    1174           try:
    1175              if not isRunningAsRoot():
    1176                 raise IOError("Only root can remote copy as another user.")
    1177           except AttributeError: pass
    1178           actualCommand = '%s "%s" "%s@%s:%s"' % (rcpCommand, sourceFile, remoteUser, remoteHost, targetFile)
    1179           command = resolveCommand(SU_COMMAND)
    1180           result = executeCommand(command, [localUser, "-c", actualCommand])[0]
    1181           if result != 0:
    1182              raise IOError("Error (%d) copying [%s] to remote host as local user [%s]." % (result, sourceFile, localUser))
    1183        else:
    1184           copyTarget = "%s@%s:%s" % (remoteUser, remoteHost, targetFile.replace(" ", "\\ "))
    1185           command = resolveCommand(rcpCommandList)
    1186           result = executeCommand(command, [sourceFile.replace(" ", "\\ "), copyTarget])[0]
    1187           if result != 0:
    1188              raise IOError("Error (%d) copying [%s] to remote host." % (result, sourceFile))
    1189
    1190     @staticmethod
    1191 -   def _executeRemoteCommand(remoteUser, localUser, remoteHost, rshCommand, rshCommandList, remoteCommand):
    1192        """
    1193        Executes a command on the peer via remote shell.
    1194
    1195        @param remoteUser: Name of the Cedar Backup user on the remote peer
    1196        @type remoteUser: String representing a username, valid on the remote host
    1197
    1198        @param localUser: Name of the Cedar Backup user on the current host
    1199        @type localUser: String representing a username, valid on the current host
    1200
    1201        @param remoteHost: Hostname of the remote peer
    1202        @type remoteHost: String representing a hostname, accessible via the copy command
    1203
    1204        @param rshCommand: An rsh-compatible copy command to use for remote shells to the peer
    1205        @type rshCommand: String representing a system command including required arguments
    1206
    1207        @param rshCommandList: An rsh-compatible copy command to use for remote shells to the peer
    1208        @type rshCommandList: Command as a list to be passed to L{util.executeCommand}
    1209
    1210        @param remoteCommand: The command to be executed on the remote host
    1211        @type remoteCommand: String command-line, with no special shell characters ($, <, etc.)
    1212
    1213        @raise IOError: If there is an error executing the remote command
    1214        """
    1215        actualCommand = "%s %s@%s '%s'" % (rshCommand, remoteUser, remoteHost, remoteCommand)
    1216        if localUser is not None:
    1217           try:
    1218              if not isRunningAsRoot():
    1219                 raise IOError("Only root can remote shell as another user.")
    1220           except AttributeError: pass
    1221           command = resolveCommand(SU_COMMAND)
    1222           result = executeCommand(command, [localUser, "-c", actualCommand])[0]
    1223           if result != 0:
    1224              raise IOError("Command failed [su -c %s \"%s\"]" % (localUser, actualCommand))
    1225        else:
    1226           command = resolveCommand(rshCommandList)
    1227           result = executeCommand(command, ["%s@%s" % (remoteUser, remoteHost), "%s" % remoteCommand])[0]
    1228           if result != 0:
    1229              raise IOError("Command failed [%s]" % (actualCommand))
    1230
    1231     @staticmethod
    1232 -   def _buildCbackCommand(cbackCommand, action, fullBackup):
    1233        """
    1234        Builds a Cedar Backup command line for the named action.
    1235
    1236        @note: If the cback command is None, then DEF_CBACK_COMMAND is used.
    1237
    1238        @param cbackCommand: cback command to execute, including required options
    1239        @param action: Name of the action to execute.
    1240        @param fullBackup: Whether a full backup should be executed.
    1241
    1242        @return: String suitable for passing to L{_executeRemoteCommand} as remoteCommand.
    1243        @raise ValueError: If action is None.
    1244        """
    1245        if action is None:
    1246           raise ValueError("Action cannot be None.")
    1247        if cbackCommand is None:
    1248           cbackCommand = DEF_CBACK_COMMAND
    1249        if fullBackup:
    1250           return "%s --full %s" % (cbackCommand, action)
    1251        else:
    1252           return "%s %s" % (cbackCommand, action)
    1253