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

# Source Code for Module Products.ZenModel.MaintenanceWindow

      1  ##############################################################################
      2  #
      3  # Copyright (C) Zenoss, Inc. 2007, 2009, all rights reserved.
      4  #
      5  # This content is made available according to terms specified in
      6  # License.zenoss under the directory where your Zenoss product is installed.
      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
     36 -def lastDayPreviousMonth(seconds):
     37      parts = list(time.localtime(seconds))
     38      # use day 1 of this month
     39      parts[2] = 1
     40      # and go back DAY_SECONDS
     41      return time.mktime(parts) - DAY_SECONDS
     42
     43 -def addMonth(secs, dayOfMonthHint=0):
     44      base = list(time.localtime(secs))
     45      # add a month
     46      base[1] += 1
     47      # year wrap
     48      if base[1] == 13:
     49          base[0] += 1
     50          base[1] = 1
     51      # Check for the case Jan 31 becomes March 3
     52      # in that case, force it back to Feb 28
     53
     54      # first, remember the month
     55      month = base[1]
     56      if dayOfMonthHint:
     57          base[2] = dayOfMonthHint
     58      # normalize
     59      base = list(time.localtime(time.mktime(base)))
     60      # if the month changed, walk back to the end of the previous month
     61      if base[1] != month:
     62          return lastDayPreviousMonth(time.mktime(base))
     63      return time.mktime(base)
     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):
    125          self.start = start
    126          self.duration = duration
    127          self.repeat = repeat
    128          self.enabled = enabled
    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      # Nice methods used by the GUI for presentation purposes
    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
    151 -    def niceStartDateTime(self):
    152          "Return start time as a string with nice sort qualities"
    153          return Time.LocalDateTime(self.start)
    154
    155 -    def niceStartProductionState(self):
    156          "Return a string version of the startProductionState"
    157          return self.convertProdState(self.startProductionState)
    158
    159 -    def niceStopProductionState(self):
    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          # startHours, startMinutes come from menus.  No need to catch
    212          # ValueError on the int conversion.
    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          # ok, so maybe "now" is a little late: start anything that
    273          # should have been started by now
    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              # start at the most recent week-even point from the start
    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              # Move time to this year/month
    350              base[2:6] = time.localtime(self.start)[2:6]
    351              base = time.mktime(base)
    352              # creep ahead by days until it's the FSOTM
    353              # (not the most efficient implementation)
    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
    365 -    def target(self):
    366          return self.productionState().primaryAq()
    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          # Get all maintenance windows + action rules from all device classes,
    391          # devices, Systems, Locations, and Groups.
    392          # Yes, it's O(m * n)
    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                  # Note: if the mw has just ended, the self.end() method
    403                  #       has already made the mw inactive before this point
    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                  # Special case: our window's devices
    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          # Note: self.begin() starts our window before we get called, so the
    459          #       following takes into account our window state too.
    460          #       Conversely, self.end() ends the window before calling this code.
    461          devices = self.fetchDevices()
    462          minDevProdStates = self.fetchDeviceMinProdStates( devices )
    463
    464          for device in devices:
    465              if ending:
    466                  # Note: If no maintenance windows apply to a device, then the
    467                  #       device won't exist in minDevProdStates
    468                  # This takes care of the case where there are still active
    469                  # maintenance windows.
    470                  minProdState = minDevProdStates.get(device.id,
    471                                              device.preMWProductionState)
    472
    473              elif device.id in minDevProdStates:
    474                  minProdState = minDevProdStates[device.id]
    475
    476              else: # This is impossible for us to ever get here as minDevProdStates
    477                    # has been added by self.fetchDeviceMinProdStates()
    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              # Changes the current state for a device, but *not*
    485              # the preMWProductionState
    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          # Make sure that we've started before the calculation of the production
    507          # state occurs.
    508          self.started = now
    509          self.setProdState(self.startProductionState)
    510
    511
    512
    513 -    def end(self):
    514          """
    515          Hook for leaving the Maintenance Window: call if you override
    516          """
    517          log.info("Mainenance window %s ending" % self.displayName())
    518          # Make sure that the window has ended before the calculation of
    519          # the production state occurs.
    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
    570 -def createMaintenanceWindowCatalog(dmd):
    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