Migration script is now a regular management command.

see #577
This commit is contained in:
Antoine Nguyen
2014-08-24 10:25:49 +02:00
parent 214025b8d8
commit 734ec0327f
5 changed files with 317 additions and 271 deletions
+21 -24
View File
@@ -6,7 +6,7 @@ Migrating from other software
PostfixAdmin
************
Since version 0.8.5, Modoboa provides a simple script to migrate an
Modoboa provides a simple script to migrate an
existing `PostfixAdmin (version 2.3.3+)
<http://postfixadmin.sourceforge.net/>`_ database to a Modoboa one.
@@ -21,40 +21,37 @@ connection named ``pfxadmin`` into the ``DATABASES`` variable
corresponding to your PostfixAdmin setup::
DATABASES = {
"default" : {
# default connection definition
},
"pfxadmin" : {
"ENGINE" : "<engine>",
"NAME" : "<database name>",
"USER" : "<database user>",
"PASSWORD" : "<user password>",
}
"default" : {
# default connection definition
},
"pfxadmin" : {
"ENGINE" : "<engine>",
"NAME" : "<database name>",
"USER" : "<database user>",
"PASSWORD" : "<user password>",
}
}
This connection should correspond to the one defined in PostfixAdmin's
configuration file.
You are now ready to start the migration. Enter Modoboa's root
directory and execute the following command::
Then, uncomment the line containing
``'modoboa.tools.pfxadmin_migrate'`` inside the ``MODOBOA_APPS``
variable and save your changes.
$ PYTHONPATH=$PWD/.. DJANGO_SETTINGS_MODULE=modoboa.settings \
./tools/pfxadmin_migrate/migrate.py -r -p <directory that stores mailboxes>
You are now ready to start the migration so run the following commands::
$ cd <modoboa_site>
$ python manage.py migrate_from_postfixadmin -r -p <directory that stores mailboxes>
Depending on how many domains/mailboxes your existing setup contains,
the migration can be long. Just wait for the script's ending.
Once the migration has succeed, go the *Admin > Configuration* panel,
click on the *admin* row and modify the value of ``MAILDIR_ROOT`` as
follow::
.. note::
MAILDIR_ROOT =
The corresponding field must be empty. Don't touch other fields except
``PASSWORD_SCHEME``, if needed. (set it to the same method as the one
used by PostfixAdmin, check its configuration file if you're not sure)
Click on the *Save* button.
The rename operation is not synchronous. If you choose this option,
you must enable **Handle mailboxes on filesystem** option. See
:ref:`fs_operations`.
The procedure is over, edit the :file:`settings.py` file and:
@@ -0,0 +1,296 @@
# coding: utf-8
"""Automatic postfixadmin to Modoboa migration
===========================================
This script provides an easy way to migrate an existing postfixadmin
database to a Modoboa one.
As the two products do not share the same schema, some information
will be lost on the new database. Here is the list:
* Description, transport and backup MX for domains
* Logs
* Fetchmail table
Mailboxes organisation on the filesystem will change. PostfixAdmin
uses the following layout::
<topdir>/domain.tld/user@domain.tld/
Whereas Modoboa uses the following::
<topdir>/domain.tld/user/
To rename directories, you'll need the appropriate permissions.
Domain administrators that manage more than one domain will not be
completly restored. They will only be administrator of the domain that
corresponds to their username.
Vacation messages will not be migrated by this script. Users will have
to define new messages by using Modoboa's autoreply plugin. (if
activated)
One last point about passwords. PostfixAdmin uses different hash
algorithms to store passwords so this script does not modify
them. They are copied "as is" in the new database. In order to use
them, use the ``--passwords-scheme`` to tell Modoboa which scheme was
used by PostfixAdmin.
.. note::
Actually, it appears that the ``crypt`` scheme is compatible with
PostfixAdmin ``md5crypt`` algorithm...
"""
from optparse import make_option
import os
import sys
from django.contrib.auth.models import Group
from django.core.management.base import BaseCommand
from django.db import transaction
from django.db.models import Q
import modoboa.core.models as core_models
import modoboa.extensions.admin.models as admin_models
from modoboa.lib.emailutils import split_mailbox
from . import models as pf_models
class Command(BaseCommand):
"""Migration from postfixadmin."""
help = "This command eases the migration from a postfixadmin database."
option_list = BaseCommand.option_list + (
make_option(
"-f", "--from", dest="_from", default="pfxadmin",
help="Name of postfixadmin db connection declared in settings.py"
"(default is pfxadmin)"
),
make_option(
"-t", "--to", dest="_to", default="default",
help="Name of the Modoboa db connection declared in settings.py"
" (default is default)"
),
make_option(
"-r", "--rename-dirs", action="store_true", default=False,
help="Rename mailbox directories (default is no)"
),
make_option(
"-p", "--mboxes-path", default=None,
help="Path where directories are stored on the filesystem"
"(used only if --rename-dirs)"
),
make_option(
"-s", "--passwords-scheme", default="crypt",
help="Scheme used to crypt user passwords"
)
)
def _migrate_dates(self, oldobj):
"""Creates a new ObjectDates instance.
Due to Django limitations, we only retrieve the creation date. The
modification date will be set to 'now' because the corresponding
field in Modoboa's schema has the 'auto_now' attribute to True.
(see https://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.DateField).
:param oldobj: the PostfixAdmin record which is beeing migrated.
"""
dates = admin_models.base.ObjectDates()
dates.save()
dates.creation = oldobj.created
dates.save()
return dates
def _migrate_domain_aliases(self, domain, options, creator):
"""Migrate aliases of a single domain."""
print "\tMigrating domain aliases"
old_domain_aliases = pf_models.AliasDomain.objects.using(
options["_from"]
).filter(target_domain=domain.name)
for old_da in old_domain_aliases:
try:
target_domain = admin_models.Domain.objects \
.using(options["_to"]).get(name=old_da.target_domain)
new_da = admin_models.DomainAlias()
new_da.name = old_da.alias_domain
new_da.target = target_domain
new_da.enabled = old_da.active
new_da.dates = self._migrate_dates(old_da)
new_da.save(using=options["_to"])
new_da.post_create(creator)
except core_models.Domain.DoesNotExist:
print ("Warning: target domain %s does not exists, "
"not creating alias domain %s"
% old_da.target_domain, old_da.alias_domain)
continue
def _migrate_mailbox_aliases(self, domain, options, creator):
"""Migrate mailbox aliases of a single domain."""
print "\tMigrating mailbox aliases"
old_aliases = pf_models.Alias.objects \
.using(options["_from"]).filter(domain=domain.name)
for old_al in old_aliases:
if old_al.address == old_al.goto:
continue
new_al = admin_models.Alias()
local_part, tmp = split_mailbox(old_al.address)
if local_part is None or not len(local_part):
if tmp is None or not len(tmp):
print (
"Warning: skipping alias %s (cannot retrieve local "
"part). You will need to recreate it manually."
% old_al.address
)
continue
new_al.address = "*"
else:
new_al.address = local_part
new_al.domain = domain
new_al.enabled = old_al.active
extmboxes = []
intmboxes = []
for goto in old_al.goto.split(","):
try:
mb = admin_models.Mailbox.objects \
.using(options["_to"]).get(user__username=goto)
except admin_models.Mailbox.DoesNotExist:
extmboxes += [goto]
else:
intmboxes += [mb]
new_al.dates = self._migrate_dates(old_al)
new_al.save(
int_rcpts=intmboxes, ext_rcpts=extmboxes, creator=creator,
using=options["_to"]
)
def _migrate_mailboxes(self, domain, options, creator):
"""Migrate mailboxes of a single domain.
.. note::
Mailboxes rename will be done later by the
handle_mailboxes_operation script.
"""
print "\tMigrating mailboxes"
old_mboxes = pf_models.Mailbox.objects \
.using(options["_from"]).filter(domain=domain.name)
for old_mb in old_mboxes:
new_user = core_models.User()
new_user.username = old_mb.username
new_user.first_name = old_mb.name.partition(' ')[0]
new_user.last_name = old_mb.name.partition(' ')[2]
new_user.email = old_mb.username
new_user.is_active = old_mb.active
new_user.date_joined = old_mb.created
new_user.password = "{%s}%s" % \
(options["passwords_scheme"].upper(), old_mb.password)
new_user.dates = self._migrate_dates(old_mb)
new_user.save(creator=creator, using=options["_to"])
new_user.set_role("SimpleUsers")
new_mb = admin_models.Mailbox()
new_mb.user = new_user
new_mb.address = old_mb.local_part
new_mb.domain = domain
new_mb.dates = self._migrate_dates(old_mb)
new_mb.set_quota(old_mb.quota / 1024000, override_rules=True)
new_mb.save(creator=creator, using=options["_to"])
if options["rename_dirs"]:
oldpath = os.path.join(
options["mboxes_path"], domain.name, old_mb.maildir)
new_mb.rename_dir(oldpath)
def _migrate_domain(self, old_dom, options, creator):
"""Single domain migration."""
print "Migrating domain %s" % old_dom.domain
newdom = admin_models.Domain()
newdom.name = old_dom.domain
newdom.enabled = old_dom.active
newdom.quota = old_dom.maxquota
newdom.dates = self._migrate_dates(old_dom)
newdom.save(creator=creator, using=options["_to"])
self._migrate_mailboxes(newdom, options, creator)
self._migrate_mailbox_aliases(newdom, options, creator)
self._migrate_domain_aliases(newdom, options, creator)
def _migrate_admins(self, options, creator):
"""Administrators migration."""
print "Migrating administrators"
dagroup = Group.objects.using(options["_to"]).get(name="DomainAdmins")
for old_admin in pf_models.Admin.objects.using(options["_from"]).all():
local_part, domname = split_mailbox(old_admin.username)
try:
query = Q(username=old_admin.username) & \
(Q(domain="ALL") | Q(domain=domname))
creds = pf_models.DomainAdmins.objects \
.using(options["_from"]).get(query)
except pf_models.DomainAdmins.DoesNotExist:
print (
"Warning: skipping useless admin %s" % (old_admin.username)
)
continue
try:
domain = admin_models.Domain.objects \
.using(options["_to"]).get(name=domname)
except admin_models.Domain.DoesNotExist:
print (
"Warning: skipping domain admin %s, domain not found"
% old_admin.username
)
continue
try:
user = core_models.User.objects \
.using(options["_to"]).get(username=old_admin.username)
except core_models.User.DoesNotExist:
user = core_models.User()
user.username = old_admin.username
user.email = old_admin.username
user.password = old_admin.password
user.is_active = old_admin.active
user.save(creator=creator, using=options["_to"])
user.date_joined = old_admin.modified
if creds.domain == "ALL":
user.is_superuser = True
else:
user.groups.add(dagroup)
domain.add_admin(user)
user.save(using=options["_to"])
def _do_migration(self, options, creator_username='admin'):
"""Run the complete migration."""
pf_domains = pf_models.Domain.objects.using(options["_from"]).all()
creator = core_models.User.objects.using(options["_to"]).get(
username=creator_username)
for pf_domain in pf_domains:
if pf_domain.domain == "ALL":
continue
try:
# Skip this domain if it's an alias
is_domain_alias = pf_models.AliasDomain.objects \
.using(options["_from"]).get(alias_domain=pf_domain.domain)
print "Info: %s looks like an alias domain, skipping it" \
% pf_domain.domain
continue
except pf_models.AliasDomain.DoesNotExist:
self._migrate_domain(pf_domain, options, creator)
self._migrate_admins(options, creator)
def handle(self, *args, **options):
"""Command entry point."""
if options["rename_dirs"] and options["mboxes_path"] is None:
print "Error: you must provide the --mboxes-path option"
sys.exit(1)
with transaction.commit_on_success():
self._do_migration(options)
-247
View File
@@ -1,247 +0,0 @@
#!/usr/bin/env python
# coding: utf-8
"""
Automatic postfixadmin to Modoboa migration
===========================================
This script provides an easy way to migrate an existing postfixadmin
database to a Modoboa one.
As the two products do not share the same schema, some informations
will be lost on the new database. Here is the list:
* Description, transport and backup MX for domains
* Logs
* Fetchmail table
Mailboxes organisation on the filesystem will change. PostfixAdmin
uses the following layout::
<topdir>/domain.tld/user@domain.tld/
Whereas Modoboa uses the following::
<topdir>/domain.tld/user/
To rename directories, you'll need the appropriate permissions.
Domain administrators that manage more than one domain will not be
completly restored. They will only be administrator of the domain that
corresponds to their username.
Vacation messages will not be migrated by this script. Users will have
to define new messages by using Modoboa's autoreply plugin. (if
activated)
One last point about passwords. PostfixAdmin uses different hash
algorithms to store passwords so this script does not modify
them. They are copied "as is" is the new database. In order to use
them, you'll have to adapt the ``PASSWORD_SCHEME`` parameter in the
admin. panel. Currently, it appears that the ``crypt`` scheme is
compatible with PostfixAdmin ``md5crypt`` algorithm...
"""
import os
import sys
from django.db import transaction
from django.db.utils import ConnectionDoesNotExist
from django.db.models import Q
from django.contrib.auth.models import Group
import modoboa.extensions.admin.models as admin_models
import modoboa.core.models as core_models
import models as pf_models
from modoboa.lib.sysutils import exec_cmd
from modoboa.lib.emailutils import split_mailbox
def migrate_dates(oldobj):
"""Creates a new ObjectDates instance
Due to Django limitations, we only retrieve the creation date. The
modification date will be set to 'now' because the corresponding
field in Modoboa's schema has the 'auto_now' attribute to True.
(see https://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.DateField).
:param oldobj: the PostfixAdmin record which is beeing migrated.
"""
dates = admin_models.base.ObjectDates()
dates.save()
dates.creation = oldobj.created
dates.save()
return dates
def migrate_domain_aliases(domain, options, creator):
print "\tMigrating domain aliases"
old_domain_aliases = pf_models.AliasDomain.objects.using(options._from).filter(target_domain=domain.name)
for old_da in old_domain_aliases:
try:
target_domain = admin_models.Domain.objects.using(options._to).get(name=old_da.target_domain)
new_da = admin_models.DomainAlias()
new_da.name = old_da.alias_domain
new_da.target = target_domain
new_da.enabled = old_da.active
new_da.dates = migrate_dates(old_da)
new_da.save(using=options._to)
new_da.post_create(creator)
except core_models.Domain.DoesNotExist as e:
print "Warning: target domain %s does not exists, not creating alias domain %s" \
% old_da.target_domain, old_da.alias_domain
continue
def migrate_mailbox_aliases(domain, options, creator):
print "\tMigrating mailbox aliases"
old_aliases = pf_models.Alias.objects.using(options._from).filter(domain=domain.name)
for old_al in old_aliases:
if old_al.address == old_al.goto:
continue
new_al = admin_models.Alias()
local_part, tmp = split_mailbox(old_al.address)
if local_part is None or not len(local_part):
if tmp is None or not len(tmp):
print """Warning: skipping alias %s (cannot retrieve local part).
You will need to recreate it manually.
""" % old_al.address
continue
new_al.address = "*"
else:
new_al.address = local_part
new_al.domain = domain
new_al.enabled = old_al.active
extmboxes = []
intmboxes = []
for goto in old_al.goto.split(","):
try:
mb = admin_models.Mailbox.objects.using(options._to).get(user__username=goto)
except admin_models.Mailbox.DoesNotExist:
extmboxes += [goto]
else:
intmboxes += [mb]
new_al.dates = migrate_dates(old_al)
new_al.save(int_rcpts=intmboxes, ext_rcpts=extmboxes, creator=creator, using=options._to)
def migrate_mailboxes(domain, options, creator):
print "\tMigrating mailboxes"
old_mboxes = pf_models.Mailbox.objects.using(options._from).filter(domain=domain.name)
for old_mb in old_mboxes:
new_user = core_models.User()
new_user.username = old_mb.username
new_user.first_name = old_mb.name.partition(' ')[0]
new_user.last_name = old_mb.name.partition(' ')[2]
new_user.email = old_mb.username
new_user.is_active = old_mb.active
new_user.date_joined = old_mb.created
new_user.password = old_mb.password
new_user.dates = migrate_dates(old_mb)
new_user.save(creator=creator, using=options._to)
new_user.set_role("SimpleUsers")
new_mb = admin_models.Mailbox()
new_mb.user = new_user
new_mb.address = old_mb.local_part
new_mb.domain = domain
new_mb.dates = migrate_dates(old_mb)
if old_mb.quota:
new_mb.quota = old_mb.quota / 1024000
else:
new_mb.quota = 0
if options.rename_dirs:
oldpath = os.path.join(options.mboxes_path, domain.name, old_mb.maildir)
newpath = os.path.join(options.mboxes_path, domain.name, new_mb.path)
code, output = exec_cmd("mv %s %s" % (oldpath, newpath))
if code:
print "Error: cannot rename mailbox directory\n%s" % output
sys.exit(1)
new_mb.save(creator=creator, using=options._to)
def migrate_domain(old_dom, options, creator):
print "Migrating domain %s" % old_dom.domain
newdom = admin_models.Domain()
newdom.name = old_dom.domain
newdom.enabled = old_dom.active
newdom.quota = old_dom.maxquota
newdom.dates = migrate_dates(old_dom)
newdom.save(creator=creator, using=options._to)
migrate_mailboxes(newdom, options, creator)
migrate_mailbox_aliases(newdom, options, creator)
migrate_domain_aliases(newdom, options, creator)
def migrate_admins(options, creator):
print "Migrating administrators"
dagroup = Group.objects.using(options._to).get(name="DomainAdmins")
for old_admin in pf_models.Admin.objects.using(options._from).all():
local_part, domname = split_mailbox(old_admin.username)
try:
query = Q(username=old_admin.username) & \
(Q(domain="ALL") | Q(domain=domname))
creds = pf_models.DomainAdmins.objects.using(options._from).get(query)
except pf_models.DomainAdmins.DoesNotExist:
print "Warning: skipping useless admin %s" % (old_admin.username)
continue
try:
user = core_models.User.objects.using(options._to).get(username=old_admin.username)
except core_models.User.DoesNotExist:
try:
domain = admin_models.Domain.objects.using(options._to).get(name=domname)
except admin_models.Domain.DoesNotExist:
print "Warning: skipping domain admin %s, domain not found" \
% old_admin.username
continue
user = core_models.User()
user.username = old_admin.username
user.email = old_admin.username
user.password = old_admin.password
user.is_active = old_admin.active
user.save(creator=creator, using=options._to)
user.date_joined = old_admin.modified
if creds.domain == "ALL":
user.is_superuser = True
else:
user.groups.add(dagroup)
user.save(using=options._to)
@transaction.commit_on_success
def do_migration(options, creator_username='admin'):
pf_domains = pf_models.Domain.objects.using(options._from).all()
creator = core_models.User.objects.using(options._to).get(username=creator_username)
for pf_domain in pf_domains:
if pf_domain.domain == "ALL":
continue
try:
#Skip this domain if it's an alias
is_domain_alias = pf_models.AliasDomain.objects.using(options._from).get(alias_domain=pf_domain.domain)
print "Info: %s looks like an alias domain, skipping it" \
% pf_domain.domain
continue
except pf_models.AliasDomain.DoesNotExist:
migrate_domain(pf_domain, options, creator)
migrate_admins(options, creator)
if __name__ == "__main__":
from optparse import OptionParser
parser = OptionParser()
parser.add_option("-f", "--from", dest="_from", default="pfxadmin",
help="Name of postfixadmin db connection declared in settings.py (default is pfxadmin)")
parser.add_option("-t", "--to", dest="_to", default="default",
help="Name of the Modoboa db connection declared in settings.py (default is default)")
parser.add_option("-r", "--rename-dirs", action="store_true",
help="Rename mailbox directories (default is no)")
parser.add_option("-p", "--mboxes-path", default=None,
help="Path where directories are stored on the filesystem (used only if --rename-dirs)")
options, args = parser.parse_args()
if options.rename_dirs and options.mboxes_path is None:
print "Error: you must provide the --mboxes-path option"
sys.exit(1)
try:
do_migration(options)
except ConnectionDoesNotExist, e:
print e