CedarBackup2.extend.encrypt

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