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