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

Source Code for Module Products.ZenModel.MaintenanceWindow

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