Changeset - 307c876a6e89
[Not reviewed]
default
0 3 0
Mads Kiilerich (mads) - 6 years ago 2020-06-25 01:51:18
mads@kiilerich.com
db: introduce db-create --reuse option

Support use of an existing database so the Kallithea database user doesn't have
to be granted permissions to create databases.

The existing database must of course have been created "correctly", for example
using the right charset and collation on MariaDB/MySQL. Creating the database
manually also provides a way to avoid the hardcoded defaults.
3 files changed with 25 insertions and 6 deletions:
0 comments (0 inline, 0 general)
docs/setup.rst
Show inline comments
 
.. _setup:
 

	
 
=====
 
Setup
 
=====
 

	
 

	
 
Setting up Kallithea
 
--------------------
 

	
 
Some further details to the steps mentioned in the overview.
 

	
 
Create low level configuration file
 
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 

	
 
First, you will need to create a Kallithea configuration file. The
 
configuration file is a ``.ini`` file that contains various low level settings
 
for Kallithea, e.g. configuration of how to use database, web server, email,
 
and logging.
 

	
 
Run the following command to create the file ``my.ini`` in the current
 
directory::
 

	
 
    kallithea-cli config-create my.ini http_server=waitress
 

	
 
To get a good starting point for your configuration, specify the http server
 
you intend to use. It can be ``waitress``, ``gearbox``, ``gevent``,
 
``gunicorn``, or ``uwsgi``. (Apache ``mod_wsgi`` will not use this
 
configuration file, and it is fine to keep the default http_server configuration
 
unused. ``mod_wsgi`` is configured using ``httpd.conf`` directives and a WSGI
 
wrapper script.)
 

	
 
Extra custom settings can be specified like::
 

	
 
    kallithea-cli config-create my.ini host=8.8.8.8 "[handler_console]" formatter=color_formatter
 

	
 
Populate the database
 
^^^^^^^^^^^^^^^^^^^^^
 

	
 
Next, you need to create the databases used by Kallithea. Kallithea currently
 
supports PostgreSQL, SQLite and MariaDB/MySQL databases. It is recommended to
 
start out using SQLite (the default) and move to PostgreSQL if it becomes a
 
bottleneck or to get a "proper" database. MariaDB/MySQL is also supported.
 

	
 
For PostgreSQL, run ``pip install psycopg2`` to get the database driver. Make
 
sure the PostgreSQL server is initialized and running. Make sure you have a
 
database user with password authentication with permissions to create databases
 
- for example by running::
 

	
 
    sudo -u postgres createuser 'kallithea' --pwprompt --createdb
 

	
 
For MariaDB/MySQL, run ``pip install mysqlclient`` to get the ``MySQLdb``
 
database driver. Make sure the database server is initialized and running. Make
 
sure you have a database user with password authentication with permissions to
 
create the database - for example by running::
 

	
 
    echo 'CREATE USER "kallithea"@"localhost" IDENTIFIED BY "password"' | sudo -u mysql mysql
 
    echo 'GRANT ALL PRIVILEGES ON `kallithea`.* TO "kallithea"@"localhost"' | sudo -u mysql mysql
 

	
 
Check and adjust ``sqlalchemy.url`` in your ``my.ini`` configuration file to use
 
this database.
 

	
 
Create the database, tables, and initial content by running the following
 
command::
 

	
 
    kallithea-cli db-create -c my.ini
 

	
 
This will first prompt you for a "root" path. This "root" path is the location
 
where Kallithea will store all of its repositories on the current machine. This
 
location must be writable for the running Kallithea application. Next,
 
``db-create`` will prompt you for a username and password for the initial admin
 
account it sets up for you.
 

	
 
The ``db-create`` values can also be given on the command line.
 
Example::
 

	
 
    kallithea-cli db-create -c my.ini --user=nn --password=secret --email=nn@example.com --repos=/srv/repos
 

	
 
The ``db-create`` command will create all needed tables and an
 
admin account. When choosing a root path you can either use a new
 
empty location, or a location which already contains existing
 
repositories. If you choose a location which contains existing
 
repositories Kallithea will add all of the repositories at the chosen
 
location to its database.  (Note: make sure you specify the correct
 
path to the root).
 

	
 
.. note:: It is also possible to use an existing database. For example,
 
          when using PostgreSQL without granting general createdb privileges to
 
          the PostgreSQL kallithea user, set ``sqlalchemy.url =
 
          postgresql://kallithea:password@localhost/kallithea`` and create the
 
          database like::
 

	
 
              sudo -u postgres createdb 'kallithea' --owner 'kallithea'
 
              kallithea-cli db-create -c my.ini --reuse
 

	
 
Prepare front-end files
 
