0001# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
0002# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
0003"""
0004Provides the two commands for preparing an application:
0005``prepare-app`` and ``setup-app``
0006"""
0007
0008import os
0009import sys
0010if sys.version_info < (2, 4):
0011    from paste.script.util import string24 as string
0012else:
0013    import string
0014import new
0015from cStringIO import StringIO
0016from paste.script.command import Command, BadCommand, run as run_command
0017import paste.script.templates
0018from paste.script import copydir
0019import pkg_resources
0020Cheetah = None
0021from ConfigParser import ConfigParser
0022from paste.util import import_string
0023from paste.deploy import appconfig
0024from paste.script.util import uuid
0025from paste.script.util import secret
0026
0027class AbstractInstallCommand(Command):
0028
0029    default_interactive = 1
0030
0031    default_sysconfigs = [
0032        (False, '/etc/paste/sysconfig.py'),
0033        (False, '/usr/local/etc/paste/sysconfig.py'),
0034        (True, 'paste.script.default_sysconfig'),
0035        ]
0036    if os.environ.get('HOME'):
0037        default_sysconfigs.insert(
0038            0, (False, os.path.join(os.environ['HOME'], '.paste', 'config',
0039                                    'sysconfig.py')))
0040    if os.environ.get('PASTE_SYSCONFIG'):
0041        default_sysconfigs.insert(
0042            0, (False, os.environ['PASTE_SYSCONFIG']))
0043
0044    def run(self, args):
0045        # This is overridden so we can parse sys-config before we pass
0046        # it to optparse
0047        self.sysconfigs = self.default_sysconfigs
0048        new_args = []
0049        while args:
0050            if args[0].startswith('--no-default-sysconfig'):
0051                self.sysconfigs = []
0052                args.pop(0)
0053                continue
0054            if args[0].startswith('--sysconfig='):
0055                self.sysconfigs.insert(
0056                    0, (True, args.pop(0)[len('--sysconfig='):]))
0057                continue
0058            if args[0] == '--sysconfig':
0059                args.pop(0)
0060                if not args:
0061                    raise BadCommand, (
0062                        "You gave --sysconfig as the last argument without "
0063                        "a value")
0064                self.sysconfigs.insert(0, (True, args.pop(0)))
0065                continue
0066            new_args.append(args.pop(0))
0067        self.load_sysconfigs()
0068        return super(AbstractInstallCommand, self).run(new_args)
0069
0070    #@classmethod
0071    def standard_parser(cls, **kw):
0072        parser = super(AbstractInstallCommand, cls).standard_parser(**kw)
0073        parser.add_option('--sysconfig',
0074                          action="append",
0075                          dest="sysconfigs",
0076                          help="System configuration file")
0077        parser.add_option('--no-default-sysconfig',
0078                          action='store_true',
0079                          dest='no_default_sysconfig',
0080                          help="Don't load the default sysconfig files")
0081        parser.add_option(
0082            '--easy-install',
0083            action='append',
0084            dest='easy_install_op',
0085            metavar='OP',
0086            help='An option to add if invoking easy_install (like --easy-install=exclude-scripts)')
0087        parser.add_option(
0088            '--no-install',
0089            action='store_true',
0090            dest='no_install',
0091            help="Don't try to install the package (it must already be installed)")
0092        parser.add_option(
0093            '-f', '--find-links',
0094            action='append',
0095            dest='easy_install_find_links',
0096            metavar='URL',
0097            help='Passed through to easy_install')
0098
0099        return parser
0100
0101    standard_parser = classmethod(standard_parser)
0102
0103    ########################################
0104    ## Sysconfig Handling
0105    ########################################
0106
0107    def load_sysconfigs(self):
0108        configs = self.sysconfigs[:]
0109        configs.reverse()
0110        self.sysconfig_modules = []
0111        for index, (explicit, name) in enumerate(configs):
0112            # @@: At some point I'd like to give the specialized
0113            # modules some access to the values in earlier modules,
0114            # e.g., to specialize those values or functions.  That's
0115            # why these modules are loaded backwards.
0116            if name.endswith('.py'):
0117                if not os.path.exists(name):
0118                    if explicit:
0119                        raise BadCommand, (
0120                            "sysconfig file %s does not exist"
0121                            % name)
0122                    else:
0123                        continue
0124                globs = {}
0125                execfile(name, globs)
0126                mod = new.module('__sysconfig_%i__' % index)
0127                for name, value in globs.items():
0128                    setattr(mod, name, value)
0129                mod.__file__ = name
0130            else:
0131                try:
0132                    mod = import_string.simple_import(name)
0133                except ImportError, e:
0134                    if explicit:
0135                        raise
0136                    else:
0137                        continue
0138            mod.paste_command = self
0139            self.sysconfig_modules.insert(0, mod)
0140        # @@: I'd really prefer to clone the parser here somehow,
0141        # not to modify it in place
0142        parser = self.parser
0143        self.call_sysconfig_functions('add_custom_options', parser)
0144
0145    def get_sysconfig_option(self, name, default=None):
0146        """
0147        Return the value of the given option in the first sysconfig
0148        module in which it is found, or ``default`` (None) if not
0149        found in any.
0150        """
0151        for mod in self.sysconfig_modules:
0152            if hasattr(mod, name):
0153                return getattr(mod, name)
0154        return default
0155
0156    def get_sysconfig_options(self, name):
0157        """
0158        Return the option value for the given name in all the
0159        sysconfig modules in which is is found (``[]`` if none).
0160        """
0161        return [getattr(mod, name) for mod in self.sysconfig_modules
0162                if hasattr(mod, name)]
0163
0164    def call_sysconfig_function(self, name, *args, **kw):
0165        """
0166        Call the specified function in the first sysconfig module it
0167        is defined in.  ``NameError`` if no function is found.
0168        """
0169        val = self.get_sysconfig_option(name)
0170        if val is None:
0171            raise NameError, (
0172                "Method %s not found in any sysconfig module" % name)
0173        return val(*args, **kw)
0174
0175    def call_sysconfig_functions(self, name, *args, **kw):
0176        """
0177        Call all the named functions in the sysconfig modules,
0178        returning a list of the return values.
0179        """
0180        return [method(*args, **kw) for method in
0181                self.get_sysconfig_options(name)]
0182
0183    def sysconfig_install_vars(self, installer):
0184        """
0185        Return the folded results of calling the
0186        ``install_variables()`` functions.
0187        """
0188        result = {}
0189        all_vars = self.call_sysconfig_functions(
0190            'install_variables', installer)
0191        all_vars.reverse()
0192        for vardict in all_vars:
0193            result.update(vardict)
0194        return result
0195
0196    ########################################
0197    ## Distributions
0198    ########################################
0199
0200    def get_distribution(self, req):
0201        """
0202        This gets a distribution object, and installs the distribution
0203        if required.
0204        """
0205        try:
0206            dist = pkg_resources.get_distribution(req)
0207            if self.verbose:
0208                print 'Distribution already installed:'
0209                print ' ', dist, 'from', dist.location
0210            return dist
0211        except pkg_resources.DistributionNotFound:
0212            if self.options.no_install:
0213                print "Because --no-install was given, we won't try to install the package %s" % req
0214                raise
0215            options = ['-v', '-m']
0216            for op in self.options.easy_install_op or []:
0217                if not op.startswith('-'):
0218                    op = '--'+op
0219                options.append(op)
0220            for op in self.options.easy_install_find_links or []:
0221                options.append('--find-links=%s' % op)
0222            if self.simulate:
0223                raise BadCommand(
0224                    "Must install %s, but in simulation mode" % req)
0225            print "Must install %s" % req
0226            from setuptools.command import easy_install
0227            from setuptools import setup
0228            setup(script_args=['-q', 'easy_install']
0229                  + options + [req])
0230            return pkg_resources.get_distribution(req)
0231
0232    def get_installer(self, distro, ep_group, ep_name):
0233        installer_class = distro.load_entry_point(
0234            'paste.app_install', ep_name)
0235        installer = installer_class(
0236            distro, ep_group, ep_name)
0237        return installer
0238
0239
0240class MakeConfigCommand(AbstractInstallCommand):
0241
0242    default_verbosity = 1
0243    max_args = None
0244    min_args = 1
0245    summary = "Install a package and create a fresh config file/directory"
0246    usage = "PACKAGE_NAME [CONFIG_FILE] [VAR=VALUE]"
0247
0248    description = """\
0249    Note: this is an experimental command, and it will probably change
0250    in several ways by the next release.
0251
0252    make-config is part of a two-phase installation process (the
0253    second phase is setup-app).  make-config installs the package
0254    (using easy_install) and asks it to create a bare configuration
0255    file or directory (possibly filling in defaults from the extra
0256    variables you give).
0257    """
0258
0259    parser = AbstractInstallCommand.standard_parser(
0260        simulate=True, quiet=True, no_interactive=True)
0261    parser.add_option('--info',
0262                      action="store_true",
0263                      dest="show_info",
0264                      help="Show information on the package (after installing it), but do not write a config.")
0265    parser.add_option('--name',
0266                      action='store',
0267                      dest='ep_name',
0268                      help='The name of the application contained in the distribution (default "main")')
0269    parser.add_option('--entry-group',
0270                      action='store',
0271                      dest='ep_group',
0272                      default='paste.app_factory',
0273                      help='The entry point group to install (i.e., the kind of application; default paste.app_factory')
0274    parser.add_option('--edit',
0275                      action='store_true',
0276                      dest='edit',
0277                      help='Edit the configuration file after generating it (using $EDITOR)')
0278    parser.add_option('--setup',
0279                      action='store_true',
0280                      dest='run_setup',
0281                      help='Run setup-app immediately after generating (and possibly editing) the configuration file')
0282
0283    def command(self):
0284        self.requirement = self.args[0]
0285        if '#' in self.requirement:
0286            if self.options.ep_name is not None:
0287                raise BadCommand(
0288                    "You may not give both --name and a requirement with "
0289                    "#name")
0290            self.requirement, self.options.ep_name = self.requirement.split('#', 1)
0291        if not self.options.ep_name:
0292            self.options.ep_name = 'main'
0293        self.distro = self.get_distribution(self.requirement)
0294        self.installer = self.get_installer(
0295            self.distro, self.options.ep_group, self.options.ep_name)
0296        if self.options.show_info:
0297            if len(self.args) > 1:
0298                raise BadCommand(
0299                    "With --info you can only give one argument")
0300            return self.show_info()
0301        if len(self.args) < 2:
0302            # See if sysconfig can give us a default filename
0303            options = filter(None, self.call_sysconfig_functions(
0304                'default_config_filename', self.installer))
0305            if not options:
0306                raise BadCommand(
0307                    "You must give a configuration filename")
0308            self.config_file = options[0]
0309        else:
0310            self.config_file = self.args[1]
0311        self.check_config_file()
0312        self.project_name = self.distro.project_name
0313        self.vars = self.sysconfig_install_vars(self.installer)
0314        self.vars.update(self.parse_vars(self.args[2:]))
0315        self.vars['project_name'] = self.project_name
0316        self.vars['requirement'] = self.requirement
0317        self.vars['ep_name'] = self.options.ep_name
0318        self.vars['ep_group'] = self.options.ep_group
0319        self.vars.setdefault('app_name', self.project_name.lower())
0320        self.vars.setdefault('app_instance_uuid', uuid.uuid4())
0321        self.vars.setdefault('app_instance_secret', secret.secret_string())
0322        if self.verbose > 1:
0323            print_vars = self.vars.items()
0324            print_vars.sort()
0325            print 'Variables for installation:'
0326            for name, value in print_vars:
0327                print '  %s: %r' % (name, value)
0328        self.installer.write_config(self, self.config_file, self.vars)
0329        edit_success = True
0330        if self.options.edit:
0331            edit_success = self.run_editor()
0332        setup_configs = self.installer.editable_config_files(self.config_file)
0333        # @@: We'll just assume the first file in the list is the one
0334        # that works with setup-app...
0335        setup_config = setup_configs[0]
0336        if self.options.run_setup:
0337            if not edit_success:
0338                print 'Config-file editing was not successful.'
0339                if self.ask('Run setup-app anyway?', default=False):
0340                    self.run_setup(setup_config)
0341            else:
0342                self.run_setup(setup_config)
0343        else:
0344            filenames = self.installer.editable_config_files(self.config_file)
0345            assert not isinstance(filenames, basestring), (
0346                "editable_config_files returned a string, not a list")
0347            if not filenames and filenames is not None:
0348                print 'No config files need editing'
0349            else:
0350                print 'Now you should edit the config files'
0351                if filenames:
0352                    for fn in filenames:
0353                        print '  %s' % fn
0354
0355    def show_info(self):
0356        text = self.installer.description(None)
0357        print text
0358
0359    def check_config_file(self):
0360        if self.installer.expect_config_directory is None:
0361            return
0362        fn = self.config_file
0363        if self.installer.expect_config_directory:
0364            if os.path.splitext(fn)[1]:
0365                raise BadCommand(
0366                    "The CONFIG_FILE argument %r looks like a filename, "
0367                    "and a directory name is expected" % fn)
0368        else:
0369            if fn.endswith('/') or not os.path.splitext(fn):
0370                raise BadCommand(
0371                    "The CONFIG_FILE argument %r looks like a directory "
0372                    "name and a filename is expected" % fn)
0373
0374    def run_setup(self, filename):
0375        run_command(['setup-app', filename])
0376
0377    def run_editor(self):
0378        filenames = self.installer.editable_config_files(self.config_file)
0379        if filenames is None:
0380            print 'Warning: the config file is not known (--edit ignored)'
0381            return False
0382        if not filenames:
0383            print 'Warning: no config files need editing (--edit ignored)'
0384            return True
0385        if len(filenames) > 1:
0386            print 'Warning: there is more than one editable config file (--edit ignored)'
0387            return False
0388        if not os.environ.get('EDITOR'):
0389            print 'Error: you must set $EDITOR if using --edit'
0390            return False
0391        if self.verbose:
0392            print '%s %s' % (os.environ['EDITOR'], filenames[0])
0393        retval = os.system('$EDITOR %s' % filenames[0])
0394        if retval:
0395            print 'Warning: editor %s returned with error code %i' % (
0396                os.environ['EDITOR'], retval)
0397            return False
0398        return True
0399
0400class SetupCommand(AbstractInstallCommand):
0401
0402    default_verbosity = 1
0403    max_args = 1
0404    min_args = 1
0405    summary = "Setup an application, given a config file"
0406    usage = "CONFIG_FILE"
0407
0408    description = """\
0409    Note: this is an experimental command, and it will probably change
0410    in several ways by the next release.
0411
0412    Setup an application according to its configuration file.  This is
0413    the second part of a two-phase web application installation
0414    process (the first phase is prepare-app).  The setup process may
0415    consist of things like creating directories and setting up
0416    databases.
0417    """
0418
0419    parser = AbstractInstallCommand.standard_parser(
0420        simulate=True, quiet=True, interactive=True)
0421    parser.add_option('--name',
0422                      action='store',
0423                      dest='section_name',
0424                      default=None,
0425                      help='The name of the section to set up (default: app:main)')
0426
0427    def command(self):
0428        config_spec = self.args[0]
0429        section = self.options.section_name
0430        if section is None:
0431            if '#' in config_spec:
0432                config_spec, section = config_spec.split('#', 1)
0433            else:
0434                section = 'main'
0435        if not ':' in section:
0436            plain_section = section
0437            section = 'app:'+section
0438        else:
0439            plain_section = section.split(':', 1)[0]
0440        if not config_spec.startswith('config:'):
0441            config_spec = 'config:' + config_spec
0442        if plain_section != 'main':
0443            config_spec += '#' + plain_section
0444        config_file = config_spec[len('config:'):].split('#', 1)[0]
0445        config_file = os.path.join(os.getcwd(), config_file)
0446        self.logging_file_config(config_file)
0447        conf = appconfig(config_spec, relative_to=os.getcwd())
0448        ep_name = conf.context.entry_point_name
0449        ep_group = conf.context.protocol
0450        dist = conf.context.distribution
0451        if dist is None:
0452            raise BadCommand(
0453                "The section %r is not the application (probably a filter).  You should add #section_name, where section_name is the section that configures your application" % plain_section)
0454        installer = self.get_installer(dist, ep_group, ep_name)
0455        installer.setup_config(
0456            self, config_file, section, self.sysconfig_install_vars(installer))
0457        self.call_sysconfig_functions(
0458            'post_setup_hook', installer, config_file)
0459
0460
0461class Installer(object):
0462
0463    """
0464    Abstract base class for installers, and also a generic
0465    installer that will run off config files in the .egg-info
0466    directory of a distribution.
0467
0468    Packages that simply refer to this installer can provide a file
0469    ``*.egg-info/paste_deploy_config.ini_tmpl`` that will be
0470    interpreted by Cheetah.  They can also provide ``websetup``
0471    modules with a ``setup_app(command, conf, vars)`` (or the
0472    now-deprecated ``setup_config(command, filename, section, vars)``)
0473    function, that will be called.
0474
0475    In the future other functions or configuration files may be
0476    called.
0477    """
0478
0479    # If this is true, then try to detect filename-looking config_file
0480    # values, and reject them.  Conversely, if false try to detect
0481    # directory-looking values and reject them.  None means don't
0482    # check.
0483    expect_config_directory = False
0484
0485    # Set this to give a default config filename when none is
0486    # specified:
0487    default_config_filename = None
0488
0489    # Set this to true to use Cheetah to fill your templates, or false
0490    # to not do so:
0491    use_cheetah = True
0492
0493    def __init__(self, dist, ep_group, ep_name):
0494        self.dist = dist
0495        self.ep_group = ep_group
0496        self.ep_name = ep_name
0497
0498    def description(self, config):
0499        return 'An application'
0500
0501    def write_config(self, command, filename, vars):
0502        """
0503        Writes the content to the filename (directory or single file).
0504        You should use the ``command`` object, which respects things
0505        like simulation and interactive.  ``vars`` is a dictionary
0506        of user-provided variables.
0507        """
0508        command.ensure_file(filename, self.config_content(command, vars))
0509
0510    def editable_config_files(self, filename):
0511        """
0512        Return a list of filenames; this is primarily used when the
0513        filename is treated as a directory and several configuration
0514        files are created.  The default implementation returns the
0515        file itself.  Return None if you don't know what files should
0516        be edited on installation.
0517        """
0518        if not self.expect_config_directory:
0519            return [filename]
0520        else:
0521            return None
0522
0523    def config_content(self, command, vars):
0524        """
0525        Called by ``self.write_config``, this returns the text content
0526        for the config file, given the provided variables.
0527
0528        The default implementation reads
0529        ``Package.egg-info/paste_deploy_config.ini_tmpl`` and fills it
0530        with the variables.
0531        """
0532        global Cheetah
0533        meta_name = 'paste_deploy_config.ini_tmpl'
0534        if not self.dist.has_metadata(meta_name):
0535            if command.verbose:
0536                print 'No %s found' % meta_name
0537            return self.simple_config(vars)
0538        return self.template_renderer(
0539            self.dist.get_metadata(meta_name), vars, filename=meta_name)
0540
0541    def template_renderer(self, content, vars, filename=None):
0542        """
0543        Subclasses may override this to provide different template
0544        substitution (e.g., use a different template engine).
0545        """
0546        if self.use_cheetah:
0547            import Cheetah.Template
0548            tmpl = Cheetah.Template.Template(content,
0549                                             searchList=[vars])
0550            return copydir.careful_sub(
0551                tmpl, vars, filename)
0552        else:
0553            tmpl = string.Template(content)
0554            return tmpl.substitute(vars)
0555
0556    def simple_config(self, vars):
0557        """
0558        Return a very simple configuration file for this application.
0559        """
0560        if self.ep_name != 'main':
0561            ep_name = '#'+self.ep_name
0562        else:
0563            ep_name = ''
0564        return ('[app:main]\n'
0565                'use = egg:%s%s\n'
0566                % (self.dist.project_name, ep_name))
0567
0568    def setup_config(self, command, filename, section, vars):
0569        """
0570        Called to setup an application, given its configuration
0571        file/directory.
0572
0573        The default implementation calls
0574        ``package.websetup.setup_config(command, filename, section,
0575        vars)`` or ``package.websetup.setup_app(command, config,
0576        vars)``
0577
0578        With ``setup_app`` the ``config`` object is a dictionary with
0579        the extra attributes ``global_conf``, ``local_conf`` and
0580        ``filename``
0581        """
0582        modules = [
0583            line.strip()
0584            for line in self.dist.get_metadata_lines('top_level.txt')
0585            if line.strip() and not line.strip().startswith('#')]
0586        if not modules:
0587            print 'No modules are listed in top_level.txt'
0588            print 'Try running python setup.py egg_info to regenerate that file'
0589        for mod_name in modules:
0590            mod_name = mod_name + '.websetup'
0591            mod = import_string.try_import_module(mod_name)
0592            if mod is None:
0593                continue
0594            if command.verbose:
0595                print 'Running setup_config() from %s' % mod_name
0596            if hasattr(mod, 'setup_app'):
0597                self._call_setup_app(
0598                    mod.setup_app, command, filename, section, vars)
0599            elif hasattr(mod, 'setup_config'):
0600                mod.setup_config(command, filename, section, vars)
0601            else:
0602                print 'No setup_app() or setup_config() function in %s (%s)' % (
0603                    mod.__name__, mod.__file__)
0604
0605    def _call_setup_app(self, func, command, filename, section, vars):
0606        filename = os.path.abspath(filename)
0607        if ':' in section:
0608            section = section.split(':', 1)[1]
0609        conf = 'config:%s#%s' % (filename, section)
0610        conf = appconfig(conf)
0611        conf.filename = filename
0612        func(command, conf, vars)