Package Products :: Package ZenModel :: Module MaintenanceWindow
[hide private]
[frames] | no frames]

Source Code for Module Products.ZenModel.MaintenanceWindow

  1  ############################################################################## 
  2  #  
  3  # Copyright (C) Zenoss, Inc. 2007, 2009, all rights reserved. 
  4  #  
  5  # This content is made available according to terms specified in 
  6  # License.zenoss under the directory where your Zenoss product is installed. 
  7  #  
  8  ############################################################################## 
  9   
 10   
 11  __doc__ = """MaintenanceWindow 
 12   
 13  A scheduled period of time during which a window is under maintenance. 
 14   
 15  """ 
 16   
 17  DAY_SECONDS = 24*60*60 
 18  WEEK_SECONDS = 7*DAY_SECONDS 
 19   
 20  import time 
 21  import logging 
 22  log = logging.getLogger("zen.MaintenanceWindows") 
 23   
 24  import Globals 
 25   
 26  from AccessControl import ClassSecurityInfo 
 27  from zope.interface import implements 
 28  from ZenossSecurity import * 
 29  from ZenModelRM import ZenModelRM 
 30  from Products.ZenModel.interfaces import IIndexed 
 31  from Products.ZenRelations.RelSchema import * 
 32  from Products.ZenUtils import Time 
 33  from Products.ZenWidgets import messaging 
 34  from Products.ZenMessaging.audit import audit 
 35   
