AlarmTool.py 9.35 KB
Newer Older
Sebastien Robin's avatar
Sebastien Robin committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
##############################################################################
#
# Copyright (c) 2004 Nexedi SARL and Contributors. All Rights Reserved.
#                    Sebastien Robin <seb@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################

29
import time
30 31
import threading

Sebastien Robin's avatar
Sebastien Robin committed
32
from AccessControl import ClassSecurityInfo
33
from AccessControl.SecurityManagement import newSecurityManager
Sebastien Robin's avatar
Sebastien Robin committed
34
from Globals import InitializeClass, DTMLFile, PersistentMapping
Jean-Paul Smets's avatar
Jean-Paul Smets committed
35
from Products.ERP5Type.Core.Folder import Folder
Sebastien Robin's avatar
Sebastien Robin committed
36 37 38 39
from Products.ERP5Type.Tool.BaseTool import BaseTool
from Products.ERP5Type import Permissions
from Products.ERP5 import _dtmldir
from DateTime import DateTime
40 41
import urllib
import socket
Sebastien Robin's avatar
Sebastien Robin committed
42

43
from zLOG import LOG, INFO
Sebastien Robin's avatar
Sebastien Robin committed
44

45 46 47 48
import re
# minimal IP:Port regexp
NODE_RE = re.compile('^\d+\.\d+\.\d+\.\d+:\d+$')

49 50 51
try:
  from Products.TimerService import getTimerService
except ImportError:
52
  def getTimerService(self):
53
    pass
Sebastien Robin's avatar
Sebastien Robin committed
54

55 56
last_tic = time.time()
last_tic_lock = threading.Lock()
57
current_node = None
58

Sebastien Robin's avatar
Sebastien Robin committed
59 60
class AlarmTool(BaseTool):
  """
61 62
    This tool manages alarms.

Vincent Pelletier's avatar
Vincent Pelletier committed
63
    It is used as a central managment point for all alarms.
Sebastien Robin's avatar
Sebastien Robin committed
64

Vincent Pelletier's avatar
Vincent Pelletier committed
65
    Inside this tool we have a way to retrieve all reports coming
Vincent Pelletier's avatar
Vincent Pelletier committed
66
    from Alarms,...
Sebastien Robin's avatar
Sebastien Robin committed
67 68 69 70 71 72 73 74 75 76 77
  """
  id = 'portal_alarms'
  meta_type = 'ERP5 Alarm Tool'
  portal_type = 'Alarm Tool'

  # Declarative Security
  security = ClassSecurityInfo()

  security.declareProtected( Permissions.ManagePortal, 'manage_overview' )
  manage_overview = DTMLFile( 'explainAlarmTool', _dtmldir )

78 79
  security.declareProtected( Permissions.ManagePortal , 'manageAlarmNode' )
  manageAlarmNode = DTMLFile( 'manageAlarmNode', _dtmldir )
80 81


Sebastien Robin's avatar
Sebastien Robin committed
82
  manage_options = ( ( { 'label'   : 'Overview'
Vincent Pelletier's avatar
Vincent Pelletier committed
83 84
                       , 'action'   : 'manage_overview'
                       }
85 86
                     , { 'label'   : 'Alarm Node'
                       , 'action'   : 'manageAlarmNode'
87 88
                       }
                     ,
Vincent Pelletier's avatar
Vincent Pelletier committed
89 90 91
                     )
                     + Folder.manage_options
                   )
Sebastien Robin's avatar
Sebastien Robin committed
92

93
  _properties = ( {'id': 'interval', 'type': 'int', 'mode': 'w', }, )
94
  interval = 60 # Default interval for alarms is 60 seconds
95 96 97 98 99 100 101
  # alarmNode possible values:
  #  ''      Bootstraping. The first node to call process_timer will cause this
  #          value to be set to its node id.
  #  (other) Node id matching this value will be the alarmNode.
  # Those values were chosen for backward compatibility with sites having an
  # alarmNode set to '' but expecting alarms to be executed. Use None to
  # disable alarm processing (see setAlarmNode).
