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