Перенос IredMail с одного сервера на другой с одновременным обновлением (с 0.9.0 на 1.1)

У меня IredMail 0.9.0 (выпущен 31 декабря 2014 г.) установлен непосредственно на хосте и Я хочу перенести его на новый сервер с dockerized IredMail + LetsEncrypt SSL-сертификат + все, что обернуто docker-compose.

Я нашел 3 способа, как это можно сделать:

  1. Обновление с одной версии перейти к следующему шагу в соответствии с «Руководством по обновлению» из здесь , а затем перенести БД в dockerized IredMail - это слишком долго и что-то можно упустить, потому что у меня пробел в 12 версий.

  2. Вносите только изменения в БД в соответствии с инструкциями из «Руководства по обновлению» - опять же, что-то можно пропустить.

  3. Запустите dockerized IredMail и Transfer данные из старой БД в новую БД, потому что в dockerized IredMail уже все настроено (поскольку полное доменное имя (FQDN) такое же) и обновлены схемы БД. Это можно сделать либо вручную, либо с помощью сценария (который можно найти в ответе ниже).

0
задан 27 April 2020 в 16:22
1 ответ

Я прочитал все «Учебники по обновлению», чтобы найти все изменения БД, необходимые для обновления с 0.9.0 до 1.1, и обнаружил, что 3-й вариант является самым простым для меня, поэтому я решил написать скрипт на питоне.

Для тех, кто плохо разбирается в Python, вот что нужно сделать на Ubuntu:

  1. $ sudo apt-get install -y python3-dev;
  2. $ python3 -m venv./migration;
  3. $ cd./migration;
  4. $ source./bin/activate;
  5. $ pip install wheel mysql-connector-python paramiko;
  6. создайте файл migration.pyвнутри ./migrationи скопируйте и вставьте код ниже;
  7. Измените код в соответствии с вашими потребностями;
  8. $python3./migration.py(для пробного прогона, т.е. ничего не передано)или python3./migration --do_insertдля реального переноса.
  9. После переноса исправлено значение поля maildirдля postmaster@your_domain.comв базе данных/таблице vmail/mailboxс помощью редактора sql. Измените его на значение со старого сервера (, также вы можете найти правильный путь вручную в каталоге .../vmail/vmail1/...).
  10. Замените папку vmailна новом сервере на папку vmailсо старого сервера.

В моем случае я устанавливаю соединения с 2 БД:

  • ssh с паролем и SQL-сервером на хосте;
  • ssh с закрытым ключом и SQL-сервером внутри IredMail Docker (не забудьте открыть порт в докере, чтобы разрешить подключения с ssh-хоста к докеру).

Кроме того, все довольно хорошо прокомментировано.

Думаю, достаточно передать только vmailБД, но не хотелось экспериментировать и переносить все что можно, кроме БД/Таблицы iredadmin/log.

import argparse
import mysql.connector
import paramiko
import traceback

from sshtunnel import SSHTunnelForwarder


class SshConnection:
    def __init__(self, server_name):
        self.server_name = server_name

    def connect_to_db(self):
        if self.server_name == 'old':
            server_host = '111.111.111.111'
            server_port = 22

            server_params = dict(
                ssh_username='root',
                ssh_password='Super Secret Password')

            mysql_username = 'root'
            mysql_password = 'Super Secret Password'

        elif self.server_name == 'new':
            server_host = '222.222.222.222'
            server_port = 22

            server_params = dict(
                ssh_username='root',
                ssh_pkey="/home/username/.ssh/id_rsa",  # NOT public (ie "id_rsa.pub")
                ssh_private_key_password="passphrase for key")

            mysql_username = 'root'
            mysql_password = 'Super Secret Password'

        # This lines probably you do not need to change
        mysql_host = '127.0.0.1'
        mysql_port = 3306

        self.tunnel = SSHTunnelForwarder(
            (server_host, server_port),
            remote_bind_address=(mysql_host, mysql_port),
            **server_params)

        self.tunnel.start()

        self.connection = mysql.connector.connect(
            host=mysql_host,
            port=int(self.tunnel.local_bind_port),
            connection_timeout=15,
            user=mysql_username,
            passwd=mysql_password
        )

        return self.connection

    def close_connection(self):
        self.connection.close()
        self.tunnel.stop()


