| #!/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' |
| DEFAULT_PROJECT = 'main' |
| DEFAULT_ENVSDIR = os.path.join('bloodhound', 'environments') |
| |
| 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'] = DEFAULT_PROJECT |
| if 'envsdir' not in options: |
| options['envsdir'] = DEFAULT_ENVSDIR |
| |
| # 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='') |
| 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='') |
| 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) |
| |
| if not options.project: |
| options.project = ask_question(""" |
| For the installation process, you can specify the top project name. |
| This installer currently assumes that project name is 'main'. |
| Project name [%s]: """, DEFAULT_PROJECT) |
| |
| if not options.envsdir: |
| options.envsdir = ask_question(""" |
| For the installation process, you can specify the directory to contain environments. |
| This installer currently assumes that environments directory is './bloodhound/environments'. |
| Environments directory [%s]: """, DEFAULT_ENVSDIR) |
| |
| return options |
| |
| |
| def run(): |
| options = handle_options() |
| bsetup = BloodhoundSetup(options) |
| bsetup.setup() |
| |
| if __name__ == '__main__': |
| run() |