IntrospectionTool.py 16.4 KB
Newer Older
1
# -*- coding: utf-8 -*-
Ivan Tyagov's avatar
Ivan Tyagov committed
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 29
##############################################################################
#
# Copyright (c) 2006 Nexedi SARL and Contributors. All Rights Reserved.
#                    Ivan Tyagov <ivan@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.
#
##############################################################################

30 31
import os
import tempfile
Ivan Tyagov's avatar
Ivan Tyagov committed
32
from AccessControl import ClassSecurityInfo
33
from Products.ERP5Type.Globals import InitializeClass, DTMLFile
Jean-Paul Smets's avatar
Jean-Paul Smets committed
34
from Products.CMFCore.utils import getToolByName
Ivan Tyagov's avatar
Ivan Tyagov committed
35 36 37
from Products.ERP5Type.Tool.BaseTool import BaseTool
from Products.ERP5Type import Permissions
from AccessControl.SecurityManagement import setSecurityManager
Ivan Tyagov's avatar
Ivan Tyagov committed
38
from Products.ERP5 import _dtmldir
Rafael Monnerat's avatar
Rafael Monnerat committed
39
from Products.ERP5.Tool.LogMixin import LogMixin
40
from Products.ERP5Type.Utils import _setSuperSecurityManager
41
from App.config import getConfiguration
42 43
from AccessControl import Unauthorized
from Products.ERP5Type.Cache import CachingMethod
44
from Products.ERP5Type import tarfile
45
from cgi import escape
Ivan Tyagov's avatar
Ivan Tyagov committed
46

Jean-Paul Smets's avatar
Jean-Paul Smets committed
47 48
_MARKER = []

Rafael Monnerat's avatar
Rafael Monnerat committed
49
class IntrospectionTool(LogMixin, BaseTool):
Ivan Tyagov's avatar
Ivan Tyagov committed
50
  """
Jean-Paul Smets's avatar
Jean-Paul Smets committed
51
    This tool provides both local and remote introspection.
Ivan Tyagov's avatar
Ivan Tyagov committed
52 53 54 55 56 57 58 59 60 61 62 63
  """

  id = 'portal_introspections'
  title = 'Introspection Tool'
  meta_type = 'ERP5 Introspection Tool'
  portal_type = 'Introspection Tool'

  security = ClassSecurityInfo()

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

64 65 66
  #
  #   Remote menu management
  #
Jérome Perrin's avatar
Jérome Perrin committed
67 68
  security.declareProtected(Permissions.AccessContentsInformation,
                            'getFilteredActionDict')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
69
  def getFilteredActionDict(self, user_name=_MARKER):
Ivan Tyagov's avatar
Ivan Tyagov committed
70 71 72 73
    """
      Returns menu items for a given user
    """
    portal = self.getPortalObject()
Rafael Monnerat's avatar
Rafael Monnerat committed
74 75 76
    is_portal_manager = portal.portal_membership.checkPermission(\
      Permissions.ManagePortal, self)

Jean-Paul Smets's avatar
Jean-Paul Smets committed
77
    downgrade_authenticated_user = user_name is not _MARKER and is_portal_manager
Ivan Tyagov's avatar
Ivan Tyagov committed
78 79
    if downgrade_authenticated_user:
      # downgrade to desired user
80
      original_security_manager = _setSuperSecurityManager(self, user_name)
Ivan Tyagov's avatar
Ivan Tyagov committed
81 82

    # call the method implementing it
Rafael Monnerat's avatar
Rafael Monnerat committed
83
    erp5_menu_dict = portal.portal_actions.listFilteredActionsFor(portal)
Ivan Tyagov's avatar
Ivan Tyagov committed
84 85 86 87 88

    if downgrade_authenticated_user:
      # restore original Security Manager
      setSecurityManager(original_security_manager)

Jean-Paul Smets's avatar
Jean-Paul Smets committed
89 90 91 92 93 94
    # Unlazyfy URLs and other lazy values so that it can be marshalled
    result = {}
    for key, action_list in erp5_menu_dict.items():
      result[key] = map(lambda action:dict(action), action_list)

    return result
Ivan Tyagov's avatar
Ivan Tyagov committed
95

Jérome Perrin's avatar
Jérome Perrin committed
96 97
  security.declareProtected(Permissions.AccessContentsInformation,
                           'getModuleItemList')
98 99
  def getModuleItemList(self, user_name=_MARKER):
    """
100
      Returns module items for a given user
101 102
    """
    portal = self.getPortalObject()
Rafael Monnerat's avatar
Rafael Monnerat committed
103 104 105
    is_portal_manager = portal.portal_membership.checkPermission(
      Permissions.ManagePortal, self)

106 107 108
    downgrade_authenticated_user = user_name is not _MARKER and is_portal_manager
    if downgrade_authenticated_user:
      # downgrade to desired user
