[Zope-Checkins] CVS: Zope/lib/python/Products/ZODBMountPoint - Mount.py:1.1 MountedObject.py:1.1 __init__.py:1.1

Chris McDonough chrism@zope.com
Sat, 19 Jul 2003 22:56:07 -0400


Update of /cvs-repository/Zope/lib/python/Products/ZODBMountPoint
In directory cvs.zope.org:/tmp/cvs-serv5583/lib/python/Products/ZODBMountPoint

Added Files:
	Mount.py MountedObject.py __init__.py 
Log Message:
Integrate DBTab into HEAD.

DBTab now obtains all values related to storages and databases from zope.conf.  It is also now just a package rather than a product.

A new product named ZODBMountPoint exposes the mount point functionality to the ZMI.


=== Added File Zope/lib/python/Products/ZODBMountPoint/Mount.py ===
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""ZODB Mounted database support, simplified for DBTab.

$Id: Mount.py,v 1.1 2003/07/20 02:56:01 chrism Exp $"""

import time, sys

import Persistence, Acquisition
from Acquisition import aq_base
from ZODB.POSException import MountedStorageError
from zLOG import LOG, ERROR, INFO, WARNING


class MountPoint(Persistence.Persistent, Acquisition.Implicit):
    '''The base class for a Zope object which, when traversed,
    accesses a different database.
    '''

    # Default values for non-persistent variables.
    _v_data = None   # An object in an open connection
    _v_connect_error = None

    def __init__(self, id):
        self.id = id

    def _getDB(self):
        """Hook for getting the DB object for this mount point.
        """
        raise NotImplementedError

    def _getDBName(self):
        """Hook for getting the name of the database for this mount point.
        """
        raise NotImplementedError

    def _getRootDBName(self):
        """Hook for getting the name of the root database.
        """
        raise NotImplementedError

    def _traverseToMountedRoot(self, root, mount_parent):
        """Hook for getting the object to be mounted.
        """
        raise NotImplementedError

    def __repr__(self):
        return "%s(id=%s)" % (self.__class__.__name__, repr(self.id))


    def _getMountedConnection(self, anyjar):
        db_name = self._getDBName()
        conn = anyjar._getMountedConnection(db_name)
        if conn is None:
            root_conn = anyjar._getRootConnection()
            if db_name == self._getRootDBName():
                conn = root_conn
            else:
                conn = self._getDB().open(version=root_conn.getVersion())
                root_conn._addMountedConnection(db_name, conn)
        return conn


    def _getOrOpenObject(self, parent):
        t = self._v_data
        if t is not None:
            data = t[0]
        else:
            self._v_connect_error = None
            conn = None
            try:
                anyjar = self._p_jar
                if anyjar is None:
                    anyjar = parent._p_jar
                conn = self._getMountedConnection(anyjar)
                root = conn.root()
                obj = self._traverseToMountedRoot(root, parent)
                data = aq_base(obj)
                # Store the data object in a tuple to hide from acquisition.
                self._v_data = (data,)
            except:
                # Possibly broken database.
                self._logConnectException()
                raise

            try:
                # XXX This method of finding the mount point is deprecated.
                # Do not use the _v_mount_point_ attribute.
                data._v_mount_point_ = (aq_base(self),)
            except:
                # Might be a read-only object.
                pass

        return data.__of__(parent)


    def __of__(self, parent):
        # Accesses the database, returning an acquisition
        # wrapper around the connected object rather than around self.
        try:
            return self._getOrOpenObject(parent)
        except:
            return Acquisition.ImplicitAcquisitionWrapper(self, parent)


    def _test(self, parent):
        '''Tests the database connection.
        '''
        self._getOrOpenObject(parent)
        return 1


    def _logConnectException(self):
        '''Records info about the exception that just occurred.
        '''
        try:
            from cStringIO import StringIO
        except:
            from StringIO import StringIO
        import traceback
        exc = sys.exc_info()
        LOG('ZODB', ERROR, 'Failed to mount database. %s (%s)' % exc[:2],
            error=exc)
        f=StringIO()
        traceback.print_tb(exc[2], 100, f)
        self._v_connect_error = (exc[0], exc[1], f.getvalue())
        exc = None



class ConnectionPatches:
    # Changes to Connection.py that might fold into ZODB

    _root_connection = None
    _mounted_connections = None

    def _getRootConnection(self):
        root_conn = self._root_connection
        if root_conn is None:
            return self
        else:
            return root_conn

    def _getMountedConnection(self, name):
        conns = self._getRootConnection()._mounted_connections
        if conns is None:
            return None
        else:
            return conns.get(name)

    def _addMountedConnection(self, name, conn):
        if conn._root_connection is not None:
            raise ValueError, 'Connection %s is already mounted' % repr(conn)
        root_conn = self._getRootConnection()
        conns = root_conn._mounted_connections
        if conns is None:
            conns = {}
            root_conn._mounted_connections = conns
        if conns.has_key(name):
            raise KeyError, 'A connection named %s already exists' % repr(name)
        conn._root_connection = root_conn
        conns[name] = conn

    def _setDB(self, odb):
        self._real_setDB(odb)
        conns = self._mounted_connections
        if conns:
            for conn in conns.values():
                conn._setDB(conn._db)

    def close(self):
        if self._root_connection is not None:
            raise RuntimeError("Should not close mounted connections directly")
        conns = self._mounted_connections
        if conns:
            for conn in conns.values():
                # Notify the activity monitor
                db = conn.db()
                f = getattr(db, 'getActivityMonitor', None)
                if f is not None:
                    am = f()
                    if am is not None:
                        am.closedConnection(conn)
                conn._incrgc() # This is a good time to do some GC
                # XXX maybe we ought to call the close callbacks.
                conn._storage = conn._tmp = conn.new_oid = conn._opened = None
                conn._debug_info = ()
                # The mounted connection keeps a reference to
                # its database, but nothing else.
                # Note that mounted connections can not operate
                # independently, so don't use _closeConnection() to
                # return them to the pool.  Only the root connection
                # should be returned.
        # Close this connection only after the mounted connections
        # have been closed.  Otherwise, this connection gets returned
        # to the pool too early and another thread might use this
        # connection before the mounted connections have all been
        # closed.
        self._real_close()

if 1:
    # patch Connection.py.
    from ZODB.Connection import Connection
    Connection._real_setDB = Connection._setDB
    Connection._real_close = Connection.close

    for k, v in ConnectionPatches.__dict__.items():
        setattr(Connection, k, v)



=== Added File Zope/lib/python/Products/ZODBMountPoint/MountedObject.py ===
##############################################################################
#
# Copyright (c) 2002 Zope Corporation and Contributors.
# All Rights Reserved.
# 
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
# 
##############################################################################
"""DBTab mount point (stored in ZODB).

