Source code for nti.site.hostpolicy

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Support for host policies.

This contains the main unique functionality for this package.

.. $Id$
"""

from __future__ import print_function, unicode_literals, absolute_import, division
__docformat__ = "restructuredtext en"

logger = __import__('logging').getLogger(__name__)

from six import string_types

from zope import lifecycleevent
from zope import component
from zope import interface

from zope.component.hooks import site as current_site
from zope.component.interfaces import ISite

from zope.interface import ro
from zope.interface.interfaces import IComponents
from zope.interface.interfaces import ComponentLookupError

from zope.traversing.interfaces import IEtcNamespace


from zope.site.folder import Folder
from zope.site.folder import rootFolder

from .folder import HostPolicyFolder
from .folder import HostPolicySiteManager
from .folder import HostSitesFolder
from .interfaces import IMainApplicationFolder
from .site import BTreeLocalSiteManager

text_type = str if bytes is not str else unicode

[docs]def synchronize_host_policies(): """ Called within a transaction with a site being the current application site, find any :mod:`z3c.baseregistry` components that should be persistent sites, and register them in the database. As a prerequisite, :func:`install_sites_folder` must have been done, and we must be in that site. """ # TODO: We will ultimately need to deal with removing and renaming # of these # Resolution order: The actual ISite __parent__ order is not # important, so we can keep them flat to mirror the GSM IComponents # registrations. What matters is the __bases__ of the site managers. # Now, if the global IComponents are themselves flat, then it doesn't matter; # however, if you have a hierarchy (and we do) then it matters critically, because # we need to pick up persistent utilities from these objects, as well as # the global components, in the right order. For example, if we have this # global hierarchy: # GSM # \ # S1 # \ # S2 # and in the database we have the nti.dataserver and root persistent site managers, # then when we create the persistent sites for S1 and S2 (PS1 and PS2) we want # the resolution order to be: # PS2 -> S2 -> PS1 -> S1 -> DS -> Root -> GSM # That is, we need to get the persistent components mixed in between the # global components. # Fortunately this is very easy to achieve. The code in zope.interface.ro handles # this. # We just need to ensure: # PS1.__bases__ = (S1, DS) # PS2.__bases__ = (S2, PS1) sites = component.getUtility(IEtcNamespace, name='hostsites') ds_folder = sites.__parent__ assert IMainApplicationFolder.providedBy(ds_folder) ds_site_manager = ds_folder.getSiteManager() # Ok, find everything that is globally registered global_sm = component.getGlobalSiteManager() all_global_named_utilities = list(global_sm.getUtilitiesFor(IComponents)) for name, comp in all_global_named_utilities: # The sites must be registered the same as their internal name assert name == comp.__name__ all_global_utilities = [x[1] for x in all_global_named_utilities] # Now, get the resolution order of each site; this is an easy way # to do a kind of topological sort. site_ros = [ro.ro(x) for x in all_global_utilities] # Next, start creating persistent sites in the database, walking from the top # of the resolution order (the end of the list) # towards the root; the first one we put in the DB gets the DS as its # base, otherwise it gets the previous one we put in. for site_ro in site_ros: site_ro = reversed(site_ro) secondary_comps = ds_site_manager for comps in site_ro: name = comps.__name__ logger.debug("Checking host policy for site %s", name) if name.endswith('base') or name.startswith('base'): # The GSM or the base global objects # TODO: better way to do this...marker interface? continue # pragma: no cover if name in sites: logger.debug("Host policy for %s already in place", name) # Ok, we've already put one in for this level. # We need to make it our next choice going forward secondary_comps = sites[name].getSiteManager() else: # Great, create the site logger.info("Installing site policy %s", name) site = HostPolicyFolder() # should fire object created event sites[name] = site site_policy = HostPolicySiteManager(site) site_policy.__bases__ = (comps, secondary_comps) # should fire INewLocalSite site.setSiteManager(site_policy) secondary_comps = site_policy
[docs]def install_sites_folder(server_folder): """ Given a :class:`~.IMainApplicationFolder` that has a site manager, install a host sites folder. The folder will be installed at "++etc++hostsites", and registered to provide :class:`IEtcNamespace` with the name "hostsites" so it can be found by traversal. .. seealso:: `zope.traversing.namespace.etc`. """ sites = HostSitesFolder() str(sites) # coverage repr(sites) # coverage server_folder['++etc++hostsites'] = sites lsm = server_folder.getSiteManager() lsm.registerUtility(sites, provided=IEtcNamespace, name='hostsites')
# synchronize_host_policies() class _StrDefault(object): def __init__(self, val, description): assert isinstance(val, text_type) self.value = val self.description = description def __str__(self): return self.value __unicode__ = __str__ def __repr__(self): return "<%r is the %s>" % ( self.value, self.description ) def __bool__(self): # In the future, we can turn this to false for things # we don't wish to install, such as the aliases. return True __nonzero__ = __bool__ __getstate__ = __reduce__ = None DEFAULT_ROOT_NAME = _StrDefault( u'Application', "default root folder") DEFAULT_ROOT_ALIAS = _StrDefault( u'nti.dataserver_root', "default alias of root folder") DEFAULT_MAIN_NAME = _StrDefault( u'dataserver2', "default main application folder") DEFAULT_MAIN_ALIAS = _StrDefault( u'nti.dataserver', "default alias of main application folder")
[docs]def install_main_application_and_sites(conn, root_name=DEFAULT_ROOT_NAME, root_alias=DEFAULT_ROOT_ALIAS, main_name=DEFAULT_MAIN_NAME, main_alias=DEFAULT_MAIN_ALIAS, main_factory=Folder, main_setup=None): """ Install the main application and site folder structure into ZODB. When this completes, the ZODB root object will have a :class:`.IRootFolder` object at *root_name* (and optionally at *root_alias*), created by :func:`zope.site.folder.rootFolder`. This will have a site manager. Note that this object does not have a ``__name__`` and will serve as the base (root, or "/") for object path traversal. The root folder in turn will have a :class:`~.IMainApplicationFolder` child named *main_name* (and optionally at *main_alias*). It will have a site manager, and in this site manager will be the "++etc++hostsites" object used to contain host site folders. The tree will look like this:: <Connection Root Dictionary> <ISite,IRootFolder>: root_name rootFolder() <ISite,IMainApplicationFolder>: main_name <main_factory> ++etc++hostsites <class 'nti.site.folder.HostSitesFolder'> main_name -> /root_name/main_name main_alias -> /root_name/main_name root_alias -> /root_name Using the default names, that would be:: <Connection Root Dictionary> <ISite,IRootFolder>: Application <ISite,IMainApplicationFolder>: dataserver2 ++etc++hostsites dataserver2 -> /Application/dataserver2 nti.dataserver -> /Application/dataserver2 nti.dataserver_root -> /Application .. caution:: Passing in duplicate names for any of the parameters may result in unexpected results. .. note:: The aliases are only installed if the *alias* parameters are true. In the future, the *alias* parameters will default to false. .. note:: The root folder .. versionchanged:: 2.1 The *root_alias* now points to the *root_name*. Previously it pointed to *main_name* (e.g., /Application/dataserver2). This made no sense because *main_alias* already pointed there. :param conn: The open ZODB connection. :keyword str root_name: The main name of the root folder. This generally should be left as "Application" as that's what many Zope 3 components expect. :keyword main_factory: The factory that will be used for the :class:`.IMainApplicationFolder`. If it produces an object that doesn't implement this interface, it will still be marked as doing so. :keyword callable main_setup: If given, a callable that will accept the main application folder object and perform further setup on it. This will be called *before* any lifecycle events are generated. This is a good time to install additional utilities, such as :class:`IIntId` utilities. After *main_setup* has been called, and the lifecycle events for the root folder and main folder have been generated, this calls :func:`install_sites_folder`. """ root_name = text_type(root_name) if root_name else None root_alias = text_type(root_alias) if root_alias else None main_name = text_type(main_name) if main_name else None main_alias = text_type(main_alias) if main_alias else None root = conn.root() # The root folder root_folder = rootFolder() # NOTE that the root_folder doesn't get a __name__! conn.add(root_folder) # Ensure we have a connection so we can become KeyRefs assert root_folder._p_jar is conn # The root is generally presumed to be an ISite, so make it so root_sm = BTreeLocalSiteManager(root_folder) # site is IRoot, so __base__ is the GSM assert root_sm.__parent__ is root_folder assert root_sm.__bases__ == (component.getGlobalSiteManager(),) conn.add(root_sm) # Ensure we have a connection so we can become KeyRefs assert root_sm._p_jar is conn root_folder.setSiteManager(root_sm) assert ISite.providedBy(root_folder) main_folder = main_factory() if not IMainApplicationFolder.providedBy(main_folder): interface.alsoProvides(main_folder, IMainApplicationFolder) conn.add(main_folder) root_folder[main_name] = main_folder assert main_folder.__parent__ is root_folder assert main_folder.__name__ == main_name assert root_folder[main_name] is main_folder lsm = BTreeLocalSiteManager(main_folder) lsm.__bases__ = (root_sm,) conn.add(lsm) assert lsm.__parent__ is main_folder assert lsm.__bases__ == (root_sm,), (lsm.__bases__, root_sm) main_folder.setSiteManager(lsm) assert ISite.providedBy(main_folder) with current_site(main_folder): current_site_man = component.getSiteManager() assert current_site_man is lsm, ("Component hooks must have been reset", current_site_man, lsm) # The name that many Zope components assume root[root_name] = root_folder if root_alias: root[root_alias] = root_folder root[main_folder.__name__] = main_folder if main_alias: root[main_alias] = main_folder if main_setup: main_setup(main_folder) # Important to include the parent and name in the event so that # it doesn't get interpreted as removal (ObjectAddedEvent extends # ObjectMovedEvent, and so does ObjectRemovedEvent; commonly one subscribes to # ObjectMovedEvent and then checks to see if there is a parent or not). lifecycleevent.added(root_folder, root, root_name) lifecycleevent.added(main_folder, root, main_folder.__name__) install_sites_folder(main_folder) return root_folder, main_folder
[docs]def get_all_host_sites(): """ The order in which sites are accessed is top-down breadth-first, that is, the shallowest to the deepest nested sites. This allows you to assume that your parent sites have already been updated. :returns: A list of sites :rtype: list """ sites = component.getUtility(IEtcNamespace, name='hostsites') sites = list(sites.values()) # The easyiest way to go top-down is to again use the resolution order; # we just have to watch out for duplicates and non-persistent components site_to_ro = {site: ro.ro(site.getSiteManager()) for site in sites} # This should be a plain, directed acyclic tree (single root) that is now # linearized. # Transform from the site manager back into the site object itself site_to_site_ro = {} for site, managers in site_to_ro.items(): site_to_site_ro[site] = [getattr(x, '__parent__', None) for x in managers] # Ok, now, go through the dictionary, walking from the top to the bottom, # one at a time, thus producing the correct order # (Because our datastructure looks like this: # site1: [site1, ds, base, GSM] # site2: [site2, site1, base, GSM] # site3: [site3, ds, base, GSM]) ordered = list() while site_to_site_ro: for site, managers in dict(site_to_site_ro).items(): if not managers: site_to_site_ro.pop(site) continue base_site = managers.pop() if base_site in sites and base_site not in ordered: # Ie., it's a real one we haven't seen before ordered.append(base_site) return ordered
[docs]def run_job_in_all_host_sites(func): """ While already operating inside of a transaction and the application environment, execute the callable given by ``func`` once for each persistent, registered host (see :func:`synchronize_host_policies`). The callable is run with that site current. This is typically used to make configuration changes/adjustments to utilities local within each site, while the appropriate event listeners for the site also fire. You are responsible for transaction management. :raises: Whatever the callable raises. :returns: A list of pairs `(site, result)` containing each site and the result of running the function in that site. :rtype: list """ logger.debug("Asked to run job %s in ALL sites", func) results = list() ordered = get_all_host_sites() for site in ordered: results.append(run_job_in_host_site(site, func)) return results
[docs]def get_host_site(site_name, safe=False): """ Find the persistent site named *site_name* and return it. :keyword bool safe: If True, silently ignore any errors. **DO NOT** use this param. Deprecated and dangerous. """ site = str(site_name) try: sites = component.getUtility(IEtcNamespace, name='hostsites') result = sites[site] return result except (ComponentLookupError, KeyError): if not safe: raise return None
get_site = get_host_site # BWC
[docs]def run_job_in_host_site(site, func): """ Helper function to run the *func* with the given site as the current site. :param site: Either the site object itself, or its unique name. """ site = get_host_site(site) if isinstance(site, string_types) else site logger.debug('Running job %s in site %s', func, site.__name__) with current_site(site): result = func() return result