109
      original_security_manager = _setSuperSecurityManager(self, user_name)
110 111 112 113 114 115 116 117 118 119

    # call the method implementing it
    erp5_module_list = portal.ERP5Site_getModuleItemList()

    if downgrade_authenticated_user:
      # restore original Security Manager
      setSecurityManager(original_security_manager)

    return erp5_module_list

120 121 122
  #
  #   Local file access
  #
123 124 125
  def _getLocalFile(self, REQUEST, RESPONSE, file_path, 
                         tmp_file_path='/tmp/', compressed=1):
    """
Aurel's avatar
Aurel committed
126
      It should return the local file compacted or not as tar.gz.
127 128 129 130 131 132 133 134 135 136 137 138 139 140
    """
    if file_path.startswith('/'):
      raise IOError, 'The file path must be relative not absolute'
    instance_home = getConfiguration().instancehome
    file_path = os.path.join(instance_home, file_path)
    if not os.path.exists(file_path):
      raise IOError, 'The file: %s does not exist.' % file_path

    if compressed:
      tmp_file_path = tempfile.mktemp(dir=tmp_file_path)
      tmp_file = tarfile.open(tmp_file_path,"w:gz")
      tmp_file.add(file_path)
      tmp_file.close()
      RESPONSE.setHeader('Content-type', 'application/x-tar')
Aurel's avatar
Aurel committed
141 142
      RESPONSE.setHeader('Content-Disposition', \
                 'attachment;filename="%s.tar.gz"' % file_path.split('/')[-1])
143
    else:
Aurel's avatar
Aurel committed
144 145 146 147
      RESPONSE.setHeader('Content-type', 'application/txt')
      RESPONSE.setHeader('Content-Disposition', \
                 'attachment;filename="%s.txt"' % file_path.split('/')[-1])

148 149
      tmp_file_path = file_path

Aurel's avatar
Aurel committed
150

151 152 153 154 155 156 157 158 159 160 161 162 163 164
    f = open(tmp_file_path)
    try:
      RESPONSE.setHeader('Content-Length', os.stat(tmp_file_path).st_size)
      for data in f:
        RESPONSE.write(data)
    finally:
      f.close()

    if compressed:
      os.remove(tmp_file_path)

    return ''

  security.declareProtected(Permissions.ManagePortal, 'getAccessLog')
Aurel's avatar
Aurel committed
165
  def getAccessLog(self, compressed=1, REQUEST=None):
166 167 168 169 170 171 172 173 174 175
    """
      Get the Access Log.
    """
    if REQUEST is not None:
      response = REQUEST.RESPONSE
    else:
      return "FAILED"

    return self._getLocalFile(REQUEST, response, 
                               file_path='log/Z2.log', 
Aurel's avatar
Aurel committed
176
                               compressed=compressed) 
177

178
  security.declareProtected(Permissions.ManagePortal, 'getEventLog')
Aurel's avatar
Aurel committed
179
  def getEventLog(self, compressed=1, REQUEST=None):
180
    """
Aurel's avatar
Aurel committed
181
      Get the Event Log.
182 183 184 185 186 187 188 189
    """
    if REQUEST is not None:
      response = REQUEST.RESPONSE
    else:
      return "FAILED"

    return self._getLocalFile(REQUEST, response,
                               file_path='log/event.log',
Aurel's avatar
Aurel committed
190
                               compressed=compressed)
191

192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234

  def _tailFile(self, file_name, line_number=10):
    """
    Do a 'tail -f -n line_number filename'
    """
    log_file = os.path.join(getConfiguration().instancehome, file_name)
    if not os.path.exists(log_file):
      raise IOError, 'The file: %s does not exist.' % log_file

    char_per_line=75

    tailed_file = open(log_file,'r')
    while 1:
      try:
        tailed_file.seek(-1 * char_per_line * line_number, 2)
      except IOError:
        tailed_file.seek(0)
      if tailed_file.tell() == 0:
        at_start = 1
      else:
        at_start = 0

      lines = tailed_file.read().split("\n")
      if (len(lines) > (line_number + 1)) or at_start:
        break
      # The lines are bigger than we thought
      char_per_line = char_per_line * 1.3 # Inc for retry

    tailed_file.close()

    if len(lines) > line_number:
      start = len(lines) - line_number - 1
    else:
      start = 0

    return "\n".join(lines[start:len(lines)])


  security.declareProtected(Permissions.ManagePortal, 'tailEventLog')
  def tailEventLog(self):
    """
    Tail the Event Log.
    """
235
    return escape(self._tailFile('log/event.log', 50))
236 237


