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_scripts() 278 self.add_superuser() 279 self.make_site_files() 280 self.make_post_install_script() 281 282 def install_moin(self, data_only=0): 283 284 "Enter the distribution directory and run the setup script." 285 286 # NOTE: Possibly check for an existing installation and skip repeated 287 # NOTE: installation attempts. 288 289 this_dir = os.getcwd() 290 os.chdir(self.moin_distribution) 291 292 log_filename = "install-%s.log" % split(self.common_dir)[-1] 293 294 status("Installing MoinMoin in %s..." % self.prefix) 295 296 if data_only: 297 install_cmd = "install_data" 298 options = "--install-dir='%s'" % self.instance_dir 299 else: 300 install_cmd = "install" 301 options = "--prefix='%s' --install-data='%s' --record='%s'" % (self.prefix, self.instance_dir, log_filename) 302 303 os.system("python setup.py --quiet %s %s --force" % (install_cmd, options)) 304 305 os.chdir(this_dir) 306 307 def install_data(self): 308 309 "Install Wiki data." 310 311 # The default wikiconfig assumes data and underlay in the same directory. 312 313 status("Installing data and underlay in %s..." % self.conf_dir) 314 315 for d in ("data", "underlay"): 316 source = join(self.moin_distribution, "wiki", d) 317 source_tar = source + os.path.extsep + "tar" 318 d_tar = source + os.path.extsep + "tar" 319 320 if os.path.exists(source): 321 shutil.copytree(source, join(self.conf_dir, d)) 322 elif os.path.exists(source_tar): 323 shutil.copy(source_tar, self.conf_dir) 324 os.system("tar xf %s -C %s" % (d_tar, self.conf_dir)) 325 else: 326 status("Could not copy %s into installed Wiki." % d) 327 328 def configure_moin(self): 329 330 "Edit the Wiki configuration file." 331 332 # NOTE: Single Wiki only so far. 333 334 # Static URLs seem to be different in MoinMoin 1.9.x. 335 # For earlier versions, reserve URL space alongside the Wiki. 336 # NOTE: MoinMoin usually uses an apparently common URL space associated 337 # NOTE: with the version, but more specific locations are probably 338 # NOTE: acceptable if less efficient. 339 340 if self.moin_version.startswith("1.9"): 341 self.static_url_path = self.url_path 342 url_prefix_static = "%r + url_prefix_static" % self.static_url_path 343 else: 344 self.static_url_path = self.url_path + "-static" 345 url_prefix_static = "%r" % self.static_url_path 346 347 # Copy the Wiki configuration file from the distribution. 348 349 wikiconfig_py = join(self.conf_dir, "wikiconfig.py") 350 shutil.copyfile(join(self.moin_distribution, "wiki", "config", "wikiconfig.py"), wikiconfig_py) 351 352 status("Editing configuration from %s..." % wikiconfig_py) 353 354 # Edit the Wiki configuration file. 355 356 wikiconfig = Configuration(wikiconfig_py) 357 358 try: 359 wikiconfig.set("url_prefix_static", url_prefix_static, raw=1) 360 wikiconfig.set("superuser", [self.superuser]) 361 wikiconfig.set("acl_rights_before", u"%s:read,write,delete,revert,admin" % self.superuser) 362 363 if not self.moin_version.startswith("1.9"): 364 data_dir = join(self.conf_dir, "data") 365 data_underlay_dir = join(self.conf_dir, "underlay") 366 367 wikiconfig.set("data_dir", data_dir) 368 wikiconfig.set("data_underlay_dir", data_underlay_dir) 369 370 self._configure_moin(wikiconfig) 371 372 finally: 373 wikiconfig.close() 374 375 def _configure_moin(self, wikiconfig): 376 377 """ 378 Configure Moin, accessing the configuration file using 'wikiconfig'. 379 """ 380 381 wikiconfig.set("site_name", self.site_name) 382 wikiconfig.set("page_front_page", self.front_page_name, count=1) 383 384 if self.theme_default is not None: 385 wikiconfig.set("theme_default", self.theme_default) 386 387 def edit_moin_scripts(self): 388 389 "Edit the moin script and the CGI script." 390 391 moin_script = join(self.prefix, "bin", "moin") 392 prefix_site_packages = join(self.prefix, "lib", "python%s.%s" % sys.version_info[:2], "site-packages") 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)" % prefix_site_packages) 398 399 writefile(moin_script, s) 400 401 # Edit and install CGI script. 402 # NOTE: CGI only so far. 403 # NOTE: Permissions should be checked. 404 405 moin_cgi = join(self.instance_dir, "share", "moin", "server", "moin.cgi") 406 moin_cgi_installed = join(self.web_app_dir, "moin.cgi") 407 408 status("Editing moin.cgi script from %s..." % moin_cgi) 409 410 s = readfile(moin_cgi) 411 s = moin_cgi_prefix.sub("sys.path.insert(0, %r)" % prefix_site_packages, s) 412 s = moin_cgi_wikiconfig.sub("sys.path.insert(0, %r)" % self.conf_dir, s) 413 414 writefile(moin_cgi_installed, s) 415 os.system("chmod a+rx '%s'" % moin_cgi_installed) 416 417 def add_superuser(self): 418 419 "Add the superuser account." 420 421 moin_script = join(self.prefix, "bin", "moin") 422 423 print "Creating superuser", self.superuser, "using..." 424 email = raw_input("E-mail address: ") 425 password = getpass("Password: ") 426 427 path = os.environ.get("PYTHONPATH", "") 428 429 if path: 430 os.environ["PYTHONPATH"] = path + ":" + self.conf_dir 431 else: 432 os.environ["PYTHONPATH"] = self.conf_dir 433 434 os.system(moin_script + " account create --name='%s' --email='%s' --password='%s'" % (self.superuser, email, password)) 435 436 if path: 437 os.environ["PYTHONPATH"] = path 438 else: 439 del os.environ["PYTHONPATH"] 440 441 def make_site_files(self): 442 443 "Make the Apache site files." 444 445 # NOTE: Using local namespace for substitution. 446 447 site_def = join(self.web_site_dir, self.site_name) 448 site_def_private = join(self.web_site_dir, "%s-private" % self.site_name) 449 450 status("Writing Apache site definitions to %s and %s..." % (site_def, site_def_private)) 451 452 s = apache_site % self.__dict__ 453 454 if not self.moin_version.startswith("1.9"): 455 s += apache_site_extra_moin18 % self.__dict__ 456 457 writefile(site_def, s) 458 459 def make_post_install_script(self): 460 461 "Write a post-install script with additional actions." 462 463 this_user = os.environ["USER"] 464 postinst_script = "moinsetup-post.sh" 465 466 s = "#!/bin/sh\n" 467 468 for d in ("data", "underlay"): 469 s += "chown -R %s.%s '%s'\n" % (this_user, self.web_group, join(self.conf_dir, d)) 470 s += "chmod -R g+w '%s'\n" % join(self.conf_dir, d) 471 472 if not self.moin_version.startswith("1.9"): 473 s += "chown -R %s.%s '%s'\n" % (this_user, self.web_group, self.htdocs_dir) 474 475 writefile(postinst_script, s) 476 os.chmod(postinst_script, 0755) 477 note("Run %s as root to set file ownership and permissions." % postinst_script) 478 479 # Accessory methods. 480 481 def reconfigure_moin(self, name=None, value=None, raw=0): 482 483 "Edit the installed Wiki configuration file." 484 485 wikiconfig_py = join(self.conf_dir, "wikiconfig.py") 486 487 status("Editing configuration from %s..." % wikiconfig_py) 488 489 wikiconfig = Configuration(wikiconfig_py) 490 491 try: 492 # Perform default configuration. 493 494 if name is None and value is None: 495 self._configure_moin(wikiconfig) 496 else: 497 wikiconfig.set(name, value, raw=raw) 498 499 finally: 500 wikiconfig.close() 501 502 def install_theme(self, theme_dir): 503 504 "Install Wiki theme provided in the given 'theme_dir'." 505 506 theme_dir = normpath(theme_dir) 507 theme_name = split(theme_dir)[-1] 508 theme_module = join(theme_dir, theme_name + extsep + "py") 509 510 data_dir = join(self.conf_dir, "data") 511 plugin_theme_dir = join(data_dir, "plugin", "theme") 512 513 # Copy the theme module. 514 515 status("Copying theme module to %s..." % plugin_theme_dir) 516 517 shutil.copy(theme_module, plugin_theme_dir) 518 519 # Copy the resources. 520 521 resources_dir = join(self.htdocs_dir, theme_name) 522 523 status("Copying theme resources to %s..." % resources_dir) 524 525 for d in ("css", "img"): 526 target_dir = join(resources_dir, d) 527 if exists(target_dir): 528 status("Replacing %s..." % target_dir) 529 shutil.rmtree(target_dir) 530 shutil.copytree(join(theme_dir, d), target_dir) 531 532 # Copy additional resources from other themes. 533 534 resources_source_dir = join(self.htdocs_dir, self.theme_master) 535 target_dir = join(resources_dir, "css") 536 537 status("Copying resources from %s..." % resources_source_dir) 538 539 for css_file in self.extra_theme_css_files: 540 css_file_path = join(resources_source_dir, "css", css_file) 541 if exists(css_file_path): 542 shutil.copy(css_file_path, target_dir) 543 544 # Command line option syntax. 545 546 syntax_description = "<argument> ... [ --method=METHOD [ <method-argument> ... ] ]" 547 548 # Main program. 549 550 if __name__ == "__main__": 551 import sys, cmdsyntax 552 553 # Check the command syntax. 554 555 syntax = cmdsyntax.Syntax(syntax_description) 556 try: 557 matches = syntax.get_args(sys.argv[1:]) 558 args = matches[0] 559 560 # Obtain as many arguments as needed for the configuration. 561 562 arguments = args["argument"] 563 method_arguments = args.get("method-argument", []) 564 565 # Attempt to initialise the configuration. 566 567 installation = Installation(*arguments) 568 569 except (IndexError, TypeError): 570 print "Syntax:" 571 print sys.argv[0], syntax_description 572 print 573 print "Arguments:" 574 print Installation.__init__.__doc__ 575 sys.exit(1) 576 577 # Obtain and perform the method. 578 579 if args.has_key("method"): 580 method = getattr(installation, args["method"]) 581 else: 582 method = installation.setup 583 584 method(*method_arguments) 585 586 # vim: tabstop=4 expandtab shiftwidth=4