manage.py 12 KB
Newer Older
Vangelis Koukis's avatar
Vangelis Koukis committed
1
# Copyright (C) 2010-2014 GRNET S.A.
2
#
Vangelis Koukis's avatar
Vangelis Koukis committed
3 4 5 6
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
7
#
Vangelis Koukis's avatar
Vangelis Koukis committed
8 9 10 11
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
12
#
Vangelis Koukis's avatar
Vangelis Koukis committed
13 14
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
"""
Extented django management module

Most of the code is shared from django.core.management module
to allow us extend the default django ManagementUtility object
used to provide command line interface of the django project
included in snf-webproject package.

The extended class provides the following:

- additional argument for the configuration of the SYNNEFO_SETTINGS_DIR
  environmental variable (--settings-dir).
- a fix for management utility to handle custom commands defined in
  applications living in namespaced packages (django ticket #14087)
- override of --version command to display the snf-webproject version
"""
32

33
from django.core.management import ManagementUtility, \
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
34 35
    BaseCommand, LaxOptionParser, handle_default_options, find_commands, \
    load_command_class
36

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
37
from django.core import management
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
38
from optparse import make_option
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
39
from synnefo.util.version import get_component_version
40
from synnefo.lib.dictconfig import dictConfig
41 42

import sys
43
import locale
44
import os
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
45 46 47 48
import imp

_commands = None

49

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
50 51 52 53 54 55
def find_modules(name, path=None):
    """Find all modules with name 'name'

    Unlike find_module in the imp package this returns a list of all
    matched modules.
    """
56

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
57
    results = []
Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
58 59
    if path is None:
        path = sys.path
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
60 61 62 63 64 65 66 67 68 69 70 71
    for p in path:
        importer = sys.path_importer_cache.get(p, None)
        if importer is None:
            find_module = imp.find_module
        else:
            find_module = importer.find_module

        try:
            result = find_module(name, [p])
            if result is not None:
                results.append(result)
        except ImportError:
72 73
            if sys.modules.get(name, None):
                modpath = sys.modules[name].__path__
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
74 75 76
                if isinstance(modpath, basestring) \
                   and not ('', modpath) in results:
                    results.append(('', sys.modules[name].__path__))
77 78 79 80
                else:
                    for mp in modpath:
                        if not ('', mp) in results:
                            results.append(('', mp))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
81
            pass
82

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
83 84
    if not results:
        raise ImportError("No module named %.200s" % name)
85

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
86 87
    return results

Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
88

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
def find_management_module(app_name):
    """
    Determines the path to the management module for the given app_name,
    without actually importing the application or the management module.

    Raises ImportError if the management module cannot be found for any reason.
    """
    parts = app_name.split('.')
    parts.append('management')
    parts.reverse()
    part = parts.pop()
    paths = None

    # When using manage.py, the project module is added to the path,
    # loaded, then removed from the path. This means that
    # testproject.testapp.models can be loaded in future, even if
    # testproject isn't in the path. When looking for the management
    # module, we need look for the case where the project name is part
    # of the app_name but the project directory itself isn't on the path.
    try:
        modules = find_modules(part, paths)
        paths = [m[1] for m in modules]
Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
111
    except ImportError:
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
112
        if os.path.basename(os.getcwd()) != part:
Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
113
            raise
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147

    while parts:
        part = parts.pop()
        modules = find_modules(part, paths)
        paths = [m[1] for m in modules]
    return paths[0]


def get_commands():
    """
    Returns a dictionary mapping command names to their callback applications.

    This works by looking for a management.commands package in django.core, and
    in each installed application -- if a commands package exists, all commands
    in that package are registered.

    Core commands are always included. If a settings module has been
    specified, user-defined commands will also be included, the
    startproject command will be disabled, and the startapp command
    will be modified to use the directory in which the settings module appears.

    The dictionary is in the format {command_name: app_name}. Key-value
    pairs from this dictionary can then be used in calls to
    load_command_class(app_name, command_name)

    If a specific version of a command must be loaded (e.g., with the
    startapp command), the instantiated module can be placed in the
    dictionary in place of the application name.

    The dictionary is cached on the first call and reused on subsequent
    calls.
    """
    global _commands
    if _commands is None:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
148 149
        _commands = dict([(name, 'django.core') for name in
                         find_commands(management.__path__[0])])
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164

        # Find the installed apps
        try:
            from django.conf import settings
            apps = settings.INSTALLED_APPS
        except (AttributeError, EnvironmentError, ImportError):
            apps = []

        # Find and load the management module for each installed app.
        for app_name in apps:
            try:
                path = find_management_module(app_name)
                _commands.update(dict([(name, app_name)
                                       for name in find_commands(path)]))
            except ImportError:
Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
165
                pass  # No management module - ignore this app
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
166

167
        if apps:
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
168 169 170 171 172
            # Remove the "startproject" command from self.commands, because
            # that's a django-admin.py command, not a manage.py command.
            del _commands['startproject']

    return _commands
173

Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
174

