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

# Source Code for Module Products.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 or (at your
      8  # option) any later version as published by 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 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
     38 -def lastDayPreviousMonth(seconds):
     39      parts = list(time.localtime(seconds))
     40      # use day 1 of this month
     41      parts[2] = 1
     42      # and go back DAY_SECONDS
     43      return time.mktime(parts) - DAY_SECONDS
     44
     45 -def addMonth(secs, dayOfMonthHint=0):
     46      base = list(time.localtime(secs))
     47      # add a month
     48      base[1] += 1
     49      # year wrap
     50      if base[1] == 13:
     51          base[0] += 1
     52          base[1] = 1
     53      # Check for the case Jan 31 becomes March 3
     54      # in that case, force it back to Feb 28
     55
     56      # first, remember the month
     57      month = base[1]
     58      if dayOfMonthHint:
     59          base[2] = dayOfMonthHint
     60      # normalize
     61      base = list(time.localtime(time.mktime(base)))
     62      # if the month changed, walk back to the end of the previous month
     63      if base[1] != month:
     64          return lastDayPreviousMonth(time.mktime(base))
     65      return time.mktime(base)
     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):
    127          self.start = start
    128          self.duration = duration
    129          self.repeat = repeat
    130          self.enabled = enabled
    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      # Nice methods used by the GUI for presentation purposes
    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
    153 -    def niceStartDateTime(self):
    154          "Return start time as a string with nice sort qualities"
    155          return Time.LocalDateTime(self.start)
    156
    157 -    def niceStartProductionState(self):
    158          "Return a string version of the startProductionState"
    159          return self.convertProdState(self.startProductionState)
    160
    161 -    def niceStopProductionState(self):
    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          # startHours, startMinutes come from menus.  No need to catch
    214          # ValueError on the int conversion.
    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          # ok, so maybe "now" is a little late: start anything that
    274          # should have been started by now
    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              # start at the most recent week-even point from the start
    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              # Move time to this year/month
    348              base[2:6] = time.localtime(self.start)[2:6]
    349              base = time.mktime(base)
    350              # creep ahead by days until it's the FSOTM
    351              # (not the most efficient implementation)
    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
    363 -    def target(self):
    364          return self.productionState().primaryAq()
    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          # Get all maintenance windows + action rules from all device classes,
    389          # devices, Systems, Locations, and Groups.
    390          # Yes, it's O(m * n)
    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                  # Note: if the mw has just ended, the self.end() method
    401                  #       has already made the mw inactive before this point
    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                  # Special case: our window's devices
    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          # Note: self.begin() starts our window before we get called, so the
    457          #       following takes into account our window state too.
    458          #       Conversely, self.end() ends the window before calling this code.
    459          devices = self.fetchDevices()
    460          minDevProdStates = self.fetchDeviceMinProdStates( devices )
    461
    462          for device in devices:
    463              if ending:
    464                  # Note: If no maintenance windows apply to a device, then the
    465                  #       device won't exist in minDevProdStates
    466                  # This takes care of the case where there are still active
    467                  # maintenance windows.
    468                  minProdState = minDevProdStates.get(device.id,
    469                                              device.preMWProductionState)
    470
    471              elif device.id in minDevProdStates:
    472                  minProdState = minDevProdStates[device.id]
    473
    474              else: # This is impossible for us to ever get here as minDevProdStates
    475                    # has been added by self.fetchDeviceMinProdStates()
    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              # Changes the current state for a device, but *not*
    483              # the preMWProductionState
    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          # Make sure that we've started before the calculation of the production
    499          # state occurs.
    500          self.started = now
    501          self.setProdState(self.startProductionState)
    502
    503
    504
    505 -    def end(self):
    506          """
    507          Hook for leaving the Maintenance Window: call if you override
    508          """
    509          log.info("Mainenance window %s ending" % self.displayName())
    510          # Make sure that the window has ended before the calculation of
    511          # the production state occurs.
    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
    562 -def createMaintenanceWindowCatalog(dmd):
    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