#!/usr/bin/env python import getpass import imp import os import pkg_resources import platform import re import shutil import sys import textwrap import warnings from optparse import OptionGroup, OptionParser from random import choice DOCS_BASE = "http://www.review-board.org/docs/manual/dev/" # See if GTK is a possibility. try: # Disable the gtk warning we might hit. This is because pygtk will # yell if it can't access X. warnings.simplefilter("ignore") import pygtk pygtk.require('2.0') import gtk can_use_gtk = True gtk.init_check() except: can_use_gtk = False # Reset the warnings so we don't ignore everything. warnings.resetwarnings() VERSION = "0.1" DEBUG = False # Global State options = None args = None site = None ui = None class Dependencies(object): memcached_modules = ["cmemcache", "memcache"] sqlite_modules = ["pysqlite2", "sqlite3"] mysql_modules = ["MySQLdb"] postgresql_modules = ["psycopg2"] cache_dependency_info = { 'required': False, 'title': 'Server Cache', 'dependencies': [ ("memcached", memcached_modules), ], } db_dependency_info = { 'required': True, 'title': 'Databases', 'dependencies': [ ("sqlite3", sqlite_modules), ("MySQL", mysql_modules), ("PostgreSQL", postgresql_modules) ], } @classmethod def get_support_memcached(cls): return cls.has_modules(cls.memcached_modules) @classmethod def get_support_mysql(cls): return cls.has_modules(cls.mysql_modules) @classmethod def get_support_postgresql(cls): return cls.has_modules(cls.postgresql_modules) @classmethod def get_support_sqlite(cls): return cls.has_modules(cls.sqlite_modules) @classmethod def get_missing(cls): fatal = False missing_groups = [] for dep_info in [cls.cache_dependency_info, cls.db_dependency_info]: missing_deps = [] for desc, modules in dep_info['dependencies']: if not cls.has_modules(modules): missing_deps.append("%s (%s)" % (desc, ", ".join(modules))) if missing_deps: if (dep_info['required'] and len(missing_deps) == len(dep_info['dependencies'])): fatal = True text = "%s (required)" % dep_info['title'] else: text = "%s (optional)" % dep_info['title'] missing_groups.append({ 'title': text, 'dependencies': missing_deps, }) return fatal, missing_groups @classmethod def has_modules(cls, names): """ Returns whether or not one of the specified modules is installed. """ for name in names: try: __import__(name) return True except ImportError: continue return False class Site(object): def __init__(self, install_dir): self.install_dir = install_dir self.abs_install_dir = os.path.abspath(install_dir) self.site_id = \ os.path.basename(install_dir).replace(" ", "_").replace(".", "_") # State saved during installation self.domain_name = None self.site_root = None self.media_url = None self.db_type = None self.db_name = None self.db_host = None self.db_port = None self.db_user = None self.db_pass = None self.cache_type = None self.cache_info = None self.web_server_type = None self.python_loader = None self.admin_user = None self.admin_password = None def rebuild_site_directory(self): """ Rebuilds the site hierarchy. """ htdocs_dir = os.path.join(self.install_dir, "htdocs") media_dir = os.path.join(htdocs_dir, "media") self.mkdir(self.install_dir) self.mkdir(os.path.join(self.install_dir, "logs")) self.mkdir(os.path.join(self.install_dir, "conf")) self.mkdir(os.path.join(self.install_dir, "tmp")) os.chmod(os.path.join(self.install_dir, "tmp"), 0777) if self.db_type == "sqlite3": self.mkdir(os.path.join(self.install_dir, "db")) self.mkdir(htdocs_dir) self.mkdir(media_dir) # TODO: In the future, support changing ownership of these # directories. self.mkdir(os.path.join(media_dir, "uploaded")) self.mkdir(os.path.join(media_dir, "uploaded", "images")) self.link_pkg_dir("reviewboard", "htdocs/errordocs", os.path.join("htdocs", "errordocs")) media_base = os.path.join("htdocs", "media") rb_djblets_src = "htdocs/media/djblets" rb_djblets_dest = os.path.join(media_base, "djblets") for media_dir in ["admin", "rb"]: path = os.path.join(media_base, media_dir) self.link_pkg_dir("reviewboard", "htdocs/media/%s" % media_dir, os.path.join(media_base, media_dir)) # Link from Djblets if available. if pkg_resources.resource_exists("djblets", "media"): self.link_pkg_dir("djblets", "media", rb_djblets_dest) elif pkg_resources.resource_exists("reviewboard", rb_djblets_src): self.link_pkg_dir("reviewboard", rb_djblets_src, rb_djblets_dest) else: ui.error("Unable to find the Djblets media path. Make sure " "Djblets is installed and try this again.") # Generate a .htaccess file that enables compression and # never expires various file types. path = os.path.join(self.install_dir, media_base, ".htaccess") fp = open(path, "w") fp.write('\n') fp.write(' \n') fp.write(' ExpiresActive on\n') fp.write(' ExpiresDefault "access plus 1 year"\n') fp.write(' \n') fp.write('\n') fp.write('\n') fp.write('\n') for mimetype in ["text/html", "text/plain", "text/xml", "text/css", "text/javascript", "application/javascript", "application/x-javascript"]: fp.write(" AddOutputFilterByType DEFLATE %s\n" % mimetype) fp.write('\n') fp.close() def setup_settings(self): # Make sure that we have our settings_local.py in our path for when # we need to run manager commands. sys.path.insert(0, os.path.join(self.abs_install_dir, "conf")) def generate_config_files(self): web_conf_filename = "" enable_fastcgi = False if self.web_server_type == "apache": if self.python_loader == "modpython": web_conf_filename = "apache-modpython.conf" elif self.python_loader == "fastcgi": web_conf_filename = "apache-fastcgi.conf" enable_fastcgi = True else: # Should never be reached. assert False elif self.web_server_type == "lighttpd": web_conf_filename = "lighttpd.conf" enable_fastcgi = True else: # Should never be reached. assert False conf_dir = os.path.join(self.install_dir, "conf") htdocs_dir = os.path.join(self.install_dir, "htdocs") self.process_template("contrib/conf/%s.in" % web_conf_filename, os.path.join(conf_dir, web_conf_filename)) self.process_template("contrib/conf/search-cron.conf.in", os.path.join(conf_dir, "search-cron.conf")) if enable_fastcgi: fcgi_filename = os.path.join(htdocs_dir, "reviewboard.fcgi") self.process_template("contrib/conf/reviewboard.fcgi.in", fcgi_filename) os.chmod(fcgi_filename, 0755) # Generate a secret key based on Django's code. secret_key = ''.join([ choice('abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)') for i in range(50) ]) # Generate the settings_local.py fp = open(os.path.join(conf_dir, "settings_local.py"), "w") fp.write("# Site-specific configuration settings for Review Board\n") fp.write("# Definitions of these settings can be found at\n") fp.write("# http://docs.djangoproject.com/en/dev/ref/settings/\n") fp.write("\n") fp.write("# Database configuration\n") db_engine = self.db_type if db_engine == "postgresql": db_engine = "postgresql_psycopg2" fp.write("DATABASE_ENGINE = '%s'\n" % db_engine) fp.write("DATABASE_NAME = '%s'\n" % self.db_name.replace("\\", "\\\\")) if self.db_type != "sqlite3": fp.write("DATABASE_USER = '%s'\n" % (self.db_user or "")) fp.write("DATABASE_PASSWORD = '%s'\n" % (self.db_pass or "")) fp.write("DATABASE_HOST = '%s'\n" % (self.db_host or "")) fp.write("DATABASE_PORT = '%s'\n" % (self.db_port or "")) fp.write("\n") fp.write("# Unique secret key. Don't share this with anybody.\n") fp.write("SECRET_KEY = '%s'\n" % secret_key) fp.write("\n") fp.write("# Cache backend settings.\n") fp.write("CACHE_BACKEND = '%s'\n" % self.cache_info) fp.write("\n") fp.write("# Extra site information.\n") fp.write("SITE_ID = 1\n") fp.write("SITE_ROOT = '%s'\n" % self.site_root) fp.write("FORCE_SCRIPT_NAME = ''\n") fp.write("DEBUG = False\n") fp.close() self.setup_settings() def sync_database(self): """ Synchronizes the database. """ self.run_manage_command("syncdb", ["--noinput"]) def migrate_database(self): """ Performs a database migration. """ self.run_manage_command("evolve", ["--noinput", "--execute"]) def create_admin_user(self): """ Creates an administrator user account. """ cwd = os.getcwd() os.chdir(self.abs_install_dir) from django.contrib.auth.models import User User.objects.create_superuser(self.admin_user, self.admin_email, self.admin_password) os.chdir(cwd) def run_manage_command(self, cmd, params=None): cwd = os.getcwd() os.chdir(self.abs_install_dir) try: from django.core.management import execute_manager from reviewboard.admin.migration import fix_django_evolution_issues import reviewboard.settings if not params: params = [] if DEBUG: params.append("--verbosity=0") fix_django_evolution_issues(reviewboard.settings) execute_manager(reviewboard.settings, [__file__, cmd] + params) except ImportError, e: ui.error("Unable to execute the manager command %s: %s" % (cmd, e)) os.chdir(cwd) def mkdir(self, dirname): """ Creates a directory, but only if it doesn't already exist. """ if not os.path.exists(dirname): os.mkdir(dirname) def link_pkg_dir(self, pkgname, src_path, dest_path, replace=True): src_dir = pkg_resources.resource_filename(pkgname, src_path) dest_dir = os.path.join(self.install_dir, dest_path) if os.path.islink(dest_dir) and not os.path.exists(dest_dir): os.unlink(dest_dir) if os.path.exists(dest_dir): if not replace: return if os.path.islink(dest_dir): os.unlink(dest_dir) else: shutil.rmtree(dest_dir) if options.copy_media: shutil.copytree(src_dir, dest_dir) else: os.symlink(src_dir, dest_dir) def process_template(self, template_path, dest_filename): """ Generates a file from a template. """ domain_name_escaped = self.domain_name.replace(".", "\\.") template = pkg_resources.resource_string("reviewboard", template_path) sitedir = os.path.abspath(self.install_dir).replace("\\", "/") # Check if this is a .exe. if (hasattr(sys, "frozen") or # new py2exe hasattr(sys, "importers") or # new py2exe imp.is_frozen("__main__")): # tools/freeze rbsite_path = sys.executable else: rbsite_path = '"%s" "%s"' % (sys.executable, sys.argv[0]) data = { 'rbsite': rbsite_path, 'sitedir': sitedir, 'sitedomain': self.domain_name, 'sitedomain_escaped': domain_name_escaped, 'siteid': self.site_id, } template = re.sub("@([a-z_]+)@", lambda m: data.get(m.group(1)), template) fp = open(dest_filename, "w") fp.write(template) fp.close() class UIToolkit(object): """ An abstract class that forms the basis for all UI interaction. Subclasses can override this to provide new ways of representing the UI to the user. """ def run(self): """ Runs the UI. """ pass def page(self, text, allow_back=True, is_visible_func=None, on_show_func=None): """ Adds a new "page" to display to the user. Input and text are associated with this page and may be displayed immediately or later, depending on the toolkit. If is_visible_func is specified and returns False, this page will be skipped. """ return None def prompt_input(self, page, prompt, default=None, password=False, normalize_func=None, save_obj=None, save_var=None): """ Prompts the user for some text. This may contain a default value. """ raise NotImplemented def prompt_choice(self, page, prompt, choices, save_obj=None, save_var=None): """ Prompts the user for an item amongst a list of choices. """ raise NotImplemented def text(self, page, text): """ Displays a block of text to the user. """ raise NotImplemented def urllink(self, page, url): """ Displays a URL to the user. """ raise NotImplemented def itemized_list(self, page, title, items): """ Displays an itemized list. """ raise NotImplemented def step(self, page, text, func): """ Adds a step of a multi-step operation. This will indicate when it's starting and when it's complete. """ raise NotImplemented def error(self, text, done_func=None): """ Displays a block of error text to the user. """ raise NotImplemented class ConsoleUI(UIToolkit): """ A UI toolkit that simply prints to the console. """ def __init__(self): super(UIToolkit, self).__init__() self.header_wrapper = textwrap.TextWrapper(initial_indent="* ", subsequent_indent=" ") indent_str = " " * 4 self.text_wrapper = textwrap.TextWrapper(initial_indent=indent_str, subsequent_indent=indent_str, break_long_words=False) self.error_wrapper = textwrap.TextWrapper(initial_indent="[!] ", subsequent_indent=" ", break_long_words=False) def page(self, text, allow_back=True, is_visible_func=None, on_show_func=None): """ Adds a new "page" to display to the user. In the console UI, we only care if we need to display or ask questions for this page. Our representation of a page in this case is simply a boolean value. If False, nothing associated with this page will be displayed to the user. """ visible = not is_visible_func or is_visible_func() if not visible: return False if on_show_func: on_show_func() print print print self.header_wrapper.fill(text) return True def prompt_input(self, page, prompt, default=None, password=False, normalize_func=None, save_obj=None, save_var=None): """ Prompts the user for some text. This may contain a default value. """ assert save_obj assert save_var if not page: return if default: self.text(page, "The default is %s" % default) prompt = "%s [%s]" % (prompt, default) print prompt += ": " value = None while not value: if password: value = getpass.getpass(prompt) else: value = raw_input(prompt) if not value: if default: value = default else: self.error("You must answer this question.") if normalize_func: value = normalize_func(value) setattr(save_obj, save_var, value) def prompt_choice(self, page, prompt, choices, save_obj=None, save_var=None): """ Prompts the user for an item amongst a list of choices. """ assert save_obj assert save_var if not page: return self.text(page, "You can type either the name or the number " "from the list below.") valid_choices = [] i = 0 for choice in choices: if isinstance(choice, basestring): text = choice enabled = True else: text, enabled = choice if enabled: self.text(page, "(%d) %s\n" % (i + 1, text), leading_newline=(i == 0)) valid_choices.append(text) i += 1 print prompt += ": " choice = None while not choice: choice = raw_input(prompt) if choice not in valid_choices: try: i = int(choice) - 1 if 0 <= i < len(valid_choices): choice = valid_choices[i] break except ValueError: pass self.error("'%s' is not a valid option." % choice) choice = None setattr(save_obj, save_var, choice) def text(self, page, text, leading_newline=True): """ Displays a block of text to the user. This will wrap the block to fit on the user's screen. """ if not page: return if leading_newline: print print self.text_wrapper.fill(text) def urllink(self, page, url): """ Displays a URL to the user. """ self.text(page, url) def itemized_list(self, page, title, items): """ Displays an itemized list. """ if title: self.text(page, "%s:" % title) for item in items: self.text(page, " * %s" % item, False) def step(self, page, text, func): """ Adds a step of a multi-step operation. This will indicate when it's starting and when it's complete. """ sys.stdout.write("%s ... " % text) func() print "OK" def error(self, text, done_func=None): """ Displays a block of error text to the user. """ print print self.error_wrapper.fill(text) if done_func: done_func() class GtkUI(UIToolkit): """ A UI toolkit that uses GTK to display a wizard. """ def __init__(self): self.pages = [] self.page_stack = [] self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) self.window.set_title("Review Board Site Tool") self.window.set_default_size(300, 500) self.window.set_border_width(12) self.window.set_resizable(False) self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) self.window.set_position(gtk.WIN_POS_CENTER_ALWAYS) vbox = gtk.VBox(False, 12) vbox.show() self.window.add(vbox) self.notebook = gtk.Notebook() self.notebook.show() vbox.pack_start(self.notebook, True, True, 0) self.notebook.set_show_border(False) self.notebook.set_show_tabs(False) self.bbox = gtk.HButtonBox() self.bbox.show() vbox.pack_start(self.bbox, False, False, 0) self.bbox.set_layout(gtk.BUTTONBOX_END) self.bbox.set_spacing(6) button = gtk.Button(stock=gtk.STOCK_CANCEL) button.show() self.bbox.pack_start(button, False, False, 0) button.connect('clicked', lambda w: self.quit()) self.prev_button = gtk.Button(stock=gtk.STOCK_GO_BACK) self.prev_button.show() self.bbox.pack_start(self.prev_button, False, False, 0) self.prev_button.connect('clicked', self.previous_page) self.next_button = gtk.Button(stock=gtk.STOCK_GO_FORWARD) self.next_button.show() self.bbox.pack_start(self.next_button, False, False, 0) self.next_button.connect('clicked', self.next_page) self.next_button.set_flags(gtk.CAN_DEFAULT) self.next_button.grab_default() self.next_button.grab_focus() self.close_button = gtk.Button(stock=gtk.STOCK_CLOSE) self.bbox.pack_start(self.close_button, False, False, 0) self.close_button.connect('clicked', lambda w: self.quit()) def run(self): if self.pages: self.window.show() self.page_stack.append(self.pages[0]) self.update_buttons() gtk.main() def quit(self): gtk.main_quit() def update_buttons(self): cur_page = self.page_stack[-1] cur_page_num = self.notebook.get_current_page() self.prev_button.set_sensitive(cur_page_num > 0 and cur_page['allow_back']) if cur_page_num == len(self.pages) - 1: self.close_button.show() self.next_button.hide() else: allow_next = True for validator in cur_page['validators']: if not validator(): allow_next = False break self.close_button.hide() self.next_button.show() self.next_button.set_sensitive(allow_next) def previous_page(self, widget): self.page_stack.pop() self.notebook.set_current_page(self.page_stack[-1]['index']) self.update_buttons() def next_page(self, widget): new_page_index = self.notebook.get_current_page() + 1 for i in range(new_page_index, len(self.pages)): page = self.pages[i] if not page['is_visible_func'] or page['is_visible_func'](): page_info = self.pages[i] self.notebook.set_current_page(i) self.page_stack.append(page) self.update_buttons() for func in page_info['on_show_funcs']: func() return def page(self, text, allow_back=True, is_visible_func=None, on_show_func=None): vbox = gtk.VBox(False, 12) vbox.show() self.notebook.append_page(vbox) label = gtk.Label("%s" % text) label.show() vbox.pack_start(label, False, True, 0) label.set_alignment(0, 0) label.set_use_markup(True) page = { 'is_visible_func': is_visible_func, 'widget': vbox, 'index': len(self.pages), 'allow_back': allow_back, 'label_sizegroup': gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL), 'validators': [], 'on_show_funcs': [], } if on_show_func: page['on_show_funcs'].append(on_show_func) self.pages.append(page) return page def prompt_input(self, page, prompt, default=None, password=False, normalize_func=None, save_obj=None, save_var=None): def save_input(widget=None, event=None): value = entry.get_text() if normalize_func: value = normalize_func(value) setattr(save_obj, save_var, value) self.update_buttons() hbox = gtk.HBox(False, 6) hbox.show() page['widget'].pack_start(hbox, False, False, 0) label = gtk.Label("%s:" % prompt) label.show() hbox.pack_start(label, False, True, 0) label.set_alignment(0, 0.5) label.set_line_wrap(True) label.set_use_markup(True) page['label_sizegroup'].add_widget(label) entry = gtk.Entry() entry.show() hbox.pack_start(entry, True, True, 0) entry.set_activates_default(True) if password: entry.set_visibility(False) if default: entry.set_text(default) entry.connect("key_release_event", save_input) page.setdefault('entries', []).append(entry) page['validators'].append(lambda: entry.get_text() != "") page['on_show_funcs'].append(save_input) # If this is the first on the page, make sure it gets focus when # we switch to this page. if len(page['entries']) == 1: page['on_show_funcs'].append(entry.grab_focus) def prompt_choice(self, page, prompt, choices, save_obj=None, save_var=None): """ Prompts the user for an item amongst a list of choices. """ def on_toggled(radio_button): if radio_button.get_active(): setattr(save_obj, save_var, radio_button.get_label()) hbox = gtk.HBox(False, 0) hbox.show() page['widget'].pack_start(hbox, False, True, 0) label = gtk.Label(" ") label.show() hbox.pack_start(label, False, False, 0) vbox = gtk.VBox(False, 6) vbox.show() hbox.pack_start(vbox, True, True, 0) label = gtk.Label("%s:" % prompt) label.show() vbox.pack_start(label, False, True, 0) label.set_alignment(0, 0) label.set_line_wrap(True) label.set_use_markup(True) buttons = [] for choice in choices: if isinstance(choice, basestring): text = choice enabled = True else: text, enabled = choice radio_button = gtk.RadioButton(label=text, use_underline=False) radio_button.show() vbox.pack_start(radio_button, False, True, 0) buttons.append(radio_button) radio_button.set_sensitive(enabled) radio_button.connect('toggled', on_toggled) if buttons[0] != radio_button: radio_button.set_group(buttons[0]) # Force this to save. on_toggled(buttons[0]) def text(self, page, text): """ Displays a block of text to the user. """ label = gtk.Label(textwrap.fill(text, 80)) label.show() page['widget'].pack_start(label, False, True, 0) label.set_alignment(0, 0) def urllink(self, page, url): """ Displays a URL to the user. """ link_button = gtk.LinkButton(url) link_button.show() page['widget'].pack_start(link_button, False, False, 0) link_button.set_alignment(0, 0) def itemized_list(self, page, title, items): """ Displays an itemized list. """ if title: label = gtk.Label() label.set_markup("%s:" % title) label.show() page['widget'].pack_start(label, False, True, 0) label.set_alignment(0, 0) for item in items: self.text(page, u" \u2022 %s" % item) def step(self, page, text, func): """ Adds a step of a multi-step operation. This will indicate when it's starting and when it's complete. """ def call_func(): self.bbox.set_sensitive(False) label.set_markup("%s" % text) while gtk.events_pending(): gtk.main_iteration() func() label.set_text(text) self.bbox.set_sensitive(True) icon.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_MENU) hbox = gtk.HBox(False, 12) hbox.show() page['widget'].pack_start(hbox, False, False, 0) icon = gtk.Image() icon.show() hbox.pack_start(icon, False, False, 0) icon.set_size_request(*gtk.icon_size_lookup(gtk.ICON_SIZE_MENU)) label = gtk.Label(text) label.show() hbox.pack_start(label, False, False, 0) label.set_alignment(0, 0) page['on_show_funcs'].append(call_func) def error(self, text, done_func=None): """ Displays a block of error text to the user. """ dlg = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, text) dlg.show() if not done_func: done_func = self.quit dlg.connect('response', lambda w, e: done_func()) class Command(object): needs_ui = False def add_options(self, parser): pass def run(self): pass class InstallCommand(Command): """ Installs a new Review Board site tree and generates web server configuration files. This will ask several questions about the site before performing the installation. """ needs_ui = True def add_options(self, parser): isWin = (platform.system() == "Windows") group = OptionGroup(parser, "'install' command", self.__doc__.strip()) group.add_option("--copy-media", action="store_true", dest="copy_media", default=isWin, help="copy media files instead of symlinking") group.add_option("--noinput", action="store_true", default=False, help="run non-interactively using configuration " "provided in command-line options") group.add_option("--domain-name", help="full domain name of the site, excluding the " "http://, port or path") group.add_option("--site-root", default="/", help="path to the site relative to the domain name") group.add_option("--media-url", default="media/", help="the URL containing the media files") group.add_option("--db-type", help="database type (mysql, postgresql or sqlite3)") group.add_option("--db-name", default="reviewboard", help="database name (not for sqlite3)") group.add_option("--db-host", default="localhost", help="database host (not for sqlite3)") group.add_option("--db-user", help="database user (not for sqlite3)") group.add_option("--db-pass", help="password for the database user " "(not for sqlite3)") group.add_option("--cache-type", help="cache server type (memcached or file)") group.add_option("--cache-info", help="cache identifier (memcached connection string " "or file cache directory)") group.add_option("--web-server-type", help="web server (apache or lighttpd)") group.add_option("--python-loader", help="python loader for apache (modpython or fastcgi)") group.add_option("--admin-user", default="admin", help="the site administrator's username") group.add_option("--admin-password", help="the site administrator's password") group.add_option("--admin-email", help="the site administrator's e-mail address") parser.add_option_group(group) def run(self): if not self.check_permissions(): return site.__dict__.update(options.__dict__) self.print_introduction() if self.print_missing_dependencies(): # There were required dependencies missing. Don't show any more # pages. return if not options.noinput: self.ask_domain() self.ask_site_root() self.ask_media_url() self.ask_database_type() self.ask_database_name() self.ask_database_host() self.ask_database_login() self.ask_cache_type() self.ask_cache_info() self.ask_web_server_type() self.ask_python_loader() self.ask_admin_user() self.show_install_status() self.show_finished() def normalize_root_url_path(self, path): if not path.endswith("/"): path += "/" if not path.startswith("/"): path = "/" + path return path def normalize_media_url_path(self, path): if not path.endswith("/"): path += "/" if path.startswith("/"): path = path[1:] return path def check_permissions(self): # Make sure we can create the directory first. try: # TODO: Do some chown tests too. os.mkdir(site.install_dir) # Don't leave a mess. We'll actually do this at the end. os.rmdir(site.install_dir) return True except OSError: # Likely a permission error. ui.error("Unable to create the %s directory. Make sure " "you're running as an administrator." % site.install_dir, done_func=lambda: sys.exit(1)) return False def print_introduction(self): page = ui.page("Welcome to the Review Board site installation wizard.") ui.text(page, "This will prepare a Review Board site installation in:") ui.text(page, site.abs_install_dir) ui.text(page, "We need to know a few things before we can prepare " "your site for installation. This will only take a few " "minutes.") def print_missing_dependencies(self): fatal, missing_dep_groups = Dependencies.get_missing() if missing_dep_groups: if fatal: page = ui.page("Required modules are missing.") ui.text(page, "You are missing Python modules that are " "needed before the installation process. " "You will need to install the necessary " "modules and restart the install.") else: page = ui.page("Make sure you have the modules you need.") ui.text(page, "Depending on your installation, you may need " "certain Python modules and servers that are " "missing.") ui.text(page, "If you need support for any of the following, " "you will need to install the necessary " "modules and restart the install.") for group in missing_dep_groups: ui.itemized_list(page, group['title'], group['dependencies']) return fatal def ask_domain(self): page = ui.page("What's the domain name for this site?") ui.text(page, "This should be the full domain without the http://, " "port or path.") ui.prompt_input(page, "Domain Name", site.domain_name, save_obj=site, save_var="domain_name") def ask_site_root(self): page = ui.page("What URL path points to Review Board?") ui.text(page, "Typically, Review Board exists at the root of a URL. " "For example, http://reviews.example.com/. In this " "case, you would specify \"/\".") ui.text(page, "However, if you want to listen to, say, " "http://example.com/reviews/, you can specify " '"/reviews/".') ui.text(page, "Note that this is the path relative to the domain and " "should not include the domain name.") ui.prompt_input(page, "Root Path", site.site_root, normalize_func=self.normalize_root_url_path, save_obj=site, save_var="site_root") def ask_media_url(self): page = ui.page("What URL will point to the media files?") ui.text(page, "While most installations distribute media files on " "the same server as the rest of Review Board, some " "custom installs may instead have a separate server " "for this purpose.") ui.prompt_input(page, "Media URL", site.media_url, normalize_func=self.normalize_media_url_path, save_obj=site, save_var="media_url") def ask_database_type(self): page = ui.page("What database type will you be using?") ui.prompt_choice(page, "Database Type", [("mysql", Dependencies.get_support_mysql()), ("postgresql", Dependencies.get_support_postgresql()), ("sqlite3", Dependencies.get_support_sqlite())], save_obj=site, save_var="db_type") def ask_database_name(self): def determine_sqlite_path(): site.db_name = sqlite_db_name sqlite_db_name = os.path.join(site.abs_install_dir, "db", "reviewboard.db") # Appears only if using sqlite. page = ui.page("Determining database file path.", is_visible_func=lambda: site.db_type == "sqlite3", on_show_func=determine_sqlite_path) ui.text(page, "The sqlite database file will be stored in %s" % sqlite_db_name) ui.text(page, "If you are migrating from an existing " "installation, you can move your existing " "database there, or edit settings_local.py to " "point to your old location.") # Appears only if not using sqlite. page = ui.page("What database name should Review Board use?", is_visible_func=lambda: site.db_type != "sqlite3") ui.text(page, "You may need to create this database and grant a " "user modification rights before continuing.") ui.prompt_input(page, "Database Name", site.db_name, save_obj=site, save_var="db_name") def ask_database_host(self): def normalize_host_port(value): if ":" in value: value, site.db_port = value.split(":", 1) return value page = ui.page("What is the database server's address?", is_visible_func=lambda: site.db_type != "sqlite3") ui.text(page, "This should be specified in hostname:port form. " "The port is optional if you're using a standard " "port for the database type.") ui.prompt_input(page, "Database Server", site.db_host, normalize_func=normalize_host_port, save_obj=site, save_var="db_host") def ask_database_login(self): page = ui.page("What is the login and password for this database?", is_visible_func=lambda: site.db_type != "sqlite3") ui.text(page, "This must be a user that has creation and modification " "rights on the database.") ui.prompt_input(page, "Database Username", site.db_user, save_obj=site, save_var="db_user") ui.prompt_input(page, "Database Password", site.db_pass, password=True, save_obj=site, save_var="db_pass") def ask_cache_type(self): page = ui.page("What cache mechanism should be used?") ui.text(page, "memcached is strongly recommended. Use it unless " "you have a good reason not to.") ui.prompt_choice(page, "Cache Type", [("memcached", Dependencies.get_support_memcached()), "file"], save_obj=site, save_var="cache_type") def ask_cache_info(self): # Appears only if using memcached. page = ui.page("What memcached connection string should be used?", is_visible_func=lambda: site.cache_type == "memcached") ui.text(page, "This is generally in the format of " "memcached://hostname:port/") ui.prompt_input(page, "Memcache Server", site.cache_info or "memcached://localhost:11211/", save_obj=site, save_var="cache_info") # Appears only if using file caching. page = ui.page("Where should the temporary cache files be stored?", is_visible_func=lambda: site.cache_type == "file") ui.prompt_input(page, "Cache Directory", site.cache_info or "/tmp/reviewboard_cache", normalize_func=lambda value: "file://" + value, save_obj=site, save_var="cache_info") def ask_web_server_type(self): page = ui.page("What web server will you be using?") ui.prompt_choice(page, "Web Server", ["apache", "lighttpd"], save_obj=site, save_var="web_server_type") def ask_python_loader(self): page = ui.page("What Python loader module will you be using?", is_visible_func=lambda: site.web_server_type == "apache") ui.text(page, "Based on our experiences, we recommend using " "modpython with Review Board.") ui.prompt_choice(page, "Python Loader", ["modpython", "fastcgi"], save_obj=site, save_var="python_loader") def ask_admin_user(self): page = ui.page("Create an administrator account") ui.text(page, "To configure Review Board, you'll need an " "administrator account. It is advised to have one " "administrator and then use that account to grant " "administrator permissions to your personal user " "account.") ui.text(page, "If you plan to use NIS or LDAP, use an account name " "other than your NIS/LDAP account so as to prevent " "conflicts.") ui.prompt_input(page, "Username", site.admin_user, save_obj=site, save_var="admin_user") ui.prompt_input(page, "Password", site.admin_password, password=True, save_obj=site, save_var="admin_password") ui.prompt_input(page, "E-Mail Address", site.admin_email, save_obj=site, save_var="admin_email") def show_install_status(self): page = ui.page("Installing the site...", allow_back=False) ui.step(page, "Building site directories", site.rebuild_site_directory) ui.step(page, "Building site configuration files", site.generate_config_files) ui.step(page, "Creating database", site.sync_database) ui.step(page, "Performing migrations", site.migrate_database) ui.step(page, "Creating administrator account", site.create_admin_user) ui.step(page, "Saving site settings", self.save_settings) def show_finished(self): page = ui.page("The site has been installed", allow_back=False) ui.text(page, "The site has been installed in %s" % site.install_dir) ui.text(page, "Sample configuration files for web servers and " "cron are available in the conf/ directory.") ui.text(page, "You need to modify the ownership of the " "\"htdocs/media/uploaded\" directory and all of its " "contents to be owned by the web server.") ui.text(page, "If using SQLite, you will also need to modify the " "ownership of the \"db\" directory and its contents.") ui.text(page, "For more information, visit:") ui.urllink(page, "%sadmin/sites/creating-sites/" % DOCS_BASE) def save_settings(self): """ Saves some settings in the database. """ from django.contrib.sites.models import Site from djblets.siteconfig.models import SiteConfiguration cur_site = Site.objects.get_current() cur_site.domain = site.domain_name cur_site.save() if site.media_url.startswith("http"): site_media_url = site.media_url else: site_media_url = site.site_root + site.media_url site_media_root = os.path.join(site.abs_install_dir, "htdocs", "media") siteconfig = SiteConfiguration.objects.get_current() siteconfig.set("site_media_url", site_media_url) siteconfig.set("site_media_root", site_media_root) siteconfig.set("site_admin_name", site.admin_user) siteconfig.set("site_admin_email", site.admin_email) siteconfig.save() class UpgradeCommand(Command): """ Upgrades an existing site installation, synchronizing media trees and upgrading the database, unless otherwise specified. """ def add_options(self, parser): group = OptionGroup(parser, "'upgrade' command", self.__doc__.strip()) group.add_option("--no-db-upgrade", action="store_false", dest="upgrade_db", default=True, help="don't upgrade the database") parser.add_option_group(group) def run(self): site.setup_settings() print "Rebuilding directory structure" site.rebuild_site_directory() if options.upgrade_db: print "Updating database. This may take a while." site.sync_database() site.migrate_database() class ManageCommand(Command): """ Runs a manage.py command on the site. """ def run(self): site.setup_settings() if len(args) == 0: ui.error("A manage command is needed.", done_func=lambda: sys.exit(1)) else: site.run_manage_command(args[0], args[1:]) sys.exit(0) # A list of all commands supported by rb-site. COMMANDS = { "install": InstallCommand(), "upgrade": UpgradeCommand(), "manage": ManageCommand(), } def parse_options(args): global options parser = OptionParser(usage="%prog command [options] path", version="%prog " + VERSION) parser.add_option("--console", action="store_true", dest="force_console", default=False, help="force the console UI") parser.add_option("-d", "--debug", action="store_true", dest="debug", default=DEBUG, help="display debug output") sorted_commands = list(COMMANDS.keys()) sorted_commands.sort() for cmd_name in sorted_commands: command = COMMANDS[cmd_name] command.add_options(parser) (options, args) = parser.parse_args(args) if options.noinput: options.force_console = True # We expect at least two args (command and install path) if len(args) < 2 or args[0] not in COMMANDS.keys(): parser.print_help() sys.exit(1) command = args[0] install_dir = args[1] globals()["args"] = args[2:] return (command, install_dir) def main(): global site global ui command_name, install_dir = parse_options(sys.argv[1:]) command = COMMANDS[command_name] site = Site(install_dir) if command.needs_ui and can_use_gtk and not options.force_console: ui = GtkUI() if not ui: ui = ConsoleUI() command.run() ui.run() if __name__ == "__main__": main()