Browse Source

COOK-462, django/gunicorn support in application cookbook

Seth Chisamore 13 years ago
parent
commit
460e52347f

+ 85 - 154
README.md

@@ -5,8 +5,9 @@ This cookbook is initially designed to be able to describe and deploy web applic
 
 * Rails
 * Java
+* Django
 
-Other application stacks (PHP, DJango, etc) will be supported as new recipes at a later date.
+Other application stacks (PHP, etc) will be supported as new recipes at a later date.
 
 This cookbook aims to provide primitives to install/deploy any kind of application driven entirely by data defined in an abstract way through a data bag.
 
@@ -20,8 +21,10 @@ The following Opscode cookbooks are dependencies:
 
 * runit
 * unicorn
-* apache2
+* passenger_apache2
 * tomcat
+* python
+* gunicorn
 
 The following are also dependencies, though the recipes are considered deprecated, may be useful for future development.
 
@@ -41,17 +44,68 @@ Searches the `apps` data bag and checks that a server role in the app exists on
 
 See below regarding the application data bag structure.
 
+django
+------
+
+Using the node's `run_state` that contains the current application in the search, this recipe will:
+
+* create an application specific virtualenv
+* install required packages and pips
+* set up the deployment scaffolding
+* creates settings_local.py file with the database connection information if required
+* performs a revision-based deploy
+
+This recipe can be used on nodes that are going to run the application, or on nodes that need to have the application code checkout available such as supporting utility nodes or a configured load balancer that needs static assets stored in the application repository.
+
+For pip requirements.txt files: ensure the requirements.txt file is present in the root of the application code (APP_ROOT/requirements.txt) or named after the node's current app_environment in a directory named requirements (requirements/production.txt) and `pip install -r` will be run before migrations.
+
+In order to manage running database migrations (python manage.py migrate), you can use a role that sets the `run_migrations` attribute for the application (`my_app`, below) in the correct environment (production, below). Note the data bag item needs to have migrate set to true. See the data bag example below.
+
+    {
+      "name": "my_app_run_migrations",
+      "description": "Run db:migrate on demand for my_app",
+      "json_class": "Chef::Role",
+      "default_attributes": {
+      },
+      "override_attributes": {
+        "apps": {
+          "my_app": {
+            "production": {
+              "run_migrations": true
+            }
+          }
+        }
+      },
+      "chef_type": "role",
+      "run_list": [
+      ]
+    }
+
+Simply apply this role to the node's run list when it is time to run migrations, and the recipe will remove the role when done.  Since Django does not have a standard database migration function built into the core framework, we assume the popular [South framework](http://south.aeracode.org/) is being used.
+
+gunicorn
+--------
+
+Requires `gunicorn` cookbook.
+
+Gunicorn is installed, default attributes are set for the node and an app specific gunicorn config and runit service are created.
+
 `java_webapp`
 -----------
 
-Using the node's `run_state` that contains the current application in the search, this recipe will install required packages, set up the deployment scaffolding, create the context configuration for the servlet container and then performs a `remote_file` deploy.
+Using the node's `run_state` that contains the current application in the search, this recipe will:
+
+* install required packages
+* set up the deployment scaffolding
+* create the context configuration for the servlet container
+* performs a `remote_file` deploy.
 
 The servlet container context configuration (`context.xml`) exposes the following JNDI resources which can be referenced by the webapp's deployment descriptor (web.xml):
 
 * A JDBC datasource for all databases in the node's current `app_environment`.  The datasource uses the information (including JDBC driver) specified in the data bag item for the application.
 * An Environment entry that matches the node's current `app_environment` attribute value.  This is useful for loading environment specific properties files in the web application. 
 
-This recipe assumes some sort of build process, such as Maven or a Continuous Integration server like Hudson, will create a deployable artifact and make it available for download via HTTP (such as S3).
+This recipe assumes some sort of build process, such as Maven or a Continuous Integration server like Hudson, will create a deployable artifact and make it available for download via HTTP (such as S3 or artifactory).
 
 `passenger_apache2`
 -------------------