^^^^^^^^^^^^^^^^^^^^^^^
 

	
 
Finally, the front-end files must be prepared. This requires ``npm`` version 6
 
or later, which needs ``node.js`` (version 12 or later). Prepare the front-end
 
by running::
 

	
 
    kallithea-cli front-end-build
 

	
 
Running
 
^^^^^^^
 

	
 
You are now ready to use Kallithea. To run it using a gearbox web server,
 
simply execute::
 

	
 
    gearbox serve -c my.ini
 

	
 
- This command runs the Kallithea server. The web app should be available at
 
  http://127.0.0.1:5000. The IP address and port is configurable via the
 
  configuration file created in the previous step.
 
- Log in to Kallithea using the admin account created when running ``db-create``.
 
- The default permissions on each repository is read, and the owner is admin.
 
  Remember to update these if needed.
 
- In the admin panel you can toggle LDAP, anonymous, and permissions
 
  settings, as well as edit more advanced options on users and
 
  repositories.
 

	
 

	
 
Internationalization (i18n support)
 
-----------------------------------
 

	
 
The Kallithea web interface is automatically displayed in the user's preferred
 
language, as indicated by the browser. Thus, different users may see the
 
application in different languages. If the requested language is not available
 
(because the translation file for that language does not yet exist or is
 
incomplete), English is used.
 

	
 
If you want to disable automatic language detection and instead configure a
 
fixed language regardless of user preference, set ``i18n.enabled = false`` and
 
specify another language by setting ``i18n.lang`` in the Kallithea
 
configuration file.
 

	
 

	
 
Using Kallithea with SSH
 
------------------------
 

	
 
Kallithea supports repository access via SSH key based authentication.
 
This means:
 

	
 
- repository URLs like ``ssh://kallithea@example.com/name/of/repository``
 

	
 
- all network traffic for both read and write happens over the SSH protocol on
 
  port 22, without using HTTP/HTTPS nor the Kallithea WSGI application
 

	
 
- encryption and authentication protocols are managed by the system's ``sshd``
 
  process, with all users using the same Kallithea system user (e.g.
 
  ``kallithea``) when connecting to the SSH server, but with users' public keys
 
  in the Kallithea system user's `.ssh/authorized_keys` file granting each user
 
  sandboxed access to the repositories.
 

	
 
- users and admins can manage SSH public keys in the web UI
 

	
 
- in their SSH client configuration, users can configure how the client should
 
  control access to their SSH key - without passphrase, with passphrase, and
 
  optionally with passphrase caching in the local shell session (``ssh-agent``).
 
  This is standard SSH functionality, not something Kallithea provides or
 
  interferes with.
 

	
 
- network communication between client and server happens in a bidirectional
 
  stateful stream, and will in some cases be faster than HTTP/HTTPS with several
 
  stateless round-trips.
 

	
 
.. note:: At this moment, repository access via SSH has been tested on Unix
 
    only. Windows users that care about SSH are invited to test it and report
 
    problems, ideally contributing patches that solve these problems.
 

	
 
Users and admins can upload SSH public keys (e.g. ``.ssh/id_rsa.pub``) through
 
the web interface. The server's ``.ssh/authorized_keys`` file is automatically
 
maintained with an entry for each SSH key. Each entry will tell ``sshd`` to run
 
``kallithea-cli`` with the ``ssh-serve`` sub-command and the right Kallithea user ID
 
when encountering the corresponding SSH key.
 

	
 
To enable SSH repository access, Kallithea must be configured with the path to
 
the ``.ssh/authorized_keys`` file for the Kallithea user, and the path to the
 
``kallithea-cli`` command. Put something like this in the ``.ini`` file::
 

	
 
    ssh_enabled = true
 
    ssh_authorized_keys = /home/kallithea/.ssh/authorized_keys
 
    kallithea_cli_path = /srv/kallithea/venv/bin/kallithea-cli
 

	
 
The SSH service must be running, and the Kallithea user account must be active
 
(not necessarily with password access, but public key access must be enabled),
 
all file permissions must be set as sshd wants it, and ``authorized_keys`` must
 
be writeable by the Kallithea user.
 

	
 
.. note:: The ``authorized_keys`` file will be rewritten from scratch on
kallithea/bin/kallithea_cli_db.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
import click
 

	
 
import kallithea
 
import kallithea.bin.kallithea_cli_base as cli_base
 
from kallithea.lib.db_manage import DbManage
 
from kallithea.model.meta import Session
 

	
 

	
 
@cli_base.register_command(config_file=True)
 
@click.option('--reuse/--no-reuse', default=False,
 
        help='Reuse and clean existing database instead of dropping and creating (default: no reuse)')
 
@click.option('--user', help='Username of administrator account.')
 