175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
class SynnefoManagementUtility(ManagementUtility):
    """
    Override django ManagementUtility to allow us provide a custom
    --settings-dir option for synnefo application.

    Most of the following code is a copy from django.core.management module
    """

    def execute(self):
        """
        Given the command-line arguments, this figures out which subcommand is
        being run, creates a parser appropriate to that command, and runs it.
        """

        # --settings-dir option
        # will remove it later to avoid django commands from raising errors
        option_list = BaseCommand.option_list + (
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
192 193
            make_option(
                '--settings-dir',
194 195 196 197 198 199 200 201 202
                action='store',
                dest='settings_dir',
                default=None,
                help='Load *.conf files from directory as settings'),)

        # Preprocess options to extract --settings and --pythonpath.
        # These options could affect the commands that are available, so they
        # must be processed early.
        parser = LaxOptionParser(usage="%prog subcommand [options] [args]",
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
203
                                 version=get_component_version('webproject'),
204 205 206 207 208 209
                                 option_list=option_list)
        self.autocomplete()
        try:
            options, args = parser.parse_args(self.argv)
            handle_default_options(options)
        except:
Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
210
            pass  # Ignore any option errors at this point.
211 212 213 214 215 216 217 218 219 220 221 222

        # user provides custom settings dir
        # set it as environmental variable and remove it from self.argv
        if options.settings_dir:
            os.environ['SYNNEFO_SETTINGS_DIR'] = options.settings_dir
            for arg in self.argv:
                if arg.startswith('--settings-dir'):
                    self.argv.remove(arg)

        try:
            subcommand = self.argv[1]
        except IndexError:
Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
223
            subcommand = 'help'  # Display help if no arguments were given.
224

225
        # Encode stdout. This check is required because of the way python
Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
226 227
        # checks if something is tty:
        # https://bugzilla.redhat.com/show_bug.cgi?id=841152
228
        if subcommand not in ['test'] and 'shell' not in subcommand:
229 230
            sys.stdout = EncodedStream(sys.stdout)
            sys.stderr = EncodedStream(sys.stderr)
231

232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
        if subcommand == 'help':
            if len(args) > 2:
                self.fetch_command(args[2]).print_help(self.prog_name, args[2])
            else:
                parser.print_lax_help()
                sys.stdout.write(self.main_help_text() + '\n')
                sys.exit(1)
        # Special-cases: We want 'django-admin.py --version' and
        # 'django-admin.py --help' to work, for backwards compatibility.
        elif self.argv[1:] == ['--version']:
            # LaxOptionParser already takes care of printing the version.
            pass
        elif self.argv[1:] in (['--help'], ['-h']):
            parser.print_lax_help()
            sys.stdout.write(self.main_help_text() + '\n')
        else:
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
            sub_command = self.fetch_command(subcommand)
            # NOTE: This is an ugly workaround to bypass the problem with
            # the required permissions for the named pipes that Pithos backend
            # is creating in order to communicate with XSEG.
            if subcommand == 'test' or\
               subcommand.startswith('image-') or\
               subcommand.startswith('snapshot-') or\
               subcommand.startswith('file-'):
                # Set common umask for known commands
                os.umask(0o007)
            # Allow command to define a custom umask
            cmd_umask = getattr(sub_command, 'umask', None)
            if cmd_umask is not None:
                os.umask(cmd_umask)
            sub_command.run_from_argv(self.argv)
Faidon Liambotis's avatar
Faidon Liambotis committed
263

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
264 265 266 267
    def main_help_text(self):
        """
        Returns the script's main help text, as a string.
        """
Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
268 269
        usage = ['', ("Type '%s help <subcommand>' for help"
                      "on a specific subcommand.") % self.prog_name, '']
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
        usage.append('Available subcommands:')
        commands = get_commands().keys()
        commands.sort()
        for cmd in commands:
            usage.append('  %s' % cmd)
        return '\n'.join(usage)

    def fetch_command(self, subcommand):
        """
        Tries to fetch the given subcommand, printing a message with the
        appropriate command called from the command line (usually
        "django-admin.py" or "manage.py") if it can't be found.
        """
        try:
            app_name = get_commands()[subcommand]
        except KeyError:
Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
286 287 288
            sys.stderr.write(("Unknown command: %r\n"
                              "Type '%s help' for usage.\n") %
                             (subcommand, self.prog_name))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
289 290 291 292 293 294 295 296
            sys.exit(1)
        if isinstance(app_name, BaseCommand):
            # If the command is already loaded, use it directly.
            klass = app_name
        else:
            klass = load_command_class(app_name, subcommand)
        return klass

297 298 299 300 301 302 303 304 305 306 307 308

def configure_logging():
    try:
        from synnefo.settings import SNF_MANAGE_LOGGING_SETUP
        dictConfig(SNF_MANAGE_LOGGING_SETUP)
    except ImportError:
        import logging
        logging.basicConfig()
        log = logging.getLogger()
        log.warning("SNF_MANAGE_LOGGING_SETUP setting missing.")


309 310
class EncodedStream(object):
    def __init__(self, stream):
311
        try:
312
            std_encoding = stream.encoding
313 314 315
        except AttributeError:
            std_encoding = None
        self.encoding = std_encoding or locale.getpreferredencoding()
316
        self.original_stream = stream
317 318 319

    def write(self, string):
        if isinstance(string, unicode):
320 321
            string = string.encode(self.encoding, errors="replace")
        self.original_stream.write(string)
322

323
    def __getattr__(self, name):
324
        return getattr(self.original_stream, name)
325

326

327
def main():
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
328 329
    os.environ['DJANGO_SETTINGS_MODULE'] = \
        os.environ.get('DJANGO_SETTINGS_MODULE', 'synnefo.settings')
330
    configure_logging()
331 332
    mu = SynnefoManagementUtility(sys.argv)
    mu.execute()
333 334 335

if __name__ == "__main__":
    main()