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 (>= 2.7)
     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