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