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 as published by
      8  # 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                  { '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):
    132          self.start = start
    133          self.duration = duration
    134          self.repeat = repeat
    135          self.enabled = enabled
    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      # Nice methods used by the GUI for presentation purposes
    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
    158 -    def niceStartDateTime(self):
    159          "Return start time as a string with nice sort qualities"
    160          return Time.LocalDateTime(self.start)
    161
    162 -    def niceStartProductionState(self):
    163          "Return a string version of the startProductionState"
    164          return self.convertProdState(self.startProductionState)
    165
    166 -    def niceStopProductionState(self):
    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          # startHours, startMinutes come from menus.  No need to catch
    219          # ValueError on the int conversion.
    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          # ok, so maybe "now" is a little late: start anything that
    279          # should have been started by now
    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              # start at the most recent week-even point from the start
    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              # Move time to this year/month
    353              base[2:6] = time.localtime(self.start)[2:6]
    354              base = time.mktime(base)
    355              # creep ahead by days until it's the FSOTM
    356              # (not the most efficient implementation)
    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
    368 -    def target(self):
    369          return self.productionState().primaryAq()
    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          # Get all maintenance windows + action rules from all device classes,
    394          # devices, Systems, Locations, and Groups.
    395          # Yes, it's O(m * n)
    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                  # Note: if the mw has just ended, the self.end() method
    406                  #       has already made the mw inactive before this point
    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                  # Special case: our window's devices
    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          # Note: self.begin() starts our window before we get called, so the
    462          #       following takes into account our window state too.
    463          #       Conversely, self.end() ends the window before calling this code.
    464          devices = self.fetchDevices()
    465          minDevProdStates = self.fetchDeviceMinProdStates( devices )
    466
    467          for device in devices:
    468              if ending:
    469                  # Note: If no maintenance windows apply to a device, then the
    470                  #       device won't exist in minDevProdStates
    471                  # This takes care of the case where there are still active
    472                  # maintenance windows.
    473                  minProdState = minDevProdStates.get(device.id,
    474                                              device.preMWProductionState)
    475
    476              elif device.id in minDevProdStates:
    477                  minProdState = minDevProdStates[device.id]
    478
    479              else: # This is impossible for us to ever get here as minDevProdStates
    480                    # has been added by self.fetchDeviceMinProdStates()
    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              # Changes the current state for a device, but *not*
    488              # the preMWProductionState
    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          # Make sure that we've started before the calculation of the production
    504          # state occurs.
    505          self.started = now
    506          self.setProdState(self.startProductionState)
    507
    508
    509
    510 -    def end(self):
    511          """
    512          Hook for leaving the Maintenance Window: call if you override
    513          """
    514          log.info("Mainenance window %s ending" % self.displayName())
    515          # Make sure that the window has ended before the calculation of
    516          # the production state occurs.
    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
    566 -def createMaintenanceWindowCatalog(dmd):
    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