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 { '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):
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
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
159 "Return start time as a string with nice sort qualities"
160 return Time.LocalDateTime(self.start)
161
163 "Return a string version of the startProductionState"
164 return self.convertProdState(self.startProductionState)
165
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
219
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
279
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
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
353 base[2:6] = time.localtime(self.start)[2:6]
354 base = time.mktime(base)
355
356
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
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
394
395
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
406
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
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
462
463
464 devices = self.fetchDevices()
465 minDevProdStates = self.fetchDeviceMinProdStates( devices )
466
467 for device in devices:
468 if ending:
469
470
471
472
473 minProdState = minDevProdStates.get(device.id,
474 device.preMWProductionState)
475
476 elif device.id in minDevProdStates:
477 minProdState = minDevProdStates[device.id]
478
479 else:
480
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
488
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
504
505 self.started = now
506 self.setProdState(self.startProductionState)
507
508
509
511 """
512 Hook for leaving the Maintenance Window: call if you override
513 """
514 log.info("Mainenance window %s ending" % self.displayName())
515
516
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
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