Commits

theoden committed 9c01873 Merge

merge from multiple web-servers branch

Comments (0)

Files changed (37)

 
         # my_project/fabfile.py
         from fabric.api import env, task
+
+        from fab_deploy.project import WebProject
         from fab_deploy.utils import update_env
+        from fab_deploy.django import Django
+        from fab_deploy.webserver.apache import Apache
+        from fab_deploy.webserver.nginx import Nginx
+
+        apps = dict(django=Django(Nginx(), Apache()))
+        WebProject(apps=apps).expose_to_current_module()
 
         @task
         def my_site():
 
         my_site()
 
-   There is a simpler syntax for the code above::
+   ``apps`` dictionary is provided with default values for WebProject. Yes, 
+   that is a fallback to previous versions of django-fab-deploy. And 
+   there is a simpler syntax for the code above::
 
+        from fab_deploy.project import WebProject
         from fab_deploy.utils import define_host
 
+        WebProject().expose_to_current_module()
+
         @define_host('my_site@example.com')
         def my_site():
             return dict(
        after initial deployment, apply the changes in web server configs
        by running ::
 
-           fab setup_web_server
+           fab update_web_servers
+
+       It will update all remote configs of all apps of your default project.
 
 
 4. Create :file:`config.server.py` near your project's ``settings.py``.
        after initial deployment, apply the changes to :file:`config.server.py`
        by running ::
 
-           fab update_django_config
+           fab apps.django.update_config
 
+       for default apps configuration. Or more generic ::
+
+           fab apps.{{ django_app_name }}.update_config
 
 5. Create ``reqs`` folder at project root. This folder should contain
    text files with `pip requirements <http://pip.openplans.org/requirement-format.html>`_.
             ...          <- you can provide extra files and include them with '-r' syntax in e.g. all.txt
 
         config.py        <- this file should be included in settings.py and ignored in .hgignore
-        config.server.py <- this is a production django config template
+        config.server.py <- this is a production django config template (should be ignored too!)
         fabfile.py       <- your project's Fabric deployment script
         settings.py
         manage.py
 Prepare the server
 ------------------
 
+.. note::
+
+    It is assumed that you would manage imports in fabfile.py appropriately. E.g.
+    for command "fab system.{{commmand}}" to work "from fab_deploy import system"
+    would be added, for command "fab db.{{command}}" - "from fab_deploy import db",
+    and so on.
+
 1. If the server doesn't have sudo installed (e.g. clean Lenny or Squeezy)
    then install sudo on server::
 
-       fab install_sudo
+       fab system.install_sudo
 
    .. note::
 
 
    ::
 
-       fab create_linux_account:"/home/kmike/.ssh/id_rsa.pub"
+       fab system.create_linux_account:"/home/kmike/.ssh/id_rsa.pub"
 
    You'll need the ssh public key.
    :func:`create_linux_account <fab_deploy.system.create_linux_account>`
 
    SSH keys for other developers can be added at any time::
 
-       fab ssh_add_key:"/home/kmike/coworker-keys/ivan.id_dsa.pub"
+       fab system.ssh_add_key:"/home/kmike/coworker-keys/ivan.id_dsa.pub"
 
 3. Setup the database. django-fab-deploy can install mysql and create empty
    DB for the project (using defaults in your default host function)::
 
-       fab mysql.install
-       fab mysql.create_db
+       fab db.mysql.install
+       fab db.mysql.create_db
 
    :func:`mysql.install <fab_deploy.db.mysql.install>` does
    nothing if mysql is already installed on server. Otherwise it installs
 
    .. note::
 
-        If the DB enging is not mysql then use appropriate commands.
+        If the DB engine is not mysql then use appropriate commands.
 
 
 4. If you feel brave you can now run ``fab full_deploy`` from the project root
    ``fab full_deploy`` command:
 
    * installs necessary system and python packages;
-   * configures apache and ngnix;
+   * configures web-servers for all applications of your project;
    * creates virtualenv;
    * uploads project to the server;
    * runs ``python manage.py syncdb`` and ``python manage.py migrate`` commands
 
        fab prod push:pip_update,migrate
 
-2. Update web server configuration::
+2. Update web servers configuration::
 
-       fab setup_web_server
+       fab update_web_servers
 
-3. Update django configuration (:file:`config.server.py`)::
+3. Update some app configuration (:file:`config.server.py` for django 
+   or :file:`production.ini` for pyramid)::
 
-       fab update_django_config
+       fab apps.{{ app_name }}.update_config
+
+   where ``app_name`` actually is a key in apps dictionary.
 
 4. Requirements are updated with :func:`fab_deploy.virtualenv.pip_update`
    command. Update requirements listed in reqs/active.txt::
 
-       fab pip_update
+       fab update_r
 
    Update requirements listed in reqs/my_apps.txt::
 
-       fab pip_update:my_apps
+       fab update_r:my_apps
 
 5. Remotely change branch or revision (assuming :attr:`env.conf.VCS`
    is not 'none')::

fab_deploy/__init__.py

+# coding: utf-8
 from fabric.api import *
 
 from fab_deploy.db import mysql
-from fab_deploy.webserver import apache, nginx
-from fab_deploy import deploy, pip, django_commands, utils, system, crontab, vcs
+from fab_deploy import pip, utils, system, crontab, vcs

fab_deploy/apps.py

+# coding: utf-8
+from __future__ import with_statement
+
+from abc import ABCMeta, abstractmethod
+
+from fabric.api import abort, run, env
+from taskset import TaskSet, task_method
+
+from fab_deploy import utils
+
+class StaticSite(TaskSet):
+
+    def __init__(self, frontend):
+        self.frontend = frontend
+
+    def expose_as_module(self, module_name):
+        module = super(StaticSite, self).expose_as_module(module_name)
+        module.frontend = self.frontend.expose_as_module('frontend')
+        return module
+
+    @task_method
+    def deploy(self):
+        """ Deploys project on prepared server. """
+        self.update_web_servers()
+
+    @task_method
+    def update_web_servers(self):
+        """ Updates frontend config. """
+        self.frontend.update_config()
+
+    @task_method
+    def install_web_servers(self):
+        """ Install frontend software. """
+        self.frontend.update_config()
+
+    @task_method
+    def restart(self):
+        # nothing to do in a static site case
+        pass
+
+    @task_method
+    def remove(self):
+        """ Removes application traces.
+        
+        Is prohibited to remove app's code since several appications may share
+        single VCS repository.
+        """
+        self.frontend.remove_config()
+
+
+class WebApp(StaticSite):
+    __metaclass__ = ABCMeta
+
+    def __init__(self, frontend, backend):
+        super(WebApp, self).__init__(frontend)
+        self.backend = backend
+
+    def expose_as_module(self, module_name):
+        module = super(WebApp, self).expose_as_module(module_name)
+        module.backend = self.backend.expose_as_module('backend')
+        return module
+
+    @task_method
+    def deploy(self):
+        """ Deploys application on prepared server. """
+        self.update_web_servers()
+        self.update_config()
+    
+        self.syncdb()
+        self.migrate()
+
+    @task_method
+    def update_web_servers(self):
+        """ Updates frontend and backend configs. """
+        # TODO: detect the reason why apache should be restarted before nginx
+        # DO NOT try to invert the order - you'll fail
+        self.backend.update_config()
+        super(WebApp, self).update_web_servers()
+
+    @task_method
+    def install_web_servers(self):
+        """ Installs frontend and backend software. """
+        self.backend.install()
+        super(WebApp, self).install_web_servers()
+
+    @task_method
+    def restart(self):
+        """ Restarts web application.
+        
+        Usually by touching conf file or restarting backend.
+        """
+        # need this check assuming non-wsgi backends in the future
+        if hasattr(self.backend, 'touch'):
+            self.backend.touch()
+        else:
+            self.backend.restart()
+
+    @task_method
+    def remove(self):
+        """ Removes application traces.
+        
+        Is prohibited to remove app's code since several appications may share
+        single VCS repository.
+        """
+        super(WebApp, self).remove()
+        self.backend.remove_config()
+
+    @task_method
+    @abstractmethod
+    def update_config(self, restart=True):
+        """ Updates config of the web application. """
+        pass
+
+    @task_method
+    @abstractmethod
+    def syncdb(self, params=''):
+        pass
+
+    @task_method
+    @abstractmethod
+    def migrate(self, params='', do_backup=True):
+        pass
+
+    @task_method
+    @utils.inside_project
+    @abstractmethod
+    def test(self, what=''):
+        """ Launches tests for the web application. """
+        pass

fab_deploy/config_templates/apache.config

     WSGIDaemonProcess {{ INSTANCE_NAME }} user={{ USER }} group={{ USER }} processes={{ PROCESSES }} threads={{ THREADS }}
     WSGIProcessGroup {{ INSTANCE_NAME }}
 
-    WSGIScriptAlias / {{ ENV_DIR }}/var/wsgi/{{ INSTANCE_NAME }}.py
-    <Directory {{ ENV_DIR }}/var/wsgi/>
+    WSGIScriptAlias / {{ CURRENT_BACKEND.get_wsgi_full_file_name() }}
+    <Directory {{ CURRENT_BACKEND.get_wsgi_dir() }}>
         Order deny,allow
         allow from all
     </Directory>

fab_deploy/config_templates/django_wsgi.py

 application = django.core.handlers.wsgi.WSGIHandler()
 
 # django >= 1.4 wsgi setup: remove "<1.4 wsgi setup" above, uncomment the
-# following line and set proper your project's name:
+# following line and change "my_project" to your project's name:
 
 # from my_project.wsgi import application

fab_deploy/crontab.py

+# coding: utf-8
 from __future__ import with_statement
+
 from fabric.api import task, settings, hide, run, puts, env
 
-
 __all__ = ['set_content', 'add_line', 'puts_content', 'remove_line',
            'update_line', 'add_management', 'get_content']
 

fab_deploy/db/base.py

-# -*- coding: utf-8 -*-
+# coding: utf-8
 from __future__ import absolute_import
-from fabric.contrib import console
 import posixpath
 from datetime import datetime
+
+from fabric.contrib import console
 from fabric.api import env, run, abort, prompt, warn
 from fabric.context_managers import settings, hide
 from fabric.utils import puts
-from taskset import TaskSet, task
+from taskset import TaskSet, task_method
+
 from fab_deploy import utils
 
 __all__ = ['Database']
             db_password = env.conf.DB_PASSWORD
         return db_name, db_user, db_password
 
-    @task
+    @task_method
     def is_installed(self):
         raise NotImplementedError()
 
-    @task
+    @task_method
     def execute_sql(self, sql, user=None, password=None, db_name=None):
         """ Executes passed sql command. """
         raise NotImplementedError()
 
-    @task
+    @task_method
     def _user_exists(self, db_user):
         sql = self.USER_EXISTS_SQL % dict(db_user=db_user)
         with settings(hide('warnings', 'running', 'stdout', 'stderr'), warn_only=True):
             result = self.execute_sql(sql, self.SUPERUSER)
         return result.succeeded
 
-    @task
+    @task_method
     def _db_exists(self, db_name):
         raise NotImplementedError()
 
-    @task
+    @task_method
     def create_user(self, db_user=None, db_password=None):
         """ Creates database user. """
         _, db_user, db_password = self._credentials(None, db_user, db_password)
         sql = self.CREATE_USER_SQL % dict(db_user=db_user, db_password=db_password)
         return self.execute_sql(sql, self.SUPERUSER)
 
-    @task
+    @task_method
     def create_db(self, db_name=None, db_user=None, root_password=None):
         """ Creates an empty database. """
         db_name, db_user, _ = self._credentials(db_name, db_user, None)
             self.create_user(db_user=db_user)
             self.grant_permissions(db_name=db_name, db_user=db_user)
 
-    @task
+    @task_method
     def drop_db(self, db_name=None, confirm=True):
         db_name, _, _ = self._credentials(db_name, None, None)
         question = "Really drop database %s?" % db_name
         sql = self.DROP_DB_SQL % dict(db_name=db_name)
         self.execute_sql(sql, self.SUPERUSER)
 
-    @task
+    @task_method
     def drop_user(self, db_user=None, confirm=True):
         _, db_user, _ = self._credentials(None, db_user, None)
         question = "Really drop user %s?" % db_user
         self.execute_sql(sql, self.SUPERUSER)
 
 
-    @task
+    @task_method
     def grant_permissions(self, db_name=None, db_user=None):
         """ Grants all permissions on ``db_name`` for ``db_user``. """
         db_name, db_user, _ = self._credentials(db_name, db_user, None)
         return posixpath.join(env.conf.PROJECT_DIR, dir, filename)
 
 
-    @task
+    @task_method
     def dump(self, dir=None, db_name=None, db_user=None, db_password=None):
         """
         Dumps database.
     def _dump(self, db_user, db_password, db_name, filename):
         raise NotImplementedError()
 
-    @task
+    @task_method
     @utils.run_as_sudo
     def install(self):
         """ Installs database. """

fab_deploy/db/dummy.py

-# -*- coding: utf-8 -*-
+# coding: utf-8
 from __future__ import absolute_import
-from taskset import task
+from taskset import task_method
 from .base import Database
 
 __all__ = ['Dummy']
 class Dummy(Database):
     name = 'Dummy'
 
-    @task
+    @task_method
     def is_installed(self):
         return True
 
-    @task
+    @task_method
     def execute_sql(self, sql, user=None, password=None):
         pass
 
-    @task
+    @task_method
     def _user_exists(self, db_user):
         return False
 
-    @task
+    @task_method
     def create_user(self, db_user=None, db_password=None):
         pass
 
-    @task
+    @task_method
     def create_db(self, db_name=None, db_user=None, root_password=None):
         pass
 
-    @task
+    @task_method
     def drop_db(self, db_name, confirm=True):
         pass
 
-    @task
+    @task_method
     def grant_permissions(self, db_name=None, db_user=None):
         pass
 
-    @task
+    @task_method
     def dump(self, dir=None, db_name=None, db_user=None, db_password=None):
         pass
 
-    @task
+    @task_method
     def install(self):
         pass
 

fab_deploy/db/mysql.py

+# coding: utf-8
 from __future__ import with_statement
+
 from fabric.api import env, run, warn, abort
 from fabric.context_managers import settings, hide
 from fabric.operations import sudo
 from fabric.utils import puts
-from taskset import task
+from taskset import task_method
+
 from fab_deploy import utils
 from fab_deploy import system
 from fab_deploy.db import base
         warn('\n=========\nThe password for mysql "root" user will be set to "%s"\n=========\n' % passwd)
 
 
-    @task
+    @task_method
     @utils.run_as_sudo
     def install(self):
         """ Installs mysql. """
         system.aptitude_install('mysql-server')
         system.aptitude_install(' '.join(extra_packages[os]))
 
-    @task
+    @task_method
     def is_installed(self):
         with settings(hide('running', 'stdout', 'stderr', 'warnings'), warn_only=True):
             output = run('mysql --version')
             password = env.conf.DB_PASSWORD
         return user, password
 
-    @task
+    @task_method
     def execute_sql(self, sql, user=None, password=None, db_name=None):
         """ Executes passed sql command using mysql shell. """
         user, password = self._credentials_for_sql(user, password)

fab_deploy/db/postgis.py

-# -*- coding: utf-8 -*-
+# coding: utf-8
 from __future__ import absolute_import
+
 from fabric.context_managers import settings, hide
 from fabric.operations import sudo
-from taskset import task
+from taskset import task_method
+
 from fab_deploy.db.postgres import Postgres
 from fab_deploy import utils
 from fab_deploy import system
 
     POSTGIS_TEMPLATE_SCRIPT = _SCRIPT
 
-    @task
+    @task_method
     @utils.run_as_sudo
     def install(self):
         """ Installs PostgreSQL + postgis. """
         with settings(hide('stdout')):
             sudo(self.POSTGIS_TEMPLATE_SCRIPT, user=self.SUPERUSER)
 
-    @task
+    @task_method
     def create_db(self, db_name=None, db_user=None, root_password=None, template=None):
         """ Creates empty PostGIS-enabled database """
         if template is None:

fab_deploy/db/postgres.py

+# coding: utf-8
 from __future__ import with_statement
+
 from fabric.api import run, env, hide, settings, sudo, puts
-
-from taskset import task
+from taskset import task_method
 
 from fab_deploy import utils
 from fab_deploy import system
     CREATE_DB_SQL = "CREATE DATABASE %(db_name)s OWNER %(db_user)s ENCODING 'UTF8' TEMPLATE %(template)s;"
     USER_EXISTS_SQL = "SELECT 1 FROM pg_roles WHERE rolname='%(db_user)s;'"
 
-    @task
+    @task_method
     def is_installed(self):
         with settings(hide('running', 'stdout', 'stderr', 'warnings'), warn_only=True):
             output = run('psql --version')
         return output.succeeded
 
-    @task
+    @task_method
     @utils.run_as_sudo
     def install(self):
         """ Installs postgresql. """
         packages = 'postgresql-server-dev-8.4 postgresql-8.4'
         system.aptitude_install(packages)
 
-    @task
+    @task_method
     def execute_sql(self, sql, user=None, password=None, db_name=None):
         db_user = user or env.conf.DB_USER
         #db_host = env.conf.DB_HOST
         cmd = 'psql %s --user="%s" --host=127.0.0.1 --command "%s"' % (db_name, db_user, sql,)
         return run(pwd_cmd + cmd, pty=False, combine_stderr=False, shell=False)
 
-    @task
+    @task_method
     def _user_exists(self, db_user):
         sql = self.USER_EXISTS_SQL % dict(db_user=db_user)
         with settings(hide('warnings', 'running', 'stdout', 'stderr'), warn_only=True):
             result = self.execute_sql(sql, self.SUPERUSER)
         return '1' in result
 
-    @task
+    @task_method
     @utils.run_as_sudo
     def _db_exists(self, db_name):
         # XXX: this is untested
         cmd = 'psql -ltA | grep -q "^%s|"' % db_name
         return sudo(cmd, user=self.SUPERUSER).succeeded
 
-    @task
+    @task_method
     @utils.run_as_sudo
     def create_db(self, db_name=None, db_user=None, root_password=None, template='template1'):
         """ Creates an empty PostgreSQL database. """

fab_deploy/deploy.py

-#coding: utf-8
-from __future__ import with_statement
-from fabric.api import task, abort, settings, cd, sudo, run, env, puts
-from fabric.contrib import console
-from fabric.contrib import files
-
-from fab_deploy import utils
-from fab_deploy import pip
-from fab_deploy import django_commands as dj_cmd
-from fab_deploy import system
-from fab_deploy import vcs
-from fab_deploy.webserver import apache, nginx
-
-
-__all__ = ['full_deploy', 'deploy_project',
-           'update_django_config', 'up', 'setup_web_server', 'push',
-           'remove']
-
-@task
-def full_deploy():
-    """ Prepares server and deploys the project. """
-    os = utils.detect_os()
-    if not console.confirm("Is the OS detected correctly (%s)?" % os, default=False):
-        abort("Detection fails. Please set env.conf.OS to correct value.")
-    system.prepare()
-    deploy_project()
-
-@task
-def deploy_project():
-    """ Deploys project on prepared server. """
-    pip.virtualenv_create()
-    _make_clone()
-
-    pip.install_r(env.conf.PIP_REQUIREMENTS, restart=False)
-
-    setup_web_server()
-    update_django_config()
-
-    dj_cmd.syncdb()
-    dj_cmd.migrate()
-
-@task
-def _make_clone():
-    """ Creates repository clone on remote server. """
-    run('mkdir -p ' + env.conf.SRC_DIR)
-    with cd(env.conf.SRC_DIR):
-        with settings(warn_only=True):
-            vcs.init()
-    vcs.push()
-    with cd(env.conf.SRC_DIR):
-        vcs.up()
-    update_django_config(restart=False)
-    vcs.configure()
-
-@task
-def update_django_config(restart=True):
-    """ Updates :file:`config.py` on server (using :file:`config.server.py`) """
-    files.upload_template(
-        utils._project_path(env.conf.REMOTE_CONFIG_TEMPLATE),
-        utils._remote_project_path(env.conf.LOCAL_CONFIG),
-        env.conf, True
-    )
-    if restart:
-        apache.touch()
-
-@task
-def up(branch=None, before_restart=lambda: None):
-    """ Runs vcs ``up`` or ``checkout`` command on server and reloads
-    mod_wsgi process. """
-    utils.delete_pyc()
-    with cd('src/' + env.conf['INSTANCE_NAME']):
-        vcs.up(branch)
-    before_restart()
-    apache.touch()
-
-@task
-def setup_web_server():
-    """ Sets up a web server (apache + nginx). """
-    apache.install()
-    nginx.install()
-
-    apache.setup()
-    nginx.setup()
-
-@task
-def push(*args, **kwargs):
-    ''' Run it instead of your VCS push command.
-
-    The following strings are allowed as positional arguments:
-
-    * 'notest' - don't run tests
-    * 'syncdb' - run syncdb before code reloading
-    * 'migrate' - run migrate before code reloading
-    * 'pip_update' - run virtualenv.update_r before code reloading
-    * 'norestart' - do not reload source code
-
-    Keyword arguments:
-
-    * before_restart - callable to be executed after code uploading
-      but before the web server reloads the code.
-
-    Customization example can be found  :ref:`here <fab-push-customization>`.
-
-    '''
-    allowed_args = set(['notest', 'syncdb', 'migrate', 'pip_update', 'norestart'])
-    for arg in args:
-        if arg not in allowed_args:
-            puts('Invalid argument: %s' % arg)
-            puts('Valid arguments are: %s' % allowed_args)
-            return
-
-    vcs.push()
-    utils.delete_pyc()
-    with cd('src/' + env.conf['INSTANCE_NAME']):
-        vcs.up()
-
-    if 'pip_update' in args:
-        pip.update_r(restart=False)
-    if 'syncdb' in args:
-        dj_cmd.syncdb()
-    if 'migrate' in args:
-        dj_cmd.migrate()
-
-    # execute 'before_restart' callback
-    kwargs.get('before_restart', lambda: None)()
-
-    if 'norestart' not in args:
-        apache.touch()
-    if 'notest' not in args:
-        dj_cmd.test()
-
-@task
-def remove(confirm=True):
-    """ Shuts site down. This command doesn't clean everything, e.g.
-    user data (database, backups) is preserved. """
-
-    if confirm:
-        message = "Do you wish to undeploy host %s?" % env.hosts[0]
-        if not console.confirm(message, default=False):
-            abort("Aborting.")
-
-    @utils.run_as_sudo
-    def wipe_web():
-        sudo('rm -f /etc/nginx/sites-enabled/' + env.conf['INSTANCE_NAME'])
-        sudo('a2dissite ' + env.conf['INSTANCE_NAME'])
-        sudo('invoke-rc.d nginx reload')
-        sudo('invoke-rc.d apache2 reload')
-
-    wipe_web()
-    run('rm -rf %s' % env.conf.SRC_DIR)
-    for folder in ['bin', 'include', 'lib', 'src']:
-        run('rm -rf %s' % env.conf.ENV_DIR + '/' + folder)

fab_deploy/django.py

+# coding: utf-8
+from __future__ import with_statement
+
+from fabric.api import run, settings, env, hide, warn
+from fabric.contrib import files
+from taskset import task_method
+
+from fab_deploy.apps import WebApp
+from fab_deploy import utils
+from fab_deploy import db
+
+__all__ = ['Django']
+
+class Django(WebApp):
+
+    def __init__(self, frontend, backend, local_config='config.py', remote_config='config.server.py'):
+        super(Django, self).__init__(frontend, backend)
+        self.local_config = local_config
+        self.remote_config = remote_config
+
+    @task_method
+    def update_config(self, restart=True):
+        """ Updates :file:`config.py` on server (using :file:`config.server.py`) """
+        files.upload_template(
+            utils._project_path(self.remote_config),
+            utils._remote_project_path(self.local_config),
+            env.conf, True
+        )
+        if restart:
+            self.backend.touch()
+
+    @task_method
+    @utils.inside_project
+    def command_is_available(self, command):
+        with settings(hide('warnings', 'running', 'stdout', 'stderr'), warn_only=True):
+            output = run('python manage.py help ' + command)
+    
+        if output.succeeded:
+            return True
+    
+        # that's ugly
+        unknown_command_msg = "Unknown command: '%s'" % command
+        if unknown_command_msg in output:
+            return False
+    
+        # re-raise the original exception
+        run('python manage.py help ' + command)
+    
+    @task_method
+    @utils.inside_project
+    def manage(self, command):
+        """
+        Runs django management command. Example::
+    
+            fab manage:createsuperuser
+    
+        """
+        command_name = command.split()[0]
+        if not self.command_is_available(command_name):
+            warn('Management command "%s" is not available' % command_name)
+        else:
+            run('python manage.py ' + command)
+    
+    @task_method
+    def migrate(self, params='', do_backup=True):
+        """ Runs migrate management command. Database backup is performed
+        before migrations until ``do_backup=False`` is passed. """
+        if do_backup:
+            database = db.get_backend()
+            backup_dir = env.conf['ENV_DIR'] + '/var/backups/before-migrate'
+            run('mkdir -p ' + backup_dir)
+            database.dump(backup_dir)
+        self.manage('migrate --noinput %s' % params)
+    
+    @task_method
+    def syncdb(self, params=''):
+        """ Runs syncdb management command. """
+        self.manage('syncdb --noinput %s' % params)
+    
+    @task_method
+    def compress(self, params=''):
+        with settings(warn_only=True):
+            self.manage('synccompress %s' % params)
+    
+    @task_method
+    def collectstatic(self, params=''):
+        self.manage('collectstatic --noinput %s' % params)
+    
+    @task_method
+    @utils.inside_project
+    def test(self, what=''):
+        """
+        Runs 'runtests.sh' script from project root.
+        Example runtests.sh content::
+    
+            #!/bin/sh
+    
+            default_tests='accounts forum firms blog'
+            if [ $# -eq 0 ]
+            then
+                ./manage.py test $default_tests --settings=test_settings
+            else
+                ./manage.py test $* --settings=test_settings
+            fi
+        """
+        with settings(warn_only=True):
+            run('./runtests.sh ' + what)
+
+#def coverage():
+#    pass
+#    with cd(env.conf['SRC_DIR']):
+#        run('source %s/bin/activate; ./bin/runcoverage.sh' % env.conf['ENV_DIR'])
+

fab_deploy/django_commands.py

-#coding: utf-8
-from __future__ import with_statement
-from fabric.api import *
-
-from fab_deploy import utils
-from fab_deploy import db
-
-__all__ = ['migrate', 'manage', 'syncdb', 'compress', 'collectstatic', 'test',
-           'command_is_available']
-
-@task
-@utils.inside_project
-def command_is_available(command):
-    with settings(hide('warnings', 'running', 'stdout', 'stderr'), warn_only=True):
-        output = run('python manage.py help ' + command)
-
-    if output.succeeded:
-        return True
-
-    # that's ugly
-    unknown_command_msg = "Unknown command: '%s'" % command
-    if unknown_command_msg in output:
-        return False
-
-    # re-raise the original exception
-    run('python manage.py help ' + command)
-
-@task
-@utils.inside_project
-def manage(command):
-    """
-    Runs django management command. Example::
-
-        fab manage:createsuperuser
-
-    """
-    command_name = command.split()[0]
-    if not command_is_available(command_name):
-        warn('Management command "%s" is not available' % command_name)
-    else:
-        run('python manage.py ' + command)
-
-@task
-def migrate(params='', do_backup=True):
-    """ Runs migrate management command. Database backup is performed
-    before migrations until ``do_backup=False`` is passed. """
-    if do_backup:
-        database = db.get_backend()
-        backup_dir = env.conf['ENV_DIR'] + '/var/backups/before-migrate'
-        run('mkdir -p ' + backup_dir)
-        database.dump(backup_dir)
-    manage('migrate --noinput %s' % params)
-
-@task
-def syncdb(params=''):
-    """ Runs syncdb management command. """
-    manage('syncdb --noinput %s' % params)
-
-@task
-def compress(params=''):
-    with settings(warn_only=True):
-        manage('synccompress %s' % params)
-
-@task
-def collectstatic(params=''):
-    manage('collectstatic --noinput %s' % params)
-
-@task
-@utils.inside_project
-def test(what=''):
-    """
-    Runs 'runtests.sh' script from project root.
-    Example runtests.sh content::
-
-        #!/bin/sh
-
-        default_tests='accounts forum firms blog'
-        if [ $# -eq 0 ]
-        then
-            ./manage.py test $default_tests --settings=test_settings
-        else
-            ./manage.py test $* --settings=test_settings
-        fi
-    """
-    with settings(warn_only=True):
-        run('./runtests.sh ' + what)
-
-#def coverage():
-#    pass
-#    with cd(env.conf['SRC_DIR']):
-#        run('source %s/bin/activate; ./bin/runcoverage.sh' % env.conf['ENV_DIR'])
-

fab_deploy/pip.py

+# coding: utf-8
 from __future__ import with_statement
+
 from fabric.api import run, env, cd, task
+
 from fab_deploy import utils
-
 # FIXME: this shouldn't be apache-centered
 from fab_deploy.webserver import apache
 
 
 @task
 @utils.inside_src
-def install_r(what=None, options='', restart=True):
+def install_r(what=None, options=''):
     """ Installs pip requirements listed in ``<PIP_REQUIREMENTS_PATH>/<file>.txt`` file. """
     what = utils._pip_req_path(what or env.conf.PIP_REQUIREMENTS_ACTIVE)
     run('pip install %s -r %s' % (options, what))
-    if restart:
-        apache.touch()
 
 @task
 @utils.inside_src
-def update_r(what=None, options='', restart=True):
+def update_r(what=None, options=''):
     """ Updates pip requirements listed in ``<PIP_REQUIREMENTS_PATH>/<file>.txt`` file. """
     what = utils._pip_req_path(what or env.conf.PIP_REQUIREMENTS_ACTIVE)
     run('pip install %s -U -r %s' % (options, what))
-    if restart:
-        apache.touch()
 
 @task
 def setup_conf(username=None):

fab_deploy/project.py

+# coding: utf-8
+from __future__ import with_statement
+from functools import wraps
+import types
+
+from fabric.api import abort, settings, cd, run, env, puts, sudo
+from fabric.contrib import console
+from taskset import TaskSet, task_method
+
+from fab_deploy import utils, pip, system, vcs, db
+from fab_deploy.webserver.apache import Apache
+from fab_deploy.webserver.nginx import Nginx
+from fab_deploy.django import Django
+
+__all__ = ['WebProject']
+
+class WebProject(TaskSet):
+
+    def __init__(self, apps=None):
+        if apps is None:
+            apps = dict(django=Django(Nginx(), Apache(wsgi='django_wsgi.py')))
+        self.apps = apps
+
+    def _expose_to(self, module_obj):
+        task_list = super(WebProject, self)._expose_to(module_obj)
+        apps_module = types.ModuleType('apps')
+        for app_name, app in self.apps.iteritems():
+            module = app.expose_as_module(app_name)
+            setattr(apps_module, app_name, module)
+        module_obj.apps = apps_module
+        task_list.append('apps')
+        return task_list
+
+    @task_method
+    def full_deploy(self):
+        """ Prepares server and deploys the project. """
+        os = utils.detect_os()
+        if not console.confirm("Is the OS detected correctly (%s)?" % os, default=False):
+            abort("Detection fails. Please set env.conf.OS to correct value.")
+        system.prepare()
+        self.install_web_servers()
+        self.install_databases()
+        self.deploy()
+
+    @task_method
+    def install_web_servers(self):
+        """ Installs servers for all of the project apps. """
+        for app in self.apps.itervalues():
+            app.install_web_servers()
+
+    @task_method
+    def install_databases(self):
+        """ Installs project's databases. """
+        # stays trivial while database is single
+        database = db.get_backend()
+        database.install()
+
+    @task_method
+    def install_r(self, what=None, options='', restart=True):
+        """
+        Installs pip requirements listed in ``<PIP_REQUIREMENTS_PATH>/<file>.txt`` file
+        and reloads all apps of the project, if specified.
+        """
+        pip.install_r(what, options)
+        if restart:
+            for app in self.apps.itervalues():
+                app.restart()
+
+    @task_method
+    def update_r(self, what=None, options='', restart=True):
+        """
+        Updates pip requirements listed in ``<PIP_REQUIREMENTS_PATH>/<file>.txt`` file
+        and reloads all apps of the project, if specified.
+        """
+        pip.update_r(what, options)
+        if restart:
+            for app in self.apps.itervalues():
+                app.restart()
+
+    @task_method
+    def deploy(self):
+        pip.virtualenv_create()
+        self._make_clone()
+        self.install_r(env.conf.PIP_REQUIREMENTS, restart=False)
+        for app in self.apps.itervalues():
+            app.deploy()
+
+    @task_method
+    def _make_clone(self):
+        """ Creates repository clone on remote server. """
+        run('mkdir -p ' + env.conf.SRC_DIR)
+        with cd(env.conf.SRC_DIR):
+            with settings(warn_only=True):
+                vcs.init()
+        vcs.push()
+        with cd(env.conf.SRC_DIR):
+            vcs.up()
+        #self.update_config(restart=False)
+        vcs.configure()
+
+    @task_method
+    def remove(self, confirm=True):
+        """ Shuts site down. This command doesn't clean everything, e.g.
+        user data (database, backups) is preserved. """
+
+        if confirm:
+            message = "Do you wish to undeploy host %s?" % env.hosts[0]
+            if not console.confirm(message, default=False):
+                abort("Aborting.")
+        for app in self.apps.itervalues():
+            app.remove()
+        # remove project sources
+        run('rm -rf %s' % env.conf.SRC_DIR)
+        # remove parts of project's virtualenv
+        for folder in ['bin', 'include', 'lib', 'src']:
+            run('rm -rf %s' % env.conf.ENV_DIR + '/' + folder)
+
+    @task_method
+    def update_web_servers(self):
+        for app in self.apps.itervalues():
+            app.update_web_servers()
+
+    @task_method
+    def up(self, branch=None, before_restart=lambda: None):
+        """ Runs vcs ``up`` or ``checkout`` command on server and reloads
+        mod_wsgi process. """
+        utils.delete_pyc()
+        with cd('src/' + env.conf['INSTANCE_NAME']):
+            vcs.up(branch)
+        before_restart()
+        for app in self.apps.itervalues():
+            app.restart()
+    
+    @task_method
+    def push(self, *args, **kwargs):
+        ''' Run it instead of your VCS push command.
+    
+        The following strings are allowed as positional arguments:
+    
+        * 'notest' - don't run tests
+        * 'syncdb' - run syncdb before code reloading
+        * 'migrate' - run migrate before code reloading
+        * 'pip_update' - run virtualenv.update_r before code reloading
+        * 'norestart' - do not reload source code
+    
+        Keyword arguments:
+    
+        * before_restart - callable to be executed after code uploading
+          but before the web server reloads the code.
+    
+        Customization example can be found  :ref:`here <fab-push-customization>`.
+    
+        '''
+        allowed_args = set(['notest', 'syncdb', 'migrate', 'pip_update', 'norestart'])
+        for arg in args:
+            if arg not in allowed_args:
+                puts('Invalid argument: %s' % arg)
+                puts('Valid arguments are: %s' % allowed_args)
+                return
+    
+        vcs.push()
+        utils.delete_pyc()
+        with cd('src/' + env.conf['INSTANCE_NAME']):
+            vcs.up()
+    
+        if 'pip_update' in args:
+            self.update_r(restart=False)
+        for app in self.apps.itervalues():
+            if 'syncdb' in args:
+                app.syncdb()
+            if 'migrate' in args:
+                app.migrate()
+        # execute 'before_restart' callback
+        kwargs.get('before_restart', lambda: None)()
+        for app in self.apps.itervalues():
+            if 'norestart' not in args:
+                app.restart()
+            if 'notest' not in args:
+                app.test()

fab_deploy/system.py

-#coding: utf-8
+# coding: utf-8
 from __future__ import with_statement
 import os.path
+
 from fabric.api import run, settings, env, cd, sudo, task
 from fabric.contrib import files
 from fabric import utils as fabric_utils
+
 from fab_deploy import utils
 
-
 __all__ = ['create_linux_account', 'create_sudo_linux_account',
            'ssh_add_key', 'ssh_add_root_key', 'install_sudo']
 

fab_deploy/utils.py

+# coding: utf-8
 from __future__ import with_statement
 import os
 import posixpath
 from copy import deepcopy
 from re import match
 from functools import wraps
+
 from fabric.api import env, prefix, cd, abort, warn, puts, run, task, settings, sudo
-from fabric.contrib import files
+from fabric.contrib import files, console
 from fabric import state
 from fabric import network
 
-
 __all__ = ['run_as', 'update_env', 'inside_project', 'inside_virtualenv',
            'inside_src', 'delete_pyc', 'print_env', 'detect_os', 'define_host']
 
     return wrapper
 
 @task
-def upload_config_template(name, to=None, skip_unexistent=False, **kwargs):
+def upload_config_template(
+    name, to=None, skip_unexistent=False, extra_context=None, **kwargs
+):
     if to is None:
         base_dir = env.conf['ENV_DIR'] + "/etc/"
         run('mkdir -p ' + base_dir)
             return
         abort('Config template "%s" is not found' % name)
 
-    files.upload_template(config_template, to, env.conf, use_jinja=True,
+    context = deepcopy(env.conf)
+    if extra_context is not None:
+        context.update(extra_context)
+
+    files.upload_template(config_template, to, context, use_jinja=True,
                           **kwargs)
 
 def update_env():
         SERVER_ADMIN='example@example.com',
         PORTS = PortManager(env.conf['INSTANCE_NAME']),
         VCS='hg',
-
+        # couldn't be here because of utils recurrent reference
+        #APPS = {
+        #    'nginx': Nginx(),
+        #    'apache': Apache(),
+        #},
         PROJECT_PATH='',
-        LOCAL_CONFIG='config.py',
-        REMOTE_CONFIG_TEMPLATE='config.server.py',
         CONFIG_TEMPLATES_PATHS=['config_templates'],
 
         PIP_REQUIREMENTS_PATH='reqs',
         if env.conf.VCS == vcs:
             env.conf.VCS = 'fab_deploy.vcs.' + vcs
 
-
 def define_host(host_string, defaults=None):
     """
     This decorator populates :attr:`env.hosts`, :attr:`env.conf` and
             else:
                 env.conf = deepcopy(defaults)
             env.conf.update(func(*args, **kwargs))
-
             update_env()
         return inner
     return decorator

fab_deploy/vcs/__init__.py

+# coding: utf-8
 from __future__ import absolute_import
 import warnings
+
 from fabric.api import env, task
 from taskset import TaskSet
 

fab_deploy/vcs/git.py

+# coding: utf-8
 from fabric.api import env, run, local
-from taskset import TaskSet, task
+from taskset import TaskSet, task_method
 
 class Git(TaskSet):
     BRANCH_OPTION = 'GIT_BRANCH'
 
-    @task
+    @task_method
     def init(self):
         run('git init')
         run('git config receive.denyCurrentBranch ignore') # allow update current branch
 
-    @task
+    @task_method
     def up(self, branch):
         run('git checkout --force %s' % branch) # overwrite local changes
 
-    @task
+    @task_method
     def push(self, branch=None):
         user, host = env.hosts[0].split('@')
         local('git push --force ssh://%s/~%s/src/%s/ %s' % (env.hosts[0], user,
             env.conf.INSTANCE_NAME, branch or env.conf[self.BRANCH_OPTION]))
 
-    @task
+    @task_method
     def configure(self):
         pass
 

fab_deploy/vcs/hg.py

-from fabric.api import run, local, env
+# coding: utf-8
+from fabric.api import run, local, env, settings, abort
+from fabric.contrib.console import confirm
+from taskset import TaskSet, task_method
+
 from fab_deploy.utils import upload_config_template
-from taskset import TaskSet, task
 
 class Hg(TaskSet):
     BRANCH_OPTION = 'HG_BRANCH'
 
-    @task
+    @task_method
     def init(self):
         run('hg init')
 
-    @task
+    @task_method
     def up(self, branch):
         run('hg up -C ' + branch)
 
-    @task
+    @task_method
     def push(self, branch=None):
-        local('hg push ssh://%s/src/%s/' % (env.hosts[0], env.conf.INSTANCE_NAME))
+        with settings(warn_only=True):
+            res = local('hg push ssh://%s/src/%s/ --new-branch' % (env.hosts[0], env.conf.INSTANCE_NAME))
+            if res.failed:
+                if not confirm("Error occured during push. Continue anyway?", default=False):
+                    abort("Aborting.")
 
-    @task
+    @task_method
     def configure(self):
         upload_config_template('hgrc', env.conf.SRC_DIR + '/.hg/hgrc',
                                skip_unexistent=True)

fab_deploy/vcs/none.py

+# coding: utf-8
 from __future__ import with_statement
 import os.path
 import tempfile
 from datetime import datetime
-from fabric.api import run, local, env, put, cd
-from taskset import TaskSet, task
+import codecs
+import sys
+
+from fabric.api import run, local, env, put, cd, warn
+from taskset import TaskSet, task_method
+
 from fab_deploy import utils
 
 class PseudoVcs(TaskSet):
     BRANCH_OPTION = None
 
+    def _extract_from_hgignore(self):
+        """ Extracts list of exclusion patterns from .hgignore file. """
+        excluded = []
+        try:
+            for str in codecs.open('.hgignore', 'rb', sys.getdefaultencoding()):
+                str = str.strip()
+                if str.startswith(('#', 'syntax: glob')) or str=='':
+                    continue
+                elif str=='syntax: regexp':
+                    raise NotImplementedError('Regexp syntax is not supported.')
+                else:
+                    if str.startswith('*'):
+                        excluded.append(str)
+                    else:
+                        excluded.append(utils._project_path(str))
+        except IOError as e:
+           warn('.hgignore file was not found. Nothing is excluded.')
+        return excluded
+
     def _exclude_string(self):
-        excludes = [utils._project_path(env.conf.LOCAL_CONFIG), '*.pyc', '*.pyo']
+        excludes = self._extract_from_hgignore()
         exclude_string = " ".join(['--exclude="%s"' % pattern for pattern in excludes])
         if os.path.exists('.excludes'):
             exclude_string =  "-X .excludes " + exclude_string
         return exclude_string
 
-    @task
+    @task_method
     def push(self, branch=None):
         """
         Upload the current project to a remote system, tar/gzipping during the move.
             run("tar -xzf " + tgz_name)
             run("rm -f " + tgz_name)
 
-    @task
+    @task_method
     def configure(self):
         pass
 
-    @task
+    @task_method
     def init(self):
         pass
 
-    @task
+    @task_method
     def up(self, branch):
         pass
 

fab_deploy/webserver/apache.py

+# coding: utf-8
 from __future__ import with_statement, absolute_import
 
-from fabric.api import env, run, settings, sudo, hide, task, abort
+from fabric.api import env, run, settings, sudo, hide, abort
 from fabric.contrib import files, console
 
+from taskset import task_method
 from fab_deploy import utils
 from fab_deploy import system
+from fab_deploy.webserver.wsgi_backend import WsgiBackend
 
-#__all__ = ['touch', 'restart']
+__all__ = ['Apache']
 
 APACHE_PORTS_FILE = '/etc/apache2/ports.conf'
-APACHE_FIRST_PORT = 50000 # see http://www.iana.org/assignments/port-numbers
-APACHE_LAST_PORT = 60000
 TAKEOVER_STRING = '# This file is managed by django-fab-deploy. "Listen" directives are in /etc/apache2/sites-available/*'
 OLD_TAKEOVER_STRING = '# This file is managed by django-fab-deploy. Please do not edit it manually.'
 
-@task
-def touch(wsgi_file=None):
-    """ Reloads source code by touching the wsgi file. """
-    if wsgi_file is None:
-        wsgi_file = env.conf['ENV_DIR'] + '/var/wsgi/' + env.conf['INSTANCE_NAME'] + '.py'
-    run('touch ' + wsgi_file)
+class Apache(WsgiBackend):
 
-@task
-def make_wsgi():
-    """ Uploads wsgi deployment script. """
-    wsgi_dir = env.conf['ENV_DIR'] + '/var/wsgi/'
-    run('mkdir -p ' + wsgi_dir)
-    file_name = env.conf['INSTANCE_NAME'] + '.py'
-    utils.upload_config_template('django_wsgi.py', wsgi_dir + file_name)
+    def __init__(self, config='apache.config', wsgi='wsgi.py'):
+        super(Apache, self).__init__(wsgi)
+        self.config = config
 
-@task
-@utils.run_as_sudo
-def is_running():
-    """
-    Returns if apache is running
-    """
-    with settings(hide('running', 'stdout', 'stderr', 'warnings'), warn_only=True):
-        output = sudo('invoke-rc.d apache2 status')
-    return output.succeeded
+    def _get_server_config_name(self):
+        return '%s_%s' % (env.conf['INSTANCE_NAME'], self.config)
 
+    @task_method
+    @utils.run_as_sudo
+    def upload_config(self):
+        """ Updates apache config. """
+        name = self._get_server_config_name()
+        utils.upload_config_template(self.config,
+                                     '/etc/apache2/sites-available/%s' % name,
+                                     extra_context={'CURRENT_BACKEND': self},
+                                     use_sudo=True)
+        sudo('a2ensite %s' % name)
 
-@task
-@utils.run_as_sudo
-def restart():
-    """ Restarts apache using init.d script. """
-    # restart is not used because it can leak memory in some cases
-    # without pty=False restart silently fails on Ubuntu 10.04.
-    sudo('invoke-rc.d apache2 stop', pty=False)
-    sudo('invoke-rc.d apache2 start', pty=False)
+    @task_method
+    @utils.run_as_sudo
+    def remove_config(self):
+        """ Removes apache config and reloads apache. """
+        name = self._get_server_config_name()
+        sudo('a2dissite %s' % name)
+        sudo('rm -f /etc/apache2/sites-available/'+name)
+        sudo('invoke-rc.d apache2 reload')
 
-# ==== installation ===
+    @task_method
+    @utils.run_as_sudo
+    def restart(self):
+        """ Restarts apache using init.d script. """
+        # restart is not used because it can leak memory in some cases
+        # without pty=False restart silently fails on Ubuntu 10.04.
+        sudo('invoke-rc.d apache2 stop', pty=False)
+        sudo('invoke-rc.d apache2 start', pty=False)
 
-@task
-@utils.run_as_sudo
-def install(confirm=True):
-    """ Installs apache. """
-    system.aptitude_install('apache2 libapache2-mod-wsgi libapache2-mod-rpaf locales-all')
-    setup_locale()
+    @task_method
+    def update_config(self):
+        """ Updates apache config, wsgi script and restarts apache. """
+        self.upload_config()
+        self.upload_wsgi()
+        self.restart()
 
-    default_sites = [
-        '/etc/apache2/sites-enabled/default',
-        '/etc/apache2/sites-enabled/000-default',
-    ]
+    @task_method
+    @utils.run_as_sudo
+    def is_running(self):
+        """
+        Returns whether apache is running
+        """
+        with settings(hide('running', 'stdout', 'stderr', 'warnings'), warn_only=True):
+            output = sudo('invoke-rc.d apache2 status')
+        return output.succeeded
 
-    for site in default_sites:
-        if files.exists(site, use_sudo=True):
-            msg = "Remote %s will be deleted.\n" \
+    # ==== installation ===
+
+    @task_method
+    @utils.run_as_sudo
+    def install(self, confirm=True):
+        """ Installs apache. """
+        system.aptitude_install('apache2 libapache2-mod-wsgi libapache2-mod-rpaf locales-all')
+        self.setup_locale()
+
+        default_sites = [
+            '/etc/apache2/sites-enabled/default',
+            '/etc/apache2/sites-enabled/000-default',
+        ]
+
+        for site in default_sites:
+            if files.exists(site, use_sudo=True):
+                msg = "Remote %s will be deleted.\n" \
+                      "This is necessary for django-fab-deploy to work.\n" \
+                      "Choose 'n' and do a backup if you have customized this file.\n"\
+                      "Do you wish to continue?"  % site
+                if not confirm or console.confirm(msg):
+                    sudo('rm -f %s' % site)
+                else:
+                    abort("Aborting.")
+
+
+        if _ports_conf_needs_disabling():
+            msg = "The contents of remote %s will be erased.\n"\
                   "This is necessary for django-fab-deploy to work.\n" \
-                  "Choose 'n' and do a backup if you have customized this file.\n"\
-                  "Do you wish to continue?"  % site
+                  "Choose 'n' and do a backup if you have customized this file.\n" \
+                  "Do you wish to continue?" % APACHE_PORTS_FILE
             if not confirm or console.confirm(msg):
-                sudo('rm -f %s' % site)
+                _disable_ports_conf()
             else:
                 abort("Aborting.")
 
-
-    if _ports_conf_needs_disabling():
-        msg = "The contents of remote %s will be erased.\n"\
-              "This is necessary for django-fab-deploy to work.\n" \
-              "Choose 'n' and do a backup if you have customized this file.\n" \
-              "Do you wish to continue?" % APACHE_PORTS_FILE
-        if not confirm or console.confirm(msg):
-            _disable_ports_conf()
-        else:
-            abort("Aborting.")
-
-
-@task
-@utils.run_as_sudo
-def make_config():
-    """ Updates apache config. """
-    name = env.conf['INSTANCE_NAME']
-    utils.upload_config_template('apache.config',
-                                 '/etc/apache2/sites-available/%s' % name,
-                                 use_sudo=True)
-    sudo('a2ensite %s' % name)
-
-@task
-def setup():
-    """ Updates apache config, wsgi script and restarts apache. """
-    make_config()
-    make_wsgi()
-    restart()
-
-@task
-@utils.run_as_sudo
-def setup_locale():
-    """ Setups apache locale. Apache is unable to handle file uploads with
-    unicode file names without this. """
-    files.append('/etc/apache2/envvars',
-                 ['export LANG="en_US.UTF-8"', 'export LC_ALL="en_US.UTF-8"'],
-                 use_sudo=True)
+    @task_method
+    @utils.run_as_sudo
+    def setup_locale(self):
+        """ Setups apache locale. Apache is unable to handle file uploads with
+        unicode file names without this. """
+        files.append('/etc/apache2/envvars',
+                     ['export LANG="en_US.UTF-8"', 'export LC_ALL="en_US.UTF-8"'],
+                     use_sudo=True)
 
 # === automatic apache ports management ===
 

fab_deploy/webserver/nginx.py

+# coding: utf-8
 from __future__ import with_statement
-from fabric.api import  env, settings, sudo, task
+
+from fabric.api import  env, settings, sudo
+from taskset import TaskSet, task_method
 
 from fab_deploy import utils
 from fab_deploy import system
 
-__all__ = ['install', 'setup']
+__all__ = ['Nginx']
 
-@task
-@utils.run_as_sudo
-def install():
-    """ Installs nginx. """
-    os = utils.detect_os()
-    options = {'lenny': '-t lenny-backports'}
-    system.aptitude_install('nginx', options.get(os, ''))
-    sudo('rm -f /etc/nginx/sites-enabled/default')
+class Nginx(TaskSet):
 
-@task
-@utils.run_as_sudo
-def setup():
-    """ Updates nginx config and restarts nginx. """
-    name = env.conf['INSTANCE_NAME']
-    utils.upload_config_template('nginx.config',
-                                 '/etc/nginx/sites-available/%s' % name,
-                                 use_sudo=True)
-    with settings(warn_only=True):
-        sudo('ln -s /etc/nginx/sites-available/%s /etc/nginx/sites-enabled/%s' % (name, name))
-    sudo('invoke-rc.d nginx restart')
+    def __init__(self, config='nginx.config'):
+        self.config = config
+
+    def _get_server_config_name(self):
+        return '%s_%s' % (env.conf['INSTANCE_NAME'], self.config)
+
+    @task_method
+    @utils.run_as_sudo
+    def install(self):
+        """ Installs nginx. """
+        os = utils.detect_os()
+        options = {'lenny': '-t lenny-backports'}
+        system.aptitude_install('nginx', options.get(os, ''))
+        sudo('rm -f /etc/nginx/sites-enabled/default')
+
+    @task_method
+    @utils.run_as_sudo
+    def update_config(self):
+        """ Updates nginx config and restarts nginx. """
+        name = self._get_server_config_name()
+        utils.upload_config_template(self.config,
+                                     '/etc/nginx/sites-available/%s' % name,
+                                     use_sudo=True)
+        with settings(warn_only=True):
+            sudo('ln -s /etc/nginx/sites-available/%s /etc/nginx/sites-enabled/%s' % (name, name))
+        sudo('invoke-rc.d nginx restart')
+
+    @task_method
+    @utils.run_as_sudo
+    def remove_config(self):
+        """ Removes nginx config and reloads nginx. """
+        name = self._get_server_config_name()
+        sudo('rm -f /etc/nginx/sites-enabled/'+name)
+        sudo('rm -f /etc/nginx/sites-available/'+name)
+        sudo('invoke-rc.d nginx reload')

fab_deploy/webserver/wsgi_backend.py

+# coding: utf-8
+from __future__ import with_statement, absolute_import
+import posixpath
+
+from fabric.api import env, run
+
+from taskset import TaskSet, task_method
+from fab_deploy import utils
+
+class WsgiBackend(TaskSet):
+    """ Base class for backends using wsgi. """
+
+    def __init__(self, wsgi):
+        self.wsgi = wsgi
+
+    def get_wsgi_dir(self):
+        return posixpath.join(
+            env.conf['ENV_DIR'], 'var', 'wsgi', env.conf['INSTANCE_NAME']
+        )
+
+    def get_wsgi_file_name(self):
+        """ Returns remote filename of the wsgi file. """
+        return self.wsgi
+
+    def get_wsgi_full_file_name(self):
+        """ Returns full remote filename of the wsgi file (with path). """
+        return posixpath.join(self.get_wsgi_dir(), self.get_wsgi_file_name())
+
+    @task_method
+    def touch(self, wsgi_file=None):
+        """
+        Reloads source code by touching the wsgi file.
+
+        If backend doesn't have this feature then this method must be
+        overriden to provide same effect in other way (restart, reload, etc).
+        """
+        if wsgi_file is None:
+            wsgi_file = self.get_wsgi_full_file_name()
+        run('touch ' + wsgi_file)
+
+    @task_method
+    def upload_wsgi(self):
+        """ Uploads wsgi deployment script. """
+        wsgi_dir = self.get_wsgi_dir()
+        run('mkdir -p ' + wsgi_dir)
+        utils.upload_config_template(
+            self.wsgi,
+            posixpath.join(wsgi_dir, self.get_wsgi_file_name())
+        )

fab_deploy_tests/preparevm.py

 #!/usr/bin/env python
 import sys
+import os
+
+# always use fab_deploy from the checkout, not the installed version
+# plus make fab_deploy_tests available for imports
+path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
+sys.path.insert(0, path)
+
 from fabric.api import env, sudo, run
 from fabtest import fab, VirtualBox
 
 from fab_deploy.utils import update_env
 from fab_deploy.system import prepare
 from fab_deploy.db import mysql, postgres
-from fab_deploy.webserver import nginx, apache
+from fab_deploy.webserver.nginx import Nginx
+from fab_deploy.webserver.apache import Apache
 
 from utils import setup_ssh, setup_sudo, private_key_path
 
         setup_sudo()
         setup_ssh()
         fab(prepare)
-        fab(apache.install, confirm=False)
-        fab(nginx.install)
+        fab(Apache().install, confirm=False)
+        fab(Nginx().install)
         fab(mysql.install)
         fab(postgres.install)
         fab(_download_pip_requirements)

fab_deploy_tests/test_project/.hgignore

+syntax: glob
+# next line contains tabs'n'spaces **intentionally**
+  	  
+# comment check
+
+# what previously was hardcoded
+config.py
+*.pyc
+*.pyo

fab_deploy_tests/test_project/config_templates/apache.config

     WSGIDaemonProcess {{ INSTANCE_NAME }} user={{ USER }} group={{ USER }} processes={{ PROCESSES }} threads={{ THREADS }}
     WSGIProcessGroup {{ INSTANCE_NAME }}
 
-    WSGIScriptAlias / {{ ENV_DIR }}/var/wsgi/{{ INSTANCE_NAME }}.py
-    <Directory {{ ENV_DIR }}/var/wsgi/>
+    WSGIScriptAlias / {{ CURRENT_BACKEND.get_wsgi_full_file_name() }}
+    <Directory {{ CURRENT_BACKEND.get_wsgi_dir() }}/>
         Order deny,allow
         allow from all
     </Directory>

fab_deploy_tests/test_project/fabfile.py

 from fabric.api import env, task
+
 from fab_deploy.utils import define_host, update_env
+from fab_deploy.project import WebProject
+
+project = WebProject().expose_as_module('project')
 
 @define_host('foo@127.0.0.1:2222')
 def foo_site():
         VCS = 'none',
         SERVER_NAME = 'invalid.example.com',
         INSTANCE_NAME = 'invalid',
-        EXTRA = 'raise Exception()'
+        EXTRA = 'raise Exception()',
     )

fab_deploy_tests/test_project2/fabfile.py

 from fab_deploy.utils import define_host
+from fab_deploy.webserver.apache import Apache
+from fab_deploy.webserver.nginx import Nginx
+from fab_deploy.django import Django
+from fab_deploy.project import WebProject
 
 LAYOUT_OPTIONS = dict(
     PROJECT_PATH = 'src',
-    LOCAL_CONFIG = 'local_settings.py',
-    REMOTE_CONFIG_TEMPLATE = 'staging_settings.py',
     PIP_REQUIREMENTS_PATH = '',
     PIP_REQUIREMENTS = 'requirements.txt',
     PIP_REQUIREMENTS_ACTIVE = 'requirements.txt',
     CONFIG_TEMPLATES_PATHS = ['hosting/staging', 'hosting'],
 )
 
+apps = dict(
+    django=Django(
+        Nginx(), 
+        Apache(wsgi='django_wsgi.py'),
+        local_config='local_settings.py',
+        remote_config='staging_settings.py'
+    )
+)
+project = WebProject(apps).expose_as_module('project')
+
 @define_host('foo2@127.0.0.1:2222', LAYOUT_OPTIONS)
 def foo_site():
     return dict(

fab_deploy_tests/test_project2/hosting/apache.config

     WSGIDaemonProcess {{ INSTANCE_NAME }} user={{ USER }} group={{ USER }} processes={{ PROCESSES }} threads={{ THREADS }}
     WSGIProcessGroup {{ INSTANCE_NAME }}
 
-    WSGIScriptAlias / {{ ENV_DIR }}/var/wsgi/{{ INSTANCE_NAME }}.py
-    <Directory {{ ENV_DIR }}/var/wsgi/>
+    WSGIScriptAlias / {{ CURRENT_BACKEND.get_wsgi_full_file_name() }}
+    <Directory {{ CURRENT_BACKEND.get_wsgi_dir() }}/>
         Order deny,allow
         allow from all
     </Directory>

fab_deploy_tests/test_project3/.hgignore

+syntax: glob
+# next line contains tabs'n'spaces **intentionally**
+  	  
+# comment check
+
+# what previously was hardcoded
+config.py
+*.pyc
+*.pyo

fab_deploy_tests/test_project3/fabfile.py

 from fab_deploy.utils import define_host
+from fab_deploy.webserver.apache import Apache
+from fab_deploy.webserver.nginx import Nginx
+from fab_deploy.django import Django
+from fab_deploy.project import WebProject
 
 COMMON = dict(
     CONFIG_TEMPLATES_PATHS=['test_project3/config_templates'],
-    LOCAL_CONFIG = 'test_project3/config.py',
-    REMOTE_CONFIG_TEMPLATE = 'test_project3/config.server.py',
     PIP_REQUIREMENTS_PATH = 'test_project3/reqs/',
 
     DB_USER = 'baz',
     SERVER_NAME = 'baz.example.com'
 )
 
+apps = dict(
+    django=Django(
+        Nginx(config='nginx.config'), 
+        Apache(config='apache.config', wsgi='django_wsgi.py'),
+        local_config='test_project3/config.py',
+        remote_config='test_project3/config.server.py'
+    )
+)
+project = WebProject(apps=apps).expose_as_module('project')
 
 @define_host('baz@127.0.0.1:2222', COMMON)
 def postgres_site():

fab_deploy_tests/test_project3/test_project3/config_templates/apache.config

     WSGIDaemonProcess {{ INSTANCE_NAME }} user={{ USER }} group={{ USER }} processes={{ PROCESSES }} threads={{ THREADS }}
     WSGIProcessGroup {{ INSTANCE_NAME }}
 
-    WSGIScriptAlias / {{ ENV_DIR }}/var/wsgi/{{ INSTANCE_NAME }}.py
-    <Directory {{ ENV_DIR }}/var/wsgi/>
+    WSGIScriptAlias / {{ CURRENT_BACKEND.get_wsgi_full_file_name() }}
+    <Directory {{ CURRENT_BACKEND.get_wsgi_dir() }}/>
         Order deny,allow
         allow from all
     </Directory>

fab_deploy_tests/tests/base.py

+# coding: utf-8
 from __future__ import absolute_import
 from fabric.api import env, run
 from fabric.state import _AttributeDict
 from fabtest import FabTest
 from fab_deploy.utils import update_env
+from fab_deploy.project import WebProject
 from fab_deploy_tests.utils import get_package_state, private_key_path
 
 class FabDeployTest(FabTest):
     host = 'foo@127.0.0.1:2222'
     key_filename = [private_key_path()]
+    project = WebProject().expose_as_module('project')
 
     def setup_env(self):
         super(FabDeployTest, self).setup_env()
     def setup_conf(self):
         env.conf = _AttributeDict(
             DB_USER='root',
-            DB_BACKEND='dummy'
+            DB_BACKEND='dummy',
         )
 
     def assertPackageInstalled(self, name):

fab_deploy_tests/tests/deploy.py

 from fabric.api import hide, run, env, settings
 from fabtest import fab, vbox_urlopen, FabricAbortException
 from fab_deploy.utils import run_as
-from fab_deploy.deploy import deploy_project, push, remove
 from fab_deploy.db import mysql, postgres, postgis
-from fab_deploy.webserver import apache
-from fab_deploy.django_commands import command_is_available
+from fab_deploy.webserver.apache import Apache
 from .base import FabDeployTest
 from ..utils import setup_ssh, setup_sudo
-from ..test_project.fabfile import foo_site, bar_site, invalid_site
-from ..test_project2.fabfile import foo_site as foo_site2
-from ..test_project3.fabfile import postgis_site, postgres_site
+from ..test_project.fabfile import foo_site, bar_site, invalid_site, project
+from ..test_project2.fabfile import foo_site as foo_site2, project as project2
+from ..test_project3.fabfile import postgis_site, postgres_site, project as project3
 
 @contextlib.contextmanager
 def answer_yes():
 
 class FabDeployProjectTest(FabDeployTest):
     snapshot = 'fabtest-prepared-server'
-    project = 'test_project'
+    project_dir = 'test_project'
+    project = project
 
     def assertResponse(self, url, response, post_data = None):
         try:
             self.assertTrue(get_file_content(path))
 
     def assertCommandAvailable(self, command):
-        result = fab(command_is_available, command)
+        result = fab(self.project.apps.django.command_is_available, command)
         self.assertTrue(result)
 
     def assertCommandNotAvailable(self, command):
-        result = fab(command_is_available, command)
+        result = fab(self.project.apps.django.command_is_available, command)
         self.assertFalse(result)
 
     def assertCommandFails(self, command):
         self.assertRaises(FabricAbortException, fab,
-                          command_is_available, command)
+                          self.project.apps.django.command_is_available, command)
 
     def assertInstanceWorks(self, instance):
         url = 'http://%s.example.com/instance/' % instance
 
     def setup_conf(self):
         self.cwd = os.getcwd()
-        os.chdir(self.project)
+        os.chdir(self.project_dir)
         fab(foo_site)
 
     def tearDown(self):
         self.assertFalse(is_local_port_bound(port))
 
     def test_apache_config(self):
-        fab(apache.install, confirm=False)
+        apacheNoConfig = Apache()
+        fab(apacheNoConfig.install, confirm=False)
 
         # first site
         fab(foo_site)
-        fab(apache.make_config)
+        fab(self.project.apps.django.backend.upload_config)
 
         foo_port = env.conf.PORTS['apache']
         self.assertPortNotBound(foo_port)
         self.assertFileExists('/etc/apache2/sites-enabled/foo')