reorganising installer code into a package, adding a new wrapping bloodhound_setup script - towards #809

git-svn-id: https://svn.apache.org/repos/asf/bloodhound/trunk@1621579 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/installer/bhsetup/__init__.py b/installer/bhsetup/__init__.py
new file mode 100644
index 0000000..534df97
--- /dev/null
+++ b/installer/bhsetup/__init__.py
@@ -0,0 +1,17 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
diff --git a/installer/bhsetup/bloodhound_setup.py b/installer/bhsetup/bloodhound_setup.py
new file mode 100644
index 0000000..747ff3a
--- /dev/null
+++ b/installer/bhsetup/bloodhound_setup.py
@@ -0,0 +1,478 @@
+#!/usr/bin/env python
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+"""Initial configuration for Bloodhound"""
+
+import os
+import pkg_resources
+import shutil
+import sys
+from bhsetup.createdigest import htdigest_create
+from getpass import getpass
+from optparse import OptionParser
+
+try:
+    from trac.admin.console import TracAdmin
+    from trac.config import Configuration
+    from trac.util import translation
+    from trac.util.translation import _, get_negotiated_locale, has_babel
+except ImportError, e:
+    print("Requirements must be installed before running "
+          "bloodhound_setup.py.\n"
+          "You can install them with the following command:\n"
+          "   pip install -r requirements.txt\n")
+    sys.exit(1)
+
+try:
+    import psycopg2
+except ImportError:
+    psycopg2 = None
+
+try:
+    import MySQLdb as mysqldb
+except ImportError:
+    mysqldb = None
+
+LANG = os.environ.get('LANG')
+
+MAXBACKUPNUMBER = 64  # Max attempts to create backup file
+
+SUPPORTED_DBTYPES = ('sqlite', 'postgres', 'mysql')
+DEFAULT_DB_USER = 'bloodhound'
+DEFAULT_DB_NAME = 'bloodhound'
+DEFAULT_ADMIN_USER = 'admin'
+
+BH_PROJECT_SITE = 'https://issues.apache.org/bloodhound/'
+BASE_CONFIG = {'components': {'bhtheme.*': 'enabled',
+                              'bhdashboard.*': 'enabled',
+                              'multiproduct.*': 'enabled',
+                              'permredirect.*': 'enabled',
+                              'themeengine.api.*': 'enabled',
+                              'themeengine.web_ui.*': 'enabled',
+                              'bhsearch.*': 'enabled',
+                              'bhrelations.*': 'enabled',
+                              'trac.ticket.web_ui.ticketmodule': 'disabled',
+                              'trac.ticket.report.reportmodule': 'disabled',
+                              },
+               'header_logo': {'src': '',},
+               'mainnav': {'roadmap': 'disabled',
+                           'search': 'disabled',
+                           'timeline': 'disabled',},
+               'metanav': {'about': 'disabled',},
+               'theme': {'theme': 'bloodhound',},
+               'trac': {'mainnav': ','.join(['dashboard', 'wiki', 'browser',
+                                             'tickets', 'newticket', 'timeline',
+                                             'roadmap', 'search']),
+                        'environment_factory': '',
+                        'request_factory': '',},
+               'project': {'footer': ('Get involved with '
+                                      '<a href="%(site)s">Apache Bloodhound</a>'
+                                      % {'site': BH_PROJECT_SITE,}),},
+               'labels': {'application_short': 'Bloodhound',
+                          'application_full': 'Apache Bloodhound',
+                          'footer_left_prefix': '',
+                          'footer_left_postfix': '',
+                          'footer_right': ''},
+               'bhsearch': {'is_default': 'true', 'enable_redirect': 'true'},
+}
+
+ACCOUNTS_CONFIG = {'account-manager': {'account_changes_notify_addresses' : '',
+                                       'authentication_url' : '',
+                                       'db_htdigest_realm' : '',
+                                       'force_passwd_change' :'true',
+                                       'hash_method' : 'HtDigestHashMethod',
+                                       'htdigest_file' : '',
+                                       'htdigest_realm' : '',
+                                       'htpasswd_file' : '',
+                                       'htpasswd_hash_type' : 'crypt',
+                                       'password_store' : 'HtDigestStore',
+                                       'persistent_sessions' : 'False',
+                                       'refresh_passwd' : 'False',
+                                       'user_lock_max_time' : '0',
+                                       'verify_email' : 'True',
+                                       },
+                   'components': {'acct_mgr.admin.*' : 'enabled',
+                                  'acct_mgr.api.accountmanager' : 'enabled',
+                                  'acct_mgr.guard.accountguard' : 'enabled',
+                                  'acct_mgr.htfile.htdigeststore' : 'enabled',
+                                  'acct_mgr.macros.*': 'enabled',
+                                  'acct_mgr.web_ui.accountmodule' : 'enabled',
+                                  'acct_mgr.web_ui.loginmodule' : 'enabled',
+                                  'trac.web.auth.loginmodule' : 'disabled',
+                                  },
+                   }
+
+class BloodhoundSetup(object):
+    """Creates a Bloodhound environment"""
+
+    def __init__(self, opts):
+        if isinstance(opts, dict):
+            options = dict(opts)
+        else:
+            options = vars(opts)
+        self.options = options
+
+        if 'project' not in options:
+            options['project'] = 'main'
+        if 'envsdir' not in options:
+            options['envsdir'] = os.path.join('bloodhound',
+                                              'environments')
+
+        # Flags used when running the functional test suite
+        self.apply_bhwiki_upgrades = True
+
+    def _generate_db_str(self, options):
+        """Builds an appropriate db string for trac-admin for sqlite and
+        postgres options. Also allows for a user to provide their own db
+        string to allow database initialisation beyond these."""
+        dbdata = {'type': options.get('dbtype', 'sqlite'),
+                  'user': options.get('dbuser'),
+                  'pass': options.get('dbpass'),
+                  'host': options.get('dbhost', 'localhost'),
+                  'port': options.get('dbport'),
+                  'name': options.get('dbname', 'bloodhound'),
+                  }
+
+        db = options.get('dbstring')
+        if db is None:
+            if dbdata['type'] in ('postgres', 'mysql') \
+                    and dbdata['user'] is not None \
+                    and dbdata['pass'] is not None:
+                if dbdata['port'] is not None:
+                    db = '%(type)s://%(user)s:%(pass)s@%(host)s:%(port)s/%(name)s'
+                else:  # no port specified = default port
+                    db = '%(type)s://%(user)s:%(pass)s@%(host)s/%(name)s'
+            else:
+                db = '%%(type)s:%s' % os.path.join('db', '%(name)s.db')
+        return db % dbdata
+
+    def setup(self, **kwargs):
+        """Do the setup. A kwargs dictionary may be passed to override base
+        options, potentially allowing for multiple environment creation."""
+
+        if has_babel:
+            import babel
+            try:
+                locale = get_negotiated_locale([LANG])
+                locale = locale or babel.Locale.default()
+            except babel.UnknownLocaleError:
+                pass
+            translation.activate(locale)
+
+        options = dict(self.options)
+        options.update(kwargs)
+        if psycopg2 is None and options.get('dbtype') == 'postgres':
+            print "psycopg2 needs to be installed to initialise a postgresql db"
+            return False
+        elif mysqldb is None and options.get('dbtype') == 'mysql':
+            print "MySQLdb needs to be installed to initialise a mysql db"
+            return False
+
+        environments_path = options['envsdir']
+        if not os.path.exists(environments_path):
+            os.makedirs(environments_path)
+
+        new_env =  os.path.join(environments_path, options['project'])
+        tracini = os.path.abspath(os.path.join(new_env, 'conf', 'trac.ini'))
+        baseini = os.path.abspath(os.path.join(new_env, 'conf', 'base.ini'))
+        options['inherit'] = '"' + baseini + '"'
+
+        options['db'] = self._generate_db_str(options)
+        if 'repo_type' not in options or options['repo_type'] is None:
+            options['repo_type'] = ''
+        if 'repo_path' not in options or options['repo_path'] is None:
+            options['repo_path'] = ''
+        if (len(options['repo_type']) > 0) ^ (len(options['repo_path']) > 0):
+            print "Error: Specifying a repository requires both the "\
+                  "repository-type and the repository-path options."
+            return False
+
+        custom_prefix = 'default_product_prefix'
+        if custom_prefix in options and options[custom_prefix]:
+            default_product_prefix = options[custom_prefix]
+        else:
+            default_product_prefix = '@'
+
+        digestfile = os.path.abspath(os.path.join(new_env,
+                                                  options['digestfile']))
+        realm =  options['realm']
+        adminuser = options['adminuser']
+        adminpass = options['adminpass']
+
+        # create base options:
+        accounts_config = dict(ACCOUNTS_CONFIG)
+        accounts_config['account-manager']['htdigest_file'] = digestfile
+        accounts_config['account-manager']['htdigest_realm'] = realm
+
+        trac = TracAdmin(os.path.abspath(new_env))
+        if not trac.env_check():
+            try:
+                rv = trac.do_initenv('%(project)s %(db)s '
+                                     '%(repo_type)s %(repo_path)s '
+                                     '--inherit=%(inherit)s '
+                                     '--nowiki'
+                                     % options)
+                if rv == 2:
+                    raise SystemExit
+            except SystemExit:
+                print ("Error: Unable to initialise the environment.")
+                return False
+        else:
+            print ("Warning: Environment already exists at %s." % new_env)
+            self.writeconfig(tracini, [{'inherit': {'file': baseini},},])
+
+        base_config = dict(BASE_CONFIG)
+        base_config['trac']['environment_factory'] = \
+            'multiproduct.hooks.MultiProductEnvironmentFactory'
+        base_config['trac']['request_factory'] = \
+            'multiproduct.hooks.ProductRequestFactory'
+        if default_product_prefix != '@':
+            base_config['multiproduct'] = dict(
+                default_product_prefix=default_product_prefix
+            )
+
+        self.writeconfig(baseini, [base_config, accounts_config])
+
+        if os.path.exists(digestfile):
+            backupfile(digestfile)
+        htdigest_create(digestfile, adminuser, realm, adminpass)
+
+        print "Adding TRAC_ADMIN permissions to the admin user %s" % adminuser
+        trac.onecmd('permission add %s TRAC_ADMIN' % adminuser)
+
+        # get fresh TracAdmin instance (original does not know about base.ini)
+        bloodhound = TracAdmin(os.path.abspath(new_env))
+
+        # final upgrade
+        print "Running upgrades"
+        bloodhound.onecmd('upgrade')
+        pages = []
+        pages.append(pkg_resources.resource_filename('bhdashboard',
+                                                 'default-pages'))
+        pages.append(pkg_resources.resource_filename('bhsearch',
+                                                 'default-pages'))
+        bloodhound.onecmd('wiki load %s' % " ".join(pages))
+
+        print "Running wiki upgrades"
+        bloodhound.onecmd('wiki upgrade')
+
+        if self.apply_bhwiki_upgrades:
+            print "Running wiki Bloodhound upgrades"
+            bloodhound.onecmd('wiki bh-upgrade')
+        else:
+            print "Skipping Bloodhound wiki upgrades"
+
+        print "Loading default product wiki"
+        bloodhound.onecmd('product admin %s wiki load %s' %
+                          (default_product_prefix,
+                           " ".join(pages)))
+
+        print "Running default product wiki upgrades"
+        bloodhound.onecmd('product admin %s wiki upgrade' %
+                          default_product_prefix)
+
+        if self.apply_bhwiki_upgrades:
+            print "Running default product Bloodhound wiki upgrades"
+            bloodhound.onecmd('product admin %s wiki bh-upgrade' %
+                              default_product_prefix)
+        else:
+            print "Skipping default product Bloodhound wiki upgrades"
+
+        print """
+You can now start Bloodhound by running:
+
+  tracd --port=8000 %s
+
+And point your browser at http://localhost:8000/%s
+""" % (os.path.abspath(new_env), options['project'])
+        return True
+
+    def writeconfig(self, filepath, dicts=[]):
+        """Writes or updates a config file. A list of dictionaries is used so
+        that options for different aspects of the configuration can be kept
+        separate while being able to update the same sections. Note that the
+        result is order dependent where two dictionaries update the same
+        option.
+        """
+        config = Configuration(filepath)
+        file_changed = False
+        for data in dicts:
+            for section, options in data.iteritems():
+                for key, value in options.iteritems():
+                    if config.get(section, key, None) != value:
+                        # This should be expected to generate a false positive
+                        # when two dictionaries update the same option
+                        file_changed = True
+                    config.set(section, key, value)
+        if file_changed:
+            if os.path.exists(filepath):
+                backupfile(filepath)
+            config.save()
+
+def backupfile(filepath):
+    """Very basic backup routine"""
+    print "Warning: Updating %s." % filepath
+    backuppath = None
+    if not os.path.exists(filepath + '_bak'):
+        backuppath = filepath + '_bak'
+    else:
+        backuptemplate = filepath + '_bak_%d'
+        for i in xrange(MAXBACKUPNUMBER):
+            if not os.path.exists(backuptemplate % i):
+                backuppath = backuptemplate % i
+                break
+    if backuppath is not None:
+        shutil.copyfile(filepath, backuppath)
+        print "Backup created at %s." % backuppath
+    else:
+        print "No backup created (too many other backups found)"
+    return backuppath
+
+def handle_options():
+    """Parses the command line, with basic prompting for choices where options
+    are not specified."""
+    parser = OptionParser()
+
+    # Base Trac Options
+    parser.add_option('--project', dest='project',
+                      help='Set the top project name', default='main')
+    parser.add_option('--source_directory', dest='sourcedir',
+                      help='Specify root source code directory',
+                      default=os.path.normpath(os.path.join(os.getcwd(), '../'))),
+    parser.add_option('--environments_directory', dest='envsdir',
+                      help='Set the directory to contain environments',
+                      default=os.path.join('bloodhound', 'environments'))
+    parser.add_option('-d', '--database-type', dest='dbtype',
+                      help="Specify as either 'sqlite', 'postgres' or 'mysql'",
+                      default='')
+    parser.add_option('--database-string', dest='dbstring',
+                      help=('Advanced: provide a custom database string, '
+                            'overriding other database options'),
+                      default=None)
+    parser.add_option('--database-name', dest='dbname',
+                      help='Specify the database name',
+                      default='bloodhound')
+    parser.add_option('-u', '--user', dest='dbuser',
+                      help='Specify the db user (required for postgres and mysql)',
+                      default='')
+    parser.add_option('-p', '--password', dest='dbpass',
+                      help='Specify the db password (required for postgres and mysql)')
+    parser.add_option('--database-host', dest='dbhost',
+                      help='Specify the database host (optional for postgres and mysql)',
+                      default='localhost')
+    parser.add_option('--database-port', dest='dbport',
+                      help='Specify the database port (optional for postgres and mysql)',
+                      default='5432')
+
+    # Account Manager Options
+    parser.add_option('--admin-password', dest='adminpass',
+                      help='create an admin user in an htdigest file')
+    parser.add_option('--digest-realm', dest='realm', default='bloodhound',
+                      help='authentication realm for htdigest file')
+    parser.add_option('--admin-user', dest='adminuser', default='',
+                      help='admin user name for htdigest file')
+    parser.add_option('--digest-file', dest='digestfile',
+                      default='bloodhound.htdigest',
+                      help='filename for the htdigest file')
+
+    # Repository Options
+    parser.add_option('--repository-type', dest='repo_type',
+                      help='specify the repository type - ')
+    parser.add_option('--repository-path', dest='repo_path',
+                      help='specify the repository type')
+
+    # Multiproduct options
+    parser.add_option('--default-product-prefix', dest='default_product_prefix',
+                      help='Specify prefix for default product (defaults to @')
+
+    (options, args) = parser.parse_args()
+    if args:
+        print "Unprocessed options/arguments: ", args
+
+    def ask_question(question, default=None):
+        """Basic question asking functionality"""
+        if default:
+            answer = raw_input(question % default)
+        else:
+            answer = raw_input(question)
+        return answer if answer else default
+
+    def ask_password(user):
+        """Asks for a password to be provided for setting purposes"""
+        attempts = 3
+        for attempt in range(attempts):
+            if attempt > 0:
+                print "Passwords empty or did not match. Please try again",
+                print "(attempt %d/%d)""" % (attempt+1, attempts)
+            password1 = getpass('Enter a new password for "%s": ' % user)
+            password2 = getpass('Please reenter the password: ')
+            if password1 and password1 == password2:
+                return password1
+        print "Passwords did not match. Quiting."
+        sys.exit(1)
+
+    if options.dbtype.lower() not in SUPPORTED_DBTYPES:
+        answer = ask_question("""
+This installer is able to install Apache Bloodhound with either SQLite,
+PostgreSQL or MySQL databases. SQLite is an easier option for installing
+Bloodhound as SQLite support is built into Python and requires no special
+permissions to run. However, PostgreSQL and MySQL are generally expected to
+be more robust for production use.
+What type of database do you want to instant to (%s)?
+[%%s]: """ % '/'.join(SUPPORTED_DBTYPES), default='sqlite')
+        answer = answer.lower()
+        if answer in SUPPORTED_DBTYPES:
+            options.dbtype = answer
+        else:
+            print "Unrecognized dbtype \"%s\". Quiting." % answer
+            sys.exit(1)
+    else:
+        options.dbtype = options.dbtype.lower()
+
+    if options.dbtype in ('postgres','mysql'):
+        if not options.dbuser:
+            options.dbuser = ask_question("""
+For PostgreSQL/MySQL you need to have PostgreSQL/MySQL installed and you need
+to have created a database user to connect to the database with. Setting this 
+up may require admin access rights to the server.
+DB user name [%s]: """, DEFAULT_DB_USER)
+
+        if not options.dbpass:
+            options.dbpass = ask_password(options.dbuser)
+
+        if not options.dbname:
+            options.dbname = ask_question("""
+For PostgreSQL/MySQL setup, you need to specify a database that you have
+created for Bloodhound to use. This installer currently assumes that this
+database will be empty. DB name [%s]: """, DEFAULT_DB_NAME)
+    if not options.adminuser:
+        options.adminuser = ask_question("""
+Please supply a username for the admin user [%s]: """, DEFAULT_ADMIN_USER)
+    if not options.adminpass:
+        options.adminpass = ask_password(options.adminuser)
+
+    return options
+
+
+def run():
+    options = handle_options()
+    bsetup = BloodhoundSetup(options)
+    bsetup.setup()
+
+if __name__ == '__main__':
+    run()
diff --git a/installer/createdigest.py b/installer/bhsetup/createdigest.py
similarity index 100%
rename from installer/createdigest.py
rename to installer/bhsetup/createdigest.py
diff --git a/installer/bloodhound_setup.py b/installer/bloodhound_setup.py
index 2da3a75..ebfb08e 100644
--- a/installer/bloodhound_setup.py
+++ b/installer/bloodhound_setup.py
@@ -18,458 +18,7 @@
 #  under the License.
 """Initial configuration for Bloodhound"""
 
