1
2
3
4
5
6
7
8
9
10
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
44
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):
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
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
154 "Return start time as a string with nice sort qualities"
155 return Time.LocalDateTime(self.start)
156
158 "Return a string version of the startProductionState"
159 return self.convertProdState(self.startProductionState)
160
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
214
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
274
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
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
348 base[2:6] = time.localtime(self.start)[2:6]
349 base = time.mktime(base)
350
351
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
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
389
390
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
401
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
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
457
458
459 devices = self.fetchDevices()
460 minDevProdStates = self.fetchDeviceMinProdStates( devices )
461
462 for device in devices:
463 if ending:
464
465
466
467
468 minProdState = minDevProdStates.get(device.id,
469 device.preMWProductionState)
470
471 elif device.id in minDevProdStates:
472 minProdState = minDevProdStates[device.id]
473
474 else:
475
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
483
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
499
500 self.started = now
501 self.setProdState(self.startProductionState)
502
503
504
506 """
507 Hook for leaving the Maintenance Window: call if you override
508 """
509 log.info("Mainenance window %s ending" % self.displayName())
510
511
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
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