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 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
42
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):
135
136 - def set(self, start, duration, repeat, enabled=True):
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
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
164 "Return start time as a string with nice sort qualities"
165 return Time.LocalDateTime(self.start)
166
168 "Return a string version of the startProductionState"
169 return self.convertProdState(self.startProductionState)
170
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
224
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
284
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
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
358 base[2:6] = time.localtime(self.start)[2:6]
359 base = time.mktime(base)
360
361
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
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
399
400
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
411
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
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
467
468
469 devices = self.fetchDevices()
470 minDevProdStates = self.fetchDeviceMinProdStates( devices )
471
472 for device in devices:
473 if ending:
474
475
476
477
478 minProdState = minDevProdStates.get(device.id,
479 device.preMWProductionState)
480
481 elif device.id in minDevProdStates:
482 minProdState = minDevProdStates[device.id]
483
484 else:
485
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
493
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
509
510 self.started = now
511 self.setProdState(self.startProductionState)
512
513
514
516 """
517 Hook for leaving the Maintenance Window: call if you override
518 """
519 log.info("Mainenance window %s ending" % self.displayName())
520
521
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
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