Commits

sodas tsai committed 605f431

0.1.50

Comments (0)

Files changed (30)

+include README.md
+include MANIFEST.in
+recursive-include beanstalk/static *

beanstalk/__init__.py

-__version__ = '0.1.0'
+import os
+
+# Pacakge information
+#-----------------------------------------------------------------------------------------------------------------------
+__version__ = '0.1.50'
 VERSION = tuple(map(lambda x: int(x), __version__.split('.')))
+BEANSTALK_ROOT_PATH = os.path.abspath(os.path.dirname(__file__))
+
+# Values
+#-----------------------------------------------------------------------------------------------------------------------
+BEANSTALK_GLOBAL_BASE_PATH = '/etc/beanstalk_stack'
+BEANSTALK_GLOBAL_SETTINGS_PATH = os.path.join(BEANSTALK_GLOBAL_BASE_PATH, 'beanstalk_settings.py')
+BEANSTALK_LOCAL_BASE_PATH = 'bsbean/'
+IDENTIFIER = 'com.wantoto.beanstalk'

beanstalk/bs_ssh_entry

+#!/usr/bin/env python
+
+import os
+import re
+#import shlex
+import sys
+
+valid_commands = [r'^rsync --server .*', r'^bsjack server\..+', '^bsjack info\..+']
+fabric_pattern = re.compile(r'^/bin/bash -l -c "(?P<command>.*)"$')
+
+
+def fabric_command(command):
+    match = fabric_pattern.match(command)
+    return match.groupdict()['command'].replace('\\', '') if match else command
+
+
+def valid_command(command):
+    for valid_command in valid_commands:
+        if re.compile(valid_command).match(command) is not None:
+            # rsync path check
+            #if command.startswith('rsync --server'):
+            #    target_path = shlex.split(command)[-1]
+            #    if not target_path.startswith('/var/beanstalk'):  # TODO: Set this path by settings
+            #        return False
+            return True
+    return False
+
+
+def main():
+    command = os.environ.get('SSH_ORIGINAL_COMMAND', None)
+    if command is None:
+        print 'You cannot use SSH as beanstalk directly.'
+        sys.exit(1)
+
+    command = fabric_command(command)
+    if not valid_command(command):
+        print 'Invalid command: {command}'.format(command=command)
+        sys.exit(2)
+
+    os.system(command)
+
+
+if __name__ == '__main__':
+    main()
+#!/usr/bin/env python
+
+import os
+import sys
+from fabric.api import local, hide, settings
+from fabric.colors import magenta, green
+import beanstalk
+
+command_name = os.path.split(os.path.splitext(__file__)[0])[-1]
+
+
+def main():
+    # get fab file
+    beanstalk_install_path = os.path.split(os.path.split(beanstalk.__file__)[0])[0]
+    beanstalk_package_path = os.path.join(beanstalk_install_path, 'beanstalk')
+    fab_file_path = os.path.join(beanstalk_package_path, 'tasks/__init__.py')
+
+    # Show usage
+    if len(sys.argv) < 2:
+        print magenta('Beanstalk-Stack man: {0}'.format(command_name), bold=True)
+        print 'Hello, I\'m Jack who plant the beanstalk.'
+        print 'Beanstalk-Stack version: {0}'.format(beanstalk.__version__)
+        print ''
+        print 'Usage: '
+        print ''
+        print '    {0} command1:argument1-1,argument1-2 [command2:argument2-1,argument2-2 ...]'.format(command_name)
+        print '    {0} --list'.format(command_name)
+        print ''
+        with hide('running'):
+            local('fab -f {0} --list'.format(fab_file_path))
+        print ''
+        print 'Beanstalk-Stack is built with ' + green('Python-Fabric') + '.'
+        print 'So you can pass all arguments for fabric to {0}.'.format(command_name)
+        print ''
+        sys.exit(1)
+
+    # Paths
+    current_dir = os.getcwd()
+    current_container = os.path.abspath(os.path.join(current_dir, '..'))
+    python_path = ':'.join([os.environ.get('PYTHONPATH', ''), current_dir, current_container])
+
+    # Go!
+    tasks = sys.argv[1:]
+    with settings(hide('running', 'warnings', 'status'), warn_only=True):
+        local('PYTHONPATH={python_path} fab --linewise -f {fab_file} -u beanstalk --hide=status {tasks}'.format(
+            python_path=python_path, fab_file=fab_file_path, tasks=' '.join(tasks)))
+
+
+if __name__ == '__main__':
+    main()

beanstalk/console/__init__.py

+

beanstalk/console/titles.py

+from fabric.api import *
+from fabric.colors import *
+
+
+def section_title(title):
+    return title if env.get('verbose_level', 0) == 0 else cyan(title)
+
+
+def tool_title(title):
+    return magenta(title, bold=True)
+
+
+def action_title(title):
+    title = '+ {title}'.format(title=title)
+    return title if env.get('verbose_level', 0) == 0 else blue(title)
+
+
+def separator(char='-', screen_width=80):
+    return char * int(screen_width / len(char))

beanstalk/decorators.py

+from functools import wraps
+from fabric.api import env
+
+
+def beanstalk_role(role):
+    def wrapper(fn):
+        @wraps(fn)
+        def wrapped(*args, **kwargs):
+            env.beanstalk_role = role
+            return fn(*args, **kwargs)
+        return wrapped
+    return wrapper

beanstalk/default_settings/__init__.py

Empty file added.

beanstalk/default_settings/app_settings.py

+import os
+import sys
+from six import callable
+from beanstalk.tasks.action import check_comments
+from beanstalk.tasks.action.django import collect_static, sync_db, south_migrate, switch_settings
+
+
+def _(var):
+    return var() if callable(var) else var
+
+
+# Local project information
+#-----------------------------------------------------------------------------------------------------------------------
+PROJECT_SOURCE_ROOT = lambda x: os.getcwd()  # PLANT
+PROJECT_NAME = lambda x: os.path.split(_(x['PROJECT_SOURCE_ROOT']))[-1]  # PLANT
+GIT_IGNORE_PATH = '.gitignore'  # PLANT
+PROJECT_TYPE = 'django'  # PLANT
+DJANGO_PROJECT_SETTINGS = lambda x: os.path.join(_(x['PROJECT_NAME']), 'settings.py')  # PLANT
+DJANGO_PROJECT_SETTINGS_DEV = lambda x: os.path.join(_(x['PROJECT_NAME']), 'dev_settings.py')  # PLANT
+DJANGO_PROJECT_SETTINGS_PROD = lambda x: os.path.join(_(x['PROJECT_NAME']), 'prod_settings.py')  # PLANT
+
+# Remote base information
+#-----------------------------------------------------------------------------------------------------------------------
+WEB_SERVERS = None  # required, PLANT
+REMOTE_ENVIRONMENTS = {}
+
+# Beanstalk tool behavior
+#-----------------------------------------------------------------------------------------------------------------------
+VERBOSE = 0
+USE_SSH_CONFIG = True
+CONFIRM_BEFORE_DEPLOY = True
+# Source code check
+CHECK_COMMENTS = True
+CHECK_TODO = False
+CHECK_NOTE = False
+CHECK_FIX_ME = True
+
+# Hooked actions
+#-----------------------------------------------------------------------------------------------------------------------
+default_source_check_actions = {
+    'django': [check_comments],
+}
+default_pre_deploy_actions = {
+    'django': [collect_static, switch_settings],
+}
+default_pre_install_actions = {}
+default_pre_send_actions = {}
+default_post_send_actions = {}
+default_post_install_actions = {
+    'django': [sync_db, south_migrate],
+}
+default_post_deploy_actions = {}
+
+# Before confirmation of deploy
+# Show issues or other warnings before doing someting to remote
+SOURCE_CHECK_ACTIONS = lambda x: default_source_check_actions.get(_(x['PROJECT_TYPE']), [])
+# Start to work.
+# Before connect to remote. Actions will be performed once.
+PRE_DEPLOY_ACTIONS = lambda x: default_pre_deploy_actions.get(_(x['PROJECT_TYPE']), [])
+# We connected to the remote
+# But we have done nothing yet.
+PRE_INSTALL_ACTIONS = lambda x: default_pre_install_actions.get(_(x['PROJECT_TYPE']), [])
+# We have remote app info now.
+PRE_SEND_ACTIONS = lambda x: default_pre_send_actions.get(_(x['PROJECT_TYPE']), [])
+# The source code has been just delivered.
+# Beanstalk modifies your code to fit its settings after this stage.
+# (For example, env information related to the rmote like uwsgi port.
+POST_SEND_ACTIONS = lambda x: default_post_send_actions.get(_(x['PROJECT_TYPE']), [])
+# The code has been cleared, modified, and normalized. You can perform db-migration here
+POST_INSTALL_ACTIONS = lambda x: default_post_install_actions.get(_(x['PROJECT_TYPE']), [])
+# Things are almost finished. You can do clean job here. They will be performed only once
+POST_DEPLOY_ACTIONS = lambda x: default_post_deploy_actions.get(_(x['PROJECT_TYPE']), [])

