Package CedarBackup2 :: Package extend :: Module encrypt
[hide private]
[frames] | no frames]

Source Code for Module CedarBackup2.extend.encrypt

  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) 2007 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.3) 
 29  # Project  : Official Cedar Backup Extensions 
 30  # Revision : $Id: encrypt.py 703 2007-02-23 05:25:10Z pronovic $ 
 31  # Purpose  : Provides an extension to encrypt staging directories. 
 32  # 
 33  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
 34   
 35  ######################################################################## 
 36  # Module documentation 
 37  ######################################################################## 
 38   
 39  """ 
 40  Provides an extension to encrypt staging directories. 
 41   
 42  When this extension is executed, all backed-up files in the configured Cedar 
 43  Backup staging directory will be encrypted using gpg.  Any directory which has 
 44  already been encrypted (as indicated by the C{cback.encrypt} file) will be 
 45  ignored. 
 46   
 47  This extension requires a new configuration section <encrypt> and is intended 
 48  to be run immediately after the standard stage action or immediately before the 
 49  standard store action.  Aside from its own configuration, it requires the 
 50  options and staging configuration sections in the standard Cedar Backup 
 51  configuration file. 
 52   
 53  @author: Kenneth J. Pronovici <pronovic@ieee.org> 
 54  """ 
 55   
 56  ######################################################################## 
 57  # Imported modules 
 58  ######################################################################## 
 59   
 60  # System modules 
 61  import os 
 62  import logging 
 63   
 64  # Cedar Backup modules 
 65  from CedarBackup2.filesystem import FilesystemList 
 66  from CedarBackup2.util import resolveCommand, executeCommand, changeOwnership 
 67  from CedarBackup2.xmlutil import createInputDom, addContainerNode, addStringNode 
 68  from CedarBackup2.xmlutil import readFirstChild, readString 
 69  from CedarBackup2.actions.util import findDailyDirs, writeIndicatorFile, getBackupFiles 
 70   
 71   
 72  ######################################################################## 
 73  # Module-wide constants and variables 
 74  ######################################################################## 
 75   
 76  logger = logging.getLogger("CedarBackup2.log.extend.encrypt") 
 77   
 78  GPG_COMMAND = [ "gpg", ] 
 79  VALID_ENCRYPT_MODES = [ "gpg", ] 
 80  ENCRYPT_INDICATOR = "cback.encrypt" 
 81   
 82   
 83  ######################################################################## 
 84  # EncryptConfig class definition 
 85  ######################################################################## 
 86   
