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

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