beanstalk/default_settings/server_settings.py

+from beanstalk.tasks.action.server import create_database
+
+
+def _(var):
+    return var() if callable(var) else var
+
+
+# Beanstalk tool behavior
+#-----------------------------------------------------------------------------------------------------------------------
+VERBOSE = 3
+
+# Beanstalk Installation
+#-----------------------------------------------------------------------------------------------------------------------
+BEANSTALK_STACK_BASE = '/var/beanstalk'
+STATIC_URL_PREFIX = 'static'
+BEANSTALK_EMAIL = 'beanstalk@beanstalk-stack.com'
+
+PRE_SETUP_ACTIONS = []  # Only beanstalk settings
+POST_SETUP_ACTIONS = []  # Only beanstalk settings
+
+PRE_CREATE_ACTIONS = []  # project_name
+POST_CREATE_ACTIONS = [
+    create_database,
+]  # project_name, uwsgi_port
+
+PRE_COMMIT_ACTIONS = []  # project_name
+POST_COMMIT_ACTIONS = []  # project_name
+
+PRE_RELOAD_ACTIONS = []  # project_name
+POST_RELOAD_ACTIONS = []  # project_name
+
+PRE_INSTALL_ACTIONS = []  # project_name
+POST_INSTALL_ACTIONS = []  # project_name
+
+PRE_COMMIT_ACTION = []  # project_name
+POST_COMMIT_ACTION = []  # project_name

beanstalk/paths/__init__.py

+

beanstalk/paths/server.py

+import os
+from fabric.state import env
+from fabric.utils import abort
+
+
+def project_base_path(project_name):
+    beanstalk_settings = env.get('beanstalk_settings', None)
+    if beanstalk_settings is None:
+        abort('Load beanstalk settings first')
+
+    beanstalk_stack_base = beanstalk_settings['BEANSTALK_STACK_BASE']
+    project_base = os.path.abspath(os.path.join(beanstalk_stack_base, 'apps/', project_name))
+
+    return project_base
+
+
+def project_source_path(project_name):
+    project_base = project_base_path(project_name)
+    return os.path.join(project_base, 'source/')
+
+
+def project_logs_path(project_name):
+    project_base = project_base_path(project_name)
+    return os.path.join(project_base, 'logs/')
+
+
+def project_venv_path(project_name):
+    project_base = project_base_path(project_name)
+    return os.path.join(project_base, 'venv/')
+
+
+def project_uwsgi_path(project_name):
+    project_base = project_base_path(project_name)
+    return os.path.join(project_base, 'uwsgi.ini')
+
+
+def apache_conf_path():
+    beanstalk_settings = env.get('beanstalk_settings', None)
+    if beanstalk_settings is None:
+        abort('Load beanstalk settings first')
+
+    beanstalk_stack_base = beanstalk_settings['BEANSTALK_STACK_BASE']
+    return os.path.join(beanstalk_stack_base, 'confs/', 'apache.conf')
+
+
+def app_ports_path():
+    beanstalk_settings = env.get('beanstalk_settings', None)
+    if beanstalk_settings is None:
+        abort('Load beanstalk settings first')
+
+    beanstalk_stack_base = beanstalk_settings['BEANSTALK_STACK_BASE']
+    return os.path.join(beanstalk_stack_base, 'confs/', 'ports.json')

beanstalk/static/beanstalk_apache.conf

+# Beanstalk apache configuration - glboal
+
+# Static files
+AliasMatch ^/{{ STATIC_URL_PREFIX }}/([^/]+)/(.*)$ {{ BEANSTALK_STACK_BASE }}/apps/$1/source/static/$2
+<DirectoryMatch {{ BEANSTALK_STACK_BASE }}/apps/([^/]+)/source/static>
+    Order allow,deny
+    Allow from all
+</DirectoryMatch>

beanstalk/static/beanstalk_init.sh

+#!/bin/sh
+
+### BEGIN INIT INFO
+# Provides:          beanstalk
+# Required-Start:    $local_fs $network $httpd
+# Required-Stop:     $local_fs $network $httpd
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: starts the beanstalk app server
+# Description:       starts beanstalk app server using start-stop-daemon
+### END INIT INFO
+
+. /etc/rc.d/init.d/functions
+
+PROG=beanstalk-stack
+DAEMON=/usr/local/bin/uwsgi
+CONFIG=/etc/beanstalk/uwsgi.ini
+RUN=/var/run/beanstalk
+PID=${RUN}/uwsgi.pid
+ARGS="--ini ${CONFIG} --log-syslog=uwsgi --pidfile=${PID}"
+
+function start() {
+    echo -n "Starting ${PROG}:"
+    if [[ -f ${PID} ]]; then
+        ${MOVE_TO_COL}
+        echo -e "[\033[31mFAILED\033[0m]"
+        echo "${PROG} has been already started."
+        exit 1
+    else
+        ${DAEMON} ${ARGS} 1>/dev/null 2>&1
+        OUT=$?
+        if [ ${OUT} -eq 0 ];then
+            ${MOVE_TO_COL}
+            echo -e "[\033[32m  OK  \033[0m]"
+        else
+            ${MOVE_TO_COL}
+            echo -e "[\033[31mFAILED\033[0m]"
+        fi
+    fi
+}
+
+function stop() {
+    echo -n "Stopping ${PROG}:"
+    if [[ -f ${PID} ]]; then
+        ${DAEMON} --stop ${PID}
+        rm -rf ${PID}
+        ${MOVE_TO_COL}
+        echo -e "[\033[32m  OK  \033[0m]"
+    else
+        ${MOVE_TO_COL}
+        echo -e "[\033[31mFAILED\033[0m]"
+        echo "${PROG} has been already stopped."
+        exit 1
+    fi
+}
+
+case "$1" in
+    start)
+        start
+        ;;
+    stop)
+        stop
+        ;;
+    reload)
+        echo "Reloading ${PROG} conf"
+        if [[ -f ${PID} ]]; then
+            ${DAEMON} --reload ${PID}
+        fi
+        ;;
+    restart)
+        echo "Restart ${PROG}"
+        stop
+        start
+        ;;
+    *)
+        echo "Usage: $0 {start|stop|reload|restart}"
+        exit 1
+    ;;
+esac
+
+exit 0

beanstalk/static/global_uwsgi.ini

