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 xmlData = open(xmlPath).read() 265 self._parseXmlData(xmlData) 266 if validate: 267 self.validate()
268
269 - def __repr__(self):
270 """ 271 Official string representation for class instance. 272 """ 273 return "LocalConfig(%s)" % (self.encrypt)
274
275 - def __str__(self):
276 """ 277 Informal string representation for class instance. 278 """ 279 return self.__repr__()
280
281 - def __eq__(self, other):
282 """Equals operator, iplemented in terms of original Python 2 compare operator.""" 283 return self.__cmp__(other) == 0
284
285 - def __lt__(self, other):
286 """Less-than operator, iplemented in terms of original Python 2 compare operator.""" 287 return self.__cmp__(other) < 0
288
289 - def __gt__(self, other):
290 """Greater-than operator, iplemented in terms of original Python 2 compare operator.""" 291 return self.__cmp__(other) > 0
292
293 - def __cmp__(self, other):
294 """ 295 Original Python 2 comparison operator. 296 Lists within this class are "unordered" for equality comparisons. 297 @param other: Other object to compare to. 298 @return: -1/0/1 depending on whether self is C{<}, C{=} or C{>} other. 299 """ 300 if other is None: 301 return 1 302 if self.encrypt != other.encrypt: 303 if self.encrypt < other.encrypt: 304 return -1 305 else: 306 return 1 307 return 0
308
309 - def _setEncrypt(self, value):
310 """ 311 Property target used to set the encrypt configuration value. 312 If not C{None}, the value must be a C{EncryptConfig} object. 313 @raise ValueError: If the value is not a C{EncryptConfig} 314 """ 315 if value is None: 316 self._encrypt = None 317 else: 318 if not isinstance(value, EncryptConfig): 319 raise ValueError("Value must be a C{EncryptConfig} object.") 320 self._encrypt = value
321
322 - def _getEncrypt(self):
323 """ 324 Property target used to get the encrypt configuration value. 325 """ 326 return self._encrypt
327 328 encrypt = property(_getEncrypt, _setEncrypt, None, "Encrypt configuration in terms of a C{EncryptConfig} object.") 329
330 - def validate(self):
331 """ 332 Validates configuration represented by the object. 333 334 Encrypt configuration must be filled in. Within that, both the encrypt 335 mode and encrypt target must be filled in. 336 337 @raise ValueError: If one of the validations fails. 338 """ 339 if self.encrypt is None: 340 raise ValueError("Encrypt section is required.") 341 if self.encrypt.encryptMode is None: 342 raise ValueError("Encrypt mode must be set.") 343 if self.encrypt.encryptTarget is None: 344 raise ValueError("Encrypt target must be set.")
345
346 - def addConfig(self, xmlDom, parentNode):
347 """ 348 Adds an <encrypt> configuration section as the next child of a parent. 349 350 Third parties should use this function to write configuration related to 351 this extension. 352 353 We add the following fields to the document:: 354 355 encryptMode //cb_config/encrypt/encrypt_mode 356 encryptTarget //cb_config/encrypt/encrypt_target 357 358 @param xmlDom: DOM tree as from C{impl.createDocument()}. 359 @param parentNode: Parent that the section should be appended to. 360 """ 361 if self.encrypt is not None: 362 sectionNode = addContainerNode(xmlDom, parentNode, "encrypt") 363 addStringNode(xmlDom, sectionNode, "encrypt_mode", self.encrypt.encryptMode) 364 addStringNode(xmlDom, sectionNode, "encrypt_target", self.encrypt.encryptTarget)
365
366 - def _parseXmlData(self, xmlData):
367 """ 368 Internal method to parse an XML string into the object. 369 370 This method parses the XML document into a DOM tree (C{xmlDom}) and then 371 calls a static method to parse the encrypt configuration section. 372 373 @param xmlData: XML data to be parsed 374 @type xmlData: String data 375 376 @raise ValueError: If the XML cannot be successfully parsed. 377 """ 378 (xmlDom, parentNode) = createInputDom(xmlData) 379 self._encrypt = LocalConfig._parseEncrypt(parentNode)
380 381 @staticmethod
382 - def _parseEncrypt(parent):
383 """ 384 Parses an encrypt configuration section. 385 386 We read the following individual fields:: 387 388 encryptMode //cb_config/encrypt/encrypt_mode 389 encryptTarget //cb_config/encrypt/encrypt_target 390 391 @param parent: Parent node to search beneath. 392 393 @return: C{EncryptConfig} object or C{None} if the section does not exist. 394 @raise ValueError: If some filled-in value is invalid. 395 """ 396 encrypt = None 397 section = readFirstChild(parent, "encrypt") 398 if section is not None: 399 encrypt = EncryptConfig() 400 encrypt.encryptMode = readString(section, "encrypt_mode") 401 encrypt.encryptTarget = readString(section, "encrypt_target") 402 return encrypt
403
404 405 ######################################################################## 406 # Public functions 407 ######################################################################## 408 409 ########################### 410 # executeAction() function 411 ########################### 412 413 -def executeAction(configPath, options, config):
414 """ 415 Executes the encrypt backup action. 416 417 @param configPath: Path to configuration file on disk. 418 @type configPath: String representing a path on disk. 419 420 @param options: Program command-line options. 421 @type options: Options object. 422 423 @param config: Program configuration. 424 @type config: Config object. 425 426 @raise ValueError: Under many generic error conditions 427 @raise IOError: If there are I/O problems reading or writing files 428 """ 429 logger.debug("Executing encrypt extended action.") 430 if config.options is None or config.stage is None: 431 raise ValueError("Cedar Backup configuration is not properly filled in.") 432 local = LocalConfig(xmlPath=configPath) 433 if local.encrypt.encryptMode not in ["gpg", ]: 434 raise ValueError("Unknown encrypt mode [%s]" % local.encrypt.encryptMode) 435 if local.encrypt.encryptMode == "gpg": 436 _confirmGpgRecipient(local.encrypt.encryptTarget) 437 dailyDirs = findDailyDirs(config.stage.targetDir, ENCRYPT_INDICATOR) 438 for dailyDir in dailyDirs: 439 _encryptDailyDir(dailyDir, local.encrypt.encryptMode, local.encrypt.encryptTarget, 440 config.options.backupUser, config.options.backupGroup) 441 writeIndicatorFile(dailyDir, ENCRYPT_INDICATOR, config.options.backupUser, config.options.backupGroup) 442 logger.info("Executed the encrypt extended action successfully.")
443
444 445 ############################## 446 # _encryptDailyDir() function 447 ############################## 448 449 -def _encryptDailyDir(dailyDir, encryptMode, encryptTarget, backupUser, backupGroup):
450 """ 451 Encrypts the contents of a daily staging directory. 452 453 Indicator files are ignored. All other files are encrypted. The only valid 454 encrypt mode is C{"gpg"}. 455 456 @param dailyDir: Daily directory to encrypt 457 @param encryptMode: Encryption mode (only "gpg" is allowed) 458 @param encryptTarget: Encryption target (GPG recipient for "gpg" mode) 459 @param backupUser: User that target files should be owned by 460 @param backupGroup: Group that target files should be owned by 461 462 @raise ValueError: If the encrypt mode is not supported. 463 @raise ValueError: If the daily staging directory does not exist. 464 """ 465 logger.debug("Begin encrypting contents of [%s].", dailyDir) 466 fileList = getBackupFiles(dailyDir) # ignores indicator files 467 for path in fileList: 468 _encryptFile(path, encryptMode, encryptTarget, backupUser, backupGroup, removeSource=True) 469 logger.debug("Completed encrypting contents of [%s].", dailyDir)
470
471 472 ########################## 473 # _encryptFile() function 474 ########################## 475 476 -def _encryptFile(sourcePath, encryptMode, encryptTarget, backupUser, backupGroup, removeSource=False):
477 """ 478 Encrypts the source file using the indicated mode. 479 480 The encrypted file will be owned by the indicated backup user and group. If 481 C{removeSource} is C{True}, then the source file will be removed after it is 482 successfully encrypted. 483 484 Currently, only the C{"gpg"} encrypt mode is supported. 485 486 @param sourcePath: Absolute path of the source file to encrypt 487 @param encryptMode: Encryption mode (only "gpg" is allowed) 488 @param encryptTarget: Encryption target (GPG recipient) 489 @param backupUser: User that target files should be owned by 490 @param backupGroup: Group that target files should be owned by 491 @param removeSource: Indicates whether to remove the source file 492 493 @return: Path to the newly-created encrypted file. 494 495 @raise ValueError: If an invalid encrypt mode is passed in. 496 @raise IOError: If there is a problem accessing, encrypting or removing the source file. 497 """ 498 if not os.path.exists(sourcePath): 499 raise ValueError("Source path [%s] does not exist." % sourcePath) 500 if encryptMode == 'gpg': 501 encryptedPath = _encryptFileWithGpg(sourcePath, recipient=encryptTarget) 502 else: 503 raise ValueError("Unknown encrypt mode [%s]" % encryptMode) 504 changeOwnership(encryptedPath, backupUser, backupGroup) 505 if removeSource: 506 if os.path.exists(sourcePath): 507 try: 508 os.remove(sourcePath) 509 logger.debug("Completed removing old file [%s].", sourcePath) 510 except: 511 raise IOError("Failed to remove file [%s] after encrypting it." % (sourcePath)) 512 return encryptedPath
513
514 515 ################################# 516 # _encryptFileWithGpg() function 517 ################################# 518 519 -def _encryptFileWithGpg(sourcePath, recipient):
520 """ 521 Encrypts the indicated source file using GPG. 522 523 The encrypted file will be in GPG's binary output format and will have the 524 same name as the source file plus a C{".gpg"} extension. The source file 525 will not be modified or removed by this function call. 526 527 @param sourcePath: Absolute path of file to be encrypted. 528 @param recipient: Recipient name to be passed to GPG's C{"-r"} option 529 530 @return: Path to the newly-created encrypted file. 531 532 @raise IOError: If there is a problem encrypting the file. 533 """ 534 encryptedPath = "%s.gpg" % sourcePath 535 command = resolveCommand(GPG_COMMAND) 536 args = [ "--batch", "--yes", "-e", "-r", recipient, "-o", encryptedPath, sourcePath, ] 537 result = executeCommand(command, args)[0] 538 if result != 0: 539 raise IOError("Error [%d] calling gpg to encrypt [%s]." % (result, sourcePath)) 540 if not os.path.exists(encryptedPath): 541 raise IOError("After call to [%s], encrypted file [%s] does not exist." % (command, encryptedPath)) 542 logger.debug("Completed encrypting file [%s] to [%s].", sourcePath, encryptedPath) 543 return encryptedPath
544
545 546 ################################# 547 # _confirmGpgRecpient() function 548 ################################# 549 550 -def _confirmGpgRecipient(recipient):
551 """ 552 Confirms that a recipient's public key is known to GPG. 553 Throws an exception if there is a problem, or returns normally otherwise. 554 @param recipient: Recipient name 555 @raise IOError: If the recipient's public key is not known to GPG. 556 """ 557 command = resolveCommand(GPG_COMMAND) 558 args = [ "--batch", "-k", recipient, ] # should use --with-colons if the output will be parsed 559 result = executeCommand(command, args)[0] 560 if result != 0: 561 raise IOError("GPG unable to find public key for [%s]." % recipient)
562