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