87 -class EncryptConfig(object):
88 89 """ 90 Class representing encrypt configuration. 91 92 Encrypt configuration is used for encrypting staging directories. 93 94 The following restrictions exist on data in this class: 95 96 - The encrypt mode must be one of the values in L{VALID_ENCRYPT_MODES} 97 - The encrypt target value must be a non-empty string 98 99 @sort: __init__, __repr__, __str__, __cmp__, encryptMode, encryptTarget 100 """ 101
102 - def __init__(self, encryptMode=None, encryptTarget=None):
103 """ 104 Constructor for the C{EncryptConfig} class. 105 106 @param encryptMode: Encryption mode 107 @param encryptTarget: Encryption target (for instance, GPG recipient) 108 109 @raise ValueError: If one of the values is invalid. 110 """ 111 self._encryptMode = None 112 self._encryptTarget = None 113 self.encryptMode = encryptMode 114 self.encryptTarget = encryptTarget
115
116 - def __repr__(self):
117 """ 118 Official string representation for class instance. 119 """ 120 return "EncryptConfig(%s, %s)" % (self.encryptMode, self.encryptTarget)
121
122 - def __str__(self):
123 """ 124 Informal string representation for class instance. 125 """ 126 return self.__repr__()
127
128 - def __cmp__(self, other):
129 """ 130 Definition of equals operator for this class. 131 Lists within this class are "unordered" for equality comparisons. 132 @param other: Other object to compare to. 133 @return: -1/0/1 depending on whether self is C{<}, C{=} or C{>} other. 134 """ 135 if other is None: 136 return 1 137 if self._encryptMode != other._encryptMode: 138 if self._encryptMode < other._encryptMode: 139 return -1 140 else: 141 return 1 142 if self._encryptTarget != other._encryptTarget: 143 if self._encryptTarget < other._encryptTarget: 144 return -1 145 else: 146 return 1 147 return 0
148
149 - def _setEncryptMode(self, value):
150 """ 151 Property target used to set the encrypt mode. 152 If not C{None}, the mode must be one of the values in L{VALID_ENCRYPT_MODES}. 153 @raise ValueError: If the value is not valid. 154 """ 155 if value is not None: 156 if value not in VALID_ENCRYPT_MODES: 157 raise ValueError("Encrypt mode must be one of %s." % VALID_ENCRYPT_MODES) 158 self._encryptMode = value
159
160 - def _getEncryptMode(self):
161 """ 162 Property target used to get the encrypt mode. 163 """ 164 return self._encryptMode
165
166 - def _setEncryptTarget(self, value):
167 """ 168 Property target used to set the encrypt target. 169 """ 170 if value is not None: 171 if len(value) < 1: 172 raise ValueError("Encrypt target must be non-empty string.") 173 self._encryptTarget = value
174
175 - def _getEncryptTarget(self):
176 """ 177 Property target used to get the encrypt target. 178 """ 179 return self._encryptTarget
180 181 encryptMode = property(_getEncryptMode, _setEncryptMode, None, doc="Encrypt mode.") 182 encryptTarget = property(_getEncryptTarget, _setEncryptTarget, None, doc="Encrypt target (i.e. GPG recipient).")
183 184 185 ######################################################################## 186 # LocalConfig class definition 187 ######################################################################## 188
189 -class LocalConfig(object):
190 191 """ 192 Class representing this extension's configuration document. 193 194 This is not a general-purpose configuration object like the main Cedar 195 Backup configuration object. Instead, it just knows how to parse and emit 196 encrypt-specific configuration values. Third parties who need to read and 197 write configuration related to this extension should access it through the 198 constructor, C{validate} and C{addConfig} methods. 199 200 @note: Lists within this class are "unordered" for equality comparisons. 201 202 @sort: __init__, __repr__, __str__, __cmp__, encrypt, validate, addConfig 203 """ 204
205 - def __init__(self, xmlData=None, xmlPath=None, validate=True):
206 """ 207 Initializes a configuration object. 208 209 If you initialize the object without passing either C{xmlData} or 210 C{xmlPath} then configuration will be empty and will be invalid until it 211 is filled in properly. 212 213 No reference to the original XML data or original path is saved off by 214 this class. Once the data has been parsed (successfully or not) this 215 original information is discarded. 216 217 Unless the C{validate} argument is C{False}, the L{LocalConfig.validate} 218 method will be called (with its default arguments) against configuration 219 after successfully parsing any passed-in XML. Keep in mind that even if 220 C{validate} is C{False}, it might not be possible to parse the passed-in 221 XML document if lower-level validations fail. 222 223 @note: It is strongly suggested that the C{validate} option always be set 224 to C{True} (the default) unless there is a specific need to read in 225 invalid configuration from disk. 226 227 @param xmlData: XML data representing configuration. 228 @type xmlData: String data. 229 230 @param xmlPath: Path to an XML file on disk. 231 @type xmlPath: Absolute path to a file on disk. 232 233 @param validate: Validate the document after parsing it. 234 @type validate: Boolean true/false. 235 236 @raise ValueError: If both C{xmlData} and C{xmlPath} are passed-in. 237 @raise ValueError: If the XML data in C{xmlData} or C{xmlPath} cannot be parsed. 238 @raise ValueError: If the parsed configuration document is not valid. 239 """ 240 self._encrypt = None 241 self.encrypt = None 242 if xmlData is not None and xmlPath is not None: 243 raise ValueError("Use either xmlData or xmlPath, but not both.") 244 if xmlData is not None: 245 self._parseXmlData(xmlData) 246 if validate: 247 self.validate() 248 elif xmlPath is not None: 249 xmlData = open(xmlPath).read() 250 self._parseXmlData(xmlData) 251 if validate: 252 self.validate()
253
254 - def __repr__(self):
255 """ 256 Official string representation for class instance. 257 """ 258 return "LocalConfig(%s)" % (self.encrypt)
259
260 - def __str__(self):
261 """ 262 Informal string representation for class instance. 263 """ 264 return self.__repr__()
265
266 - def __cmp__(self, other):
267 """ 268 Definition of equals operator for this class. 269 Lists within this class are "unordered" for equality comparisons. 270 @param other: Other object to compare to. 271 @return: -1/0/1 depending on whether self is C{<}, C{=} or C{>} other. 272 """ 273 if other is None: 274 return 1 275 if self._encrypt != other._encrypt: 276 if self._encrypt < other._encrypt: 277 return -1 278 else: 279 return 1 280 return 0
281
282 - def _setEncrypt(self, value):
283 """ 284 Property target used to set the encrypt configuration value. 285 If not C{None}, the value must be a C{EncryptConfig} object. 286 @raise ValueError: If the value is not a C{EncryptConfig} 287 """ 288 if value is None: 289 self._encrypt = None 290 else: 291 if not isinstance(value, EncryptConfig): 292 raise ValueError("Value must be a C{EncryptConfig} object.") 293 self._encrypt = value
294
295 - def _getEncrypt(self):
296 """ 297 Property target used to get the encrypt configuration value. 298 """ 299 return self._encrypt
300 301 encrypt = property(_getEncrypt, _setEncrypt, None, "Encrypt configuration in terms of a C{EncryptConfig} object.") 302
303 - def validate(self):
304 """ 305 Validates configuration represented by the object. 306 307 Encrypt configuration must be filled in. Within that, both the encrypt 308 mode and encrypt target must be filled in. 309 310 @raise ValueError: If one of the validations fails. 311 """ 312 if self.encrypt is None: 313 raise ValueError("Encrypt section is required.") 314 if self.encrypt.encryptMode is None: 315 raise ValueError("Encrypt mode must be set.") 316 if self.encrypt.encryptTarget is None: 317 raise ValueError("Encrypt target must be set.")
318
319 - def addConfig(self, xmlDom, parentNode):
320 """ 321 Adds an <encrypt> configuration section as the next child of a parent. 322 323 Third parties should use this function to write configuration related to 324 this extension. 325 326 We add the following fields to the document:: 327 328 encryptMode //cb_config/encrypt/encrypt_mode 329 encryptTarget //cb_config/encrypt/encrypt_target 330 331 @param xmlDom: DOM tree as from C{impl.createDocument()}. 332 @param parentNode: Parent that the section should be appended to. 333 """ 334 if self.encrypt is not None: 335 sectionNode = addContainerNode(xmlDom, parentNode, "encrypt") 336 addStringNode(xmlDom, sectionNode, "encrypt_mode", self.encrypt.encryptMode) 337 addStringNode(xmlDom, sectionNode, "encrypt_target", self.encrypt.encryptTarget)
338
339 - def _parseXmlData(self, xmlData):
340 """ 341 Internal method to parse an XML string into the object. 342 343 This method parses the XML document into a DOM tree (C{xmlDom}) and then 344 calls a static method to parse the encrypt configuration section. 345 346 @param xmlData: XML data to be parsed 347 @type xmlData: String data 348 349 @raise ValueError: If the XML cannot be successfully parsed. 350 """ 351 (xmlDom, parentNode) = createInputDom(xmlData) 352 self._encrypt = LocalConfig._parseEncrypt(parentNode)
353
354 - def _parseEncrypt(parent):
355 """ 356 Parses an encrypt configuration section. 357 358 We read the following individual fields:: 359 360 encryptMode //cb_config/encrypt/encrypt_mode 361 encryptTarget //cb_config/encrypt/encrypt_target 362 363 @param parent: Parent node to search beneath. 364 365 @return: C{EncryptConfig} object or C{None} if the section does not exist. 366 @raise ValueError: If some filled-in value is invalid. 367 """ 368 encrypt = None 369 section = readFirstChild(parent, "encrypt") 370 if section is not None: 371 encrypt = EncryptConfig() 372 encrypt.encryptMode = readString(section, "encrypt_mode") 373 encrypt.encryptTarget = readString(section, "encrypt_target") 374 return encrypt
375 _parseEncrypt = staticmethod(_parseEncrypt)
376 377 378 ######################################################################## 379 # Public functions 380 ######################################################################## 381 382 ########################### 383 # executeAction() function 384 ########################### 385
386 -def executeAction(configPath, options, config):
387 """ 388 Executes the encrypt backup action. 389 390 @param configPath: Path to configuration file on disk. 391 @type configPath: String representing a path on disk. 392 393 @param options: Program command-line options. 394 @type options: Options object. 395 396 @param config: Program configuration. 397 @type config: Config object. 398 399 @raise ValueError: Under many generic error conditions 400 @raise IOError: If there are I/O problems reading or writing files 401 """ 402 logger.debug("Executing encrypt extended action.") 403 if config.options is None or config.stage is None: 404 raise ValueError("Cedar Backup configuration is not properly filled in.") 405 local = LocalConfig(xmlPath=configPath) 406 if local.encrypt.encryptMode not in ["gpg", ]: 407 raise ValueError("Unknown encrypt mode [%s]" % local.encrypt.encryptMode); 408 if local.encrypt.encryptMode == "gpg": 409 _confirmGpgRecipient(local.encrypt.encryptTarget) 410 dailyDirs = findDailyDirs(config.stage.targetDir, ENCRYPT_INDICATOR) 411 for dailyDir in dailyDirs: 412 _encryptDailyDir(dailyDir, local.encrypt.encryptMode, local.encrypt.encryptTarget, 413 config.options.backupUser, config.options.backupGroup) 414 writeIndicatorFile(dailyDir, ENCRYPT_INDICATOR, config.options.backupUser, config.options.backupGroup) 415 logger.info("Executed the encrypt extended action successfully.")
416 417 418 ############################## 419 # _encryptDailyDir() function 420 ############################## 421
422 -def _encryptDailyDir(dailyDir, encryptMode, encryptTarget, backupUser, backupGroup):
423 """ 424 Encrypts the contents of a daily staging directory. 425 426 Indicator files are ignored. All other files are encrypted. The only valid 427 encrypt mode is C{"gpg"}. 428 429 @param dailyDir: Daily directory to encrypt 430 @param encryptMode: Encryption mode (only "gpg" is allowed) 431 @param encryptTarget: Encryption target (GPG recipient for "gpg" mode) 432 @param backupUser: User that target files should be owned by 433 @param backupGroup: Group that target files should be owned by 434 435 @raise ValueError: If the encrypt mode is not supported. 436 @raise ValueError: If the daily staging directory does not exist. 437 """ 438 logger.debug("Begin encrypting contents of [%s]." % dailyDir) 439 fileList = getBackupFiles(dailyDir) # ignores indicator files 440 for path in fileList: 441 _encryptFile(path, encryptMode, encryptTarget, backupUser, backupGroup, removeSource=True) 442 logger.debug("Completed encrypting contents of [%s]." % dailyDir)
443 444 445 ########################## 446 # _encryptFile() function 447 ########################## 448
449 -def _encryptFile(sourcePath, encryptMode, encryptTarget, backupUser, backupGroup, removeSource=False):
450 """ 451 Encrypts the source file using the indicated mode. 452 453 The encrypted file will be owned by the indicated backup user and group. If 454 C{removeSource} is C{True}, then the source file will be removed after it is 455 successfully encrypted. 456 457 Currently, only the C{"gpg"} encrypt mode is supported. 458 459 @param sourcePath: Absolute path of the source file to encrypt 460 @param encryptMode: Encryption mode (only "gpg" is allowed) 461 @param encryptTarget: Encryption target (GPG recipient) 462 @param backupUser: User that target files should be owned by 463 @param backupGroup: Group that target files should be owned by 464 @param removeSource: Indicates whether to remove the source file 465 466 @return: Path to the newly-created encrypted file. 467 468 @raise ValueError: If an invalid encrypt mode is passed in. 469 @raise IOError: If there is a problem accessing, encrypting or removing the source file. 470 """ 471 if not os.path.exists(sourcePath): 472 raise ValueError("Source path [%s] does not exist." % sourcePath); 473 if encryptMode == 'gpg': 474 encryptedPath = _encryptFileWithGpg(sourcePath, recipient=encryptTarget) 475 else: 476 raise ValueError("Unknown encrypt mode [%s]" % encryptMode); 477 changeOwnership(encryptedPath, backupUser, backupGroup) 478 if removeSource: 479 if os.path.exists(sourcePath): 480 try: 481 os.remove(sourcePath) 482 logger.debug("Completed removing old file [%s]." % sourcePath) 483 except: 484 raise IOError("Failed to remove file [%s] after encrypting it." % (sourcePath)) 485 return encryptedPath
486 487 488 ################################# 489 # _encryptFileWithGpg() function 490 ################################# 491
492 -def _encryptFileWithGpg(sourcePath, recipient):
493 """ 494 Encrypts the indicated source file using GPG. 495 496 The encrypted file will be in GPG's binary output format and will have the 497 same name as the source file plus a C{".gpg"} extension. The source file 498 will not be modified or removed by this function call. 499 500 @param sourcePath: Absolute path of file to be encrypted. 501 @param recipient: Recipient name to be passed to GPG's C{"-r"} option 502 503 @return: Path to the newly-created encrypted file. 504 505 @raise IOError: If there is a problem encrypting the file. 506 """ 507 encryptedPath = "%s.gpg" % sourcePath 508 command = resolveCommand(GPG_COMMAND) 509 args = [ "--batch", "--yes", "-e", "-r", recipient, "-o", encryptedPath, sourcePath, ] 510 result = executeCommand(command, args)[0] 511 if result != 0: 512 raise IOError("Error [%d] calling gpg to encrypt [%s]." % (result, sourcePath)) 513 if not os.path.exists(encryptedPath): 514 raise IOError("After call to [%s], encrypted file [%s] does not exist." % (command, encryptedPath)) 515 logger.debug("Completed encrypting file [%s] to [%s]." % (sourcePath, encryptedPath)) 516 return encryptedPath
517 518 519 ################################# 520 # _confirmGpgRecpient() function 521 ################################# 522
523 -def _confirmGpgRecipient(recipient):
524 """ 525 Confirms that a recipient's public key is known to GPG. 526 Throws an exception if there is a problem, or returns normally otherwise. 527 @param recipient: Recipient name 528 @raise IOError: If the recipient's public key is not known to GPG. 529 """ 530 command = resolveCommand(GPG_COMMAND) 531 args = [ "--batch", "-k", recipient, ] # should use --with-colons if the output will be parsed 532 result = executeCommand(command, args)[0] 533 if result != 0: 534 raise IOError("GPG unable to find public key for [%s]." % recipient)
535