102
  alarmNode = ''
Vincent Pelletier's avatar
Vincent Pelletier committed
103

Sebastien Robin's avatar
Sebastien Robin committed
104
  # API to manage alarms
Vincent Pelletier's avatar
Vincent Pelletier committed
105 106 107 108 109 110 111
  # Aim of this API:
  #-- see all alarms stored everywhere
  #-- defines global alarms
  #-- activate an alarm
  #-- see reports
  #-- see active alarms
  #-- retrieve all alarms
Sebastien Robin's avatar
Sebastien Robin committed
112 113

  security.declareProtected(Permissions.ModifyPortalContent, 'getAlarmList')
Vincent Pelletier's avatar
Vincent Pelletier committed
114
  def getAlarmList(self, to_active = 0):
Sebastien Robin's avatar
Sebastien Robin committed
115
    """
Vincent Pelletier's avatar
Vincent Pelletier committed
116
      We retrieve thanks to the catalog the full list of alarms
Sebastien Robin's avatar
Sebastien Robin committed
117
    """
Sebastien Robin's avatar
Sebastien Robin committed
118
    if to_active:
119
      now = DateTime()
120 121
      catalog_search = self.portal_catalog.unrestrictedSearchResults(
        portal_type = self.getPortalAlarmTypeList(),
122
        alarm_date={'query':now,'range':'ngt'}
Vincent Pelletier's avatar
Vincent Pelletier committed
123
      )
124
      # check again the alarm date in case the alarm was not yet reindexed
Vincent Pelletier's avatar
Vincent Pelletier committed
125 126
      alarm_list = [x.getObject() for x in catalog_search \
          if x.getObject().getAlarmDate()<=now]
Sebastien Robin's avatar
Sebastien Robin committed
127
    else:
128
      catalog_search = self.portal_catalog.unrestrictedSearchResults(
Vincent Pelletier's avatar
Vincent Pelletier committed
129 130
        portal_type = self.getPortalAlarmTypeList()
      )
131
      alarm_list = [x.getObject() for x in catalog_search]
Sebastien Robin's avatar
Sebastien Robin committed
132 133 134 135 136
    return alarm_list

  security.declareProtected(Permissions.ModifyPortalContent, 'tic')
  def tic(self):
    """
Vincent Pelletier's avatar
Vincent Pelletier committed
137 138
      We will look at all alarms and see if they should be activated,
      if so then we will activate them.
Sebastien Robin's avatar
Sebastien Robin committed
139
    """
140
    for alarm in self.getAlarmList(to_active=1):
141
      if alarm is not None:
142
        user = alarm.getWrappedOwner()
143
        newSecurityManager(self.REQUEST, user)
144
        if alarm.isActive() or not alarm.isEnabled():
145 146
          # do nothing if already active, or not enabled
          continue
147
        alarm.activeSense()
148

149 150
  security.declareProtected(Permissions.ManageProperties, 'isSubscribed')
  def isSubscribed(self):
Jérome Perrin's avatar
Jérome Perrin committed
151 152 153 154 155 156
    """ return True, if we are subscribed to TimerService.
    Otherwise return False.
    """
    service = getTimerService(self)
    if not service:
      LOG('AlarmTool', INFO, 'TimerService not available')
157 158
      return False

Jérome Perrin's avatar
Jérome Perrin committed
159 160 161 162 163
    path = '/'.join(self.getPhysicalPath())
    if path in service.lisSubscriptions():
      return True
    return False

164 165
  security.declareProtected(Permissions.ManageProperties, 'subscribe')
  def subscribe(self):
Vincent Pelletier's avatar
Vincent Pelletier committed
166 167 168
    """
      Subscribe to the global Timer Service.
    """
169 170
    service = getTimerService(self)
    if not service:
171 172
      LOG('AlarmTool', INFO, 'TimerService not available')
      return
173 174 175 176 177
    service.subscribe(self)
    return "Subscribed to Timer Service"

  security.declareProtected(Permissions.ManageProperties, 'unsubscribe')
  def unsubscribe(self):
