CedarBackup2.extend.capacity

Package CedarBackup2 :: Package extend :: Module capacity |
[hide private]
[frames] | no frames]

# Source Code for Module CedarBackup2.extend.capacity

      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) 2008,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  : Cedar Backup, release 2
     30  # Purpose  : Provides an extension to check remaining media capacity.
     31  #
     32  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
     33
     34  ########################################################################
     35  # Module documentation
     36  ########################################################################
     37
     38  """
     39  Provides an extension to check remaining media capacity.
     40
     41  Some users have asked for advance warning that their media is beginning to fill
     42  up.  This is an extension that checks the current capacity of the media in the
     43  writer, and prints a warning if the media is more than X% full, or has fewer
     44  than X bytes of capacity remaining.
     45
     46  @author: Kenneth J. Pronovici <pronovic@ieee.org>
     47  """
     48
     49  ########################################################################
     50  # Imported modules
     51  ########################################################################
     52
     53  # System modules
     54  import logging
     55
     56  # Cedar Backup modules
     57  from CedarBackup2.util import displayBytes
     58  from CedarBackup2.config import ByteQuantity, readByteQuantity, addByteQuantityNode
     59  from CedarBackup2.xmlutil import createInputDom, addContainerNode, addStringNode
     60  from CedarBackup2.xmlutil import readFirstChild, readString
     61  from CedarBackup2.actions.util import createWriter, checkMediaState
     62
     63
     64  ########################################################################
     65  # Module-wide constants and variables
     66  ########################################################################
     67
     68  logger = logging.getLogger("CedarBackup2.log.extend.capacity")
     69
     70
     71  ########################################################################
     72  # Percentage class definition
     73  ########################################################################
     74
     75 -class PercentageQuantity(object):
     76
     77     """
     78     Class representing a percentage quantity.
     79
     80     The percentage is maintained internally as a string so that issues of
     81     precision can be avoided.  It really isn't possible to store a floating
     82     point number here while being able to losslessly translate back and forth
     83     between XML and object representations.  (Perhaps the Python 2.4 Decimal
     84     class would have been an option, but I originally wanted to stay compatible
     85     with Python 2.3.)
     86
     87     Even though the quantity is maintained as a string, the string must be in a
     88     valid floating point positive number.  Technically, any floating point
     89     string format supported by Python is allowble.  However, it does not make
     90     sense to have a negative percentage in this context.
     91
     92     @sort: __init__, __repr__, __str__, __cmp__, quantity
     93     """
     94
     95 -   def __init__(self, quantity=None):
     96        """
     97        Constructor for the C{PercentageQuantity} class.
     98        @param quantity: Percentage quantity, as a string (i.e. "99.9" or "12")
     99        @raise ValueError: If the quantity value is invaid.
    100        """
    101        self._quantity = None
    102        self.quantity = quantity
    103
    104 -   def __repr__(self):
    105        """
    106        Official string representation for class instance.
    107        """
    108        return "PercentageQuantity(%s)" % (self.quantity)
    109
    110 -   def __str__(self):
    111        """
    112        Informal string representation for class instance.
    113        """
    114        return self.__repr__()
    115
    116 -   def __cmp__(self, other):
    117        """
    118        Definition of equals operator for this class.
    119        Lists within this class are "unordered" for equality comparisons.
    120        @param other: Other object to compare to.
    121        @return: -1/0/1 depending on whether self is C{<}, C{=} or C{>} other.
    122        """
    123        if other is None:
    124           return 1
    125        if self.quantity != other.quantity:
    126           if self.quantity < other.quantity:
    127              return -1
    128           else:
    129              return 1
    130        return 0
    131
    132 -   def _setQuantity(self, value):
    133        """
    134        Property target used to set the quantity
    135        The value must be a non-empty string if it is not C{None}.
    136        @raise ValueError: If the value is an empty string.
    137        @raise ValueError: If the value is not a valid floating point number
    138        @raise ValueError: If the value is less than zero
    139        """
    140        if value is not None:
    141           if len(value) < 1:
    142              raise ValueError("Percentage must be a non-empty string.")
    143           floatValue = float(value)
    144           if floatValue < 0.0 or floatValue > 100.0:
    145              raise ValueError("Percentage must be a positive value from 0.0 to 100.0")
    146        self._quantity = value # keep around string
    147
    148 -   def _getQuantity(self):
    149        """
    150        Property target used to get the quantity.
    151        """
    152        return self._quantity
    153
    154 -   def _getPercentage(self):
    155        """
    156        Property target used to get the quantity as a floating point number.
    157        If there is no quantity set, then a value of 0.0 is returned.
    158        """
    159        if self.quantity is not None:
    160           return float(self.quantity)
    161        return 0.0
    162
    163     quantity = property(_getQuantity, _setQuantity, None, doc="Percentage value, as a string")
    164     percentage = property(_getPercentage, None, None, "Percentage value, as a floating point number.")
    165
    166
    167  ########################################################################
    168  # CapacityConfig class definition
    169  ########################################################################
    170
    171 -class CapacityConfig(object):
    172
    173     """
    174     Class representing capacity configuration.
    175
    176     The following restrictions exist on data in this class:
    177
    178        - The maximum percentage utilized must be a PercentageQuantity
    179        - The minimum bytes remaining must be a ByteQuantity
    180
    181     @sort: __init__, __repr__, __str__, __cmp__, maxPercentage, minBytes
    182     """
    183
    184 -   def __init__(self, maxPercentage=None, minBytes=None):
    185        """
    186        Constructor for the C{CapacityConfig} class.
    187
    188        @param maxPercentage: Maximum percentage of the media that may be utilized
    189        @param minBytes: Minimum number of free bytes that must be available
    190        """
    191        self._maxPercentage = None
    192        self._minBytes = None
    193        self.maxPercentage = maxPercentage
    194        self.minBytes = minBytes
    195
    196 -   def __repr__(self):
    197        """
    198        Official string representation for class instance.
    199        """
    200        return "CapacityConfig(%s, %s)" % (self.maxPercentage, self.minBytes)
    201
    202 -   def __str__(self):
    203        """
    204        Informal string representation for class instance.
    205        """
    206        return self.__repr__()
    207
    208 -   def __cmp__(self, other):
    209        """
    210        Definition of equals operator for this class.
    211        @param other: Other object to compare to.
    212        @return: -1/0/1 depending on whether self is C{<}, C{=} or C{>} other.
    213        """
    214        if other is None:
    215           return 1
    216        if self.maxPercentage != other.maxPercentage:
    217           if self.maxPercentage < other.maxPercentage:
    218              return -1
    219           else:
    220              return 1
    221        if self.minBytes != other.minBytes:
    222           if self.minBytes < other.minBytes:
    223              return -1
    224           else:
    225              return 1
    226        return 0
    227
    228 -   def _setMaxPercentage(self, value):
    229        """
    230        Property target used to set the maxPercentage value.
    231        If not C{None}, the value must be a C{PercentageQuantity} object.
    232        @raise ValueError: If the value is not a C{PercentageQuantity}
    233        """
    234        if value is None:
    235           self._maxPercentage = None
    236        else:
    237           if not isinstance(value, PercentageQuantity):
    238              raise ValueError("Value must be a C{PercentageQuantity} object.")
    239           self._maxPercentage = value
    240
    241 -   def _getMaxPercentage(self):
    242        """
    243        Property target used to get the maxPercentage value
    244        """
    245        return self._maxPercentage
    246
    247 -   def _setMinBytes(self, value):
    248        """
    249        Property target used to set the bytes utilized value.
    250        If not C{None}, the value must be a C{ByteQuantity} object.
    251        @raise ValueError: If the value is not a C{ByteQuantity}
    252        """
    253        if value is None:
    254           self._minBytes = None
    255        else:
    256           if not isinstance(value, ByteQuantity):
    257              raise ValueError("Value must be a C{ByteQuantity} object.")
    258           self._minBytes = value
    259
    260 -   def _getMinBytes(self):
    261        """
    262        Property target used to get the bytes remaining value.
    263        """
    264        return self._minBytes
    265
    266     maxPercentage = property(_getMaxPercentage, _setMaxPercentage, None, "Maximum percentage of the media that may be utilized.")
    267     minBytes = property(_getMinBytes, _setMinBytes, None, "Minimum number of free bytes that must be available.")
    268
    269
    270  ########################################################################
    271  # LocalConfig class definition
    272  ########################################################################
    273
    274 -class LocalConfig(object):
    275
    276     """
    277     Class representing this extension's configuration document.
    278
    279     This is not a general-purpose configuration object like the main Cedar
    280     Backup configuration object.  Instead, it just knows how to parse and emit
    281     specific configuration values to this extension.  Third parties who need to
    282     read and write configuration related to this extension should access it
    283     through the constructor, C{validate} and C{addConfig} methods.
    284
    285     @note: Lists within this class are "unordered" for equality comparisons.
    286
    287     @sort: __init__, __repr__, __str__, __cmp__, capacity, validate, addConfig
    288     """
    289
    290 -   def __init__(self, xmlData=None, xmlPath=None, validate=True):
    291        """
    292        Initializes a configuration object.
    293
    294        If you initialize the object without passing either C{xmlData} or
    295        C{xmlPath} then configuration will be empty and will be invalid until it
    296        is filled in properly.
    297
    298        No reference to the original XML data or original path is saved off by
    299        this class.  Once the data has been parsed (successfully or not) this
    300        original information is discarded.
    301
    302        Unless the C{validate} argument is C{False}, the L{LocalConfig.validate}
    303        method will be called (with its default arguments) against configuration
    304        after successfully parsing any passed-in XML.  Keep in mind that even if
    305        C{validate} is C{False}, it might not be possible to parse the passed-in
    306        XML document if lower-level validations fail.
    307
    308        @note: It is strongly suggested that the C{validate} option always be set
    309        to C{True} (the default) unless there is a specific need to read in
    310        invalid configuration from disk.
    311
    312        @param xmlData: XML data representing configuration.
    313        @type xmlData: String data.
    314
    315        @param xmlPath: Path to an XML file on disk.
    316        @type xmlPath: Absolute path to a file on disk.
    317
    318        @param validate: Validate the document after parsing it.
    319        @type validate: Boolean true/false.
    320
    321        @raise ValueError: If both C{xmlData} and C{xmlPath} are passed-in.
    322        @raise ValueError: If the XML data in C{xmlData} or C{xmlPath} cannot be parsed.
    323        @raise ValueError: If the parsed configuration document is not valid.
    324        """
    325        self._capacity = None
    326        self.capacity = None
    327        if xmlData is not None and xmlPath is not None:
    328           raise ValueError("Use either xmlData or xmlPath, but not both.")
    329        if xmlData is not None:
    330           self._parseXmlData(xmlData)
    331           if validate:
    332              self.validate()
    333        elif xmlPath is not None:
    334           xmlData = open(xmlPath).read()
    335           self._parseXmlData(xmlData)
    336           if validate:
    337              self.validate()
    338
    339 -   def __repr__(self):
    340        """
    341        Official string representation for class instance.
    342        """
    343        return "LocalConfig(%s)" % (self.capacity)
    344
    345 -   def __str__(self):
    346        """
    347        Informal string representation for class instance.
    348        """
    349        return self.__repr__()
    350
    351 -   def __cmp__(self, other):
    352        """
    353        Definition of equals operator for this class.
    354        Lists within this class are "unordered" for equality comparisons.
    355        @param other: Other object to compare to.
    356        @return: -1/0/1 depending on whether self is C{<}, C{=} or C{>} other.
    357        """
    358        if other is None:
    359           return 1
    360        if self.capacity != other.capacity:
    361           if self.capacity < other.capacity:
    362              return -1
    363           else:
    364              return 1
    365        return 0
    366
    367 -   def _setCapacity(self, value):
    368        """
    369        Property target used to set the capacity configuration value.
    370        If not C{None}, the value must be a C{CapacityConfig} object.
    371        @raise ValueError: If the value is not a C{CapacityConfig}
    372        """
    373        if value is None:
    374           self._capacity = None
    375        else:
    376           if not isinstance(value, CapacityConfig):
    377              raise ValueError("Value must be a C{CapacityConfig} object.")
    378           self._capacity = value
    379
    380 -   def _getCapacity(self):
    381        """
    382        Property target used to get the capacity configuration value.
    383        """
    384        return self._capacity
    385
    386     capacity = property(_getCapacity, _setCapacity, None, "Capacity configuration in terms of a C{CapacityConfig} object.")
    387
    388 -   def validate(self):
    389        """
    390        Validates configuration represented by the object.
    391        THere must be either a percentage, or a byte capacity, but not both.
    392        @raise ValueError: If one of the validations fails.
    393        """
    394        if self.capacity is None:
    395           raise ValueError("Capacity section is required.")
    396        if self.capacity.maxPercentage is None and self.capacity.minBytes is None:
    397           raise ValueError("Must provide either max percentage or min bytes.")
    398        if self.capacity.maxPercentage is not None and self.capacity.minBytes is not None:
    399           raise ValueError("Must provide either max percentage or min bytes, but not both.")
    400
    401 -   def addConfig(self, xmlDom, parentNode):
    402        """
    403        Adds a <capacity> configuration section as the next child of a parent.
    404
    405        Third parties should use this function to write configuration related to
    406        this extension.
    407
    408        We add the following fields to the document::
    409
    410           maxPercentage  //cb_config/capacity/max_percentage
    411           minBytes       //cb_config/capacity/min_bytes
    412
    413        @param xmlDom: DOM tree as from C{impl.createDocument()}.
    414        @param parentNode: Parent that the section should be appended to.
    415        """
    416        if self.capacity is not None:
    417           sectionNode = addContainerNode(xmlDom, parentNode, "capacity")
    418           LocalConfig._addPercentageQuantity(xmlDom, sectionNode, "max_percentage", self.capacity.maxPercentage)
    419           if self.capacity.minBytes is not None: # because utility function fills in empty section on None
    420              addByteQuantityNode(xmlDom, sectionNode, "min_bytes", self.capacity.minBytes)
    421
    422 -   def _parseXmlData(self, xmlData):
    423        """
    424        Internal method to parse an XML string into the object.
    425
    426        This method parses the XML document into a DOM tree (C{xmlDom}) and then
    427        calls a static method to parse the capacity configuration section.
    428
    429        @param xmlData: XML data to be parsed
    430        @type xmlData: String data
    431
    432        @raise ValueError: If the XML cannot be successfully parsed.
    433        """
    434        (xmlDom, parentNode) = createInputDom(xmlData)
    435        self._capacity = LocalConfig._parseCapacity(parentNode)
    436
    437     @staticmethod
    438 -   def _parseCapacity(parentNode):
    439        """
    440        Parses a capacity configuration section.
    441
    442        We read the following fields::
    443
    444           maxPercentage  //cb_config/capacity/max_percentage
    445           minBytes       //cb_config/capacity/min_bytes
    446
    447        @param parentNode: Parent node to search beneath.
    448
    449        @return: C{CapacityConfig} object or C{None} if the section does not exist.
    450        @raise ValueError: If some filled-in value is invalid.
    451        """
    452        capacity = None
    453        section = readFirstChild(parentNode, "capacity")
    454        if section is not None:
    455           capacity = CapacityConfig()
    456           capacity.maxPercentage = LocalConfig._readPercentageQuantity(section, "max_percentage")
    457           capacity.minBytes = readByteQuantity(section, "min_bytes")
    458        return capacity
    459
    460     @staticmethod
    461 -   def _readPercentageQuantity(parent, name):
    462        """
    463        Read a percentage quantity value from an XML document.
    464        @param parent: Parent node to search beneath.
    465        @param name: Name of node to search for.
    466        @return: Percentage quantity parsed from XML document
    467        """
    468        quantity = readString(parent, name)
    469        if quantity is None:
    470           return None
    471        return PercentageQuantity(quantity)
    472
    473     @staticmethod
    474 -   def _addPercentageQuantity(xmlDom, parentNode, nodeName, percentageQuantity):
    475        """
    476        Adds a text node as the next child of a parent, to contain a percentage quantity.
    477
    478        If the C{percentageQuantity} is None, then no node will be created.
    479
    480        @param xmlDom: DOM tree as from C{impl.createDocument()}.
    481        @param parentNode: Parent node to create child for.
    482        @param nodeName: Name of the new container node.
    483        @param percentageQuantity: PercentageQuantity object to put into the XML document
    484
    485        @return: Reference to the newly-created node.
    486        """
    487        if percentageQuantity is not None:
    488           addStringNode(xmlDom, parentNode, nodeName, percentageQuantity.quantity)
    489
    490
    491  ########################################################################
    492  # Public functions
    493  ########################################################################
    494
    495  ###########################
    496  # executeAction() function
    497  ###########################
    498
    499 -def executeAction(configPath, options, config):
    500     """
    501     Executes the capacity action.
    502
    503     @param configPath: Path to configuration file on disk.
    504     @type configPath: String representing a path on disk.
    505
    506     @param options: Program command-line options.
    507     @type options: Options object.
    508
    509     @param config: Program configuration.
    510     @type config: Config object.
    511
    512     @raise ValueError: Under many generic error conditions
    513     @raise IOError: If there are I/O problems reading or writing files
    514     """
    515     logger.debug("Executing capacity extended action.")
    516     if config.options is None or config.store is None:
    517        raise ValueError("Cedar Backup configuration is not properly filled in.")
    518     local = LocalConfig(xmlPath=configPath)
    519     if config.store.checkMedia:
    520        checkMediaState(config.store)  # raises exception if media is not initialized
    521     capacity = createWriter(config).retrieveCapacity()
    522     logger.debug("Media capacity: %s", capacity)
    523     if local.capacity.maxPercentage is not None:
    524        if capacity.utilized > local.capacity.maxPercentage.percentage:
    525           logger.error("Media has reached capacity limit of %s%%: %.2f%% utilized",
    526                        local.capacity.maxPercentage.quantity, capacity.utilized)
    527     else:
    528        if capacity.bytesAvailable < local.capacity.minBytes:
    529           logger.error("Media has reached capacity limit of %s: only %s available",
    530                        local.capacity.minBytes, displayBytes(capacity.bytesAvailable))
    531     logger.info("Executed the capacity extended action successfully.")
    532