CedarBackup3.extend.capacity

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

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