238 239 240
  security.declareProtected(Permissions.ManagePortal, 'getAccessLog')
  def getDataFs(self,  compressed=1, REQUEST=None):
    """
Aurel's avatar
Aurel committed
241
      Get the Data.fs.
242 243 244 245 246 247 248 249
    """
    if REQUEST is not None:
      response = REQUEST.RESPONSE
    else:
      return "FAILED"

    return self._getLocalFile(REQUEST, response,
                               file_path='var/Data.fs',
Aurel's avatar
Aurel committed
250
                               compressed=compressed)
251

252 253 254
  #
  #   Instance variable definition access
  #
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288
  security.declareProtected(Permissions.ManagePortal, '_loadExternalConfig')
  def _loadExternalConfig(self):
    """
      Load configuration from one external file, this configuration 
      should be set for security reasons to prevent people access 
      forbidden areas in the system.
    """
    def cached_loadExternalConfig():
      import ConfigParser
      config = ConfigParser.ConfigParser()
      config.readfp(open('/etc/erp5.cfg'))
      return config     

    cached_loadExternalConfig = CachingMethod(cached_loadExternalConfig,
                                id='IntrospectionTool__loadExternalConfig',
                                cache_factory='erp5_content_long')
    return  cached_loadExternalConfig()

  security.declareProtected(Permissions.ManagePortal, '_getZopeConfigurationFile')
  def _getZopeConfigurationFile(self, relative_path="", mode="r"):
    """
     Get a configuration file from the instance using relative path
    """
    if ".." in relative_path or relative_path.startswith("/"):
      raise Unauthorized("In Relative Path, you cannot use .. or startwith / for security reason.")

    instance_home = getConfiguration().instancehome
    file_path = os.path.join(instance_home, relative_path)
    if not os.path.exists(file_path):
      raise IOError, 'The file: %s does not exist.' % file_path

    return open(file_path, mode)
    

289 290 291 292 293 294 295 296
  security.declareProtected(Permissions.ManagePortal, 'getSoftwareHome')
  def getSoftwareHome(self):
    """
      EXPERIMENTAL - DEVELOPMENT

      Get the value of SOFTWARE_HOME for zopectl startup script
      or from zope.conf (whichever is most relevant)
    """
297
    return getConfiguration().softwarehome
298 299

  security.declareProtected(Permissions.ManagePortal, 'setSoftwareHome')
300
  def setSoftwareHome(self, relative_path):
301 302 303 304 305 306 307 308 309 310 311 312
    """
      EXPERIMENTAL - DEVELOPMENT

      Set the value of SOFTWARE_HOME for zopectl startup script
      or from zope.conf (whichever is most relevant)

      Rationale: multiple versions of ERP5 / Zope can be present
      at the same time on the same system

      WARNING: the list of possible path should be protected 
      if possible (ex. /etc/erp5/software_home)
    """
313 314
    config = self._loadExternalConfig()
    allowed_path_list = config.get("main", "zopehome").split("\n")
315 316
    base_zope_path = config.get("base", "base_zope_path").split("\n")
    path = "%s/%s/lib/python" % (base_zope_path,relative_path)
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337
  
    if path not in allowed_path_list:
      raise Unauthorized("You are setting one Unauthorized path as Zope Home.")

    config_file = self._getZopeConfigurationFile("bin/zopectl")
    new_file_list = []
    for line in config_file:
      if line.startswith("SOFTWARE_HOME="):
        # Only comment the line, so it can easily reverted 
        new_file_list.append("#%s" % (line))
        new_file_list.append('SOFTWARE_HOME="%s"\n' % (path))
      else:
        new_file_list.append(line)

    config_file.close()

    # reopen file for write
    config_file = self._getZopeConfigurationFile("bin/zopectl", "w")
    config_file.write("".join(new_file_list))
    config_file.close()
    return 
338 339 340 341 342 343 344

  security.declareProtected(Permissions.ManagePortal, 'getPythonExecutable')
  def getPythonExecutable(self):
    """
      Get the value of PYTHON for zopectl startup script
      or from zope.conf (whichever is most relevant)
    """
345 346 347 348 349 350 351 352 353
    config_file = self._getZopeConfigurationFile("bin/zopectl")
    new_file_list = []
    for line in config_file:
      if line.startswith("PYTHON="):
        return line.replace("PYTHON=","")

    # Not possible get configuration from the zopecl
    return None
    