Vincent Pelletier's avatar
Vincent Pelletier committed
178 179 180
    """
      Unsubscribe from the global Timer Service.
    """
181 182
    service = getTimerService(self)
    if not service:
183 184
      LOG('AlarmTool', INFO, 'TimerService not available')
      return
185 186 187 188 189
    service.unsubscribe(self)
    return "Usubscribed from Timer Service"

  def manage_beforeDelete(self, item, container):
    self.unsubscribe()
190
    BaseTool.inheritedAttribute('manage_beforeDelete')(self, item, container)
191

192 193
  def manage_afterAdd(self, item, container):
    self.subscribe()
194
    BaseTool.inheritedAttribute('manage_afterAdd')(self, item, container)
195

196
  security.declarePrivate('process_timer')
197
  def process_timer(self, interval, tick, prev="", next=""):
198
    """
Vincent Pelletier's avatar
Vincent Pelletier committed
199 200 201
      Call tic() every x seconds. x is defined in self.interval
      This method is called by TimerService in the interval given
      in zope.conf. The Default is every 5 seconds.
202
    """
203 204 205 206 207 208 209 210 211 212 213 214
    acquired = last_tic_lock.acquire(0)
    if not acquired:
      return
    try:
      # only start when we are the alarmNode
      alarmNode = self.getAlarmNode()
      current_node = self.getCurrentNode()
      if alarmNode == '':
        self.setAlarmNode(current_node)
        alarmNode = current_node
      if alarmNode == current_node:
        global last_tic
215 216
        now = tick.timeTime()
        if now - last_tic >= self.interval:
217
          self.tic()
218
          last_tic = now
219 220
    finally:
      last_tic_lock.release()
221 222 223

  def getCurrentNode(self):
      """ Return current node in form ip:port """
224 225 226 227 228 229 230 231 232 233 234 235 236 237
      global current_node
      if current_node is None:
        port = ''
        from asyncore import socket_map
        for k, v in socket_map.items():
            if hasattr(v, 'port'):
                # see Zope/lib/python/App/ApplicationManager.py: def getServers(self)
                type = str(getattr(v, '__class__', 'unknown'))
                if type == 'ZServer.HTTPServer.zhttp_server':
                    port = v.port
                    break
        ip = socket.gethostbyname(socket.gethostname())
        current_node = '%s:%s' %(ip, port)
      return current_node
238 239 240 241 242 243
      
  security.declarePublic('getAlarmNode')
  def getAlarmNode(self):
      """ Return the alarmNode """
      return self.alarmNode

244 245 246 247 248 249 250 251 252 253 254
  def setAlarmNode(self, alarm_node):
    """
      When alarm_node evaluates to false, set a None value:
      Its meaning is that alarm processing is disabled.
      This avoids an empty string to make the system re-enter boostrap mode.
    """
    if alarm_node:
      self.alarmNode = alarm_node
    else:
      self.alarmNode = None

255 256 257 258 259 260 261 262
  def _isValidNodeName(self, node_name) :
    """Check we have been provided a good node name"""
    return isinstance(node_name, str) and NODE_RE.match(node_name)
      
  security.declarePublic('manage_setAlarmNode')
  def manage_setAlarmNode(self, alarmNode, REQUEST=None):
      """ set the alarm node """   
      if not alarmNode or self._isValidNodeName(alarmNode):
263
        self.setAlarmNode(alarmNode)
264 265 266 267 268 269 270 271 272 273 274
        if REQUEST is not None:
            REQUEST.RESPONSE.redirect(
                REQUEST.URL1 +
                '/manageAlarmNode?manage_tabs_message=' +
                urllib.quote("Distributing Node successfully changed."))
      else :
        if REQUEST is not None:
            REQUEST.RESPONSE.redirect(
                REQUEST.URL1 +
                '/manageAlarmNode?manage_tabs_message=' +
                urllib.quote("Malformed Distributing Node."))
Sebastien Robin's avatar
Sebastien Robin committed
275