Back to index

plone3  3.1.7
tool.py
Go to the documentation of this file.
00001 ##############################################################################
00002 #
00003 # Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
00004 #
00005 # This software is subject to the provisions of the Zope Public License,
00006 # Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
00007 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
00008 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
00009 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
00010 # FOR A PARTICULAR PURPOSE.
00011 #
00012 ##############################################################################
00013 """ Classes:  SetupTool
00014 
00015 $Id: tool.py 85170 2008-04-08 21:13:10Z wichert $
00016 """
00017 
00018 import os
00019 import time
00020 import logging
00021 from warnings import warn
00022 from cgi import escape
00023 
00024 from AccessControl import ClassSecurityInfo
00025 from Acquisition import aq_base
00026 from Globals import InitializeClass
00027 from OFS.Folder import Folder
00028 from OFS.Image import File
00029 from Products.PageTemplates.PageTemplateFile import PageTemplateFile
00030 from ZODB.POSException import ConflictError
00031 from zope.interface import implements
00032 from zope import event 
00033 
00034 from interfaces import BASE
00035 from interfaces import EXTENSION
00036 from interfaces import ISetupTool
00037 from interfaces import SKIPPED_FILES
00038 from permissions import ManagePortal
00039 from events import BeforeProfileImportEvent
00040 from events import ProfileImportedEvent
00041 from context import DirectoryImportContext
00042 from context import SnapshotImportContext
00043 from context import TarballExportContext
00044 from context import TarballImportContext
00045 from context import SnapshotExportContext
00046 from differ import ConfigDiff
00047 from registry import ImportStepRegistry
00048 from registry import ExportStepRegistry
00049 from registry import ToolsetRegistry
00050 from registry import _profile_registry
00051 from registry import _import_step_registry
00052 from registry import _export_step_registry
00053 
00054 from upgrade import listUpgradeSteps
00055 from upgrade import listProfilesWithUpgrades
00056 from upgrade import _upgrade_registry
00057 
00058 from utils import _getDottedName
00059 from utils import _resolveDottedName
00060 from utils import _wwwdir
00061 from utils import _computeTopologicalSort
00062 from utils import _getProductPath
00063 
00064 IMPORT_STEPS_XML = 'import_steps.xml'
00065 EXPORT_STEPS_XML = 'export_steps.xml'
00066 TOOLSET_XML = 'toolset.xml'
00067 
00068 def exportStepRegistries(context):
00069 
00070     """ Built-in handler for exporting import / export step registries.
00071     """
00072     setup_tool = context.getSetupTool()
00073     logger = context.getLogger('registries')
00074 
00075     import_step_registry = setup_tool.getImportStepRegistry()
00076     if len(import_step_registry.listSteps()) > 0:
00077         import_steps_xml = import_step_registry.generateXML()
00078         context.writeDataFile('import_steps.xml', import_steps_xml, 'text/xml')
00079         logger.info('Local import steps exported.')
00080     else:
00081         logger.debug('No local import steps.')
00082 
00083     export_step_registry = setup_tool.getExportStepRegistry()
00084     if len(export_step_registry.listSteps()) > 0:
00085         export_steps_xml = export_step_registry.generateXML()
00086         context.writeDataFile('export_steps.xml', export_steps_xml, 'text/xml')
00087         logger.info('Local export steps exported.')
00088     else:
00089         logger.debug('No local export steps.')
00090 
00091 def importToolset(context):
00092 
00093     """ Import required / forbidden tools from XML file.
00094     """
00095     site = context.getSite()
00096     encoding = context.getEncoding()
00097     logger = context.getLogger('toolset')
00098 
00099     xml = context.readDataFile(TOOLSET_XML)
00100     if xml is None:
00101         logger.debug('Nothing to import.')
00102         return
00103 
00104     setup_tool = context.getSetupTool()
00105     toolset = setup_tool.getToolsetRegistry()
00106 
00107     toolset.parseXML(xml, encoding)
00108 
00109     existing_ids = site.objectIds()
00110     existing_values = site.objectValues()
00111 
00112     for tool_id in toolset.listForbiddenTools():
00113 
00114         if tool_id in existing_ids:
00115             site._delObject(tool_id)
00116 
00117     for info in toolset.listRequiredToolInfo():
00118 
00119         tool_id = str(info['id'])
00120         tool_class = _resolveDottedName(info['class'])
00121 
00122         existing = getattr(aq_base(site), tool_id, None)
00123         # Don't even initialize the tool again, if it already exists.
00124         if existing is None:
00125             try:
00126                 new_tool = tool_class()
00127             except TypeError:
00128                 new_tool = tool_class(tool_id)
00129             else:
00130                 try:
00131                     new_tool._setId(tool_id)
00132                 except (ConflictError, KeyboardInterrupt):
00133                     raise
00134                 except:
00135                     # XXX: ImmutableId raises result of calling MessageDialog
00136                     pass
00137 
00138             site._setObject(tool_id, new_tool)
00139         else:
00140             unwrapped = aq_base(existing)
00141             if not isinstance(unwrapped, tool_class):
00142                 site._delObject(tool_id)
00143                 site._setObject(tool_id, tool_class())
00144 
00145     logger.info('Toolset imported.')
00146 
00147 def exportToolset(context):
00148 
00149     """ Export required / forbidden tools to XML file.
00150     """
00151     setup_tool = context.getSetupTool()
00152     toolset = setup_tool.getToolsetRegistry()
00153     logger = context.getLogger('toolset')
00154 
00155     xml = toolset.generateXML()
00156     context.writeDataFile(TOOLSET_XML, xml, 'text/xml')
00157 
00158     logger.info('Toolset exported.')
00159 
00160 
00161 class SetupTool(Folder):
00162 
00163     """ Profile-based site configuration manager.
00164     """
00165 
00166     implements(ISetupTool)
00167 
00168     meta_type = 'Generic Setup Tool'
00169 
00170     _baseline_context_id = ''
00171     # BBB _import_context_id is a vestige of a stateful import context
00172     _import_context_id = ''
00173 
00174     _profile_upgrade_versions = {}
00175 
00176     security = ClassSecurityInfo()
00177 
00178     def __init__(self, id):
00179         self.id = str(id)
00180         self._import_registry = ImportStepRegistry()
00181         self._export_registry = ExportStepRegistry()
00182         self._toolset_registry = ToolsetRegistry()
00183 
00184     #
00185     #   ISetupTool API
00186     #
00187     security.declareProtected(ManagePortal, 'getEncoding')
00188     def getEncoding(self):
00189 
00190         """ See ISetupTool.
00191         """
00192         return 'utf-8'
00193 
00194     security.declareProtected(ManagePortal, 'getImportContextID')
00195     def getImportContextID(self):
00196 
00197         """ See ISetupTool.
00198         """
00199         warn('getImportContextId, and the very concept of a stateful '
00200              'active import context, is deprecated.  You can find the '
00201              'base profile that was applied using getBaselineContextID.',
00202              DeprecationWarning, stacklevel=2)
00203         return self._import_context_id
00204 
00205     security.declareProtected(ManagePortal, 'getBaselineContextID')
00206     def getBaselineContextID(self):
00207 
00208         """ See ISetupTool.
00209         """
00210         return self._baseline_context_id
00211 
00212     security.declareProtected(ManagePortal, 'setImportContext')
00213     def setImportContext(self, context_id, encoding=None):
00214         """ See ISetupTool.
00215         """
00216         warn('setImportContext is deprecated.  Use setBaselineContext to '
00217              'specify the baseline context, and/or runImportStepFromProfile '
00218              'to run the steps from a specific import context.',
00219              DeprecationWarning, stacklevel=2)
00220         self._import_context_id = context_id
00221 
00222         context_type = BASE  # snapshots are always baseline contexts
00223         if context_id.startswith('profile-'):
00224             profile_info = _profile_registry.getProfileInfo(context_id[8:])
00225             context_type = profile_info['type']
00226 
00227         if context_type == BASE:
00228             self.setBaselineContext(context_id, encoding)
00229 
00230     security.declareProtected(ManagePortal, 'setBaselineContext')
00231     def setBaselineContext(self, context_id, encoding=None):
00232         """ See ISetupTool.
00233         """
00234         self._baseline_context_id = context_id
00235         self.applyContextById(context_id, encoding)
00236 
00237 
00238     security.declareProtected(ManagePortal, 'applyContextById')
00239     def applyContextById(self, context_id, encoding=None):
00240         context = self._getImportContext(context_id)
00241         self.applyContext(context, encoding)
00242 
00243 
00244     security.declareProtected(ManagePortal, 'applyContext')
00245     def applyContext(self, context, encoding=None):
00246         self._updateImportStepsRegistry(context, encoding)
00247         self._updateExportStepsRegistry(context, encoding)
00248 
00249     security.declareProtected(ManagePortal, 'getImportStepRegistry')
00250     def getImportStepRegistry(self):
00251 
00252         """ See ISetupTool.
00253         """
00254         return self._import_registry
00255 
00256     security.declareProtected(ManagePortal, 'getExportStepRegistry')
00257     def getExportStepRegistry(self):
00258 
00259         """ See ISetupTool.
00260         """
00261         return self._export_registry
00262 
00263     security.declareProtected(ManagePortal, 'getExportStep')
00264     def getExportStep(self, step, default=None):
00265         """Simple wrapper to query both the global and local step registry."""
00266         res=_export_step_registry.getStep(step, default)
00267         if res is not default:
00268             return res
00269         return self._export_registry.getStep(step, default)
00270 
00271     security.declareProtected(ManagePortal, 'listExportSteps')
00272     def listExportSteps(self):
00273         steps = _export_step_registry.listSteps() + \
00274                 self._export_registry.listSteps()
00275         return tuple(set(steps))
00276 
00277     security.declareProtected(ManagePortal, 'getImportStep')
00278     def getImportStep(self, step, default=None):
00279         """Simple wrapper to query both the global and local step registry."""
00280         res=_import_step_registry.getStep(step, default)
00281         if res is not default:
00282             return res
00283         return self._import_registry.getStep(step, default)
00284 
00285     security.declareProtected(ManagePortal, 'getSortedImportSteps')
00286     def getSortedImportSteps(self):
00287         steps = _import_step_registry.listSteps() + \
00288                 self._import_registry.listSteps()
00289         step_infos = [ self.getImportStepMetadata(step)
00290                        for step in set(steps) ]
00291         return tuple(_computeTopologicalSort(step_infos))
00292 
00293     security.declareProtected(ManagePortal, 'getImportStepMetadata')
00294     def getImportStepMetadata(self, step, default=None):
00295         """Simple wrapper to query both the global and local step registry."""
00296         res=self._import_registry.getStepMetadata(step, default)
00297         if res is not default:
00298             return res
00299         return _import_step_registry.getStepMetadata(step, default)
00300 
00301     security.declareProtected(ManagePortal, 'getExportStepMetadata')
00302     def getExportStepMetadata(self, step, default=None):
00303         """Simple wrapper to query both the global and local step registry."""
00304         res=self._export_registry.getStepMetadata(step, default)
00305         if res is not default:
00306             return res
00307         return _export_step_registry.getStepMetadata(step, default)
00308 
00309     security.declareProtected(ManagePortal, 'getToolsetRegistry')
00310     def getToolsetRegistry(self):
00311 
00312         """ See ISetupTool.
00313         """
00314         return self._toolset_registry
00315 
00316     security.declareProtected(ManagePortal, 'runImportStepFromProfile')
00317     def runImportStepFromProfile(self, profile_id, step_id,
00318                                  run_dependencies=True, purge_old=None):
00319         """ See ISetupTool.
00320         """
00321         old_context = self._import_context_id
00322         context = self._getImportContext(profile_id, purge_old)
00323 
00324         self.applyContext(context)
00325 
00326         info = self.getImportStepMetadata(step_id)
00327 
00328         if info is None:
00329             self._import_context_id = old_context
00330             raise ValueError, 'No such import step: %s' % step_id
00331 
00332         dependencies = info.get('dependencies', ())
00333 
00334         messages = {}
00335         steps = []
00336 
00337         if run_dependencies:
00338             for dependency in dependencies:
00339                 if dependency not in steps:
00340                     steps.append(dependency)
00341         steps.append (step_id)
00342 
00343         full_import=(set(steps)==set(self.getSortedImportSteps()))
00344         event.notify(BeforeProfileImportEvent(self, profile_id, steps, full_import))
00345 
00346         for step in steps:
00347             message = self._doRunImportStep(step, context)
00348             messages[step] = message or ''
00349 
00350         message_list = filter(None, [message])
00351         message_list.extend( ['%s: %s' % x[1:] for x in context.listNotes()] )
00352         messages[step_id] = '\n'.join(message_list)
00353 
00354         self._import_context_id = old_context
00355 
00356         event.notify(ProfileImportedEvent(self, profile_id, steps, full_import))
00357 
00358         return { 'steps' : steps, 'messages' : messages }
00359 
00360     security.declareProtected(ManagePortal, 'runImportStep')
00361     def runImportStep(self, step_id, run_dependencies=True, purge_old=None):
00362 
00363         """ See ISetupTool.
00364         """
00365         warn('The runImportStep method is deprecated.  Please use '
00366              'runImportStepFromProfile instead.',
00367              DeprecationWarning, stacklevel=2)
00368         return self.runImportStepFromProfile(self._import_context_id,
00369                                              step_id,
00370                                              run_dependencies,
00371                                              purge_old,
00372                                              )
00373 
00374     security.declareProtected(ManagePortal, 'runAllImportStepsFromProfile')
00375     def runAllImportStepsFromProfile(self,
00376                                      profile_id,
00377                                      purge_old=None,
00378                                      ignore_dependencies=False,
00379                                      archive=None):
00380 
00381         """ See ISetupTool.
00382         """
00383         __traceback_info__ = profile_id
00384 
00385         old_context = self._import_context_id
00386 
00387         result = self._runImportStepsFromContext(purge_old=purge_old,
00388                                                  profile_id=profile_id,
00389                                                  archive=archive,
00390                                                  ignore_dependencies=ignore_dependencies)
00391         if profile_id is None:
00392             prefix = 'import-all-from-tar'
00393         else:
00394             prefix = 'import-all-%s' % profile_id.replace(':', '_')
00395         name = self._mangleTimestampName(prefix, 'log')
00396         self._createReport(name, result['steps'], result['messages'])
00397 
00398         self._import_context_id = old_context
00399 
00400         return result
00401 
00402     security.declareProtected(ManagePortal, 'runAllImportSteps')
00403     def runAllImportSteps(self, purge_old=None):
00404 
00405         """ See ISetupTool.
00406         """
00407         warn('The runAllImportSteps method is deprecated.  Please use '
00408              'runAllImportStepsFromProfile instead.',
00409              DeprecationWarning, stacklevel=2)
00410         context_id = self._import_context_id
00411         return self.runAllImportStepsFromProfile(self._import_context_id,
00412                                                  purge_old)
00413 
00414     security.declareProtected(ManagePortal, 'runExportStep')
00415     def runExportStep(self, step_id):
00416 
00417         """ See ISetupTool.
00418         """
00419         return self._doRunExportSteps([step_id])
00420 
00421     security.declareProtected(ManagePortal, 'runAllExportSteps')
00422     def runAllExportSteps(self):
00423 
00424         """ See ISetupTool.
00425         """
00426         return self._doRunExportSteps(self.listExportSteps())
00427 
00428     security.declareProtected(ManagePortal, 'createSnapshot')
00429     def createSnapshot(self, snapshot_id):
00430 
00431         """ See ISetupTool.
00432         """
00433         context = SnapshotExportContext(self, snapshot_id)
00434         messages = {}
00435         steps = self.listExportSteps()
00436 
00437         for step_id in steps:
00438 
00439             handler = self.getExportStep(step_id)
00440 
00441             if handler is None:
00442                 logger = logging.getLogger('GenericSetup')
00443                 logger.error('Step %s has an invalid handler' % step_id)
00444                 continue
00445 
00446             messages[step_id] = handler(context)
00447 
00448 
00449         return { 'steps' : steps
00450                , 'messages' : messages
00451                , 'url' : context.getSnapshotURL()
00452                , 'snapshot' : context.getSnapshotFolder()
00453                }
00454 
00455     security.declareProtected(ManagePortal, 'compareConfigurations')
00456     def compareConfigurations(self,
00457                               lhs_context,
00458                               rhs_context,
00459                               missing_as_empty=False,
00460                               ignore_blanks=False,
00461                               skip=SKIPPED_FILES,
00462                              ):
00463         """ See ISetupTool.
00464         """
00465         differ = ConfigDiff(lhs_context,
00466                             rhs_context,
00467                             missing_as_empty,
00468                             ignore_blanks,
00469                             skip,
00470                            )
00471 
00472         return differ.compare()
00473 
00474     security.declareProtected(ManagePortal, 'markupComparison')
00475     def markupComparison(self, lines):
00476 
00477         """ See ISetupTool.
00478         """
00479         result = []
00480 
00481         for line in lines.splitlines():
00482 
00483             if line.startswith('** '):
00484 
00485                 if line.find('File') > -1:
00486                     if line.find('replaced') > -1:
00487                         result.append(('file-to-dir', line))
00488                     elif line.find('added') > -1:
00489                         result.append(('file-added', line))
00490                     else:
00491                         result.append(('file-removed', line))
00492                 else:
00493                     if line.find('replaced') > -1:
00494                         result.append(('dir-to-file', line))
00495                     elif line.find('added') > -1:
00496                         result.append(('dir-added', line))
00497                     else:
00498                         result.append(('dir-removed', line))
00499 
00500             elif line.startswith('@@'):
00501                 result.append(('diff-range', line))
00502 
00503             elif line.startswith(' '):
00504                 result.append(('diff-context', line))
00505 
00506             elif line.startswith('+'):
00507                 result.append(('diff-added', line))
00508 
00509             elif line.startswith('-'):
00510                 result.append(('diff-removed', line))
00511 
00512             elif line == '\ No newline at end of file':
00513                 result.append(('diff-context', line))
00514 
00515             else:
00516                 result.append(('diff-header', line))
00517 
00518         return '<pre>\n%s\n</pre>' % (
00519             '\n'.join([('<span class="%s">%s</span>' % (cl, escape(l)))
00520                                   for cl, l in result]))
00521 
00522     #
00523     #   ZMI
00524     #
00525     manage_options = (Folder.manage_options[:1]
00526                     + ({'label' : 'Profiles',
00527                         'action' : 'manage_tool'
00528                        },
00529                        {'label' : 'Import',
00530                         'action' : 'manage_importSteps'
00531                        },
00532                        {'label' : 'Export',
00533                         'action' : 'manage_exportSteps'
00534                        },
00535                        {'label' : 'Upgrades',
00536                         'action' : 'manage_upgrades'
00537                         },
00538                        {'label' : 'Snapshots',
00539                         'action' : 'manage_snapshots'
00540                        },
00541                        {'label' : 'Comparison',
00542                         'action' : 'manage_showDiff'
00543                        },
00544                        {'label' : 'Manage',
00545                         'action' : 'manage_stepRegistry'
00546                        },
00547                       )
00548                     + Folder.manage_options[3:] # skip "View", "Properties"
00549                      )
00550 
00551     security.declareProtected(ManagePortal, 'manage_tool')
00552     manage_tool = PageTemplateFile('sutProperties', _wwwdir)
00553 
00554     security.declareProtected(ManagePortal, 'manage_updateToolProperties')
00555     def manage_updateToolProperties(self, context_id, RESPONSE):
00556         """ Update the tool's settings.
00557         """
00558         self.setBaselineContext(context_id)
00559 
00560         RESPONSE.redirect('%s/manage_tool?manage_tabs_message=%s'
00561                          % (self.absolute_url(), 'Properties+updated.'))
00562 
00563     security.declareProtected(ManagePortal, 'manage_importSteps')
00564     manage_importSteps = PageTemplateFile('sutImportSteps', _wwwdir)
00565 
00566     security.declareProtected(ManagePortal, 'manage_importSelectedSteps')
00567     def manage_importSelectedSteps(self, ids, run_dependencies, context_id=None):
00568         """ Import the steps selected by the user.
00569         """
00570         messages = {}
00571         if not ids:
00572             summary = 'No steps selected.'
00573 
00574         else:
00575             if context_id is None:
00576                 context_id = self.getBaselineContextID()
00577             steps_run = []
00578             for step_id in ids:
00579                 result = self.runImportStepFromProfile(context_id,
00580                                                        step_id,
00581                                                        run_dependencies)
00582                 steps_run.extend(result['steps'])
00583                 messages.update(result['messages'])
00584 
00585             summary = 'Steps run: %s' % ', '.join(steps_run)
00586 
00587             name = self._mangleTimestampName('import-selected', 'log')
00588             self._createReport(name, result['steps'], result['messages'])
00589 
00590         return self.manage_importSteps(manage_tabs_message=summary,
00591                                        messages=messages)
00592 
00593     security.declareProtected(ManagePortal, 'manage_importSelectedSteps')
00594     def manage_importAllSteps(self, context_id=None):
00595 
00596         """ Import all steps.
00597         """
00598         if context_id is None:
00599             context_id = self.getBaselineContextID()
00600         result = self.runAllImportStepsFromProfile(context_id, purge_old=None)
00601 
00602         steps_run = 'Steps run: %s' % ', '.join(result['steps'])
00603 
00604         return self.manage_importSteps(manage_tabs_message=steps_run,
00605                                        messages=result['messages'])
00606 
00607     security.declareProtected(ManagePortal, 'manage_importExtensions')
00608     def manage_importExtensions(self, RESPONSE, profile_ids=()):
00609 
00610         """ Import all steps for the selected extension profiles.
00611         """
00612         detail = {}
00613         if len(profile_ids) == 0:
00614             message = 'Please select one or more extension profiles.'
00615             RESPONSE.redirect('%s/manage_tool?manage_tabs_message=%s'
00616                                   % (self.absolute_url(), message))
00617         else:
00618             message = 'Imported profiles: %s' % ', '.join(profile_ids)
00619         
00620             for profile_id in profile_ids:
00621 
00622                 result = self.runAllImportStepsFromProfile(profile_id)
00623 
00624                 for k, v in result['messages'].items():
00625                     detail['%s:%s' % (profile_id, k)] = v
00626 
00627             return self.manage_importSteps(manage_tabs_message=message,
00628                                         messages=detail)
00629 
00630     security.declareProtected(ManagePortal, 'manage_importTarball')
00631     def manage_importTarball(self, tarball):
00632         """ Import steps from the uploaded tarball.
00633         """
00634         if getattr(tarball, 'read', None) is not None:
00635             tarball = tarball.read()
00636 
00637         result = self.runAllImportStepsFromProfile(None, True, archive=tarball)
00638 
00639         steps_run = 'Steps run: %s' % ', '.join(result['steps'])
00640 
00641         return self.manage_importSteps(manage_tabs_message=steps_run,
00642                                        messages=result['messages'])
00643 
00644     security.declareProtected(ManagePortal, 'manage_exportSteps')
00645     manage_exportSteps = PageTemplateFile('sutExportSteps', _wwwdir)
00646 
00647     security.declareProtected(ManagePortal, 'manage_exportSelectedSteps')
00648     def manage_exportSelectedSteps(self, ids, RESPONSE):
00649 
00650         """ Export the steps selected by the user.
00651         """
00652         if not ids:
00653             RESPONSE.redirect('%s/manage_exportSteps?manage_tabs_message=%s'
00654                              % (self.absolute_url(), 'No+steps+selected.'))
00655 
00656         result = self._doRunExportSteps(ids)
00657         RESPONSE.setHeader('Content-type', 'application/x-gzip')
00658         RESPONSE.setHeader('Content-disposition',
00659                            'attachment; filename=%s' % result['filename'])
00660         return result['tarball']
00661 
00662     security.declareProtected(ManagePortal, 'manage_exportAllSteps')
00663     def manage_exportAllSteps(self, RESPONSE):
00664 
00665         """ Export all steps.
00666         """
00667         result = self.runAllExportSteps()
00668         RESPONSE.setHeader('Content-type', 'application/x-gzip')
00669         RESPONSE.setHeader('Content-disposition',
00670                            'attachment; filename=%s' % result['filename'])
00671         return result['tarball']
00672 
00673     security.declareProtected(ManagePortal, 'manage_upgrades')
00674     manage_upgrades = PageTemplateFile('setup_upgrades', _wwwdir)
00675 
00676     security.declareProtected(ManagePortal, 'upgradeStepMacro')
00677     upgradeStepMacro = PageTemplateFile('upgradeStep', _wwwdir)
00678 
00679     security.declareProtected(ManagePortal, 'manage_snapshots')
00680     manage_snapshots = PageTemplateFile('sutSnapshots', _wwwdir)
00681 
00682     security.declareProtected(ManagePortal, 'listSnapshotInfo')
00683     def listSnapshotInfo(self):
00684 
00685         """ Return a list of mappings describing available snapshots.
00686 
00687         o Keys include:
00688 
00689           'id' -- snapshot ID
00690 
00691           'title' -- snapshot title or ID
00692 
00693           'url' -- URL of the snapshot folder
00694         """
00695         result = []
00696         snapshots = self._getOb('snapshots', None)
00697 
00698         if snapshots:
00699 
00700             for id, folder in snapshots.objectItems('Folder'):
00701 
00702                 result.append({ 'id' : id
00703                                , 'title' : folder.title_or_id()
00704                                , 'url' : folder.absolute_url()
00705                                })
00706         return result
00707 
00708     security.declareProtected(ManagePortal, 'listProfileInfo')
00709     def listProfileInfo(self):
00710 
00711         """ Return a list of mappings describing registered profiles.
00712         Base profile is listed first, extensions are sorted.
00713 
00714         o Keys include:
00715 
00716           'id' -- profile ID
00717 
00718           'title' -- profile title or ID
00719 
00720           'description' -- description of the profile
00721 
00722           'path' -- path to the profile within its product
00723 
00724           'product' -- name of the registering product
00725         """
00726         base = []
00727         ext = []
00728         for info in _profile_registry.listProfileInfo():
00729             if info.get('type', BASE) == BASE:
00730                 base.append(info)
00731             else:
00732                 ext.append(info)
00733         ext.sort(lambda x, y: cmp(x['id'], y['id']))
00734         return base + ext
00735 
00736     security.declareProtected(ManagePortal, 'listContextInfos')
00737     def listContextInfos(self):
00738 
00739         """ List registered profiles and snapshots.
00740         """
00741         def readableType(x):
00742             if x is BASE:
00743                 return 'base'
00744             elif x is EXTENSION:
00745                 return 'extension'
00746             return 'unknown'
00747 
00748         s_infos = [{'id': 'snapshot-%s' % info['id'],
00749                      'title': info['title'],
00750                      'type': 'snapshot',
00751                    }
00752                     for info in self.listSnapshotInfo()]
00753         p_infos = [{'id': 'profile-%s' % info['id'],
00754                     'title': info['title'],
00755                     'type': readableType(info['type']),
00756                    }
00757                    for info in self.listProfileInfo()]
00758 
00759         return tuple(s_infos + p_infos)
00760 
00761     security.declareProtected(ManagePortal, 'getProfileImportDate')
00762     def getProfileImportDate(self, profile_id):
00763         """ See ISetupTool.
00764         """
00765         prefix = ('import-all-%s-' % profile_id).replace(':', '_')
00766         candidates = [x for x in self.objectIds('File')
00767                         if x[:-18]==prefix and x.endswith('.log')]
00768         if len(candidates) == 0:
00769             return None
00770         candidates.sort()
00771         last = candidates[-1]
00772         stamp = last[-18:-4]
00773         return '%s-%s-%sT%s:%s:%sZ' % (stamp[0:4],
00774                                        stamp[4:6],
00775                                        stamp[6:8],
00776                                        stamp[8:10],
00777                                        stamp[10:12],
00778                                        stamp[12:14],
00779                                       )
00780 
00781     security.declareProtected(ManagePortal, 'manage_createSnapshot')
00782     def manage_createSnapshot(self, RESPONSE, snapshot_id=None):
00783 
00784         """ Create a snapshot with the given ID.
00785 
00786         o If no ID is passed, generate one.
00787         """
00788         if snapshot_id is None:
00789             snapshot_id = self._mangleTimestampName('snapshot')
00790 
00791         self.createSnapshot(snapshot_id)
00792 
00793         RESPONSE.redirect('%s/manage_snapshots?manage_tabs_message=%s'
00794                          % (self.absolute_url(), 'Snapshot+created.'))
00795         return ""
00796 
00797     security.declareProtected(ManagePortal, 'manage_showDiff')
00798     manage_showDiff = PageTemplateFile('sutCompare', _wwwdir)
00799 
00800     def manage_downloadDiff(self,
00801                             lhs,
00802                             rhs,
00803                             missing_as_empty,
00804                             ignore_blanks,
00805                             RESPONSE,
00806                            ):
00807         """ Crack request vars and call compareConfigurations.
00808 
00809         o Return the result as a 'text/plain' stream, suitable for framing.
00810         """
00811         comparison = self.manage_compareConfigurations(lhs,
00812                                                        rhs,
00813                                                        missing_as_empty,
00814                                                        ignore_blanks,
00815                                                       )
00816         RESPONSE.setHeader('Content-Type', 'text/plain')
00817         return _PLAINTEXT_DIFF_HEADER % (lhs, rhs, comparison)
00818 
00819     security.declareProtected(ManagePortal, 'manage_compareConfigurations')
00820     def manage_compareConfigurations(self,
00821                                      lhs,
00822                                      rhs,
00823                                      missing_as_empty,
00824                                      ignore_blanks,
00825                                     ):
00826         """ Crack request vars and call compareConfigurations.
00827         """
00828         lhs_context = self._getImportContext(lhs)
00829         rhs_context = self._getImportContext(rhs)
00830 
00831         return self.compareConfigurations(lhs_context,
00832                                           rhs_context,
00833                                           missing_as_empty,
00834                                           ignore_blanks,
00835                                          )
00836 
00837     security.declareProtected(ManagePortal, 'manage_stepRegistry')
00838     manage_stepRegistry = PageTemplateFile('sutManage', _wwwdir)
00839 
00840     security.declareProtected(ManagePortal, 'manage_deleteImportSteps')
00841     def manage_deleteImportSteps(self, ids, request=None):
00842         if request is None:
00843             request = self.REQUEST
00844         for id in ids:
00845             self._import_registry.unregisterStep(id)
00846         self._p_changed=True
00847         url = self.absolute_url()
00848         request.RESPONSE.redirect("%s/manage_stepRegistry" % url)
00849 
00850     security.declareProtected(ManagePortal, 'manage_deleteExportSteps')
00851     def manage_deleteExportSteps(self, ids, request=None):
00852         if request is None:
00853             request = self.REQUEST
00854         for id in ids:
00855             self._export_registry.unregisterStep(id)
00856         self._p_changed=True
00857         url = self.absolute_url()
00858         request.RESPONSE.redirect("%s/manage_stepRegistry" % url)
00859 
00860     #
00861     # Upgrades management
00862     #
00863     security.declareProtected(ManagePortal, 'getLastVersionForProfile')
00864     def getLastVersionForProfile(self, profile_id):
00865         """Return the last upgraded version for the specified profile.
00866         """
00867         version = self._profile_upgrade_versions.get(profile_id, 'unknown')
00868         return version
00869 
00870     security.declareProtected(ManagePortal, 'setLastVersionForProfile')
00871     def setLastVersionForProfile(self, profile_id, version):
00872         """Set the last upgraded version for the specified profile.
00873         """
00874         if isinstance(version, basestring):
00875             version = tuple(version.split('.'))
00876         prof_versions = self._profile_upgrade_versions.copy()
00877         prof_versions[profile_id] = version
00878         self._profile_upgrade_versions = prof_versions
00879 
00880     security.declareProtected(ManagePortal, 'getVersionForProfile')
00881     def getVersionForProfile(self, profile_id):
00882         """Return the registered filesystem version for the specified
00883         profile.
00884         """
00885         return self.getProfileInfo( profile_id ).get('version', 'unknown')
00886 
00887     security.declareProtected(ManagePortal, 'profileExists')
00888     def profileExists(self, profile_id):
00889         """Check if a profile exists."""
00890         try:
00891             self.getProfileInfo( profile_id )
00892         except KeyError:
00893             return False
00894         else:
00895             return True
00896 
00897     security.declareProtected(ManagePortal, "getProfileInfo")
00898     def getProfileInfo(self, profile_id):
00899         if profile_id.startswith("profile-"):
00900             profile_id = profile_id[len('profile-'):]
00901         elif profile_id.startswith("snapshot-"):
00902             profile_id = profile_id[len('snapshot-'):]
00903         return _profile_registry.getProfileInfo(profile_id)
00904 
00905     security.declareProtected(ManagePortal, 'getDependenciesForProfile')
00906     def getDependenciesForProfile(self, profile_id):
00907         if profile_id.startswith("snapshot-"):
00908             return ()
00909 
00910         if not self.profileExists( profile_id ):
00911             raise KeyError, profile_id
00912         try:
00913             return self.getProfileInfo( profile_id ).get('dependencies', ())
00914         except KeyError:
00915             return ()
00916 
00917 
00918     security.declareProtected(ManagePortal, 'listProfilesWithUpgrades')
00919     def listProfilesWithUpgrades(self):
00920         return listProfilesWithUpgrades()
00921 
00922     security.declarePrivate('_massageUpgradeInfo')
00923     def _massageUpgradeInfo(self, info):
00924         """Add a couple of data points to the upgrade info dictionary.
00925         """
00926         info = info.copy()
00927         info['haspath'] = info['source'] and info['dest']
00928         info['ssource'] = '.'.join(info['source'] or ('all',))
00929         info['sdest'] = '.'.join(info['dest'] or ('all',))
00930         return info
00931 
00932     security.declareProtected(ManagePortal, 'listUpgrades')
00933     def listUpgrades(self, profile_id, show_old=False):
00934         """Get the list of available upgrades.
00935         """
00936         if show_old:
00937             source = None
00938         else:
00939             source = self.getLastVersionForProfile(profile_id)
00940         upgrades = listUpgradeSteps(self, profile_id, source)
00941         res = []
00942         for info in upgrades:
00943             if type(info) == list:
00944                 subset = []
00945                 for subinfo in info:
00946                     subset.append(self._massageUpgradeInfo(subinfo))
00947                 res.append(subset)
00948             else:
00949                 res.append(self._massageUpgradeInfo(info))
00950         return res
00951 
00952     security.declareProtected(ManagePortal, 'manage_doUpgrades')
00953     def manage_doUpgrades(self, request=None):
00954         """Perform all selected upgrade steps.
00955         """
00956         if request is None:
00957             request = self.REQUEST
00958         logger = logging.getLogger('GenericSetup')
00959         steps_to_run = request.form.get('upgrades', [])
00960         profile_id = request.get('profile_id', '')
00961         for step_id in steps_to_run:
00962             step = _upgrade_registry.getUpgradeStep(profile_id, step_id)
00963             if step is not None:
00964                 step.doStep(self)
00965                 msg = "Ran upgrade step %s for profile %s" % (step.title,
00966                                                               profile_id)
00967                 logger.log(logging.INFO, msg)
00968 
00969         # XXX should be a bit smarter about deciding when to up the
00970         #     profile version
00971         profile_info = _profile_registry.getProfileInfo(profile_id)
00972         version = profile_info.get('version', None)
00973         if version is not None:
00974             self.setLastVersionForProfile(profile_id, version)
00975 
00976         url = self.absolute_url()
00977         request.RESPONSE.redirect("%s/manage_upgrades?saved=%s" % (url, profile_id))
00978 
00979     #
00980     #   Helper methods
00981     #
00982     security.declarePrivate('_getImportContext')
00983     def _getImportContext(self, context_id, should_purge=None, archive=None):
00984 
00985         """ Crack ID and generate appropriate import context.
00986         """
00987         encoding = self.getEncoding()
00988 
00989         if context_id is not None:
00990             if context_id.startswith('profile-'):
00991                 context_id = context_id[len('profile-'):]
00992                 info = _profile_registry.getProfileInfo(context_id)
00993 
00994                 if info.get('product'):
00995                     path = os.path.join(_getProductPath(info['product'])
00996                                        , info['path'])
00997                 else:
00998                     path = info['path']
00999                 if should_purge is None:
01000                     should_purge = (info.get('type') != EXTENSION)
01001                 return DirectoryImportContext(self, path, should_purge, encoding)
01002 
01003             elif context_id.startswith('snapshot-'):
01004                 context_id = context_id[len('snapshot-'):]
01005                 if should_purge is None:
01006                     should_purge = True
01007                 return SnapshotImportContext(self, context_id, should_purge, encoding)
01008 
01009         if archive is not None:
01010             return TarballImportContext(tool=self,
01011                                        archive_bits=archive,
01012                                        encoding='UTF8',
01013                                        should_purge=should_purge,
01014                                       )
01015 
01016         raise KeyError, 'Unknown context "%s"' % context_id
01017 
01018     security.declarePrivate('_updateImportStepsRegistry')
01019     def _updateImportStepsRegistry(self, context, encoding):
01020 
01021         """ Update our import steps registry from our profile.
01022         """
01023         if context is None:
01024             context = self._getImportContext(self._import_context_id)
01025         xml = context.readDataFile(IMPORT_STEPS_XML)
01026         if xml is None:
01027             return
01028 
01029         info_list = self._import_registry.parseXML(xml, encoding)
01030 
01031         for step_info in info_list:
01032 
01033             id = step_info['id']
01034             version = step_info['version']
01035             handler = step_info['handler']
01036             dependencies = tuple(step_info.get('dependencies', ()))
01037             title = step_info.get('title', id)
01038             description = ''.join(step_info.get('description', []))
01039 
01040             self._import_registry.registerStep(id=id,
01041                                                version=version,
01042                                                handler=handler,
01043                                                dependencies=dependencies,
01044                                                title=title,
01045                                                description=description,
01046                                               )
01047 
01048     security.declarePrivate('_updateExportStepsRegistry')
01049     def _updateExportStepsRegistry(self, context, encoding):
01050 
01051         """ Update our export steps registry from our profile.
01052         """
01053         if context is None:
01054             context = self._getImportContext(self._import_context_id)
01055         xml = context.readDataFile(EXPORT_STEPS_XML)
01056         if xml is None:
01057             return
01058 
01059         info_list = self._export_registry.parseXML(xml, encoding)
01060 
01061         for step_info in info_list:
01062 
01063             id = step_info['id']
01064             handler = step_info['handler']
01065             title = step_info.get('title', id)
01066             description = ''.join(step_info.get('description', []))
01067 
01068             self._export_registry.registerStep(id=id,
01069                                                handler=handler,
01070                                                title=title,
01071                                                description=description,
01072                                               )
01073 
01074     security.declarePrivate('_doRunImportStep')
01075     def _doRunImportStep(self, step_id, context):
01076 
01077         """ Run a single import step, using a pre-built context.
01078         """
01079         __traceback_info__ = step_id
01080         marker = object()
01081 
01082         handler = self.getImportStep(step_id)
01083 
01084         if handler is marker:
01085             raise ValueError('Invalid import step: %s' % step_id)
01086 
01087         if handler is None:
01088             msg = 'Step %s has an invalid import handler' % step_id
01089             logger = logging.getLogger('GenericSetup')
01090             logger.error(msg)
01091             return 'ERROR: ' + msg
01092 
01093         return handler(context)
01094 
01095     security.declarePrivate('_doRunExportSteps')
01096     def _doRunExportSteps(self, steps):
01097 
01098         """ See ISetupTool.
01099         """
01100         context = TarballExportContext(self)
01101         messages = {}
01102         marker = object()
01103 
01104         for step_id in steps:
01105 
01106             handler = self.getExportStep(step_id, marker)
01107 
01108             if handler is marker:
01109                 raise ValueError('Invalid export step: %s' % step_id)
01110 
01111             if handler is None:
01112                 msg = 'Step %s has an invalid import handler' % step_id
01113                 logger = logging.getLogger('GenericSetup')
01114                 logger.error(msg)
01115                 messages[step_id] = msg
01116             else:
01117                 messages[step_id] = handler(context)
01118 
01119         return { 'steps' : steps
01120                , 'messages' : messages
01121                , 'tarball' : context.getArchive()
01122                , 'filename' : context.getArchiveFilename()
01123                }
01124 
01125 
01126     security.declareProtected(ManagePortal, 'getProfileDependencyChain')
01127     def getProfileDependencyChain(self, profile_id, seen=None):
01128         if seen is None:
01129             seen = set()
01130         elif profile_id in seen:
01131             return [] # cycle break
01132         seen.add( profile_id )
01133         chain = []
01134 
01135         dependencies = self.getDependenciesForProfile( profile_id )
01136         for dependency in dependencies:
01137             chain.extend(self.getProfileDependencyChain( dependency, seen ))
01138 
01139         chain.append(profile_id)
01140 
01141         return chain
01142 
01143 
01144     security.declarePrivate('_runImportStepsFromContext')
01145     def _runImportStepsFromContext(self,
01146                                    steps=None,
01147                                    purge_old=None,
01148                                    profile_id=None,
01149                                    archive=None,
01150                                    ignore_dependencies=False,
01151                                    seen=None):
01152 
01153         if profile_id is not None and not ignore_dependencies:
01154             try: 
01155                 chain = self.getProfileDependencyChain( profile_id )
01156             except KeyError, e:
01157                 logger = logging.getLogger('GenericSetup')
01158                 logger.error('Unknown step in dependency chain: %s' % str(e))
01159                 raise
01160         else:
01161             chain = [ profile_id ]
01162             if seen is None:
01163                 seen=set()
01164             seen.add( profile_id )
01165 
01166         
01167         results = []
01168 
01169         detect_steps = steps is None
01170 
01171         for profile_id in chain:
01172             context = self._getImportContext(profile_id, purge_old, archive)
01173             self.applyContext(context)
01174 
01175             if detect_steps:
01176                 steps = self.getSortedImportSteps()
01177 
01178             messages = {}
01179 
01180             event.notify(BeforeProfileImportEvent(self, profile_id, steps, True))
01181             for step in steps:
01182                 message = self._doRunImportStep(step, context)
01183                 message_list = filter(None, [message])
01184                 message_list.extend( ['%s: %s' % x[1:]
01185                                       for x in context.listNotes()] )
01186                 messages[step] = '\n'.join(message_list)
01187                 context.clearNotes()
01188 
01189             event.notify(ProfileImportedEvent(self, profile_id, steps, True))
01190 
01191             results.append({'steps' : steps, 'messages' : messages })
01192 
01193         data = { 'steps' : [], 'messages' : {}}
01194         for result in results:
01195             for step in result['steps']:
01196                 if step not in data['steps']:
01197                     data['steps'].append(step)
01198 
01199             for (step, msg) in result['messages'].items():
01200                 if step in data['messages']:
01201                     data['messages'][step]+="\n"+msg
01202                 else:
01203                     data['messages'][step]=msg
01204         data['steps'] = list(data['steps'])
01205 
01206         return data
01207 
01208     security.declarePrivate('_mangleTimestampName')
01209     def _mangleTimestampName(self, prefix, ext=None):
01210 
01211         """ Create a mangled ID using a timestamp.
01212         """
01213         timestamp = time.gmtime()
01214         items = (prefix,) + timestamp[:6]
01215 
01216         if ext is None:
01217             fmt = '%s-%4d%02d%02d%02d%02d%02d'
01218         else:
01219             fmt = '%s-%4d%02d%02d%02d%02d%02d.%s'
01220             items += (ext,)
01221 
01222         return fmt % items
01223 
01224     security.declarePrivate('_createReport')
01225     def _createReport(self, name, steps, messages):
01226 
01227         """ Record the results of a run.
01228         """
01229         lines = []
01230         # Create report
01231         for step in steps:
01232             lines.append('=' * 65)
01233             lines.append('Step: %s' % step)
01234             lines.append('=' * 65)
01235             msg = messages[step]
01236             lines.extend(msg.split('\n'))
01237             lines.append('')
01238 
01239         report = '\n'.join(lines)
01240         if isinstance(report, unicode):
01241             report = report.encode('latin-1')
01242 
01243         # BBB: ObjectManager won't allow unicode IDS
01244         if isinstance(name, unicode):
01245             name = name.encode('UTF-8')
01246 
01247         file = File(id=name,
01248                     title='',
01249                     file=report,
01250                     content_type='text/plain'
01251                    )
01252         self._setObject(name, file)
01253 
01254 InitializeClass(SetupTool)
01255 
01256 _PLAINTEXT_DIFF_HEADER ="""\
01257 Comparing configurations: '%s' and '%s'
01258 
01259 %s"""
01260 
01261 _TOOL_ID = 'setup_tool'
01262 
01263 addSetupToolForm = PageTemplateFile('toolAdd.zpt', _wwwdir)
01264 
01265 def addSetupTool(dispatcher, RESPONSE):
01266     """
01267     """
01268     dispatcher._setObject(_TOOL_ID, SetupTool(_TOOL_ID))
01269 
01270     RESPONSE.redirect('%s/manage_main' % dispatcher.absolute_url())