| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634 | import jsonimport osimport shutilimport subprocessimport sysimport tempfileimport pymongoimport pytestimport ruamel.yamldef write_configuration(    source_directory,    config_path,    repository_path,    user_runtime_directory,    postgresql_dump_format='custom',    postgresql_all_dump_format=None,    mariadb_mysql_all_dump_format=None,    mongodb_dump_format='archive',):    '''    Write out borgmatic configuration into a file at the config path. Set the options so as to work    for testing. This includes injecting the given repository path, borgmatic source directory for    storing database dumps, dump format (for PostgreSQL), and encryption passphrase.    '''    postgresql_all_format_option = (        f'format: {postgresql_all_dump_format}' if postgresql_all_dump_format else ''    )    mariadb_mysql_dump_format_option = (        f'format: {mariadb_mysql_all_dump_format}' if mariadb_mysql_all_dump_format else ''    )    config_yaml = f'''source_directories:    - {source_directory}repositories:    - path: {repository_path}user_runtime_directory: {user_runtime_directory}encryption_passphrase: "test"postgresql_databases:    - name: test      hostname: postgresql      username: postgres      password: test      format: {postgresql_dump_format}    - name: all      {postgresql_all_format_option}      hostname: postgresql      username: postgres      password: testmariadb_databases:    - name: test      hostname: mariadb      username: root      password: test    - name: all      {mariadb_mysql_dump_format_option}      hostname: mariadb      username: root      password: testmysql_databases:    - name: test      hostname: not-actually-mysql      username: root      password: test    - name: all      {mariadb_mysql_dump_format_option}      hostname: not-actually-mysql      username: root      password: testmongodb_databases:    - name: test      hostname: mongodb      username: root      password: test      authentication_database: admin      format: {mongodb_dump_format}    - name: all      hostname: mongodb      username: root      password: testsqlite_databases:    - name: sqlite_test      path: /tmp/sqlite_test.db'''    with open(config_path, 'w') as config_file:        config_file.write(config_yaml)    return ruamel.yaml.YAML(typ='safe').load(config_yaml)@pytest.mark.parametrize(    'postgresql_all_dump_format,mariadb_mysql_all_dump_format',    (        (None, None),        ('custom', 'sql'),    ),)def write_custom_restore_configuration(    source_directory,    config_path,    repository_path,    user_runtime_directory,    postgresql_dump_format='custom',    postgresql_all_dump_format=None,    mariadb_mysql_all_dump_format=None,    mongodb_dump_format='archive',):    '''    Write out borgmatic configuration into a file at the config path. Set the options so as to work    for testing with custom restore options. This includes a custom restore_hostname, restore_port,    restore_username, restore_password and restore_path.    '''    config_yaml = f'''source_directories:    - {source_directory}repositories:    - path: {repository_path}user_runtime_directory: {user_runtime_directory}encryption_passphrase: "test"postgresql_databases:    - name: test      hostname: postgresql      username: postgres      password: test      format: {postgresql_dump_format}      restore_hostname: postgresql2      restore_port: 5433      restore_password: test2mariadb_databases:    - name: test      hostname: mariadb      username: root      password: test      restore_hostname: mariadb2      restore_port: 3307      restore_username: root      restore_password: test2mysql_databases:    - name: test      hostname: not-actually-mysql      username: root      password: test      restore_hostname: not-actually-mysql2      restore_port: 3307      restore_username: root      restore_password: test2mongodb_databases:    - name: test      hostname: mongodb      username: root      password: test      authentication_database: admin      format: {mongodb_dump_format}      restore_hostname: mongodb2      restore_port: 27018      restore_username: root2      restore_password: test2sqlite_databases:    - name: sqlite_test      path: /tmp/sqlite_test.db      restore_path: /tmp/sqlite_test2.db'''    with open(config_path, 'w') as config_file:        config_file.write(config_yaml)    return ruamel.yaml.YAML(typ='safe').load(config_yaml)def write_simple_custom_restore_configuration(    source_directory,    config_path,    repository_path,    user_runtime_directory,    postgresql_dump_format='custom',):    '''    Write out borgmatic configuration into a file at the config path. Set the options so as to work    for testing with custom restore options, but this time using CLI arguments. This includes a    custom restore_hostname, restore_port, restore_username and restore_password as we only test    these options for PostgreSQL.    '''    config_yaml = f'''source_directories:    - {source_directory}repositories:    - path: {repository_path}user_runtime_directory: {user_runtime_directory}encryption_passphrase: "test"postgresql_databases:    - name: test      hostname: postgresql      username: postgres      password: test      format: {postgresql_dump_format}'''    with open(config_path, 'w') as config_file:        config_file.write(config_yaml)    return ruamel.yaml.YAML(typ='safe').load(config_yaml)def get_connection_params(database, use_restore_options=False):    hostname = (database.get('restore_hostname') if use_restore_options else None) or database.get(        'hostname'    )    port = (database.get('restore_port') if use_restore_options else None) or database.get('port')    username = (database.get('restore_username') if use_restore_options else None) or database.get(        'username'    )    password = (database.get('restore_password') if use_restore_options else None) or database.get(        'password'    )    return (hostname, port, username, password)def run_postgresql_command(command, config, use_restore_options=False):    (hostname, port, username, password) = get_connection_params(        config['postgresql_databases'][0], use_restore_options    )    subprocess.check_call(        [            '/usr/bin/psql',            f'--host={hostname}',            f'--port={port or 5432}',            f"--username={username or 'root'}",            f'--command={command}',            'test',        ],        env={'PGPASSWORD': password},    )def run_mariadb_command(command, config, use_restore_options=False, binary_name='mariadb'):    (hostname, port, username, password) = get_connection_params(        config[f'{binary_name}_databases'][0], use_restore_options    )    subprocess.check_call(        [            f'/usr/bin/{binary_name}',            f'--host={hostname}',            f'--port={port or 3306}',            f'--user={username}',            f'--execute={command}',            'test',        ],        env={'MYSQL_PWD': password},    )def get_mongodb_database_client(config, use_restore_options=False):    (hostname, port, username, password) = get_connection_params(        config['mongodb_databases'][0], use_restore_options    )    return pymongo.MongoClient(f'mongodb://{username}:{password}@{hostname}:{port or 27017}').testdef run_sqlite_command(command, config, use_restore_options=False):    database = config['sqlite_databases'][0]    path = (database.get('restore_path') if use_restore_options else None) or database.get('path')    subprocess.check_call(        [            '/usr/bin/sqlite3',            path,            command,            '.exit',        ],    )DEFAULT_HOOK_NAMES = {'postgresql', 'mariadb', 'mysql', 'mongodb', 'sqlite'}def create_test_tables(config, use_restore_options=False):    '''    Create test tables for borgmatic to dump and backup.    '''    command = 'create table test{id} (thing int); insert into test{id} values (1);'    if 'postgresql_databases' in config:        run_postgresql_command(command.format(id=1), config, use_restore_options)    if 'mariadb_databases' in config:        run_mariadb_command(command.format(id=2), config, use_restore_options)    if 'mysql_databases' in config:        run_mariadb_command(command.format(id=3), config, use_restore_options, binary_name='mysql')    if 'mongodb_databases' in config:        get_mongodb_database_client(config, use_restore_options)['test4'].insert_one({'thing': 1})    if 'sqlite_databases' in config:        run_sqlite_command(command.format(id=5), config, use_restore_options)def drop_test_tables(config, use_restore_options=False):    '''    Drop the test tables in preparation for borgmatic restoring them.    '''    command = 'drop table if exists test{id};'    if 'postgresql_databases' in config:        run_postgresql_command(command.format(id=1), config, use_restore_options)    if 'mariadb_databases' in config:        run_mariadb_command(command.format(id=2), config, use_restore_options)    if 'mysql_databases' in config:        run_mariadb_command(command.format(id=3), config, use_restore_options, binary_name='mysql')    if 'mongodb_databases' in config:        get_mongodb_database_client(config, use_restore_options)['test4'].drop()    if 'sqlite_databases' in config:        run_sqlite_command(command.format(id=5), config, use_restore_options)def select_test_tables(config, use_restore_options=False):    '''    Select the test tables to make sure they exist.    Raise if the expected tables cannot be selected, for instance if a restore hasn't worked as    expected.    '''    command = 'select count(*) from test{id};'    if 'postgresql_databases' in config:        run_postgresql_command(command.format(id=1), config, use_restore_options)    if 'mariadb_databases' in config:        run_mariadb_command(command.format(id=2), config, use_restore_options)    if 'mysql_databases' in config:        run_mariadb_command(command.format(id=3), config, use_restore_options, binary_name='mysql')    if 'mongodb_databases' in config:        assert (            get_mongodb_database_client(config, use_restore_options)['test4'].count_documents(                filter={}            )            > 0        )    if 'sqlite_databases' in config:        run_sqlite_command(command.format(id=5), config, use_restore_options)def test_database_dump_and_restore():    # Create a Borg repository.    temporary_directory = tempfile.mkdtemp()    repository_path = os.path.join(temporary_directory, 'test.borg')    # Write out a special file to ensure that it gets properly excluded and Borg doesn't hang on it.    os.mkfifo(os.path.join(temporary_directory, 'special_file'))    original_working_directory = os.getcwd()    try:        config_path = os.path.join(temporary_directory, 'test.yaml')        config = write_configuration(            temporary_directory, config_path, repository_path, temporary_directory        )        create_test_tables(config)        select_test_tables(config)        subprocess.check_call(            [                'borgmatic',                '-v',                '2',                '--config',                config_path,                'repo-create',                '--encryption',                'repokey',            ]        )        # Run borgmatic to generate a backup archive including database dumps.        subprocess.check_call(['borgmatic', 'create', '--config', config_path, '-v', '2'])        # Get the created archive name.        output = subprocess.check_output(            ['borgmatic', '--config', config_path, 'list', '--json']        ).decode(sys.stdout.encoding)        parsed_output = json.loads(output)        assert len(parsed_output) == 1        assert len(parsed_output[0]['archives']) == 1        archive_name = parsed_output[0]['archives'][0]['archive']        # Restore the databases from the archive.        drop_test_tables(config)        subprocess.check_call(            ['borgmatic', '-v', '2', '--config', config_path, 'restore', '--archive', archive_name]        )        # Ensure the test tables have actually been restored.        select_test_tables(config)    finally:        os.chdir(original_working_directory)        shutil.rmtree(temporary_directory)        drop_test_tables(config)def test_database_dump_and_restore_with_restore_cli_flags():    # Create a Borg repository.    temporary_directory = tempfile.mkdtemp()    repository_path = os.path.join(temporary_directory, 'test.borg')    original_working_directory = os.getcwd()    try:        config_path = os.path.join(temporary_directory, 'test.yaml')        config = write_simple_custom_restore_configuration(            temporary_directory, config_path, repository_path, temporary_directory        )        create_test_tables(config)        select_test_tables(config)        subprocess.check_call(            [                'borgmatic',                '-v',                '2',                '--config',                config_path,                'repo-create',                '--encryption',                'repokey',            ]        )        # Run borgmatic to generate a backup archive including a database dump.        subprocess.check_call(['borgmatic', 'create', '--config', config_path, '-v', '2'])        # Get the created archive name.        output = subprocess.check_output(            ['borgmatic', '--config', config_path, 'list', '--json']        ).decode(sys.stdout.encoding)        parsed_output = json.loads(output)        assert len(parsed_output) == 1        assert len(parsed_output[0]['archives']) == 1        archive_name = parsed_output[0]['archives'][0]['archive']        # Restore the database from the archive.        drop_test_tables(config)        subprocess.check_call(            [                'borgmatic',                '-v',                '2',                '--config',                config_path,                'restore',                '--archive',                archive_name,                '--hostname',                'postgresql2',                '--port',                '5433',                '--password',                'test2',            ]        )        # Ensure the test tables have actually been restored. But first modify the config to contain        # the altered restore values from the borgmatic command above. This ensures that the test        # tables are selected from the correct database.        database = config['postgresql_databases'][0]        database['restore_hostname'] = 'postgresql2'        database['restore_port'] = '5433'        database['restore_password'] = 'test2'        select_test_tables(config, use_restore_options=True)    finally:        os.chdir(original_working_directory)        shutil.rmtree(temporary_directory)        drop_test_tables(config)        drop_test_tables(config, use_restore_options=True)def test_database_dump_and_restore_with_restore_configuration_options():    # Create a Borg repository.    temporary_directory = tempfile.mkdtemp()    repository_path = os.path.join(temporary_directory, 'test.borg')    original_working_directory = os.getcwd()    try:        config_path = os.path.join(temporary_directory, 'test.yaml')        config = write_custom_restore_configuration(            temporary_directory, config_path, repository_path, temporary_directory        )        create_test_tables(config)        select_test_tables(config)        subprocess.check_call(            [                'borgmatic',                '-v',                '2',                '--config',                config_path,                'repo-create',                '--encryption',                'repokey',            ]        )        # Run borgmatic to generate a backup archive including a database dump.        subprocess.check_call(['borgmatic', 'create', '--config', config_path, '-v', '2'])        # Get the created archive name.        output = subprocess.check_output(            ['borgmatic', '--config', config_path, 'list', '--json']        ).decode(sys.stdout.encoding)        parsed_output = json.loads(output)        assert len(parsed_output) == 1        assert len(parsed_output[0]['archives']) == 1        archive_name = parsed_output[0]['archives'][0]['archive']        # Restore the database from the archive.        drop_test_tables(config)        subprocess.check_call(            ['borgmatic', '-v', '2', '--config', config_path, 'restore', '--archive', archive_name]        )        # Ensure the test tables have actually been restored.        select_test_tables(config, use_restore_options=True)    finally:        os.chdir(original_working_directory)        shutil.rmtree(temporary_directory)        drop_test_tables(config)        drop_test_tables(config, use_restore_options=True)def test_database_dump_and_restore_with_directory_format():    # Create a Borg repository.    temporary_directory = tempfile.mkdtemp()    repository_path = os.path.join(temporary_directory, 'test.borg')    original_working_directory = os.getcwd()    try:        config_path = os.path.join(temporary_directory, 'test.yaml')        config = write_configuration(            temporary_directory,            config_path,            repository_path,            temporary_directory,            postgresql_dump_format='directory',            mongodb_dump_format='directory',        )        create_test_tables(config)        select_test_tables(config)        subprocess.check_call(            [                'borgmatic',                '-v',                '2',                '--config',                config_path,                'repo-create',                '--encryption',                'repokey',            ]        )        # Run borgmatic to generate a backup archive including a database dump.        subprocess.check_call(['borgmatic', 'create', '--config', config_path, '-v', '2'])        # Restore the database from the archive.        drop_test_tables(config)        subprocess.check_call(            ['borgmatic', '--config', config_path, 'restore', '--archive', 'latest']        )        # Ensure the test tables have actually been restored.        select_test_tables(config)    finally:        os.chdir(original_working_directory)        shutil.rmtree(temporary_directory)        drop_test_tables(config)def test_database_dump_with_error_causes_borgmatic_to_exit():    # Create a Borg repository.    temporary_directory = tempfile.mkdtemp()    repository_path = os.path.join(temporary_directory, 'test.borg')    original_working_directory = os.getcwd()    try:        config_path = os.path.join(temporary_directory, 'test.yaml')        write_configuration(temporary_directory, config_path, repository_path, temporary_directory)        subprocess.check_call(            [                'borgmatic',                '-v',                '2',                '--config',                config_path,                'repo-create',                '--encryption',                'repokey',            ]        )        # Run borgmatic with a config override such that the database dump fails.        with pytest.raises(subprocess.CalledProcessError):            subprocess.check_call(                [                    'borgmatic',                    'create',                    '--config',                    config_path,                    '-v',                    '2',                    '--override',                    "hooks.postgresql_databases=[{'name': 'nope'}]",  # noqa: FS003                ]            )    finally:        os.chdir(original_working_directory)        shutil.rmtree(temporary_directory)
 |