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