moinsetup

moinsetup.py

42:1afd3bcd9c80
2011-09-14 Paul Boddie Introduced a special site-packages location setting where libraries are installed outside the prefix hierarchy. Added more specific error messages when unsuitable or insufficient settings are given. Added an example configuration file for Debian-compatible systems.
     1 #!/usr/bin/env python     2      3 """     4 A setup and configuration script for MoinMoin.     5      6 Copyright (C) 2010, 2011 Paul Boddie <paul@boddie.org.uk>     7      8 This program is free software; you can redistribute it and/or modify it under     9 the terms of the GNU General Public License as published by the Free Software    10 Foundation; either version 3 of the License, or (at your option) any later    11 version.    12     13 This program is distributed in the hope that it will be useful, but WITHOUT    14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS    15 FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more    16 details.    17     18 You should have received a copy of the GNU General Public License along with    19 this program.  If not, see <http://www.gnu.org/licenses/>.    20 """    21     22 from os.path import abspath, exists, extsep, isdir, join, normpath, split    23 from getpass import getpass    24 from glob import glob    25 from zipfile import ZipFile    26 import os    27 import sys    28 import shutil    29 import re    30 import tempfile    31     32 __version__ = "0.3"    33     34 # Regular expressions for editing MoinMoin scripts and configuration files.    35     36 def compile_definition(name):    37     return re.compile(r"^(\s*)#*\s*(%s =)\s*(.*)$" % name, re.MULTILINE)    38     39 wikiconfig_import        = re.compile(r"^(\s*)#*\s*"    40                             r"(from\s+)(?P<module>\S+)"    41                             r"(\s+import\s+)(?P<names>(?:\S|[^,\s])+(?:\s*,\s*(?:\S|[^,\s])+)*)"    42                             r"(\s*)$", re.MULTILINE)    43     44 moin_cgi_prefix          = re.compile(r"^#sys\.path\.insert\(0, 'PREFIX.*$", re.MULTILINE)    45 moin_cgi_wikiconfig      = re.compile(r"^#sys\.path\.insert\(0, '/path/to/wikiconfigdir.*$", re.MULTILINE)    46 moin_cgi_properties      = compile_definition("properties")    47 moin_cgi_fix_script_name = compile_definition("fix_script_name")    48 moin_cgi_force_cgi       = re.compile(r"^#(os.environ\['FCGI_FORCE_CGI'\].*)$", re.MULTILINE)    49     50 css_import_stylesheet    = re.compile(r"(\s*@import\s+[\"'])(.*?)([\"']\s*;)")    51     52 # Templates for Apache site definitions.    53     54 apache_site = """    55 ScriptAlias %(url_path)s "%(web_app_dir)s/moin.cgi"    56 """    57     58 apache_site_extra_moin18 = """    59 Alias %(static_url_path)s "%(htdocs_dir)s/"    60 """    61     62 # Limited hosting .htaccess definitions require the following settings to be    63 # configured in the main Apache configuration files:    64 #    65 # Options ExecCGI FollowSymLinks Indexes SymLinksIfOwnerMatch    66 # AllowOverride FileInfo Limit    67 # AddHandler cgi-script .cgi    68     69 apache_htaccess_combined_mod_rewrite = """    70 DirectoryIndex moin.cgi    71 RewriteEngine On    72 RewriteBase %(url_path)s    73 RewriteCond %%{REQUEST_FILENAME} !-f    74 RewriteCond %%{REQUEST_FILENAME} !-d    75 RewriteRule ^(.*) moin.cgi/$1 [PT,L,QSA]    76 """    77     78 # Post-setup templates.    79     80 postsetup_setfacl = """#!/bin/sh    81     82 find '%(conf_dir)s/data' -type f | xargs setfacl -m u:%(web_user)s:rw    83 find '%(conf_dir)s/data' -type d | xargs setfacl -m u:%(web_user)s:rwx    84 find '%(conf_dir)s/underlay' -type f | xargs setfacl -m u:%(web_user)s:rw    85 find '%(conf_dir)s/underlay' -type d | xargs setfacl -m u:%(web_user)s:rwx    86 """    87     88 postsetup_setfacl_moin18_extra = """    89 find '%(htdocs_dir)s' -type f | xargs setfacl -m u:%(web_user)s:r    90 find '%(htdocs_dir)s' -type d | xargs setfacl -m u:%(web_user)s:rx    91 """    92     93 postsetup_chown_chmod = """#!/bin/sh    94     95 chown -R %(this_user)s.%(web_group)s '%(conf_dir)s/data'    96 chown -R %(this_user)s.%(web_group)s '%(conf_dir)s/underlay'    97 chmod -R g+w '%(conf_dir)s/data'    98 chmod -R g+w '%(conf_dir)s/underlay'    99 """   100    101 postsetup_chown_moin18_extra = """   102 chown -R %(this_user)s.%(web_group)s '%(htdocs_dir)s'   103 """   104    105 # Utility functions.   106    107 def readfile(filename):   108     f = open(filename)   109     try:   110         return f.read()   111     finally:   112         f.close()   113    114 def writefile(filename, s):   115     f = open(filename, "w")   116     try:   117         f.write(s)   118     finally:   119         f.close()   120    121 def status(message):   122     print message   123    124 note = status   125 error = status   126    127 def format(s, indent):   128     return re.sub("\n\s+", "\n" + " " * indent, s)   129    130 # Classes.   131    132 class SetupException(Exception):   133    134     "An exception indicating a problem with a setup action."   135    136     pass   137    138 class Configuration:   139    140     "A class representing the configuration."   141    142     special_names = ["site_name"]   143    144     def __init__(self, filename):   145         self.content = readfile(filename)   146         self.filename = filename   147    148     def get_pattern(self, name):   149    150         # Make underscores optional for certain names.   151    152         if name in self.special_names:   153             name = name.replace("_", "_?")   154    155         return compile_definition(name)   156    157     def get(self, name):   158    159         """   160         Return the raw value of the last definition having the given 'name'.   161         """   162    163         pattern = self.get_pattern(name)   164         results = [match.group(3) for match in pattern.finditer(self.content)]   165         if results:   166             return results[-1]   167         else:   168             return None   169    170     def set(self, name, value, count=None, raw=0):   171    172         """   173         Set the configuration parameter having the given 'name' with the given   174         'value', limiting the number of appropriately named parameters changed   175         to 'count', if specified.   176    177         If the configuration parameter of the given 'name' does not exist,   178         insert such a parameter at the end of the file.   179    180         If the optional 'raw' parameter is specified and set to a true value,   181         the provided 'value' is inserted directly into the configuration file.   182         """   183    184         if not self.replace(name, value, count, raw):   185             self.insert(name, value, raw)   186    187     def replace(self, name, value, count=None, raw=0):   188    189         """   190         Replace configuration parameters having the given 'name' with the given   191         'value', limiting the number of appropriately named parameters changed   192         to 'count', if specified.   193    194         If the optional 'raw' parameter is specified and set to a true value,   195         the provided 'value' is inserted directly into the configuration file.   196    197         Return the number of substitutions made.   198         """   199    200         if raw:   201             substitution = r"\1\2 %s" % value   202         else:   203             substitution = r"\1\2 %r" % value   204    205         pattern = self.get_pattern(name)   206    207         if count is None:   208             self.content, n = pattern.subn(substitution, self.content)   209         else:   210             self.content, n = pattern.subn(substitution, self.content, count=count)   211    212         return n   213    214     def insert(self, name, value, raw=0):   215    216         """   217         Insert the configuration parameter having the given 'name' and 'value'.   218    219         If the optional 'raw' parameter is specified and set to a true value,   220         the provided 'value' is inserted directly into the configuration file.   221         """   222    223         if raw:   224             insertion = "%s = %s"   225         else:   226             insertion = "%s = %r"   227    228         self.insert_text(insertion % (name, value))   229    230     def insert_text(self, text):   231    232         "Insert the given 'text' at the end of the configuration."   233    234         if not self.content.endswith("\n"):   235             self.content += "\n"   236         self.content += "    %s\n" % text   237    238     def set_import(self, imported_module, imported_names):   239    240         """   241         Set up an import of the given 'imported_module' exposing the given   242         'imported_names'.   243         """   244    245         s = self.content   246         after_point = 0   247         first_point = None   248    249         for module_import in wikiconfig_import.finditer(s):   250             before, from_keyword, module, import_keyword, names, after = module_import.groups()   251             before_point, after_point = module_import.span()   252    253             if first_point is None:   254                 first_point = after_point   255    256             names = [name.strip() for name in names.split(",")]   257    258             # Test the import for a reference to the requested imported module.   259    260             if imported_module == module:   261                 for name in imported_names:   262                     if name not in names:   263                         names.append(name)   264    265                 self.content = s[:before_point] + (   266                     "%s%s%s%s%s%s" % (before, from_keyword, module, import_keyword, ", ".join(names), after)   267                     ) + s[after_point:]   268                 break   269    270         # Where no import references the imported module, insert a reference   271         # into the configuration.   272    273         else:   274             # Add the import after the first one.   275    276             if first_point is not None:   277                 self.content = s[:first_point] + ("\nfrom %s import %s" % (imported_module, ", ".join(imported_names))) + s[first_point:]   278    279     def close(self):   280    281         "Close the file, writing the content."   282    283         writefile(self.filename, self.content)   284    285 class Installation:   286    287     "A class for installing and initialising MoinMoin."   288    289     method_names = (   290         "setup",   291         "setup_wiki",   292         "install_moin",   293         "install_data",   294         "configure_moin",   295         "edit_moin_script",   296         "edit_moin_web_script",   297         "add_superuser",   298         "make_site_files",   299         "make_post_install_script",   300         "reconfigure_moin",   301         "set_auth_method",   302    303         # Post-installation activities.   304    305         "install_theme",   306         "install_extension_package",   307         "install_plugins",   308         "install_actions",   309         "install_macros",   310         "install_parsers",   311         "install_theme_resources",   312         "edit_theme_stylesheet",   313    314         # Other activities.   315    316         "make_page_package",   317         "install_page_package",   318         )   319    320     # NOTE: Need to detect Web server user.   321    322     web_user = "www-data"   323     web_group = "www-data"   324    325     # MoinMoin resources.   326    327     theme_master = "modernized"   328     extra_theme_css_files = ["SlideShow.css"]   329    330     def __init__(self, moin_distribution=None, moin_data=None, prefix=None,   331         site_packages=None, web_app_dir=None, web_static_dir=None, web_site_dir=None,   332         common_dir=None, farm_config=None, site_config=None, url_path=None,   333         superuser=None, site_name=None, front_page_name=None, theme_default=None):   334    335         """   336         Initialise a Wiki installation using the following:   337    338           * moin_distribution - the directory containing a MoinMoin source   339                                 distribution   340           * moin_data         - the directory containing MoinMoin shared data   341                                 (can be omitted if 'moin_distribution' is given)   342           * prefix            - the installation prefix (equivalent to /usr)   343           * web_app_dir       - the directory where Web applications and scripts   344                                 reside (such as /home/www-user/cgi-bin)   345           * common_dir        - the directory where the Wiki configuration,   346                                 resources and instance will reside (such as   347                                 /home/www-user/mywiki)   348           * url_path          - the URL path at which the Wiki will be made   349                                 available (such as / or /mywiki)   350           * superuser         - the name of the site's superuser (such as   351                                 "AdminUser")   352           * site_name         - the name of the site (such as "My Wiki")   353           * front_page_name   - the front page name for the site (such as   354                                 "FrontPage" or a specific name for the site)   355           * web_site_dir      - optional: the directory where Web site   356                                 definitions reside (such as   357                                 /etc/apache2/sites-available)   358           * web_static_dir    - optional: the directory where static Web   359                                 resources reside (such as /home/www-user/htdocs)   360           * farm_config       - optional: any Wiki farm configuration file for   361                                 multiple Wiki deployments (overrides the   362                                 'common_dir' setting)   363           * site_config       - optional: a specific configuration file location   364                                 (overrides the 'common_dir' setting)   365           * site_packages     - optional: the location of the Python   366                                 site-packages directory if outside the 'prefix'   367                                 (overrides the path calculated using 'prefix')   368           * theme_default     - optional: the default theme (such as modern)   369         """   370    371         self.moin_distribution = moin_distribution   372         self.moin_data = moin_data   373         self.superuser = superuser   374         self.site_name = site_name   375         self.front_page_name = front_page_name   376         self.farm_config = farm_config   377         self.site_config = site_config   378         self.theme_default = theme_default   379    380         # NOTE: Support the detection of the Apache sites directory.   381    382         self.prefix, self.web_app_dir, self.web_site_dir, self.web_static_dir, self.common_dir = \   383             map(self._get_abspath, (prefix, web_app_dir, web_site_dir, web_static_dir, common_dir))   384    385         # Strip any trailing "/" from the URL path.   386    387         if url_path != "/" and url_path.endswith("/"):   388             self.url_path = url_path[:-1]   389         else:   390             self.url_path = url_path   391    392         # Define and create specific directories.   393         # Here are the configuration and actual Wiki data directories.   394    395         self.conf_dir = join(self.common_dir, "conf")   396         self.instance_dir = join(self.common_dir, "wikidata")   397    398         # Define the place where the MoinMoin package will actually reside.   399    400         self.prefix_site_packages = site_packages or \   401             join(self.prefix, "lib", "python%s.%s" % sys.version_info[:2], "site-packages")   402    403         # Find the version.   404    405         self.moin_version = self.get_moin_version()   406    407         # The static resources reside in different locations depending on the   408         # version of MoinMoin. Moreover, these resources may end up in a   409         # published directory for 1.8 installations where the Web server cannot   410         # be instructed to fetch the content from outside certain designated   411         # locations.   412    413         # 1.9: moin/lib/python2.x/site-packages/MoinMoin/web/static/htdocs   414    415         if self.moin_version.startswith("1.9"):   416    417             # A shared data directory may be in use.   418    419             if self.moin_data:   420                 self.htdocs_dir = self.htdocs_dir_source = join(self.moin_data, "htdocs")   421             else:   422                 self.htdocs_dir = self.htdocs_dir_source = join(self.prefix_site_packages, "MoinMoin", "web", "static", "htdocs")   423    424             self.static_url_path = self.url_path   425    426         # 1.8: moin/share/moin/htdocs (optionally copied to a Web directory)   427    428         else:   429    430             # A shared data directory may be in use.   431    432             if self.moin_data:   433                 self.htdocs_dir_source = join(self.moin_data, "htdocs")   434             else:   435                 self.htdocs_dir_source = join(self.instance_dir, "share", "moin", "htdocs")   436    437             # Add the static identifier to the URL path. For example:   438             # /         -> /moin_static187   439             # /hgwiki   -> /hgwiki-moin_static187   440    441             self.static_url_path = self.url_path + (self.url_path != "/" and "-" or "") + self.get_static_identifier()   442    443             # In limited hosting, the static resources directory is related to   444             # the URL path.   445    446             if self.limited_hosting():   447                 self.htdocs_dir = join(self.web_static_dir or self.web_app_dir, self.static_url_path.lstrip("/"))   448    449             # Otherwise, a mapping is made to the directory.   450    451             else:   452                 self.htdocs_dir = self.htdocs_dir_source   453    454     def _get_abspath(self, d):   455         return d and abspath(d) or None   456    457     def get_moin_version(self):   458    459         "Inspect the MoinMoin package information, returning the version."   460    461         if self.moin_distribution:   462             try:   463                 this_dir = os.getcwd()   464                 try:   465                     os.chdir(self.moin_distribution)   466                     f = open("PKG-INFO")   467                     try:   468                         for line in f.xreadlines():   469                             columns = line.split()   470                             if columns[0] == "Version:":   471                                 return columns[1]   472    473                         return None   474    475                     finally:   476                         f.close()   477    478                 finally:   479                     os.chdir(this_dir)   480    481             except IOError:   482                 pass   483    484         # Where no distribution information can be read, try and import an   485         # installed version module.   486    487         f = os.popen("%s -c 'from MoinMoin.version import release; print release'" % sys.executable)   488         try:   489             return f.read().strip()   490         finally:   491             f.close()   492    493     def get_moin_data(self):   494    495         "Return the location of MoinMoin data."   496    497         return self.moin_data or self.moin_distribution and join(self.moin_distribution, "wiki") or None   498    499     def get_static_identifier(self):   500    501         "Return the static URL/directory identifier for the Wiki."   502    503         return "moin_static%s" % self.moin_version.replace(".", "")   504    505     def get_plugin_directory(self, plugin_type):   506    507         "Return the directory for plugins of the given 'plugin_type'."   508    509         data_dir = join(self.conf_dir, "data")   510         return join(data_dir, "plugin", plugin_type)   511    512     def limited_hosting(self):   513    514         "Return whether limited Web hosting is being used."   515    516         return not self.web_site_dir   517    518     def ensure_directories(self):   519    520         "Make sure that all the directories are available."   521    522         for d in (self.conf_dir, self.instance_dir, self.web_app_dir, self.web_static_dir, self.web_site_dir):   523             if d is not None and not exists(d):   524                 os.makedirs(d)   525    526     def get_theme_directories(self, theme_name=None):   527    528         """   529         Return tuples of the form (theme name, theme directory) for all themes,   530         or for a single theme if the optional 'theme_name' is specified.   531         """   532    533         filenames = theme_name and [theme_name] or os.listdir(self.htdocs_dir)   534         directories = []   535    536         for filename in filenames:   537             theme_dir = join(self.htdocs_dir, filename)   538    539             if not exists(theme_dir) or not isdir(theme_dir):   540                 continue   541    542             directories.append((filename, theme_dir))   543    544         return directories   545    546     # Main methods.   547    548     def setup(self):   549    550         "Set up the installation."   551    552         self.ensure_directories()   553         self.install_moin()   554         self._setup_wiki()   555    556     def setup_wiki(self):   557    558         "Set up a Wiki without installing MoinMoin."   559    560         self.ensure_directories()   561    562         try:   563             self.install_moin(data_only=1)   564         except SetupException:   565             if not self.moin_data:   566                 raise   567    568         self._setup_wiki()   569    570     def _setup_wiki(self):   571    572         "Set up a Wiki without installing MoinMoin."   573    574         self.install_data()   575         self.configure_moin()   576         self.edit_moin_script()   577         self.add_superuser()   578         self.edit_moin_web_script(self.make_site_files())   579         self.make_post_install_script()   580    581     def install_moin(self, data_only=0):   582    583         "Enter the distribution directory and run the setup script."   584    585         # NOTE: Possibly check for an existing installation and skip repeated   586         # NOTE: installation attempts.   587    588         if not self.moin_distribution:   589             raise SetupException, "Cannot install MoinMoin without a 'moin_distribution' setting being defined."   590    591         this_dir = os.getcwd()   592         os.chdir(self.moin_distribution)   593    594         log_filename = "install-%s.log" % split(self.common_dir)[-1]   595    596         status("Installing MoinMoin%s in %s..." % (data_only and " (data only)" or "", self.prefix))   597    598         if data_only:   599             install_cmd = "install_data"   600             options = "--install-dir='%s'" % self.instance_dir   601         else:   602             install_cmd = "install"   603             options = "--prefix='%s' --install-data='%s' --record='%s'" % (self.prefix, self.instance_dir, log_filename)   604    605         os.system("python setup.py --quiet %s %s --force" % (install_cmd, options))   606    607         os.chdir(this_dir)   608    609     def install_data(self):   610    611         "Install Wiki data."   612    613         moin_data = self.get_moin_data()   614    615         if not moin_data:   616             raise SetupException, \   617                 "Cannot install MoinMoin data without either a 'moin_distribution' or a 'moin_data' setting being defined."   618    619         # The default wikiconfig assumes data and underlay in the same directory.   620    621         status("Installing data and underlay in %s..." % self.conf_dir)   622    623         for d in ("data", "underlay"):   624             source = join(moin_data, d)   625             source_tar = source + os.path.extsep + "tar"   626             d_tar = source + os.path.extsep + "tar"   627    628             if os.path.exists(source):   629                 shutil.copytree(source, join(self.conf_dir, d))   630             elif os.path.exists(source_tar):   631                 shutil.copy(source_tar, self.conf_dir)   632                 os.system("tar xf %s -C %s" % (d_tar, self.conf_dir))   633             else:   634                 status("Could not copy %s into installed Wiki." % d)   635    636         # Copy static Web data if appropriate.   637    638         if not self.moin_version.startswith("1.9") and self.limited_hosting():   639    640             if not exists(self.htdocs_dir):   641                 os.mkdir(self.htdocs_dir)   642    643             for item in os.listdir(self.htdocs_dir_source):   644                 path = join(self.htdocs_dir_source, item)   645                 if isdir(path):   646                     shutil.copytree(path, join(self.htdocs_dir, item))   647                 else:   648                     shutil.copy(path, join(self.htdocs_dir, item))   649    650     def configure_moin(self):   651    652         "Edit the Wiki configuration file."   653    654         moin_data = self.get_moin_data()   655    656         if not moin_data:   657             raise SetupException, \   658                 "Cannot configure MoinMoin without either a 'moin_distribution' or a 'moin_data' setting being defined."   659    660         # NOTE: Single Wiki only so far.   661    662         # Static URLs seem to be different in MoinMoin 1.9.x.   663         # For earlier versions, reserve URL space alongside the Wiki.   664         # NOTE: MoinMoin usually uses an apparently common URL space associated   665         # NOTE: with the version, but more specific locations are probably   666         # NOTE: acceptable if less efficient.   667    668         if self.moin_version.startswith("1.9"):   669             url_prefix_static = "%r + url_prefix_static" % self.static_url_path   670         else:   671             url_prefix_static = "%r" % self.static_url_path   672    673         # Use a farm configuration file.   674    675         if self.farm_config:   676             wikiconfig_py = self.farm_config   677    678         # Or copy the Wiki configuration file from the distribution.   679    680         else:   681             wikiconfig_py = join(self.conf_dir, "wikiconfig.py")   682             shutil.copyfile(join(moin_data, "config", "wikiconfig.py"), wikiconfig_py)   683    684         status("Editing configuration from %s..." % wikiconfig_py)   685    686         # Edit the Wiki configuration file.   687    688         wikiconfig = Configuration(wikiconfig_py)   689    690         try:   691             wikiconfig.set("url_prefix_static", url_prefix_static, raw=1)   692             wikiconfig.set("superuser", [self.superuser])   693             wikiconfig.set("acl_rights_before", u"%s:read,write,delete,revert,admin" % self.superuser)   694    695             # Edit any created Wiki configuration.   696    697             if not self.site_config:   698                 self._configure_moin(wikiconfig)   699    700         finally:   701             wikiconfig.close()   702    703         # Edit any separate site configuration file.   704    705         if self.site_config:   706             status("Editing configuration from %s..." % self.site_config)   707    708             wikiconfig = Configuration(self.site_config)   709    710             try:   711                 self._configure_moin(wikiconfig)   712             finally:   713                 wikiconfig.close()   714    715     def _configure_moin(self, wikiconfig):   716    717         """   718         Configure Moin, accessing the configuration file using 'wikiconfig'.   719         """   720    721         if not self.moin_version.startswith("1.9"):   722             data_dir = join(self.conf_dir, "data")   723             data_underlay_dir = join(self.conf_dir, "underlay")   724    725             wikiconfig.set("data_dir", data_dir)   726             wikiconfig.set("data_underlay_dir", data_underlay_dir)   727    728         wikiconfig.set("site_name", self.site_name)   729         wikiconfig.set("page_front_page", self.front_page_name, count=1)   730    731         if self.theme_default is not None:   732             wikiconfig.set("theme_default", self.theme_default)   733    734     def edit_moin_script(self):   735    736         "Edit the moin script."   737    738         moin_script = join(self.prefix, "bin", "moin")   739    740         status("Editing moin script at %s..." % moin_script)   741    742         s = readfile(moin_script)   743         s = s.replace("#import sys", "import sys\nsys.path.insert(0, %r)" % self.prefix_site_packages)   744    745         writefile(moin_script, s)   746    747     def edit_moin_web_script(self, site_file_configured=1):   748    749         "Edit and install CGI script."   750    751         # NOTE: CGI only so far.   752         # NOTE: Permissions should be checked.   753    754         moin_data = self.get_moin_data()   755    756         if self.moin_version.startswith("1.9"):   757             moin_cgi_script = "moin.fcgi"   758         else:   759             moin_cgi_script = "moin.cgi"   760    761         moin_cgi = join(moin_data, "server", moin_cgi_script)   762         moin_cgi_installed = join(self.web_app_dir, "moin.cgi")   763    764         status("Editing moin.cgi script from %s, writing to %s..." % (moin_cgi, moin_cgi_installed))   765    766         s = readfile(moin_cgi)   767         s = moin_cgi_prefix.sub("sys.path.insert(0, %r)" % self.prefix_site_packages, s)   768         s = moin_cgi_wikiconfig.sub("sys.path.insert(0, %r)" % self.conf_dir, s)   769    770         # Handle differences in script names when using limited hosting with   771         # URL rewriting.   772    773         if self.limited_hosting():   774             if not site_file_configured:   775                 note("Site file not configured: script name not changed.")   776             else:   777                 if self.moin_version.startswith("1.9"):   778                     s = moin_cgi_fix_script_name.sub(r"\1\2 %r" % self.url_path, s)   779                 else:   780                     s = moin_cgi_properties.sub(r"\1\2 %r" % {"script_name" : self.url_path}, s)   781    782         # NOTE: Use CGI for now.   783    784         if self.moin_version.startswith("1.9"):   785             s = moin_cgi_force_cgi.sub(r"\1", s)   786    787         writefile(moin_cgi_installed, s)   788         os.system("chmod a+rx '%s'" % moin_cgi_installed)   789    790     def add_superuser(self):   791    792         "Add the superuser account."   793    794         moin_script = join(self.prefix, "bin", "moin")   795    796         print "Creating superuser", self.superuser, "using..."   797         email = raw_input("E-mail address: ")   798         password = getpass("Password: ")   799    800         path = os.environ.get("PYTHONPATH", "")   801    802         if path:   803             os.environ["PYTHONPATH"] = path + ":" + self.conf_dir   804         else:   805             os.environ["PYTHONPATH"] = self.conf_dir   806    807         cmd = moin_script + " --config-dir='%s' account create --name='%s' --email='%s' --password='%s'" % (   808             self.conf_dir, self.superuser, email, password)   809         os.system(cmd)   810    811         if path:   812             os.environ["PYTHONPATH"] = path   813         else:   814             del os.environ["PYTHONPATH"]   815    816     def make_site_files(self):   817    818         "Make the Apache site files."   819    820         # NOTE: Using local namespace for substitution.   821    822         # Where the site definitions and applications directories are different,   823         # use a normal site definition.   824    825         if not self.limited_hosting():   826    827             site_def = join(self.web_site_dir, self.site_name)   828    829             s = apache_site % self.__dict__   830    831             if not self.moin_version.startswith("1.9"):   832                 s += apache_site_extra_moin18 % self.__dict__   833    834             status("Writing Apache site definitions to %s..." % site_def)   835             writefile(site_def, s)   836    837             note("Copy the site definitions to the appropriate sites directory if appropriate.")   838             note("Then, make sure that the site is enabled by running the appropriate tools (such as a2ensite).")   839    840             return 1   841    842         # Otherwise, use an .htaccess file.   843    844         else:   845             site_def = join(self.web_app_dir, ".htaccess")   846    847             s = apache_htaccess_combined_mod_rewrite % self.__dict__   848    849             status("Writing .htaccess file to %s..." % site_def)   850             try:   851                 writefile(site_def, s)   852             except IOError:   853                 note("The .htaccess file could not be written. This will also affect the script name setting.")   854                 return 0   855             else:   856                 return 1   857    858     def make_post_install_script(self):   859    860         "Write a post-install script with additional actions."   861    862         # Work out whether setfacl works.   863    864         fd, temp_filename = tempfile.mkstemp(dir=self.conf_dir)   865         os.close(fd)   866    867         have_setfacl = os.system("setfacl -m user:%(web_user)s:r %(file)s > /dev/null 2>&1" % {   868             "web_user" : self.web_user, "file" : temp_filename}) == 0   869    870         os.remove(temp_filename)   871    872         # Create the scripts.   873    874         this_user = os.environ["USER"]   875         postinst_scripts = "moinsetup-post-chown.sh", "moinsetup-post-setfacl.sh"   876    877         vars = {}   878         vars.update(Installation.__dict__)   879         vars.update(self.__dict__)   880         vars.update(locals())   881    882         for postinst_script, start, extra in [   883             (postinst_scripts[0], postsetup_chown_chmod, postsetup_chown_moin18_extra),   884             (postinst_scripts[1], postsetup_setfacl, postsetup_setfacl_moin18_extra)   885             ]:   886    887             s = start % vars   888    889             if not self.moin_version.startswith("1.9"):   890                 s += extra % vars   891    892             writefile(postinst_script, s)   893             os.chmod(postinst_script, 0755)   894    895         if have_setfacl:   896             note("Run %s to set file ownership and permissions." % postinst_scripts[1])   897             note("If this somehow fails...")   898    899         note("Run %s as root to set file ownership and permissions." % postinst_scripts[0])   900    901     # Accessory methods.   902    903     def reconfigure_moin(self, name=None, value=None, raw=0):   904    905         """   906         Edit the installed Wiki configuration file, setting a parameter with any   907         given 'name' to the given 'value', treating the value as a raw   908         expression (not a string) if 'raw' is set to a true value.   909    910         If 'name' and the remaining parameters are omitted, the default   911         configuration activity is performed.   912    913         If the 'site_config' setting is defined, the specific site configuration   914         will be changed.   915         """   916    917         if self.site_config:   918             wikiconfig_py = self.site_config   919         else:   920             wikiconfig_py = join(self.conf_dir, "wikiconfig.py")   921    922         status("Editing configuration from %s..." % wikiconfig_py)   923    924         wikiconfig = Configuration(wikiconfig_py)   925    926         try:   927             # Perform default configuration.   928    929             if name is None and value is None:   930                 self._configure_moin(wikiconfig)   931             else:   932                 wikiconfig.set(name, value, raw=raw)   933    934         finally:   935             wikiconfig.close()   936    937     def set_auth_method(self, method_name):   938    939         """   940         Edit the installed Wiki configuration file, configuring the   941         authentication method having the given 'method_name'.   942    943         If the 'farm_config' setting is defined, the Wiki farm configuration   944         will be changed.   945         """   946    947         if self.farm_config:   948             wikiconfig_py = self.farm_config   949         else:   950             wikiconfig_py = join(self.conf_dir, "wikiconfig.py")   951    952         status("Editing configuration from %s..." % wikiconfig_py)   953    954         wikiconfig = Configuration(wikiconfig_py)   955    956         try:   957             # OpenID authentication.   958    959             if method_name.lower() == "openid":   960                 wikiconfig.set_import("MoinMoin.auth.openidrp", ["OpenIDAuth"])   961    962                 if self.moin_version.startswith("1.9"):   963                     if wikiconfig.get("cookie_lifetime"):   964                         wikiconfig.replace("cookie_lifetime", "(12, 12)", raw=1)   965                     else:   966                         wikiconfig.set("cookie_lifetime", "(12, 12)", raw=1)   967                 else:   968                     if wikiconfig.get("anonymous_session_lifetime"):   969                         wikiconfig.replace("anonymous_session_lifetime", "1000", raw=1)   970                     else:   971                         wikiconfig.set("anonymous_session_lifetime", "1000", raw=1)   972    973                 auth_object = "OpenIDAuth()"   974    975             # Default Moin authentication.   976    977             elif method_name.lower() in ("moin", "default"):   978                 wikiconfig.set_import("MoinMoin.auth", ["MoinAuth"])   979                 auth_object = "MoinAuth()"   980    981             # REMOTE_USER authentication.   982    983             elif method_name.lower() in ("given", "remote-user"):   984                 wikiconfig.set_import("MoinMoin.auth.http", ["HTTPAuth"])   985                 auth_object = "HTTPAuth(autocreate=True)"   986    987             # Other methods are not currently supported.   988    989             else:   990                 return   991    992             # Edit the authentication setting.   993    994             auth = wikiconfig.get("auth")   995             if auth:   996                 wikiconfig.replace("auth", "%s + [%s]" % (auth, auth_object), raw=1)   997             else:   998                 wikiconfig.set("auth", "[%s]" % auth_object, raw=1)   999   1000         finally:  1001             wikiconfig.close()  1002   1003     def install_theme(self, theme_dir, theme_name=None):  1004   1005         """  1006         Install Wiki theme provided in the given 'theme_dir' having the given  1007         optional 'theme_name' (if different from the 'theme_dir' name).  1008         """  1009   1010         theme_dir = normpath(theme_dir)  1011         theme_name = theme_name or split(theme_dir)[-1]  1012         theme_module = join(theme_dir, theme_name + extsep + "py")  1013   1014         plugin_theme_dir = self.get_plugin_directory("theme")  1015   1016         # Copy the theme module.  1017   1018         status("Copying theme module to %s..." % plugin_theme_dir)  1019   1020         shutil.copy(theme_module, plugin_theme_dir)  1021   1022         # Copy the resources.  1023   1024         resources_dir = join(self.htdocs_dir, theme_name)  1025   1026         if not exists(resources_dir):  1027             os.mkdir(resources_dir)  1028   1029         status("Copying theme resources to %s..." % resources_dir)  1030   1031         for d in ("css", "img"):  1032             target_dir = join(resources_dir, d)  1033             if exists(target_dir):  1034                 status("Replacing %s..." % target_dir)  1035                 shutil.rmtree(target_dir)  1036             shutil.copytree(join(theme_dir, d), target_dir)  1037   1038         # Copy additional resources from other themes.  1039   1040         resources_source_dir = join(self.htdocs_dir, self.theme_master)  1041         target_dir = join(resources_dir, "css")  1042   1043         status("Copying resources from %s..." % resources_source_dir)  1044   1045         for css_file in self.extra_theme_css_files:  1046             css_file_path = join(resources_source_dir, "css", css_file)  1047             if exists(css_file_path):  1048                 shutil.copy(css_file_path, target_dir)  1049   1050         note("Don't forget to add theme resources for extensions for this theme.")  1051         note("Don't forget to edit this theme's stylesheets for extensions.")  1052   1053     def install_extension_package(self, extension_dir):  1054   1055         "Install any libraries from 'extension_dir' using a setup script."  1056   1057         this_dir = os.getcwd()  1058         os.chdir(extension_dir)  1059         os.system("python setup.py install --prefix=%s" % self.prefix)  1060         os.chdir(this_dir)  1061   1062     def install_plugins(self, plugins_dir, plugin_type):  1063   1064         """  1065         Install Wiki actions provided in the given 'plugins_dir' of the  1066         specified 'plugin_type'.  1067         """  1068   1069         plugin_target_dir = self.get_plugin_directory(plugin_type)  1070   1071         # Copy the modules.  1072   1073         status("Copying %s modules to %s..." % (plugin_type, plugin_target_dir))  1074   1075         for module in glob(join(plugins_dir, "*%spy" % extsep)):  1076             shutil.copy(module, plugin_target_dir)  1077   1078     def install_actions(self, actions_dir):  1079   1080         "Install Wiki actions provided in the given 'actions_dir'."  1081   1082         self.install_plugins(actions_dir, "action")  1083   1084     def install_macros(self, macros_dir):  1085   1086         "Install Wiki macros provided in the given 'macros_dir'."  1087   1088         self.install_plugins(macros_dir, "macro")  1089   1090     def install_parsers(self, parsers_dir):  1091   1092         "Install Wiki macros provided in the given 'parsers_dir'."  1093   1094         self.install_plugins(parsers_dir, "parser")  1095   1096     def install_theme_resources(self, theme_resources_dir, theme_name=None):  1097   1098         """  1099         Install theme resources provided in the given 'theme_resources_dir'. If  1100         a specific 'theme_name' is given, only that theme will be given the  1101         specified resources.  1102         """  1103   1104         for theme_name, theme_dir in self.get_theme_directories(theme_name):  1105   1106             # Copy the resources.  1107   1108             copied = 0  1109   1110             for d in ("css", "img"):  1111                 source_dir = join(theme_resources_dir, d)  1112                 target_dir = join(theme_dir, d)  1113   1114                 if not exists(target_dir):  1115                     continue  1116   1117                 for resource in glob(join(source_dir, "*%s*" % extsep)):  1118                     shutil.copy(resource, target_dir)  1119                     copied = 1  1120   1121             if copied:  1122                 status("Copied theme resources into %s..." % theme_dir)  1123   1124         note("Don't forget to edit theme stylesheets for any extensions.")  1125   1126     def edit_theme_stylesheet(self, theme_stylesheet, imported_stylesheet, action="ensure", theme_name=None):  1127   1128         """  1129         Edit the given 'theme_stylesheet', ensuring (or removing) a reference to  1130         the 'imported_stylesheet' according to the given 'action' (optional,  1131         defaulting to "ensure"). If a specific 'theme_name' is given, only that  1132         theme will be affected.  1133         """  1134   1135         if action == "ensure":  1136             ensure = 1  1137         elif action == "remove":  1138             ensure = 0  1139         else:  1140             error("Action %s not valid: it must be given as either 'ensure' or 'remove'." % action)  1141             return  1142   1143         for theme_name, theme_dir in self.get_theme_directories(theme_name):  1144   1145             # Locate the resources.  1146   1147             css_dir = join(theme_dir, "css")  1148   1149             if not exists(css_dir):  1150                 continue  1151   1152             theme_stylesheet_filename = join(css_dir, theme_stylesheet)  1153             imported_stylesheet_filename = join(css_dir, imported_stylesheet)  1154   1155             if not exists(theme_stylesheet_filename):  1156                 error("Stylesheet %s not defined in theme %s." % (theme_stylesheet, theme_name))  1157                 continue  1158   1159             if not exists(imported_stylesheet_filename):  1160                 error("Stylesheet %s not defined in theme %s." % (imported_stylesheet, theme_name))  1161                 continue  1162   1163             # Edit the resources.  1164   1165             s = readfile(theme_stylesheet_filename)  1166             after_point = 0  1167   1168             for stylesheet_import in css_import_stylesheet.finditer(s):  1169                 before, filename, after = stylesheet_import.groups()  1170                 before_point, after_point = stylesheet_import.span()  1171   1172                 # Test the import for a reference to the requested imported  1173                 # stylesheet.  1174   1175                 if filename == imported_stylesheet:  1176                     if ensure:  1177                         break  1178                     else:  1179                         if s[after_point:after_point+1] == "\n":  1180                             after_point += 1  1181                         s = "%s%s" % (s[:before_point], s[after_point:])  1182   1183                         status("Removing %s from %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name))  1184                         writefile(theme_stylesheet_filename, s)  1185                         break  1186   1187             # Where no import references the imported stylesheet, insert a  1188             # reference into the theme stylesheet.  1189   1190             else:  1191                 if ensure:  1192   1193                     # Assume that the stylesheet can follow other imports.  1194   1195                     if s[after_point:after_point+1] == "\n":  1196                         after_point += 1  1197                     s = "%s%s\n%s" % (s[:after_point], '@import "%s";' % imported_stylesheet, s[after_point:])  1198   1199                     status("Adding %s to %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name))  1200                     writefile(theme_stylesheet_filename, s)  1201   1202     def make_page_package(self, page_directory, package_filename):  1203   1204         """  1205         Make a package containing the pages in 'page_directory', using the  1206         filenames as the page names, and writing the package to a file with the  1207         given 'package_filename'.  1208         """  1209   1210         package = ZipFile(package_filename, "w")  1211   1212         try:  1213             script = ["MoinMoinPackage|1"]  1214   1215             for filename in os.listdir(page_directory):  1216                 pathname = join(page_directory, filename)  1217   1218                 # Add files as pages having the filename as page name.  1219   1220                 if os.path.isfile(pathname):  1221                     package.write(pathname, filename)  1222                     script.append("AddRevision|%s|%s" % (filename, filename))  1223   1224                 # Add directories ending with "-attachments" as collections of  1225                 # attachments for a particular page.  1226   1227                 elif os.path.isdir(pathname) and filename.endswith("-attachments"):  1228                     parent = filename[:-len("-attachments")]  1229   1230                     # Add each file as an attachment.  1231   1232                     for attachment in os.listdir(pathname):  1233                         zipname = "%s_%s" % (filename, attachment)  1234                         package.write(join(pathname, attachment), zipname)  1235                         script.append("AddAttachment|%s|%s|%s||" % (zipname, attachment, parent))  1236   1237             package.writestr("MOIN_PACKAGE", "\n".join(script))  1238   1239         finally:  1240             package.close()  1241   1242     def install_page_package(self, package_filename):  1243   1244         """  1245         Install a package from the file with the given 'package_filename'.  1246         """  1247   1248         path = os.environ.get("PYTHONPATH", "")  1249   1250         if path:  1251             os.environ["PYTHONPATH"] = path + ":" + self.prefix_site_packages + ":" + self.conf_dir  1252         else:  1253             os.environ["PYTHONPATH"] = self.prefix_site_packages + ":" + self.conf_dir  1254   1255         installer = join(self.prefix_site_packages, "MoinMoin", "packages.py")  1256         cmd = "python %s i %s" % (installer, package_filename)  1257         os.system(cmd)  1258   1259         if path:  1260             os.environ["PYTHONPATH"] = path  1261         else:  1262             del os.environ["PYTHONPATH"]  1263   1264 def show_methods():  1265     print "Methods:"  1266     print  1267     for method_name in Installation.method_names:  1268         doc = getattr(Installation, method_name).__doc__.strip()  1269         print "%-30s%-s" % (method_name, format(doc, 30))  1270     print  1271   1272 # Command line option syntax.  1273   1274 syntax_description = "[ -f <config-filename> ] ( <method> | --method=METHOD ) [ <method-argument> ... ]"  1275   1276 # Main program.  1277   1278 if __name__ == "__main__":  1279     from ConfigParser import ConfigParser  1280     import sys, cmdsyntax  1281   1282     # Check the command syntax.  1283   1284     syntax = cmdsyntax.Syntax(syntax_description)  1285     try:  1286         matches = syntax.get_args(sys.argv[1:])  1287         args = matches[0]  1288     except IndexError:  1289         print "Syntax:"  1290         print sys.argv[0], syntax_description  1291         print  1292         show_methods()  1293         sys.exit(1)  1294   1295     # Obtain configuration details.  1296   1297     try:  1298         config_filename = args.get("config-filename", "moinsetup.cfg")  1299         config = ConfigParser()  1300         config.read(config_filename)  1301   1302         # Obtain as many arguments as needed from the configuration.  1303   1304         config_arguments = dict(config.items("installation") + config.items("site"))  1305         method_arguments = args.get("method-argument", [])  1306   1307         # Attempt to initialise the configuration.  1308   1309         installation = Installation(**config_arguments)  1310   1311     except TypeError, exc:  1312         print exc.args[0]  1313         print  1314         print "Configuration settings:"  1315         print  1316         print Installation.__init__.__doc__  1317         print  1318         sys.exit(1)  1319   1320     # Obtain the method.  1321   1322     try:  1323         method = getattr(installation, args["method"])  1324     except AttributeError:  1325         show_methods()  1326         sys.exit(1)  1327   1328     try:  1329         method(*method_arguments)  1330     except TypeError:  1331         print exc.args[0]  1332         print  1333         print "Method documentation:"  1334         print  1335         print method.__doc__  1336         print  1337         raise  1338   1339 # vim: tabstop=4 expandtab shiftwidth=4