354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386
  #security.declareProtected(Permissions.ManagePortal, 'setPythonExecutable')
  #def setPythonExecutable(self, path):
  #  """
  #    Set the value of PYTHON for zopectl startup script
  #    or from zope.conf (whichever is most relevant)

  #    Rationale: some day Zope will no longer use python2.4

  #    WARNING: the list of possible path should be protected 
  #    if possible (ex. /etc/erp5/python)
  #  """
  #  config = self._loadExternalConfig()
  #  allowed_path_list = config.get("main", "python").split("\n")

  #  if path not in allowed_path_list:
  #    raise Unauthorized("You are setting one Unauthorized path as Python.")

  #  config_file = self._getZopeConfigurationFile("bin/zopectl")
  #  new_file_list = []
  #  for line in config_file:
  #    if line.startswith("PYTHON="):
  #      # Only comment the line, so it can easily reverted 
  #      new_file_list.append("#%s" % (line))
  #      new_file_list.append('PYTHON="%s"\n' % (path))
  #    else:
  #      new_file_list.append(line)

  #  config_file.close()    
  #  # reopen file for write
  #  config_file = self._getZopeConfigurationFile("bin/zopectl", "w")
  #  config_file.write("".join(new_file_list))
  #  config_file.close()
  #  return 
387 388 389

  security.declareProtected(Permissions.ManagePortal, 'getProductPathList')
  def getProductPathList(self):
390 391 392 393
    """
      Get the value of SOFTWARE_HOME for zopectl startup script
      or from zope.conf (whichever is most relevant)
    """
394
    return getConfiguration().products
395

396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445
  #security.declareProtected(Permissions.ManagePortal, 'setProductPath')
  #def setProductPath(self, relative_path):
  #  """
  #    Set the value of SOFTWARE_HOME for zopectl startup script
  #    or from zope.conf (whichever is most relevant)

  #    Rationale: multiple versions of Products can be present
  #    on the same system

  #    relative_path is usually defined by a number of release 
  #     (ex. 5.4.2)

  #    WARNING: the list of possible path should be protected 
  #    if possible (ex. /etc/erp5/product)
  #  """
  #  config = self._loadExternalConfig()
  #  allowed_path_list = config.get("main", "products").split("\n")
  #  base_product_path_list = config.get("base", "base_product_path").split("\n")
  #  if len(base_product_path_list) == 0:
  #    raise Unauthorized(
  #           "base_product_path_list is not defined into configuration.")

  #  base_product_path = base_product_path_list[0]
  #  path = base_product_path + relative_path

  #  if path not in allowed_path_list:
  #    raise Unauthorized(
  #             "You are setting one Unauthorized path as Product Path (%s)." \
  #             % (path))

  #  if path not in allowed_path_list:
  #    raise Unauthorized("You are setting one Unauthorized path as Product Path.")

  #  config_file = self._getZopeConfigurationFile("etc/zope.conf")
  #  new_file_list = []
  #  for line in config_file:
  #    new_line = line
  #    if line.strip(" ").startswith("products %s" % (base_product_path)):
  #      # Only comment the line, so it can easily reverted 
  #      new_line = "#%s" % (line)
  #    new_file_list.append(new_line)
  #  # Append the new line.
  #  new_file_list.append("products %s\n" % (path))
  #  config_file.close()    

  #  # reopen file for write
  #  config_file = self._getZopeConfigurationFile("etc/zope.conf", "w")
  #  config_file.write("".join(new_file_list))
  #  config_file.close()
  #  return 
446 447 448 449

  #
  #   Library signature
  #
450
  # XXX this function can be cached to prevent disk access.
451 452 453 454 455 456 457 458
  security.declareProtected(Permissions.ManagePortal, 'getSystemSignatureDict')
  def getSystemSignatureDict(self):
    """
      Returns a dictionnary with all versions of installed libraries

      {
         'python': '2.4.3'
       , 'pysvn': '1.2.3'
459
       , 'ERP5' : "5.4.3"       
460
      }
461 462
      NOTE: consider using autoconf / automake tools ?
    """
463 464
    def tuple_to_format_str(t):
       return '.'.join([str(i) for i in t])
465 466
    from Products import ERP5 as erp5_product
    erp5_product_path =  erp5_product.__file__.split("/")[:-1]
467 468 469 470 471 472 473
    try:
      erp5_v = open("/".join((erp5_product_path) + ["VERSION.txt"])).read().strip()
      erp5_version = erp5_v.replace("ERP5 ", "")
    except:
       erp5_version = None

    from App import version_txt
474
    zope_version = tuple_to_format_str(version_txt.getZopeVersion()[:3])
475

476 477
    from sys import version_info
    # Get only x.x.x numbers.
478
    py_version = tuple_to_format_str(version_info[:3])
479 480 481 482 483 484 485
    try:
      import pysvn
      # Convert tuple to x.x.x format
      pysvn_version =  tuple_to_format_str(pysvn.version)
    except:
      pysvn_version = None
    
486 487
    return { "python" : py_version , "pysvn"  : pysvn_version ,
             "erp5"   : erp5_version, "zope"   : zope_version
488
           }
489

Ivan Tyagov's avatar
Ivan Tyagov committed
490
InitializeClass(IntrospectionTool)