moinsetup

moinsetup.py

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