36 -def lastDayPreviousMonth(seconds):
37 parts = list(time.localtime(seconds)) 38 # use day 1 of this month 39 parts[2] = 1 40 # and go back DAY_SECONDS 41 return time.mktime(parts) - DAY_SECONDS
42
43 -def addMonth(secs, dayOfMonthHint=0):
44 base = list(time.localtime(secs)) 45 # add a month 46 base[1] += 1 47 # year wrap 48 if base[1] == 13: 49 base[0] += 1 50 base[1] = 1 51 # Check for the case Jan 31 becomes March 3 52 # in that case, force it back to Feb 28 53 54 # first, remember the month 55 month = base[1] 56 if dayOfMonthHint: 57 base[2] = dayOfMonthHint 58 # normalize 59 base = list(time.localtime(time.mktime(base))) 60 # if the month changed, walk back to the end of the previous month 61 if base[1] != month: 62 return lastDayPreviousMonth(time.mktime(base)) 63 return time.mktime(base)
64 65 66 RETURN_TO_ORIG_PROD_STATE = -99 67
68 -class MaintenanceWindow(ZenModelRM):
69 70 implements(IIndexed) 71 meta_type = 'Maintenance Window' 72 73 default_catalog = 'maintenanceWindowSearch' 74 75 name = None 76 start = None 77 started = None 78 duration = 60 79 repeat = 'Never' 80 startProductionState = 300 81 stopProductionState = RETURN_TO_ORIG_PROD_STATE 82 enabled = True 83 skip = 1 84 85 _properties = ( 86 {'id':'name', 'type':'string', 'mode':'w'}, 87 {'id':'start', 'type':'int', 'mode':'w'}, 88 {'id':'started', 'type':'int', 'mode':'w'}, 89 {'id':'duration', 'type':'int', 'mode':'w'}, 90 {'id':'repeat', 'type':'string', 'mode':'w'}, 91 {'id':'skip', 'type':'int', 'mode':'w'}, 92 ) 93 94 factory_type_information = ( 95 { 96 'immediate_view' : 'maintenanceWindowDetail', 97 'actions' : 98 ( 99 { 'id' : 'status' 100 , 'name' : 'Status' 101 , 'action' : 'maintenanceWindowDetail' 102 , 'permissions' : (ZEN_MAINTENANCE_WINDOW_VIEW, ) 103 }, 104 ) 105 }, 106 ) 107 108 backCrumb = 'deviceManagement' 109 _relations = ( 110 ("productionState", ToOne(ToManyCont, "Products.ZenModel.MaintenanceWindowable", "maintenanceWindows")), 111 ) 112 113 security = ClassSecurityInfo() 114 115 116 REPEAT = "Never/Daily/Every Weekday/Weekly/Monthly/First Sunday of the Month".split('/') 117 NEVER, DAILY, EVERY_WEEKDAY, WEEKLY, MONTHLY, FSOTM = REPEAT 118
119 - def __init__(self, id):
120 ZenModelRM.__init__(self, id) 121 self.start = time.time() 122 self.enabled = False
123
124 - def set(self, start, duration, repeat, enabled=True):
125 self.start = start 126 self.duration = duration 127 self.repeat = repeat 128 self.enabled = enabled
129
130 - def displayName(self):
131 if self.name is not None: return self.name 132 else: return self.id
133
134 - def repeatOptions(self):
135 "Provide the list of REPEAT options" 136 return self.REPEAT
137
138 - def getTargetId(self):
139 return self.target().id
140 141 # Nice methods used by the GUI for presentation purposes
142 - def niceDuration(self):
143 """Return a human readable version of the duration in 144 days, hours, minutes""" 145 return Time.Duration(self.duration*60)
146
147 - def niceStartDate(self):
148 "Return a date in the format use by the calendar javascript" 149 return Time.USDate(self.start)
150
151 - def niceStartDateTime(self):
152 "Return start time as a string with nice sort qualities" 153 return Time.LocalDateTime(self.start)
154
155 - def niceStartProductionState(self):
156 "Return a string version of the startProductionState" 157 return self.convertProdState(self.startProductionState)
158
159 - def niceStopProductionState(self):
160 "Return a string version of the stopProductionState" 161 return 'Original'
162
163 - def niceStartHour(self):
164 return time.localtime(self.start)[3]
165
166 - def niceStartMinute(self):
167 return time.localtime(self.start)[4]
168 169 security.declareProtected(ZEN_MAINTENANCE_WINDOW_EDIT, 170 'manage_editMaintenanceWindow')
171 - def manage_editMaintenanceWindow(self, 172 startDate='', 173 startHours='', 174 startMinutes='00', 175 durationDays='0', 176 durationHours='00', 177 durationMinutes='00', 178 repeat='Never', 179 startProductionState=300, 180 stopProductionState=RETURN_TO_ORIG_PROD_STATE, 181 enabled=True, 182 skip=1, 183 REQUEST=None):
184 "Update the maintenance window from GUI elements" 185 def makeInt(v, fieldName, minv=None, maxv=None, acceptBlanks=True): 186 if acceptBlanks: 187 if isinstance(v, str): 188 v = v.strip() 189 v = v or '0' 190 try: 191 v = int(v) 192 if minv is not None and v < minv: 193 raise ValueError 194 if maxv is not None and v > maxv: 195 raise ValueError 196 except ValueError: 197 if minv is None and maxv is None: 198 msg = '%s must be an integer.' % fieldName 199 elif minv is not None and maxv is not None: 200 msg = '%s must be between %s and %s inclusive.' % ( 201 fieldName, minv, maxv) 202 elif minv is not None: 203 msg = '%s must be at least %s' % (fieldName, minv) 204 else: 205 msg = '%s must be no greater than %s' % (fieldName, maxv) 206 msgs.append(msg) 207 v = None 208 return v
209 210 msgs = [] 211 # startHours, startMinutes come from menus. No need to catch 212 # ValueError on the int conversion. 213 startHours = int(startHours) 214 startMinutes = int(startMinutes) 215 self.enabled = bool(enabled) 216 import re 217 try: 218 month, day, year = re.split('[^ 0-9]', startDate) 219 except ValueError: 220 msgs.append("Date needs three number fields") 221 day = int(day) 222 month = int(month) 223 year = int(year) 224 if not msgs: 225 t = time.mktime((year, month, day, startHours, startMinutes, 226 0, 0, 0, -1)) 227 if not msgs: 228 durationDays = makeInt(durationDays, 'Duration days', 229 minv=0) 230 durationHours = makeInt(durationHours, 'Duration hours', 231 minv=0, maxv=23) 232 durationMinutes = makeInt(durationMinutes, 'Duration minutes', 233 minv=0, maxv=59) 234 if not msgs: 235 duration = (durationDays * (60*24) + 236 durationHours * 60 + 237 durationMinutes) 238 239 if duration < 1: 240 msgs.append('Duration must be at least 1 minute.') 241 if msgs: 242 if REQUEST: 243 messaging.IMessageSender(self).sendToBrowser( 244 'Window Edit Failed', 245 '\n'.join(msgs), 246 messaging.WARNING 247 ) 248 else: 249 self.start = t 250 self.duration = duration 251 self.repeat = repeat 252 self.startProductionState = startProductionState 253 self.stopProductionState = stopProductionState 254 self.skip = skip 255 now = time.time() 256 if self.started and self.nextEvent(now) < now: 257 self.end() 258 if REQUEST: 259 messaging.IMessageSender(self).sendToBrowser( 260 'Window Updated', 261 'Maintenance window changes were saved.' 262 ) 263 audit('UI.MaintenanceWindow.Edit', self) 264 if REQUEST: 265 return REQUEST.RESPONSE.redirect(self.getUrlForUserCommands())
266 267
268 - def nextEvent(self, now):
269 "Return the time of the next begin() or end()" 270 if self.started: 271 return self.adjustDST(self.started + self.duration * 60 - 1) 272 # ok, so maybe "now" is a little late: start anything that 273 # should have been started by now 274 return self.next(self.padDST(now) - self.duration * 60 + 1)
275 276 277 security.declareProtected(ZEN_VIEW, 'breadCrumbs')
278 - def breadCrumbs(self, terminator='dmd'):
279 "fix up breadCrumbs to add a link back to the Manage tab" 280 bc = super(MaintenanceWindow, self).breadCrumbs(terminator) 281 url, display = bc[-2] 282 url += "/" + self.backCrumb 283 bc.insert(-1, (url, 'manage')) 284 return bc
285 286
287 - def next(self, now = None):
288 """ 289 From Unix time_t now value, return next time_t value 290 for the window to start, or None 291 This adjusts for DST changes. 292 """ 293 return self.adjustDST(self._next(now))
294
295 - def _next(self, now):
296 if not self.enabled: 297 return None 298 299 if self.skip is None: 300 self.skip = 1 301 302 if now is None: 303 now = time.time() 304 305 if now < self.start: 306 return self.start 307 308 if self.repeat == self.NEVER: 309 if now > self.start: 310 return None 311 return self.start 312 313 elif self.repeat == self.DAILY: 314 skip = (DAY_SECONDS * self.skip) 315 last = self.start + ((now - self.start) // skip * skip) 316 return last + skip 317 318 elif self.repeat == self.EVERY_WEEKDAY: 319 weeksSince = (now - self.start) // WEEK_SECONDS 320 weekdaysSince = weeksSince * 5 321 # start at the most recent week-even point from the start 322 base = self.start + weeksSince * DAY_SECONDS * 7 323 while 1: 324 dow = time.localtime(base).tm_wday 325 if dow not in (5,6): 326 if base > now and weekdaysSince % self.skip == 0: 327 break 328 weekdaysSince += 1 329 base += DAY_SECONDS 330 assert base >= now 331 return base 332 333 elif self.repeat == self.WEEKLY: 334 skip = (WEEK_SECONDS * self.skip) 335 last = self.start + ((now - self.start) // skip * skip) 336 return last + skip 337 338 elif self.repeat == self.MONTHLY: 339 months = 0 340 m = self.start 341 dayOfMonthHint = time.localtime(self.start).tm_mday 342 while m < now or months % self.skip: 343 m = addMonth(m, dayOfMonthHint) 344 months += 1 345 return m 346 347 elif self.repeat == self.FSOTM: 348 base = list(time.localtime(now)) 349 # Move time to this year/month 350 base[2:6] = time.localtime(self.start)[2:6] 351 base = time.mktime(base) 352 # creep ahead by days until it's the FSOTM 353 # (not the most efficient implementation) 354 count = 0 355 while 1: 356 tm = time.localtime(base) 357 if base > now and 1 <= tm.tm_mday <= 7 and tm.tm_wday == 6: 358 count += 1 359 if count % self.skip == 0: 360 break 361 base += DAY_SECONDS 362 return base 363 raise ValueError('bad value for MaintenanceWindow repeat: %r' %self.repeat)
364
365 - def target(self):
366 return self.productionState().primaryAq()
367
368 - def isActive(self):
369 """ 370 Return whether or not the maintenance window is active. 371 372 @return: is this window active or not? 373 @rtype: boolean 374 """ 375 if not self.enabled or self.started is None: 376 return False 377 return True
378
379 - def fetchDeviceMinProdStates(self, devices=None):
380 """ 381 Return a dictionary of devices and their minimum production state from 382 all maintenance windows. 383 384 Note: This method should be moved to the zenjobs command in order to 385 improve performance. 386 387 @return: dictionary of device_id:production_state 388 @rtype: dictionary 389 """ 390 # Get all maintenance windows + action rules from all device classes, 391 # devices, Systems, Locations, and Groups. 392 # Yes, it's O(m * n) 393 minDevProdStates = {} 394 cat = getattr(self, self.default_catalog) 395 for entry in cat(): 396 try: 397 mw = entry.getObject() 398 except Exception: 399 continue 400 401 if not mw.isActive(): 402 # Note: if the mw has just ended, the self.end() method 403 # has already made the mw inactive before this point 404 continue 405 406 log.debug("Updating min MW Prod state using state %s from window %s", 407 mw.startProductionState, mw.displayName()) 408 409 if self.primaryAq() == mw.primaryAq(): 410 # Special case: our window's devices 411 mwDevices = devices 412 else: 413 mwDevices = mw.fetchDevices() 414 415 for device in mwDevices: 416 state = minDevProdStates.get(device.id, None) 417 if state is None or state > mw.startProductionState: 418 minDevProdStates[device.id] = mw.startProductionState 419 log.debug("MW %s has lowered %s's min MW prod state to %s", 420 mw.displayName(), device.id, mw.startProductionState) 421 422 return minDevProdStates
423 424
425 - def fetchDevices(self):
426 """ 427 Get the list of devices from our maintenance window. 428 """ 429 target = self.target() 430 from Products.ZenModel.DeviceOrganizer import DeviceOrganizer 431 if isinstance(target, DeviceOrganizer): 432 devices = target.getSubDevices() 433 else: 434 devices = [target] 435 436 return devices
437 438 439 security.declareProtected(ZEN_MAINTENANCE_WINDOW_EDIT, 'setProdState')
440 - def setProdState(self, state, ending=False):
441 """ 442 At any one time there is one production state for each device to be in, 443 and that is the state that is the most 'blacked out' in all of the active 444 maintenance windows affecting that device. When the last maintenance 445 window affecting a device has ended, the original production state of the 446 device is used to determine the end state of the device. 447 448 Maintenance windows are processed by zenjobs in batch so the ordering 449 of when two maintenance windows that end at the same time get processed 450 is non-deterministic. Since there is only one stop production state now, 451 this is not an issue. 452 453 @parameter state: hint from the maint window about device's start or stop state 454 @type state: integer 455 @parameter ending: are we ending a maintenance window? 456 @type ending: boolean 457 """ 458 # Note: self.begin() starts our window before we get called, so the 459 # following takes into account our window state too. 460 # Conversely, self.end() ends the window before calling this code. 461 devices = self.fetchDevices() 462 minDevProdStates = self.fetchDeviceMinProdStates( devices ) 463 464 for device in devices: 465 if ending: 466 # Note: If no maintenance windows apply to a device, then the 467 # device won't exist in minDevProdStates 468 # This takes care of the case where there are still active 469 # maintenance windows. 470 minProdState = minDevProdStates.get(device.id, 471 device.preMWProductionState) 472 473 elif device.id in minDevProdStates: 474 minProdState = minDevProdStates[device.id] 475 476 else: # This is impossible for us to ever get here as minDevProdStates 477 # has been added by self.fetchDeviceMinProdStates() 478 log.error("The device %s does not appear in any maintenance" 479 " windows (including %s -- which is just starting).", 480 device.id, self.displayName()) 481 continue 482 483 self._p_changed = 1 484 # Changes the current state for a device, but *not* 485 # the preMWProductionState 486 oldProductionState = self.dmd.convertProdState(device.productionState) 487 newProductionState = self.dmd.convertProdState(minProdState) 488 log.info("MW %s changes %s's production state from %s to %s", 489 self.displayName(), device.id, oldProductionState, 490 newProductionState) 491 audit('System.Device.Edit', device, starting=str(not ending), 492 maintenanceWindow=self.displayName(), 493 productionState=newProductionState, 494 oldData_={'productionState':oldProductionState}) 495 device.setProdState(minProdState, maintWindowChange=True)
496 497
498 - def begin(self, now = None):
499 """ 500 Hook for entering the Maintenance Window: call if you override 501 """ 502 log.info("Mainenance window %s starting" % self.displayName()) 503 if not now: 504 now = time.time() 505 506 # Make sure that we've started before the calculation of the production 507 # state occurs. 508 self.started = now 509 self.setProdState(self.startProductionState)
510 511 512
513 - def end(self):
514 """ 515 Hook for leaving the Maintenance Window: call if you override 516 """ 517 log.info("Mainenance window %s ending" % self.displayName()) 518 # Make sure that the window has ended before the calculation of 519 # the production state occurs. 520 self.started = None 521 self.setProdState(self.stopProductionState, ending=True)
522 523
524 - def execute(self, now = None):
525 "Take the next step: either start or stop the Maintenance Window" 526 if self.started: 527 self.end() 528 else: 529 self.begin(now)
530
531 - def adjustDST(self, result):
532 if result is None: 533 return None 534 if self.started: 535 startTime = time.localtime(self.started) 536 else: 537 startTime = time.localtime(self.start) 538 resultTime = time.localtime(result) 539 if startTime.tm_isdst == resultTime.tm_isdst: 540 return result 541 if startTime.tm_isdst: 542 return result + 60*60 543 return result - 60*60
544 545
546 - def padDST(self, now):
547 """ 548 When incrementing or decrementing timestamps within a DST switch we 549 need to add or subtract the DST offset accordingly. 550 """ 551 startTime = time.localtime(self.start) 552 nowTime = time.localtime(now) 553 if startTime.tm_isdst == nowTime.tm_isdst: 554 return now 555 elif startTime.tm_isdst: 556 return now - 60 * 60 557 else: 558 return now + 60 * 60
559 560 561 DeviceMaintenanceWindow = MaintenanceWindow 562 OrganizerMaintenanceWindow = MaintenanceWindow 563 564 565 from Products.ZCatalog.ZCatalog import manage_addZCatalog 566 from Products.ZenUtils.Search import makeCaseInsensitiveFieldIndex 567 from Products.CMFCore.utils import getToolByName 568 569
570 -def createMaintenanceWindowCatalog(dmd):
571 572 catalog_name = 'maintenanceWindowSearch' 573 574 manage_addZCatalog(dmd, catalog_name, catalog_name) 575 cat = getToolByName(dmd, catalog_name) 576 577 id_index = makeCaseInsensitiveFieldIndex('getId') 578 cat._catalog.addIndex('id', id_index) 579 cat.addColumn('id')
580