@click.option('--password', help='Password for administrator account.')
 
@click.option('--email', help='Email address of administrator account.')
 
@click.option('--repos', help='Absolute path to repositories location.')
 
@click.option('--force-yes', is_flag=True, help='Answer yes to every question.')
 
@click.option('--force-no', is_flag=True, help='Answer no to every question.')
 
@click.option('--public-access/--no-public-access', default=True,
 
        help='Enable/disable public access on this installation (default: enable)')
 
def db_create(user, password, email, repos, force_yes, force_no, public_access):
 
def db_create(user, password, email, repos, force_yes, force_no, public_access, reuse):
 
    """Initialize the database.
 

	
 
    Create all required tables in the database specified in the configuration
 
    file. Create the administrator account. Set certain settings based on
 
    values you provide.
 

	
 
    You can pass the answers to all questions as options to this command.
 
    """
 
    dbconf = kallithea.CONFIG['sqlalchemy.url']
 

	
 
    # force_ask should be True (yes), False (no), or None (ask)
 
    if force_yes:
 
        force_ask = True
 
    elif force_no:
 
        force_ask = False
 
    else:
 
        force_ask = None
 

	
 
    cli_args = dict(
 
            username=user,
 
            password=password,
 
            email=email,
 
            repos_location=repos,
 
            force_ask=force_ask,
 
            public_access=public_access,
 
    )
 
    dbmanage = DbManage(dbconf=dbconf, root=kallithea.CONFIG['here'],
 
                        tests=False, cli_args=cli_args)
 
    dbmanage.create_tables()
 
    dbmanage.create_tables(reuse_database=reuse)
 
    repo_root_path = dbmanage.prompt_repo_root_path(None)
 
    dbmanage.create_settings(repo_root_path)
 
    dbmanage.create_default_user()
 
    dbmanage.admin_prompt()
 
    dbmanage.create_permissions()
 
    dbmanage.populate_default_permissions()
 
    Session().commit()
 

	
 
    # initial repository scan
 
    kallithea.config.application.make_app(
 
            kallithea.CONFIG.global_conf, **kallithea.CONFIG.local_conf)
 
    added, _ = kallithea.lib.utils.repo2db_mapper(kallithea.model.scm.ScmModel().repo_scan())
 
    if added:
 
        click.echo('Initial repository scan: added following repositories:')
 
        click.echo('\t%s' % '\n\t'.join(added))
 
    else:
 
        click.echo('Initial repository scan: no repositories found.')
 

	
 
    click.echo('Database set up successfully.')
 
    click.echo("Don't forget to build the front-end using 'kallithea-cli front-end-build'.")
kallithea/lib/db_manage.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
"""
 
kallithea.lib.db_manage
 
~~~~~~~~~~~~~~~~~~~~~~~
 

	
 
Database creation, and setup module for Kallithea. Used for creation
 
of database as well as for migration operations
 

	
 
This file was forked by the Kallithea project in July 2014.
 
Original author and date, and relevant copyright and licensing information is below:
 
:created_on: Apr 10, 2010
 
:author: marcink
 
:copyright: (c) 2013 RhodeCode GmbH, and others.
 
:license: GPLv3, see LICENSE.md for more details.
 
"""
 

	
 
import logging
 
import os
 
import sys
 
import uuid
 

	
 
import alembic.command
 
import alembic.config
 
import sqlalchemy
 
from sqlalchemy.engine import create_engine
 

	
 
from kallithea.model.base import init_model
 
from kallithea.model.db import Repository, Setting, Ui, User
 
from kallithea.model.meta import Base, Session
 
from kallithea.model.permission import PermissionModel
 
