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