1
2
3
4
5
6
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
42
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):
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
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
152 "Return start time as a string with nice sort qualities"
153 return Time.LocalDateTime(self.start)
154
156 "Return a string version of the startProductionState"
157 return self.convertProdState(self.startProductionState)
158
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
212
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
273
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
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
350 base[2:6] = time.localtime(self.start)[2:6]
351 base = time.mktime(base)
352
353
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
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
391
392
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
403
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
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
459
460
461 devices = self.fetchDevices()
462 minDevProdStates = self.fetchDeviceMinProdStates( devices )
463
464 for device in devices:
465 if ending:
466
467
468
469
470 minProdState = minDevProdStates.get(device.id,
471 device.preMWProductionState)
472
473 elif device.id in minDevProdStates:
474 minProdState = minDevProdStates[device.id]
475
476 else:
477
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
485
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
507
508 self.started = now
509 self.setProdState(self.startProductionState)
510
511
512
514 """
515 Hook for leaving the Maintenance Window: call if you override
516 """
517 log.info("Mainenance window %s ending" % self.displayName())
518
519
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
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