moinsetup

moinsetup.py

15:866ec2faae57
2010-05-26 Paul Boddie Fixed URL path normalisation.
     1 #!/usr/bin/env python     2      3 from os.path import abspath, exists, extsep, isdir, join, normpath, split     4 from getpass import getpass     5 import os     6 import sys     7 import shutil     8 import re     9     10 # Regular expressions for editing MoinMoin scripts and configuration files.    11     12 def compile_definition(name):    13     return re.compile(r"^(\s*)#*(%s =).*$" % name, re.MULTILINE)    14     15 moin_cgi_prefix          = re.compile("^#sys\.path\.insert\(0, 'PREFIX.*$", re.MULTILINE)    16 moin_cgi_wikiconfig      = re.compile("^#sys\.path\.insert\(0, '/path/to/wikiconfigdir.*$", re.MULTILINE)    17 moin_cgi_properties      = compile_definition("properties")    18 moin_cgi_fix_script_name = compile_definition("fix_script_name")    19 moin_cgi_force_cgi       = re.compile("^#(os.environ\['FCGI_FORCE_CGI'\].*)$", re.MULTILINE)    20     21 # Templates for Apache site definitions.    22     23 apache_site = """    24 ScriptAlias %(url_path)s "%(web_app_dir)s/moin.cgi"    25 """    26     27 apache_site_extra_moin18 = """    28 Alias %(static_url_path)s "%(htdocs_dir)s/"    29 """    30     31 # Limited hosting .htaccess definitions require the following settings to be    32 # configured in the main Apache configuration files:    33 #    34 # Options ExecCGI FollowSymLinks Indexes SymLinksIfOwnerMatch    35 # AllowOverride FileInfo Limit    36 # AddHandler cgi-script .cgi    37     38 apache_htaccess_combined_mod_rewrite = """    39 DirectoryIndex moin.cgi    40 RewriteEngine On    41 RewriteBase %(url_path)s    42 RewriteCond %%{REQUEST_FILENAME} !-f    43 RewriteCond %%{REQUEST_FILENAME} !-d    44 RewriteRule ^(.*) moin.cgi/$1 [PT,L,QSA]    45 """    46     47 # Utility functions.    48     49 def readfile(filename):    50     f = open(filename)    51     try:    52         return f.read()    53     finally:    54         f.close()    55     56 def writefile(filename, s):    57     f = open(filename, "w")    58     try:    59         f.write(s)    60     finally:    61         f.close()    62     63 def status(message):    64     print message    65     66 def note(message):    67     print message    68     69 class Configuration:    70     71     "A class representing the configuration."    72     73     special_names = ["site_name"]    74     75     def __init__(self, filename):    76         self.content = readfile(filename)    77         self.filename = filename    78     79     def get_pattern(self, name):    80     81         # Make underscores optional for certain names.    82     83         if name in self.special_names:    84             name = name.replace("_", "_?")    85     86         return compile_definition(name)    87     88     def set(self, name, value, count=None, raw=0):    89     90         """    91         Set the configuration parameter having the given 'name' with the given    92         'value', limiting the number of appropriately named parameters changed    93         to 'count', if specified.    94     95         If the configuration parameter of the given 'name' does not exist,    96         insert such a parameter at the end of the file.    97     98         If the optional 'raw' parameter is specified and set to a true value,    99         the provided 'value' is inserted directly into the configuration file.   100         """   101    102         if not self.replace(name, value, count, raw):   103             self.insert(name, value, raw)   104    105     def replace(self, name, value, count=None, raw=0):   106    107         """   108         Replace configuration parameters having the given 'name' with the given   109         'value', limiting the number of appropriately named parameters changed   110         to 'count', if specified.   111    112         If the optional 'raw' parameter is specified and set to a true value,   113         the provided 'value' is inserted directly into the configuration file.   114    115         Return the number of substitutions made.   116         """   117    118         if raw:   119             substitution = r"\1\2 %s" % value   120         else:   121             substitution = r"\1\2 %r" % value   122    123         pattern = self.get_pattern(name)   124    125         if count is None:   126             self.content, n = pattern.subn(substitution, self.content)   127         else:   128             self.content, n = pattern.subn(substitution, self.content, count=count)   129    130         return n   131    132     def insert(self, name, value, raw=0):   133    134         """   135         Insert the configuration parameter having the given 'name' and 'value'.   136    137         If the optional 'raw' parameter is specified and set to a true value,   138         the provided 'value' is inserted directly into the configuration file.   139         """   140    141         if raw:   142             insertion = "\n    %s = %s\n"   143         else:   144             insertion = "\n    %s = %r\n"   145    146         self.content += insertion % (name, value)   147    148     def close(self):   149    150         "Close the file, writing the content."   151    152         writefile(self.filename, self.content)   153    154 class Installation:   155    156     "A class for installing and initialising MoinMoin."   157    158     # NOTE: Need to detect Web server user.   159    160     web_user = "www-data"   161     web_group = "www-data"   162    163     # MoinMoin resources.   164    165     theme_master = "modernized"   166     extra_theme_css_files = ["SlideShow.css"]   167    168     def __init__(self, moin_distribution, prefix, web_app_dir, web_site_dir,   169         common_dir, url_path, superuser, site_name, front_page_name,   170         theme_default=None):   171    172         """   173         Initialise a Wiki installation using the following:   174    175           * moin_distribution - the directory containing a MoinMoin source   176                                 distribution   177           * prefix            - the installation prefix (equivalent to /usr)   178           * web_app_dir       - the directory where Web applications and scripts   179                                 reside (such as /home/www-user/cgi-bin)   180           * web_site_dir      - the directory where Web site definitions reside   181                                 (such as /etc/apache2/sites-available)   182           * common_dir        - the directory where the Wiki configuration,   183                                 resources and instance will reside (such as   184                                 /home/www-user/mywiki)   185           * url_path          - the URL path at which the Wiki will be made   186                                 available (such as / or /mywiki)   187           * superuser         - the name of the site's superuser (such as   188                                 "AdminUser")   189           * site_name         - the name of the site (such as "My Wiki")   190           * front_page_name   - the front page name for the site (such as   191                                 "FrontPage" or a specific name for the site)   192           * theme_default     - optional: the default theme (such as modern)   193         """   194    195         self.moin_distribution = moin_distribution   196         self.superuser = superuser   197         self.site_name = site_name   198         self.front_page_name = front_page_name   199         self.theme_default = theme_default   200    201         # NOTE: Support the detection of the Apache sites directory.   202    203         self.prefix, self.web_app_dir, self.web_site_dir, self.common_dir = \   204             map(abspath, (prefix, web_app_dir, web_site_dir, common_dir))   205    206         # Strip any trailing "/" from the URL path.   207    208         if url_path != "/" and url_path.endswith("/"):   209             self.url_path = url_path[:-1]   210         else:   211             self.url_path = url_path   212    213         # Define and create specific directories.   214    215         self.conf_dir = join(self.common_dir, "conf")   216         self.instance_dir = join(self.common_dir, "wikidata")   217    218         # Define useful directories.   219    220         self.prefix_site_packages = join(self.prefix, "lib", "python%s.%s" % sys.version_info[:2], "site-packages")   221    222         # Find the version.   223    224         self.moin_version = self.get_moin_version()   225    226         # The static resources reside in different locations depending on the   227         # version of MoinMoin. Moreover, these resources may end up in a   228         # published directory for 1.8 installations where the Web server cannot   229         # be instructed to fetch the content from outside certain designated   230         # locations.   231    232         # 1.9: moin/lib/python2.x/site-packages/MoinMoin/web/static/htdocs   233    234         if self.moin_version.startswith("1.9"):   235             self.htdocs_dir = self.htdocs_dir_source = join(self.prefix_site_packages, "MoinMoin", "web", "static", "htdocs")   236    237         # 1.8: moin/share/moin/htdocs (optionally copied to a Web directory)   238    239         else:   240             self.htdocs_dir_source = join(self.instance_dir, "share", "moin", "htdocs")   241    242             if self.limited_hosting():   243                 self.htdocs_dir = join(self.web_app_dir, self.get_static_identifier())   244             else:   245                 self.htdocs_dir = self.htdocs_dir_source   246    247     def get_moin_version(self):   248    249         "Inspect the MoinMoin package information, returning the version."   250    251         this_dir = os.getcwd()   252         os.chdir(self.moin_distribution)   253    254         try:   255             try:   256                 f = open("PKG-INFO")   257                 try:   258                     for line in f.xreadlines():   259                         columns = line.split()   260                         if columns[0] == "Version:":   261                             return columns[1]   262    263                     return None   264    265                 finally:   266                     f.close()   267    268             except IOError:   269                 f = os.popen("%s -c 'from MoinMoin.version import release; print release'" % sys.executable)   270                 try:   271                     return f.read()   272                 finally:   273                     f.close()   274         finally:   275             os.chdir(this_dir)   276    277     def get_static_identifier(self):   278    279         "Return the static URL/directory identifier for the Wiki."   280    281         return "moin_static%s" % self.moin_version.replace(".", "")   282    283     def limited_hosting(self):   284    285         "Return whether limited Web hosting is being used."   286    287         return self.web_site_dir == self.web_app_dir   288    289     def ensure_directories(self):   290    291         "Make sure that all the directories are available."   292    293         for d in (self.conf_dir, self.instance_dir, self.web_app_dir, self.web_site_dir):   294             if not exists(d):   295                 os.makedirs(d)   296    297     # Main methods.   298    299     def setup(self):   300    301         "Set up the installation."   302    303         self.ensure_directories()   304         self.install_moin()   305         self._setup_wiki()   306    307     def setup_wiki(self):   308    309         "Set up a Wiki without installing MoinMoin."   310    311         self.ensure_directories()   312         self.install_moin(data_only=1)   313         self._setup_wiki()   314    315     def _setup_wiki(self):   316    317         "Set up a Wiki without installing MoinMoin."   318    319         self.install_data()   320         self.configure_moin()   321         self.edit_moin_script()   322         self.edit_moin_web_script()   323         self.add_superuser()   324         self.make_site_files()   325         self.make_post_install_script()   326    327     def install_moin(self, data_only=0):   328    329         "Enter the distribution directory and run the setup script."   330    331         # NOTE: Possibly check for an existing installation and skip repeated   332         # NOTE: installation attempts.   333    334         this_dir = os.getcwd()   335         os.chdir(self.moin_distribution)   336    337         log_filename = "install-%s.log" % split(self.common_dir)[-1]   338    339         status("Installing MoinMoin in %s..." % self.prefix)   340    341         if data_only:   342             install_cmd = "install_data"   343             options = "--install-dir='%s'" % self.instance_dir   344         else:   345             install_cmd = "install"   346             options = "--prefix='%s' --install-data='%s' --record='%s'" % (self.prefix, self.instance_dir, log_filename)   347    348         os.system("python setup.py --quiet %s %s --force" % (install_cmd, options))   349    350         os.chdir(this_dir)   351    352     def install_data(self):   353    354         "Install Wiki data."   355    356         # The default wikiconfig assumes data and underlay in the same directory.   357    358         status("Installing data and underlay in %s..." % self.conf_dir)   359    360         for d in ("data", "underlay"):   361             source = join(self.moin_distribution, "wiki", d)   362             source_tar = source + os.path.extsep + "tar"   363             d_tar = source + os.path.extsep + "tar"   364    365             if os.path.exists(source):   366                 shutil.copytree(source, join(self.conf_dir, d))   367             elif os.path.exists(source_tar):   368                 shutil.copy(source_tar, self.conf_dir)   369                 os.system("tar xf %s -C %s" % (d_tar, self.conf_dir))   370             else:   371                 status("Could not copy %s into installed Wiki." % d)   372    373         # Copy static Web data if appropriate.   374    375         if not self.moin_version.startswith("1.9") and self.limited_hosting():   376    377             if not exists(self.htdocs_dir):   378                 os.mkdir(self.htdocs_dir)   379    380             for item in os.listdir(self.htdocs_dir_source):   381                 path = join(self.htdocs_dir_source, item)   382                 if isdir(path):   383                     shutil.copytree(path, join(self.htdocs_dir, item))   384                 else:   385                     shutil.copy(path, join(self.htdocs_dir, item))   386    387     def configure_moin(self):   388    389         "Edit the Wiki configuration file."   390    391         # NOTE: Single Wiki only so far.   392    393         # Static URLs seem to be different in MoinMoin 1.9.x.   394         # For earlier versions, reserve URL space alongside the Wiki.   395         # NOTE: MoinMoin usually uses an apparently common URL space associated   396         # NOTE: with the version, but more specific locations are probably   397         # NOTE: acceptable if less efficient.   398    399         if self.moin_version.startswith("1.9"):   400             self.static_url_path = self.url_path   401             url_prefix_static = "%r + url_prefix_static" % self.static_url_path   402         else:   403             # Add the static identifier to the URL path. For example:   404             # /         -> /moin_static187   405             # /hgwiki   -> /hgwiki/moin_static187   406    407             self.static_url_path = self.url_path + (self.url_path != "/" and "/" or "") + self.get_static_identifier()   408             url_prefix_static = "%r" % self.static_url_path   409    410         # Copy the Wiki configuration file from the distribution.   411    412         wikiconfig_py = join(self.conf_dir, "wikiconfig.py")   413         shutil.copyfile(join(self.moin_distribution, "wiki", "config", "wikiconfig.py"), wikiconfig_py)   414    415         status("Editing configuration from %s..." % wikiconfig_py)   416    417         # Edit the Wiki configuration file.   418    419         wikiconfig = Configuration(wikiconfig_py)   420    421         try:   422             wikiconfig.set("url_prefix_static", url_prefix_static, raw=1)   423             wikiconfig.set("superuser", [self.superuser])   424             wikiconfig.set("acl_rights_before", u"%s:read,write,delete,revert,admin" % self.superuser)   425    426             if not self.moin_version.startswith("1.9"):   427                 data_dir = join(self.conf_dir, "data")   428                 data_underlay_dir = join(self.conf_dir, "underlay")   429    430                 wikiconfig.set("data_dir", data_dir)   431                 wikiconfig.set("data_underlay_dir", data_underlay_dir)   432    433             self._configure_moin(wikiconfig)   434    435         finally:   436             wikiconfig.close()   437    438     def _configure_moin(self, wikiconfig):   439    440         """   441         Configure Moin, accessing the configuration file using 'wikiconfig'.   442         """   443    444         wikiconfig.set("site_name", self.site_name)   445         wikiconfig.set("page_front_page", self.front_page_name, count=1)   446    447         if self.theme_default is not None:   448             wikiconfig.set("theme_default", self.theme_default)   449    450     def edit_moin_script(self):   451    452         "Edit the moin script."   453    454         moin_script = join(self.prefix, "bin", "moin")   455    456         status("Editing moin script at %s..." % moin_script)   457    458         s = readfile(moin_script)   459         s = s.replace("#import sys", "import sys\nsys.path.insert(0, %r)" % self.prefix_site_packages)   460    461         writefile(moin_script, s)   462    463     def edit_moin_web_script(self):   464    465         "Edit and install CGI script."   466    467         # NOTE: CGI only so far.   468         # NOTE: Permissions should be checked.   469    470         if self.moin_version.startswith("1.9"):   471             moin_cgi = join(self.instance_dir, "share", "moin", "server", "moin.fcgi")   472         else:   473             moin_cgi = join(self.instance_dir, "share", "moin", "server", "moin.cgi")   474    475         moin_cgi_installed = join(self.web_app_dir, "moin.cgi")   476    477         status("Editing moin.cgi script from %s..." % moin_cgi)   478    479         s = readfile(moin_cgi)   480         s = moin_cgi_prefix.sub("sys.path.insert(0, %r)" % self.prefix_site_packages, s)   481         s = moin_cgi_wikiconfig.sub("sys.path.insert(0, %r)" % self.conf_dir, s)   482    483         # Handle differences in script names when using limited hosting with   484         # URL rewriting.   485    486         if self.limited_hosting():   487             if self.moin_version.startswith("1.9"):   488                 s = moin_cgi_fix_script_name.sub(r"\1\2 %r" % self.url_path, s)   489                 s = moin_cgi_force_cgi.sub(r"\1", s)   490             else:   491                 s = moin_cgi_properties.sub(r"\1\2 %r" % {"script_name" : self.url_path}, s)   492    493         writefile(moin_cgi_installed, s)   494         os.system("chmod a+rx '%s'" % moin_cgi_installed)   495    496     def add_superuser(self):   497    498         "Add the superuser account."   499    500         moin_script = join(self.prefix, "bin", "moin")   501    502         print "Creating superuser", self.superuser, "using..."   503         email = raw_input("E-mail address: ")   504         password = getpass("Password: ")   505    506         path = os.environ.get("PYTHONPATH", "")   507    508         if path:   509             os.environ["PYTHONPATH"] = path + ":" + self.conf_dir   510         else:   511             os.environ["PYTHONPATH"] = self.conf_dir   512    513         os.system(moin_script + " account create --name='%s' --email='%s' --password='%s'" % (self.superuser, email, password))   514    515         if path:   516             os.environ["PYTHONPATH"] = path   517         else:   518             del os.environ["PYTHONPATH"]   519    520     def make_site_files(self):   521    522         "Make the Apache site files."   523    524         # NOTE: Using local namespace for substitution.   525    526         # Where the site definitions and applications directories are different,   527         # use a normal site definition.   528    529         if not self.limited_hosting():   530    531             site_def = join(self.web_site_dir, self.site_name)   532    533             s = apache_site % self.__dict__   534    535             if not self.moin_version.startswith("1.9"):   536                 s += apache_site_extra_moin18 % self.__dict__   537    538         # Otherwise, use an .htaccess file.   539    540         else:   541             site_def = join(self.web_site_dir, ".htaccess")   542    543             s = apache_htaccess_combined_mod_rewrite % self.__dict__   544    545         status("Writing Apache site definitions to %s..." % site_def)   546    547         writefile(site_def, s)   548    549     def make_post_install_script(self):   550    551         "Write a post-install script with additional actions."   552    553         this_user = os.environ["USER"]   554         postinst_script = "moinsetup-post.sh"   555    556         s = "#!/bin/sh\n"   557    558         for d in ("data", "underlay"):   559             s += "chown -R %s.%s '%s'\n" % (this_user, self.web_group, join(self.conf_dir, d))   560             s += "chmod -R g+w '%s'\n" % join(self.conf_dir, d)   561    562         if not self.moin_version.startswith("1.9"):   563             s += "chown -R %s.%s '%s'\n" % (this_user, self.web_group, self.htdocs_dir)   564    565         writefile(postinst_script, s)   566         os.chmod(postinst_script, 0755)   567         note("Run %s as root to set file ownership and permissions." % postinst_script)   568    569     # Accessory methods.   570    571     def reconfigure_moin(self, name=None, value=None, raw=0):   572    573         "Edit the installed Wiki configuration file."   574    575         wikiconfig_py = join(self.conf_dir, "wikiconfig.py")   576    577         status("Editing configuration from %s..." % wikiconfig_py)   578    579         wikiconfig = Configuration(wikiconfig_py)   580    581         try:   582             # Perform default configuration.   583    584             if name is None and value is None:   585                 self._configure_moin(wikiconfig)   586             else:   587                 wikiconfig.set(name, value, raw=raw)   588    589         finally:   590             wikiconfig.close()   591    592     def install_theme(self, theme_dir):   593    594         "Install Wiki theme provided in the given 'theme_dir'."   595    596         theme_dir = normpath(theme_dir)   597         theme_name = split(theme_dir)[-1]   598         theme_module = join(theme_dir, theme_name + extsep + "py")   599    600         data_dir = join(self.conf_dir, "data")   601         plugin_theme_dir = join(data_dir, "plugin", "theme")   602    603         # Copy the theme module.   604    605         status("Copying theme module to %s..." % plugin_theme_dir)   606    607         shutil.copy(theme_module, plugin_theme_dir)   608    609         # Copy the resources.   610    611         resources_dir = join(self.htdocs_dir, theme_name)   612    613         status("Copying theme resources to %s..." % resources_dir)   614    615         for d in ("css", "img"):   616             target_dir = join(resources_dir, d)   617             if exists(target_dir):   618                 status("Replacing %s..." % target_dir)   619                 shutil.rmtree(target_dir)   620             shutil.copytree(join(theme_dir, d), target_dir)   621    622         # Copy additional resources from other themes.   623    624         resources_source_dir = join(self.htdocs_dir, self.theme_master)   625         target_dir = join(resources_dir, "css")   626    627         status("Copying resources from %s..." % resources_source_dir)   628    629         for css_file in self.extra_theme_css_files:   630             css_file_path = join(resources_source_dir, "css", css_file)   631             if exists(css_file_path):   632                 shutil.copy(css_file_path, target_dir)   633    634 # Command line option syntax.   635    636 syntax_description = "<argument> ... [ --method=METHOD [ <method-argument> ... ] ]"   637    638 # Main program.   639    640 if __name__ == "__main__":   641     import sys, cmdsyntax   642    643     # Check the command syntax.   644    645     syntax = cmdsyntax.Syntax(syntax_description)   646     try:   647         matches = syntax.get_args(sys.argv[1:])   648         args = matches[0]   649    650         # Obtain as many arguments as needed for the configuration.   651    652         arguments = args["argument"]   653         method_arguments = args.get("method-argument", [])   654    655         # Attempt to initialise the configuration.   656    657         installation = Installation(*arguments)   658    659     except (IndexError, TypeError):   660         print "Syntax:"   661         print sys.argv[0], syntax_description   662         print   663         print "Arguments:"   664         print Installation.__init__.__doc__   665         sys.exit(1)   666    667     # Obtain and perform the method.   668    669     if args.has_key("method"):   670         method = getattr(installation, args["method"])   671     else:   672         method = installation.setup   673    674     method(*method_arguments)   675    676 # vim: tabstop=4 expandtab shiftwidth=4