+[uwsgi]
+emperor = {{ BEANSTALK_STACK_BASE }}/apps/*/source/uwsgi.ini
+uid = {{ BEANSTALK_UID }}
+master = true
+enable-threads = true
+daemonize = {{ BEANSTALK_STACK_BASE }}/global-logs/uwsgi.log

beanstalk/static/httpd_reload.sh

+#!/bin/sh
+service httpd reload

beanstalk/static/uwsgi_build.ini

+[uwsgi]
+main_plugin = python
+inherit = base
+plugin_dir = /usr/local/lib/uwsgi

beanstalk/tasks/__init__.py

+from beanstalk.tasks import app, server, users, info

beanstalk/tasks/action/__init__.py

+import os
+from fabric.api import *
+from fabric.colors import *
+from fabric.state import env
+from ground_soil.fabric import virtual_env
+from beanstalk.decorators import beanstalk_role
+from beanstalk.console.titles import action_title
+
+
+def action(command, work_directory=None, venv=None, chdir=None):
+    # Where to run?
+    remote = env.in_remote
+    exec_command = run if remote else local
+
+    if remote and chdir in ('base', 'source', 'logs'):
+        work_directory = env.remote_app_info['path'][chdir]
+        venv = env.remote_app_info['path']['venv']
+    elif not remote and chdir in ('source',):
+        work_directory = env.temp_directory
+        venv = os.environ.get('VIRTUAL_ENV', None)
+
+    # Settings of fabric
+    fab_settings = []
+    # cd/lcd
+    if work_directory is not None:
+        fab_settings.append(cd(work_directory) if remote else lcd(work_directory))
+    # venv
+    if venv is not None:
+        fab_settings.append(virtual_env(venv))
+
+    # core
+    def _action():
+        with settings(*fab_settings):
+            exec_command(command)
+    return _action
+
+
+@task
+@beanstalk_role('app')
+def check_comments(beanstalk_settings=None):
+    if beanstalk_settings is None:
+        beanstalk_settings = env.beanstalk_settings
+    if beanstalk_settings is None:
+        abort('Load beanstalk settings first')
+
+    project_source_root = beanstalk_settings['PROJECT_SOURCE_ROOT']
+
+    if beanstalk_settings['CHECK_COMMENTS']:
+        print action_title('Check comment in source code')
+        check_todo = beanstalk_settings['CHECK_TODO']
+        check_note = beanstalk_settings['CHECK_NOTE']
+        check_fix_me = beanstalk_settings['CHECK_FIX_ME']
+        comment_types = []
+        if check_todo:
+            comment_types.append('TODO')
+        if check_note:
+            comment_types.append('NOTE')
+        if check_fix_me:
+            comment_types.append('FIXME')
+        grep_commands = []
+        comment_styles = [
+            ('# %s:', ['py']),
+            ('<!-- %s:', ['html']),
+            ('// %s:', ['less', 'js']),
+            ('/\* %s:', ['css']),
+        ]
+        for comment_type in comment_types:
+            for comment_style, file_types in comment_styles:
+                for file_type in file_types:
+                    comment_string = comment_style % comment_type
+                    grep_commands.append(
+                        "grep '{pattern}' -n -r {folder} --include=*.{file_ext}".format(
+                            pattern=comment_string, folder=project_source_root, file_ext=file_type)
+                    )
+        comment_search_results = []
+        for grep_command in grep_commands:
+            local_results = []
+            with settings(hide('everything'), warn_only=True):
+                result = local(grep_command, capture=True)
+                if len(result):
+                    local_results.append(result)
+            for raw_result in local_results:
+                result_components = raw_result.split(':')
+                path = os.path.relpath(result_components[0], project_source_root)
+                line_number = result_components[1]
+                content = ':'.join(result_components[2:]).strip()
+                comment_search_results.append((path, line_number, content))
+        for path, line_number, content in comment_search_results:
+            print 'Line {line} at {filename}: {content}'.format(
+                line=yellow(line_number), filename=yellow(path, bold=True), content=content)
+        if len(comment_search_results) == 0 and beanstalk_settings['VERBOSE'] != 0:
+            print 'No comment mentioned about %s' % comment_types
+        print ''

beanstalk/tasks/action/django.py

+import os
+from fabric.api import *
+from beanstalk.decorators import beanstalk_role
+from beanstalk.tasks.action import action
+from beanstalk.console.titles import action_title
+
+
+@task
+@beanstalk_role('app')
+def collect_static():
+    """Collect static files in a django project (Django 1.3+)
+    """
+    print action_title('Collect static files in Django project')
+    verbose = env.beanstalk_settings['VERBOSE']
+    action('python manage.py collectstatic --verbosity={0:d} --noinput'.format(verbose), chdir='source')()
+
+
+@task
+@beanstalk_role('app')
+def sync_db():
+    """Sync db
+    """
+    print action_title('Sync database')
+    verbose = env.beanstalk_settings['VERBOSE']
+    action('python manage.py syncdb --noinput -v {0:d}'.format(verbose), chdir='source')()
+
+
+@task
+@beanstalk_role('app')
+def south_migrate():
+    """Do a South migration
+    """
+    print action_title('Migrate database')
+    verbose = env.beanstalk_settings['VERBOSE']
+    action('python manage.py migrate --noinput -v {0:d}'.format(verbose), chdir='source')()
+
+
+@task
+@beanstalk_role('app')
+def switch_settings():
+    """Switch django settings to production one
+    This implementation is designed for link-based settings file
+    """
+    print action_title('Switch to production settings')
+    beanstalk_settings = env.beanstalk_settings
+    source_root = env.remote_app_info['path']['source'] if env.in_remote else env.temp_directory
+    django_settings = os.path.abspath(os.path.join(source_root, beanstalk_settings['DJANGO_PROJECT_SETTINGS']))
+    prod_settings = os.path.abspath(os.path.join(source_root, beanstalk_settings['DJANGO_PROJECT_SETTINGS_PROD']))
+
+    exec_command = run if env.in_remote else local
+    exec_command('mv {settings} {settings}.bak'.format(settings=django_settings))
+    exec_command('ln -s {prod_settings} {settings}'.format(prod_settings=prod_settings, settings=django_settings))
+    exec_command('rm -rf {settings}.bak'.format(settings=django_settings))

beanstalk/tasks/action/server.py

+from fabric.api import *
+
+
+def create_database(db_name=None, db_user=None, db_password=None, permissions=None):
+    pass
+    # TODO: Create database
+#    if permissions is None:
+#        permissions = env.get(
+#            'db_permissions',
+#            ['SELECT', 'UPDATE', 'INSERT', 'DELETE', 'INDEX', 'CREATE', 'DROP', 'ALTER', 'REFERENCES'])
+#    if db_name is None:
+#        db_name = env.db_name
+#    if db_user is None:
+#        db_user = env.db_user
+#    if db_password is None:
+#        db_password = env.db_password
+#
+#    db_info = {
+#        'db_name': db_name,
+#        'db_user': db_user,
+#        'db_password': db_password,
+#        'db_perms': ', '.join(permissions),
+#    }
+#
+#    sql_commands = [
+#        'CREATE DATABASE IF NOT EXISTS {db_name}',
+#        'GRANT {db_perms} ON {db_name}.* TO {db_user}@\'localhost\' IDENTIFIED BY \'{db_password}\'',
+#        'FLUSH PRIVILEGES',
+#    ]
+#
+#    for sql_command in sql_commands:
+#        print sql_command.format(**db_info)

beanstalk/tasks/app.py

+import cStringIO as StringIO
+import json
+import os
+import datetime
+from fabric.api import *
+from fabric.colors import *
+from fabric.contrib.console import confirm
+from netaddr import IPNetwork, AddrFormatError
+from ground_soil.fabric import eval_kwargs
+from ground_soil.filesystem import temporary_directory, rsync
+from beanstalk import (IDENTIFIER as beanstalk_identifier, BEANSTALK_LOCAL_BASE_PATH,
+                       __version__ as beanstalk_version_string, VERSION as beanstalk_version_tuple)
+from beanstalk.tasks.utils import (set_verbose_level, load_role_settings as load_beanstalk_settings,
+                                   load_web_servers, run_hooked_actions)
+from beanstalk.decorators import beanstalk_role
+from beanstalk.console.titles import section_title, tool_title, separator
+from beanstalk.validator import validate_web_server, validate_file_existence, validate_project_type, supported_projects
+
+
+@task
+def web_server(*servers):
+    """Set targeted web server
+    :param servers: list of severs (IP, CIDR, or domain name)
+    """
+    hosts = []
+    for server in servers:
+        try:
+            hosts += map(lambda x: '{0}'.format(x), list(IPNetwork(server)))
+        except (ValueError, AddrFormatError):
+            hosts.append(server)
+
+    user_hosts = []
+    for host in hosts:
+        puts('Add "%s" to web server list' % host)
+        user_hosts.append(host)
+
+    if 'web_servers' not in env.roledefs:
+        env.roledefs['web_servers'] = list(set(user_hosts))
+    else:
+        env.roledefs.update({
+            'web_servers': list(set(env.roledefs['web_servers'] + user_hosts))
+        })
+
+
+@task
+@beanstalk_role('app')
+def plant(VERBOSE=0):
+    """plant beanstalk-stack for project in current work directory
+    """
+    set_verbose_level(VERBOSE)
+    # Create beanstalk_stack home
+    beanstalk_local_base_path = os.path.normpath(os.path.join(os.getcwd(), BEANSTALK_LOCAL_BASE_PATH))
+    app_settings_path = os.path.normpath(os.path.join(beanstalk_local_base_path, 'app_settings.py'))
+
+    if not os.path.exists(beanstalk_local_base_path):
+        print cyan('Make beanstalk_stack local base: ./{path}'.format(path=os.path.relpath(beanstalk_local_base_path)))
+        local('mkdir -p {path}'.format(path=beanstalk_local_base_path))
+
+    print magenta('Beanstalk-Stack app plant tool')
+    print separator('=-')
+
+    result = {}
+
+    def collector(setting_sets):
+        for setting_item in setting_sets:
+            if setting_item is None:
+                print separator()
+                continue
+            key, question, validator, value_type = setting_item
+            default_value = beanstalk_default_settings[key] or ''
+            value = prompt(question, default=default_value, validate=validator)
+            if value != default_value:
+                result[key] = (value, value_type)
+
+        for key, (value, value_type) in result.items():
+            beanstalk_default_settings[key] = value
+
+    # Collect
+    beanstalk_default_settings = load_beanstalk_settings(default_only=True)
+    setting_set1 = [
+        ('PROJECT_SOURCE_ROOT', 'Project Source Root?', None, 'str'),
+        ('PROJECT_NAME', 'Project Name?', None, 'str'),
+        ('GIT_IGNORE_PATH', 'Where\'s git ignore file?', validate_file_existence, 'str'),
+        None,
+        ('WEB_SERVERS', 'Where to deploy? (Python list/tuple with server address)', validate_web_server, 'obj'),
+        None,
+        ('PROJECT_TYPE',
+         'Which project type you are? (\'plain\' for project not in ({projects}))'.format(
+             projects=', '.join(supported_projects)),
+         validate_project_type, 'str'),
+    ]
+    collector(setting_set1)
+
+    # Project type
+    if ('PROJECT_TYPE' in result and result['PROJECT_TYPE'] == 'django') or ('PROJECT_TYPE' not in result):
+        setting_set2 = [
+            ('DJANGO_PROJECT_SETTINGS', 'Where\'s your django project settings? (normal one)',
+             validate_file_existence, 'str'),
+            ('DJANGO_PROJECT_SETTINGS_DEV', 'Where\'s your django project settings for development?',
+             validate_file_existence, 'str'),
+            ('DJANGO_PROJECT_SETTINGS_PROD', 'Where\'s your django project settings for production?',
+             validate_file_existence, 'str'),
+        ]
+        collector(setting_set2)
+
+    print ''
+
+    # Generate beanstalk_settings
+    print cyan('Generate beanstalk_stack app settings')
+    sorted_result = []
+    for key, (value, value_type) in result.items():
+        sorted_result.append((key, (value, value_type)))
+    sorted_result.sort(cmp=lambda x, y: cmp(x[0], y[0]))
+
+    print 'Genreated setting file:'
+    print separator('=')
+    output = StringIO.StringIO()
+    for key, (value, value_type) in sorted_result:
+        value = value if value_type == 'obj' else '\'{0}\''.format(value)
+        output.write('{0} = {1}\n'.format(key, value))
+    final_settings = output.getvalue()
+    print final_settings
+    output.close()
+    print separator('=')
+
+    write_to_file = confirm('Write to {path}'.format(path=app_settings_path), default=True)
+    if write_to_file:
+        if os.path.exists(app_settings_path):
+            local('mv {path} {path}.bak.{ts}'.format(
+                path=app_settings_path, ts=datetime.datetime.now().strftime('%s-%f')))
+        with open(app_settings_path, 'w') as f:
+            f.write(final_settings)
+
+    print green('Planted. Grow up now!')
+
+
+@task
+@beanstalk_role('app')
+def deploy(**settings_patches):
+    """Deploy project in current work folder to beanstalk-stack
+    """
+    # Set variables
+    env.work_directory = os.getcwd()
+    env.in_remote = False
+
+    # Load beanstalk settings
+    settings_patches = eval_kwargs(settings_patches)
+    beanstalk_settings = load_beanstalk_settings(**settings_patches)
+
+    # Get variables from settings
+    project_name = beanstalk_settings['PROJECT_NAME']
+    project_source_root = beanstalk_settings['PROJECT_SOURCE_ROOT']
+
+    # Setup environment, ssh, and web servers
+    set_verbose_level()
+    env.use_ssh_config = beanstalk_settings['USE_SSH_CONFIG']
+    env.shell_env = beanstalk_settings['REMOTE_ENVIRONMENTS']
+    load_web_servers()
+
+    # Start
+    print tool_title('Beanstalk-Stack app deploy tool')
+    print separator('=-')
+    print 'Project Name   : ' + green(project_name)
+    print 'Source root    : ' + green(project_source_root)
+    print 'Target servers : ' + yellow('{0!s}'.format(env.roledefs['web_servers']))
+    print separator('-')
+
+    # Start to work in room!
+    tmp_dir_prefix = 'deploy~{0}~'.format(project_name)
+    with temporary_directory(identifier=beanstalk_identifier, prefix=tmp_dir_prefix) as tmp_directory:
+        print section_title('Copy source root to temp directory')
+        puts('tmp dir: {path}'.format(path=tmp_directory))
+        env.temp_directory = tmp_directory
+
+        # Find the exclude file (gitignore)
+        gitignore_path = beanstalk_settings['GIT_IGNORE_PATH']
+        gitignore_at_source_root = gitignore_path == '.gitignore'
+        gitignore_path = os.path.normpath(os.path.join(project_source_root, gitignore_path))
+        if os.path.exists(os.path.join(project_source_root, beanstalk_settings['GIT_IGNORE_PATH'])):
+            # Copy the ignore to source root
+            if not gitignore_at_source_root:
+                local(rsync(gitignore_path, os.path.join(tmp_directory, '.gitignore')))
+        else:
+            abort('Cannot find gitignore file at {path}'.format(path=gitignore_path))
+
+        # Copy it
+        rsync_argument = '-rlpctzD --exclude .git --exclude-from \'{path}\''.format(
+            path=os.path.relpath(gitignore_path))
+        local(rsync(project_source_root, tmp_directory, rsync_argument=rsync_argument, expand_to_destination=True))
+
+        # Go to the tmp work dir
+        with lcd(tmp_directory):
+            puts(section_title('Switch to {path}'.format(path=tmp_directory)))
+
+            # We wanna preserve pyc/pyo files in our remote
+            # Remove *.pyc, *.pyo, *.py[co] in ignore patterns and append "logs/" in the ignore file
+            local(
+                'sed -i.bak -e '
+                '\'/^\*\.py[c|o]$/d;/^\*\.py\[co\]$/d;$a\\\nlogs\/;$a\\\nvenv\/\''
+                ' {path} && rm -rf {path}.bak'.format(path='.gitignore')
+            )
+
+            # Check source code first
+            run_hooked_actions('SOURCE_CHECK_ACTIONS')
+
+            # Ask
+            # TODO: Authenticate if possible
+            if beanstalk_settings['CONFIRM_BEFORE_DEPLOY']:
+                try:
+                    if not confirm('deploy?', default=False):
+                        raise KeyboardInterrupt
+                except KeyboardInterrupt:
+                    abort(red('User cancels deployment ...', bold=True))
+                print ''
+
+            # Call pre-deploy script
+            if run_hooked_actions('PRE_DEPLOY_ACTIONS'):
+                print ''
+
+            # Work on remote
+            print separator('+')
+            with settings(hide('running')):
+                env.in_remote = True
+                execute(deploy_remote_core)
+                env.in_remote = False
+            print separator('+')
+            print ''
+
+            # Call post-deploy script
+            if run_hooked_actions('POST_DEPLOY_ACTIONS'):
+                print ''
+
+            print green('Deployed!')
+
+
+@roles('web_servers')
+@beanstalk_role('app')
+def deploy_remote_core():
+    """ The part to run on each server
+    """
+    env.host_name = host_name = env.host_string.split('@')[-1]
+    print section_title('Deploy to {host}'.format(host=white(host_name)))
+
+    # Find Jack first
+    with settings(hide('everything'), warn_only=True):
+        if run('which bsjack').return_code != 0:
+            abort(
+                red('Hey! I cannot find Jack at ') + red(host_name, bold=True) + red('. ') +
+                'Please setup beanstalk-stack on this server first.'
+            )
+        remote_bs_version = json.loads(run('bsjack info.version:USE_JSON=True --hide=status'))
+        if beanstalk_version_tuple != tuple(remote_bs_version['list']):
+            abort(red('Hey! Jack at the remote is not the same as me. He\'s {0}. But I\'m {1}.').format(
+                remote_bs_version['string'], beanstalk_version_string))
+
+    # Get/Set variables
+    beanstalk_settings = env.beanstalk_settings
+    project_name = beanstalk_settings['PROJECT_NAME']
+    verbose_level = beanstalk_settings['VERBOSE']
+
+    # Call pre-install script
+    run_hooked_actions('PRE_INSTALL_ACTIONS')
+
+    # Create this app to server
+    print section_title('Find room for {name} in {host}'.format(name=project_name, host=host_name))
+    run('bsjack server.create_app:{project_name},VERBOSE={verbose:d} --hide=status'.format(
+        project_name=project_name, verbose=beanstalk_settings['VERBOSE']))
+
+    # Get app info first
+    with hide('everything'):
+        app_info = json.loads(
+            run(
+                'bsjack server.app_info:{project_name},VERBOSE={verbose:d},USE_JSON=True --hide=status'.format(
+                    project_name=project_name, verbose=beanstalk_settings['VERBOSE'])))
+    env.remote_app_info = app_info
+    remote_base_path = app_info['path']['base']
+    remote_source_path = app_info['path']['source']
+
+    # Call pre-install script
+    run_hooked_actions('PRE_SEND_ACTIONS')
+
+    print section_title('Send {name} to remote: {path}'.format(name=project_name, path=white(remote_base_path)))
+    # Send code
+    rsync_argument = '-DLhrctz --delete --delete-excluded'
+    rsync_argument += ' --exclude \'*.pyc\' --exclude \'*.pyo\' --exclude-from \'.gitignore\''
+    if verbose_level == 0:
+        rsync_argument += ' -q'
+    elif verbose_level == 2:
+        rsync_argument += ' -v'
+    elif verbose_level == 3:
+        rsync_argument += ' -Pv'
+    target_source_path = 'beanstalk@{0}:{1}'.format(host_name, remote_source_path)
+    target_base_path = 'beanstalk@{0}:{1}'.format(host_name, remote_base_path)
+    local(rsync(env.temp_directory, target_source_path, rsync_argument=rsync_argument, expand_to_destination=True))
+    local(rsync('{path}/.gitignore'.format(path=env.temp_directory), target_base_path))
+
+    # Call post-install script
+    run_hooked_actions('POST_SEND_ACTIONS')
+
+    # Update this app in server
+    print section_title('Install %s in remote' % project_name)
+    run('bsjack server.install_app:{name},VERBOSE={verbose:d} --hide=status'.format(
+        name=project_name, verbose=beanstalk_settings['VERBOSE']))
+
+    # Call post-install script
+    run_hooked_actions('POST_INSTALL_ACTIONS')
+
+    # Commit state to git repo
+    print section_title('Commit %s in remote' % project_name)
+    run('bsjack server.commit_app:{name},VERBOSE={verbose:d} --hide=status'.format(
+        name=project_name, verbose=beanstalk_settings['VERBOSE']))
+    run('bsjack server.reload_app:{name},VERBOSE={verbose:d} --hide=status'.format(
+        name=project_name, verbose=beanstalk_settings['VERBOSE']))
+
+    print separator(' . ')

beanstalk/tasks/info.py

+import json
+from fabric.api import *
+from ground_soil.fabric import eval_kwargs
+
+
+@task
+def version(**kwargs):
+    """Get version of beanstalk-stack
+    """
+    import beanstalk
+    kwargs = eval_kwargs(kwargs)
+
+    use_json = kwargs.get('USE_JSON', False)
+
+    if use_json:
+        print json.dumps({
+            'string': beanstalk.__version__,
+            'list': beanstalk.VERSION,
+        })
+    else:
+        print 'Beanstalk-Stack version {0}'.format(beanstalk.__version__)

beanstalk/tasks/server.py

+import ConfigParser
+import datetime
+import json
+import os
+import socket
+from fabric.api import *
+from fabric.colors import *
+from fabric.contrib.console import confirm
+from ground_soil.fabric import eval_kwargs, virtual_env
+from ground_soil.filesystem import temporary_directory, render_file, sed
+from beanstalk import IDENTIFIER, BEANSTALK_ROOT_PATH
+from beanstalk.decorators import beanstalk_role
+from beanstalk.paths.server import (project_base_path, project_source_path, project_logs_path, project_venv_path,
+                                    project_uwsgi_path, apache_conf_path, app_ports_path)
+from beanstalk.tasks.utils import load_role_settings as load_beanstalk_settings, set_verbose_level, run_hooked_actions
+from beanstalk.console.titles import tool_title, section_title, separator
+
+
+# Tasks
+#-----------------------------------------------------------------------------------------------------------------------
+
+
+@task
+@beanstalk_role('server')
+def setup(**settings_patches):
+    """Setup remote base of beanstalk-stack in this server
+    """
+    print tool_title('Beanstalk-stack server setup tool')
+    print ''
+
+    # Load beanstalk settings
+    settings_patches = eval_kwargs(settings_patches)
+    beanstalk_settings = load_beanstalk_settings(**settings_patches)
+    beanstalk_stack_base = beanstalk_settings['BEANSTALK_STACK_BASE']
+    set_verbose_level()
+
+    # Call pre-setup script
+    run_hooked_actions('PRE_SETUP_ACTIONS')
+
+    # Install packages for building uwsgi
+    print section_title('Install system packages')
+    local('yum install -y python-virtualenv python-devel sqlite-devel libxml2-devel pcre-devel zeromq-devel'
+          ' libcap-devel libuuid-devel libev-devel python-greenlet-devel httpd-devel')
+
+    # Install Uwsgi
+    with temporary_directory(identifier=IDENTIFIER, prefix='init_remote_base') as tmp_directory:
+        with lcd(tmp_directory):
+            print section_title('Install uwsgi and mod_uwsgi')
+            # Get source code
+            local('wget http://projects.unbit.it/downloads/uwsgi-lts.tar.gz')
+            local('tar -zxf uwsgi-lts.tar.gz')
+            try:
+                uwsgi_src_folder = [path for path in os.listdir(tmp_directory)
+                                    if path.startswith('uwsgi-') and '-lts' not in path][0]
+            except IndexError:
+                abort('Failed to extract uwsgi source folder')
+                return
+
+            # Check version
+            install_uwsgi = True
+            with settings(hide('everything'), warn_only=True):
+                has_uwsgi = local('which uwsgi', capture=True).return_code == 0
+                if has_uwsgi:
+                    new_version = tuple(map(lambda x: int(x), uwsgi_src_folder.split('-')[-1].split('.')))
+                    current_version = tuple(map(lambda x: int(x), local('uwsgi --version', capture=True).split('.')))
+                    print new_version, current_version
+                    if new_version == current_version:
+                        install_uwsgi = confirm('Re-install Uwsgi?', default=False)
+
+            if install_uwsgi:
+                # Stop services before install it
+                if os.path.exists('/etc/init.d/beanstalk'):
+                    with settings(hide('warnings'), warn_only=True):
+                        local('/etc/init.d/beanstalk stop')
+
+                with lcd(uwsgi_src_folder):
+                    # Create build configuration
+                    local('mkdir -p /usr/local/lib/uwsgi')
+                    uwsgi_build_ini_path = os.path.join(BEANSTALK_ROOT_PATH, 'static/uwsgi_build.ini')
+                    local('cp %s buildconf/beanstalk.ini' % uwsgi_build_ini_path)
+                    # Build and install
+                    local('python uwsgiconfig.py --build beanstalk')
+                    local('cp uwsgi /usr/local/bin/uwsgi')
+                    if os.path.exists('/usr/bin/uwsgi'):
+                        local('rm -rf /usr/bin/uwsgi')
+                    local('ln -s /usr/local/bin/uwsgi /usr/bin/uwsgi')
+                    # mod_uwsgi
+                    local('apxs -i -c apache2/mod_uwsgi.c')
+                    local('echo "LoadModule uwsgi_module modules/mod_uwsgi.so" > /etc/httpd/conf.d/uwsgi.conf')
+
+    # Init script
+    local('mkdir -p /var/run/beanstalk')
+    local('cp %s /etc/init.d/beanstalk' % os.path.join(BEANSTALK_ROOT_PATH, 'static/beanstalk_init.sh'))
+    local('chmod +x /etc/init.d/beanstalk')
+
+    # Do you have user "beanstalk"?
+    with settings(hide('everything'), warn_only=True):
+        has_beanstalk_user = local('egrep ^beanstalk: /etc/passwd').return_code == 0
+    if not has_beanstalk_user:
+        print section_title('Create user "beanstalk"')
+        # Create user
+        local('useradd beanstalk')
+        # Setup user password
+        print yellow('Set password for user beanstalk', bold=True)
+        local('passwd beanstalk')
+        # Setup git
+        local('su -c "git config --global user.name \'beanstalk\'" - beanstalk')
+        local('su -c "git config --global user.email %s" - beanstalk' % beanstalk_settings['BEANSTALK_EMAIL'])
+        # Setup .ssh
+        local('mkdir -p ~beanstalk/.ssh')
+        local('chmod 700 ~beanstalk/.ssh')
+        local('touch ~beanstalk/.ssh/authorized_keys')
+        local('chmod 644 ~beanstalk/.ssh/authorized_keys')
+        local('chown -R beanstalk:beanstalk ~beanstalk/.ssh')
+
+    # Create homebase
+    print section_title('Deploy target is: %s' % beanstalk_stack_base)
+    local('mkdir -p %s' % beanstalk_stack_base)
+    local('mkdir -p %s' % os.path.join(beanstalk_stack_base, 'apps'))
+    local('mkdir -p %s' % os.path.join(beanstalk_stack_base, 'global-logs'))
+    local('mkdir -p %s' % os.path.join(beanstalk_stack_base, 'confs'))
+    if not os.path.exists(app_ports_path()):
+        local('echo "[]" > %s' % app_ports_path())
+    local('chmod o+rx %s' % beanstalk_stack_base)
+
+    # Global ini
+    local('mkdir -p /etc/beanstalk')
+    if not os.path.exists('/etc/beanstalk/uwsgi.ini'):
+        global_uwsgi_ini_template_path = os.path.join(BEANSTALK_ROOT_PATH, 'static/global_uwsgi.ini')
+        with hide('everything'):
+            uid = local('id -u beanstalk', capture=True)
+        global_uwsgi_ini_content = render_file(global_uwsgi_ini_template_path, beanstalk_settings, BEANSTALK_UID=uid)
+        with open('/etc/beanstalk/uwsgi.ini', 'w') as f:
+            f.write(global_uwsgi_ini_content)
+
+    # Apache command
+    print section_title('Create apache-reload command')
+    httpd_cmd_path = '/usr/sbin/httpd_reload'
+    if not os.path.exists(httpd_cmd_path):
+        httpd_cmd_source = os.path.join(BEANSTALK_ROOT_PATH, 'static/httpd_reload.sh')
+        local('/bin/cp -f %s %s' % (httpd_cmd_source, httpd_cmd_path))
+        local('chmod +x %s' % httpd_cmd_path)
+
+    # Give beanstalk permission
+    print section_title('Give beanstalk permission for apache-reload command')
+    sudoer_file = '/etc/sudoers.d/beanstalk_reload_httpd'
+    if not os.path.exists(sudoer_file):
+        local('echo "beanstalk ALL=(root) NOPASSWD: %s" > %s' % (httpd_cmd_path, sudoer_file))
+        local('chmod 440 %s' % sudoer_file)
+
+    # Make beanstalk access ssh
+    print section_title('Configure SSH server')
+    sshd_config = '/etc/ssh/sshd_config'
+    new_lines = [
+        'Match Group beanstalk',
+        '       ForceCommand /usr/bin/bs_ssh_entry',
+    ]
+    local(sed('$ a\\\n{command}'.format(command='\\n'.join(new_lines)), sshd_config))
+    local(sed('s/^AllowGroups (.*)$/AllowGroups \\1 beanstalk/g', sshd_config, '-E'))
+
+    # Apache conf
+    print section_title('Configure Apache HTTP server')
+    if not os.path.exists('/etc/httpd/conf.d/beanstalk.conf'):
+        beanstalk_apache_conf_template = os.path.join(BEANSTALK_ROOT_PATH, 'static/beanstalk_apache.conf')
+        beanstalk_apache_conf = render_file(beanstalk_apache_conf_template, beanstalk_settings)
+        with open('/etc/httpd/conf.d/beanstalk.conf', 'w') as f:
+            f.write(beanstalk_apache_conf)
+    if not os.path.exists(apache_conf_path()):
+        local('echo "# Beanstalk Apache conf" > %s' % apache_conf_path())
+        local('chown beanstalk:beanstalk %s' % apache_conf_path())
+    if not os.path.exists('/etc/httpd/conf.d/beanstalk_app.conf'):
+        local('ln -s %s /etc/httpd/conf.d/beanstalk_app.conf' % apache_conf_path())
+
+    local('chown -R beanstalk:beanstalk %s' % beanstalk_stack_base)
+    local('/etc/init.d/sshd restart')
+    local('/etc/init.d/httpd restart')
+    local('/etc/init.d/beanstalk start')
+    local('chkconfig beanstalk on')
+
+    # Call post-setup script
+    run_hooked_actions('POST_SETUP_ACTIONS')
+
+
+@task
+@beanstalk_role('server')
+def create_app(project_name, **settings_patches):
+    """Create an app
+    """
+    settings_patches = eval_kwargs(settings_patches)
+    load_beanstalk_settings(**settings_patches)
+    env.project_name = project_name
+    set_verbose_level()
+
+    # Folder paths
+    project_base = project_base_path(project_name)
+    project_source = project_source_path(project_name)
+    project_logs = project_logs_path(project_name)
+    project_venv = project_venv_path(project_name)
+
+    if os.path.exists(project_base):
+        print 'Nothing to do. App existed!'
+        return
+
+    # Pre-create actions
+    run_hooked_actions('PRE_CREATE_ACTIONS')
+
+    # Create folders
+    print section_title('Create folders')
+    local('mkdir -p %s' % project_base)
+    local('mkdir -p %s' % project_source)
+    local('mkdir -p %s' % project_logs)
+
+    # Initialize venv
+    if not os.path.exists(project_venv):
+        print section_title('Initialize virtual env')
+        with lcd(project_base):
+            local('virtualenv venv')
+
+    # Initialize git
+    if not os.path.exists(os.path.join(project_base, '.git')):
+        print section_title('Make git repository')
+        with lcd(project_base):
+            local('git init')
+            local('touch .gitignore')
+            local('git add .gitignore')
+            local('git commit -m "Init commit"')
+
+    # Get uwsgi port
+    with open(app_ports_path(), 'r') as f:
+        ports = json.loads(f.read())
+    if len(ports) == 0:
+        uwsgi_port = 50000
+    else:
+        uwsgi_port = max(ports) + 1
+    # Available ports?
+    try_times = 1
+    port_available = False
+    while try_times <= 10:
+        try:
+            s = socket.socket()
+            s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+            s.bind(('localhost', uwsgi_port))
+            s.close()
+            port_available = True
+            break
+        except socket.error:
+            uwsgi_port += 1
+            try_times += 1
+    if not port_available:
+        abort('No available port for uwsgi')
+    else:
+        env.uwsgi_port = uwsgi_port
+        ports.append(uwsgi_port)
+    # Save it
+    with open(app_ports_path(), 'w') as f:
+        f.write(json.dumps(ports, separators=(',', ':')))
+
+    print section_title('Add to apache httpd')
+    with open(apache_conf_path(), 'r') as f:
+        apache_conf = f.read()
+    if not '# %s' % project_name in apache_conf:
+        commands = [
+            '# %s' % project_name,
+            '#' + '-' * 20,
+            '<Location /%s>' % project_name,
+            '    SetHandler uwsgi-handler',
+            '    uWSGISocket 127.0.0.1:%s' % uwsgi_port,
+            '</Location>',
+            '#' + '-' * 20,
+            ' ',
+        ]
+        sed_command = '\\\n'.join(commands)
+        local(sed('$a\\{command}'.format(command=sed_command), apache_conf_path()))
+        local('sudo /usr/sbin/httpd_reload')
+
+    port_file_path = os.path.join(project_base_path(project_name), '%d.port' % uwsgi_port)
+    local('echo "%d" > %s' % (uwsgi_port, port_file_path))
+
+    # Post-create actions
+    run_hooked_actions('POST_CREATE_ACTIONS')
+
+
+@task
+@beanstalk_role('server')
+def delete_app(project_name, **settings_patches):
+    """Delete an app
+    """
+    settings_patches = eval_kwargs(settings_patches)
+    load_beanstalk_settings(**settings_patches)
+    set_verbose_level()
+    env.project_name = project_name
+
+    # Folder paths
+    project_base = project_base_path(project_name)
+
+    remove = confirm(red('Remove project: %s ?' % project_name), default=False)
+
+    # delete
+    if remove:
+        print section_title('Remove app @ %s' % project_base)
+        try:
+            port_number = int([name for name in os.listdir(project_base) if name.endswith('.port')][0].split('.')[0])
+            with open(app_ports_path(), 'r') as f:
+                ports = json.loads(f.read())
+            if port_number in ports:
+                ports.remove(port_number)
+            with open(app_ports_path(), 'w') as f:
+                f.write(json.dumps(ports))
+        except (IndexError, ValueError):
+            pass
+
+        local('rm -rf %s' % project_base)
+
+    local(sed('/# {name}/,+7d'.format(name=project_name), apache_conf_path()))
+    local('sudo /usr/sbin/httpd_reload')
+
+
+@task
+@beanstalk_role('server')
+def install_app(project_name, **settings_patches):
+    """Install an app (call when u updated the code)
+    """
+    settings_patches = eval_kwargs(settings_patches)
+    load_beanstalk_settings(**settings_patches)
+    set_verbose_level()
+    env.project_name = project_name
+
+    run_hooked_actions('PRE_INSTALL_ACTIONS')
+
+    project_source = project_source_path(project_name)
+    project_venv = project_venv_path(project_name)
+
+    print section_title('Install/Update packages in virtualenv')
+    with settings(virtual_env(project_venv)):
+        local('pip install -r %s' % os.path.join(project_source, 'requirements.txt'))
+
+    print section_title('Compile python code')
+    local('find %s -name \'*.pyc\' -exec rm -rf {} \;' % project_source)
+    local('find %s -name \'*.pyo\' -exec rm -rf {} \;' % project_source)
+    local('python -m compileall -q %s' % project_source)
+
+    print section_title('Setup uwsgi')
+    # uwsgi port
+    try:
+        uwsgi_port = int([name for name in os.listdir(project_base_path(project_name))
+                          if name.endswith('.port')][0].split('.')[0])
+    except (IndexError, ValueError):
+        abort('No port number!?')
+        return
+    # edit ini
+    uwsgi_path = os.path.join(project_source_path(project_name), 'uwsgi.ini')
+    config = ConfigParser.ConfigParser()
+    if os.path.exists(uwsgi_path):
+        config.read(uwsgi_path)
+    if not config.has_section('uwsgi'):
+        config.add_section('uwsgi')
+        config.set('uwsgi', 'master', True)
+        config.set('uwsgi', 'enable-threads', True)
+        config.set('uwsgi', 'threads', 20)
+        config.set('uwsgi', 'processes', 4)
+        config.set('uwsgi', 'module', '%s.wsgi:application' % project_name)
+    config.set('uwsgi', 'socket', '127.0.0.1:%d' % uwsgi_port)
+    config.set('uwsgi', 'chdir', project_source_path(project_name))
+    config.set('uwsgi', 'home', project_venv_path(project_name))
+    with hide('everything'):
+        uid = local('id -u beanstalk', capture=True)
+    config.set('uwsgi', 'uid', uid)
+    config.set('uwsgi', 'logto', os.path.join(project_logs_path(project_name), 'uwsgi.log'))
+    with open(uwsgi_path, 'wb') as f:
+        config.write(f)
+
+    run_hooked_actions('POST_INSTALL_ACTIONS')
+
+
+@task
+@beanstalk_role('server')
+def reload_app(project_name, **settings_patches):
+    """Update an app (call when u updated the code)
+    """
+    settings_patches = eval_kwargs(settings_patches)
+    load_beanstalk_settings(**settings_patches)
+    set_verbose_level()
+    env.project_name = project_name
+
+    run_hooked_actions('PRE_RELOAD_ACTIONS')
+
+    print section_title('Reload app: %s' % project_name)
+    local('touch %s' % project_uwsgi_path(project_name))
+
+    run_hooked_actions('POST_RELOAD_ACTIONS')
+
+
+@task
+@beanstalk_role('server')
+def build_venv(project_name, **settings_patches):
+    """Build virtual env of target app.
+    """
+    settings_patches = eval_kwargs(settings_patches)
+    load_beanstalk_settings(**settings_patches)
+    set_verbose_level()
+
+    # path
+    project_venv = project_venv_path(project_name)
+    requirments_path = os.path.join(project_source_path(project_name), 'requirments.txt')
+
+    print section_title('Build virtualenv for %s' % project_name)
+
+    # Clean it
+    local('rm -rf %s' % project_venv)
+
+    venv_base, venv_name = os.path.split(project_venv)
+    with lcd(venv_base):
+        local('virtualenv %s' % venv_name)
+        with settings(virtual_env(project_venv)):
+            local('pip intall -r %s' % requirments_path)
+
+
+@task
+@beanstalk_role('server')
+def app_info(project_name, **settings_patches):
+    """Get info of an app
+    """
+    settings_patches = eval_kwargs(settings_patches)
+    beanstalk_settings = load_beanstalk_settings(**settings_patches)
+    set_verbose_level()
+
+    # Gather information
+    apps = os.listdir(os.path.join(beanstalk_settings['BEANSTALK_STACK_BASE'], 'apps'))
+
+    if project_name not in apps:
+        if beanstalk_settings.get('USE_JSON', False):
+            print json.dumps({'error': 'No such app'})
+        else:
+            print red('No such app')
+        return
+
+    try:
+        uwsgi_port = int([name for name in os.listdir(project_base_path(project_name))
+                          if name.endswith('.port')][0].split('.')[0])
+    except (IndexError, ValueError):
+        abort('No port number!?')
+        return
+
+    if beanstalk_settings.get('USE_JSON', False):
+        result = {
+            'path': {
+                'base': project_base_path(project_name),
+                'source': project_source_path(project_name),
+                'logs': project_logs_path(project_name),
+                'venv': project_venv_path(project_name),
+            },
+            'uwsgi': {
+                'port': uwsgi_port,
+            },
+        }
+        json_string = json.dumps(result, separators=(',', ':'))
+
+        if beanstalk_settings.get('PRINT_JSON', True):
+            print json_string
+        return json_string
+    else:
+        print section_title('Beanstalk-stack app: %s' % project_name)
+        print separator('-=')
+        print 'Base   : %s' % project_base_path(project_name)
+        print 'Source : %s' % project_source_path(project_name)
+        print 'Logs   : %s' % project_logs_path(project_name)
+        print 'venv   : %s' % project_venv_path(project_name)
+        print separator('-', 40)
+        print 'uwsgi port : %s' % uwsgi_port
+
+
+@task
+@beanstalk_role('server')
+def list_apps(**settings_patches):
+    """Get list of apps
+    """
+    settings_patches = eval_kwargs(settings_patches)
+    beanstalk_settings = load_beanstalk_settings(**settings_patches)
+    set_verbose_level()
+
+    apps = os.listdir(os.path.join(beanstalk_settings['BEANSTALK_STACK_BASE'], 'apps/'))
+
+    print section_title('Beanstalk-stack apps')
+    print separator('-=')
+    for app in apps:
+        print app
+
+
+@task
+@beanstalk_role('server')
+def commit_app(project_name, **settings_patches):
+    """Commit app
+    """
+    settings_patches = eval_kwargs(settings_patches)
+    load_beanstalk_settings(**settings_patches)
+    env.project_name = project_name
+    set_verbose_level()
+
+    run_hooked_actions('PRE_COMMIT_ACTION')
+
+    project_base = project_base_path(project_name)
+    with settings(lcd(project_base), hide('warnings'), warning=True):
+        local('git add -A && git commit -m "state at %s"' % datetime.datetime.now().strftime('%s'))
+
+    run_hooked_actions('POST_COMMIT_ACTION')
+
+
+@task
+@beanstalk_role('server')
+def add_user(tmp_key_path, clean_tmp_key=False):
+    """Add a user's SSH public key to beanstalk
+    """
+    tmp_key_path = os.path.expanduser(tmp_key_path)
+
+    with open(tmp_key_path, 'r') as f:
+        raw_key = f.read().strip()
+
+    new_key = 'no-port-forwarding,no-X11-forwarding,no-agent-forwarding {key}'.format(key=raw_key)
+
+    has_this_key = False
+    authorized_keys_path = os.path.expanduser('~/.ssh/authorized_keys')
+    with open(authorized_keys_path, 'r') as authorized_keys:
+        for authorized_key in authorized_keys:
+            if authorized_key.strip() == new_key:
+                has_this_key = True
+                break
+
+    if not has_this_key:
+        # NOTE: Be careful that new_key may contains " (double quote)
+        local('cp {0} {0}.bak && echo "{1}" >> {0} && rm -rf {0}.bak'.format(authorized_keys_path, new_key))
+
+    if clean_tmp_key:
+        local('rm -rf {path}'.format(path=tmp_key_path))
+
+    print green('User added.')

beanstalk/tasks/users.py

+import hashlib
+from fabric.api import *
+from ground_soil.filesystem import rsync
+from beanstalk.decorators import beanstalk_role
+from beanstalk.tasks.utils import load_role_settings as load_beanstalk_settings, load_web_servers
+from beanstalk.console.titles import tool_title
+from beanstalk.validator import validate_file_existence
+
+
+@task
+@beanstalk_role('app')
+def add(**settings_patches):
+    """Add current user to beanstalk-stack server
+    """
+    # Load settings
+    load_beanstalk_settings(**settings_patches)
+    load_web_servers()
+
+    print tool_title('Beanstalk-Stack users tool')
+    print ''
+
+    # Get ssh public key
+    ssh_pub_key_path = prompt('Where\'s your ssh public key?', default='~/.ssh/id_rsa.pub',
+                              validate=validate_file_existence)
+    with open(ssh_pub_key_path, 'r') as f:
+        key_content = f.read().strip()
+    hash_key = hashlib.md5(key_content).hexdigest()[:16]
+    tmp_path = '/tmp/beanstalk-stack.{hash}.pub.key'.format(hash=hash_key)
+
+    @roles('web_servers')
+    def add_user_core():
+        host_name = env.host_string.split('@')[-1]
+
+        local(rsync(ssh_pub_key_path, 'beanstalk@{host}:{key_path}'.format(host=host_name, key_path=tmp_path)))
+        run('bsjack server.add_user:{key_path},clean_tmp_key=True'.format(key_path=tmp_path))
+
+    with settings(hide('running')):
+        # TODO: Should not prompt beanstalk's SSH password
+        execute(add_user_core)

beanstalk/tasks/utils.py

+import os
+from fabric.api import *
+from fabric.state import output as output_level
+from ground_soil.datastucture import SettingsDict
+from beanstalk import (BEANSTALK_ROOT_PATH, BEANSTALK_GLOBAL_BASE_PATH,
+                       BEANSTALK_LOCAL_BASE_PATH)
+from beanstalk.console.titles import section_title
+
+
+def load_role_settings(role=None, load_again=False, default_only=False, **setting_patches):
+    """Load beanstalk settings in
+    """
+    # Loaded?
+    env_key = 'beanstalk_settings' if not default_only else 'beanstalk_default_settings'
+    if not load_again and env_key in env:
+        return env[env_key]
+
+    # Check role
+    if role is None:
+        role = env.get('beanstalk_role', None)
+    if role not in ('app', 'server'):
+        abort('I don\'t know this role. ({role})'.format(role=role))
+
+    # Get default settings path
+    setting_paths = []
+    default_settings_path = os.path.join(BEANSTALK_ROOT_PATH, 'default_settings/{0}_settings.py'.format(role))
+    setting_paths.append(default_settings_path)
+
+    # Get other's settings
+    if not default_only:
+        # Get global role settings path
+        global_settings_path = os.path.join(BEANSTALK_GLOBAL_BASE_PATH, '{0}_settings.py'.format(role))
+        if os.path.exists(global_settings_path):
+            setting_paths.append(global_settings_path)
+        # Get user role settings path
+        user_settings_path = os.path.expanduser(
+            os.path.join('~/.beanstalk_stack/{0}_settings.py'.format(role)))
+        if os.path.exists(user_settings_path):
+            setting_paths.append(user_settings_path)
+        # Get project role settings path
+        project_settings_path = os.path.join(
+            os.getcwd(), BEANSTALK_LOCAL_BASE_PATH, '{0}_settings.py'.format(role))
+        if os.path.exists(project_settings_path):
+            setting_paths.append(project_settings_path)
+
+    beanstalk_settings = SettingsDict(*setting_paths, **setting_patches)
+
+    env[env_key] = beanstalk_settings
+    return beanstalk_settings
+
+
+def set_verbose_level(verbose_level=None):
+    if verbose_level is None:
+        beanstalk_settings = env.get('beanstalk_settings', None)
+        if beanstalk_settings is None:
+            abort('Give verbose_level or Load beanstalk settings first')
+        verbose_level = beanstalk_settings['VERBOSE']
+
+    # Check output level
+    if verbose_level == 0:
+        hide_output_status = ['everything']
+    elif verbose_level == 1:
+        hide_output_status = ['status', 'stdout', 'stderr', 'running']
+    elif verbose_level == 2:
+        hide_output_status = ['status', 'running']
+    else:
+        hide_output_status = []
+
+    for status in hide_output_status:
+        setattr(output_level, status, False)
+
+    env.verbose_level = verbose_level
+
+
+def load_web_servers():
+    beanstalk_settings = env.get('beanstalk_settings', None)
+    if beanstalk_settings is None:
+        abort('Load beanstalk settings first')
+
+    with hide('everything'):
+        execute('app.web_server', *beanstalk_settings['WEB_SERVERS'])
+        if beanstalk_settings['VERBOSE'] != 0:
+            puts('')
+
+    if len(env.roledefs['web_servers']) == 0:
+        abort('Where do you want to deploy to?')
+
+
+def run_hooked_actions(action_name):
+    beanstalk_settings = env.get('beanstalk_settings', None)
+    if beanstalk_settings is None:
+        abort('Load beanstalk settings first')
+
+    actions = beanstalk_settings[action_name]
+    if len(actions) != 0:
+        print section_title('Run {0}-{1} {2}'.format(*action_name.lower().split('_')))
+        for action in actions:
+            if isinstance(action, (list, tuple)):
+                action, action_args, action_kwargs = action
+            else:
+                action_args = []
+                action_kwargs = {}
+            with hide('status'):
+                execute(action, *action_args, **action_kwargs)
+        return True
+    else:
+        return False

beanstalk/validator.py

+import ast
+import os
+
+
+def required_input(value, msg=None):
+    if len(value) == 0:
+        raise ValueError(msg or 'You must enter someting.')
+
+
+def validate_web_server(raw_value):
+    required_input(raw_value, 'You must enter a Python list/tuple.')
+
+    try:
+        web_servers = ast.literal_eval(raw_value)
+    except SyntaxError:
+        raise ValueError('Invalid Python syntax')
+
+    if not isinstance(web_servers, (list, tuple)):
+        raise TypeError('You should input a list or a tuple')
+
+    def element_check(e):
+        if not isinstance(e, (str, unicode)):
+            raise TypeError('Each element should be a string/unicode.')
+    map(element_check, web_servers)
+
+    return raw_value
+
+
+def validate_file_existence(raw_value):
+    required_input(raw_value, 'You must enter a path.')
+
+    path = os.path.abspath(os.path.join(os.getcwd(), os.path.expanduser(raw_value)))
+    if not os.path.exists(path):
+        raise ValueError('There\'s no such file at this path.')
+
+    return os.path.normpath(os.path.expanduser(raw_value))
+
+
+def validate_project_type(raw_value):
+    if raw_value.lower() in ('plain',) + supported_projects:
+        return raw_value.lower()
+    raise ValueError('\'{0}\' is not a supported project type. Use \'plain\' for other project'.format(raw_value))
+supported_projects = ('django',)
+from fabric.api import *
+
+
+@task(alias='2pypi')
+def distribute_to_pypi():
+    """
+    Distribute the eggplant to PyPI
+    """
+    local('python setup.py sdist upload')
+    local('rm -rf beanstalk_stack.egg-info')
+    local('rm -rf dist')
 Fabric==1.5.1
 Jinja2==2.6
+ground-soil==0.1.2
+netaddr==0.7.10
 paramiko==1.9.0
 pycrypto==2.6
 six==1.2.0
 from setuptools import find_packages
 from beanstalk import __version__ as beanstalk_version
 
+
 setup(
     name='beanstalk-stack',
     version=beanstalk_version,
     license='Apache Software licence 2.0, see LICENCE.txt',
     description='Django base for common use',
     long_description=open('README.md').read(),
+    scripts=['beanstalk/bsjack', 'beanstalk/bs_ssh_entry'],
     keywords='django',
     zip_safe=False,
     include_package_data=True,
         'Fabric>=1.5',
         'Jinja2>=2.6',
         'six>=1.2',
+        'ground-soil>=0.1.11',
+        'netaddr>=0.7',
     ],
     classifiers=[
         'Development Status :: 2 - Pre-Alpha',