예제 #1
0
def _local_restore(db_name_or_dump_key, *, conn_db, temp_db, curr_db, prev_db):
    """
    Performs a restore using a local database
    """
    if db_name_or_dump_key == ':current':
        local_restore_db = curr_db
    elif db_name_or_dump_key == ':previous':
        local_restore_db = prev_db
    else:
        local_restore_db = database.make_config(db_name_or_dump_key[1:])

    if not _db_exists(local_restore_db):
        raise RuntimeError(
            f'local database {local_restore_db["NAME"]} does not exist.'
            ' Use "pgclone ls --local" to see local database keys.')

    # Perform the local restore process. It is completely valid to
    # try to restore the temp_db, so do nothing if this database
    # is provided by the user
    if local_restore_db != temp_db:  # pragma: no branch
        _drop_db(temp_db)
        create_temp_sql = f'''
            CREATE DATABASE {temp_db["NAME"]}
            TEMPLATE {local_restore_db["NAME"]}
        '''
        command.run_psql(create_temp_sql, db=conn_db)

    _set_search_path(temp_db, conn_db)

    return db_name_or_dump_key
예제 #2
0
def _remote_restore(db_name_or_dump_key, *, temp_db, conn_db):
    # We are restoring from a remote dump. If the dump key is not valid,
    # assume it is the latest dump of a database name and get the latest
    # dump key
    dump_key = db_name_or_dump_key
    if not is_valid_dump_key(dump_key):
        dump_key = get_latest_dump_key(db_name_or_dump_key)
        if not dump_key:
            raise RuntimeError('Could not find a dump for database name'
                               f' "{db_name_or_dump_key}"')

    storage_location = settings.get_storage_location()
    file_path = os.path.join(storage_location, dump_key)

    print_msg(f'Creating the temporary restore db')
    _drop_db(temp_db)
    create_temp_sql = f'CREATE DATABASE "{temp_db["NAME"]}"'
    command.run_psql(create_temp_sql, db=conn_db)
    _set_search_path(temp_db, conn_db)

    print_msg(f'Running pg_restore on "{dump_key}"')
    pg_restore_cmd = ('pg_restore --verbose --no-acl --no-owner '
                      f'-d \'{database.get_url(temp_db)}\'')

    # When restoring, we need to ignore errors because there are certain
    # errors we cannot get around when pg restoring some DBs (like Aurora).
    # In the future, we may parse the output of the pg_restore command to see
    # if an unexpected error happened.
    if storage_location.startswith('s3://'):  # pragma: no cover
        command.run_shell(f'aws s3 cp {file_path} - | {pg_restore_cmd}',
                          ignore_errors=True)
    else:
        command.run_shell(f'{pg_restore_cmd} {file_path}', ignore_errors=True)

    return dump_key
예제 #3
0
def _kill_connections_to_database(db):
    conn_db = database.make_config('postgres')

    kill_connections_sql = f'''
        SELECT pg_terminate_backend(pg_stat_activity.pid)
        FROM pg_stat_activity
        WHERE pg_stat_activity.datname = '{db["NAME"]}'
            AND pid <> pg_backend_pid()
    '''
    command.run_psql(kill_connections_sql, db=conn_db)
예제 #4
0
def _set_search_path(db, conn_db):
    """
    pg_restore does not restore the original search_path variable of the
    database, which can cause issues with extensions. This function obtains
    the original search path and sets it as the search path of the restored
    database

    https://www.postgresql.org/message-id/CAKFQuwbWpd1DXA_YY3zXRKGKKLL1ZTi9MHDQ15zJ8ufVBwVJEA%40mail.gmail.com
    """
    with connection.cursor() as cursor:
        cursor.execute('SHOW search_path;')
        search_path = cursor.fetchone()[0]

    set_search_path_sql = (f'ALTER DATABASE "{db["NAME"]}"'
                           f' SET search_path to {search_path}')
    command.run_psql(set_search_path_sql, db=conn_db)
예제 #5
0
def _drop_db(db):
    conn_db = database.make_config('postgres')
    _kill_connections_to_database(db)
    drop_sql = f'DROP DATABASE IF EXISTS "{db["NAME"]}"'
    command.run_psql(drop_sql, db=conn_db)