def main(parser_args):
    """
    Copy Data from Source Server to Target Server.
    The next algorithm used (Data are copied just when this conditions met):
    1. Target server checks if Source server has needed DB name. If yes...
    2.... Target server checks if Source DB has needed table name. If yes...
    3.... Take data from Source table;
    4. Remove columns from taken Source data, which do not exist in Target table;
    5. Transfer taken Source data to the Target server.

    # AFTER MIGRATION FIX VALUE OF "maildir" field for postmaster@your_domain.com
    # IN DB/TABLE "vmail->mailbox" VIA SQL EDITOR. Change it to the value from old
    # server (also you can find correct path manually inside.../vmail/vmail1/... dir).
    """

    do_insert = parser_args.do_insert

    databases_ignore = ('information_schema', 'mysql', 'performance_schema')
    tables_ignore = dict(
            iredadmin=['log']
        )

    # Establich SSH Connection and Connect to DB
    ssh_connection_source_sever = SshConnection('old')
    ssh_connection_target_server = SshConnection('new')

    connection_source_sever = ssh_connection_source_sever.connect_to_db()
    connection_target_server = ssh_connection_target_server.connect_to_db()

    # Get Cursors
    cursor_source_sever = connection_source_sever.cursor()
    cursor_target_server = connection_target_server.cursor()

    cursor_dict_source_sever = connection_source_sever.cursor(dictionary=True)
    cursor_dict_target_server = connection_target_server.cursor(dictionary=True)

    is_error = False

    try:
        # [ START: Get list of Databases ]
        print('\n\nDATABASE NAMES')
        cursor_source_sever.execute('SHOW DATABASES')
        database_names_source_sever = [t[0] for t in cursor_source_sever.fetchall()]  # return data from last query
        print('database_names_source_sever:', database_names_source_sever)

        cursor_target_server.execute('SHOW DATABASES')   
        database_names_target_server = [t[0] for t in cursor_target_server.fetchall()]  # return data from last query
        print('database_names_target_server:', database_names_target_server)
        # [ END: Get list of Databases ]

        # [ START: Handle Database's tables ]
        # "sogo" is a view (not a table) which is based on data from "vmail" db
        # https://www.guru99.com/views.html
        for db_name_target_server in database_names_target_server:
            if (db_name_target_server in databases_ignore) \
                    or (db_name_target_server not in database_names_source_sever) \
                    or db_name_target_server == 'sogo':
                continue

            print(f'\n\nDATABASE NAME: {db_name_target_server}')

            cursor_source_sever.execute(f"USE {db_name_target_server}")  # select the database
            cursor_source_sever.execute("SHOW TABLES")
            table_names_source_sever = [t[0] for t in cursor_source_sever.fetchall()]  #return data from last query
            print('table_names_source_sever:', table_names_source_sever)

            cursor_target_server.execute(f"USE {db_name_target_server}")  # select the database
            cursor_target_server.execute("SHOW TABLES")
            table_names_target_server = [t[0] for t in cursor_target_server.fetchall()]  # return data from last query
            print('table_names_target_server:', table_names_target_server)

            # [ START: Transfer data from Source table ]

            if db_name_target_server == 'roundcubemail':
                # Change position
                for idx, table_name in enumerate(['users', 'contacts', 'contactgroups']):
                    table_names_target_server.remove(table_name)
                    table_names_target_server.insert(idx, table_name)

                print('table_names_target_server CHANGED ORDER:', table_names_target_server)

            for table_name_on_target_server in table_names_target_server:
                if (table_name_on_target_server not in table_names_source_sever) \
                        or (table_name_on_target_server in tables_ignore.get(db_name_target_server, [])):
                    continue

                print(f'\nTransfer data from Source table: {table_name_on_target_server}')

                # [ START: Get list of Source and Target tables' columns ]
                cursor_source_sever.execute(f"SHOW COLUMNS FROM {table_name_on_target_server} FROM {db_name_target_server}")
                table_fields_source_server = [t[0] for t in cursor_source_sever.fetchall()]
                print('-->> table_fields_source_server:', table_fields_source_server)

                cursor_target_server.execute(f"SHOW COLUMNS FROM {table_name_on_target_server} FROM {db_name_target_server}")
                table_fields_target_server = [t[0] for t in cursor_target_server.fetchall()]
                print('-->> table_fields_target_server:', table_fields_target_server)
                # [ END: Get list of Source and Target tables' columns ]

                # Select all table's data
                cursor_dict_source_sever.execute(f"SELECT * FROM {table_name_on_target_server};")  # get table's data
                table_data_source_sever: list = cursor_dict_source_sever.fetchall()

                for row_source_sever_dict in table_data_source_sever:
                    # Del columns from record/row, which do not exist in Target table
                    for column_name in row_source_sever_dict.copy():
                        if column_name not in table_fields_target_server:
                            del row_source_sever_dict[column_name]

                        # Avoid "You have an error in your SQL syntax;" error for "reply-to" field.
                        elif '-' in column_name:
                            row_source_sever_dict[f'`{column_name}`'] = row_source_sever_dict[column_name]
                            del row_source_sever_dict[column_name]

                        # Change filesystem path where mailboxes are stored.
                        if db_name_target_server == 'vmail':
                            time_set = '2020-04-26 21:00:00'

                            if table_name_on_target_server == 'mailbox':
                                if column_name == 'storagebasedirectory':
                                    # /home/antonio/mails
                                    row_source_sever_dict[column_name] = '/var/vmail'

                                elif column_name in ('lastlogindate', 'passwordlastchange', 'modified'):
                                    row_source_sever_dict[column_name] = time_set

                            if table_name_on_target_server in ('alias', 'domain_admins'):
                                if column_name == 'modified':
                                    row_source_sever_dict[column_name] = time_set

                    # Send data to Target table
                    placeholders = ', '.join(['%s'] * len(row_source_sever_dict))
                    columns = ', '.join(row_source_sever_dict.keys())
                    sql = "INSERT INTO %s ( %s ) VALUES ( %s )" % (table_name_on_target_server, columns, placeholders)

                    if not do_insert:
                        print('row_source_sever_dict to be sent to Target table:', row_source_sever_dict)
                        print('sql:', sql)

                    if do_insert:
                        try:
                            cursor_target_server.execute(sql, list(row_source_sever_dict.values()))
                        except Exception as e:
                            e_str = str(e)

                            if any(s in e_str for s in ('Duplicate entry',
                                                         'Cannot add or update a child row: a foreign key constraint fails',
                                                         'You have an error in your SQL syntax',
                                                         'The target table users of the INSERT is not insertable-into')):
                                print(f'\n!!! ERROR: {e}')
                                print(f'DB name: {db_name_target_server}; Table name: {table_name_on_target_server}')
                                print('transfer data:', row_source_sever_dict)
                                print('transfer sql:', sql)
                                print('----------')

                            else:
                                raise NotImplementedError()

                            if any(s in e_str for s in ('You have an error in your SQL syntax',
                                                         'The target table users of the INSERT is not insertable-into')):
                                raise NotImplementedError()
            # [ END: Transfer data from Source table ]
        # [ END: Handle Database's tables ]

    except (Exception, KeyboardInterrupt) as e:
        print(f'\n\n!!! ERROR: {e}')
        traceback.print_exc()

        is_error = True

    finally:
        print('\n\n** FINISHED **')
        print('is_error:', is_error)

        # Close cursors
        cursor_source_sever.close()
        cursor_target_server.close()

        cursor_dict_source_sever.close()
        cursor_dict_target_server.close()

        # Commit changes
        if not is_error:
            connection_target_server.commit()

        # Disconnect DB and SSH
        ssh_connection_source_sever.close_connection()
        ssh_connection_target_server.close_connection()

if __name__ == "__main__":
    # Command Line section
    parser = argparse.ArgumentParser(description="Transfer data from Source server to Target server. Details read in main() description.")
    parser.add_argument('--do_insert', action='store_true',
                        help='Test (by default) or Production run (if --do_insert provided)')

    main(parser.parse_args())

Код позволяет переносить БД из версии 0.9.0+ в версию 1.2. (Возможно, это может работать и с 0.9.0-, но я не читал соответствующие "Обучения по обновлению").

Не стесняйтесь задавать любые вопросы.

0
ответ дан 27 April 2020 в 13:22

Теги

Похожие вопросы