diff --git a/doc/additional/migrating.rst b/doc/additional/migrating.rst index 448e7fdd1..4f1e8f71c 100644 --- a/doc/additional/migrating.rst +++ b/doc/additional/migrating.rst @@ -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+) `_ 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" : "", - "NAME" : "", - "USER" : "", - "PASSWORD" : "", - } + "default" : { + # default connection definition + }, + "pfxadmin" : { + "ENGINE" : "", + "NAME" : "", + "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 +You are now ready to start the migration so run the following commands:: + + $ cd + $ python manage.py migrate_from_postfixadmin -r -p 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: diff --git a/modoboa/tools/pfxadmin_migrate/management/__init__.py b/modoboa/tools/pfxadmin_migrate/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/tools/pfxadmin_migrate/management/commands/__init__.py b/modoboa/tools/pfxadmin_migrate/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/tools/pfxadmin_migrate/management/commands/migrate_from_postfixadmin.py b/modoboa/tools/pfxadmin_migrate/management/commands/migrate_from_postfixadmin.py new file mode 100644 index 000000000..9d338742e --- /dev/null +++ b/modoboa/tools/pfxadmin_migrate/management/commands/migrate_from_postfixadmin.py @@ -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:: + + /domain.tld/user@domain.tld/ + +Whereas Modoboa uses the following:: + + /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) diff --git a/modoboa/tools/pfxadmin_migrate/migrate.py b/modoboa/tools/pfxadmin_migrate/migrate.py deleted file mode 100644 index 6824d3787..000000000 --- a/modoboa/tools/pfxadmin_migrate/migrate.py +++ /dev/null @@ -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:: - - /domain.tld/user@domain.tld/ - -Whereas Modoboa uses the following:: - - /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