From 419eb66fef4ba6524d0a22391b092136d422104a Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 13 Oct 2012 20:21:08 +0200 Subject: [PATCH] wip: command-line tool The first set of commands is done. see #314 --- modoboa/core/commands/deploy.py | 5 +- modoboa/core/commands/help.py | 19 +++++ modoboa/core/commands/postfix_maps.py | 109 ++++++++++++++++++++++++-- modoboa/core/management.py | 62 ++++++++++++--- 4 files changed, 176 insertions(+), 19 deletions(-) create mode 100644 modoboa/core/commands/help.py diff --git a/modoboa/core/commands/deploy.py b/modoboa/core/commands/deploy.py index 3043265f4..e7073ecd1 100644 --- a/modoboa/core/commands/deploy.py +++ b/modoboa/core/commands/deploy.py @@ -5,9 +5,10 @@ from django.core import management from modoboa.core.management import Command class DeployCommand(Command): + help = "Create a fresh django project (calling startproject) and apply Modoboa specific settings." - def __init__(self): - super(DeployCommand, self).__init__() + def __init__(self, *args, **kwargs): + super(DeployCommand, self).__init__(*args, **kwargs) self._parser.add_argument('name', type=str, help='The name of your Modoboa instance') diff --git a/modoboa/core/commands/help.py b/modoboa/core/commands/help.py new file mode 100644 index 000000000..bbf6f282c --- /dev/null +++ b/modoboa/core/commands/help.py @@ -0,0 +1,19 @@ +import sys +from modoboa.core.management import Command + +class HelpCommand(Command): + help = "Display the help message associated to a specific command" + + def __init__(self, *args, **kwargs): + super(HelpCommand, self).__init__(*args, **kwargs) + self._parser.add_argument('name', type=str, + help='A command name') + + def handle(self, parsed_args): + if not self._commands.has_key(parsed_args.name): + print >>sys.stderr, "Unknown command: %s" % parsed_args.name + sys.exit(1) + cmd = self._commands[parsed_args.name](self._commands) + cmd._parser.prog = "modoboa-admin.py %s" % parsed_args.name + cmd._parser.description = cmd.help + cmd._parser.print_help() diff --git a/modoboa/core/commands/postfix_maps.py b/modoboa/core/commands/postfix_maps.py index a55edb4a5..8f0aef9f6 100644 --- a/modoboa/core/commands/postfix_maps.py +++ b/modoboa/core/commands/postfix_maps.py @@ -1,8 +1,107 @@ - +import getpass +import inspect +import os +from django.template import Context, Template from modoboa.core.management import Command -class PostfixMapsCommand(Command): +class MapFile(object): + category = 'std' + def __init__(self): - super(PostfixMapsCommand, self).__init__() - self._parser.add_argument('--dbtype', type=str, choices=['mysql', 'postgres'], - help='The database type you use') + self.base_tpl = Template("""user = {{ dbuser }} +password = {{ dbpassword }} +dbname = {{ dbname }} +hosts = {{ dbhost }} +query = {{ query|safe }} +""") + + def render(self, destdir, dbtype, **options): + try: + options['query'] = getattr(self, dbtype) + except AttributeError: + return + + content = self.base_tpl.render(Context(options)) + fp = open("%s/%s" % (destdir, self.filename), "w") + fp.write(content) + fp.close() + +class DomainsMap(MapFile): + filename = 'sql-domains.cf' + mysql = "SELECT name FROM admin_domain WHERE name='%s' AND enabled=1" + postgres = "SELECT name FROM admin_domain WHERE name='%s' AND enabled" + +class DomainsAliasesMap(MapFile): + filename = 'sql-domain-aliases.cf' + mysql = "SELECT dom.name FROM admin_domain dom INNER JOIN admin_domainalias domal ON dom.id=domal.target_id WHERE domal.name='%s' AND domal.enabled=1 AND dom.enabled=1" + postgres = "SELECT dom.name FROM admin_domain dom INNER JOIN admin_domainalias domal ON dom.id=domal.target_id WHERE domal.name='%s' AND domal.enabled AND dom.enabled" + +class MailboxesMap(MapFile): + filename = 'sql-mailboxes.cf' + mysql = "SELECT concat(dom.name, '/', mb.path) FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN auth_user user ON mb.user_id=user.id WHERE dom.enabled=1 AND dom.name='%d' AND user.is_active=1 AND mb.address='%u'" + postgres = "SELECT dom.name || '/' || mb.path FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN auth_user u ON mb.user_id=u.id WHERE dom.enabled AND dom.name='%d' AND u.is_active AND mb.address='%u'" + +class AliasesMap(MapFile): + filename = 'sql-aliases.cf' + mysql = "(SELECT concat(mb.address, '@', dom.name) FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias_mboxes al_mb INNER JOIN admin_alias al ON al_mb.alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1 AND al.extmboxes<>'')" + postgres = "(SELECT mb.address || '@' || dom.name FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias_mboxes al_mb INNER JOIN admin_alias al ON al_mb.alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled AND al.address='%u' AND al.enabled)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled AND al.address='%u' AND al.enabled AND al.extmboxes<>'')" + +class DomainAliasesMailboxesMap(MapFile): + filename = 'sql-domain-aliases-mailboxes.cf' + mysql = "SELECT DISTINCT concat(mb.address, '@', dom.name) FROM admin_alias al INNER JOIN admin_domain dom ON dom.id=al.domain_id INNER JOIN admin_domainalias domal ON domal.target_id=dom.id INNER JOIN (admin_alias_mboxes almb, admin_mailbox mb) ON (almb.alias_id=al.id AND almb.mailbox_id=mb.id) WHERE domal.name='%d' AND domal.enabled=1 AND (al.address='%u' OR mb.address='%u')" + postgres = "SELECT DISTINCT mb.address || '@' || dom.name FROM admin_alias al INNER JOIN admin_domain dom ON dom.id=al.domain_id INNER JOIN admin_domainalias domal ON domal.target_id=dom.id INNER JOIN admin_alias_mboxes almb ON almb.alias_id=al.id INNER JOIN admin_mailbox mb ON almb.mailbox_id=mb.id WHERE domal.name='%d' AND domal.enabled AND (al.address='%u' OR mb.address='%u')" + +class Email2EmailMap(MapFile): + filename = 'sql-email2email.cf' + mysql = "SELECT concat(mb.address, '@', dom.name) FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN auth_user user ON mb.user_id=user.id WHERE dom.name='%d' AND dom.enabled=1 AND mb.address='%u' AND user.is_active=1" + postgres = "SELECT mb.address || '@' || dom.name FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN auth_user u ON mb.user_id=u.id WHERE dom.name='%d' AND dom.enabled AND mb.address='%u' AND u.is_active" + +class CatchallAliasesMap(MapFile): + filename = 'sql-catchall-aliases.cf' + mysql = "(SELECT concat(mb.address, '@', dom.name) FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id INNER JOIN admin_alias_mboxes al_mb ON al.id=al_mb.alias_id WHERE al.enabled=1 AND al.address='*' AND dom.name='%d' AND dom.enabled=1)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.enabled='1' AND al.extmboxes<>'' AND al.address='*' AND dom.name='%d' AND dom.enabled=1)" + postgres = "(SELECT mb.address || '@' || dom.name FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id INNER JOIN admin_alias_mboxes al_mb ON al.id=al_mb.alias_id WHERE al.enabled AND al.address='*' AND dom.name='%d' AND dom.enabled)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.enabled AND al.extmboxes<>'' AND al.address='*' AND dom.name='%d' AND dom.enabled)" + +class TransportMap(MapFile): + category = "autoreply" + filename = 'sql-transport.cf' + mysql = "SELECT method FROM postfix_autoreply_transport WHERE domain='%s'" + postgres = "SELECT method FROM postfix_autoreply_transport WHERE domain='%s'" + +class AutoRepliesMap(MapFile): + category = "autoreply" + filename = 'sql-autoreplies.cf' + mysql = "SELECT full_address, autoreply_address FROM postfix_autoreply_alias WHERE full_address='%s'" + postgres = "SELECT full_address, autoreply_address FROM postfix_autoreply_alias WHERE full_address='%s'" + +class PostfixMapsCommand(Command): + 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'], default='mysql', + help='Used database type') + self._parser.add_argument('--dbhost', type=str, default='127.0.0.1', + help='Database host address') + self._parser.add_argument('--categories', type=str, nargs='*', default=['std'], + help='Map file categories to generate (choices: std, autoreply)') + + def handle(self, parsed_args): + dbname = raw_input('Database name: ') + dbuser = raw_input('Username: ') + dbpass = getpass.getpass('Password: ') + + try: + os.mkdir(parsed_args.destdir) + except OSError: + pass + + for k, v in globals().iteritems(): + if not inspect.isclass(v) or not issubclass(v, MapFile): + continue + if not v.category in parsed_args.categories: + continue + v().render(parsed_args.destdir, parsed_args.dbtype, + dbname=dbname, dbuser=dbuser, dbpassword=dbpass, + dbhost=parsed_args.dbhost) diff --git a/modoboa/core/management.py b/modoboa/core/management.py index ee38e8b3b..2e8874e64 100644 --- a/modoboa/core/management.py +++ b/modoboa/core/management.py @@ -7,8 +7,14 @@ import os import sys class Command(object): - - def __init__(self): + """Base command class + + A valid administrative command must inherit from this class. + """ + help = "No help available." + + def __init__(self, commands, **kwargs): + self._commands = commands self._parser = argparse.ArgumentParser() if not settings.configured: settings.configure() @@ -25,23 +31,55 @@ class Command(object): self.handle(args) def handle(self, parsed_args): + """A command must overload this method to be called + + :param parsed_args: + """ raise NotImplemented +def scan_for_commands(dirname="commands"): + """Build a dictionnary containing all commands + + :param dirname: the directory where commands are located + :return: a dict of commands (name : class) + """ + path = os.path.join(os.path.dirname(__file__), dirname) + result = {} + for f in os.listdir(path): + if f in ['.', '..', '__init__.py']: + continue + if not f.endswith('.py'): + continue + if os.path.isfile(f): + continue + cmdname = f.replace('.py', '') + cmdmod = __import__("modoboa.core.commands", globals(), locals(), + [cmdname]) + cmdmod = getattr(cmdmod, cmdname) + if '_' in cmdname: + cmdclassname = ''.join(map(lambda s: s.capitalize(), cmdname.split('_'))) + else: + cmdclassname = cmdname.capitalize() + try: + cmdclass = getattr(cmdmod, "%sCommand" % cmdclassname) + except AttributeError: + continue + result[cmdname] = cmdclass + return result + def handle_command_line(): - parser = argparse.ArgumentParser() + commands = scan_for_commands() + parser = argparse.ArgumentParser( + description="A set of utilities to ease the installation of Modoboa.", + epilog="""Available commands: +%s +""" % "\n".join(map(lambda c: "\t" + c, sorted(commands)))) parser.add_argument('command', type=str, help='A valid command name') (args, remaining) = parser.parse_known_args() - - try: - cmdmod = __import__("modoboa.core.commands", globals(), locals(), - [args.command]) - cmdmod = getattr(cmdmod, args.command) - cmdclass = getattr(cmdmod, "%sCommand" % args.command.capitalize()) - cmd = cmdclass() - except ImportError: + if not commands.has_key(args.command): print >>sys.stderr, "Unknown command '%s'" % args.command sys.exit(1) - cmd.run(remaining) + commands[args.command](commands).run(remaining)