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.5) 
  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