I read all "Upgrade tutorial"s in order to find all DB changes required to update 0.9.0 to 1.1 and found the 3rd option is the easiest for me, so I decided to write a python script.
For those, who is not well with Python, here what is needed to do on Ubuntu:
- $
sudo apt-get install -y python3-dev
;
- $
python3 -m venv ./migration
;
- $
cd ./migration
;
- $
source ./bin/activate
;
- $
pip install wheel mysql-connector-python paramiko
;
- create
migration.py
file inside ./migration
and copy paste code below;
- Modify code according to your needs;
- $
python3 ./migration.py
(for test run, ie nothing transferred) or python3 ./migration --do_insert
for real transfer.
- 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).
- Replace
vmail
folder on the new server by vmail
folder from the old server.
In my case I establish connections to 2 DBs:
- ssh with password and SQL server on the host;
- ssh with private key and SQL server inside IredMail Docker (do not forget to open port in docker to allow connections from ssh-host to docker).
Additionally everything pretty well commented.
I guess that it is enough to transfer just vmail
DB, but I did not want to experiment and transfer everything possible except DB/Table 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())
The code can transfer DBs from v0.9.0+ to v1.2 (Probably it may work with 0.9.0-, but I did not read appropriate "Upgrade tutorial"s).
Feel free to ask any questions.