from kallithea.model.user import UserModel
 

	
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
class DbManage(object):
 
    def __init__(self, dbconf, root, tests=False, SESSION=None, cli_args=None):
 
        self.dbname = dbconf.split('/')[-1]
 
        self.tests = tests
 
        self.root = root
 
        self.dburi = dbconf
 
        self.cli_args = cli_args or {}
 
        self.init_db(SESSION=SESSION)
 

	
 
    def _ask_ok(self, msg):
 
        """Invoke ask_ok unless the force_ask option provides the answer"""
 
        force_ask = self.cli_args.get('force_ask')
 
        if force_ask is not None:
 
            return force_ask
 
        from kallithea.lib.utils2 import ask_ok
 
        return ask_ok(msg)
 

	
 
    def init_db(self, SESSION=None):
 
        if SESSION:
 
            self.sa = SESSION
 
        else:
 
            # init new sessions
 
            engine = create_engine(self.dburi)
 
            init_model(engine)
 
            self.sa = Session()
 

	
 
    def create_tables(self):
 
    def create_tables(self, reuse_database=False):
 
        """
 
        Create database (optional) and tables.
 
        The database will be dropped (if it exists) and a new one created.
 
        If reuse_database is false, the database will be dropped (if it exists)
 
        and a new one created. If true, the existing database will be reused
 
        and cleaned for content.
 
        """
 
        url = sqlalchemy.engine.url.make_url(self.dburi)
 
        database = url.database
 
        log.info("The existing database %r will be destroyed and created." % database)
 
        if reuse_database:
 
            log.info("The content of the database %r will be destroyed and new tables created." % database)
 
        else:
 
            log.info("The existing database %r will be destroyed and a new one created." % database)
 

	
 
        if not self.tests:
 
            if not self._ask_ok('Are you sure to destroy old database? [y/n]'):
 
                print('Nothing done.')
 
                sys.exit(0)
 

	
 
        if True:
 
        if reuse_database:
 
            Base.metadata.drop_all()
 
        else:
 
            if url.drivername == 'mysql':
 
                url.database = None  # don't connect to the database (it might not exist)
 
                engine = sqlalchemy.create_engine(url)
 
                with engine.connect() as conn:
 
                    conn.execute('DROP DATABASE IF EXISTS `%s`' % database)
 
                    conn.execute('CREATE DATABASE `%s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci' % database)
 
            elif url.drivername == 'postgresql':
 
                from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
 
                url.database = 'postgres'  # connect to the system database (as the real one might not exist)
 
                engine = sqlalchemy.create_engine(url)
 
                with engine.connect() as conn:
 
                    conn.connection.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
 
                    conn.execute('DROP DATABASE IF EXISTS "%s"' % database)
 
                    conn.execute('CREATE DATABASE "%s"' % database)
 
            else:
 
                # Some databases enforce foreign key constraints and Base.metadata.drop_all() doesn't work, but this is
 
                # known to work on SQLite - possibly not on other databases with strong referential integrity
 
                Base.metadata.drop_all()
 

	
 
        Base.metadata.create_all(checkfirst=False)
 

	
 
        # Create an Alembic configuration and generate the version table,
 
        # "stamping" it with the most recent Alembic migration revision, to
 
        # tell Alembic that all the schema upgrades are already in effect.
 
        alembic_cfg = alembic.config.Config()
 
        alembic_cfg.set_main_option('script_location', 'kallithea:alembic')
 
        alembic_cfg.set_main_option('sqlalchemy.url', self.dburi)
 
        # This command will give an error in an Alembic multi-head scenario,
 
        # but in practice, such a scenario should not come up during database
 
        # creation, even during development.
 
        alembic.command.stamp(alembic_cfg, 'head')
 

	
 
        log.info('Created tables for %s', self.dbname)
 

	
 
    def admin_prompt(self, second=False):
 
        if not self.tests:
 
            import getpass
 

	
 
            username = self.cli_args.get('username')
 
            password = self.cli_args.get('password')
 
            email = self.cli_args.get('email')
 

	
 
            def get_password():
 
                password = getpass.getpass('Specify admin password '
 
                                           '(min 6 chars):')
 
                confirm = getpass.getpass('Confirm password:')
 

	
 
                if password != confirm:
 
                    log.error('passwords mismatch')
 
                    return False
 
                if len(password) < 6:
 
                    log.error('password is to short use at least 6 characters')
 
                    return False
 

	
 
                return password
 
            if username is None:
 
                username = input('Specify admin username:')
 
            if password is None:
 
                password = get_password()
 
                if not password:
 
                    # second try
 
                    password = get_password()
 
                    if not password:
 
                        sys.exit()
 
            if email is None:
 
                email = input('Specify admin email:')
 
            self.create_user(username, password, email, True)
 
        else:
 
            log.info('creating admin and regular test users')
 
            from kallithea.tests.base import TEST_USER_ADMIN_LOGIN, \
 
                TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL, \
 
                TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, \
 
                TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
 
                TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
 

	
 
            self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
 
                             TEST_USER_ADMIN_EMAIL, True)
 

	
 
            self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
 
                             TEST_USER_REGULAR_EMAIL, False)
 

	
 
            self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
 
                             TEST_USER_REGULAR2_EMAIL, False)
 

	
 
    def create_auth_plugin_options(self, skip_existing=False):
 
        """
 
        Create default auth plugin settings, and make it active
 

	
 
        :param skip_existing:
 
        """
 

	
 
        for k, v, t in [('auth_plugins', 'kallithea.lib.auth_modules.auth_internal', 'list'),
 
                        ('auth_internal_enabled', 'True', 'bool')]:
 
            if skip_existing and Setting.get_by_name(k) is not None:
 
                log.debug('Skipping option %s', k)
 
                continue
0 comments (0 inline, 0 general)