$Id: MountedObject.py,v 1.1 2003/07/20 02:56:01 chrism Exp $
"""

import os

import Globals
from Acquisition import aq_base, aq_inner, aq_parent
from AccessControl.ZopeGuards import guarded_getattr
from OFS.SimpleItem import SimpleItem
from OFS.Folder import Folder
from Products.PageTemplates.PageTemplateFile import PageTemplateFile

from Mount import MountPoint


_www = os.path.join(os.path.dirname(__file__), 'www')


configuration = None

def getConfiguration():
    from App.config import getConfiguration
    global configuration
    if configuration is None:
        configuration = getConfiguration().dbtab
    return configuration

def setConfiguration(c):
    global configuration
    configuration = c

class SimpleTrailblazer:
    """Follows Zope paths.  If a path is not found, creates a Folder.

    Respects Zope security.
    """

    restricted = 1

    def __init__(self, base):
        self.base = base

    def _construct(self, context, id, final):
        """Creates and returns the named folder."""
        dispatcher = guarded_getattr(context, 'manage_addProduct')['OFSP']
        factory = guarded_getattr(dispatcher, 'manage_addFolder')
        factory(id)
        o = context.restrictedTraverse(id)
        # Commit a subtransaction to assign the new object to
        # the correct database.
        get_transaction().commit(1)
        return o

    def traverseOrConstruct(self, path, omit_final=0):
        """Traverses a path, constructing it if necessary."""
        container = self.base
        parts = filter(None, path.split('/'))
        if omit_final:
            if len(parts) < 1:
                raise ValueError, 'Path %s is not a valid mount path' % path
            parts = parts[:-1]
        for part in parts:
            try:
                if self.restricted:
                    container = container.restrictedTraverse(part)
                else:
                    container = container.unrestrictedTraverse(part)
            except (KeyError, AttributeError):
                # Try to create a container in this place.
                container = self._construct(container, part)
        return container

    
class CustomTrailblazer (SimpleTrailblazer):
    """Like SimpleTrailblazer but creates custom objects.

    Does not respect Zope security because this may be invoked before
    security and products get initialized.
    """

    restricted = 0

    def __init__(self, base, container_class=None):
        self.base = base
        if not container_class:
            container_class = 'OFS.Folder.Folder'
        pos = container_class.rfind('.')
        if pos < 0:
            raise ValueError("Not a valid container_class: %s" % repr(
                container_class))
        self.module_name = container_class[:pos]
        self.class_name = container_class[pos + 1:]

    def _construct(self, context, id):
        """Creates and returns the named object."""
        jar = self.base._p_jar
        klass = jar.db()._classFactory(jar, self.module_name, self.class_name)
        obj = klass(id)
        obj._setId(id)
        context._setObject(id, obj)
        obj = context.unrestrictedTraverse(id)
        # Commit a subtransaction to assign the new object to
        # the correct database.
        get_transaction().commit(1)
        return obj


class MountedObject(MountPoint, SimpleItem):
    '''A MountPoint with a basic interface for displaying the
    reason the database did not connect.
    '''
    meta_type = 'ZODB Mount Point'
    _isMountedObject = 1
    _create_mount_points = 0

    icon = 'p_/broken'
    manage_options = ({'label':'Traceback', 'action':'manage_traceback'},)
    _v_mount_params = None

    manage_traceback = PageTemplateFile('mountfail.pt', _www)

    def __init__(self, path):
        path = str(path)
        self._path = path
        id = path.split('/')[-1]
        MountPoint.__init__(self, id)

    def mount_error_(self):
        return self._v_connect_error

    def _getDB(self):
        """Hook for getting the DB object for this mount point.
        """
        return getConfiguration().getDatabase(self._path)

    def _getDBName(self):
        """Hook for getting the name of the database for this mount point.
        """
        return getConfiguration().getDatabaseFactory(self._path).getName()

    def _getRootDBName(self):
        """Hook for getting the name of the root database.
        """
        return getConfiguration().getDatabaseFactory('/').getName()

    def _loadMountParams(self):
        factory = getConfiguration().getDatabaseFactory(self._path)
        params = factory.getMountParams(self._path)
        self._v_mount_params = params
        return params

    def _traverseToMountedRoot(self, root, mount_parent):
        """Hook for getting the object to be mounted.
        """
        params = self._v_mount_params
        if params is None:
            params = self._loadMountParams()
        real_root, real_path, container_class = params
        if real_root is None:
            real_root = 'Application'
        try:
            obj = root[real_root]
        except KeyError:
            if container_class or self._create_mount_points:
                # Create a database automatically.
                from OFS.Application import Application
                obj = Application()
                root[real_root] = obj
                # Get it into the database
                get_transaction().commit(1)
            else:
                raise

        if real_path is None:
            real_path = self._path
        if real_path and real_path != '/':
            try:
                obj = obj.unrestrictedTraverse(real_path)
            except (KeyError, AttributeError):
                if container_class or self._create_mount_points:
                    blazer = CustomTrailblazer(obj, container_class)
                    obj = blazer.traverseOrConstruct(real_path)
                else:
                    raise
        return obj

Globals.InitializeClass(MountedObject)


def getMountPoint(ob):
    """Gets the mount point for a mounted object.

    Returns None if the object is not a mounted object.
    """
    container = aq_parent(aq_inner(ob))
    mps = getattr(container, '_mount_points', None)
    if mps:
        mp = mps.get(ob.getId())
        if mp is not None and (mp._p_jar is ob._p_jar or ob._p_jar is None):
            # Since the mount point and the mounted object are from
            # the same connection, the mount point must have been
            # replaced.  The object is not mounted after all.
            return None
        # else the object is mounted.
        return mp
    return None


def setMountPoint(container, id, mp):
    mps = getattr(container, '_mount_points', None)
    if mps is None:
        container._mount_points = {id: aq_base(mp)}
    else:
        container._p_changed = 1
        mps[id] = aq_base(mp)


manage_addMountsForm = PageTemplateFile('addMountsForm.pt', _www)

def manage_getMountStatus(dispatcher):
    """Returns the status of each mount point specified by dbtab.conf.
    """
    res = []
    conf = getConfiguration()
    items = conf.listMountPaths()
    items.sort()
    root = dispatcher.getPhysicalRoot()
    for path, name in items:
        if not path or path == '/':
            # Ignore the root mount.
            continue
        o = root.unrestrictedTraverse(path, None)
        # Examine the _v_mount_point_ attribute to verify traversal
        # to the correct mount point.
        if o is None:
            exists = 0
            status = 'Ready to create'
        elif getattr(o, '_isMountedObject', 0):
            # Oops, didn't actually mount!
            exists = 1
            t, v = o._v_connect_error[:2]
            status = '%s: %s' % (t, v)
        else:
            exists = 1
            mp = getMountPoint(o)
            if mp is None:
                mp_old = getattr(o, '_v_mount_point_', None)
                if mp_old is not None:
                    # Use the old method of accessing mount points
                    # to update to the new method.
                    # Update the container right now.
                    setMountPoint(dispatcher.this(), o.getId(), mp_old[0])
                    status = 'Ok (updated)'
                else:
                    status = '** Something is in the way **'
            else:
                mp_path = getattr(mp, '_path', None)
                if mp_path != path:
                    status = '** Set to wrong path: %s **' % repr(mp_path)
                else:
                    status = 'Ok'
        res.append({
            'path': path, 'name': name, 'exists': exists,
            'status': status,
            })
    return res


def manage_addMounts(dispatcher, paths=(), create_mount_points=0,
                     REQUEST=None):
    """Adds MountedObjects at the requested paths.
    """
    count = 0
    app = dispatcher.getPhysicalRoot()
    for path in paths:
        mo = MountedObject(path)
        mo._create_mount_points = not not create_mount_points
        # Raise an error now if there is any problem.
        mo._test(app)
        blazer = SimpleTrailblazer(app)
        container = blazer.traverseOrConstruct(path, omit_final=1)
        mo._p_jar = container._p_jar
        loaded = mo.__of__(container)

        # Add a faux object to avoid generating manage_afterAdd() events
        # while appeasing OFS.ObjectManager._setObject(), then discreetly
        # replace the faux object with a MountedObject.
        faux = Folder()
        faux.id = mo.id
        faux.meta_type = loaded.meta_type
        container._setObject(faux.id, faux)
        del mo._create_mount_points
        container._setOb(faux.id, mo)
        setMountPoint(container, faux.id, mo)
        count += 1
    if REQUEST is not None:
        REQUEST['RESPONSE'].redirect(
            REQUEST['URL1'] + ('/manage_main?manage_tabs_message='
            'Added %d mount points.' % count))



=== Added File Zope/lib/python/Products/ZODBMountPoint/__init__.py ===
##############################################################################
#
# Copyright (c) 2002 Zope Corporation and Contributors.
# All Rights Reserved.
# 
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
# 
##############################################################################
"""ZODBMountPoint product.

$Id: __init__.py,v 1.1 2003/07/20 02:56:01 chrism Exp $
"""

def initialize(context):
    # Configure and load databases if not already done.
    import MountedObject
    context.registerClass(
        MountedObject.MountedObject,
        constructors=(MountedObject.manage_addMountsForm,
                      MountedObject.manage_getMountStatus,
                      MountedObject.manage_addMounts,),
        )