moinsetup

moinsetup.py

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