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

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