@@ -68,11 +122,16 @@ Sets up a passenger vhost template for the application using the `apache2` cookb
 rails
 -----
 
-Using the node's `run_state` that contains the current application in the search, this recipe will install required packages and gems, set up the deployment scaffolding, creates database and memcached configurations if required and then performs a revision-based deploy.
+Using the node's `run_state` that contains the current application in the search, this recipe will:
+
+* install required packages and gems
+* set up the deployment scaffolding
+* creates database and memcached configurations if required
+* performs a revision-based deploy.
 
 This recipe can be used on nodes that are going to run the application, or on nodes that need to have the application code checkout available such as supporting utility nodes or a configured load balancer that needs static assets stored in the application repository.
 
-For Gem Bundler: include `bundler` or `bundler08` in the gems list.  `bundle install` or `gem bundle` will be run before migrations.
+For Gem Bundler: include `bundler` or `bundler08` in the gems list.  `bundle install` or `gem bundle` will be run before migrations.  The `bundle install` command is invoked with the `--deployment` and `--without` flags following [Bundler best practices](http://gembundler.com/deploying.html).
 
 For config.gem in environment: `rake gems:install RAILS_ENV=<node environment>` will be run when a Gem Bundler command is not.
 
@@ -124,8 +183,8 @@ The following recipes are deprecated and have been removed from the cookbook. To
 `rails_nginx_ree_passenger`
 
 ---
-Application Data Bag (Rail's version)
-====================
+Application Data Bag 
+=====================
 
 The applications data bag expects certain values in order to configure parts of the recipe. Below is a paste of the JSON, where the value is a description of the key. Use your own values, as required. Note that this data bag is also used by the `database` cookbook, so it will contain database information as well. Items that may be ambiguous have an example.
 
@@ -133,162 +192,34 @@ The application used in examples is named `my_app` and the environment is `produ
 
 Note about "type": the recipes listed in the "type" will be included in the run list via `include_recipe` in the application default recipe based on the type matching one of the `server_roles` values.
 
-Note about `databases`, the data specified will be rendered as the `database.yml` file. In the `database` cookbook, this information is also used to set up privileges for the application user, and create the databases.
+Note about packages, the version is optional. If specified, the version will be passed as a parameter to the resource. Otherwise it will use the latest available version per the default `:install` action for the package provider.
 
-Note about gems and packages, the version is optional. If specified, the version will be passed as a parameter to the resource. Otherwise it will use the latest available version per the default `:install` action for the package provider.
+Rail's version additional notes
+-------------------------------
 
-    {
-      "id": "my_app",
-      "server_roles": [
-        "application specific role(s), typically the name of the app, e.g., my_app",
-        "my_app"
-      ],
-      "type": {
-        "my_app": [
-          "recipes in this application cookbook to run for this role",
-          "rails",
-          "unicorn"
-        ]
-      },
-      "memcached_role": [
-        "name of the role used for the app-specific memcached server",
-        "my_app_memcached"
-      ],
-      "database_slave_role": [
-        "name of the role used by database slaves, typically named after the app, 'my_app_database_slave'",
-        "my_app_database_slave"
-      ],
-      "database_master_role": [
-        "name of the role used by database master, typically named after the app 'my_app_database_master'",
-        "my_app_database_master"
-      ],
-      "repository": "git@github.com:company/my_app.git",
-      "revision": {
-        "production": "commit hash, branch or tag to deploy"
-      },
-      "force": {
-        "production": "true or false w/o quotes to force deployment, see the rails.rb recipe"
-      },
-      "migrate": {
-        "production": "true or false boolean to force migration, see rails.rb recipe"
-      },
-      "databases": {
-        "production": {
-          "reconnect": "true",
-          "encoding": "utf8",
-          "username": "db_user",
-          "adapter": "mysql",
-          "password": "awesome_password",
-          "database": "db_name_production"
-        }
-      },
-      "mysql_root_password": {
-        "production": "password for the root user in mysql"
-      },
-      "mysql_debian_password": {
-        "production": "password for the debian-sys-maint user on ubuntu/debian"
-      },
-      "mysql_repl_password": {
-        "production": "password for the 'repl' user for replication."
-      },
-      "snapshots_to_keep": {
-        "production": "if using EBS, integer of the number of snapshots we're going to keep for this environment."
-      },
-      "deploy_key": "SSH private key used to deploy from a private git repository",
-      "deploy_to": "path to deploy, e.g. /srv/my_app",
-      "owner": "owner for the application files when deployed",
-      "group": "group for the application files when deployed",
-      "packages": {
-        "package_name": "specific packages required for installation at the OS level to run the app like libraries and specific version, e.g.",
-        "curl": "7.19.5-1ubuntu2"
-      },
-      "gems": {
-        "gem_name": "specific gems required for installation to run the application, and if a specific version is required, e.g.",
-        "rails": "2.3.5"
-      },
-      "memcached": {
-        "production": {
-          "namespace": "specify the memcache namespace, ie my_app_environment"
-        }
-      }
-    }
+Note about `databases`, the data specified will be rendered as the `database.yml` file. In the `database` cookbook, this information is also used to set up privileges for the application user, and create the databases.
 
+Note about gems, the version is optional. If specified, the version will be passed as a parameter to the resource. Otherwise it will use the latest available version per the default `:install` action for the package provider.
 
----
-Application Data Bag (Java webapp version)
-====================
+An example is data bag item is included in this cookbook at `examples/data_bags/apps/rails_app.json`.
 
-The applications data bag expects certain values in order to configure parts of the recipe. Below is a paste of the JSON, where the value is a description of the key. Use your own values, as required. Note that this data bag is also used by the `database` cookbook, so it will contain database information as well. Items that may be ambiguous have an example.
+Java webapp version additional notes
+------------------------------------
 
-The application used in examples is named `my_app` and the environment is `production`. Most top-level keys are Arrays, and each top-level key has an entry that describes what it is for, followed by the example entries. Entries that are hashes themselves will have the description in the value.
+Note about `databases`, the data specified will be rendered as JNDI Datasource `Resources` in the servlet container context confiruation (`context.xml`) file. In the `database` cookbook, this information is also used to set up privileges for the application user, and create the databases.
 
-Note about "type": the recipes listed in the "type" will be included in the run list via `include_recipe` in the application default recipe based on the type matching one of the `server_roles` values.
+An example is data bag item is included in this cookbook at `examples/data_bags/apps/java_app.json`.
 
-Note about `databases`, the data specified will be rendered as JNDI Datasource `Resources` in the servlet container context confiruation (`context.xml`) file. In the `database` cookbook, this information is also used to set up privileges for the application user, and create the databases.
+Django version additional notes
+-------------------------------
 
-Note about packages, the version is optional. If specified, the version will be passed as a parameter to the resource. Otherwise it will use the latest available version per the default `:install` action for the package provider.
+Note about `databases`, the data specified will be rendered as the `settings_local.py` file. In the `database` cookbook, this information is also used to set up privileges for the application user, and create the databases.
 
-    {
-      "id": "my_app",
-      "server_roles": [
-        "application specific role(s), typically the name of the app, e.g., my_app",
-        "my_app"
-      ],
-      "type": {
-        "my_app": [
-          "recipes in this application cookbook to run for this role",
-          "java_webapp",
-          "tomcat"
-        ]
-      },
-      "database_slave_role": [
-        "name of the role used by database slaves, typically named after the app, 'my_app_database_slave'",
-        "my_app_database_slave"
-      ],
-      "database_master_role": [
-        "name of the role used by database master, typically named after the app 'my_app_database_master'",
-        "my_app_database_master"
-      ],
-      "wars": {
-        "production": {
-          "source": "source url of WAR file to deploy",
-          "checksum": "SHA256 (or portion thereof) of the WAR file to deploy"
-        }
-      },
-      "databases": {
-        "production": {
-          "max_active": "100",
-          "max_idle": "30",
-          "max_wait": "10000",
-          "username": "db_user",
-          "adapter": "mysql",
-          "driver": "com.mysql.jdbc.Driver",
-          "port": "3306",
-          "password": "awesome_password",
-          "database": "db_name_production"
-        }
-      },
-      "mysql_root_password": {
-        "production": "password for the root user in mysql"
-      },
-      "mysql_debian_password": {
-        "production": "password for the debian-sys-maint user on ubuntu/debian"
-      },
-      "mysql_repl_password": {
-        "production": "password for the 'repl' user for replication."
-      },
-      "snapshots_to_keep": {
-        "production": "if using EBS, integer of the number of snapshots we're going to keep for this environment."
-      },
-      "deploy_to": "path to deploy, e.g. /srv/my_app",
-      "owner": "owner for the application files when deployed",
-      "group": "group for the application files when deployed",
-      "packages": {
-        "package_name": "specific packages required for installation at the OS level to run the app like libraries and specific version, e.g.",
-        "curl": "7.19.5-1ubuntu2"
-      }
-    }
+Note about pips, the version is optional. If specified, the version will be passed as a parameter to the resource. Otherwise it will use the latest available version per the default `:install` action for the python_pip package provider.
+
+The `local_settings_file_name` value may be used to supply an alternate name for the environment specific `settings_local.py`, since Django projects do not have a standard name for this file.
 
+An example is data bag item is included in this cookbook at `examples/data_bags/apps/django_app.json`.
 
 ---
 Usage
@@ -341,7 +272,7 @@ Author:: Adam Jacob (<adam@opscode.com>)
 Author:: Joshua Timberman (<joshua@opscode.com>)
 Author:: Seth Chisamore (<schisamo@opscode.com>)
 
-Copyright 2009-2010, Opscode, Inc.
+Copyright 2009-2011, Opscode, Inc.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.

+ 67 - 0
examples/data_bags/apps/django_app.json

@@ -0,0 +1,67 @@
+{
+  "id": "my_app",
+  "server_roles": [
+    "application specific role(s), typically the name of the app, e.g., my_app",
+    "my_app"
+  ],
+  "type": {
+    "my_app": [
+      "recipes in this application cookbook to run for this role",
+      "django",
+      "gunicorn"
+    ]
+  },
+  "database_slave_role": [
+    "name of the role used by database slaves, typically named after the app, 'my_app_database_slave'",
+    "my_app_database_slave"
+  ],
+  "database_master_role": [
+    "name of the role used by database master, typically named after the app 'my_app_database_master'",
+    "my_app_database_master"
+  ],
+  "repository": "git@github.com:company/my_app.git",
+  "revision": {
+    "production": "commit hash, branch or tag to deploy"
+  },
+  "force": {
+    "production": "true or false w/o quotes to force deployment, see the rails.rb recipe"
+  },
+  "migrate": {
+    "production": "true or false boolean to force migration, see rails.rb recipe"
+  },
+  "databases": {
+    "production": {
+      "encoding": "utf8",
+      "username": "db_user",
+      "adapter": "mysql",
+      "port": "3306",
+      "password": "awesome_password",
+      "database": "db_name_production"
+    }
+  },
+  "mysql_root_password": {
+    "production": "password for the root user in mysql"
+  },
+  "mysql_debian_password": {
+    "production": "password for the debian-sys-maint user on ubuntu/debian"
+  },
+  "mysql_repl_password": {
+    "production": "password for the 'repl' user for replication."
+  },
+  "snapshots_to_keep": {
+    "production": "if using EBS, integer of the number of snapshots we're going to keep for this environment."
+  },
+  "deploy_key": "SSH private key used to deploy from a private git repository",
+  "deploy_to": "path to deploy, e.g. /srv/my_app",
+  "owner": "owner for the application files when deployed",
+  "group": "group for the application files when deployed",
+  "packages": {
+    "package_name": "specific packages required for installation at the OS level to run the app like libraries and specific version, e.g.",
+    "curl": "7.19.5-1ubuntu2"
+  },
+  "pips": {
+    "pip_name": "specific gems required for installation to run the application, and if a specific version is required, e.g.",
+    "django": "1.2"
+  },
+  "local_settings_file_name":"local_settings.py"
+}

+ 60 - 0
examples/data_bags/apps/java_app.json

@@ -0,0 +1,60 @@
+{
+  "id": "my_app",
+  "server_roles": [
+    "application specific role(s), typically the name of the app, e.g., my_app",
+    "my_app"
+  ],
+  "type": {
+    "my_app": [
+      "recipes in this application cookbook to run for this role",
+      "java_webapp",
+      "tomcat"
+    ]
+  },
+  "database_slave_role": [
+    "name of the role used by database slaves, typically named after the app, 'my_app_database_slave'",
+    "my_app_database_slave"
+  ],
+  "database_master_role": [
+    "name of the role used by database master, typically named after the app 'my_app_database_master'",
+    "my_app_database_master"
+  ],
+  "wars": {
+    "production": {
+      "source": "source url of WAR file to deploy",
+      "checksum": "SHA256 (or portion thereof) of the WAR file to deploy"
+    }
+  },
+  "databases": {
+    "production": {
+      "max_active": "100",
+      "max_idle": "30",
+      "max_wait": "10000",
+      "username": "db_user",
+      "adapter": "mysql",
+      "driver": "com.mysql.jdbc.Driver",
+      "port": "3306",
+      "password": "awesome_password",
+      "database": "db_name_production"
+    }
+  },
+  "mysql_root_password": {
+    "production": "password for the root user in mysql"
+  },
+  "mysql_debian_password": {
+    "production": "password for the debian-sys-maint user on ubuntu/debian"
+  },
+  "mysql_repl_password": {
+    "production": "password for the 'repl' user for replication."
+  },
+  "snapshots_to_keep": {
+    "production": "if using EBS, integer of the number of snapshots we're going to keep for this environment."
+  },
+  "deploy_to": "path to deploy, e.g. /srv/my_app",
+  "owner": "owner for the application files when deployed",
+  "group": "group for the application files when deployed",
+  "packages": {
+    "package_name": "specific packages required for installation at the OS level to run the app like libraries and specific version, e.g.",
+    "curl": "7.19.5-1ubuntu2"
+  }
+}

+ 75 - 0
examples/data_bags/apps/rails_app.json

@@ -0,0 +1,75 @@
+{
+  "id": "my_app",
+  "server_roles": [
+    "application specific role(s), typically the name of the app, e.g., my_app",
+    "my_app"
+  ],
+  "type": {
+    "my_app": [
+      "recipes in this application cookbook to run for this role",
+      "rails",
+      "unicorn"
+    ]
+  },
+  "memcached_role": [
+    "name of the role used for the app-specific memcached server",
+    "my_app_memcached"
+  ],
+  "database_slave_role": [
+    "name of the role used by database slaves, typically named after the app, 'my_app_database_slave'",
+    "my_app_database_slave"
+  ],
+  "database_master_role": [
+    "name of the role used by database master, typically named after the app 'my_app_database_master'",
+    "my_app_database_master"
+  ],
+  "repository": "git@github.com:company/my_app.git",
+  "revision": {
+    "production": "commit hash, branch or tag to deploy"
+  },
+  "force": {
+    "production": "true or false w/o quotes to force deployment, see the rails.rb recipe"
+  },
+  "migrate": {
+    "production": "true or false boolean to force migration, see rails.rb recipe"
+  },
+  "databases": {
+    "production": {
+      "reconnect": "true",
+      "encoding": "utf8",
+      "username": "db_user",
+      "adapter": "mysql",
+      "password": "awesome_password",
+      "database": "db_name_production"
+    }
+  },
+  "mysql_root_password": {
+    "production": "password for the root user in mysql"
+  },
+  "mysql_debian_password": {
+    "production": "password for the debian-sys-maint user on ubuntu/debian"
+  },
+  "mysql_repl_password": {
+    "production": "password for the 'repl' user for replication."
+  },
+  "snapshots_to_keep": {
+    "production": "if using EBS, integer of the number of snapshots we're going to keep for this environment."
+  },
+  "deploy_key": "SSH private key used to deploy from a private git repository",
+  "deploy_to": "path to deploy, e.g. /srv/my_app",
+  "owner": "owner for the application files when deployed",
+  "group": "group for the application files when deployed",
+  "packages": {
+    "package_name": "specific packages required for installation at the OS level to run the app like libraries and specific version, e.g.",
+    "curl": "7.19.5-1ubuntu2"
+  },
+  "gems": {
+    "gem_name": "specific gems required for installation to run the application, and if a specific version is required, e.g.",
+    "rails": "2.3.5"
+  },
+  "memcached": {
+    "production": {
+      "namespace": "specify the memcache namespace, ie my_app_environment"
+    }
+  }
+}

File diff suppressed because it is too large
+ 0 - 0
metadata.json


+ 4 - 2
metadata.rb

@@ -3,14 +3,16 @@ maintainer_email "cookbooks@opscode.com"
 license          "Apache 2.0"
 description      "Deploys and configures a variety of applications defined from databag 'apps'"
 long_description  IO.read(File.join(File.dirname(__FILE__), 'README.md'))
-version          "0.99.2"
+version          "0.99.3"
 recipe           "application", "Loads application databags and selects recipes to use"
+recipe           "application::django", "Deploys a Django application specified in a data bag with the deploy_revision resource"
+recipe           "application::gunicorn", "Sets up the deployed Django application with Gunicorn as the web server"
 recipe           "application::java_webapp", "Deploys a Java web application WAR specified in a data bag with the remote_file resource"
 recipe           "application::passenger_apache2", "Sets up a deployed Rails application as a Passenger virtual host in Apache2"
 recipe           "application::rails", "Deploys a Rails application specified in a data bag with the deploy_revision resource"
 recipe           "application::tomcat", "Sets up the deployed Java application with Tomcat as the servlet container"
 recipe           "application::unicorn", "Sets up the deployed Rails application with Unicorn as the web server"
 
-%w{ runit unicorn apache2 passenger_apache2 tomcat }.each do |cb|
+%w{ runit unicorn apache2 passenger_apache2 tomcat python gunicorn }.each do |cb|
   depends cb
 end

+ 191 - 0
recipes/django.rb

@@ -0,0 +1,191 @@
+#
+# Cookbook Name:: application
+# Recipe:: wsgi
+#
+# Copyright 2011, Opscode, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+app = node.run_state[:current_app]
+
+include_recipe "python"
+
+###
+# You really most likely don't want to run this recipe from here - let the
+# default application recipe work it's mojo for you.
+###
+
+node.default[:apps][app['id']][node.app_environment][:run_migrations] = false
+
+# the Django split-settings file name varies from project to project...+1 for standardization
+local_settings_file_name = app[:local_settings_file_name] || 'settings_local.py'
+
+## Create required directories
+
+directory app['deploy_to'] do
+  owner app['owner']
+  group app['group']
+  mode '0755'
+  recursive true
+end
+
+directory "#{app['deploy_to']}/shared" do
+  owner app['owner']
+  group app['group']
+  mode '0755'
+  recursive true
+end
+
+## Create a virtualenv for the app
+ve = python_virtualenv "#{app['id']}_env"  do
+  path "#{app['deploy_to']}/shared/env"
+  action :create
+end
+
+## First, install any application specific packages
+if app['packages']
+  app['packages'].each do |pkg,ver|
+    package pkg do
+      action :install
+      version ver if ver && ver.length > 0
+    end
+  end
+end
+
+## Next, install any application specific gems
+if app['pips']
+  app['pips'].each do |pip,ver|
+    python_pip pip do
+      version ver if ver && ver.length > 0
+      virtualenv ve.path
+      action :install
+    end
+  end
+end
+
+if app.has_key?("deploy_key")
+  ruby_block "write_key" do
+    block do
+      f = ::File.open("#{app['deploy_to']}/id_deploy", "w")
+      f.print(app["deploy_key"])
+      f.close
+    end
+    not_if do ::File.exists?("#{app['deploy_to']}/id_deploy"); end
+  end
+
+  file "#{app['deploy_to']}/id_deploy" do
+    owner app['owner']
+    group app['group']
+    mode '0600'
+  end
+
+  template "#{app['deploy_to']}/deploy-ssh-wrapper" do
+    source "deploy-ssh-wrapper.erb"
+    owner app['owner']
+    group app['group']
+    mode "0755"
+    variables app.to_hash
+  end
+end
+
+if app["database_master_role"]
+  dbm = nil
+  # If we are the database master
+  if node.run_list.roles.include?(app["database_master_role"][0])
+    dbm = node
+  else
+  # Find the database master
+    results = search(:node, "run_list:role\\[#{app["database_master_role"][0]}\\] AND app_environment:#{node[:app_environment]}", nil, 0, 1)
+    rows = results[0]
+    if rows.length == 1
+      dbm = rows[0]
+    end
+  end
+  
+  # we need the django version to render the correct type of settings.py file
+  django_version = 1.2
+  if app['pips'].has_key?('django') && !app['pips']['django'].blank?
+    django_version = app['pips']['django'].to_f
+  end
+
+  # Assuming we have one...
+  if dbm
+    # local_settings.py
+    template "#{app['deploy_to']}/shared/settings_local.py" do
+      source "settings.py.erb"
+      owner app["owner"]
+      group app["group"]
+      mode "644"
+      variables(
+        :host => dbm['fqdn'],
+        :database => app['databases'][node.app_environment],
+        :django_version => django_version
+      )
+    end
+  else
+    Chef::Log.warn("No node with role #{app["database_master_role"][0]}, settings_local.py not rendered!")
+  end
+end
+
+## Then, deploy
+deploy_revision app['id'] do
+  revision app['revision'][node.app_environment]
+  repository app['repository']
+  user app['owner']
+  group app['group']
+  deploy_to app['deploy_to']
+  action app['force'][node.app_environment] ? :force_deploy : :deploy
+  ssh_wrapper "#{app['deploy_to']}/deploy-ssh-wrapper" if app['deploy_key']
+  purge_before_symlink([])
+  create_dirs_before_symlink([])
+  symlinks({})
+  before_migrate do
+    requirements_file = nil
+    # look for requirements.txt files in common locations
+    if ::File.exists?(::File.join(release_path, "requirements", "#{node[:app_environment]}.txt"))
+      requirements_file = ::File.join(release_path, "requirements", "#{node[:app_environment]}.txt")
+    elsif ::File.exists?(::File.join(release_path, "requirements.txt"))
+      requirements_file = ::File.join(release_path, "requirements.txt")
+    end
+    
+    if requirements_file
+      Chef::Log.info("Installing pips using requirements file: #{requirements_file}")
+      execute "pip install -E #{ve.path} -r #{requirements_file}" do
+        ignore_failure true
+        cwd release_path
+      end
+    end
+  end
+
+  symlink_before_migrate({
+    "settings_local.py" => local_settings_file_name
+  })
+
+  if app['migrate'][node.app_environment] && node[:apps][app['id']][node.app_environment][:run_migrations]
+    migrate true
+    migration_command app['migration_command'] || "#{::File.join(ve.path, "bin", "python")} manage.py migrate"
+  else
+    migrate false
+  end
+  before_symlink do
+    ruby_block "remove_run_migrations" do
+      block do
+        if node.role?("#{app['id']}_run_migrations")
+          Chef::Log.info("Migrations were run, removing role[#{app['id']}_run_migrations]")
+          node.run_list.remove("role[#{app['id']}_run_migrations]")
+        end
+      end
+    end
+  end
+end

+ 55 - 0
recipes/gunicorn.rb

@@ -0,0 +1,55 @@
+#
+# Cookbook Name:: application
+# Recipe:: gunicorn 
+#
+# Copyright 2011, Opscode, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+app = node.run_state[:current_app] 
+
+ve = resources(:python_virtualenv => "#{app['id']}_env")
+node.default[:gunicorn][:virtualenv] = ve.path
+
+include_recipe "gunicorn"
+
+node.default[:gunicorn][:worker_timeout] = 60
+node.default[:gunicorn][:preload_app] = false
+node.default[:gunicorn][:worker_processes] = [node[:cpu][:total].to_i * 4, 8].min
+node.default[:gunicorn][:server_hooks] = {:pre_fork => 'import time;time.sleep(1)'}
+node.default[:gunicorn][:port] = '8080'
+
+gunicorn_config "/etc/gunicorn/#{app['id']}.py" do
+  listen "#{node[:ipaddress]}:#{node[:gunicorn][:port]}"
+  worker_timeout node[:gunicorn][:worker_timeout] 
+  preload_app node[:gunicorn][:preload_app] 
+  worker_processes node[:gunicorn][:worker_processes]
+  server_hooks node[:gunicorn][:server_hooks]
+  action :create
+end
+ 
+runit_service app['id'] do
+  template_name 'gunicorn'
+  cookbook 'application'
+  options('app' => app, 'virtualenv' => ve.path)
+  run_restart false
+end
+
+if ::File.exists?(::File.join(app['deploy_to'], "current"))
+  d = resources(:deploy => app['id'])
+  d.restart_command do
+    execute "/etc/init.d/#{app['id']} hup"
+  end
+end
+

+ 19 - 0
templates/default/settings.py.erb

@@ -0,0 +1,19 @@
+<% if @django_version >= 1.2 -%>
+DATABASES = {
+    'default': {
+        'NAME': '<%= @database['database'] %>',
+        'ENGINE': 'django.db.backends.<%= @database['adapter'] %>',
+        'USER': '<%= @database['username'] %>',
+        'PASSWORD': '<%= @database['password'] %>',
+        'HOST': '<%= @host %>',
+        'PORT': '<%= @database['port']  %>'
+    }
+}
+<% else -%>
+DATABASE_ENGINE = '<%= @database['adapter'] %>'
+DATABASE_NAME = '<%= @database['database'] %>'
+DATABASE_USER = '<%= @database['username'] %>'
+DATABASE_PASSWORD = '<%= @database['password'] %>'
+DATABASE_HOST = '<%= @host %>'
+DATABASE_PORT = '<%= @database['port']  %>'
+<% end -%>

+ 2 - 0
templates/default/sv-gunicorn-log-run.erb

@@ -0,0 +1,2 @@
+#!/bin/sh
+exec svlogd -tt ./main

+ 10 - 0
templates/default/sv-gunicorn-run.erb

@@ -0,0 +1,10 @@
+#!/bin/bash
+# TODO make this more WSGI generic
+GUNICORN=<%= @options['virtualenv'] %>/bin/gunicorn_django
+ROOT=<%= @options['app']['deploy_to'] %>/current
+CONFIG=/etc/gunicorn/<%= @options['app']['id'] %>.py
+
+cd $ROOT
+
+exec 2>&1
+exec chpst -u <%= @options['app']['owner'] %>:<%= @options['app']['group'] %> $GUNICORN -c $CONFIG

Some files were not shown because too many files changed in this diff