diff --git a/modoboa/admin/handlers.py b/modoboa/admin/handlers.py index b6fbae762..b7b6554e3 100644 --- a/modoboa/admin/handlers.py +++ b/modoboa/admin/handlers.py @@ -3,7 +3,10 @@ from django.db.models import signals from django.dispatch import receiver +from modoboa.core import signals as core_signals + from . import models +from . import postfix_maps @receiver(signals.post_save, sender=models.Domain) @@ -97,3 +100,14 @@ def remove_alias_for_mailbox(sender, instance, **kwargs): """Remove "self alias" for this mailbox.""" models.Alias.objects.filter( address=instance.full_address).delete() + + +@receiver(core_signals.register_postfix_maps) +def register_postfix_maps(sender, **kwargs): + """Register admin map files.""" + return [ + postfix_maps.DomainsMap, postfix_maps.DomainsAliasesMap, + postfix_maps.AliasesMap, postfix_maps.MaintainMap, + postfix_maps.SenderLoginAliasMap, postfix_maps.SenderLoginMailboxMap, + postfix_maps.SenderLoginMailboxExtraMap + ] diff --git a/modoboa/admin/postfix_maps.py b/modoboa/admin/postfix_maps.py index 2d13440f4..b8e19a3b0 100644 --- a/modoboa/admin/postfix_maps.py +++ b/modoboa/admin/postfix_maps.py @@ -1,7 +1,5 @@ """Map file definitions for postfix.""" -from modoboa.core.commands.postfix_maps import registry - class DomainsMap(object): @@ -166,8 +164,3 @@ class SenderLoginAliasMap(object): " INNER JOIN admin_alias al ON alr.alias_id=al.id " "WHERE al.enabled=1 AND al.address='%s'" ) - -registry.add_files([ - DomainsMap, DomainsAliasesMap, AliasesMap, MaintainMap, - SenderLoginAliasMap, SenderLoginMailboxMap, SenderLoginMailboxExtraMap -]) diff --git a/modoboa/core/commands/postfix_maps.py b/modoboa/core/commands/postfix_maps.py deleted file mode 100644 index 2fbfab935..000000000 --- a/modoboa/core/commands/postfix_maps.py +++ /dev/null @@ -1,185 +0,0 @@ -# coding: utf-8 - -"""postfix_maps command.""" - -import datetime -import getpass -import os -import sys - -import django -from django.template import Context, Template - -import dj_database_url - -from . import Command - -MAP_FILE_TEMPLATE = """user = {{ dbuser }} -password = {{ dbpass }} -dbname = {{ dbname }} -hosts = {{ dbhost }} -query = {{ query|safe }} -""" - - -class MapFilesRegistry(object): - - """A registry of all map files.""" - - def __init__(self): - self.files = [] - - def add_files(self, files): - """Add map files to registry.""" - self.files += files - - -registry = MapFilesRegistry() - - -class MapFilesGenerator(object): - - """Map files generator.""" - - def __init__(self, dbinfo=None): - """Constructor.""" - self.template = Template(MAP_FILE_TEMPLATE) - if dbinfo is None: - self.tpl_context = { - 'dbhost': raw_input('Database host: '), - 'dbname': raw_input('Database name: '), - 'dbuser': raw_input('Username: '), - 'dbpass': getpass.getpass('Password: ') - } - else: - self.tpl_context = { - 'dbhost': dbinfo['HOST'], - 'dbname': dbinfo['NAME'], - 'dbuser': dbinfo['USER'], - 'dbpass': dbinfo['PASSWORD'] - } - if not self.tpl_context['dbhost']: - self.tpl_context['dbhost'] = '127.0.0.1' - - def __render_map(self, args, mapobject): - """Render a map file. - - :param args: command line arguments - :param mapobject: a ``MapFile`` subclass - """ - content = self.template.render( - Context(dict( - self.tpl_context.items(), - query=getattr(mapobject, args.dbtype) - )) - ) - with open("%s/%s" % (args.destdir, mapobject.filename), "w") as fp: - fp.write("""# This file was generated on {} by running: -# {} {} -# DO NOT EDIT! -""" - .format(datetime.datetime.now().isoformat(), - os.path.basename(sys.argv[0]), - ' '.join(sys.argv[1:]))) - fp.write("{}\n".format(content)) - - def __load_applications(self, applications): - """Load specified applications.""" - for app in applications: - extension = app.replace("-", "_") - try: - __import__(extension, locals(), globals(), ["postfix_maps"]) - except ImportError: - sys.stderr.write("Unknown application {0}".format(extension)) - - def render(self, args): - """Render all map files. - - :param args: arguments received from the command line - """ - try: - os.mkdir(args.destdir) - except OSError: - pass - applications = ["modoboa.admin", "modoboa.relaydomains"] - if args.extensions: - if "all" in applications: - from modoboa.lib.api_client import ModoAPIClient - - # Retrieve extension list from the API - url = "http://api.modoboa.org/" - official_exts = ModoAPIClient(url).list_extensions() - extensions = [ - extension["name"] for extension in official_exts] - else: - extensions = args.extensions - applications += extensions - self.__load_applications(applications) - for mapfile in registry.files: - self.__render_map(args, mapfile) - - -class SQLiteMapFilesGenerator(MapFilesGenerator): - - """SQLite special generator.""" - - def __init__(self, dbinfo=None): - self.template = Template("""dbpath = {{ dbpath }} -query = {{ query|safe }} -""") - if dbinfo is None: - self.tpl_context = { - 'dbpath': raw_input('Database path: ') - } - else: - self.tpl_context = { - 'dbpath': dbinfo['NAME'] - } - - -class PostfixMapsCommand(Command): - - """Base command to generate map files.""" - - help = "Generate ready-to-use postfix map files" - - def __init__(self, *args, **kwargs): - super(PostfixMapsCommand, self).__init__(*args, **kwargs) - self._parser.add_argument( - 'destdir', type=str, - help='Directory that will contain generated map files' - ) - self._parser.add_argument( - '--dbtype', type=str, choices=['mysql', 'postgres', 'sqlite'], - default='mysql', - help='Used database type' - ) - self._parser.add_argument( - '--dburl', type=str, nargs=1, default=None, - help="url of Modoboa's database" - ) - self._parser.add_argument( - '--extensions', type=str, nargs='+', - help=( - 'Generate map files for this extensions ' - '("all" shortcut available)' - ) - ) - - def handle(self, parsed_args): - """Command entry point.""" - django.setup() - dbinfo = None - if parsed_args.dburl: - dbinfo = dj_database_url.config(default=parsed_args.dburl[0]) - if 'sqlite' in dbinfo['ENGINE']: - parsed_args.dbtype = 'sqlite' - elif 'psycopg2' in dbinfo['ENGINE']: - parsed_args.dbtype = 'postgres' - else: - parsed_args.dbtype = 'mysql' - if parsed_args.dbtype == 'sqlite': - generator = SQLiteMapFilesGenerator(dbinfo) - else: - generator = MapFilesGenerator(dbinfo) - generator.render(parsed_args) diff --git a/modoboa/core/management/commands/generate_postfix_maps.py b/modoboa/core/management/commands/generate_postfix_maps.py new file mode 100644 index 000000000..6db643eec --- /dev/null +++ b/modoboa/core/management/commands/generate_postfix_maps.py @@ -0,0 +1,112 @@ +"""Management command to generate/update postfix map files.""" + +import os +import sys + +from django.conf import settings +from django.core.management.base import BaseCommand +from django.template import Context, Template +from django.utils import timezone + +import dj_database_url + +from ... import signals + +MAP_FILE_TEMPLATE = """# This file was generated on {{ date }} by running: +# {{ commandline }} +# DO NOT EDIT! +""" + + +class Command(BaseCommand): + """Command class.""" + + help = "Generate/update postfix map files." + + def add_arguments(self, parser): + """Add extra arguments.""" + parser.add_argument( + "--dburl", help="Custom database url") + parser.add_argument( + "--destdir", default=".", + help="Directory where files will be created") + parser.add_argument( + "--extensions", type=str, nargs="+", + help=( + "Generate map files for those extensions " + "('all' shortcut available)" + ) + ) + + def __register_map_files(self): + """Load specified applications.""" + responses = signals.register_postfix_maps.send(sender=self.__class__) + mapfiles = [] + for response in responses: + mapfiles += response[1] + return mapfiles + + def get_template(self, dbtype): + """Return map file template.""" + tplcontent = MAP_FILE_TEMPLATE + if dbtype == "sqlite": + tplcontent += """dbpath = {{ dbname }} +query = {{ query|safe }} +""" + else: + tplcontent += """user = {{ dbuser }} +password = {{ dbpass }} +dbname = {{ dbname }} +hosts = {{ dbhost }} +query = {{ query|safe }} +""" + return Template(tplcontent) + + def get_template_context(self, options): + """Build the context used to render templates.""" + dburl = options.get("dburl") + db_settings = ( + dj_database_url.config(default=dburl) + if dburl else settings.DATABASES["default"]) + if "sqlite" in db_settings["ENGINE"]: + dbtype = "sqlite" + elif "psycopg2" in db_settings["ENGINE"]: + dbtype = "postgres" + else: + dbtype = "mysql" + commandline = "{} {}".format( + os.path.basename(sys.argv[0]), " ".join(sys.argv[1:])) + context = { + "date": timezone.now(), + "commandline": commandline, + "dbtype": dbtype, + "dbuser": db_settings["USER"], + "dbpass": db_settings["PASSWORD"], + "dbname": db_settings["NAME"], + "dbhost": db_settings.get("HOST", "127.0.0.1"), + } + return context + + def __render_map_file(self, mapobject, destdir, context): + """Render a map file.""" + content = self.get_template(context["dbtype"]).render( + Context( + dict(context.items(), + query=getattr(mapobject, context["dbtype"])) + ) + ) + fullpath = os.path.join(destdir, mapobject.filename) + with open(fullpath, "w") as fp: + fp.write("{}\n".format(content)) + + def handle(self, *args, **options): + """Command entry point.""" + mapfiles = self.__register_map_files() + destdir = os.path.realpath(options["destdir"]) + try: + os.mkdir(destdir) + except OSError: + pass + context = self.get_template_context(options) + for mapobject in mapfiles: + self.__render_map_file(mapobject, destdir, context) diff --git a/modoboa/core/signals.py b/modoboa/core/signals.py index 1275ffe1b..3169a6313 100644 --- a/modoboa/core/signals.py +++ b/modoboa/core/signals.py @@ -4,3 +4,4 @@ import django.dispatch can_create_object = django.dispatch.Signal( providing_args=["context", "object_type", "count"]) +register_postfix_maps = django.dispatch.Signal() diff --git a/modoboa/lib/test_utils.py b/modoboa/lib/test_utils.py index 3b5f070c5..e4bb1e4d9 100644 --- a/modoboa/lib/test_utils.py +++ b/modoboa/lib/test_utils.py @@ -1,13 +1,13 @@ """Tools for top level testing (ie. when ran by Travis).""" import os +import shutil import tempfile -from .sysutils import exec_cmd +from django.core.management import call_command class MapFilesTestCaseMixin(object): - """A generic test case to check map files generation.""" MAP_FILES = None @@ -18,17 +18,13 @@ class MapFilesTestCaseMixin(object): self.workdir = tempfile.mkdtemp() def tearDown(self): - exec_cmd("rm -rf {0}".format(self.workdir)) + shutil.rmtree(self.workdir) def _test_maps_generation(self, engine): dburl = "{0}://user:password@localhost/testdb".format(engine) - code, output = exec_cmd( - "modoboa-admin.py postfix_maps {0}--dburl {1} {2}".format( - "--extensions {} ".format(self.extension) - if self.extension else "", dburl, self.workdir - ) - ) - self.assertEqual(code, 0) + call_command( + "generate_postfix_maps", + "--dburl", dburl, "--destdir", self.workdir) for mapfile in self.MAP_FILES: path = "{0}/{1}".format(self.workdir, mapfile)