예제 #6
0
def restore(
    db_name_or_dump_key,
    pre_swap_hooks=None,
    restore_config_name=None,
    reversible=False,
):
    """
    Restores a database dump

    Args:
        db_name_or_dump_key (str): When a database name
            is provided, finds the most recent dump for that
            database and restores it. Restores a specific dump
            when a dump key is provided.
        pre_swap_hooks (List[str], default=None): The list of pre_swap hooks to
            run before swapping the temp restore database with the
            main database. The strings are management command names.
        restore_config_name (str, default=None): The name of the restore
            config to use when running the restore. Configured
            in settings.PGCLONE_RESTORE_CONFIGS

    Returns:
        str: The dump key that was restored.
    """
    settings.validate()

    if not settings.get_allow_restore():  # pragma: no cover
        raise RuntimeError('Restore not allowed')

    if restore_config_name and pre_swap_hooks:  # pragma: no cover
        raise ValueError(
            'Cannot pass in pre_swap_hooks when using a restore config')
    elif pre_swap_hooks:
        restore_config_name = '_custom'
        restore_config = _make_restore_config(pre_swap_hooks=pre_swap_hooks)
    else:
        restore_config_name = restore_config_name or 'default'
        restore_config = _get_restore_config(restore_config_name)

    # Restore works in the following steps with the following databases:
    # 1. Create the temp_db database to perform the restore without
    #    affecting the default_db
    # 2. Call pg_restore on temp_db
    # 3. Apply any pre_swap_hooks to temp_db
    # 4. Terminate all connections on default_db so that we
    #    can swap in the temp_db
    # 4. Rename default_db to prev_db
    # 5. Rename temp_db to default_db
    # 6. Delete prev_db
    #
    # Database variable names below reflect this process. We use the
    # "postgres" database (i.e. conn_db) as the connection database
    # when using psql for most of these commands

    default_db = database.get_default_config()
    temp_db = database.make_config(default_db['NAME'] + '__temp')
    prev_db = database.make_config(default_db['NAME'] + '__prev')
    curr_db = database.make_config(default_db['NAME'] + '__curr')
    conn_db = database.make_config('postgres')
    local_restore_db = None

    if db_name_or_dump_key.startswith(':'):
        dump_key = _local_restore(
            db_name_or_dump_key,
            conn_db=conn_db,
            temp_db=temp_db,
            curr_db=curr_db,
            prev_db=prev_db,
        )
    else:
        dump_key = _remote_restore(db_name_or_dump_key,
                                   temp_db=temp_db,
                                   conn_db=conn_db)

    # When in reversible mode, make a special __curr db snapshot.
    # Avoid this if we are restoring the current db
    if reversible and local_restore_db != curr_db:
        print_msg('Creating snapshot for reversible restore')
        _drop_db(curr_db)
        create_current_db_sql = f'''
            CREATE DATABASE "{curr_db['NAME']}"
             WITH TEMPLATE
            "{temp_db['NAME']}"
        '''
        command.run_psql(create_current_db_sql, db=conn_db)

    # pre-swap hook step
    with pgconnection.route(temp_db):
        for management_command_name in restore_config['pre_swap_hooks']:
            print_msg(
                f'Running "manage.py {management_command_name}" pre_swap hook')
            command.run_management(management_command_name)

    # swap step
    print_msg('Swapping the restored copy with the primary database')
    _drop_db(prev_db)
    _kill_connections_to_database(default_db)
    alter_db_sql = f'''
        ALTER DATABASE "{default_db['NAME']}" RENAME TO
        "{prev_db['NAME']}"
    '''
    # There's a scenario where the default DB may not exist before running
    # this, so just ignore errors on this command
    command.run_psql(alter_db_sql, db=conn_db, ignore_errors=True)

    rename_sql = f'''
        ALTER DATABASE "{temp_db["NAME"]}"
        RENAME TO "{default_db["NAME"]}"
    '''
    command.run_psql(rename_sql, db=conn_db)

    if not reversible:
        print_msg('Cleaning old pgclone resources')
        _drop_db(curr_db)
        _drop_db(prev_db)

    print_msg(f'Successfully restored dump "{dump_key}"')

    return dump_key