-import os
-import pkg_resources
-import shutil
-import sys
-from createdigest import htdigest_create
-from getpass import getpass
-from optparse import OptionParser
-
-try:
-    from trac.admin.console import TracAdmin
-    from trac.config import Configuration
-    from trac.util import translation
-    from trac.util.translation import _, get_negotiated_locale, has_babel
-except ImportError, e:
-    print("Requirements must be installed before running "
-          "bloodhound_setup.py.\n"
-          "You can install them with the following command:\n"
-          "   pip install -r requirements.txt\n")
-    sys.exit(1)
-
-try:
-    import psycopg2
-except ImportError:
-    psycopg2 = None
-
-try:
-    import MySQLdb as mysqldb
-except ImportError:
-    mysqldb = None
-
-LANG = os.environ.get('LANG')
-
-MAXBACKUPNUMBER = 64  # Max attempts to create backup file
-
-SUPPORTED_DBTYPES = ('sqlite', 'postgres', 'mysql')
-DEFAULT_DB_USER = 'bloodhound'
-DEFAULT_DB_NAME = 'bloodhound'
-DEFAULT_ADMIN_USER = 'admin'
-
-BH_PROJECT_SITE = 'https://issues.apache.org/bloodhound/'
-BASE_CONFIG = {'components': {'bhtheme.*': 'enabled',
-                              'bhdashboard.*': 'enabled',
-                              'multiproduct.*': 'enabled',
-                              'permredirect.*': 'enabled',
-                              'themeengine.api.*': 'enabled',
-                              'themeengine.web_ui.*': 'enabled',
-                              'bhsearch.*': 'enabled',
-                              'bhrelations.*': 'enabled',
-                              'trac.ticket.web_ui.ticketmodule': 'disabled',
-                              'trac.ticket.report.reportmodule': 'disabled',
-                              },
-               'header_logo': {'src': '',},
-               'mainnav': {'roadmap': 'disabled',
-                           'search': 'disabled',
-                           'timeline': 'disabled',},
-               'metanav': {'about': 'disabled',},
-               'theme': {'theme': 'bloodhound',},
-               'trac': {'mainnav': ','.join(['dashboard', 'wiki', 'browser',
-                                             'tickets', 'newticket', 'timeline',
-                                             'roadmap', 'search']),
-                        'environment_factory': '',
-                        'request_factory': '',},
-               'project': {'footer': ('Get involved with '
-                                      '<a href="%(site)s">Apache Bloodhound</a>'
-                                      % {'site': BH_PROJECT_SITE,}),},
-               'labels': {'application_short': 'Bloodhound',
-                          'application_full': 'Apache Bloodhound',
-                          'footer_left_prefix': '',
-                          'footer_left_postfix': '',
-                          'footer_right': ''},
-               'bhsearch': {'is_default': 'true', 'enable_redirect': 'true'},
-}
-
-ACCOUNTS_CONFIG = {'account-manager': {'account_changes_notify_addresses' : '',
-                                       'authentication_url' : '',
-                                       'db_htdigest_realm' : '',
-                                       'force_passwd_change' :'true',
-                                       'hash_method' : 'HtDigestHashMethod',
-                                       'htdigest_file' : '',
-                                       'htdigest_realm' : '',
-                                       'htpasswd_file' : '',
-                                       'htpasswd_hash_type' : 'crypt',
-                                       'password_store' : 'HtDigestStore',
-                                       'persistent_sessions' : 'False',
-                                       'refresh_passwd' : 'False',
-                                       'user_lock_max_time' : '0',
-                                       'verify_email' : 'True',
-                                       },
-                   'components': {'acct_mgr.admin.*' : 'enabled',
-                                  'acct_mgr.api.accountmanager' : 'enabled',
-                                  'acct_mgr.guard.accountguard' : 'enabled',
-                                  'acct_mgr.htfile.htdigeststore' : 'enabled',
-                                  'acct_mgr.macros.*': 'enabled',
-                                  'acct_mgr.web_ui.accountmodule' : 'enabled',
-                                  'acct_mgr.web_ui.loginmodule' : 'enabled',
-                                  'trac.web.auth.loginmodule' : 'disabled',
-                                  },
-                   }
-
-class BloodhoundSetup(object):
-    """Creates a Bloodhound environment"""
-
-    def __init__(self, opts):
-        if isinstance(opts, dict):
-            options = dict(opts)
-        else:
-            options = vars(opts)
-        self.options = options
-
-        if 'project' not in options:
-            options['project'] = 'main'
-        if 'envsdir' not in options:
-            options['envsdir'] = os.path.join('bloodhound',
-                                              'environments')
-
-        # Flags used when running the functional test suite
-        self.apply_bhwiki_upgrades = True
-
-    def _generate_db_str(self, options):
-        """Builds an appropriate db string for trac-admin for sqlite and
-        postgres options. Also allows for a user to provide their own db
-        string to allow database initialisation beyond these."""
-        dbdata = {'type': options.get('dbtype', 'sqlite'),
-                  'user': options.get('dbuser'),
-                  'pass': options.get('dbpass'),
-                  'host': options.get('dbhost', 'localhost'),
-                  'port': options.get('dbport'),
-                  'name': options.get('dbname', 'bloodhound'),
-                  }
-
-        db = options.get('dbstring')
-        if db is None:
-            if dbdata['type'] in ('postgres', 'mysql') \
-                    and dbdata['user'] is not None \
-                    and dbdata['pass'] is not None:
-                if dbdata['port'] is not None:
-                    db = '%(type)s://%(user)s:%(pass)s@%(host)s:%(port)s/%(name)s'
-                else:  # no port specified = default port
-                    db = '%(type)s://%(user)s:%(pass)s@%(host)s/%(name)s'
-            else:
-                db = '%%(type)s:%s' % os.path.join('db', '%(name)s.db')
-        return db % dbdata
-
-    def setup(self, **kwargs):
-        """Do the setup. A kwargs dictionary may be passed to override base
-        options, potentially allowing for multiple environment creation."""
-
-        if has_babel:
-            import babel
-            try:
-                locale = get_negotiated_locale([LANG])
-                locale = locale or babel.Locale.default()
-            except babel.UnknownLocaleError:
-                pass
-            translation.activate(locale)
-
-        options = dict(self.options)
-        options.update(kwargs)
-        if psycopg2 is None and options.get('dbtype') == 'postgres':
-            print "psycopg2 needs to be installed to initialise a postgresql db"
-            return False
-        elif mysqldb is None and options.get('dbtype') == 'mysql':
-            print "MySQLdb needs to be installed to initialise a mysql db"
-            return False
-
-        environments_path = options['envsdir']
-        if not os.path.exists(environments_path):
-            os.makedirs(environments_path)
-
-        new_env =  os.path.join(environments_path, options['project'])
-        tracini = os.path.abspath(os.path.join(new_env, 'conf', 'trac.ini'))
-        baseini = os.path.abspath(os.path.join(new_env, 'conf', 'base.ini'))
-        options['inherit'] = '"' + baseini + '"'
-
-        options['db'] = self._generate_db_str(options)
-        if 'repo_type' not in options or options['repo_type'] is None:
-            options['repo_type'] = ''
-        if 'repo_path' not in options or options['repo_path'] is None:
-            options['repo_path'] = ''
-        if (len(options['repo_type']) > 0) ^ (len(options['repo_path']) > 0):
-            print "Error: Specifying a repository requires both the "\
-                  "repository-type and the repository-path options."
-            return False
-
-        custom_prefix = 'default_product_prefix'
-        if custom_prefix in options and options[custom_prefix]:
-            default_product_prefix = options[custom_prefix]
-        else:
-            default_product_prefix = '@'
-
-        digestfile = os.path.abspath(os.path.join(new_env,
-                                                  options['digestfile']))
-        realm =  options['realm']
-        adminuser = options['adminuser']
-        adminpass = options['adminpass']
-
-        # create base options:
-        accounts_config = dict(ACCOUNTS_CONFIG)
-        accounts_config['account-manager']['htdigest_file'] = digestfile
-        accounts_config['account-manager']['htdigest_realm'] = realm
-
-        trac = TracAdmin(os.path.abspath(new_env))
-        if not trac.env_check():
-            try:
-                rv = trac.do_initenv('%(project)s %(db)s '
-                                     '%(repo_type)s %(repo_path)s '
-                                     '--inherit=%(inherit)s '
-                                     '--nowiki'
-                                     % options)
-                if rv == 2:
-                    raise SystemExit
-            except SystemExit:
-                print ("Error: Unable to initialise the environment.")
-                return False
-        else:
-            print ("Warning: Environment already exists at %s." % new_env)
-            self.writeconfig(tracini, [{'inherit': {'file': baseini},},])
-
-        base_config = dict(BASE_CONFIG)
-        base_config['trac']['environment_factory'] = \
-            'multiproduct.hooks.MultiProductEnvironmentFactory'
-        base_config['trac']['request_factory'] = \
-            'multiproduct.hooks.ProductRequestFactory'
-        if default_product_prefix != '@':
-            base_config['multiproduct'] = dict(
-                default_product_prefix=default_product_prefix
-            )
-
-        self.writeconfig(baseini, [base_config, accounts_config])
-
-        if os.path.exists(digestfile):
-            backupfile(digestfile)
-        htdigest_create(digestfile, adminuser, realm, adminpass)
-
-        print "Adding TRAC_ADMIN permissions to the admin user %s" % adminuser
-        trac.onecmd('permission add %s TRAC_ADMIN' % adminuser)
-
-        # get fresh TracAdmin instance (original does not know about base.ini)
-        bloodhound = TracAdmin(os.path.abspath(new_env))
-
-        # final upgrade
-        print "Running upgrades"
-        bloodhound.onecmd('upgrade')
-        pages = []
-        pages.append(pkg_resources.resource_filename('bhdashboard',
-                                                 'default-pages'))
-        pages.append(pkg_resources.resource_filename('bhsearch',
-                                                 'default-pages'))
-        bloodhound.onecmd('wiki load %s' % " ".join(pages))
-
-        print "Running wiki upgrades"
-        bloodhound.onecmd('wiki upgrade')
-
-        if self.apply_bhwiki_upgrades:
-            print "Running wiki Bloodhound upgrades"
-            bloodhound.onecmd('wiki bh-upgrade')
-        else:
-            print "Skipping Bloodhound wiki upgrades"
-
-        print "Loading default product wiki"
-        bloodhound.onecmd('product admin %s wiki load %s' %
-                          (default_product_prefix,
-                           " ".join(pages)))
-
-        print "Running default product wiki upgrades"
-        bloodhound.onecmd('product admin %s wiki upgrade' %
-                          default_product_prefix)
-
-        if self.apply_bhwiki_upgrades:
-            print "Running default product Bloodhound wiki upgrades"
-            bloodhound.onecmd('product admin %s wiki bh-upgrade' %
-                              default_product_prefix)
-        else:
-            print "Skipping default product Bloodhound wiki upgrades"
-
-        print """
-You can now start Bloodhound by running:
-
-  tracd --port=8000 %s
-
-And point your browser at http://localhost:8000/%s
-""" % (os.path.abspath(new_env), options['project'])
-        return True
-
-    def writeconfig(self, filepath, dicts=[]):
-        """Writes or updates a config file. A list of dictionaries is used so
-        that options for different aspects of the configuration can be kept
-        separate while being able to update the same sections. Note that the
-        result is order dependent where two dictionaries update the same
-        option.
-        """
-        config = Configuration(filepath)
-        file_changed = False
-        for data in dicts:
-            for section, options in data.iteritems():
-                for key, value in options.iteritems():
-                    if config.get(section, key, None) != value:
-                        # This should be expected to generate a false positive
-                        # when two dictionaries update the same option
-                        file_changed = True
-                    config.set(section, key, value)
-        if file_changed:
-            if os.path.exists(filepath):
-                backupfile(filepath)
-            config.save()
-
-def backupfile(filepath):
-    """Very basic backup routine"""
-    print "Warning: Updating %s." % filepath
-    backuppath = None
-    if not os.path.exists(filepath + '_bak'):
-        backuppath = filepath + '_bak'
-    else:
-        backuptemplate = filepath + '_bak_%d'
-        for i in xrange(MAXBACKUPNUMBER):
-            if not os.path.exists(backuptemplate % i):
-                backuppath = backuptemplate % i
-                break
-    if backuppath is not None:
-        shutil.copyfile(filepath, backuppath)
-        print "Backup created at %s." % backuppath
-    else:
-        print "No backup created (too many other backups found)"
-    return backuppath
-
-def handle_options():
-    """Parses the command line, with basic prompting for choices where options
-    are not specified."""
-    parser = OptionParser()
-
-    # Base Trac Options
-    parser.add_option('--project', dest='project',
-                      help='Set the top project name', default='main')
-    parser.add_option('--source_directory', dest='sourcedir',
-                      help='Specify root source code directory',
-                      default=os.path.normpath(os.path.join(os.getcwd(), '../'))),
-    parser.add_option('--environments_directory', dest='envsdir',
-                      help='Set the directory to contain environments',
-                      default=os.path.join('bloodhound', 'environments'))
-    parser.add_option('-d', '--database-type', dest='dbtype',
-                      help="Specify as either 'sqlite', 'postgres' or 'mysql'",
-                      default='')
-    parser.add_option('--database-string', dest='dbstring',
-                      help=('Advanced: provide a custom database string, '
-                            'overriding other database options'),
-                      default=None)
-    parser.add_option('--database-name', dest='dbname',
-                      help='Specify the database name',
-                      default='bloodhound')
-    parser.add_option('-u', '--user', dest='dbuser',
-                      help='Specify the db user (required for postgres and mysql)',
-                      default='')
-    parser.add_option('-p', '--password', dest='dbpass',
-                      help='Specify the db password (required for postgres and mysql)')
-    parser.add_option('--database-host', dest='dbhost',
-                      help='Specify the database host (optional for postgres and mysql)',
-                      default='localhost')
-    parser.add_option('--database-port', dest='dbport',
-                      help='Specify the database port (optional for postgres and mysql)',
-                      default='5432')
-
-    # Account Manager Options
-    parser.add_option('--admin-password', dest='adminpass',
-                      help='create an admin user in an htdigest file')
-    parser.add_option('--digest-realm', dest='realm', default='bloodhound',
-                      help='authentication realm for htdigest file')
-    parser.add_option('--admin-user', dest='adminuser', default='',
-                      help='admin user name for htdigest file')
-    parser.add_option('--digest-file', dest='digestfile',
-                      default='bloodhound.htdigest',
-                      help='filename for the htdigest file')
-
-    # Repository Options
-    parser.add_option('--repository-type', dest='repo_type',
-                      help='specify the repository type - ')
-    parser.add_option('--repository-path', dest='repo_path',
-                      help='specify the repository type')
-
-    # Multiproduct options
-    parser.add_option('--default-product-prefix', dest='default_product_prefix',
-                      help='Specify prefix for default product (defaults to @')
-
-    (options, args) = parser.parse_args()
-    if args:
-        print "Unprocessed options/arguments: ", args
-
-    def ask_question(question, default=None):
-        """Basic question asking functionality"""
-        if default:
-            answer = raw_input(question % default)
-        else:
-            answer = raw_input(question)
-        return answer if answer else default
-
-    def ask_password(user):
-        """Asks for a password to be provided for setting purposes"""
-        attempts = 3
-        for attempt in range(attempts):
-            if attempt > 0:
-                print "Passwords empty or did not match. Please try again",
-                print "(attempt %d/%d)""" % (attempt+1, attempts)
-            password1 = getpass('Enter a new password for "%s": ' % user)
-            password2 = getpass('Please reenter the password: ')
-            if password1 and password1 == password2:
-                return password1
-        print "Passwords did not match. Quiting."
-        sys.exit(1)
-
-    if options.dbtype.lower() not in SUPPORTED_DBTYPES:
-        answer = ask_question("""
-This installer is able to install Apache Bloodhound with either SQLite,
-PostgreSQL or MySQL databases. SQLite is an easier option for installing
-Bloodhound as SQLite support is built into Python and requires no special
-permissions to run. However, PostgreSQL and MySQL are generally expected to
-be more robust for production use.
-What type of database do you want to instant to (%s)?
-[%%s]: """ % '/'.join(SUPPORTED_DBTYPES), default='sqlite')
-        answer = answer.lower()
-        if answer in SUPPORTED_DBTYPES:
-            options.dbtype = answer
-        else:
-            print "Unrecognized dbtype \"%s\". Quiting." % answer
-            sys.exit(1)
-    else:
-        options.dbtype = options.dbtype.lower()
-
-    if options.dbtype in ('postgres','mysql'):
-        if not options.dbuser:
-            options.dbuser = ask_question("""
-For PostgreSQL/MySQL you need to have PostgreSQL/MySQL installed and you need
-to have created a database user to connect to the database with. Setting this 
-up may require admin access rights to the server.
-DB user name [%s]: """, DEFAULT_DB_USER)
-
-        if not options.dbpass:
-            options.dbpass = ask_password(options.dbuser)
-
-        if not options.dbname:
-            options.dbname = ask_question("""
-For PostgreSQL/MySQL setup, you need to specify a database that you have
-created for Bloodhound to use. This installer currently assumes that this
-database will be empty. DB name [%s]: """, DEFAULT_DB_NAME)
-    if not options.adminuser:
-        options.adminuser = ask_question("""
-Please supply a username for the admin user [%s]: """, DEFAULT_ADMIN_USER)
-    if not options.adminpass:
-        options.adminpass = ask_password(options.adminuser)
-
-    return options
-
+from bhsetup import bloodhound_setup
 
 if __name__ == '__main__':
-    options = handle_options()
-    bsetup = BloodhoundSetup(options)
-    bsetup.setup()
+    bloodhound_setup.run()
diff --git a/installer/tests.py b/installer/tests.py
index b20c61e..846e033 100644
--- a/installer/tests.py
+++ b/installer/tests.py
@@ -23,7 +23,7 @@
 import shutil
 import os
 from tempfile import mkdtemp, NamedTemporaryFile
-from bloodhound_setup import backupfile, BloodhoundSetup
+from bhsetup.bloodhound_setup import backupfile, BloodhoundSetup
 from functools import partial
 
 class BackupfileTest(unittest.TestCase):