├── mywebsite ├── __init__.py ├── deploy │ ├── crontab │ ├── gunicorn.conf.py │ ├── supervisor.conf │ ├── live_settings.py │ └── nginx.conf ├── requirements │ └── project.txt ├── manage.py ├── urls.py ├── settings.py └── fabfile.py ├── .gitignore └── README.md /mywebsite/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.db 3 | local_settings.py -------------------------------------------------------------------------------- /mywebsite/deploy/crontab: -------------------------------------------------------------------------------- 1 | */5 * * * * %(user)s %(manage)s poll_twitter 2 | -------------------------------------------------------------------------------- /mywebsite/requirements/project.txt: -------------------------------------------------------------------------------- 1 | Django==1.4.5 2 | Mezzanine==1.2.4 3 | -------------------------------------------------------------------------------- /mywebsite/deploy/gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | bind = "127.0.0.1:%(gunicorn_port)s" 4 | workers = (os.sysconf("SC_NPROCESSORS_ONLN") * 2) + 1 5 | loglevel = "error" 6 | proc_name = "%(proj_name)s" 7 | -------------------------------------------------------------------------------- /mywebsite/deploy/supervisor.conf: -------------------------------------------------------------------------------- 1 | [group:%(proj_name)s] 2 | programs=gunicorn_%(proj_name)s 3 | 4 | [program:gunicorn_%(proj_name)s] 5 | command=%(venv_path)s/bin/gunicorn_django -c gunicorn.conf.py -p gunicorn.pid 6 | directory=%(proj_path)s 7 | user=%(user)s 8 | autostart=true 9 | autorestart=true 10 | redirect_stderr=true 11 | -------------------------------------------------------------------------------- /mywebsite/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | 6 | 7 | # Corrects some pathing issues in various contexts, such as cron jobs, 8 | # and the project layout still being in Django 1.3 format. 9 | from settings import PROJECT_ROOT, PROJECT_DIRNAME 10 | os.chdir(PROJECT_ROOT) 11 | sys.path.insert(0, os.path.abspath(os.path.join(PROJECT_ROOT, ".."))) 12 | 13 | 14 | # Add the site ID CLI arg to the environment, which allows for the site 15 | # used in any site related queries to be manually set for management 16 | # commands. 17 | for i, arg in enumerate(sys.argv): 18 | if arg.startswith("--site"): 19 | os.environ["MEZZANINE_SITE_ID"] = arg.split("=")[1] 20 | sys.argv.pop(i) 21 | 22 | 23 | # Run Django. 24 | if __name__ == "__main__": 25 | settings_module = "%s.settings" % PROJECT_DIRNAME 26 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_module) 27 | from django.core.management import execute_from_command_line 28 | execute_from_command_line(sys.argv) 29 | -------------------------------------------------------------------------------- /mywebsite/deploy/live_settings.py: -------------------------------------------------------------------------------- 1 | 2 | DATABASES = { 3 | "default": { 4 | # Ends with "postgresql_psycopg2", "mysql", "sqlite3" or "oracle". 5 | "ENGINE": "django.db.backends.postgresql_psycopg2", 6 | # DB name or path to database file if using sqlite3. 7 | "NAME": "%(proj_name)s", 8 | # Not used with sqlite3. 9 | "USER": "%(proj_name)s", 10 | # Not used with sqlite3. 11 | "PASSWORD": "%(db_pass)s", 12 | # Set to empty string for localhost. Not used with sqlite3. 13 | "HOST": "127.0.0.1", 14 | # Set to empty string for default. Not used with sqlite3. 15 | "PORT": "", 16 | } 17 | } 18 | 19 | SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTOCOL", "https") 20 | 21 | CACHE_MIDDLEWARE_SECONDS = 60 22 | 23 | CACHES = { 24 | "default": { 25 | "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", 26 | "LOCATION": "127.0.0.1:11211", 27 | } 28 | } 29 | 30 | SESSION_ENGINE = "django.contrib.sessions.backends.cache" 31 | -------------------------------------------------------------------------------- /mywebsite/deploy/nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | upstream %(proj_name)s { 3 | server 127.0.0.1:%(gunicorn_port)s; 4 | } 5 | 6 | server { 7 | 8 | listen 80; 9 | server_name %(live_host)s; 10 | client_max_body_size 10M; 11 | keepalive_timeout 15; 12 | 13 | ssl on; 14 | ssl_certificate conf/%(proj_name)s.crt; 15 | ssl_certificate_key conf/%(proj_name)s.key; 16 | ssl_session_cache shared:SSL:10m; 17 | ssl_session_timeout 10m; 18 | 19 | location / { 20 | proxy_redirect off; 21 | proxy_set_header Host $host; 22 | proxy_set_header X-Real-IP $remote_addr; 23 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 24 | proxy_set_header X-Forwarded-Protocol $scheme; 25 | proxy_pass http://%(proj_name)s; 26 | } 27 | 28 | location /static/ { 29 | root %(proj_path)s; 30 | access_log off; 31 | log_not_found off; 32 | } 33 | 34 | location /robots.txt { 35 | root %(proj_path)s/static; 36 | access_log off; 37 | log_not_found off; 38 | } 39 | 40 | location /favicon.ico { 41 | root %(proj_path)s/static/img; 42 | access_log off; 43 | log_not_found off; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PaaS bakeoff 2 | ============ 3 | 4 | A [Mezzanine](http://mezzanine.jupo.org) project template that demonstrates how to deploy Django projects to [several PaaS providers](http://appsembler.com/blog/paas-bakeoff-comparing-stackato-openshift-dotcloud-and-heroku-for-django-hosting-and-deployment/) 5 | 6 | Usage 7 | ===== 8 | 9 | First clone the code repository: 10 | 11 | ``` 12 | $ git clone git://github.com/appsembler/paasbakeoff.git 13 | ``` 14 | 15 | See the different PaaS providers that you can deploy to by looking at the branches: 16 | 17 | ``` 18 | $ git branch 19 | * dotcloud 20 | heroku 21 | master 22 | openshift 23 | stackato 24 | ``` 25 | 26 | Then checkout the branch for the PaaS provider that you want to deploy to: 27 | 28 | Stackato 29 | -------- 30 | 31 | Download the Stackato [client](http://www.activestate.com/stackato/download_client) and create an account on their [sandbox](http://www.activestate.com/stackato/sandbox). 32 | 33 | ``` 34 | $ cd paasbakeoff 35 | $ git checkout stackato 36 | $ stackato push 37 | ``` 38 | 39 | See this [blog post](http://appsembler.com/blog/django-deployment-using-stackato/) for more details about deploying Mezzanine to Stackato. 40 | 41 | OpenShift 42 | --------- 43 | 44 | Download the OpenShift 'rhc' client and create an account on [OpenShift](http://openshift.redhat.com/). 45 | 46 | ``` 47 | $ rhc create -a mymezzanine -t python-2.6 48 | $ rhc app cartridge add -c mysql-5.1 -a mymezzanine 49 | $ cd mezzanine 50 | $ git remote add paasbakeoff git://github.com/appsembler/paasbakeoff.git 51 | $ git fetch paasbakeoff 52 | $ git merge paasbakeoff/openshift 53 | $ git push 54 | ``` 55 | 56 | See this [blog post](http://appsembler.com/blog/django-deployment-using-openshift/) for more details about deploying Mezzanine to OpenShift. 57 | 58 | Dotcloud 59 | -------- 60 | 61 | Pip install the dotcloud client and create an account on [Dotcloud](http://dotcloud.com) 62 | 63 | ``` 64 | $ cd paasbakeoff 65 | $ git checkout dotcloud 66 | $ dotcloud create mymezzanine 67 | $ dotcloud push 68 | ``` 69 | 70 | Heroku 71 | ------ 72 | 73 | Download the [Heroku toolbelt](https://toolbelt.heroku.com/) and create an account on [Heroku](http://heroku.com). 74 | 75 | ``` 76 | $ cd paasbakeoff 77 | $ git checkout heroku 78 | $ heroku create myapp 79 | $ heroku addons:add heroku-postgresql:dev 80 | $ git push heroku heroku:master 81 | $ heroku run python mywebsite/manage.py syncdb 82 | $ heroku run python mywebsite/manage.py collectstatic 83 | ``` 84 | 85 | 86 | If you would like to contribute another PaaS (Gondor, AWS Elastic Beanstalk, Google App Engine, etc), please fork the project and submit a pull request. Thanks! 87 | 88 | -------------------------------------------------------------------------------- /mywebsite/urls.py: -------------------------------------------------------------------------------- 1 | 2 | from django.conf.urls.defaults import patterns, include, url 3 | from django.contrib import admin 4 | 5 | from mezzanine.core.views import direct_to_template 6 | 7 | 8 | admin.autodiscover() 9 | 10 | # Add the urlpatterns for any custom Django applications here. 11 | # You can also change the ``home`` view to add your own functionality 12 | # to the project's homepage. 13 | 14 | urlpatterns = patterns("", 15 | 16 | # Change the admin prefix here to use an alternate URL for the 17 | # admin interface, which would be marginally more secure. 18 | ("^admin/", include(admin.site.urls)), 19 | 20 | # We don't want to presume how your homepage works, so here are a 21 | # few patterns you can use to set it up. 22 | 23 | # HOMEPAGE AS STATIC TEMPLATE 24 | # --------------------------- 25 | # This pattern simply loads the index.html template. It isn't 26 | # commented out like the others, so it's the default. You only need 27 | # one homepage pattern, so if you use a different one, comment this 28 | # one out. 29 | 30 | url("^$", direct_to_template, {"template": "index.html"}, name="home"), 31 | 32 | # HOMEPAGE AS AN EDITABLE PAGE IN THE PAGE TREE 33 | # --------------------------------------------- 34 | # This pattern gives us a normal ``Page`` object, so that your 35 | # homepage can be managed via the page tree in the admin. If you 36 | # use this pattern, you'll need to create a page in the page tree, 37 | # and specify its URL (in the Meta Data section) as "/", which 38 | # is the value used below in the ``{"slug": "/"}`` part. Make 39 | # sure to uncheck "show in navigation" when you create the page, 40 | # since the link to the homepage is always hard-coded into all the 41 | # page menus that display navigation on the site. Also note that 42 | # the normal rule of adding a custom template per page with the 43 | # template name using the page's slug doesn't apply here, since 44 | # we can't have a template called "/.html" - so for this case, the 45 | # template "pages/index.html" can be used. 46 | 47 | # url("^$", "mezzanine.pages.views.page", {"slug": "/"}, name="home"), 48 | 49 | # HOMEPAGE FOR A BLOG-ONLY SITE 50 | # ----------------------------- 51 | # This pattern points the homepage to the blog post listing page, 52 | # and is useful for sites that are primarily blogs. If you use this 53 | # pattern, you'll also need to set BLOG_SLUG = "" in your 54 | # ``settings.py`` module, and delete the blog page object from the 55 | # page tree in the admin if it was installed. 56 | 57 | # url("^$", "mezzanine.blog.views.blog_post_list", name="home"), 58 | 59 | # MEZZANINE'S URLS 60 | # ---------------- 61 | # ADD YOUR OWN URLPATTERNS *ABOVE* THE LINE BELOW. 62 | # ``mezzanine.urls`` INCLUDES A *CATCH ALL* PATTERN 63 | # FOR PAGES, SO URLPATTERNS ADDED BELOW ``mezzanine.urls`` 64 | # WILL NEVER BE MATCHED! 65 | 66 | # If you'd like more granular control over the patterns in 67 | # ``mezzanine.urls``, go right ahead and take the parts you want 68 | # from it, and use them directly below instead of using 69 | # ``mezzanine.urls``. 70 | ("^", include("mezzanine.urls")), 71 | 72 | # MOUNTING MEZZANINE UNDER A PREFIX 73 | # --------------------------------- 74 | # You can also mount all of Mezzanine's urlpatterns under a 75 | # URL prefix if desired. When doing this, you need to define the 76 | # ``SITE_PREFIX`` setting, which will contain the prefix. Eg: 77 | # SITE_PREFIX = "my/site/prefix" 78 | # For convenience, and to avoid repeating the prefix, use the 79 | # commented out pattern below (commenting out the one above of course) 80 | # which will make use of the ``SITE_PREFIX`` setting. Make sure to 81 | # add the import ``from django.conf import settings`` to the top 82 | # of this file as well. 83 | # Note that for any of the various homepage patterns above, you'll 84 | # need to use the ``SITE_PREFIX`` setting as well. 85 | 86 | # ("^%s/" % settings.SITE_PREFIX, include("mezzanine.urls")) 87 | 88 | ) 89 | 90 | # Adds ``STATIC_URL`` to the context of error pages, so that error 91 | # pages can use JS, CSS and images. 92 | handler500 = "mezzanine.core.views.server_error" 93 | -------------------------------------------------------------------------------- /mywebsite/settings.py: -------------------------------------------------------------------------------- 1 | 2 | ###################### 3 | # MEZZANINE SETTINGS # 4 | ###################### 5 | 6 | # The following settings are already defined with default values in 7 | # the ``defaults.py`` module within each of Mezzanine's apps, but are 8 | # common enough to be put here, commented out, for convenient 9 | # overriding. Please consult the settings documentation for a full list 10 | # of settings Mezzanine implements: 11 | # http://mezzanine.jupo.org/docs/configuration.html#default-settings 12 | 13 | # Controls the ordering and grouping of the admin menu. 14 | # 15 | # ADMIN_MENU_ORDER = ( 16 | # ("Content", ("pages.Page", "blog.BlogPost", 17 | # "generic.ThreadedComment", ("Media Library", "fb_browse"),)), 18 | # ("Site", ("sites.Site", "redirects.Redirect", "conf.Setting")), 19 | # ("Users", ("auth.User", "auth.Group",)), 20 | # ) 21 | 22 | # A three item sequence, each containing a sequence of template tags 23 | # used to render the admin dashboard. 24 | # 25 | # DASHBOARD_TAGS = ( 26 | # ("blog_tags.quick_blog", "mezzanine_tags.app_list"), 27 | # ("comment_tags.recent_comments",), 28 | # ("mezzanine_tags.recent_actions",), 29 | # ) 30 | 31 | # A sequence of templates used by the ``page_menu`` template tag. Each 32 | # item in the sequence is a three item sequence, containing a unique ID 33 | # for the template, a label for the template, and the template path. 34 | # These templates are then available for selection when editing which 35 | # menus a page should appear in. Note that if a menu template is used 36 | # that doesn't appear in this setting, all pages will appear in it. 37 | 38 | # PAGE_MENU_TEMPLATES = ( 39 | # (1, "Top navigation bar", "pages/menus/dropdown.html"), 40 | # (2, "Left-hand tree", "pages/menus/tree.html"), 41 | # (3, "Footer", "pages/menus/footer.html"), 42 | # ) 43 | 44 | # A sequence of fields that will be injected into Mezzanine's (or any 45 | # library's) models. Each item in the sequence is a four item sequence. 46 | # The first two items are the dotted path to the model and its field 47 | # name to be added, and the dotted path to the field class to use for 48 | # the field. The third and fourth items are a sequence of positional 49 | # args and a dictionary of keyword args, to use when creating the 50 | # field instance. When specifying the field class, the path 51 | # ``django.models.db.`` can be omitted for regular Django model fields. 52 | # 53 | # EXTRA_MODEL_FIELDS = ( 54 | # ( 55 | # # Dotted path to field. 56 | # "mezzanine.blog.models.BlogPost.image", 57 | # # Dotted path to field class. 58 | # "somelib.fields.ImageField", 59 | # # Positional args for field class. 60 | # ("Image",), 61 | # # Keyword args for field class. 62 | # {"blank": True, "upload_to": "blog"}, 63 | # ), 64 | # # Example of adding a field to *all* of Mezzanine's content types: 65 | # ( 66 | # "mezzanine.pages.models.Page.another_field", 67 | # "IntegerField", # 'django.db.models.' is implied if path is omitted. 68 | # ("Another name",), 69 | # {"blank": True, "default": 1}, 70 | # ), 71 | # ) 72 | 73 | # Setting to turn on featured images for blog posts. Defaults to False. 74 | # 75 | # BLOG_USE_FEATURED_IMAGE = True 76 | 77 | # If ``True``, users will be automatically redirected to HTTPS 78 | # for the URLs specified by the ``SSL_FORCE_URL_PREFIXES`` setting. 79 | # 80 | # SSL_ENABLED = True 81 | 82 | # Host name that the site should always be accessed via that matches 83 | # the SSL certificate. 84 | # 85 | # SSL_FORCE_HOST = "www.example.com" 86 | 87 | # Sequence of URL prefixes that will be forced to run over 88 | # SSL when ``SSL_ENABLED`` is ``True``. i.e. 89 | # ('/admin', '/example') would force all URLs beginning with 90 | # /admin or /example to run over SSL. Defaults to: 91 | # 92 | # SSL_FORCE_URL_PREFIXES = ("/admin", "/account") 93 | 94 | # If True, the south application will be automatically added to the 95 | # INSTALLED_APPS setting. 96 | USE_SOUTH = True 97 | 98 | 99 | ######################## 100 | # MAIN DJANGO SETTINGS # 101 | ######################## 102 | 103 | # People who get code error notifications. 104 | # In the format (('Full Name', 'email@example.com'), 105 | # ('Full Name', 'anotheremail@example.com')) 106 | ADMINS = ( 107 | # ('Your Name', 'your_email@domain.com'), 108 | ) 109 | MANAGERS = ADMINS 110 | 111 | # Local time zone for this installation. Choices can be found here: 112 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 113 | # although not all choices may be available on all operating systems. 114 | # On Unix systems, a value of None will cause Django to use the same 115 | # timezone as the operating system. 116 | # If running in a Windows environment this must be set to the same as your 117 | # system time zone. 118 | TIME_ZONE = None 119 | 120 | # If you set this to True, Django will use timezone-aware datetimes. 121 | USE_TZ = True 122 | 123 | # Language code for this installation. All choices can be found here: 124 | # http://www.i18nguy.com/unicode/language-identifiers.html 125 | LANGUAGE_CODE = "en" 126 | 127 | # A boolean that turns on/off debug mode. When set to ``True``, stack traces 128 | # are displayed for error pages. Should always be set to ``False`` in 129 | # production. Best set to ``True`` in local_settings.py 130 | DEBUG = False 131 | 132 | # Whether a user's session cookie expires when the Web browser is closed. 133 | SESSION_EXPIRE_AT_BROWSER_CLOSE = True 134 | 135 | SITE_ID = 1 136 | 137 | # If you set this to False, Django will make some optimizations so as not 138 | # to load the internationalization machinery. 139 | USE_I18N = False 140 | 141 | # Make this unique, and don't share it with anybody. 142 | SECRET_KEY = "7ee22c5b-0ec7-43ee-88bd-183664dc099f3430e495-2580-4347-be78-4bb72cbe92efacf6b627-56c4-4fcf-99c3-e5566203b7fc" 143 | 144 | # Tuple of IP addresses, as strings, that: 145 | # * See debug comments, when DEBUG is true 146 | # * Receive x-headers 147 | INTERNAL_IPS = ("127.0.0.1",) 148 | 149 | # List of callables that know how to import templates from various sources. 150 | TEMPLATE_LOADERS = ( 151 | "django.template.loaders.filesystem.Loader", 152 | "django.template.loaders.app_directories.Loader", 153 | ) 154 | 155 | AUTHENTICATION_BACKENDS = ("mezzanine.core.auth_backends.MezzanineBackend",) 156 | 157 | # List of finder classes that know how to find static files in 158 | # various locations. 159 | STATICFILES_FINDERS = ( 160 | "django.contrib.staticfiles.finders.FileSystemFinder", 161 | "django.contrib.staticfiles.finders.AppDirectoriesFinder", 162 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 163 | ) 164 | 165 | 166 | ############# 167 | # DATABASES # 168 | ############# 169 | 170 | DATABASES = { 171 | "default": { 172 | # Add "postgresql_psycopg2", "mysql", "sqlite3" or "oracle". 173 | "ENGINE": "django.db.backends.", 174 | # DB name or path to database file if using sqlite3. 175 | "NAME": "", 176 | # Not used with sqlite3. 177 | "USER": "", 178 | # Not used with sqlite3. 179 | "PASSWORD": "", 180 | # Set to empty string for localhost. Not used with sqlite3. 181 | "HOST": "", 182 | # Set to empty string for default. Not used with sqlite3. 183 | "PORT": "", 184 | } 185 | } 186 | 187 | 188 | ######### 189 | # PATHS # 190 | ######### 191 | 192 | import os 193 | 194 | # Full filesystem path to the project. 195 | PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) 196 | 197 | # Name of the directory for the project. 198 | PROJECT_DIRNAME = PROJECT_ROOT.split(os.sep)[-1] 199 | 200 | # Every cache key will get prefixed with this value - here we set it to 201 | # the name of the directory the project is in to try and use something 202 | # project specific. 203 | CACHE_MIDDLEWARE_KEY_PREFIX = PROJECT_DIRNAME 204 | 205 | # URL prefix for static files. 206 | # Example: "http://media.lawrence.com/static/" 207 | STATIC_URL = "/static/" 208 | 209 | # Absolute path to the directory static files should be collected to. 210 | # Don't put anything in this directory yourself; store your static files 211 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 212 | # Example: "/home/media/media.lawrence.com/static/" 213 | STATIC_ROOT = os.path.join(PROJECT_ROOT, STATIC_URL.strip("/")) 214 | 215 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 216 | # trailing slash. 217 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 218 | MEDIA_URL = STATIC_URL + "media/" 219 | 220 | # Absolute filesystem path to the directory that will hold user-uploaded files. 221 | # Example: "/home/media/media.lawrence.com/media/" 222 | MEDIA_ROOT = os.path.join(PROJECT_ROOT, *MEDIA_URL.strip("/").split("/")) 223 | 224 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 225 | # trailing slash. 226 | # Examples: "http://foo.com/media/", "/media/". 227 | ADMIN_MEDIA_PREFIX = STATIC_URL + "grappelli/" 228 | 229 | # Package/module name to import the root urlpatterns from for the project. 230 | ROOT_URLCONF = "%s.urls" % PROJECT_DIRNAME 231 | 232 | # Put strings here, like "/home/html/django_templates" 233 | # or "C:/www/django/templates". 234 | # Always use forward slashes, even on Windows. 235 | # Don't forget to use absolute paths, not relative paths. 236 | TEMPLATE_DIRS = (os.path.join(PROJECT_ROOT, "templates"),) 237 | 238 | 239 | ################ 240 | # APPLICATIONS # 241 | ################ 242 | 243 | INSTALLED_APPS = ( 244 | "django.contrib.admin", 245 | "django.contrib.auth", 246 | "django.contrib.contenttypes", 247 | "django.contrib.redirects", 248 | "django.contrib.sessions", 249 | "django.contrib.sites", 250 | "django.contrib.sitemaps", 251 | "django.contrib.staticfiles", 252 | "mezzanine.boot", 253 | "mezzanine.conf", 254 | "mezzanine.core", 255 | "mezzanine.generic", 256 | "mezzanine.blog", 257 | "mezzanine.forms", 258 | "mezzanine.pages", 259 | "mezzanine.galleries", 260 | "mezzanine.twitter", 261 | #"mezzanine.accounts", 262 | #"mezzanine.mobile", 263 | ) 264 | 265 | # List of processors used by RequestContext to populate the context. 266 | # Each one should be a callable that takes the request object as its 267 | # only parameter and returns a dictionary to add to the context. 268 | TEMPLATE_CONTEXT_PROCESSORS = ( 269 | "django.contrib.auth.context_processors.auth", 270 | "django.contrib.messages.context_processors.messages", 271 | "django.core.context_processors.debug", 272 | "django.core.context_processors.i18n", 273 | "django.core.context_processors.static", 274 | "django.core.context_processors.media", 275 | "django.core.context_processors.request", 276 | "django.core.context_processors.tz", 277 | "mezzanine.conf.context_processors.settings", 278 | ) 279 | 280 | # List of middleware classes to use. Order is important; in the request phase, 281 | # these middleware classes will be applied in the order given, and in the 282 | # response phase the middleware will be applied in reverse order. 283 | MIDDLEWARE_CLASSES = ( 284 | "mezzanine.core.middleware.UpdateCacheMiddleware", 285 | "django.contrib.sessions.middleware.SessionMiddleware", 286 | "django.contrib.auth.middleware.AuthenticationMiddleware", 287 | "django.contrib.redirects.middleware.RedirectFallbackMiddleware", 288 | "django.middleware.common.CommonMiddleware", 289 | "django.middleware.csrf.CsrfViewMiddleware", 290 | "django.contrib.messages.middleware.MessageMiddleware", 291 | "mezzanine.core.request.CurrentRequestMiddleware", 292 | "mezzanine.core.middleware.TemplateForDeviceMiddleware", 293 | "mezzanine.core.middleware.TemplateForHostMiddleware", 294 | "mezzanine.core.middleware.AdminLoginInterfaceSelectorMiddleware", 295 | # Uncomment the following if using any of the SSL settings: 296 | # "mezzanine.core.middleware.SSLRedirectMiddleware", 297 | "mezzanine.pages.middleware.PageMiddleware", 298 | "mezzanine.core.middleware.FetchFromCacheMiddleware", 299 | ) 300 | 301 | # Store these package names here as they may change in the future since 302 | # at the moment we are using custom forks of them. 303 | PACKAGE_NAME_FILEBROWSER = "filebrowser_safe" 304 | PACKAGE_NAME_GRAPPELLI = "grappelli_safe" 305 | 306 | 307 | ######################### 308 | # OPTIONAL APPLICATIONS # 309 | ######################### 310 | 311 | # These will be added to ``INSTALLED_APPS``, only if available. 312 | OPTIONAL_APPS = ( 313 | "debug_toolbar", 314 | "django_extensions", 315 | "compressor", 316 | PACKAGE_NAME_FILEBROWSER, 317 | PACKAGE_NAME_GRAPPELLI, 318 | ) 319 | 320 | DEBUG_TOOLBAR_CONFIG = {"INTERCEPT_REDIRECTS": False} 321 | 322 | ################### 323 | # DEPLOY SETTINGS # 324 | ################### 325 | 326 | # These settings are used by the default fabfile.py provided. 327 | # Check fabfile.py for defaults. 328 | 329 | # FABRIC = { 330 | # "SSH_USER": "", # SSH username 331 | # "SSH_PASS": "", # SSH password (consider key-based authentication) 332 | # "SSH_KEY_PATH": "", # Local path to SSH key file, for key-based auth 333 | # "HOSTS": [], # List of hosts to deploy to 334 | # "VIRTUALENV_HOME": "", # Absolute remote path for virtualenvs 335 | # "PROJECT_NAME": "", # Unique identifier for project 336 | # "REQUIREMENTS_PATH": "", # Path to pip requirements, relative to project 337 | # "GUNICORN_PORT": 8000, # Port gunicorn will listen on 338 | # "LOCALE": "en_US.UTF-8", # Should end with ".UTF-8" 339 | # "LIVE_HOSTNAME": "www.example.com", # Host for public site. 340 | # "REPO_URL": "", # Git or Mercurial remote repo URL for the project 341 | # "DB_PASS": "", # Live database password 342 | # "ADMIN_PASS": "", # Live admin user password 343 | # } 344 | 345 | 346 | ################## 347 | # LOCAL SETTINGS # 348 | ################## 349 | 350 | # Allow any settings to be defined in local_settings.py which should be 351 | # ignored in your version control system allowing for settings to be 352 | # defined per machine. 353 | try: 354 | from local_settings import * 355 | except ImportError: 356 | pass 357 | 358 | 359 | #################### 360 | # DYNAMIC SETTINGS # 361 | #################### 362 | 363 | # set_dynamic_settings() will rewrite globals based on what has been 364 | # defined so far, in order to provide some better defaults where 365 | # applicable. We also allow this settings module to be imported 366 | # without Mezzanine installed, as the case may be when using the 367 | # fabfile, where setting the dynamic settings below isn't strictly 368 | # required. 369 | try: 370 | from mezzanine.utils.conf import set_dynamic_settings 371 | except ImportError: 372 | pass 373 | else: 374 | set_dynamic_settings(globals()) 375 | -------------------------------------------------------------------------------- /mywebsite/fabfile.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | from functools import wraps 5 | from getpass import getpass, getuser 6 | from glob import glob 7 | from contextlib import contextmanager 8 | 9 | from fabric.api import env, cd, prefix, sudo as _sudo, run as _run, hide, task 10 | from fabric.contrib.files import exists, upload_template 11 | from fabric.colors import yellow, green, blue, red 12 | 13 | 14 | ################ 15 | # Config setup # 16 | ################ 17 | 18 | conf = {} 19 | if sys.argv[0].split(os.sep)[-1] == "fab": 20 | # Ensure we import settings from the current dir 21 | try: 22 | conf = __import__("settings", globals(), locals(), [], 0).FABRIC 23 | try: 24 | conf["HOSTS"][0] 25 | except (KeyError, ValueError): 26 | raise ImportError 27 | except (ImportError, AttributeError): 28 | print "Aborting, no hosts defined." 29 | exit() 30 | 31 | env.db_pass = conf.get("DB_PASS", None) 32 | env.admin_pass = conf.get("ADMIN_PASS", None) 33 | env.user = conf.get("SSH_USER", getuser()) 34 | env.password = conf.get("SSH_PASS", None) 35 | env.key_filename = conf.get("SSH_KEY_PATH", None) 36 | env.hosts = conf.get("HOSTS", []) 37 | 38 | env.proj_name = conf.get("PROJECT_NAME", os.getcwd().split(os.sep)[-1]) 39 | env.venv_home = conf.get("VIRTUALENV_HOME", "/home/%s" % env.user) 40 | env.venv_path = "%s/%s" % (env.venv_home, env.proj_name) 41 | env.proj_dirname = "project" 42 | env.proj_path = "%s/%s" % (env.venv_path, env.proj_dirname) 43 | env.manage = "%s/bin/python %s/project/manage.py" % (env.venv_path, 44 | env.venv_path) 45 | env.live_host = conf.get("LIVE_HOSTNAME", env.hosts[0] if env.hosts else None) 46 | env.repo_url = conf.get("REPO_URL", None) 47 | env.reqs_path = conf.get("REQUIREMENTS_PATH", None) 48 | env.gunicorn_port = conf.get("GUNICORN_PORT", 8000) 49 | env.locale = conf.get("LOCALE", "en_US.UTF-8") 50 | 51 | 52 | ################## 53 | # Template setup # 54 | ################## 55 | 56 | # Each template gets uploaded at deploy time, only if their 57 | # contents has changed, in which case, the reload command is 58 | # also run. 59 | 60 | templates = { 61 | "nginx": { 62 | "local_path": "deploy/nginx.conf", 63 | "remote_path": "/etc/nginx/sites-enabled/%(proj_name)s.conf", 64 | "reload_command": "service nginx restart", 65 | }, 66 | "supervisor": { 67 | "local_path": "deploy/supervisor.conf", 68 | "remote_path": "/etc/supervisor/conf.d/%(proj_name)s.conf", 69 | "reload_command": "supervisorctl reload", 70 | }, 71 | "cron": { 72 | "local_path": "deploy/crontab", 73 | "remote_path": "/etc/cron.d/%(proj_name)s", 74 | "owner": "root", 75 | "mode": "600", 76 | }, 77 | "gunicorn": { 78 | "local_path": "deploy/gunicorn.conf.py", 79 | "remote_path": "%(proj_path)s/gunicorn.conf.py", 80 | }, 81 | "settings": { 82 | "local_path": "deploy/live_settings.py", 83 | "remote_path": "%(proj_path)s/local_settings.py", 84 | }, 85 | } 86 | 87 | 88 | ###################################### 89 | # Context for virtualenv and project # 90 | ###################################### 91 | 92 | @contextmanager 93 | def virtualenv(): 94 | """ 95 | Runs commands within the project's virtualenv. 96 | """ 97 | with cd(env.venv_path): 98 | with prefix("source %s/bin/activate" % env.venv_path): 99 | yield 100 | 101 | 102 | @contextmanager 103 | def project(): 104 | """ 105 | Runs commands within the project's directory. 106 | """ 107 | with virtualenv(): 108 | with cd(env.proj_dirname): 109 | yield 110 | 111 | 112 | @contextmanager 113 | def update_changed_requirements(): 114 | """ 115 | Checks for changes in the requirements file across an update, 116 | and gets new requirements if changes have occurred. 117 | """ 118 | reqs_path = os.path.join(env.proj_path, env.reqs_path) 119 | get_reqs = lambda: run("cat %s" % reqs_path, show=False) 120 | old_reqs = get_reqs() if env.reqs_path else "" 121 | yield 122 | if old_reqs: 123 | new_reqs = get_reqs() 124 | if old_reqs == new_reqs: 125 | # Unpinned requirements should always be checked. 126 | for req in new_reqs.split("\n"): 127 | if req.startswith("-e"): 128 | if "@" not in req: 129 | # Editable requirement without pinned commit. 130 | break 131 | elif req.strip() and not req.startswith("#"): 132 | if not set(">=<") & set(req): 133 | # PyPI requirement without version. 134 | break 135 | else: 136 | # All requirements are pinned. 137 | return 138 | pip("-r %s/%s" % (env.proj_path, env.reqs_path)) 139 | 140 | 141 | ########################################### 142 | # Utils and wrappers for various commands # 143 | ########################################### 144 | 145 | def _print(output): 146 | print 147 | print output 148 | print 149 | 150 | 151 | def print_command(command): 152 | _print(blue("$ ", bold=True) + 153 | yellow(command, bold=True) + 154 | red(" ->", bold=True)) 155 | 156 | 157 | @task 158 | def run(command, show=True): 159 | """ 160 | Runs a shell comand on the remote server. 161 | """ 162 | if show: 163 | print_command(command) 164 | with hide("running"): 165 | return _run(command) 166 | 167 | 168 | @task 169 | def sudo(command, show=True): 170 | """ 171 | Runs a command as sudo. 172 | """ 173 | if show: 174 | print_command(command) 175 | with hide("running"): 176 | return _sudo(command) 177 | 178 | 179 | def log_call(func): 180 | @wraps(func) 181 | def logged(*args, **kawrgs): 182 | header = "-" * len(func.__name__) 183 | _print(green("\n".join([header, func.__name__, header]), bold=True)) 184 | return func(*args, **kawrgs) 185 | return logged 186 | 187 | 188 | def get_templates(): 189 | """ 190 | Returns each of the templates with env vars injected. 191 | """ 192 | injected = {} 193 | for name, data in templates.items(): 194 | injected[name] = dict([(k, v % env) for k, v in data.items()]) 195 | return injected 196 | 197 | 198 | def upload_template_and_reload(name): 199 | """ 200 | Uploads a template only if it has changed, and if so, reload a 201 | related service. 202 | """ 203 | template = get_templates()[name] 204 | local_path = template["local_path"] 205 | remote_path = template["remote_path"] 206 | reload_command = template.get("reload_command") 207 | owner = template.get("owner") 208 | mode = template.get("mode") 209 | remote_data = "" 210 | if exists(remote_path): 211 | with hide("stdout"): 212 | remote_data = sudo("cat %s" % remote_path, show=False) 213 | with open(local_path, "r") as f: 214 | local_data = f.read() 215 | if "%(db_pass)s" in local_data: 216 | env.db_pass = db_pass() 217 | local_data %= env 218 | clean = lambda s: s.replace("\n", "").replace("\r", "").strip() 219 | if clean(remote_data) == clean(local_data): 220 | return 221 | upload_template(local_path, remote_path, env, use_sudo=True, backup=False) 222 | if owner: 223 | sudo("chown %s %s" % (owner, remote_path)) 224 | if mode: 225 | sudo("chmod %s %s" % (mode, remote_path)) 226 | if reload_command: 227 | sudo(reload_command) 228 | 229 | 230 | def db_pass(): 231 | """ 232 | Prompts for the database password if unknown. 233 | """ 234 | if not env.db_pass: 235 | env.db_pass = getpass("Enter the database password: ") 236 | return env.db_pass 237 | 238 | 239 | @task 240 | def apt(packages): 241 | """ 242 | Installs one or more system packages via apt. 243 | """ 244 | return sudo("apt-get install -y -q " + packages) 245 | 246 | 247 | @task 248 | def pip(packages): 249 | """ 250 | Installs one or more Python packages within the virtual environment. 251 | """ 252 | with virtualenv(): 253 | return sudo("pip install %s" % packages) 254 | 255 | 256 | def postgres(command): 257 | """ 258 | Runs the given command as the postgres user. 259 | """ 260 | show = not command.startswith("psql") 261 | return run("sudo -u root sudo -u postgres %s" % command, show=show) 262 | 263 | 264 | @task 265 | def psql(sql, show=True): 266 | """ 267 | Runs SQL against the project's database. 268 | """ 269 | out = postgres('psql -c "%s"' % sql) 270 | if show: 271 | print_command(sql) 272 | return out 273 | 274 | 275 | @task 276 | def backup(filename): 277 | """ 278 | Backs up the database. 279 | """ 280 | return postgres("pg_dump -Fc %s > %s" % (env.proj_name, filename)) 281 | 282 | 283 | @task 284 | def restore(filename): 285 | """ 286 | Restores the database. 287 | """ 288 | return postgres("pg_restore -c -d %s %s" % (env.proj_name, filename)) 289 | 290 | 291 | @task 292 | def python(code, show=True): 293 | """ 294 | Runs Python code in the project's virtual environment, with Django loaded. 295 | """ 296 | setup = "import os; os.environ[\'DJANGO_SETTINGS_MODULE\']=\'settings\';" 297 | with project(): 298 | return run('python -c "%s%s"' % (setup, code), show=False) 299 | if show: 300 | print_command(code) 301 | 302 | 303 | def static(): 304 | """ 305 | Returns the live STATIC_ROOT directory. 306 | """ 307 | return python("from django.conf import settings;" 308 | "print settings.STATIC_ROOT") 309 | 310 | 311 | @task 312 | def manage(command): 313 | """ 314 | Runs a Django management command. 315 | """ 316 | return run("%s %s" % (env.manage, command)) 317 | 318 | 319 | ######################### 320 | # Install and configure # 321 | ######################### 322 | 323 | @task 324 | @log_call 325 | def install(): 326 | """ 327 | Installs the base system and Python requirements for the entire server. 328 | """ 329 | locale = "LC_ALL=%s" % env.locale 330 | with hide("stdout"): 331 | if locale not in sudo("cat /etc/default/locale"): 332 | sudo("update-locale %s" % locale) 333 | run("exit") 334 | sudo("apt-get update -y -q") 335 | apt("nginx libjpeg-dev python-dev python-setuptools git-core " 336 | "postgresql libpq-dev memcached supervisor") 337 | sudo("easy_install pip") 338 | sudo("pip install virtualenv mercurial") 339 | 340 | 341 | @task 342 | @log_call 343 | def create(): 344 | """ 345 | Create a new virtual environment for a project. 346 | Pulls the project's repo from version control, adds system-level 347 | configs for the project, and initialises the database with the 348 | live host. 349 | """ 350 | 351 | # Create virtualenv 352 | with cd(env.venv_home): 353 | if exists(env.proj_name): 354 | prompt = raw_input("\nVirtualenv exists: %s\nWould you like " 355 | "to replace it? (yes/no) " % env.proj_name) 356 | if prompt.lower() != "yes": 357 | print "\nAborting!" 358 | return False 359 | remove() 360 | run("virtualenv %s --distribute" % env.proj_name) 361 | vcs = "git" if env.repo_url.startswith("git") else "hg" 362 | run("%s clone %s %s" % (vcs, env.repo_url, env.proj_path)) 363 | 364 | # Create DB and DB user. 365 | pw = db_pass() 366 | user_sql_args = (env.proj_name, pw.replace("'", "\'")) 367 | user_sql = "CREATE USER %s WITH ENCRYPTED PASSWORD '%s';" % user_sql_args 368 | psql(user_sql, show=False) 369 | shadowed = "*" * len(pw) 370 | print_command(user_sql.replace("'%s'" % pw, "'%s'" % shadowed)) 371 | psql("CREATE DATABASE %s WITH OWNER %s ENCODING = 'UTF8' " 372 | "LC_CTYPE = '%s' LC_COLLATE = '%s' TEMPLATE template0;" % 373 | (env.proj_name, env.proj_name, env.locale, env.locale)) 374 | 375 | # Set up SSL certificate. 376 | conf_path = "/etc/nginx/conf" 377 | if not exists(conf_path): 378 | sudo("mkdir %s" % conf_path) 379 | with cd(conf_path): 380 | crt_file = env.proj_name + ".crt" 381 | key_file = env.proj_name + ".key" 382 | if not exists(crt_file) and not exists(key_file): 383 | try: 384 | crt_local, = glob(os.path.join("deploy", "*.crt")) 385 | key_local, = glob(os.path.join("deploy", "*.key")) 386 | except ValueError: 387 | parts = (crt_file, key_file, env.live_host) 388 | sudo("openssl req -new -x509 -nodes -out %s -keyout %s " 389 | "-subj '/CN=%s' -days 3650" % parts) 390 | else: 391 | upload_template(crt_file, crt_local, use_sudo=True) 392 | upload_template(key_file, key_local, use_sudo=True) 393 | 394 | # Set up project. 395 | upload_template_and_reload("settings") 396 | with project(): 397 | if env.reqs_path: 398 | pip("-r %s/%s" % (env.proj_path, env.reqs_path)) 399 | pip("gunicorn setproctitle south psycopg2 " 400 | "django-compressor python-memcached") 401 | manage("createdb --noinput") 402 | python("from django.conf import settings;" 403 | "from django.contrib.sites.models import Site;" 404 | "site, _ = Site.objects.get_or_create(id=settings.SITE_ID);" 405 | "site.domain = '" + env.live_host + "';" 406 | "site.save();") 407 | if env.admin_pass: 408 | pw = env.admin_pass 409 | user_py = ("from django.contrib.auth.models import User;" 410 | "u, _ = User.objects.get_or_create(username='admin');" 411 | "u.is_staff = u.is_superuser = True;" 412 | "u.set_password('%s');" 413 | "u.save();" % pw) 414 | python(user_py, show=False) 415 | shadowed = "*" * len(pw) 416 | print_command(user_py.replace("'%s'" % pw, "'%s'" % shadowed)) 417 | 418 | return True 419 | 420 | 421 | @task 422 | @log_call 423 | def remove(): 424 | """ 425 | Blow away the current project. 426 | """ 427 | if exists(env.venv_path): 428 | sudo("rm -rf %s" % env.venv_path) 429 | for template in get_templates().values(): 430 | remote_path = template["remote_path"] 431 | if exists(remote_path): 432 | sudo("rm %s" % remote_path) 433 | psql("DROP DATABASE %s;" % env.proj_name) 434 | psql("DROP USER %s;" % env.proj_name) 435 | 436 | 437 | ############## 438 | # Deployment # 439 | ############## 440 | 441 | @task 442 | @log_call 443 | def restart(): 444 | """ 445 | Restart gunicorn worker processes for the project. 446 | """ 447 | pid_path = "%s/gunicorn.pid" % env.proj_path 448 | if exists(pid_path): 449 | sudo("kill -HUP `cat %s`" % pid_path) 450 | else: 451 | start_args = (env.proj_name, env.proj_name) 452 | sudo("supervisorctl start %s:gunicorn_%s" % start_args) 453 | 454 | 455 | @task 456 | @log_call 457 | def deploy(): 458 | """ 459 | Deploy latest version of the project. 460 | Check out the latest version of the project from version 461 | control, install new requirements, sync and migrate the database, 462 | collect any new static assets, and restart gunicorn's work 463 | processes for the project. 464 | """ 465 | if not exists(env.venv_path): 466 | prompt = raw_input("\nVirtualenv doesn't exist: %s\nWould you like " 467 | "to create it? (yes/no) " % env.proj_name) 468 | if prompt.lower() != "yes": 469 | print "\nAborting!" 470 | return False 471 | create() 472 | for name in get_templates(): 473 | upload_template_and_reload(name) 474 | with project(): 475 | backup("last.db") 476 | run("tar -cf last.tar %s" % static()) 477 | git = env.repo_url.startswith("git") 478 | run("%s > last.commit" % "git rev-parse HEAD" if git else "hg id -i") 479 | with update_changed_requirements(): 480 | run("git pull origin master -f" if git else "hg pull && hg up -C") 481 | manage("collectstatic -v 0 --noinput") 482 | manage("syncdb --noinput") 483 | manage("migrate --noinput") 484 | restart() 485 | return True 486 | 487 | 488 | @task 489 | @log_call 490 | def rollback(): 491 | """ 492 | Reverts project state to the last deploy. 493 | When a deploy is performed, the current state of the project is 494 | backed up. This includes the last commit checked out, the database, 495 | and all static files. Calling rollback will revert all of these to 496 | their state prior to the last deploy. 497 | """ 498 | with project(): 499 | with update_changed_requirements(): 500 | git = env.repo_url.startswith("git") 501 | update = "git checkout" if git else "hg up -C" 502 | run("%s `cat last.commit`" % update) 503 | with cd(os.path.join(static(), "..")): 504 | run("tar -xf %s" % os.path.join(env.proj_path, "last.tar")) 505 | restore("last.db") 506 | restart() 507 | 508 | 509 | @task 510 | @log_call 511 | def all(): 512 | """ 513 | Installs everything required on a new system and deploy. 514 | From the base software, up to the deployed project. 515 | """ 516 | install() 517 | if create(): 518 | deploy() 519 | --------------------------------------------------------------------------------