From ba97f4d10e3566510ec0904863100be7fffe072e Mon Sep 17 00:00:00 2001
From: Dave Paola
Date: Tue, 29 Nov 2011 11:38:56 -0800
Subject: [PATCH] initial commit
---
.gitignore | 5 +
Makefile | 49 +
README | 59 +
install/README | 2 +
install/S3.py | 617 ++++++
install/apache.py | 20 +
install/application_uids_gids.py | 126 ++
install/backup.py | 19 +
.../apache/000-defaults/config/apache.conf | 25 +
.../apache/000-defaults/content/index.html | 0
install/conf/apache/ports.conf | 24 +
install/conf/crontab | 17 +
install/conf/etc_ssh/README | 8 +
install/conf/git_hooks/post_receive.py | 1 +
install/conf/gitosis.conf | 5 +
install/conf/mysql/my.cnf.new | 132 ++
install/conf/mysql/my.cnf.orig | 130 ++
install/conf/proxycache_manager/500.html | 145 ++
install/conf/proxycache_manager/502.html | 151 ++
install/conf/proxycache_manager/nginx.conf | 1 +
install/conf/rc.local | 16 +
install/conf/ssh_keys/README | 7 +
install/conf/ssh_keys/root.gitconfig | 3 +
install/conf/ssh_keys/ssh_config | 2 +
install/conf/ssh_keys/www.gitconfig | 3 +
install/conf/ssl_keys/README | 9 +
install/config.py | 33 +
install/core.py | 324 +++
install/database.py | 111 +
install/dump_archive.py | 93 +
install/git_serve.py | 19 +
install/install.py | 176 ++
install/load_archive.py | 39 +
install/nginx.py | 55 +
install/s3get.py | 23 +
install/s3list.py | 18 +
install/s3put.py | 21 +
jobs/report_billing.py | 10 +
misc/README | 54 +
misc/SETUP | 23 +
misc/gen_invite_code/adjectives.txt | 355 ++++
misc/gen_invite_code/gen_invite_code.py | 29 +
misc/gen_invite_code/nouns.txt | 120 ++
src/client/.gitignore | 4 +
src/client/Makefile | 12 +
src/client/README | 7 +
src/client/djangy/__init__.py | 1 +
src/client/djangy/djangy.py | 407 ++++
src/client/find_git_repository/__init__.py | 1 +
.../find_git_repository.py | 60 +
src/client/setup.py | 25 +
src/server/master/README | 5 +
.../master/management_database/.gitignore | 3 +
.../management_database/__init__.py | 4 +
.../management_database/loadadmins.yaml | 6 +
.../management_database/loadchargables.yaml | 11 +
.../loadsubscriptiontypes.yaml | 6 +
.../management_database/manage.py | 10 +
.../migrations/0001_initial.py | 108 +
.../migrations/0002_add_admins.py | 57 +
.../migrations/0003_add_app_gid.py | 60 +
...o__add_field_application_bundle_version.py | 61 +
.../migrations/0005_resource_allocation.py | 101 +
.../migrations/0006_mark_deletion.py | 66 +
.../migrations/0007_add_chargify_ids.py | 81 +
.../migrations/0008_remove_masked_cc.py | 68 +
.../migrations/0009_add_allocation_change.py | 85 +
...yCache_and_VirtualHost_and_Process_port.py | 112 +
.../migrations/0011_add_port_to_proxycache.py | 91 +
...ort_and_add_some_uniqueness_constraints.py | 108 +
.../0013_create_table_WorkerHost.py | 101 +
.../0014_add_application_num_procs.py | 97 +
.../migrations/0015_default_VirtualHost.py | 98 +
.../migrations/0016_default_ProxyCache.py | 100 +
.../0017_make_virtualhost_unique.py | 97 +
.../migrations/0018_add_referrers.py | 105 +
.../migrations/0019_add_invite_limit.py | 100 +
.../0020_add_SshPublicKey_and_Collaborator.py | 142 ++
.../0021_chargify_to_devpayments_schema.py | 126 ++
.../migrations/0022_add_chargables.py | 125 ++
.../0023_alter_allocation_change.py | 126 ++
.../migrations/0024_add_billing_events.py | 143 ++
.../0025_add_ActiveApplicationName_table.py | 147 ++
.../migrations/0026_add_subscriptions.py | 173 ++
.../migrations/0027_add_cache_sizes.py | 160 ++
.../migrations/0028_add_celery_procs.py | 156 ++
.../0029_add_proc_type_to_Process.py | 169 ++
.../migrations/__init__.py | 0
.../management_database/models.py | 369 ++++
.../management_database/settings.py | 15 +
.../master/management_database/setup.py | 13 +
src/server/master/master_api/.gitignore | 3 +
.../master/master_api/master_api/__init__.py | 2 +
.../master_api/master_api/application_api.py | 125 ++
.../master_api/master_api/billing_api.py | 217 ++
.../master_api/devpayments/__init__.py | 174 ++
.../master_api/master_api/exceptions.py | 60 +
src/server/master/master_api/setup.py | 13 +
src/server/master/master_manager/__init__.py | 0
.../master/master_manager/add_application.py | 97 +
.../master_manager/add_ssh_public_key.py | 12 +
src/server/master/master_manager/allocate.py | 55 +
.../master/master_manager/change_password.py | 23 +
src/server/master/master_manager/command.py | 95 +
.../master_manager/configure_proxycache.py | 20 +
.../master/master_manager/copy_etc_hosts.py | 33 +
.../master_manager/delete_application.py | 47 +
src/server/master/master_manager/deploy.py | 301 +++
.../master/master_manager/deploy_all.py | 24 +
src/server/master/master_manager/git_serve.py | 42 +
.../master_manager/import_ssh_public_keys.py | 39 +
.../master/master_manager/post_receive.py | 34 +
.../master_manager/purge_old_bundles.py | 19 +
.../regenerate_ssh_authorized_keys.py | 11 +
.../master_manager/remove_ssh_public_key.py | 13 +
.../master/master_manager/retrieve_logs.py | 19 +
.../master/master_manager/setuid/.gitignore | 8 +
.../master/master_manager/setuid/Makefile | 8 +
.../master/master_manager/setuid/config.h | 6 +
src/server/master/master_manager/setuid/run.h | 22 +
.../setuid/run_add_application.c | 11 +
.../setuid/run_add_ssh_public_key.c | 11 +
.../master_manager/setuid/run_allocate.c | 11 +
.../master_manager/setuid/run_command.c | 11 +
.../setuid/run_configure_proxycache.c | 11 +
.../setuid/run_delete_application.c | 11 +
.../master/master_manager/setuid/run_deploy.c | 11 +
.../run_regenerate_ssh_authorized_keys.c | 11 +
.../setuid/run_remove_ssh_public_key.c | 11 +
.../master_manager/setuid/run_retrieve_logs.c | 11 +
.../master_manager/setuid/run_shell_serve.c | 26 +
.../master/master_manager/shared/__init__.py | 11 +
.../master_manager/shared/allocate_workers.py | 235 ++
.../master_manager/shared/call_remote.py | 135 ++
.../master_manager/shared/ssh_and_git.py | 75 +
.../master/master_manager/shell_serve.py | 55 +
.../uid_application_setup/__init__.py | 0
.../create_virtualenv.py | 87 +
.../get_admin_media_prefix.py | 14 +
.../master/master_manager/uid_git/__init__.py | 0
.../master_manager/uid_git/clone_repo.py | 41 +
.../web_api/application/web_api/__init__.py | 0
.../web_api/application/web_api/api/Router.py | 23 +
.../application/web_api/api/__init__.py | 1 +
.../web_api/application/web_api/api/models.py | 0
.../web_api/application/web_api/api/tests.py | 23 +
.../web_api/application/web_api/api/views.py | 137 ++
.../web_api/application/web_api/manage.py | 12 +
.../web_api/application/web_api/settings.py | 121 ++
.../application/web_api/static/foo.txt | 0
.../web_api/application/web_api/urls.py | 15 +
src/server/master/web_api/config/apache.conf | 24 +
.../master/web_api/config/production.wsgi | 12 +
.../master/web_ui/application/web_ui/.eggs | 2 +
.../web_ui/application/web_ui/__init__.py | 0
.../application/web_ui/docs/__init__.py | 0
.../web_ui/application/web_ui/docs/admin.py | 6 +
.../web_ui/application/web_ui/docs/dump_docs | 4 +
.../web_ui/application/web_ui/docs/forms.py | 23 +
.../docs/migrations/0001_create_tables.py | 37 +
.../web_ui/docs/migrations/__init__.py | 0
.../web_ui/application/web_ui/docs/models.py | 19 +
.../web_ui/docs/templates/wiki/edit.html | 30 +
.../web_ui/docs/templates/wiki/index.html | 16 +
.../web_ui/docs/templates/wiki/view.html | 26 +
.../web_ui/docs/templates/wiki/wiki_base.html | 21 +
.../web_ui/docs/templatetags/__init__.py | 0
.../web_ui/docs/templatetags/wiki.py | 19 +
.../web_ui/application/web_ui/docs/urls.py | 11 +
.../web_ui/application/web_ui/docs/views.py | 55 +
.../application/web_ui/docs/wiki_docs.yaml | 1886 +++++++++++++++++
.../web_ui/application/web_ui/main/Router.py | 21 +
.../application/web_ui/main/__init__.py | 1 +
.../web_ui/main/invite_code/__init__.py | 1 +
.../web_ui/main/invite_code/adjectives.py | 357 ++++
.../main/invite_code/gen_invite_code.py | 31 +
.../web_ui/main/invite_code/nouns.py | 122 ++
.../web_ui/main/migrations/0001_initial.py | 35 +
.../main/migrations/0002_add_invited_field.py | 31 +
.../web_ui/main/migrations/__init__.py | 0
.../web_ui/application/web_ui/main/models.py | 7 +
.../web_ui/main/templates/admin.html | 46 +
.../main/templates/dashboard_account.html | 78 +
.../main/templates/dashboard_application.html | 157 ++
.../templates/dashboard_applicationlist.html | 38 +
.../main/templates/dashboard_billing.html | 103 +
.../main/templates/dashboard_invite.html | 39 +
.../web_ui/main/templates/emails.txt | 1 +
.../web_ui/main/templates/hackerdojo.html | 27 +
.../web_ui/main/templates/index.html | 51 +
.../web_ui/main/templates/join.html | 19 +
.../web_ui/main/templates/login.html | 26 +
.../web_ui/main/templates/pricing.html | 175 ++
.../templates/request_reset_password.html | 25 +
.../main/templates/reset_password_form.html | 24 +
.../web_ui/main/templates/setpassword.html | 11 +
.../web_ui/application/web_ui/main/tests.py | 23 +
.../web_ui/application/web_ui/main/utils.py | 9 +
.../application/web_ui/main/views/__init__.py | 0
.../application/web_ui/main/views/admin.py | 96 +
.../web_ui/main/views/create_account.py | 110 +
.../web_ui/main/views/dashboard_account.py | 106 +
.../main/views/dashboard_application.py | 221 ++
.../main/views/dashboard_applicationlist.py | 17 +
.../web_ui/main/views/dashboard_billing.py | 83 +
.../web_ui/main/views/dashboard_invite.py | 24 +
.../application/web_ui/main/views/index.py | 21 +
.../web_ui/main/views/login_logout.py | 106 +
.../application/web_ui/main/views/shared.py | 104 +
.../web_ui/application/web_ui/manage.py | 12 +
.../web_ui/application/web_ui/settings.py | 130 ++
.../static/assets/fonts/DroidSans-Bold.eot | Bin 0 -> 30374 bytes
.../static/assets/fonts/DroidSans-Bold.ttf | Bin 0 -> 150804 bytes
.../web_ui/static/assets/fonts/DroidSans.eot | Bin 0 -> 30002 bytes
.../web_ui/static/assets/fonts/DroidSans.ttf | Bin 0 -> 149076 bytes
.../assets/fonts/Google Android License.txt | 18 +
.../web_ui/static/assets/fonts/demo.html | 38 +
.../web_ui/static/assets/fonts/stylesheet.css | 24 +
.../static/assets/js/form.validation.js | 64 +
.../static/assets/js/jquery-1.2.3.min.js | 32 +
.../static/assets/js/jquery-1.4.2.min.js | 154 ++
.../static/assets/js/jquery.functions.js | 53 +
.../static/assets/js/jquery.infieldlabel.js | 141 ++
.../static/assets/js/jquery.innerfade.js | 128 ++
.../web_ui/static/assets/js/pngfix.js | 330 +++
.../lightbox/css/jquery.lightbox-0.5.css | 101 +
.../assets/lightbox/js/jquery.lightbox-0.5.js | 472 +++++
.../lightbox/js/jquery.lightbox-0.5.min.js | 42 +
.../lightbox/js/jquery.lightbox-0.5.pack.js | 14 +
.../web_ui/static/images/.DS_Store | Bin 0 -> 6148 bytes
.../web_ui/static/images/celery128.png | Bin 0 -> 14771 bytes
.../static/images/djangopowered126x54.gif | Bin 0 -> 3312 bytes
.../images/djangopowered126x54_gray.gif | Bin 0 -> 3312 bytes
.../static/images/djangy_laptop_noshadow.png | Bin 0 -> 24309 bytes
.../web_ui/static/images/gunicorn128.png | Bin 0 -> 5649 bytes
.../web_ui/static/images/laptop.png | Bin 0 -> 27190 bytes
.../web_ui/static/images/lightbox-blank.gif | Bin 0 -> 43 bytes
.../static/images/lightbox-btn-close.gif | Bin 0 -> 700 bytes
.../static/images/lightbox-btn-next.gif | Bin 0 -> 812 bytes
.../static/images/lightbox-btn-prev.gif | Bin 0 -> 832 bytes
.../static/images/lightbox-ico-loading.gif | Bin 0 -> 3990 bytes
.../application/web_ui/static/images/logo.png | Bin 0 -> 4285 bytes
.../web_ui/static/images/ponypowered.png | Bin 0 -> 10833 bytes
.../web_ui/static/images/ponypowered_gray.png | Bin 0 -> 11929 bytes
.../view_from_pontevecchio_florence.jpg | Bin 0 -> 272016 bytes
.../application/web_ui/templates/404.html | 18 +
.../application/web_ui/templates/500.html | 20 +
.../application/web_ui/templates/base.html | 90 +
.../web_ui/templates/docs_navbar.html | 8 +
.../templates/docs_tutorial_navbar.html | 16 +
.../application/web_ui/templates/footer.html | 3 +
.../application/web_ui/templates/navbar.html | 16 +
.../master/web_ui/application/web_ui/urls.py | 47 +
src/server/master/web_ui/config/apache.conf | 40 +
.../master/web_ui/config/production.wsgi | 12 +
src/server/proxycache/nginx.conf | 78 +
.../proxycache_manager/clear_cache.py | 28 +
.../proxycache_manager/configure.py | 79 +
.../proxycache_manager/delete_application.py | 39 +
.../proxycache_manager/setuid/.gitignore | 3 +
.../proxycache_manager/setuid/Makefile | 8 +
.../proxycache_manager/setuid/config.h | 7 +
.../proxycache_manager/setuid/run.h | 22 +
.../setuid/run_clear_cache.c | 11 +
.../proxycache_manager/setuid/run_configure.c | 11 +
.../setuid/run_delete_application.c | 11 +
.../proxycache_manager/shared/__init__.py | 6 +
.../proxycache_manager/shared/nginx.py | 8 +
.../templates/generic_nginx_conf | 34 +
.../shared/djangy_server_shared/__init__.py | 9 +
.../djangy_server_shared/bundle_info.py | 68 +
.../shared/djangy_server_shared/constants.py | 123 ++
.../shared/djangy_server_shared/exceptions.py | 85 +
.../find_django_project.py | 38 +
.../shared/djangy_server_shared/functions.py | 133 ++
.../installer_configured_constants.py | 8 +
.../shared/djangy_server_shared/json_log.py | 109 +
.../resource_allocation.py | 35 +
.../run_external_program.py | 106 +
src/server/shared/setup.py | 13 +
src/server/worker/worker_manager/__init__.py | 0
.../worker_manager/delete_application.py | 27 +
src/server/worker/worker_manager/deploy.py | 296 +++
.../worker/worker_manager/orm/__init__.py | 0
.../worker/worker_manager/orm/manage.py | 12 +
.../orm/migrations/0001_initial.py | 64 +
.../orm/migrations/0002_add_celery_procs.py | 43 +
.../worker_manager/orm/migrations/__init__.py | 0
.../worker/worker_manager/orm/models.py | 62 +
.../worker/worker_manager/orm/settings.py | 17 +
.../worker_manager/purge_old_bundles.py | 19 +
.../worker/worker_manager/purge_old_logs.py | 20 +
.../worker/worker_manager/retrieve_logs.py | 36 +
.../worker/worker_manager/setuid/.gitignore | 6 +
.../worker/worker_manager/setuid/Makefile | 8 +
.../worker/worker_manager/setuid/config.h | 6 +
src/server/worker/worker_manager/setuid/run.h | 22 +
.../setuid/run_delete_application.c | 11 +
.../worker/worker_manager/setuid/run_deploy.c | 11 +
.../worker_manager/setuid/run_retrieve_logs.c | 11 +
.../worker/worker_manager/setuid/run_start.c | 11 +
.../worker/worker_manager/setuid/run_stop.c | 11 +
.../worker/worker_manager/shared/__init__.py | 10 +
.../worker_manager/shared/lock_application.py | 12 +
.../worker_manager/shared/start_stop.py | 81 +
src/server/worker/worker_manager/start.py | 15 +
src/server/worker/worker_manager/stop.py | 15 +
.../templates/generic_django_wsgi | 26 +
.../templates/generic_gunicorn_conf | 8 +
.../worker_manager/templates/generic_settings | 132 ++
.../worker/worker_manager/templates/logs.txt | 4 +
test/data/testapp-v1/__init__.py | 0
test/data/testapp-v1/main/__init__.py | 0
test/data/testapp-v1/main/models.py | 3 +
test/data/testapp-v1/main/tests.py | 23 +
test/data/testapp-v1/main/views.py | 5 +
test/data/testapp-v1/manage.py | 11 +
test/data/testapp-v1/settings.py | 94 +
test/data/testapp-v1/urls.py | 19 +
test/data/testapp-v2/__init__.py | 0
test/data/testapp-v2/main/__init__.py | 0
test/data/testapp-v2/main/models.py | 3 +
test/data/testapp-v2/main/tests.py | 23 +
test/data/testapp-v2/main/views.py | 5 +
test/data/testapp-v2/manage.py | 11 +
test/data/testapp-v2/settings.py | 94 +
test/data/testapp-v2/site_media/index.html | 1 +
test/data/testapp-v2/urls.py | 20 +
test/data/testapp-v3/__init__.py | 0
test/data/testapp-v3/main/__init__.py | 0
test/data/testapp-v3/main/models.py | 6 +
test/data/testapp-v3/main/tests.py | 22 +
test/data/testapp-v3/main/views.py | 14 +
test/data/testapp-v3/manage.py | 11 +
test/data/testapp-v3/settings.py | 95 +
test/data/testapp-v3/urls.py | 20 +
test/data/testapp-v4/__init__.py | 0
test/data/testapp-v4/main/__init__.py | 0
.../main/migrations/0001_initial.py | 33 +
.../testapp-v4/main/migrations/__init__.py | 0
test/data/testapp-v4/main/models.py | 6 +
test/data/testapp-v4/main/tests.py | 22 +
test/data/testapp-v4/main/views.py | 14 +
test/data/testapp-v4/manage.py | 11 +
test/data/testapp-v4/settings.py | 96 +
test/data/testapp-v4/urls.py | 20 +
test/fetch_url.py | 35 +
test/test_cases.py | 184 ++
test/testlib.py | 105 +
test/update_billing.py | 29 +
test/urls.py | 8 +
351 files changed, 19515 insertions(+)
create mode 100644 .gitignore
create mode 100644 Makefile
create mode 100644 README
create mode 100644 install/README
create mode 100644 install/S3.py
create mode 100644 install/apache.py
create mode 100644 install/application_uids_gids.py
create mode 100755 install/backup.py
create mode 100644 install/conf/apache/000-defaults/config/apache.conf
create mode 100644 install/conf/apache/000-defaults/content/index.html
create mode 100644 install/conf/apache/ports.conf
create mode 100644 install/conf/crontab
create mode 100644 install/conf/etc_ssh/README
create mode 120000 install/conf/git_hooks/post_receive.py
create mode 100644 install/conf/gitosis.conf
create mode 100644 install/conf/mysql/my.cnf.new
create mode 100644 install/conf/mysql/my.cnf.orig
create mode 100644 install/conf/proxycache_manager/500.html
create mode 100644 install/conf/proxycache_manager/502.html
create mode 120000 install/conf/proxycache_manager/nginx.conf
create mode 100755 install/conf/rc.local
create mode 100644 install/conf/ssh_keys/README
create mode 100644 install/conf/ssh_keys/root.gitconfig
create mode 100644 install/conf/ssh_keys/ssh_config
create mode 100644 install/conf/ssh_keys/www.gitconfig
create mode 100644 install/conf/ssl_keys/README
create mode 100644 install/config.py
create mode 100644 install/core.py
create mode 100644 install/database.py
create mode 100755 install/dump_archive.py
create mode 100644 install/git_serve.py
create mode 100755 install/install.py
create mode 100755 install/load_archive.py
create mode 100644 install/nginx.py
create mode 100755 install/s3get.py
create mode 100755 install/s3list.py
create mode 100755 install/s3put.py
create mode 100755 jobs/report_billing.py
create mode 100644 misc/README
create mode 100644 misc/SETUP
create mode 100644 misc/gen_invite_code/adjectives.txt
create mode 100755 misc/gen_invite_code/gen_invite_code.py
create mode 100644 misc/gen_invite_code/nouns.txt
create mode 100644 src/client/.gitignore
create mode 100644 src/client/Makefile
create mode 100644 src/client/README
create mode 100644 src/client/djangy/__init__.py
create mode 100755 src/client/djangy/djangy.py
create mode 100644 src/client/find_git_repository/__init__.py
create mode 100644 src/client/find_git_repository/find_git_repository.py
create mode 100644 src/client/setup.py
create mode 100644 src/server/master/README
create mode 100644 src/server/master/management_database/.gitignore
create mode 100644 src/server/master/management_database/management_database/__init__.py
create mode 100644 src/server/master/management_database/management_database/loadadmins.yaml
create mode 100644 src/server/master/management_database/management_database/loadchargables.yaml
create mode 100644 src/server/master/management_database/management_database/loadsubscriptiontypes.yaml
create mode 100755 src/server/master/management_database/management_database/manage.py
create mode 100644 src/server/master/management_database/management_database/migrations/0001_initial.py
create mode 100644 src/server/master/management_database/management_database/migrations/0002_add_admins.py
create mode 100644 src/server/master/management_database/management_database/migrations/0003_add_app_gid.py
create mode 100644 src/server/master/management_database/management_database/migrations/0004_auto__add_field_application_bundle_version.py
create mode 100644 src/server/master/management_database/management_database/migrations/0005_resource_allocation.py
create mode 100644 src/server/master/management_database/management_database/migrations/0006_mark_deletion.py
create mode 100644 src/server/master/management_database/management_database/migrations/0007_add_chargify_ids.py
create mode 100644 src/server/master/management_database/management_database/migrations/0008_remove_masked_cc.py
create mode 100644 src/server/master/management_database/management_database/migrations/0009_add_allocation_change.py
create mode 100644 src/server/master/management_database/management_database/migrations/0010_add_ProxyCache_and_VirtualHost_and_Process_port.py
create mode 100644 src/server/master/management_database/management_database/migrations/0011_add_port_to_proxycache.py
create mode 100644 src/server/master/management_database/management_database/migrations/0012_remove_ProxyCache_port_and_add_some_uniqueness_constraints.py
create mode 100644 src/server/master/management_database/management_database/migrations/0013_create_table_WorkerHost.py
create mode 100644 src/server/master/management_database/management_database/migrations/0014_add_application_num_procs.py
create mode 100644 src/server/master/management_database/management_database/migrations/0015_default_VirtualHost.py
create mode 100644 src/server/master/management_database/management_database/migrations/0016_default_ProxyCache.py
create mode 100644 src/server/master/management_database/management_database/migrations/0017_make_virtualhost_unique.py
create mode 100644 src/server/master/management_database/management_database/migrations/0018_add_referrers.py
create mode 100644 src/server/master/management_database/management_database/migrations/0019_add_invite_limit.py
create mode 100644 src/server/master/management_database/management_database/migrations/0020_add_SshPublicKey_and_Collaborator.py
create mode 100644 src/server/master/management_database/management_database/migrations/0021_chargify_to_devpayments_schema.py
create mode 100644 src/server/master/management_database/management_database/migrations/0022_add_chargables.py
create mode 100644 src/server/master/management_database/management_database/migrations/0023_alter_allocation_change.py
create mode 100644 src/server/master/management_database/management_database/migrations/0024_add_billing_events.py
create mode 100644 src/server/master/management_database/management_database/migrations/0025_add_ActiveApplicationName_table.py
create mode 100644 src/server/master/management_database/management_database/migrations/0026_add_subscriptions.py
create mode 100644 src/server/master/management_database/management_database/migrations/0027_add_cache_sizes.py
create mode 100644 src/server/master/management_database/management_database/migrations/0028_add_celery_procs.py
create mode 100644 src/server/master/management_database/management_database/migrations/0029_add_proc_type_to_Process.py
create mode 100644 src/server/master/management_database/management_database/migrations/__init__.py
create mode 100644 src/server/master/management_database/management_database/models.py
create mode 100644 src/server/master/management_database/management_database/settings.py
create mode 100644 src/server/master/management_database/setup.py
create mode 100644 src/server/master/master_api/.gitignore
create mode 100644 src/server/master/master_api/master_api/__init__.py
create mode 100644 src/server/master/master_api/master_api/application_api.py
create mode 100644 src/server/master/master_api/master_api/billing_api.py
create mode 100644 src/server/master/master_api/master_api/devpayments/__init__.py
create mode 100644 src/server/master/master_api/master_api/exceptions.py
create mode 100644 src/server/master/master_api/setup.py
create mode 100644 src/server/master/master_manager/__init__.py
create mode 100755 src/server/master/master_manager/add_application.py
create mode 100755 src/server/master/master_manager/add_ssh_public_key.py
create mode 100755 src/server/master/master_manager/allocate.py
create mode 100644 src/server/master/master_manager/change_password.py
create mode 100755 src/server/master/master_manager/command.py
create mode 100755 src/server/master/master_manager/configure_proxycache.py
create mode 100755 src/server/master/master_manager/copy_etc_hosts.py
create mode 100755 src/server/master/master_manager/delete_application.py
create mode 100755 src/server/master/master_manager/deploy.py
create mode 100755 src/server/master/master_manager/deploy_all.py
create mode 100755 src/server/master/master_manager/git_serve.py
create mode 100644 src/server/master/master_manager/import_ssh_public_keys.py
create mode 100755 src/server/master/master_manager/post_receive.py
create mode 100644 src/server/master/master_manager/purge_old_bundles.py
create mode 100755 src/server/master/master_manager/regenerate_ssh_authorized_keys.py
create mode 100755 src/server/master/master_manager/remove_ssh_public_key.py
create mode 100755 src/server/master/master_manager/retrieve_logs.py
create mode 100644 src/server/master/master_manager/setuid/.gitignore
create mode 100644 src/server/master/master_manager/setuid/Makefile
create mode 100644 src/server/master/master_manager/setuid/config.h
create mode 100644 src/server/master/master_manager/setuid/run.h
create mode 100644 src/server/master/master_manager/setuid/run_add_application.c
create mode 100644 src/server/master/master_manager/setuid/run_add_ssh_public_key.c
create mode 100644 src/server/master/master_manager/setuid/run_allocate.c
create mode 100644 src/server/master/master_manager/setuid/run_command.c
create mode 100644 src/server/master/master_manager/setuid/run_configure_proxycache.c
create mode 100644 src/server/master/master_manager/setuid/run_delete_application.c
create mode 100644 src/server/master/master_manager/setuid/run_deploy.c
create mode 100644 src/server/master/master_manager/setuid/run_regenerate_ssh_authorized_keys.c
create mode 100644 src/server/master/master_manager/setuid/run_remove_ssh_public_key.c
create mode 100644 src/server/master/master_manager/setuid/run_retrieve_logs.c
create mode 100644 src/server/master/master_manager/setuid/run_shell_serve.c
create mode 100644 src/server/master/master_manager/shared/__init__.py
create mode 100644 src/server/master/master_manager/shared/allocate_workers.py
create mode 100644 src/server/master/master_manager/shared/call_remote.py
create mode 100644 src/server/master/master_manager/shared/ssh_and_git.py
create mode 100755 src/server/master/master_manager/shell_serve.py
create mode 100644 src/server/master/master_manager/uid_application_setup/__init__.py
create mode 100644 src/server/master/master_manager/uid_application_setup/create_virtualenv.py
create mode 100644 src/server/master/master_manager/uid_application_setup/get_admin_media_prefix.py
create mode 100644 src/server/master/master_manager/uid_git/__init__.py
create mode 100644 src/server/master/master_manager/uid_git/clone_repo.py
create mode 100755 src/server/master/web_api/application/web_api/__init__.py
create mode 100644 src/server/master/web_api/application/web_api/api/Router.py
create mode 100755 src/server/master/web_api/application/web_api/api/__init__.py
create mode 100644 src/server/master/web_api/application/web_api/api/models.py
create mode 100755 src/server/master/web_api/application/web_api/api/tests.py
create mode 100755 src/server/master/web_api/application/web_api/api/views.py
create mode 100755 src/server/master/web_api/application/web_api/manage.py
create mode 100644 src/server/master/web_api/application/web_api/settings.py
create mode 100644 src/server/master/web_api/application/web_api/static/foo.txt
create mode 100755 src/server/master/web_api/application/web_api/urls.py
create mode 100644 src/server/master/web_api/config/apache.conf
create mode 100644 src/server/master/web_api/config/production.wsgi
create mode 100644 src/server/master/web_ui/application/web_ui/.eggs
create mode 100644 src/server/master/web_ui/application/web_ui/__init__.py
create mode 100644 src/server/master/web_ui/application/web_ui/docs/__init__.py
create mode 100644 src/server/master/web_ui/application/web_ui/docs/admin.py
create mode 100755 src/server/master/web_ui/application/web_ui/docs/dump_docs
create mode 100644 src/server/master/web_ui/application/web_ui/docs/forms.py
create mode 100644 src/server/master/web_ui/application/web_ui/docs/migrations/0001_create_tables.py
create mode 100644 src/server/master/web_ui/application/web_ui/docs/migrations/__init__.py
create mode 100644 src/server/master/web_ui/application/web_ui/docs/models.py
create mode 100644 src/server/master/web_ui/application/web_ui/docs/templates/wiki/edit.html
create mode 100644 src/server/master/web_ui/application/web_ui/docs/templates/wiki/index.html
create mode 100644 src/server/master/web_ui/application/web_ui/docs/templates/wiki/view.html
create mode 100644 src/server/master/web_ui/application/web_ui/docs/templates/wiki/wiki_base.html
create mode 100644 src/server/master/web_ui/application/web_ui/docs/templatetags/__init__.py
create mode 100644 src/server/master/web_ui/application/web_ui/docs/templatetags/wiki.py
create mode 100644 src/server/master/web_ui/application/web_ui/docs/urls.py
create mode 100644 src/server/master/web_ui/application/web_ui/docs/views.py
create mode 100644 src/server/master/web_ui/application/web_ui/docs/wiki_docs.yaml
create mode 100644 src/server/master/web_ui/application/web_ui/main/Router.py
create mode 100644 src/server/master/web_ui/application/web_ui/main/__init__.py
create mode 100644 src/server/master/web_ui/application/web_ui/main/invite_code/__init__.py
create mode 100644 src/server/master/web_ui/application/web_ui/main/invite_code/adjectives.py
create mode 100644 src/server/master/web_ui/application/web_ui/main/invite_code/gen_invite_code.py
create mode 100644 src/server/master/web_ui/application/web_ui/main/invite_code/nouns.py
create mode 100644 src/server/master/web_ui/application/web_ui/main/migrations/0001_initial.py
create mode 100644 src/server/master/web_ui/application/web_ui/main/migrations/0002_add_invited_field.py
create mode 100644 src/server/master/web_ui/application/web_ui/main/migrations/__init__.py
create mode 100644 src/server/master/web_ui/application/web_ui/main/models.py
create mode 100644 src/server/master/web_ui/application/web_ui/main/templates/admin.html
create mode 100644 src/server/master/web_ui/application/web_ui/main/templates/dashboard_account.html
create mode 100644 src/server/master/web_ui/application/web_ui/main/templates/dashboard_application.html
create mode 100644 src/server/master/web_ui/application/web_ui/main/templates/dashboard_applicationlist.html
create mode 100644 src/server/master/web_ui/application/web_ui/main/templates/dashboard_billing.html
create mode 100644 src/server/master/web_ui/application/web_ui/main/templates/dashboard_invite.html
create mode 100644 src/server/master/web_ui/application/web_ui/main/templates/emails.txt
create mode 100644 src/server/master/web_ui/application/web_ui/main/templates/hackerdojo.html
create mode 100644 src/server/master/web_ui/application/web_ui/main/templates/index.html
create mode 100644 src/server/master/web_ui/application/web_ui/main/templates/join.html
create mode 100644 src/server/master/web_ui/application/web_ui/main/templates/login.html
create mode 100644 src/server/master/web_ui/application/web_ui/main/templates/pricing.html
create mode 100644 src/server/master/web_ui/application/web_ui/main/templates/request_reset_password.html
create mode 100644 src/server/master/web_ui/application/web_ui/main/templates/reset_password_form.html
create mode 100644 src/server/master/web_ui/application/web_ui/main/templates/setpassword.html
create mode 100644 src/server/master/web_ui/application/web_ui/main/tests.py
create mode 100644 src/server/master/web_ui/application/web_ui/main/utils.py
create mode 100644 src/server/master/web_ui/application/web_ui/main/views/__init__.py
create mode 100644 src/server/master/web_ui/application/web_ui/main/views/admin.py
create mode 100644 src/server/master/web_ui/application/web_ui/main/views/create_account.py
create mode 100644 src/server/master/web_ui/application/web_ui/main/views/dashboard_account.py
create mode 100644 src/server/master/web_ui/application/web_ui/main/views/dashboard_application.py
create mode 100644 src/server/master/web_ui/application/web_ui/main/views/dashboard_applicationlist.py
create mode 100644 src/server/master/web_ui/application/web_ui/main/views/dashboard_billing.py
create mode 100644 src/server/master/web_ui/application/web_ui/main/views/dashboard_invite.py
create mode 100644 src/server/master/web_ui/application/web_ui/main/views/index.py
create mode 100644 src/server/master/web_ui/application/web_ui/main/views/login_logout.py
create mode 100644 src/server/master/web_ui/application/web_ui/main/views/shared.py
create mode 100644 src/server/master/web_ui/application/web_ui/manage.py
create mode 100644 src/server/master/web_ui/application/web_ui/settings.py
create mode 100644 src/server/master/web_ui/application/web_ui/static/assets/fonts/DroidSans-Bold.eot
create mode 100644 src/server/master/web_ui/application/web_ui/static/assets/fonts/DroidSans-Bold.ttf
create mode 100644 src/server/master/web_ui/application/web_ui/static/assets/fonts/DroidSans.eot
create mode 100644 src/server/master/web_ui/application/web_ui/static/assets/fonts/DroidSans.ttf
create mode 100644 src/server/master/web_ui/application/web_ui/static/assets/fonts/Google Android License.txt
create mode 100644 src/server/master/web_ui/application/web_ui/static/assets/fonts/demo.html
create mode 100644 src/server/master/web_ui/application/web_ui/static/assets/fonts/stylesheet.css
create mode 100644 src/server/master/web_ui/application/web_ui/static/assets/js/form.validation.js
create mode 100644 src/server/master/web_ui/application/web_ui/static/assets/js/jquery-1.2.3.min.js
create mode 100644 src/server/master/web_ui/application/web_ui/static/assets/js/jquery-1.4.2.min.js
create mode 100644 src/server/master/web_ui/application/web_ui/static/assets/js/jquery.functions.js
create mode 100644 src/server/master/web_ui/application/web_ui/static/assets/js/jquery.infieldlabel.js
create mode 100644 src/server/master/web_ui/application/web_ui/static/assets/js/jquery.innerfade.js
create mode 100644 src/server/master/web_ui/application/web_ui/static/assets/js/pngfix.js
create mode 100644 src/server/master/web_ui/application/web_ui/static/assets/lightbox/css/jquery.lightbox-0.5.css
create mode 100644 src/server/master/web_ui/application/web_ui/static/assets/lightbox/js/jquery.lightbox-0.5.js
create mode 100644 src/server/master/web_ui/application/web_ui/static/assets/lightbox/js/jquery.lightbox-0.5.min.js
create mode 100644 src/server/master/web_ui/application/web_ui/static/assets/lightbox/js/jquery.lightbox-0.5.pack.js
create mode 100644 src/server/master/web_ui/application/web_ui/static/images/.DS_Store
create mode 100644 src/server/master/web_ui/application/web_ui/static/images/celery128.png
create mode 100644 src/server/master/web_ui/application/web_ui/static/images/djangopowered126x54.gif
create mode 100644 src/server/master/web_ui/application/web_ui/static/images/djangopowered126x54_gray.gif
create mode 100644 src/server/master/web_ui/application/web_ui/static/images/djangy_laptop_noshadow.png
create mode 100644 src/server/master/web_ui/application/web_ui/static/images/gunicorn128.png
create mode 100644 src/server/master/web_ui/application/web_ui/static/images/laptop.png
create mode 100644 src/server/master/web_ui/application/web_ui/static/images/lightbox-blank.gif
create mode 100644 src/server/master/web_ui/application/web_ui/static/images/lightbox-btn-close.gif
create mode 100644 src/server/master/web_ui/application/web_ui/static/images/lightbox-btn-next.gif
create mode 100644 src/server/master/web_ui/application/web_ui/static/images/lightbox-btn-prev.gif
create mode 100644 src/server/master/web_ui/application/web_ui/static/images/lightbox-ico-loading.gif
create mode 100644 src/server/master/web_ui/application/web_ui/static/images/logo.png
create mode 100644 src/server/master/web_ui/application/web_ui/static/images/ponypowered.png
create mode 100644 src/server/master/web_ui/application/web_ui/static/images/ponypowered_gray.png
create mode 100644 src/server/master/web_ui/application/web_ui/static/images/view_from_pontevecchio_florence.jpg
create mode 100644 src/server/master/web_ui/application/web_ui/templates/404.html
create mode 100644 src/server/master/web_ui/application/web_ui/templates/500.html
create mode 100644 src/server/master/web_ui/application/web_ui/templates/base.html
create mode 100644 src/server/master/web_ui/application/web_ui/templates/docs_navbar.html
create mode 100644 src/server/master/web_ui/application/web_ui/templates/docs_tutorial_navbar.html
create mode 100644 src/server/master/web_ui/application/web_ui/templates/footer.html
create mode 100644 src/server/master/web_ui/application/web_ui/templates/navbar.html
create mode 100644 src/server/master/web_ui/application/web_ui/urls.py
create mode 100644 src/server/master/web_ui/config/apache.conf
create mode 100644 src/server/master/web_ui/config/production.wsgi
create mode 100644 src/server/proxycache/nginx.conf
create mode 100755 src/server/proxycache/proxycache_manager/clear_cache.py
create mode 100755 src/server/proxycache/proxycache_manager/configure.py
create mode 100755 src/server/proxycache/proxycache_manager/delete_application.py
create mode 100644 src/server/proxycache/proxycache_manager/setuid/.gitignore
create mode 100644 src/server/proxycache/proxycache_manager/setuid/Makefile
create mode 100644 src/server/proxycache/proxycache_manager/setuid/config.h
create mode 100644 src/server/proxycache/proxycache_manager/setuid/run.h
create mode 100644 src/server/proxycache/proxycache_manager/setuid/run_clear_cache.c
create mode 100644 src/server/proxycache/proxycache_manager/setuid/run_configure.c
create mode 100644 src/server/proxycache/proxycache_manager/setuid/run_delete_application.c
create mode 100644 src/server/proxycache/proxycache_manager/shared/__init__.py
create mode 100644 src/server/proxycache/proxycache_manager/shared/nginx.py
create mode 100644 src/server/proxycache/proxycache_manager/templates/generic_nginx_conf
create mode 100644 src/server/shared/djangy_server_shared/__init__.py
create mode 100644 src/server/shared/djangy_server_shared/bundle_info.py
create mode 100644 src/server/shared/djangy_server_shared/constants.py
create mode 100644 src/server/shared/djangy_server_shared/exceptions.py
create mode 100644 src/server/shared/djangy_server_shared/find_django_project.py
create mode 100644 src/server/shared/djangy_server_shared/functions.py
create mode 100644 src/server/shared/djangy_server_shared/installer_configured_constants.py
create mode 100644 src/server/shared/djangy_server_shared/json_log.py
create mode 100644 src/server/shared/djangy_server_shared/resource_allocation.py
create mode 100644 src/server/shared/djangy_server_shared/run_external_program.py
create mode 100644 src/server/shared/setup.py
create mode 100644 src/server/worker/worker_manager/__init__.py
create mode 100755 src/server/worker/worker_manager/delete_application.py
create mode 100755 src/server/worker/worker_manager/deploy.py
create mode 100644 src/server/worker/worker_manager/orm/__init__.py
create mode 100755 src/server/worker/worker_manager/orm/manage.py
create mode 100644 src/server/worker/worker_manager/orm/migrations/0001_initial.py
create mode 100644 src/server/worker/worker_manager/orm/migrations/0002_add_celery_procs.py
create mode 100644 src/server/worker/worker_manager/orm/migrations/__init__.py
create mode 100644 src/server/worker/worker_manager/orm/models.py
create mode 100644 src/server/worker/worker_manager/orm/settings.py
create mode 100644 src/server/worker/worker_manager/purge_old_bundles.py
create mode 100644 src/server/worker/worker_manager/purge_old_logs.py
create mode 100755 src/server/worker/worker_manager/retrieve_logs.py
create mode 100644 src/server/worker/worker_manager/setuid/.gitignore
create mode 100644 src/server/worker/worker_manager/setuid/Makefile
create mode 100644 src/server/worker/worker_manager/setuid/config.h
create mode 100644 src/server/worker/worker_manager/setuid/run.h
create mode 100644 src/server/worker/worker_manager/setuid/run_delete_application.c
create mode 100644 src/server/worker/worker_manager/setuid/run_deploy.c
create mode 100644 src/server/worker/worker_manager/setuid/run_retrieve_logs.c
create mode 100644 src/server/worker/worker_manager/setuid/run_start.c
create mode 100644 src/server/worker/worker_manager/setuid/run_stop.c
create mode 100644 src/server/worker/worker_manager/shared/__init__.py
create mode 100644 src/server/worker/worker_manager/shared/lock_application.py
create mode 100644 src/server/worker/worker_manager/shared/start_stop.py
create mode 100755 src/server/worker/worker_manager/start.py
create mode 100755 src/server/worker/worker_manager/stop.py
create mode 100644 src/server/worker/worker_manager/templates/generic_django_wsgi
create mode 100644 src/server/worker/worker_manager/templates/generic_gunicorn_conf
create mode 100644 src/server/worker/worker_manager/templates/generic_settings
create mode 100644 src/server/worker/worker_manager/templates/logs.txt
create mode 100755 test/data/testapp-v1/__init__.py
create mode 100755 test/data/testapp-v1/main/__init__.py
create mode 100755 test/data/testapp-v1/main/models.py
create mode 100755 test/data/testapp-v1/main/tests.py
create mode 100755 test/data/testapp-v1/main/views.py
create mode 100755 test/data/testapp-v1/manage.py
create mode 100755 test/data/testapp-v1/settings.py
create mode 100755 test/data/testapp-v1/urls.py
create mode 100755 test/data/testapp-v2/__init__.py
create mode 100755 test/data/testapp-v2/main/__init__.py
create mode 100755 test/data/testapp-v2/main/models.py
create mode 100755 test/data/testapp-v2/main/tests.py
create mode 100755 test/data/testapp-v2/main/views.py
create mode 100755 test/data/testapp-v2/manage.py
create mode 100755 test/data/testapp-v2/settings.py
create mode 100644 test/data/testapp-v2/site_media/index.html
create mode 100755 test/data/testapp-v2/urls.py
create mode 100755 test/data/testapp-v3/__init__.py
create mode 100755 test/data/testapp-v3/main/__init__.py
create mode 100755 test/data/testapp-v3/main/models.py
create mode 100755 test/data/testapp-v3/main/tests.py
create mode 100755 test/data/testapp-v3/main/views.py
create mode 100755 test/data/testapp-v3/manage.py
create mode 100755 test/data/testapp-v3/settings.py
create mode 100755 test/data/testapp-v3/urls.py
create mode 100755 test/data/testapp-v4/__init__.py
create mode 100755 test/data/testapp-v4/main/__init__.py
create mode 100644 test/data/testapp-v4/main/migrations/0001_initial.py
create mode 100644 test/data/testapp-v4/main/migrations/__init__.py
create mode 100755 test/data/testapp-v4/main/models.py
create mode 100755 test/data/testapp-v4/main/tests.py
create mode 100755 test/data/testapp-v4/main/views.py
create mode 100755 test/data/testapp-v4/manage.py
create mode 100755 test/data/testapp-v4/settings.py
create mode 100755 test/data/testapp-v4/urls.py
create mode 100644 test/fetch_url.py
create mode 100755 test/test_cases.py
create mode 100644 test/testlib.py
create mode 100644 test/update_billing.py
create mode 100644 test/urls.py
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..eff697f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+*.pyc
+*.db
+*.log
+*~
+*python-virtual*
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..43a098c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,49 @@
+all: run/python-virtual run/python-virtual/bin/post_receive.py run/python-virtual/bin/git_serve.py run/python-virtual/bin/shell_serve.py setuid
+
+run/python-virtual: src/server/master/management_database/management_database/* src/server/master/master_api/master_api/* src/server/shared/djangy_server_shared/*
+ virtualenv run/python-virtual
+ bash -c 'source run/python-virtual/bin/activate; easy_install Django==1.2.1 Mako==0.3.4 South==0.7.2 django-sentry==1.0.9'
+ bash -c 'source run/python-virtual/bin/activate; easy_install src/server/master/management_database src/server/master/master_api src/server/shared'
+
+run/python-virtual/bin/post_receive.py: run/python-virtual src/server/master/master_manager/post_receive.py
+ cp src/server/master/master_manager/post_receive.py run/python-virtual/bin/post_receive.py
+ chmod +x run/python-virtual/bin/post_receive.py
+
+run/python-virtual/bin/git_serve.py: run/python-virtual src/server/master/master_manager/git_serve.py
+ cp src/server/master/master_manager/git_serve.py run/python-virtual/bin/git_serve.py
+ chmod +x run/python-virtual/bin/git_serve.py
+
+run/python-virtual/bin/shell_serve.py: run/python-virtual src/server/master/master_manager/shell_serve.py
+ cp src/server/master/master_manager/shell_serve.py run/python-virtual/bin/shell_serve.py
+ chmod +x run/python-virtual/bin/shell_serve.py
+
+setuid: run/master_manager/setuid run/proxycache_manager/setuid run/worker_manager/setuid
+
+run/master_manager/setuid: src/server/master/master_manager/setuid/*
+ rm -rf run/master_manager
+ mkdir -p run/master_manager/setuid
+ cd src/server/master/master_manager/setuid; make clean; make
+ cp -a src/server/master/master_manager/setuid/run_* run/master_manager/setuid
+ rm run/master_manager/setuid/*.c
+
+run/proxycache_manager/setuid: src/server/proxycache/proxycache_manager/setuid/*
+ rm -rf run/proxycache_manager
+ mkdir -p run/proxycache_manager/setuid
+ cd src/server/proxycache/proxycache_manager/setuid; make clean; make
+ cp -a src/server/proxycache/proxycache_manager/setuid/run_* run/proxycache_manager/setuid
+ rm run/proxycache_manager/setuid/*.c
+
+run/worker_manager/setuid: src/server/worker/worker_manager/setuid/*
+ rm -rf run/worker_manager
+ mkdir -p run/worker_manager/setuid
+ cd src/server/worker/worker_manager/setuid; make clean; make
+ cp -a src/server/worker/worker_manager/setuid/run_* run/worker_manager/setuid
+ rm run/worker_manager/setuid/*.c
+
+clean:
+ rm -rf run
+ rm -rf src/server/master/management_database/temp src/server/master/management_database/build src/server/master/management_database/management_database.egg-info
+ rm -rf src/server/master/master_api/temp src/server/master/master_api/build src/server/master/master_api/master_api.egg-info
+ rm -rf src/server/shared/temp src/server/shared/build src/server/shared/djangy_server_shared.egg-info
+ -find * -name '*.pyc' | xargs rm
+ -find * -name '*~' | xargs rm
diff --git a/README b/README
new file mode 100644
index 0000000..e879cac
--- /dev/null
+++ b/README
@@ -0,0 +1,59 @@
+djangy.git layout
+=================
+
+docs@ -- symlink to user docs in web_ui/
+install/ -- used to install/deploy djangy to a host
+ conf/ -- configuration files installed on a host
+ apache/
+ git_hooks/
+ post_receive.py@
+ gitosis.conf
+ nginx.conf@
+ rc.local
+ ssh_keys/
+ ssl_keys/
+misc/
+src/
+ client/ -- code run by users on their own machine
+ server/
+ master/ -- code run on the master node
+ management_database/ -- used by master_manager, web_ui, web_api
+ master_api/ -- internal API used by web_api and web_ui
+ master_manager/ -- privileged operations of master_api
+ post_receive.py -- goes in git_hooks
+ web_api/ -- django project for API called by client
+ web_ui/ -- django project for website
+ proxycache/ -- code run on the frontend nginx proxy/cache nodes
+ nginx.conf
+ proxycache_manager/
+ shared/
+ lib/
+ worker/ -- code run on the application worker nodes
+ worker_manager/
+test/ -- test cases
+
+generated files
+===============
+
+run/ -- runtime environment; generated, not checked into repository
+ python-virtual/ -- used by all server components
+ master_manager/sbin/
+ proxycache_manager/sbin/
+ worker_manager/sbin/
+
+/srv layout
+===========
+
+/srv/
+ bundles/ 0711 root root
+ / 0550 bundles
+ djangy/ 0510 root djangy
+ gitosis/ 0700 gitosis gitosis
+ local_manager/ 0700 root root
+ logs/ 0710 root www-data
+ / 0710 root www-data
+
+Notes:
+ * djangy group = root, gitosis, www-data
+ * = -
+ * not 100% sure about all the permissions (e.g., logs)
diff --git a/install/README b/install/README
new file mode 100644
index 0000000..619a55d
--- /dev/null
+++ b/install/README
@@ -0,0 +1,2 @@
+Note: some subdirectories need to be populated with SSH and SSL keys before
+you can install. Please see the README files in the subdirectories.
diff --git a/install/S3.py b/install/S3.py
new file mode 100644
index 0000000..77691e6
--- /dev/null
+++ b/install/S3.py
@@ -0,0 +1,617 @@
+#!/usr/bin/env python
+
+# This software code is made available "AS IS" without warranties of any
+# kind. You may copy, display, modify and redistribute the software
+# code either by itself or as incorporated into your code; provided that
+# you do not remove any proprietary notices. Your use of this software
+# code is at your own risk and you waive any claim against Amazon
+# Digital Services, Inc. or its affiliates with respect to your use of
+# this software code. (c) 2006-2007 Amazon Digital Services, Inc. or its
+# affiliates.
+
+import base64
+import hmac
+import httplib
+import re
+import sha
+import sys
+import time
+import urllib
+import urlparse
+import xml.sax
+
+DEFAULT_HOST = 's3.amazonaws.com'
+PORTS_BY_SECURITY = { True: 443, False: 80 }
+METADATA_PREFIX = 'x-amz-meta-'
+AMAZON_HEADER_PREFIX = 'x-amz-'
+
+# generates the aws canonical string for the given parameters
+def canonical_string(method, bucket="", key="", query_args={}, headers={}, expires=None):
+ interesting_headers = {}
+ for header_key in headers:
+ lk = header_key.lower()
+ if lk in ['content-md5', 'content-type', 'date'] or lk.startswith(AMAZON_HEADER_PREFIX):
+ interesting_headers[lk] = headers[header_key].strip()
+
+ # these keys get empty strings if they don't exist
+ if not interesting_headers.has_key('content-type'):
+ interesting_headers['content-type'] = ''
+ if not interesting_headers.has_key('content-md5'):
+ interesting_headers['content-md5'] = ''
+
+ # just in case someone used this. it's not necessary in this lib.
+ if interesting_headers.has_key('x-amz-date'):
+ interesting_headers['date'] = ''
+
+ # if you're using expires for query string auth, then it trumps date
+ # (and x-amz-date)
+ if expires:
+ interesting_headers['date'] = str(expires)
+
+ sorted_header_keys = interesting_headers.keys()
+ sorted_header_keys.sort()
+
+ buf = "%s\n" % method
+ for header_key in sorted_header_keys:
+ if header_key.startswith(AMAZON_HEADER_PREFIX):
+ buf += "%s:%s\n" % (header_key, interesting_headers[header_key])
+ else:
+ buf += "%s\n" % interesting_headers[header_key]
+
+ # append the bucket if it exists
+ if bucket != "":
+ buf += "/%s" % bucket
+
+ # add the key. even if it doesn't exist, add the slash
+ buf += "/%s" % urllib.quote_plus(key)
+
+ # handle special query string arguments
+
+ if query_args.has_key("acl"):
+ buf += "?acl"
+ elif query_args.has_key("torrent"):
+ buf += "?torrent"
+ elif query_args.has_key("logging"):
+ buf += "?logging"
+ elif query_args.has_key("location"):
+ buf += "?location"
+
+ return buf
+
+# computes the base64'ed hmac-sha hash of the canonical string and the secret
+# access key, optionally urlencoding the result
+def encode(aws_secret_access_key, str, urlencode=False):
+ b64_hmac = base64.encodestring(hmac.new(aws_secret_access_key, str, sha).digest()).strip()
+ if urlencode:
+ return urllib.quote_plus(b64_hmac)
+ else:
+ return b64_hmac
+
+def merge_meta(headers, metadata):
+ final_headers = headers.copy()
+ for k in metadata.keys():
+ final_headers[METADATA_PREFIX + k] = metadata[k]
+
+ return final_headers
+
+# builds the query arg string
+def query_args_hash_to_string(query_args):
+ query_string = ""
+ pairs = []
+ for k, v in query_args.items():
+ piece = k
+ if v != None:
+ piece += "=%s" % urllib.quote_plus(str(v))
+ pairs.append(piece)
+
+ return '&'.join(pairs)
+
+
+class CallingFormat:
+ PATH = 1
+ SUBDOMAIN = 2
+ VANITY = 3
+
+ def build_url_base(protocol, server, port, bucket, calling_format):
+ url_base = '%s://' % protocol
+
+ if bucket == '':
+ url_base += server
+ elif calling_format == CallingFormat.SUBDOMAIN:
+ url_base += "%s.%s" % (bucket, server)
+ elif calling_format == CallingFormat.VANITY:
+ url_base += bucket
+ else:
+ url_base += server
+
+ url_base += ":%s" % port
+
+ if (bucket != '') and (calling_format == CallingFormat.PATH):
+ url_base += "/%s" % bucket
+
+ return url_base
+
+ build_url_base = staticmethod(build_url_base)
+
+
+
+class Location:
+ DEFAULT = None
+ EU = 'EU'
+
+
+
+class AWSAuthConnection:
+ def __init__(self, aws_access_key_id, aws_secret_access_key, is_secure=True,
+ server=DEFAULT_HOST, port=None, calling_format=CallingFormat.SUBDOMAIN):
+
+ if not port:
+ port = PORTS_BY_SECURITY[is_secure]
+
+ self.aws_access_key_id = aws_access_key_id
+ self.aws_secret_access_key = aws_secret_access_key
+ self.is_secure = is_secure
+ self.server = server
+ self.port = port
+ self.calling_format = calling_format
+
+ def create_bucket(self, bucket, headers={}):
+ return Response(self._make_request('PUT', bucket, '', {}, headers))
+
+ def create_located_bucket(self, bucket, location=Location.DEFAULT, headers={}):
+ if location == Location.DEFAULT:
+ body = ""
+ else:
+ body = "" + \
+ location + \
+ " "
+ return Response(self._make_request('PUT', bucket, '', {}, headers, body))
+
+ def check_bucket_exists(self, bucket):
+ return self._make_request('HEAD', bucket, '', {}, {})
+
+ def list_bucket(self, bucket, options={}, headers={}):
+ return ListBucketResponse(self._make_request('GET', bucket, '', options, headers))
+
+ def delete_bucket(self, bucket, headers={}):
+ return Response(self._make_request('DELETE', bucket, '', {}, headers))
+
+ def put(self, bucket, key, object, headers={}):
+ if not isinstance(object, S3Object):
+ object = S3Object(object)
+
+ return Response(
+ self._make_request(
+ 'PUT',
+ bucket,
+ key,
+ {},
+ headers,
+ object.data,
+ object.metadata))
+
+ def get(self, bucket, key, headers={}):
+ return GetResponse(
+ self._make_request('GET', bucket, key, {}, headers))
+
+ def delete(self, bucket, key, headers={}):
+ return Response(
+ self._make_request('DELETE', bucket, key, {}, headers))
+
+ def get_bucket_logging(self, bucket, headers={}):
+ return GetResponse(self._make_request('GET', bucket, '', { 'logging': None }, headers))
+
+ def put_bucket_logging(self, bucket, logging_xml_doc, headers={}):
+ return Response(self._make_request('PUT', bucket, '', { 'logging': None }, headers, logging_xml_doc))
+
+ def get_bucket_acl(self, bucket, headers={}):
+ return self.get_acl(bucket, '', headers)
+
+ def get_acl(self, bucket, key, headers={}):
+ return GetResponse(
+ self._make_request('GET', bucket, key, { 'acl': None }, headers))
+
+ def put_bucket_acl(self, bucket, acl_xml_document, headers={}):
+ return self.put_acl(bucket, '', acl_xml_document, headers)
+
+ def put_acl(self, bucket, key, acl_xml_document, headers={}):
+ return Response(
+ self._make_request(
+ 'PUT',
+ bucket,
+ key,
+ { 'acl': None },
+ headers,
+ acl_xml_document))
+
+ def list_all_my_buckets(self, headers={}):
+ return ListAllMyBucketsResponse(self._make_request('GET', '', '', {}, headers))
+
+ def get_bucket_location(self, bucket):
+ return LocationResponse(self._make_request('GET', bucket, '', {'location' : None}))
+
+ # end public methods
+
+ def _make_request(self, method, bucket='', key='', query_args={}, headers={}, data='', metadata={}):
+
+ server = ''
+ if bucket == '':
+ server = self.server
+ elif self.calling_format == CallingFormat.SUBDOMAIN:
+ server = "%s.%s" % (bucket, self.server)
+ elif self.calling_format == CallingFormat.VANITY:
+ server = bucket
+ else:
+ server = self.server
+
+ path = ''
+
+ if (bucket != '') and (self.calling_format == CallingFormat.PATH):
+ path += "/%s" % bucket
+
+ # add the slash after the bucket regardless
+ # the key will be appended if it is non-empty
+ path += "/%s" % urllib.quote_plus(key)
+
+
+ # build the path_argument string
+ # add the ? in all cases since
+ # signature and credentials follow path args
+ if len(query_args):
+ path += "?" + query_args_hash_to_string(query_args)
+
+ is_secure = self.is_secure
+ host = "%s:%d" % (server, self.port)
+ while True:
+ if (is_secure):
+ connection = httplib.HTTPSConnection(host)
+ else:
+ connection = httplib.HTTPConnection(host)
+
+ final_headers = merge_meta(headers, metadata);
+ # add auth header
+ self._add_aws_auth_header(final_headers, method, bucket, key, query_args)
+
+ connection.request(method, path, data, final_headers)
+ resp = connection.getresponse()
+ if resp.status < 300 or resp.status >= 400:
+ return resp
+ # handle redirect
+ location = resp.getheader('location')
+ if not location:
+ return resp
+ # (close connection)
+ resp.read()
+ scheme, host, path, params, query, fragment \
+ = urlparse.urlparse(location)
+ if scheme == "http": is_secure = True
+ elif scheme == "https": is_secure = False
+ else: raise invalidURL("Not http/https: " + location)
+ if query: path += "?" + query
+ # retry with redirect
+
+ def _add_aws_auth_header(self, headers, method, bucket, key, query_args):
+ if not headers.has_key('Date'):
+ headers['Date'] = time.strftime("%a, %d %b %Y %X GMT", time.gmtime())
+
+ c_string = canonical_string(method, bucket, key, query_args, headers)
+ headers['Authorization'] = \
+ "AWS %s:%s" % (self.aws_access_key_id, encode(self.aws_secret_access_key, c_string))
+
+
+class QueryStringAuthGenerator:
+ # by default, expire in 1 minute
+ DEFAULT_EXPIRES_IN = 60
+
+ def __init__(self, aws_access_key_id, aws_secret_access_key, is_secure=True,
+ server=DEFAULT_HOST, port=None, calling_format=CallingFormat.SUBDOMAIN):
+
+ if not port:
+ port = PORTS_BY_SECURITY[is_secure]
+
+ self.aws_access_key_id = aws_access_key_id
+ self.aws_secret_access_key = aws_secret_access_key
+ if (is_secure):
+ self.protocol = 'https'
+ else:
+ self.protocol = 'http'
+
+ self.is_secure = is_secure
+ self.server = server
+ self.port = port
+ self.calling_format = calling_format
+ self.__expires_in = QueryStringAuthGenerator.DEFAULT_EXPIRES_IN
+ self.__expires = None
+
+ # for backwards compatibility with older versions
+ self.server_name = "%s:%s" % (self.server, self.port)
+
+ def set_expires_in(self, expires_in):
+ self.__expires_in = expires_in
+ self.__expires = None
+
+ def set_expires(self, expires):
+ self.__expires = expires
+ self.__expires_in = None
+
+ def create_bucket(self, bucket, headers={}):
+ return self.generate_url('PUT', bucket, '', {}, headers)
+
+ def list_bucket(self, bucket, options={}, headers={}):
+ return self.generate_url('GET', bucket, '', options, headers)
+
+ def delete_bucket(self, bucket, headers={}):
+ return self.generate_url('DELETE', bucket, '', {}, headers)
+
+ def put(self, bucket, key, object, headers={}):
+ if not isinstance(object, S3Object):
+ object = S3Object(object)
+
+ return self.generate_url(
+ 'PUT',
+ bucket,
+ key,
+ {},
+ merge_meta(headers, object.metadata))
+
+ def get(self, bucket, key, headers={}):
+ return self.generate_url('GET', bucket, key, {}, headers)
+
+ def delete(self, bucket, key, headers={}):
+ return self.generate_url('DELETE', bucket, key, {}, headers)
+
+ def get_bucket_logging(self, bucket, headers={}):
+ return self.generate_url('GET', bucket, '', { 'logging': None }, headers)
+
+ def put_bucket_logging(self, bucket, logging_xml_doc, headers={}):
+ return self.generate_url('PUT', bucket, '', { 'logging': None }, headers)
+
+ def get_bucket_acl(self, bucket, headers={}):
+ return self.get_acl(bucket, '', headers)
+
+ def get_acl(self, bucket, key='', headers={}):
+ return self.generate_url('GET', bucket, key, { 'acl': None }, headers)
+
+ def put_bucket_acl(self, bucket, acl_xml_document, headers={}):
+ return self.put_acl(bucket, '', acl_xml_document, headers)
+
+ # don't really care what the doc is here.
+ def put_acl(self, bucket, key, acl_xml_document, headers={}):
+ return self.generate_url('PUT', bucket, key, { 'acl': None }, headers)
+
+ def list_all_my_buckets(self, headers={}):
+ return self.generate_url('GET', '', '', {}, headers)
+
+ def make_bare_url(self, bucket, key=''):
+ full_url = self.generate_url(self, bucket, key)
+ return full_url[:full_url.index('?')]
+
+ def generate_url(self, method, bucket='', key='', query_args={}, headers={}):
+ expires = 0
+ if self.__expires_in != None:
+ expires = int(time.time() + self.__expires_in)
+ elif self.__expires != None:
+ expires = int(self.__expires)
+ else:
+ raise "Invalid expires state"
+
+ canonical_str = canonical_string(method, bucket, key, query_args, headers, expires)
+ encoded_canonical = encode(self.aws_secret_access_key, canonical_str)
+
+ url = CallingFormat.build_url_base(self.protocol, self.server, self.port, bucket, self.calling_format)
+
+ url += "/%s" % urllib.quote_plus(key)
+
+ query_args['Signature'] = encoded_canonical
+ query_args['Expires'] = expires
+ query_args['AWSAccessKeyId'] = self.aws_access_key_id
+
+ url += "?%s" % query_args_hash_to_string(query_args)
+
+ return url
+
+
+class S3Object:
+ def __init__(self, data, metadata={}):
+ self.data = data
+ self.metadata = metadata
+
+class Owner:
+ def __init__(self, id='', display_name=''):
+ self.id = id
+ self.display_name = display_name
+
+class ListEntry:
+ def __init__(self, key='', last_modified=None, etag='', size=0, storage_class='', owner=None):
+ self.key = key
+ self.last_modified = last_modified
+ self.etag = etag
+ self.size = size
+ self.storage_class = storage_class
+ self.owner = owner
+
+class CommonPrefixEntry:
+ def __init(self, prefix=''):
+ self.prefix = prefix
+
+class Bucket:
+ def __init__(self, name='', creation_date=''):
+ self.name = name
+ self.creation_date = creation_date
+
+class Response:
+ def __init__(self, http_response):
+ self.http_response = http_response
+ # you have to do this read, even if you don't expect a body.
+ # otherwise, the next request fails.
+ self.body = http_response.read()
+ if http_response.status >= 300 and self.body:
+ self.message = self.body
+ else:
+ self.message = "%03d %s" % (http_response.status, http_response.reason)
+
+
+
+class ListBucketResponse(Response):
+ def __init__(self, http_response):
+ Response.__init__(self, http_response)
+ if http_response.status < 300:
+ handler = ListBucketHandler()
+ xml.sax.parseString(self.body, handler)
+ self.entries = handler.entries
+ self.common_prefixes = handler.common_prefixes
+ self.name = handler.name
+ self.marker = handler.marker
+ self.prefix = handler.prefix
+ self.is_truncated = handler.is_truncated
+ self.delimiter = handler.delimiter
+ self.max_keys = handler.max_keys
+ self.next_marker = handler.next_marker
+ else:
+ self.entries = []
+
+class ListAllMyBucketsResponse(Response):
+ def __init__(self, http_response):
+ Response.__init__(self, http_response)
+ if http_response.status < 300:
+ handler = ListAllMyBucketsHandler()
+ xml.sax.parseString(self.body, handler)
+ self.entries = handler.entries
+ else:
+ self.entries = []
+
+class GetResponse(Response):
+ def __init__(self, http_response):
+ Response.__init__(self, http_response)
+ response_headers = http_response.msg # older pythons don't have getheaders
+ metadata = self.get_aws_metadata(response_headers)
+ self.object = S3Object(self.body, metadata)
+
+ def get_aws_metadata(self, headers):
+ metadata = {}
+ for hkey in headers.keys():
+ if hkey.lower().startswith(METADATA_PREFIX):
+ metadata[hkey[len(METADATA_PREFIX):]] = headers[hkey]
+ del headers[hkey]
+
+ return metadata
+
+class LocationResponse(Response):
+ def __init__(self, http_response):
+ Response.__init__(self, http_response)
+ if http_response.status < 300:
+ handler = LocationHandler()
+ xml.sax.parseString(self.body, handler)
+ self.location = handler.location
+
+class ListBucketHandler(xml.sax.ContentHandler):
+ def __init__(self):
+ self.entries = []
+ self.curr_entry = None
+ self.curr_text = ''
+ self.common_prefixes = []
+ self.curr_common_prefix = None
+ self.name = ''
+ self.marker = ''
+ self.prefix = ''
+ self.is_truncated = False
+ self.delimiter = ''
+ self.max_keys = 0
+ self.next_marker = ''
+ self.is_echoed_prefix_set = False
+
+ def startElement(self, name, attrs):
+ if name == 'Contents':
+ self.curr_entry = ListEntry()
+ elif name == 'Owner':
+ self.curr_entry.owner = Owner()
+ elif name == 'CommonPrefixes':
+ self.curr_common_prefix = CommonPrefixEntry()
+
+
+ def endElement(self, name):
+ if name == 'Contents':
+ self.entries.append(self.curr_entry)
+ elif name == 'CommonPrefixes':
+ self.common_prefixes.append(self.curr_common_prefix)
+ elif name == 'Key':
+ self.curr_entry.key = self.curr_text
+ elif name == 'LastModified':
+ self.curr_entry.last_modified = self.curr_text
+ elif name == 'ETag':
+ self.curr_entry.etag = self.curr_text
+ elif name == 'Size':
+ self.curr_entry.size = int(self.curr_text)
+ elif name == 'ID':
+ self.curr_entry.owner.id = self.curr_text
+ elif name == 'DisplayName':
+ self.curr_entry.owner.display_name = self.curr_text
+ elif name == 'StorageClass':
+ self.curr_entry.storage_class = self.curr_text
+ elif name == 'Name':
+ self.name = self.curr_text
+ elif name == 'Prefix' and self.is_echoed_prefix_set:
+ self.curr_common_prefix.prefix = self.curr_text
+ elif name == 'Prefix':
+ self.prefix = self.curr_text
+ self.is_echoed_prefix_set = True
+ elif name == 'Marker':
+ self.marker = self.curr_text
+ elif name == 'IsTruncated':
+ self.is_truncated = self.curr_text == 'true'
+ elif name == 'Delimiter':
+ self.delimiter = self.curr_text
+ elif name == 'MaxKeys':
+ self.max_keys = int(self.curr_text)
+ elif name == 'NextMarker':
+ self.next_marker = self.curr_text
+
+ self.curr_text = ''
+
+ def characters(self, content):
+ self.curr_text += content
+
+
+class ListAllMyBucketsHandler(xml.sax.ContentHandler):
+ def __init__(self):
+ self.entries = []
+ self.curr_entry = None
+ self.curr_text = ''
+
+ def startElement(self, name, attrs):
+ if name == 'Bucket':
+ self.curr_entry = Bucket()
+
+ def endElement(self, name):
+ if name == 'Name':
+ self.curr_entry.name = self.curr_text
+ elif name == 'CreationDate':
+ self.curr_entry.creation_date = self.curr_text
+ elif name == 'Bucket':
+ self.entries.append(self.curr_entry)
+
+ def characters(self, content):
+ self.curr_text = content
+
+
+class LocationHandler(xml.sax.ContentHandler):
+ def __init__(self):
+ self.location = None
+ self.state = 'init'
+
+ def startElement(self, name, attrs):
+ if self.state == 'init':
+ if name == 'LocationConstraint':
+ self.state = 'tag_location'
+ self.location = ''
+ else: self.state = 'bad'
+ else: self.state = 'bad'
+
+ def endElement(self, name):
+ if self.state == 'tag_location' and name == 'LocationConstraint':
+ self.state = 'done'
+ else: self.state = 'bad'
+
+ def characters(self, content):
+ if self.state == 'tag_location':
+ self.location += content
diff --git a/install/apache.py b/install/apache.py
new file mode 100644
index 0000000..36359e7
--- /dev/null
+++ b/install/apache.py
@@ -0,0 +1,20 @@
+import os.path
+from core import *
+
+@print_when_used
+def require_apache():
+ run_ignore_failure('/etc/init.d/apache2', 'stop')
+ run('a2enmod', 'ssl')
+ require_file('/etc/apache2/ports.conf', 'root', 'root', 0644, contents=read_file('conf/apache/ports.conf'), overwrite=True)
+ for (site, apache_conf) in [('000-defaults', '/srv/djangy/install/conf/apache/000-defaults/config/apache.conf'), \
+ ('api.djangy.com', '/srv/djangy/src/server/master/web_api/config/apache.conf' ), \
+ ('djangy.com', '/srv/djangy/src/server/master/web_ui/config/apache.conf' )]:
+ assert os.path.exists(apache_conf)
+ require_link(os.path.join('/etc/apache2/sites-available', site), apache_conf)
+ require_link(os.path.join('/etc/apache2/sites-enabled', site), os.path.join('/etc/apache2/sites-available', site))
+ run_ignore_failure('/etc/init.d/apache2', 'start')
+
+@print_when_used
+def require_no_apache():
+ if os.path.isfile('/etc/init.d/apache2'):
+ run_ignore_failure('/etc/init.d/apache2', 'stop')
diff --git a/install/application_uids_gids.py b/install/application_uids_gids.py
new file mode 100644
index 0000000..d1ae0c0
--- /dev/null
+++ b/install/application_uids_gids.py
@@ -0,0 +1,126 @@
+import re
+from core import *
+
+_UID_GID_BASE = 100000
+
+def _is_application_uid(uid):
+ return (uid >= _UID_GID_BASE)
+
+_username_regex = re.compile('^([swc])([1-9][0-9]*)$')
+
+def _assert_valid_application_user(username, uid, gid, homedir, shell):
+ match = _username_regex.match(username)
+ user_type = match.group(1)
+ n = int(match.group(2))
+ if user_type == 's':
+ assert uid == _setup_uid(n)
+ elif user_type == 'w':
+ assert uid == _web_uid(n)
+ elif user_type == 'c':
+ assert uid == _cron_uid(n)
+ else:
+ assert False
+ assert gid == _application_gid(n)
+ assert homedir == '/'
+ assert shell == '/bin/sh'
+
+def _setup_uid(n):
+ return 3*(n-1) + _UID_GID_BASE
+
+def _web_uid(n):
+ return _setup_uid(n) + 1
+
+def _cron_uid(n):
+ return _setup_uid(n) + 2
+
+def _is_application_gid(gid):
+ return (gid >= _UID_GID_BASE)
+
+_groupname_regex = re.compile('^g([1-9][0-9]*)$')
+
+def _assert_valid_application_group(groupname, gid, member_usernames):
+ n = int(_groupname_regex.match(groupname).group(1))
+ assert gid == _application_gid(n)
+ assert member_usernames == set(['www-data'])
+
+def _application_gid(n):
+ return 3*(n-1) + _UID_GID_BASE
+
+def _get_existing_application_uids():
+ file = open('/etc/passwd', 'r')
+ existing_application_uids = set()
+ for line in file.readlines():
+ try:
+ line = line[:-1]
+ (username, x, uid, gid, description, homedir, shell) = line.split(':')
+ uid = int(uid)
+ gid = int(gid)
+ if _is_application_uid(uid):
+ _assert_valid_application_user(username, uid, gid, homedir, shell)
+ existing_application_uids.add(uid)
+ else:
+ assert None == _username_regex.match(username)
+ except ValueError:
+ print 'malformed /etc/passwd entry "%s"' % line
+ file.close()
+ return existing_application_uids
+
+def _get_existing_application_gids():
+ file = open('/etc/group', 'r')
+ existing_application_groups = set()
+ for line in file.readlines():
+ try:
+ line = line[:-1]
+ (groupname, x, gid, member_usernames) = line.split(':')
+ gid = int(gid)
+ if _is_application_gid(gid):
+ _assert_valid_application_group(groupname, gid, set(member_usernames.split(',')))
+ existing_application_groups.add(gid)
+ else:
+ assert None == _groupname_regex.match(groupname)
+ except ValueError:
+ print 'malformed /etc/group entry "%s"' % line
+ file.close()
+ return existing_application_groups
+
+# Returns (etc_passwd_entries, etc_shadow_entries, etc_group_entries)
+def _get_application_entries():
+ existing_application_uids = _get_existing_application_uids()
+ existing_application_gids = _get_existing_application_gids()
+ etc_passwd_entries = []
+ etc_shadow_entries = []
+ etc_group_entries = []
+ for n in range(1, 20000+1):
+ gid = _application_gid(n)
+ setup_uid = _setup_uid(n)
+ web_uid = _web_uid(n)
+ cron_uid = _cron_uid(n)
+ if setup_uid not in existing_application_uids:
+ etc_passwd_entries.append('s%i:x:%i:%i::/:/bin/sh' % (n, setup_uid, gid))
+ etc_shadow_entries.append('s%i:*:0:0:99999:7:::' % n)
+ if web_uid not in existing_application_uids:
+ etc_passwd_entries.append('w%i:x:%i:%i::/:/bin/sh' % (n, web_uid, gid))
+ etc_shadow_entries.append('w%i:*:0:0:99999:7:::' % n)
+ if cron_uid not in existing_application_uids:
+ etc_passwd_entries.append('c%i:x:%i:%i::/:/bin/sh' % (n, cron_uid, gid))
+ etc_shadow_entries.append('c%i:*:0:0:99999:7:::' % n)
+ if gid not in existing_application_gids:
+ etc_group_entries.append('g%i:x:%i:www-data' % (n, gid))
+ return (etc_passwd_entries, etc_shadow_entries, etc_group_entries)
+
+def _file_append_entries(file_path, entries):
+ if len(entries) > 0:
+ print "Adding %i entries to %s" % (len(entries), file_path)
+ buf = '\n'.join(entries + [''])
+ file = open(file_path, 'a')
+ file.write(buf)
+ file.close()
+ else:
+ print "%s already populated" % file_path
+
+@print_when_used
+def require_application_uids_gids():
+ (etc_passwd_entries, etc_shadow_entries, etc_group_entries) = _get_application_entries()
+ _file_append_entries('/etc/passwd', etc_passwd_entries)
+ _file_append_entries('/etc/shadow', etc_shadow_entries)
+ _file_append_entries('/etc/group', etc_group_entries)
diff --git a/install/backup.py b/install/backup.py
new file mode 100755
index 0000000..5e70da4
--- /dev/null
+++ b/install/backup.py
@@ -0,0 +1,19 @@
+#! /usr/bin/env python
+import subprocess, dump_archive, os
+from s3put import *
+
+# Makes a dump of the master node and uploads it to S3
+
+def main():
+ print "Dumping archive...",
+ filename = dump_archive.main()
+ print "Done."
+ print "Uploading to S3...",
+ upload(filename)
+ print "Done."
+ print "Cleaning up...",
+ os.remove(filename)
+ print "Done."
+
+if __name__ == '__main__':
+ main()
diff --git a/install/conf/apache/000-defaults/config/apache.conf b/install/conf/apache/000-defaults/config/apache.conf
new file mode 100644
index 0000000..af5b658
--- /dev/null
+++ b/install/conf/apache/000-defaults/config/apache.conf
@@ -0,0 +1,25 @@
+
+ ServerAdmin support@djangy.com
+
+ DocumentRoot /srv/djangy/install/conf/apache/000-defaults/content/
+
+ ErrorLog /srv/logs/000-defaults/error.log
+ CustomLog /srv/logs/000-defaults/access.log combined
+
+ RedirectMatch temp / https://www.djangy.com/
+
+
+
+ ServerAdmin support@djangy.com
+
+ DocumentRoot /srv/djangy/install/conf/apache/000-defaults/content/
+
+ ErrorLog /srv/logs/000-defaults/error.log
+ CustomLog /srv/logs/000-defaults/access.log combined
+
+ SSLEngine on
+ SSLCertificateFile /srv/djangy/install/conf/ssl_keys/djangy.com.crt
+ SSLCertificateKeyFile /srv/djangy/install/conf/ssl_keys/djangy.com.key
+
+ RedirectMatch temp / https://www.djangy.com/
+
diff --git a/install/conf/apache/000-defaults/content/index.html b/install/conf/apache/000-defaults/content/index.html
new file mode 100644
index 0000000..e69de29
diff --git a/install/conf/apache/ports.conf b/install/conf/apache/ports.conf
new file mode 100644
index 0000000..ff5eb51
--- /dev/null
+++ b/install/conf/apache/ports.conf
@@ -0,0 +1,24 @@
+# If you just change the port or add more ports here, you will likely also
+# have to change the VirtualHost statement in
+# /etc/apache2/sites-enabled/000-default
+# This is also true if you have upgraded from before 2.2.9-3 (i.e. from
+# Debian etch). See /usr/share/doc/apache2.2-common/NEWS.Debian.gz and
+# README.Debian.gz
+
+NameVirtualHost *:8080
+Listen 8080
+
+
+ # If you add NameVirtualHost *:443 here, you will also have to change
+ # the VirtualHost statement in /etc/apache2/sites-available/default-ssl
+ # to
+ # Server Name Indication for SSL named virtual hosts is currently not
+ # supported by MSIE on Windows XP.
+ NameVirtualHost *:443
+ Listen 443
+
+
+
+ Listen 443
+
+
diff --git a/install/conf/crontab b/install/conf/crontab
new file mode 100644
index 0000000..c339739
--- /dev/null
+++ b/install/conf/crontab
@@ -0,0 +1,17 @@
+# /etc/crontab: system-wide crontab
+# Unlike any other crontab you don't have to run the `crontab'
+# command to install the new version when you edit this file
+# and files in /etc/cron.d. These files also have username fields,
+# that none of the other crontabs do.
+
+SHELL=/bin/sh
+PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
+
+# m h dom mon dow user command
+17 * * * * root cd / && run-parts --report /etc/cron.hourly
+25 6 * * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
+47 6 * * 7 root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
+52 6 1 * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
+#
+01 2 * * * root cd /srv/djangy/install && python backup.py
+01 3 * * * root /srv/djangy/jobs/report_billing.py
diff --git a/install/conf/etc_ssh/README b/install/conf/etc_ssh/README
new file mode 100644
index 0000000..8de465e
--- /dev/null
+++ b/install/conf/etc_ssh/README
@@ -0,0 +1,8 @@
+Include the following files in this directory so that newly-installed Djangy
+hosts have a known ssh key. This is necessary to prevent prompting whether
+the ssh key is valid when the master manager connects to a new host.
+
+ssh_host_dsa_key
+ssh_host_dsa_key.pub
+ssh_host_rsa_key
+ssh_host_rsa_key.pub
diff --git a/install/conf/git_hooks/post_receive.py b/install/conf/git_hooks/post_receive.py
new file mode 120000
index 0000000..60cb158
--- /dev/null
+++ b/install/conf/git_hooks/post_receive.py
@@ -0,0 +1 @@
+../../../src/server/master/master_manager/post_receive.py
\ No newline at end of file
diff --git a/install/conf/gitosis.conf b/install/conf/gitosis.conf
new file mode 100644
index 0000000..b79e94e
--- /dev/null
+++ b/install/conf/gitosis.conf
@@ -0,0 +1,5 @@
+[gitosis]
+
+[group gitosis-admin]
+writable = gitosis-admin
+members = root@djangy.com
diff --git a/install/conf/mysql/my.cnf.new b/install/conf/mysql/my.cnf.new
new file mode 100644
index 0000000..2fb8d16
--- /dev/null
+++ b/install/conf/mysql/my.cnf.new
@@ -0,0 +1,132 @@
+#
+# The MySQL database server configuration file.
+#
+# You can copy this to one of:
+# - "/etc/mysql/my.cnf" to set global options,
+# - "~/.my.cnf" to set user-specific options.
+#
+# One can use all long options that the program supports.
+# Run program with --help to get a list of available options and with
+# --print-defaults to see which it would actually understand and use.
+#
+# For explanations see
+# http://dev.mysql.com/doc/mysql/en/server-system-variables.html
+
+# This will be passed to all mysql clients
+# It has been reported that passwords should be enclosed with ticks/quotes
+# escpecially if they contain "#" chars...
+# Remember to edit /etc/mysql/debian.cnf when changing the socket location.
+[client]
+port = 3306
+socket = /var/run/mysqld/mysqld.sock
+
+# Here is entries for some specific programs
+# The following values assume you have at least 32M ram
+
+# This was formally known as [safe_mysqld]. Both versions are currently parsed.
+[mysqld_safe]
+socket = /var/run/mysqld/mysqld.sock
+nice = 0
+
+[mysqld]
+#
+# * Basic Settings
+#
+
+default-storage-engine = INNODB
+
+#
+# * IMPORTANT
+# If you make changes to these settings and your system uses apparmor, you may
+# also need to also adjust /etc/apparmor.d/usr.sbin.mysqld.
+#
+
+user = mysql
+socket = /var/run/mysqld/mysqld.sock
+port = 3306
+basedir = /usr
+datadir = /var/lib/mysql
+tmpdir = /tmp
+skip-external-locking
+#
+# Instead of skip-networking the default is now to listen only on
+# localhost which is more compatible and is not less secure.
+bind-address = 0.0.0.0
+#
+# * Fine Tuning
+#
+key_buffer = 16M
+max_allowed_packet = 16M
+thread_stack = 192K
+thread_cache_size = 8
+# This replaces the startup script and checks MyISAM tables if needed
+# the first time they are touched
+myisam-recover = BACKUP
+#max_connections = 100
+#table_cache = 64
+#thread_concurrency = 10
+#
+# * Query Cache Configuration
+#
+query_cache_limit = 1M
+query_cache_size = 16M
+#
+# * Logging and Replication
+#
+# Both location gets rotated by the cronjob.
+# Be aware that this log type is a performance killer.
+# As of 5.1 you can enable the log at runtime!
+#general_log_file = /var/log/mysql/mysql.log
+#general_log = 1
+
+log_error = /var/log/mysql/error.log
+
+# Here you can see queries with especially long duration
+#log_slow_queries = /var/log/mysql/mysql-slow.log
+#long_query_time = 2
+#log-queries-not-using-indexes
+#
+# The following can be used as easy to replay backup logs or for replication.
+# note: if you are setting up a replication slave, see README.Debian about
+# other settings you may need to change.
+#server-id = 1
+#log_bin = /var/log/mysql/mysql-bin.log
+expire_logs_days = 10
+max_binlog_size = 100M
+#binlog_do_db = include_database_name
+#binlog_ignore_db = include_database_name
+#
+# * InnoDB
+#
+# InnoDB is enabled by default with a 10MB datafile in /var/lib/mysql/.
+# Read the manual for more InnoDB related options. There are many!
+#
+# * Security Features
+#
+# Read the manual, too, if you want chroot!
+# chroot = /var/lib/mysql/
+#
+# For generating SSL certificates I recommend the OpenSSL GUI "tinyca".
+#
+# ssl-ca=/etc/mysql/cacert.pem
+# ssl-cert=/etc/mysql/server-cert.pem
+# ssl-key=/etc/mysql/server-key.pem
+
+
+
+[mysqldump]
+quick
+quote-names
+max_allowed_packet = 16M
+
+[mysql]
+#no-auto-rehash # faster start of mysql but no tab completition
+
+[isamchk]
+key_buffer = 16M
+
+#
+# * IMPORTANT: Additional settings that can override those from this file!
+# The files must end with '.cnf', otherwise they'll be ignored.
+#
+!includedir /etc/mysql/conf.d/
diff --git a/install/conf/mysql/my.cnf.orig b/install/conf/mysql/my.cnf.orig
new file mode 100644
index 0000000..92b55c2
--- /dev/null
+++ b/install/conf/mysql/my.cnf.orig
@@ -0,0 +1,130 @@
+#
+# The MySQL database server configuration file.
+#
+# You can copy this to one of:
+# - "/etc/mysql/my.cnf" to set global options,
+# - "~/.my.cnf" to set user-specific options.
+#
+# One can use all long options that the program supports.
+# Run program with --help to get a list of available options and with
+# --print-defaults to see which it would actually understand and use.
+#
+# For explanations see
+# http://dev.mysql.com/doc/mysql/en/server-system-variables.html
+
+# This will be passed to all mysql clients
+# It has been reported that passwords should be enclosed with ticks/quotes
+# escpecially if they contain "#" chars...
+# Remember to edit /etc/mysql/debian.cnf when changing the socket location.
+[client]
+port = 3306
+socket = /var/run/mysqld/mysqld.sock
+
+# Here is entries for some specific programs
+# The following values assume you have at least 32M ram
+
+# This was formally known as [safe_mysqld]. Both versions are currently parsed.
+[mysqld_safe]
+socket = /var/run/mysqld/mysqld.sock
+nice = 0
+
+[mysqld]
+#
+# * Basic Settings
+#
+
+#
+# * IMPORTANT
+# If you make changes to these settings and your system uses apparmor, you may
+# also need to also adjust /etc/apparmor.d/usr.sbin.mysqld.
+#
+
+user = mysql
+socket = /var/run/mysqld/mysqld.sock
+port = 3306
+basedir = /usr
+datadir = /var/lib/mysql
+tmpdir = /tmp
+skip-external-locking
+#
+# Instead of skip-networking the default is now to listen only on
+# localhost which is more compatible and is not less secure.
+bind-address = 127.0.0.1
+#
+# * Fine Tuning
+#
+key_buffer = 16M
+max_allowed_packet = 16M
+thread_stack = 192K
+thread_cache_size = 8
+# This replaces the startup script and checks MyISAM tables if needed
+# the first time they are touched
+myisam-recover = BACKUP
+#max_connections = 100
+#table_cache = 64
+#thread_concurrency = 10
+#
+# * Query Cache Configuration
+#
+query_cache_limit = 1M
+query_cache_size = 16M
+#
+# * Logging and Replication
+#
+# Both location gets rotated by the cronjob.
+# Be aware that this log type is a performance killer.
+# As of 5.1 you can enable the log at runtime!
+#general_log_file = /var/log/mysql/mysql.log
+#general_log = 1
+
+log_error = /var/log/mysql/error.log
+
+# Here you can see queries with especially long duration
+#log_slow_queries = /var/log/mysql/mysql-slow.log
+#long_query_time = 2
+#log-queries-not-using-indexes
+#
+# The following can be used as easy to replay backup logs or for replication.
+# note: if you are setting up a replication slave, see README.Debian about
+# other settings you may need to change.
+#server-id = 1
+#log_bin = /var/log/mysql/mysql-bin.log
+expire_logs_days = 10
+max_binlog_size = 100M
+#binlog_do_db = include_database_name
+#binlog_ignore_db = include_database_name
+#
+# * InnoDB
+#
+# InnoDB is enabled by default with a 10MB datafile in /var/lib/mysql/.
+# Read the manual for more InnoDB related options. There are many!
+#
+# * Security Features
+#
+# Read the manual, too, if you want chroot!
+# chroot = /var/lib/mysql/
+#
+# For generating SSL certificates I recommend the OpenSSL GUI "tinyca".
+#
+# ssl-ca=/etc/mysql/cacert.pem
+# ssl-cert=/etc/mysql/server-cert.pem
+# ssl-key=/etc/mysql/server-key.pem
+
+
+
+[mysqldump]
+quick
+quote-names
+max_allowed_packet = 16M
+
+[mysql]
+#no-auto-rehash # faster start of mysql but no tab completition
+
+[isamchk]
+key_buffer = 16M
+
+#
+# * IMPORTANT: Additional settings that can override those from this file!
+# The files must end with '.cnf', otherwise they'll be ignored.
+#
+!includedir /etc/mysql/conf.d/
diff --git a/install/conf/proxycache_manager/500.html b/install/conf/proxycache_manager/500.html
new file mode 100644
index 0000000..26ccc4a
--- /dev/null
+++ b/install/conf/proxycache_manager/500.html
@@ -0,0 +1,145 @@
+
+
+
+
+ Application Error
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/install/conf/proxycache_manager/502.html b/install/conf/proxycache_manager/502.html
new file mode 100644
index 0000000..5ad2f24
--- /dev/null
+++ b/install/conf/proxycache_manager/502.html
@@ -0,0 +1,151 @@
+
+
+
+
+ Application Error
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/install/conf/proxycache_manager/nginx.conf b/install/conf/proxycache_manager/nginx.conf
new file mode 120000
index 0000000..39d6a82
--- /dev/null
+++ b/install/conf/proxycache_manager/nginx.conf
@@ -0,0 +1 @@
+../../../src/server/proxycache/nginx.conf
\ No newline at end of file
diff --git a/install/conf/rc.local b/install/conf/rc.local
new file mode 100755
index 0000000..96ace54
--- /dev/null
+++ b/install/conf/rc.local
@@ -0,0 +1,16 @@
+#!/bin/sh -e
+#
+# rc.local
+#
+# This script is executed at the end of each multiuser runlevel.
+# Make sure that the script will "exit 0" on success or any other
+# value on error.
+#
+# In order to enable or disable this script just change the execution
+# bits.
+#
+# By default this script does nothing.
+
+/srv/proxycache_manager/nginx/sbin/nginx
+
+exit 0
diff --git a/install/conf/ssh_keys/README b/install/conf/ssh_keys/README
new file mode 100644
index 0000000..d10906d
--- /dev/null
+++ b/install/conf/ssh_keys/README
@@ -0,0 +1,7 @@
+Include the following files in this directory so that root can ssh to Djangy
+hosts. (Note that we ssh as root to perform host-wide administrative
+functions anyway, so there wouldn't be much benefit to using separate,
+non-administrative users.)
+
+root_key
+root_key.pub
diff --git a/install/conf/ssh_keys/root.gitconfig b/install/conf/ssh_keys/root.gitconfig
new file mode 100644
index 0000000..4255b65
--- /dev/null
+++ b/install/conf/ssh_keys/root.gitconfig
@@ -0,0 +1,3 @@
+[user]
+ name = Djangy Root
+ email = root@djangy.com
diff --git a/install/conf/ssh_keys/ssh_config b/install/conf/ssh_keys/ssh_config
new file mode 100644
index 0000000..c23f7c1
--- /dev/null
+++ b/install/conf/ssh_keys/ssh_config
@@ -0,0 +1,2 @@
+Host *
+ StrictHostKeyChecking=no
diff --git a/install/conf/ssh_keys/www.gitconfig b/install/conf/ssh_keys/www.gitconfig
new file mode 100644
index 0000000..8f380c8
--- /dev/null
+++ b/install/conf/ssh_keys/www.gitconfig
@@ -0,0 +1,3 @@
+[user]
+ name = Djangy Apache
+ email = www-data@djangy.com
diff --git a/install/conf/ssl_keys/README b/install/conf/ssl_keys/README
new file mode 100644
index 0000000..d247ec3
--- /dev/null
+++ b/install/conf/ssl_keys/README
@@ -0,0 +1,9 @@
+Put your SSL certificates and public keys in here. Ideally you should use a
+wildcard SSL certificate, so that it works for subdomains too. Note that
+you will need to modify the nginx-related configuration files and the
+installer itself in /install to use your domain instead of djangy.com
+
+djangy.com.crt
+djangy.com.key
+www.djangy.com.crt
+www.djangy.com.key
diff --git a/install/config.py b/install/config.py
new file mode 100644
index 0000000..a4db5b9
--- /dev/null
+++ b/install/config.py
@@ -0,0 +1,33 @@
+# Configuration options set by command-line arguments to install.py
+ACTION = None # 'install' or 'upgrade'
+MASTER_NODE = False
+WORKER_NODE = False
+PROXYCACHE_NODE = False
+MASTER_MANAGER_HOST = None
+DEFAULT_PROXYCACHE_HOST = None
+DEFAULT_DATABASE_HOST = None
+WORKERHOSTS = []
+PRODUCTION = False
+TO_SOUTH = False
+
+RABBITMQ_THIS_HOST = None
+RABBITMQ_LEADER_HOST = None
+
+# Configuration options set in config.py
+DB_ROOT_PASSWORD = 'password goes here'
+MASTER_DATABASES = [
+ # (username, password, dbname)
+ ('djangy', 'password goes here', 'djangy'),
+ ('web_ui', 'password goes here', 'web_ui'),
+ ('web_api', 'password goes here', 'web_api')
+]
+
+# S3 access stuff
+S3_ACCESS_KEY = 'password goes here'
+S3_SECRET = 'password goes here'
+S3_BUCKET = 'djangy_backups'
+
+# Billing stuff
+DEVPAYMENTS_TESTING = 'password goes here'
+DEVPAYMENTS_PRODUCTION = 'password goes here'
+DEVPAYMENTS_API_KEY = DEVPAYMENTS_TESTING
diff --git a/install/core.py b/install/core.py
new file mode 100644
index 0000000..4c11273
--- /dev/null
+++ b/install/core.py
@@ -0,0 +1,324 @@
+import grp, os, os.path, pwd, shutil, subprocess, tempfile
+
+
+# Decorator to print out a status message boxing the output of a function.
+# Useful for long running functions or those that print output.
+def print_when_used(func):
+ def _line(char):
+ return ''.join([char for i in range(0, 80/len(char))])
+ def print_when_used(*args):
+ print _line('=')
+ print func.__name__ + str(args)
+ print _line('- ')
+ try:
+ return func(*args)
+ finally:
+ print _line('-')
+ return print_when_used
+
+# Decorator to run a function in a temporary directory, and then delete the
+# temporary directory when the function returns (or throws an exception).
+def in_tempdir(func):
+ def in_tempdir(*args, **kwargs):
+ tempdir = tempfile.mkdtemp(prefix='djangy_install_')
+ assert tempdir.startswith('/tmp/djangy_install_')
+ old_dir = os.getcwd()
+ try:
+ os.chdir(tempdir)
+ return func(*args, **kwargs)
+ finally:
+ os.chdir(old_dir)
+ shutil.rmtree(tempdir)
+ return in_tempdir
+
+# Decorator to run a function in a given (static) directory
+def in_dir(dir):
+ def in_dir(func):
+ def in_dir(*args, **kwargs):
+ old_dir = os.getcwd()
+ os.chdir(dir)
+ try:
+ return func(*args, **kwargs)
+ finally:
+ os.chdir(old_dir)
+ return in_dir
+ return in_dir
+
+# For use as "with cd(dir): ..."
+class cd(object):
+ def __init__(self, dir_path):
+ self._dir_path = dir_path
+ def __enter__(self):
+ self._old_dir_path = os.getcwd()
+ os.chdir(self._dir_path)
+ def __exit__(self, type, value, traceback):
+ os.chdir(self._old_dir_path)
+
+# Run an external program, fail if it returns non-zero
+def run(*args):
+ assert 0 == subprocess.call(list(args))
+
+# Run an external program supplying stdin contents, fail if it returns non-zero
+def run_with_stdin(args, stdin=None):
+ p = subprocess.Popen(args, stdin=subprocess.PIPE)
+ p.stdin.write(stdin)
+ p.stdin.close()
+ assert 0 == p.wait()
+
+# Run an external program, ignore its return value
+def run_ignore_failure(*args):
+ subprocess.call(list(args))
+
+# Check that a user exists, with the given settings.
+# Settings with value None are not checked.
+def user_exists(username=None, uid=None, gid=None, homedir=None, shell=None):
+ try:
+ passwd = pwd.getpwnam(username)
+ except:
+ try:
+ passwd = pwd.getpwuid(uid)
+ except:
+ return False
+
+ return not (
+ (uid and passwd.pw_uid != uid ) or
+ (gid and passwd.pw_gid != gid ) or
+ (homedir and passwd.pw_dir != homedir ) or
+ (shell and passwd.pw_shell != shell ))
+
+# Check that a group exists, with the given settings.
+# Settings with value None are not checked.
+def group_exists(groupname=None, gid=None, member_usernames=None):
+ try:
+ group = grp.getgrnam(groupname)
+ except:
+ try:
+ group = grp.getgrgid(gid)
+ except:
+ return False
+
+ return not (
+ (gid and group.gr_gid != gid ) or
+ (member_usernames and set(group.gr_mem) != set(member_usernames)))
+
+# Try to allocate a fresh UID.
+# Raises UidAllocationException.
+def _get_fresh_uid():
+ for uid in range(100, 1000):
+ try:
+ pwd.getpwuid(uid)
+ except KeyError:
+ return uid
+ raise UidAllocationException()
+
+# Try to allocate a fresh GID.
+# Raises UidAllocationException.
+def _get_fresh_gid():
+ for gid in range(100, 1000):
+ try:
+ grp.getgrgid(gid)
+ except KeyError:
+ return gid
+ raise GidAllocationException()
+
+# Called by require_user() to update /etc/passwd and /etc/shadow
+# Called by require_group() to update /etc/group
+def _append_to_file(file_path, line):
+ file = open(file_path, 'a')
+ file.write(line + '\n')
+ file.close()
+
+# Called by require_file() to create a file that doesn't exist but
+# whose contents are specified.
+def _create_file(file_path, contents):
+ file = open(file_path, 'w')
+ file.write(contents)
+ file.close()
+
+# Called by require_file() to read and check a file's contents.
+# Also called by install.py
+def read_file(file_path):
+ file = open(file_path, 'r')
+ contents = file.read()
+ file.close()
+ return contents
+
+# Check that a user exists with the given settings, creating one if
+# necessary and possible. Raises RequireUserException
+def require_user(username, gid=None, groupname=None, uid=None, homedir=None, shell=None, description=None, create=True):
+ # Canonicalize arguments
+ if gid == None:
+ gid = grp.getgrnam(groupname).gr_gid
+ # Create user if it doesn't exist
+ if create and not user_exists(username=username):
+ if uid != None:
+ if user_exists(uid=uid):
+ raise RequireUserException(username)
+ else:
+ uid = _get_fresh_uid()
+ etc_passwd_line = '%s:x:%i:%i:%s:%s:%s' % (username, uid, gid, description or '', homedir or '/', shell)
+ etc_shadow_line = '%s:*:14907:0:99999:7:::' % username
+ _append_to_file('/etc/passwd', etc_passwd_line)
+ _append_to_file('/etc/shadow', etc_shadow_line)
+ # Check user has correct settings
+ if not user_exists(username=username, uid=uid, gid=gid, homedir=homedir, shell=shell):
+ raise RequireUserException(username)
+
+# Check that a group exists with the given settings, creating one if
+# necessary and possible. member_usernames must match exactly.
+# Raises RequireGroupException
+def require_group(groupname, gid=None, member_usernames=[], create=True):
+ # Create group if it doesn't exist
+ if create and not group_exists(groupname=groupname):
+ if gid != None:
+ if group_exists(gid=gid):
+ raise RequireGroupException(groupname)
+ else:
+ gid = _get_fresh_gid()
+ etc_group_line = '%s:x:%i:%s' % (groupname, gid, ','.join(member_usernames))
+ _append_to_file('/etc/group', etc_group_line)
+ # Check group has correct settings
+ if not group_exists(groupname=groupname, gid=gid, member_usernames=member_usernames):
+ raise RequireGroupException(groupname)
+
+@print_when_used
+def _copy_directory(dir_path, initial_contents_path):
+ run('cp', '-r', initial_contents_path, dir_path)
+ print 'Done.'
+
+# Check that a directory exists with the given settings, creating one if
+# necessary and possible.
+def require_directory(dir_path, username, groupname, mode, initial_contents_path=None, create=True):
+ # Canonicalize arguments
+ dir_path = os.path.abspath(dir_path)
+ uid = pwd.getpwnam(username).pw_uid
+ gid = grp.getgrnam(groupname).gr_gid
+ # Create directory if it doesn't exist
+ if create and not os.path.isdir(dir_path):
+ if initial_contents_path:
+ _copy_directory(dir_path, initial_contents_path)
+ else:
+ os.mkdir(dir_path, mode)
+ # Ensure correct access permissions
+ os.chown(dir_path, uid, gid)
+ os.chmod(dir_path, mode)
+
+# Check that a given file exists with the given settings. If the
+# initial_contents are specified and file does not exist, then it is
+# created.
+# Raises RequireFileException
+def require_file(file_path, username, groupname, mode, contents=None, initial_contents=None, overwrite=False):
+ # Canonicalize arguments
+ file_path = os.path.abspath(file_path)
+ uid = pwd.getpwnam(username).pw_uid
+ gid = grp.getgrnam(groupname).gr_gid
+ if initial_contents == None:
+ initial_contents = contents
+ # Create file if it doesn't exist or overwrite==True
+ if type(initial_contents) == str and (overwrite and os.path.isfile(file_path) or not os.path.exists(file_path)):
+ _create_file(file_path, initial_contents)
+ # Check file exists
+ if not os.path.isfile(file_path) or (contents != None and read_file(file_path) != contents):
+ raise RequireFileException(file_path)
+ # Ensure correct access permissions
+ os.chown(file_path, uid, gid)
+ os.chmod(file_path, mode)
+
+# Check that a given link exists and points to a given source path, creating
+# the link if possible and neccessary. Raises RequireLinkException
+def require_link(link_path, source_path):
+ # Canonicalize arguments
+ link_path = os.path.abspath(link_path)
+ source_path = os.path.abspath(source_path)
+ # Create link if it doesn't exist or is already a link
+ if os.path.islink(link_path):
+ os.remove(link_path)
+ if not os.path.exists(link_path):
+ os.symlink(source_path, link_path)
+ else:
+ raise RequireLinkException(link_path)
+
+# Make sure that a given file or directory has been removed.
+def require_remove(path):
+ if os.path.exists(path):
+ run('rm', '-rf', path)
+
+# Ensure that a given directory and all its recursive contents are owned by
+# a given username/groupname. Raises RequirePermisException
+def require_recursive(root_path, username=None, groupname=None):
+ # Canonicalize arguments
+ root_path = os.path.abspath(root_path)
+ # Call external program to do it
+ try:
+ run('chown', '-R', '%s:%s' % (username, groupname), root_path)
+ except:
+ raise RequirePermsException(root_path)
+
+# Raises RequireUbuntuPackagesException
+@print_when_used
+def require_ubuntu_packages(*packages):
+ if subprocess.call(['apt-get', '--yes', 'install'] + list(packages)) != 0:
+ raise RequireUbuntuPackagesException(packages)
+
+# Raises RequirePythonPackagesException
+@print_when_used
+def require_python_packages(*packages):
+ for package in packages:
+ if subprocess.call(['easy_install', package]) != 0:
+ raise RequirePythonPackagesException([package])
+
+# Failure exceptions below.
+
+class RequireUserException(Exception):
+ def __init__(self, username):
+ self.username = username
+ def __str__(self):
+ return 'RequireUserException(username=\'%s\')' % self.username
+
+class RequireGroupException(Exception):
+ def __init__(self, groupname):
+ self.groupname = groupname
+ def __str__(self):
+ return 'RequireGroupException(groupname=\'%s\')' % self.groupname
+
+class RequireFileException(Exception):
+ def __init__(self, file_path):
+ self.file_path = file_path
+ def __str__(self):
+ return 'RequireFileException(file_path=\'%s\')' % self.file_path
+
+class RequireLinkException(Exception):
+ def __init__(self, link_path):
+ self.link_path = link_path
+ def __str__(self):
+ return 'RequireLinkException(link_path=\'%s\')' % self.link_path
+
+class RequirePermsException(Exception):
+ def __init__(self, root_path):
+ self.root_path = root_path
+ def __str__(self):
+ return 'RequirePermsException(root_path=\'%s\')' % self.root_path
+
+class RequireUbuntuPackagesException(Exception):
+ def __init__(self, packages):
+ self.packages = packages
+ def __str__(self):
+ return 'RequireUbuntuPackagesException(packages=%s)' % str(self.packages)
+
+class RequirePythonPackagesException(Exception):
+ def __init__(self, package):
+ self.package = package
+ def __str__(self):
+ return 'RequirePythonPackagesException(package=\'%s\')' % self.package
+
+class UidAllocationException(Exception):
+ def __init__(self):
+ pass
+ def __str__(self):
+ return 'UidAllocationException(): could not find a free system UID'
+
+class GidAllocationException(Exception):
+ def __init__(self):
+ pass
+ def __str__(self):
+ return 'GidAllocationException(): could not find a free system GID'
diff --git a/install/database.py b/install/database.py
new file mode 100644
index 0000000..9ee3d05
--- /dev/null
+++ b/install/database.py
@@ -0,0 +1,111 @@
+import os.path
+import config
+from core import *
+
+_PYTHON = '/srv/djangy/run/python-virtual/bin/python'
+
+def require_database():
+ if config.MASTER_NODE:
+ _configure_mysql_server()
+ else:
+ run_ignore_failure('service', 'mysql', 'stop')
+ if config.MASTER_NODE:
+ _create_databases(config.MASTER_DATABASES)
+ _syncdb_and_migrate()
+ if config.ACTION == 'install' and config.MASTER_NODE:
+ _load_admins()
+ _load_docs()
+ if config.MASTER_NODE:
+ _load_chargables()
+ _load_subscription_types()
+ if len(config.WORKERHOSTS) > 0:
+ _load_workerhosts()
+
+@print_when_used
+def _configure_mysql_server():
+ try:
+ require_file('/etc/mysql/my.cnf', 'root', 'root', 0644, contents=read_file('conf/mysql/my.cnf.new'))
+ except:
+ require_file('/etc/mysql/my.cnf', 'root', 'root', 0644, contents=read_file('conf/mysql/my.cnf.orig'))
+ require_file('/etc/mysql/my.cnf', 'root', 'root', 0644, contents=read_file('conf/mysql/my.cnf.new'), overwrite=True)
+ run('service', 'mysql', 'restart')
+
+@print_when_used
+def _create_databases(databases):
+ for user, password, db in databases:
+ cmd1 = 'CREATE DATABASE IF NOT EXISTS %s;' % db
+ cmd2 = 'GRANT ALL ON %s.* TO %s@\'%%\' IDENTIFIED BY \'%s\';' % (db, user, password)
+ run_with_stdin(['mysql', '-u', 'root', '-p%s' % config.DB_ROOT_PASSWORD], stdin=cmd1+cmd2)
+
+def _syncdb_and_migrate():
+ if config.WORKER_NODE:
+ _syncdb('/srv/djangy/src/server/worker/worker_manager/orm')
+ if config.TO_SOUTH:
+ _migrate('/srv/djangy/src/server/worker/worker_manager/orm', 'orm', '0001', '--fake')
+ _migrate('/srv/djangy/src/server/worker/worker_manager/orm', 'orm')
+
+ if config.MASTER_NODE:
+ _syncdb('/srv/djangy/src/server/master/web_ui/application/web_ui')
+ _syncdb('/srv/djangy/src/server/master/web_api/application/web_api')
+ _syncdb('/srv/djangy/src/server/master/management_database/management_database')
+
+ if config.MASTER_NODE:
+ _migrate('/srv/djangy/src/server/master/management_database/management_database', 'management_database')
+ _migrate('/srv/djangy/src/server/master/web_ui/application/web_ui', 'main')
+ _migrate('/srv/djangy/src/server/master/web_ui/application/web_ui', 'docs')
+ _migrate('/srv/djangy/src/server/master/web_ui/application/web_ui', 'management_database', 'zero')
+
+@print_when_used
+def _syncdb(dir_path):
+ with cd(dir_path):
+ run(_PYTHON, 'manage.py', 'syncdb', '--noinput')
+ print 'Done.'
+
+@print_when_used
+def _migrate(dir_path, application_name, *args):
+ with cd(dir_path):
+ command = [_PYTHON, 'manage.py', 'migrate', application_name] + list(args)
+ run(*command)
+ print 'Done.'
+
+@print_when_used
+def _load_admins():
+ with cd('/srv/djangy/src/server/master/management_database/management_database'):
+ run(_PYTHON, 'manage.py', 'loaddata', 'loadadmins.yaml')
+ print 'Done.'
+
+@print_when_used
+def _load_chargables():
+ with cd('/srv/djangy/src/server/master/management_database/management_database'):
+ run(_PYTHON, 'manage.py', 'loaddata', 'loadchargables.yaml')
+ print 'Done.'
+
+@print_when_used
+def _load_subscription_types():
+ with cd('/srv/djangy/src/server/master/management_database/management_database'):
+ run(_PYTHON, 'manage.py', 'loaddata', 'loadsubscriptiontypes.yaml')
+ print 'Done.'
+
+@print_when_used
+def _load_docs():
+ with cd('/srv/djangy/src/server/master/web_ui/application/web_ui'):
+ run(_PYTHON, 'manage.py', 'loaddata', 'docs/wiki_docs.yaml')
+ print 'Done.'
+
+@in_tempdir
+@print_when_used
+def _load_workerhosts():
+ file = open('load_workerhosts.yaml', 'w')
+ pk = 1
+ for workerhost in config.WORKERHOSTS:
+ file.write('- model: management_database.WorkerHost\n')
+ file.write(' pk: %i\n' % pk)
+ file.write(' fields:\n')
+ file.write(' host: %s\n' % workerhost)
+ file.write(' max_procs: 100\n\n')
+ pk = pk+1
+ file.close()
+ yaml_path = os.path.abspath('load_workerhosts.yaml')
+ with cd('/srv/djangy/src/server/master/management_database/management_database'):
+ run(_PYTHON, 'manage.py', 'loaddata', yaml_path)
+ print 'Done.'
diff --git a/install/dump_archive.py b/install/dump_archive.py
new file mode 100755
index 0000000..28359c6
--- /dev/null
+++ b/install/dump_archive.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python
+#
+# Dump an archive of a master node. This Python file is self-contained, so
+# you can copy it onto a machine without having to pull the whole git
+# repository.
+#
+# Usage: dump_archive.py
+# Creates an archive file called djangy_dump_YYYY-MM-DD_hh-mm-ss.fff.tar.gz
+# in the current directory.
+#
+import os, os.path, re, shutil, subprocess, sys, tempfile, time
+
+# This is the MySQL root password on the old master node whose state you're
+# archiving, which might not be the same as the latest root password.
+_MYSQL_ROOT_PASSWORD = 'password goes here'
+
+def main():
+ return dump_archive(os.path.abspath('.'))
+
+################################################################################
+# Decorators copied from core.py so that dump_archive.py is self-contained.
+
+# Decorator to print out a status message boxing the output of a function.
+# Useful for long running functions or those that print output.
+def print_when_used(func):
+ def _line(char):
+ return ''.join([char for i in range(0, 80/len(char))])
+ def print_when_used(*args):
+ print _line('=')
+ print func.__name__ + str(args)
+ print _line('- ')
+ try:
+ return func(*args)
+ finally:
+ print _line('-')
+ return print_when_used
+
+# Decorator to run a function in a temporary directory, and then delete the
+# temporary directory when the function returns (or throws an exception).
+def in_tempdir(func):
+ def in_tempdir(*args, **kwargs):
+ tempdir = tempfile.mkdtemp(prefix='djangy_install_')
+ assert tempdir.startswith('/tmp/djangy_install_')
+ old_dir = os.getcwd()
+ try:
+ os.chdir(tempdir)
+ return func(*args, **kwargs)
+ finally:
+ os.chdir(old_dir)
+ shutil.rmtree(tempdir)
+ return in_tempdir
+
+################################################################################
+
+# Creates a timestamp of the form:
+# YYYY-MM-DD_hh-mm-ss.fff
+def make_text_timestamp(numeric_time=None):
+ if numeric_time == None:
+ numeric_time = time.time()
+ time_struct = time.gmtime(numeric_time)
+ fractional_seconds = int((numeric_time % 1) * 1000)
+ return '%04i-%02i-%02i_%02i-%02i-%02i.%03i' \
+ % (time_struct.tm_year, time_struct.tm_mon, time_struct.tm_mday, \
+ time_struct.tm_hour, time_struct.tm_min, time_struct.tm_sec, \
+ fractional_seconds)
+
+@in_tempdir
+def dump_archive(dest_dir_path):
+ dump_name = 'djangy_dump_%s' % make_text_timestamp()
+ dest_file_path = os.path.join(dest_dir_path, '%s.tar.gz' % dump_name)
+ # Create contents for archive
+ os.mkdir(dump_name)
+ create_mysql_dump(os.path.join(dump_name, 'all-databases.mysqldump'))
+ create_archive(os.path.join(dump_name, 'git-repositories.tar.gz'), '/srv/git')
+ # Create archive
+ create_archive(dest_file_path, dump_name)
+ return dest_file_path
+
+@print_when_used
+def create_mysql_dump(mysql_dump_file_path):
+ # The MySQL dump could be pretty big, so it's easier to just use the
+ # shell to redirect output.
+ assert 0 == subprocess.call(['mysqldump -u root -p%s --all-databases > %s' % (_MYSQL_ROOT_PASSWORD, mysql_dump_file_path)], shell=True)
+ print 'Done.'
+
+@print_when_used
+def create_archive(dest_tar_file_path, src_path):
+ assert 0 == subprocess.call(['tar', '-czf', dest_tar_file_path, src_path])
+ print 'Done.'
+
+
+if __name__ == '__main__':
+ main()
diff --git a/install/git_serve.py b/install/git_serve.py
new file mode 100644
index 0000000..2afe010
--- /dev/null
+++ b/install/git_serve.py
@@ -0,0 +1,19 @@
+import os, os.path, subprocess
+from core import *
+
+_POST_RECEIVE_HOOK_PATH='/srv/djangy/run/python-virtual/bin/post_receive.py'
+
+@print_when_used
+def require_no_git_serve():
+ require_remove('/srv/git/.ssh')
+
+@print_when_used
+def require_git_serve():
+ # Update system-wide post-receive hook template
+ require_link('/usr/share/git-core/templates/hooks/post-receive', _POST_RECEIVE_HOOK_PATH)
+ # Update post-receive hooks in all repositories
+ for repo_name in os.listdir('/srv/git/repositories'):
+ repo_post_receive = os.path.join('/srv/git/repositories', repo_name, 'hooks/post-receive')
+ require_link(repo_post_receive, _POST_RECEIVE_HOOK_PATH)
+ # Ownership permissions
+ require_recursive('/srv/git', username='git', groupname='git')
diff --git a/install/install.py b/install/install.py
new file mode 100755
index 0000000..3e7ac39
--- /dev/null
+++ b/install/install.py
@@ -0,0 +1,176 @@
+#!/usr/bin/env python
+import os, sys
+import config
+from core import *
+from apache import *
+from application_uids_gids import *
+from git_serve import *
+from nginx import *
+from database import *
+
+def equals_single_arg(arg):
+ return arg.split('=', 1)[1]
+
+def equals_multiple_args(arg):
+ return arg.split('=', 1)[1].split(',')
+
+# Command-line arguments
+if len(sys.argv) >= 2:
+ if (sys.argv[1] == 'install' or sys.argv[1] == 'upgrade'):
+ config.ACTION = sys.argv[1]
+ config.MASTER_NODE = 'master' in sys.argv[2:]
+ config.WORKER_NODE = 'worker' in sys.argv[2:]
+ config.PROXYCACHE_NODE = 'proxycache' in sys.argv[2:]
+ for arg in sys.argv[2:]:
+ if arg.startswith('--master-manager-host='):
+ config.MASTER_MANAGER_HOST = equals_single_arg(arg)
+ if arg.startswith('--default-database-host='):
+ config.DEFAULT_DATABASE_HOST = equals_single_arg(arg)
+ if arg.startswith('--default-proxycache-host='):
+ config.DEFAULT_PROXYCACHE_HOST = equals_single_arg(arg)
+ if arg.startswith('--to-south'):
+ config.TO_SOUTH = True
+ if arg.startswith('--workerhosts='):
+ if config.ACTION != 'install':
+ print '"--workerhosts=" argument is only valid for action "install"'
+ sys.exit(1)
+ config.WORKERHOSTS.extend(equals_multiple_args(arg))
+ if arg.startswith('--production'):
+ config.PRODUCTION = True
+ config.DEVPAYMENTS_API_KEY = config.DEVPAYMENTS_PRODUCTION
+if not config.ACTION or not config.MASTER_MANAGER_HOST or not config.DEFAULT_DATABASE_HOST or not config.DEFAULT_PROXYCACHE_HOST:
+ print 'Usage: sudo install.py { install | upgrade } [master] [worker] [proxycache] --master-manager-host= --default-database-host= --default-proxycache-host= [--workerhosts=,,...] [--production] [--to-south]'
+ sys.exit(1)
+
+# Find the source code
+_DJANGY_CODE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+os.chdir(os.path.dirname(os.path.abspath(__file__)))
+
+# Automate setup of mysql
+run_with_stdin(['debconf-set-selections'], stdin='mysql-server-5.0 mysql-server/root_password password %s\n' % config.DB_ROOT_PASSWORD)
+run_with_stdin(['debconf-set-selections'], stdin='mysql-server-5.0 mysql-server/root_password_again password %s\n' % config.DB_ROOT_PASSWORD)
+
+require_ubuntu_packages('apache2', 'apache2-dev', 'build-essential', 'bzr',
+ 'cron', 'gcc', 'git-core', 'joe', 'libapache2-mod-wsgi',
+ 'libfreetype6-dev', 'libjpeg-dev', 'libyaml-dev', 'mercurial',
+ 'mysql-server', 'openssh-server', 'python', 'python-dev',
+ 'python-mysqldb', 'python-setuptools', 'python-sqlite', 'python-xapian',
+ 'python-yaml', 'rsync', 'sqlite3', 'subversion', 'vim')
+
+require_python_packages('Django==1.2.1', 'Fabric==0.9.1', 'Mako==0.3.4',
+ 'PIL==1.1.7', 'South==0.7.2', 'django-sentry==1.0.9',
+ 'gunicorn==0.11.1', 'simplejson==2.1.1', 'virtualenv==1.4.9')
+
+require_user('root', uid=0, gid=0, homedir='/root', create=False)
+require_directory('/root', 'root', 'root', 0700)
+require_directory('/root/.ssh', 'root', 'root', 0700)
+require_file('/root/.ssh/id_rsa', 'root', 'root', 0600, contents=read_file('conf/ssh_keys/root_key'), overwrite=True)
+require_file('/root/.ssh/id_rsa.pub', 'root', 'root', 0600, contents=read_file('conf/ssh_keys/root_key.pub'), overwrite=True)
+require_file('/root/.ssh/authorized_keys', 'root', 'root', 0600, contents=read_file('conf/ssh_keys/root_key.pub'), overwrite=True)
+require_file('/root/.ssh/config', 'root', 'root', 0600, contents=read_file('conf/ssh_keys/ssh_config'), overwrite=True)
+if config.MASTER_NODE and config.PRODUCTION:
+ require_file('/etc/ssh/ssh_host_dsa_key', 'root', 'root', 0600, contents=read_file('conf/etc_ssh/ssh_host_dsa_key'), overwrite=True)
+ require_file('/etc/ssh/ssh_host_dsa_key.pub', 'root', 'root', 0644, contents=read_file('conf/etc_ssh/ssh_host_dsa_key.pub'), overwrite=True)
+ require_file('/etc/ssh/ssh_host_rsa_key', 'root', 'root', 0600, contents=read_file('conf/etc_ssh/ssh_host_rsa_key'), overwrite=True)
+ require_file('/etc/ssh/ssh_host_rsa_key.pub', 'root', 'root', 0644, contents=read_file('conf/etc_ssh/ssh_host_rsa_key.pub'), overwrite=True)
+ require_file('/etc/crontab', 'root', 'root', 0644, contents=read_file('conf/crontab'), overwrite=True)
+require_group('www-data', create=False)
+require_user('www-data', groupname='www-data', homedir='/var/www', create=False)
+
+require_group('git')
+require_user('git', groupname='git', homedir='/srv/git', shell='/bin/sh', description='git version control')
+require_group('proxycache')
+require_user('proxycache', groupname='proxycache', homedir='/srv/proxycache_manager', shell='/bin/sh', description='proxy cache manager')
+
+require_group('shell')
+require_user('shell', groupname='shell', homedir='/srv/shell', shell='/bin/sh', description='shell account')
+require_directory('/srv/shell', 'shell', 'shell', 0700)
+require_directory('/srv/shell/.ssh', 'shell', 'shell', 0700)
+
+require_group('djangy', member_usernames=['www-data', 'git', 'shell'])
+
+if config.PRODUCTION:
+ require_directory('/proc', 'root', 'admin', 0550, create=False)
+
+require_directory('/srv', 'root', 'root', 0711)
+require_directory('/srv/bundles', 'root', 'root', 0711)
+if config.ACTION == 'install':
+ assert not os.path.exists('/srv/djangy')
+else:
+ assert config.ACTION == 'upgrade'
+require_remove('/srv/djangy')
+require_directory('/srv/djangy', 'root', 'djangy', 0710, initial_contents_path=_DJANGY_CODE_PATH)
+require_file ('/srv/djangy/src/server/shared/djangy_server_shared/installer_configured_constants.py', 'root', 'djangy', 0644, overwrite=True,
+ contents = (('DEFAULT_DATABASE_HOST = \'%s\'\n' % config.DEFAULT_DATABASE_HOST) +
+ ('DEFAULT_PROXYCACHE_HOST = \'%s\'\n' % config.DEFAULT_PROXYCACHE_HOST) +
+ ('MASTER_MANAGER_HOST = \'%s\'\n' % config.MASTER_MANAGER_HOST) +
+ ('DEVPAYMENTS_API_KEY = \'%s\'\n' % config.DEVPAYMENTS_API_KEY)))
+if config.MASTER_NODE:
+ require_directory('/srv/git', 'git', 'git', 0710)
+ require_directory('/srv/git/.ssh', 'git', 'git', 0700)
+ require_directory('/srv/git/repositories', 'git', 'git', 0710)
+
+require_directory('/srv/logs', 'root', 'root', 0711)
+if config.MASTER_NODE:
+ require_directory('/srv/logs/api.djangy.com', 'root', 'www-data', 0710)
+ require_file ('/srv/logs/api.djangy.com/django.log', 'www-data', 'www-data', 0600, initial_contents='')
+ require_directory('/srv/logs/djangy.com', 'root', 'www-data', 0710)
+ require_file ('/srv/logs/djangy.com/django.log', 'www-data', 'www-data', 0600, initial_contents='')
+ require_directory('/srv/logs/000-defaults', 'root', 'www-data', 0710)
+ require_file ('/srv/logs/000-defaults/access.log', 'www-data', 'www-data', 0600, initial_contents='')
+ require_file ('/srv/logs/000-defaults/error.log', 'www-data', 'www-data', 0600, initial_contents='')
+ require_file ('/srv/logs/master_api.log', 'www-data', 'www-data', 0600, initial_contents='')
+
+if config.PROXYCACHE_NODE:
+ if config.ACTION == 'install':
+ assert not os.path.exists('/srv/proxycache_manager')
+ require_directory('/srv/proxycache_manager', 'proxycache', 'proxycache', 0700)
+require_directory('/srv/scratch', 'root', 'root', 0700)
+if config.WORKER_NODE:
+ if config.ACTION == 'install':
+ assert not os.path.exists('/srv/worker_manager')
+ require_directory('/srv/worker_manager', 'root', 'root', 0700)
+
+@print_when_used
+def require_make_djangy():
+ with cd('/srv/djangy'):
+ run('make', 'clean')
+ run('make')
+
+require_application_uids_gids()
+require_make_djangy()
+require_database()
+
+if config.MASTER_NODE:
+ require_git_serve()
+else:
+ require_no_git_serve()
+
+if config.MASTER_NODE:
+ require_apache()
+else:
+ require_no_apache()
+
+if config.PROXYCACHE_NODE:
+ require_nginx()
+else:
+ require_no_nginx()
+
+# Create an upgrade script in /srv/upgrade
+require_file('/srv/upgrade', 'root', 'root', 0700, contents="""#!/bin/bash
+./install.py upgrade %(master)s %(worker)s %(proxycache)s \
+--master-manager-host=%(master-manager-host)s \
+--default-database-host=%(default-database-host)s \
+--default-proxycache-host=%(default-proxycache-host)s \
+%(production)s\
+""" % {
+ 'master' : 'master' if config.MASTER_NODE else '',
+ 'worker' : 'worker' if config.WORKER_NODE else '',
+ 'proxycache' : 'proxycache' if config.PROXYCACHE_NODE else '',
+ 'master-manager-host' : config.MASTER_MANAGER_HOST,
+ 'default-database-host' : config.DEFAULT_DATABASE_HOST,
+ 'default-proxycache-host': config.DEFAULT_PROXYCACHE_HOST,
+ 'production' : 'production' if config.PRODUCTION else ''
+}, overwrite=True)
+
+# Rebuild bundles and deploy applications...
diff --git a/install/load_archive.py b/install/load_archive.py
new file mode 100755
index 0000000..37141c4
--- /dev/null
+++ b/install/load_archive.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+import os, os.path, re, subprocess, sys, time
+from core import *
+import config
+
+def main():
+ if len(sys.argv) != 2:
+ print 'Usage: load_archive.py djangy_dump_YYYY-MM-DD_hh-mm-ss.fff.tar.gz'
+ sys.exit(1)
+ src_file_path = os.path.abspath(sys.argv[1])
+ assert os.path.isfile(src_file_path)
+ assert not os.path.exists('/srv/git')
+ load_archive(src_file_path)
+
+@in_tempdir
+def load_archive(src_file_path):
+ run('tar', '-xzf', src_file_path)
+ dir_contents = os.listdir('.')
+ assert len(dir_contents) == 1 and os.path.isdir(dir_contents[0])
+ os.chdir(dir_contents[0])
+ load_mysql_dump(os.path.abspath('all-databases.mysqldump'))
+ extract_git_repositories(os.path.abspath('git-repositories.tar.gz'))
+
+@print_when_used
+def load_mysql_dump(mysql_dump_file_path):
+ run_with_stdin( ['mysql', '-u', 'root', '-p%s' % config.DB_ROOT_PASSWORD], stdin='DROP DATABASE djangy;')
+ subprocess.call(['mysql -u root -p%s < %s' % (config.DB_ROOT_PASSWORD, mysql_dump_file_path)], shell=True)
+ run_with_stdin( ['mysql', '-u', 'root', '-p%s' % config.DB_ROOT_PASSWORD, 'djangy'], stdin='DELETE FROM process; FLUSH PRIVILEGES;')
+ print 'Done.'
+
+@print_when_used
+def extract_git_repositories(git_repositories_tar_file_path):
+ git_repositories_tar_file_path = os.path.abspath(git_repositories_tar_file_path)
+ os.chdir('/')
+ run('tar', '-xzf', git_repositories_tar_file_path, 'srv/git')
+ print 'Done.'
+
+if __name__ == '__main__':
+ main()
diff --git a/install/nginx.py b/install/nginx.py
new file mode 100644
index 0000000..fdd91bc
--- /dev/null
+++ b/install/nginx.py
@@ -0,0 +1,55 @@
+import os, os.path
+from core import *
+
+_NGINX_VERSION = 'nginx-0.8.52'
+_NGINX_INSTALL_PATH = os.path.join('/srv/proxycache_manager', _NGINX_VERSION)
+_NGINX_BIN_PATH = '/srv/proxycache_manager/nginx/sbin/nginx'
+
+def require_no_nginx():
+ if os.path.isfile(_NGINX_BIN_PATH):
+ run_ignore_failure(_NGINX_BIN_PATH, '-s', 'quit')
+
+def require_nginx():
+ require_group('proxycache')
+ require_user('proxycache', groupname='proxycache', homedir='/srv/proxycache_manager', shell='/bin/sh', description='proxy cache manager')
+ require_directory('/srv/proxycache_manager', 'proxycache', 'proxycache', 0700)
+ require_install_nginx()
+ require_configure_nginx()
+ start_nginx()
+
+def require_install_nginx():
+ if not os.path.exists(_NGINX_INSTALL_PATH):
+ _install_nginx()
+
+@in_tempdir
+@print_when_used
+def _install_nginx():
+ # Cache this file locally?
+ run('wget', 'http://sysoev.ru/nginx/%s.tar.gz' % _NGINX_VERSION)
+ run('tar', '-xzf', '%s.tar.gz' % _NGINX_VERSION)
+ os.chdir(_NGINX_VERSION)
+ run('./configure', '--prefix=%s' % _NGINX_INSTALL_PATH)
+ run('make')
+ require_directory(_NGINX_INSTALL_PATH, 'proxycache', 'proxycache', 0700)
+ run('make', 'install')
+
+def require_configure_nginx():
+ require_remove(os.path.join(_NGINX_INSTALL_PATH, 'conf/nginx.conf.default'))
+ require_directory(_NGINX_INSTALL_PATH, 'proxycache', 'proxycache', 0700)
+ require_directory(os.path.join(_NGINX_INSTALL_PATH, 'conf/applications'), 'proxycache', 'proxycache', 0700)
+ require_directory(os.path.join(_NGINX_INSTALL_PATH, 'cache'), 'proxycache', 'proxycache', 0700)
+ require_file(os.path.join(_NGINX_INSTALL_PATH, 'conf/nginx.conf'), 'proxycache', 'proxycache', 0600, contents=read_file('conf/proxycache_manager/nginx.conf'), overwrite=True)
+ require_link('/srv/proxycache_manager/nginx', _NGINX_INSTALL_PATH)
+ require_recursive(_NGINX_INSTALL_PATH, username='proxycache', groupname='proxycache')
+ require_file('/etc/rc.local', 'root', 'root', 0700, contents=read_file('conf/rc.local'), overwrite=True)
+ require_file('/srv/proxycache_manager/502.html', 'proxycache', 'proxycache', 0400, contents=read_file('conf/proxycache_manager/502.html'), overwrite=True)
+
+@print_when_used
+def start_nginx():
+ print "Stopping old nginx..."
+ run_ignore_failure(_NGINX_BIN_PATH, '-s', 'quit')
+ print "Starting new nginx..."
+ run(_NGINX_BIN_PATH)
+
+
+# chmod -R g-rwx,o-rwx $PROXYCACHE_MANAGER_PATH
diff --git a/install/s3get.py b/install/s3get.py
new file mode 100755
index 0000000..801aedb
--- /dev/null
+++ b/install/s3get.py
@@ -0,0 +1,23 @@
+#! /usr/bin/env python
+
+import S3, sys, config, os
+
+def retrieve(filename):
+ conn = S3.AWSAuthConnection(config.S3_ACCESS_KEY, config.S3_SECRET)
+ assert 200 == conn.check_bucket_exists(config.S3_BUCKET).status
+
+ result = conn.get(config.S3_BUCKET, filename)
+ assert 200 == result.http_response.status
+
+ f = open(filename, "w")
+ f.write(result.object.data)
+ f.close()
+
+ print "File %s successfully retrieved (with same filename)." % filename
+
+if __name__ == '__main__':
+ if len(sys.argv) != 2:
+ print "Usage: s3get.py "
+ sys.exit(1)
+ filename = sys.argv[1]
+ retrieve(filename)
diff --git a/install/s3list.py b/install/s3list.py
new file mode 100755
index 0000000..203d2eb
--- /dev/null
+++ b/install/s3list.py
@@ -0,0 +1,18 @@
+#! /usr/bin/env python
+
+import S3, sys, config, os
+
+def list_files():
+ conn = S3.AWSAuthConnection(config.S3_ACCESS_KEY, config.S3_SECRET)
+ result = conn.check_bucket_exists(config.S3_BUCKET)
+ if result.status != 200:
+ result = conn.create_located_bucket(config.S3_BUCKET, S3.Location.DEFAULT)
+
+ result = conn.list_bucket(config.S3_BUCKET)
+ assert 200 == result.http_response.status
+ print "Size\t\tKey"
+ for entry in result.entries:
+ print "%s\t%s" % (entry.size, entry.key)
+
+if __name__ == '__main__':
+ list_files()
diff --git a/install/s3put.py b/install/s3put.py
new file mode 100755
index 0000000..cbfef4e
--- /dev/null
+++ b/install/s3put.py
@@ -0,0 +1,21 @@
+#! /usr/bin/env python
+
+import S3, sys, config, os
+from core import read_file
+
+def upload(filename):
+ conn = S3.AWSAuthConnection(config.S3_ACCESS_KEY, config.S3_SECRET)
+ result = conn.check_bucket_exists(config.S3_BUCKET)
+ if result.status != 200:
+ result = conn.create_located_bucket(config.S3_BUCKET, S3.Location.DEFAULT)
+
+ assert 200 == conn.put(config.S3_BUCKET, os.path.basename(filename), read_file(filename)).http_response.status
+
+ print "File %s successfully backed up to S3 (with same filename)." % filename
+
+if __name__ == '__main__':
+ if len(sys.argv) != 2:
+ print "Usage: s3put.py "
+ sys.exit(1)
+ filename = sys.argv[1]
+ upload(filename)
diff --git a/jobs/report_billing.py b/jobs/report_billing.py
new file mode 100755
index 0000000..c073fbd
--- /dev/null
+++ b/jobs/report_billing.py
@@ -0,0 +1,10 @@
+#! /srv/djangy/run/python-virtual/bin/python
+
+from master_api import report_all_usage
+import sys
+
+def main():
+ return report_all_usage()
+
+if __name__ == '__main__':
+ main()
diff --git a/misc/README b/misc/README
new file mode 100644
index 0000000..d5f9d1b
--- /dev/null
+++ b/misc/README
@@ -0,0 +1,54 @@
+see the SETUP file for a more up-to-date roadmap for this process.
+
+
+Requirements list for deploying Djangy
+
+NOTE: places where www-data is mentioned may be replaced by whatever user apache/wsgi is running under.
+
+Required symlinks:
+/etc/apache2/sites-available/api.djangy.com -> /srv/djangy/www/external_api/config/apache.conf
+/etc/apache2/sites-available/djangy.com -> /srv/djangy/www/d2/config/apache.conf
+/usr/share/git-core/templates/hooks/post-receive -> /srv/djangy/post_receive.py
+
+Gitosis must be set up according to:
+http://scie.nti.st/2007/11/14/hosting-git-repositories-the-easy-and-secure-way
+
+Gitosis repositories must live in:
+/srv/git/repositories
+
+Gitosis-admin must be accessible to (meaning gitosis.conf needs these and the public keys):
+www-data
+
+Required public keys:
+/srv/git/.ssh/id_rsa.pub
+/var/www/.ssh/id_rsa.pub
+
+Required permissions:
+/srv/git/repositories must be 755, owned by git
+/srv/bundles must be 777
+/srv/logs must be 777, owned by www-data
+/srv/scratch must be 777, owned by www-data
+
+Required database users:(username:password:db_name)
+(root:gatorade94)
+(djangy:djangy:djangy)
+
+To get databases sync'd, assuming the users exist:
+
+# this will run the fixtures and populate initial data
+cd /srv/djangy/www/external_api/application/external_api/
+python manage.py syncdb --settings=production
+
+# this might not be necessary. let's think about this some more
+cd /srv/djangy/www/d2/application/d2
+python manage.py syncdb --settings=production
+
+Finally, ensure that each running django instance has a virtual environment (this should be scripted):
+
+cd /srv/djangy/www/d2
+virtualenv python-virtual
+source python-virtual/bin/activate
+easy_install django mako (and anything else we need)
+deactivate
+
+Good to go! (Hopefully)
diff --git a/misc/SETUP b/misc/SETUP
new file mode 100644
index 0000000..5eda22a
--- /dev/null
+++ b/misc/SETUP
@@ -0,0 +1,23 @@
+At first, a host setup should be required and then a deployment. Minimize overlap.
+
+Host setup:
+
+-assume completely clean system (includes apt-get uninstall for packages?)
+-install system packages required for things to work. Keep easy_install to an absolute minimum.
+-add necessary users (right now, just gitosis. possibly a management user?)
+-generate all necessary public/private keypairs
+-install gitosis and ensure users have correct permissions
+-create necessary database users
+-do an initial repository clone
+-trac and blog stuff is excluded (possibly on a different host)
+
+
+Deployment:
+
+-do not delete directories, just ensure they exist and have the right chmod settings
+-do not delete repository directory, just pull (unless it doesn't exist, then clone)
+-rebuild all virtual environments
+-redo symlinks (cleaning beforehand)
+-ensure databases and their users exist, but do not clean them beforehand
+-run synced
+-restart apache (gracefully, probably)
diff --git a/misc/gen_invite_code/adjectives.txt b/misc/gen_invite_code/adjectives.txt
new file mode 100644
index 0000000..600f511
--- /dev/null
+++ b/misc/gen_invite_code/adjectives.txt
@@ -0,0 +1,355 @@
+adorable
+adventurous
+aggressive
+agreeable
+alert
+amused
+ancient
+angry
+annoyed
+annoying
+anxious
+arrogant
+ashamed
+attractive
+average
+awful
+beautiful
+bewildered
+big
+bitter
+black
+bloody
+blue
+blue-eyed
+blushing
+boiling
+bored
+brainy
+brave
+breakable
+breezy
+brief
+bright
+broad
+broken
+bumpy
+busy
+calm
+careful
+cautious
+charming
+cheerful
+chilly
+chubby
+clean
+clear
+clever
+cloudy
+clumsy
+cold
+colorful
+colossal
+combative
+comfortable
+concerned
+condemned
+confused
+cooing
+cool
+cooperative
+courageous
+crazy
+creepy
+crooked
+crowded
+cruel
+cuddly
+curious
+curly
+curved
+cute
+damaged
+damp
+dangerous
+dark
+dead
+deafening
+deep
+defeated
+defiant
+delicious
+delightful
+depressed
+determined
+difficult
+dirty
+disgusted
+distinct
+disturbed
+ditzy
+dizzy
+doubtful
+drab
+dry
+dull
+dusty
+eager
+easy
+elated
+elegant
+embarrassed
+empty
+enchanting
+encouraging
+energetic
+enthusiastic
+envious
+evil
+excited
+expensive
+exuberant
+faint
+faithful
+famous
+fancy
+fantastic
+fast
+fat
+fierce
+filthy
+fine
+flaky
+flat
+fluffy
+fluttering
+foolish
+fragile
+frail
+frantic
+freezing
+fresh
+friendly
+frightened
+funny
+fuzzy
+gentle
+gifted
+gigantic
+glamorous
+gleaming
+glorious
+gorgeous
+graceful
+greasy
+grieving
+grotesque
+grubby
+grumpy
+handsome
+happy
+hard
+harsh
+healthy
+heavy
+helpful
+helpless
+high-pitched
+hilarious
+hissing
+hollow
+homeless
+homely
+horrible
+hot
+huge
+hungry
+hurt
+hushed
+husky
+icy
+immense
+important
+impossible
+inexpensive
+innocent
+inquisitive
+itchy
+jealous
+jittery
+jolly
+joyous
+juicy
+kind
+large
+late
+lazy
+little
+lively
+living
+lonely
+loud
+lovely
+lucky
+magnificent
+mammoth
+manly
+massive
+melodic
+melted
+miniature
+misty
+moaning
+modern
+motionless
+muddy
+mushy
+mute
+mysterious
+narrow
+nasty
+naughty
+nervous
+nice
+noisy
+nutritious
+nutty
+obedient
+obnoxious
+odd
+old
+old-fashioned
+outrageous
+outstanding
+panicky
+perfect
+petite
+plain
+plastic
+pleasant
+poised
+poor
+powerful
+precious
+prickly
+proud
+puny
+purring
+puzzled
+quaint
+quick
+quiet
+rainy
+rapid
+raspy
+real
+relieved
+repulsive
+resonant
+rich
+ripe
+rotten
+rough
+round
+salty
+scary
+scattered
+scrawny
+screeching
+selfish
+shaggy
+shaky
+shallow
+sharp
+shiny
+shivering
+short
+shrill
+shy
+silent
+silky
+silly
+skinny
+sleepy
+slimy
+slippery
+slow
+small
+smiling
+smitten
+smoggy
+smooth
+soft
+solid
+sore
+sour
+sparkling
+spicy
+splendid
+spotless
+square
+squealing
+stale
+steady
+steep
+sticky
+stormy
+straight
+strange
+strong
+stupid
+substantial
+successful
+super
+sweet
+swift
+talented
+tall
+tame
+tasteless
+tasty
+teeny
+teeny-tiny
+tender
+tense
+terrible
+thankful
+thirsty
+thoughtful
+thoughtless
+thundering
+tight
+tiny
+tired
+tough
+troubled
+ugly
+uneven
+uninterested
+unsightly
+unusual
+upset
+uptight
+victorious
+vivacious
+voiceless
+wandering
+warm
+weak
+weary
+wet
+whispering
+wicked
+wide
+wide-eyed
+wild
+witty
+wonderful
+wooden
+worldly
+worried
+young
+yummy
+zany
+zealous
+zombie
diff --git a/misc/gen_invite_code/gen_invite_code.py b/misc/gen_invite_code/gen_invite_code.py
new file mode 100755
index 0000000..206d75f
--- /dev/null
+++ b/misc/gen_invite_code/gen_invite_code.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python
+
+import random, sys
+
+n = 1
+if len(sys.argv) > 1:
+ n = int(sys.argv[1])
+
+def strip_newlines(lines):
+ return map(lambda x: x[:-1], lines)
+
+def choose_word(words):
+ return words[random.randint(0, len(words)-1)]
+
+adjectives = strip_newlines(open('adjectives.txt').readlines())
+nouns = strip_newlines(open('nouns.txt').readlines())
+
+for i in range(0, n):
+ adj1 = choose_word(adjectives)
+ adj2 = choose_word(adjectives)
+ while adj1[-1] == adj2[-1]:
+ adj2 = choose_word(adjectives)
+ if adj1[-1] > adj2[-1]:
+ (adj1, adj2) = (adj2, adj1)
+ if adj1 == 'zombie':
+ (adj1, adj2) = (adj2, adj1)
+ noun = choose_word(nouns)
+
+ print "%s %s %s" % (adj1, adj2, noun)
diff --git a/misc/gen_invite_code/nouns.txt b/misc/gen_invite_code/nouns.txt
new file mode 100644
index 0000000..fa4eda9
--- /dev/null
+++ b/misc/gen_invite_code/nouns.txt
@@ -0,0 +1,120 @@
+alien
+artist
+baby
+badger
+basketball
+basketcase
+bedsheet
+bicycle
+boy
+boyscout
+bratwurst
+camera
+candle
+captain
+cat
+caveman
+ceo
+chair
+cheek
+cheesecake
+chihuahua
+chipmunk
+cruller
+digerati
+dog
+donkey
+donut
+dork
+driver
+drunk
+elf
+eskimo
+fairy
+fan
+father
+football
+friend
+frog
+gangster
+ghost
+girl
+girlscout
+goalie
+gorilla
+hacker
+hedgehog
+helmet
+hipster
+hobo
+horse
+house
+icecream
+inmate
+insect
+jock
+kangaroo
+keyboard
+king
+kitten
+knife
+koala
+lamp
+llama
+magistrate
+mathematician
+mom
+monkey
+monologue
+moped
+narwhal
+nerd
+ninja
+painting
+panda
+pants
+pencil
+penguin
+pig
+pikachu
+pirate
+pizza
+pogostick
+pony
+priest
+prince
+princess
+pumpkin
+puppy
+queen
+rabbit
+racquet
+redditor
+roommate
+scientist
+sheep
+skateboard
+snail
+solicitor
+spork
+spring
+statue
+summer
+superstar
+swimmer
+teaspoon
+toothbrush
+towel
+train
+trashcan
+troll
+tulip
+turtle
+unicorn
+viking
+wallaby
+weather
+winnebago
+wino
+winter
+yankee
diff --git a/src/client/.gitignore b/src/client/.gitignore
new file mode 100644
index 0000000..7eccdfb
--- /dev/null
+++ b/src/client/.gitignore
@@ -0,0 +1,4 @@
+Djangy.egg-info
+build
+dist
+*.pyc
diff --git a/src/client/Makefile b/src/client/Makefile
new file mode 100644
index 0000000..c3dfb20
--- /dev/null
+++ b/src/client/Makefile
@@ -0,0 +1,12 @@
+all: develop
+
+develop: setup.py djangy/* find_git_repository/*
+ python setup.py develop
+
+upload: setup.py djangy/* find_git_repository/*
+ python setup.py register sdist upload
+
+clean:
+ rm -rf dist
+ rm -rf build
+ rm -rf Djangy.egg-info
diff --git a/src/client/README b/src/client/README
new file mode 100644
index 0000000..a477ddd
--- /dev/null
+++ b/src/client/README
@@ -0,0 +1,7 @@
+To build the client and install it locally, simply run:
+
+$ make
+
+To build the client and upload it to pypi, run:
+
+$ make upload
diff --git a/src/client/djangy/__init__.py b/src/client/djangy/__init__.py
new file mode 100644
index 0000000..536e197
--- /dev/null
+++ b/src/client/djangy/__init__.py
@@ -0,0 +1 @@
+from djangy import *
diff --git a/src/client/djangy/djangy.py b/src/client/djangy/djangy.py
new file mode 100755
index 0000000..0f28d38
--- /dev/null
+++ b/src/client/djangy/djangy.py
@@ -0,0 +1,407 @@
+#! /usr/bin/env python
+
+import getpass, os, re, socket, subprocess, sys, urllib, urllib2, xmlrpclib, platform
+from hashlib import md5
+from pkg_resources import parse_version
+from find_git_repository import *
+from ConfigParser import RawConfigParser
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+GIT_HOST = 'api.djangy.com'
+API_BASE_URL = 'https://api.djangy.com'
+VERSION = '0.14'
+
+HOME = None
+if platform.system() == 'Windows':
+ HOME = os.environ['USERPROFILE']
+else:
+ HOME = os.environ['HOME']
+CONFIG_PATH = os.path.join(HOME, '.djangy')
+
+COMMANDS = [
+ 'create',
+ 'logs',
+ 'manage.py',
+]
+
+HELP_MESSAGE = """ Djangy Commands:
+ # NOTE: all commands accept
+ # the [-a app_name] argument:
+ # $ djangy -a myproject create
+
+djangy create # create a new djangy application
+
+djangy manage.py # remotely execute manage.py command
+djangy manage.py syncdb
+djangy manage.py migrate
+djangy manage.py shell
+
+djangy logs # display recent log output (last 100 lines)
+djangy help # display this message
+
+# Example:
+
+ django-startproject myproject
+ cd myproject
+ git init
+ git add .
+ git commit -m "my new project"
+ djangy create
+ git push djangy master
+
+# http://www.djangy.com/docs/ | support@djangy.com
+"""
+
+#### Update checker ####
+
+def check_for_update():
+ try:
+ socket.setdefaulttimeout(2) # 2 second default timeout
+ client = xmlrpclib.ServerProxy('http://pypi.python.org/pypi')
+ version = client.package_releases('Djangy')[0]
+ if parse_version(version) > parse_version(VERSION):
+ print ''
+ print 'Warning: There is an updated version of djangy available.'
+ print ' Run easy_install -U Djangy to update.'
+ print ''
+ except Exception, e:
+ print ''
+ print 'Warning: Connection to pypi.python.org timed out, so we'
+ print ' couldn\'t check if your djangy client is up-to-date.'
+ print ''
+ pass
+ finally:
+ socket.setdefaulttimeout(None)
+
+#### Basic input ####
+
+def prompt(text, blank_line=True):
+ print ''
+ print text + ' ',
+ response = sys.stdin.readline().strip('\n')
+ if blank_line:
+ print ''
+ return response
+
+#### Communication with the API server ####
+
+def request(command, email_address = None, hashed_password = None, pubkey = None, application_name = None, args = ' '):
+ if not command in COMMANDS:
+ print 'Invalid command.'
+ sys.exit(1)
+ data = {}
+ if application_name: data['application_name'] = application_name
+ if email_address: data['email'] = email_address
+ if args: data['args'] = json.dumps(args)
+ if pubkey: data['pubkey'] = pubkey
+ if hashed_password: data['hashed_password'] = hashed_password
+ url_values = urllib.urlencode(data)
+ req = urllib2.Request('%s/%s' % (API_BASE_URL, command), url_values)
+ try:
+ response = urllib2.urlopen(req)
+ print response.read()
+ return True
+ except urllib2.HTTPError as error:
+ if error.code == 403:
+ yn = prompt('Authentication error: would you like to re-enter your credentials (y/n)?')[0]
+ if yn == 'y' or yn == 'Y':
+ os.remove(CONFIG_PATH)
+ set_retry()
+ else:
+ sys.exit(1)
+ else:
+ print error.read()
+ sys.exit(1)
+ return False
+
+#### Git repository ####
+
+def get_git_repository(command):
+ try:
+ git_repo_root = find_git_repository(os.getcwd())
+ print 'Using git repository "%s"' % git_repo_root
+ return git_repo_root
+ except GitRepositoryNotFoundException as e:
+ print 'Please run "djangy %s" from within a valid git repository.' % command
+ sys.exit(1)
+
+#### Application name ####
+
+def validate_application_name(application_name):
+ return re.match('^[A-Za-z][A-Za-z0-9]{4,14}$', application_name) != None
+
+def warn_invalid_application_name(application_name):
+ print 'Invalid application name "%s", please try again.' % application_name
+ print 'Application name must be 5-15 characters long, A-Z a-z 0-9, starting with a letter.'
+
+def ask_for_application_name():
+ while True:
+ application_name = prompt('Please enter your application name [Enter for %s]:' % os.path.basename(os.getcwd()))
+ if application_name == '':
+ # If the user just hit enter, default to the name of the git repo
+ application_name = os.path.basename(os.getcwd())
+ if validate_application_name(application_name):
+ return application_name
+ else:
+ warn_invalid_application_name(application_name)
+
+def load_application_name():
+ parser = RawConfigParser()
+ parser.read('djangy.config')
+ try:
+ return parser.get('application', 'application_name')
+ except:
+ return None
+
+def save_application_name(application_name):
+ # Only actually save if djangy.config doesn't exist. We don't want to
+ # potentially mess up a user-customized file.
+ if not os.path.exists('djangy.config'):
+ f = open('djangy.config', 'w')
+ f.write('[application]\napplication_name=%s\nrootdir=%s\n' \
+ % (application_name, os.path.basename(os.getcwd())))
+ f.close()
+ return True
+ elif load_application_name() != application_name:
+ print 'Warning: please update application_name in "%s"' % os.path.abspath('djangy.config')
+ return False
+
+def print_application_name(application_name, source_of_application_name):
+ print 'Using application name "%s" from %s' % (application_name, source_of_application_name)
+
+def get_application_name(application_name_arg=None, application_name_retry=None, write_djangy_config=True):
+ if application_name_arg != None:
+ application_name = application_name_arg
+ print_application_name(application_name, '-a option')
+ if not validate_application_name(application_name):
+ warn_invalid_application_name(application_name)
+ sys.exit(1)
+ if write_djangy_config:
+ save_application_name(application_name)
+ return application_name
+ application_name = load_application_name()
+ if application_name != None:
+ print_application_name(application_name, '"%s"' % os.path.abspath('djangy.config'))
+ if not validate_application_name(application_name):
+ warn_invalid_application_name(application_name)
+ sys.exit(1)
+ else:
+ if application_name_retry != None:
+ # We're retrying due to authentication failure, but the user
+ # already entered an application name
+ application_name = application_name_retry
+ else:
+ application_name = ask_for_application_name()
+ print_application_name(application_name, 'user input')
+ if write_djangy_config:
+ save_application_name(application_name)
+ return application_name
+
+
+#### User credentials: email address and password ####
+
+def validate_email_address(email_address):
+ return re.match('^.+\\@[a-zA-Z0-9\\-\\.]+\\.([a-zA-Z]{2,6}|[0-9]{1,3})$', email_address) != None
+
+def ask_for_email_address():
+ num_failures = 0
+ while True:
+ email_address = prompt('Enter your email address:', blank_line=False)
+ if validate_email_address(email_address):
+ return email_address
+ else:
+ num_failures = num_failures + 1
+ print 'Invalid email address, please try again.'
+ if num_failures > 1:
+ print '(or email support@djangy.com if "%s" is correct.)' % email_address
+
+def ask_for_password(email_address):
+ hashed_password = md5('%s:%s' % (email_address, getpass.getpass('Please enter your password: '))).hexdigest()
+ print ''
+ return hashed_password
+
+def ask_for_credentials():
+ email_address = ask_for_email_address()
+ hashed_password = ask_for_password(email_address)
+ return (email_address, hashed_password)
+
+def load_credentials():
+ data = [d.strip('\n') for d in open(CONFIG_PATH).readlines()]
+ if len(data) != 2:
+ return None
+ email_address = data[0]
+ if not validate_email_address(email_address):
+ return None
+ hashed_password = data[1]
+ return (email_address, hashed_password)
+
+def save_credentials(email_address, hashed_password):
+ f = open(CONFIG_PATH, 'w')
+ f.write('%s\n%s' % (email_address, hashed_password))
+ f.close()
+ print 'Saved credentials.'
+ print 'To change your email address or password, remove "%s"' % CONFIG_PATH
+
+def get_credentials():
+ try:
+ return load_credentials()
+ except:
+ (email_address, hashed_password) = ask_for_credentials()
+ save_credentials(email_address, hashed_password)
+ return (email_address, hashed_password)
+
+#### User public key ####
+
+def validate_pubkey(pubkey_path):
+ if os.path.isfile(pubkey_path):
+ return True
+ return False
+
+def get_pubkey():
+ # try to find public key path
+ pubkey_path = None
+ if os.path.exists('%s/.ssh/id_rsa.pub' % HOME):
+ pubkey_path = '%s/.ssh/id_rsa.pub' % HOME
+ else:
+ is_valid_pubkey = False
+ while not is_valid_pubkey:
+ pubkey_path = os.path.abspath(prompt('Please enter the path to your ssh public key:'))
+ if validate_pubkey(pubkey_path):
+ is_valid_pubkey = True
+ else:
+ print 'Unable to locate ssh public key at path "%s"' % pubkey_path
+ print 'Using public key file "%s"' % pubkey_path
+ return open(pubkey_path).read()
+
+#### Commands ####
+
+_retry = True
+
+def run_command(command, application_name_arg, args):
+ global _retry
+ application_name = application_name_arg
+ while _retry:
+ _retry = False
+ email_address, hashed_password = get_credentials()
+ application_name = get_application_name(application_name_arg=application_name_arg, \
+ application_name_retry=application_name, write_djangy_config=(command != 'create'))
+ if command == 'create':
+ create(application_name, email_address, hashed_password)
+ elif command == 'manage.py':
+ manage_py(application_name, email_address, hashed_password, args)
+ else:
+ simple_command(command, application_name, email_address, hashed_password)
+
+def set_retry():
+ global _retry
+ _retry = True
+
+def create(application_name, email_address, hashed_password):
+ pubkey = get_pubkey()
+ if request('create', application_name = application_name, email_address = email_address, hashed_password = hashed_password, pubkey = pubkey):
+ init_default_files(application_name)
+ init_git_remote(application_name)
+
+def init_default_files(application_name):
+ files_created = []
+ if os.path.exists('djangy.config'):
+ if load_application_name() != application_name:
+ print 'Please update application_name in "%s"' % os.path.abspath('djangy.config')
+ else:
+ if save_application_name(application_name):
+ files_created.append('djangy.config')
+ if not os.path.exists('djangy.eggs'):
+ f = open('djangy.eggs', 'w')
+ f.write('Django\nSouth\n')
+ f.close()
+ files_created.append('djangy.eggs')
+ if not os.path.exists('djangy.pip'):
+ f = open('djangy.pip', 'w')
+ f.write('')
+ f.close()
+ files_created.append('djangy.pip')
+ if len(files_created) > 0:
+ n = len(files_created)
+ git_add_and_commit(files_created)
+
+def format_and_list(list, when_empty=''):
+ list = map(lambda x: '"%s"' % x, list)
+ if len(list) > 0:
+ if len(list) == 1:
+ return list[0]
+ else:
+ return (', '.join(list[:-1]) + ' and ' + list[-1])
+ else:
+ return when_empty
+
+def singular_plural(n, singular, plural):
+ if n > 1:
+ return plural
+ else:
+ return singular
+
+def init_git_remote(application_name):
+ """Add the "djangy" remote"""
+ subprocess.call(['git remote add djangy git@%s:%s.git > /dev/null 2>&1' % (GIT_HOST, application_name)], shell=True)
+ print ""
+ print 'You can now run "git push djangy master"'
+
+def git_add_and_commit(files):
+ if len(files) < 1:
+ return False
+ status = subprocess.call(['git', 'add'] + files)
+ if status == 0:
+ status = subprocess.call(['git', 'commit', '-m', 'added %s to repository' % format_and_list(files)])
+ if status == 0:
+ return True
+ return False
+
+def manage_py(application_name, email_address, hashed_password, args):
+ print ""
+ command = "ssh -oPasswordAuthentication=no shell@api.djangy.com %s manage.py %s" % (application_name, " ".join(args))
+ os.system(command)
+
+def simple_command(command, application_name, email_address, hashed_password):
+ request(command, application_name = application_name, email_address = email_address, hashed_password = hashed_password)
+
+#### Main ####
+
+def main():
+ # Parse command line arguments
+ command = ''
+ application_name = None
+ args = sys.argv[1:]
+ try:
+ if args[0][0:2] == '-a':
+ application_name = args[0][2:]
+ if application_name != '':
+ args = args[1:]
+ else:
+ application_name = args[1]
+ args = args[2:]
+ command = args[0]
+ args = args[1:]
+ except:
+ pass
+
+ if command in COMMANDS:
+ check_for_update()
+ # Go to the root directory of the repository so that any files we
+ # create are stored at the root level, and so the djangy.config and
+ # djangy.eggs files can be created with the right contents/location.
+ os.chdir(get_git_repository(command));
+ run_command(command, application_name, args)
+ elif command == 'help':
+ print HELP_MESSAGE
+ else:
+ print HELP_MESSAGE
+ sys.exit(1)
+
+if __name__ == '__main__':
+ try:
+ main()
+ except KeyboardInterrupt:
+ pass
diff --git a/src/client/find_git_repository/__init__.py b/src/client/find_git_repository/__init__.py
new file mode 100644
index 0000000..f038b8f
--- /dev/null
+++ b/src/client/find_git_repository/__init__.py
@@ -0,0 +1 @@
+from find_git_repository import *
diff --git a/src/client/find_git_repository/find_git_repository.py b/src/client/find_git_repository/find_git_repository.py
new file mode 100644
index 0000000..5d0b2bd
--- /dev/null
+++ b/src/client/find_git_repository/find_git_repository.py
@@ -0,0 +1,60 @@
+import os.path
+
+__DOT_GIT_FILES__ = ['config', 'description', 'HEAD']
+__DOT_GIT_SUBDIRECTORIES__ = ['hooks', 'info', 'objects', 'refs']
+
+def find_git_repository(cwd):
+ """Finds the nearest enclosing git repository. Raises a
+ GitRepositoryNotFoundException if there is none."""
+
+ # Normalize the path
+ dir_path = os.path.abspath(cwd)
+ # Start with this directory, and iterate up a level until we find a git
+ # repository root directory.
+ while dir_path != '/':
+ if is_git_repository_root(dir_path):
+ return dir_path
+ else:
+ dir_path = os.path.dirname(dir_path)
+
+ # Check one more time just in case / is a git repository
+ if is_git_repository_root(dir_path):
+ return dir_path
+ else:
+ raise GitRepositoryNotFoundException(cwd)
+
+def is_git_repository_root(dir_path):
+ """Is dir_path the root directory of a git repository?"""
+
+ # Is there a .git subdirectory? And is it well-formed?
+ git_dir = os.path.join(dir_path, '.git')
+ if os.path.isdir(git_dir) \
+ and is_git_dir(git_dir):
+ return True
+
+ # Is this directory itself a bare git repository with no working copy
+ # checkout directory?
+ return os.path.basename(dir_path).endswith('.git') \
+ and os.path.basename(dir_path) != '.git' \
+ and is_git_dir(dir_path)
+
+def is_git_dir(dir_path):
+ """Is dir_path a reasonably well-formed .git directory?"""
+
+ # Files that must exist in a .git directory
+ for git_file in __DOT_GIT_FILES__:
+ if not os.path.isfile(os.path.join(dir_path, git_file)):
+ return False
+
+ # Subdirectories that must exist in a .git directory
+ for git_subdir in __DOT_GIT_SUBDIRECTORIES__:
+ if not os.path.isdir(os.path.join(dir_path, git_subdir)):
+ return False
+ return True
+
+class GitRepositoryNotFoundException(Exception):
+ """No git repository root found in any parent of specified directory."""
+ def __init__(self, path):
+ self.path = path
+ def __str__(self):
+ return 'No git repository root found in any parent of directory "%s".' % self.path
diff --git a/src/client/setup.py b/src/client/setup.py
new file mode 100644
index 0000000..13a77d1
--- /dev/null
+++ b/src/client/setup.py
@@ -0,0 +1,25 @@
+from setuptools import setup, find_packages
+
+dependencies = []
+try:
+ import json
+except ImportError:
+ dependencies.append('simplejson')
+
+setup(
+ name="Djangy",
+ version="0.14",
+ packages = find_packages(),
+ author="David J. Paola",
+ author_email="dave@djangy.com",
+ description="Djangy.com client application",
+ keywords="djangy django",
+ url="http://www.djangy.com",
+ install_requires = dependencies,
+ entry_points = {
+ 'console_scripts': [
+ 'djangy = djangy:main'
+ ]
+ },
+ license="University of Illinois/NCSA Open Source License"
+)
diff --git a/src/server/master/README b/src/server/master/README
new file mode 100644
index 0000000..87839db
--- /dev/null
+++ b/src/server/master/README
@@ -0,0 +1,5 @@
+The www_* projects will be exposed on the web:
+
+web_api is the django project that the djangy.py client calls. It lives on api.djangy.com.
+
+web_ui is the django project we'll use as our main website, dashboard, and admin interface. It lives on [www.]djangy.com
diff --git a/src/server/master/management_database/.gitignore b/src/server/master/management_database/.gitignore
new file mode 100644
index 0000000..b2e4957
--- /dev/null
+++ b/src/server/master/management_database/.gitignore
@@ -0,0 +1,3 @@
+build
+dist
+management_database.egg-info
diff --git a/src/server/master/management_database/management_database/__init__.py b/src/server/master/management_database/management_database/__init__.py
new file mode 100644
index 0000000..db51548
--- /dev/null
+++ b/src/server/master/management_database/management_database/__init__.py
@@ -0,0 +1,4 @@
+import os, sys
+sys.path.append(os.path.dirname(os.path.realpath(__file__)))
+os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
+from models import *
diff --git a/src/server/master/management_database/management_database/loadadmins.yaml b/src/server/master/management_database/management_database/loadadmins.yaml
new file mode 100644
index 0000000..f50e2a6
--- /dev/null
+++ b/src/server/master/management_database/management_database/loadadmins.yaml
@@ -0,0 +1,6 @@
+- model: management_database.User
+ pk: 1
+ fields:
+ email: bob@jones.mil
+ passwd: encrypted password hash goes here
+ admin: True
diff --git a/src/server/master/management_database/management_database/loadchargables.yaml b/src/server/master/management_database/management_database/loadchargables.yaml
new file mode 100644
index 0000000..cf402da
--- /dev/null
+++ b/src/server/master/management_database/management_database/loadchargables.yaml
@@ -0,0 +1,11 @@
+- model: management_database.Chargable
+ pk: 1
+ fields:
+ component: 0 # application processes
+ price: 5
+- model: management_database.Chargable
+ pk: 2
+ fields:
+ component: 1 # celeryd processes
+ price: 5
+
diff --git a/src/server/master/management_database/management_database/loadsubscriptiontypes.yaml b/src/server/master/management_database/management_database/loadsubscriptiontypes.yaml
new file mode 100644
index 0000000..680a2b2
--- /dev/null
+++ b/src/server/master/management_database/management_database/loadsubscriptiontypes.yaml
@@ -0,0 +1,6 @@
+- model: management_database.SubscriptionType
+ pk: 1
+ fields:
+ name: launch_plan
+ description: Launch Plan
+ price: 500
diff --git a/src/server/master/management_database/management_database/manage.py b/src/server/master/management_database/management_database/manage.py
new file mode 100755
index 0000000..d47073c
--- /dev/null
+++ b/src/server/master/management_database/management_database/manage.py
@@ -0,0 +1,10 @@
+from django.core.management import execute_manager
+try:
+ import settings # Assumed to be in the same directory.
+except ImportError:
+ import sys
+ sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+ sys.exit(1)
+
+if __name__ == "__main__":
+ execute_manager(settings)
diff --git a/src/server/master/management_database/management_database/migrations/0001_initial.py b/src/server/master/management_database/management_database/migrations/0001_initial.py
new file mode 100644
index 0000000..fe20b52
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0001_initial.py
@@ -0,0 +1,108 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding model 'WhiteList'
+ db.create_table('whitelist', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('email', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ('invite_code', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ))
+ db.send_create_signal('management_database', ['WhiteList'])
+
+ # Adding model 'User'
+ db.create_table('user', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('email', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ('passwd', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ('admin', self.gf('django.db.models.fields.BooleanField')(default=False)),
+ ))
+ db.send_create_signal('management_database', ['User'])
+
+ # Adding model 'Application'
+ db.create_table('application', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('account', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['management_database.User'])),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ('max_processes', self.gf('django.db.models.fields.IntegerField')(default=1)),
+ ('db_name', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ('db_username', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ('db_password', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ('db_host', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ('db_port', self.gf('django.db.models.fields.IntegerField')(default=3306)),
+ ('db_max_size_mb', self.gf('django.db.models.fields.IntegerField')(default=5)),
+ ('setup_uid', self.gf('django.db.models.fields.IntegerField')(default=-1)),
+ ('web_uid', self.gf('django.db.models.fields.IntegerField')(default=-1)),
+ ('cron_uid', self.gf('django.db.models.fields.IntegerField')(default=-1)),
+ ))
+ db.send_create_signal('management_database', ['Application'])
+
+ # Adding model 'Process'
+ db.create_table('process', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('application', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['management_database.Application'])),
+ ('host', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ))
+ db.send_create_signal('management_database', ['Process'])
+
+
+ def backwards(self, orm):
+
+ # Deleting model 'WhiteList'
+ db.delete_table('whitelist')
+
+ # Deleting model 'User'
+ db.delete_table('user')
+
+ # Deleting model 'Application'
+ db.delete_table('application')
+
+ # Deleting model 'Process'
+ db.delete_table('process')
+
+
+ models = {
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_processes': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.process': {
+ 'Meta': {'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0002_add_admins.py b/src/server/master/management_database/management_database/migrations/0002_add_admins.py
new file mode 100644
index 0000000..b7229d4
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0002_add_admins.py
@@ -0,0 +1,57 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+from django.core.management import call_command
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ "Write your forwards methods here."
+ # outdated
+
+
+ def backwards(self, orm):
+ "Write your backwards methods here."
+
+
+ models = {
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_processes': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.process': {
+ 'Meta': {'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0003_add_app_gid.py b/src/server/master/management_database/management_database/migrations/0003_add_app_gid.py
new file mode 100644
index 0000000..c92f832
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0003_add_app_gid.py
@@ -0,0 +1,60 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding field 'Application.app_gid'
+ db.add_column('application', 'app_gid', self.gf('django.db.models.fields.IntegerField')(default=-1))
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'Application.app_gid'
+ db.delete_column('application', 'app_gid')
+
+
+ models = {
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_processes': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.process': {
+ 'Meta': {'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0004_auto__add_field_application_bundle_version.py b/src/server/master/management_database/management_database/migrations/0004_auto__add_field_application_bundle_version.py
new file mode 100644
index 0000000..02e6b64
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0004_auto__add_field_application_bundle_version.py
@@ -0,0 +1,61 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding field 'Application.bundle_version'
+ db.add_column('application', 'bundle_version', self.gf('django.db.models.fields.CharField')(default='', max_length=255), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'Application.bundle_version'
+ db.delete_column('application', 'bundle_version')
+
+
+ models = {
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_processes': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.process': {
+ 'Meta': {'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0005_resource_allocation.py b/src/server/master/management_database/management_database/migrations/0005_resource_allocation.py
new file mode 100644
index 0000000..380e2b2
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0005_resource_allocation.py
@@ -0,0 +1,101 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Deleting field 'Application.max_processes'
+ db.delete_column('application', 'max_processes')
+
+ # Adding field 'Application.proc_num_threads'
+ db.add_column('application', 'proc_num_threads', self.gf('django.db.models.fields.IntegerField')(default=5), keep_default=False)
+
+ # Adding field 'Application.proc_mem_mb'
+ db.add_column('application', 'proc_mem_mb', self.gf('django.db.models.fields.IntegerField')(default=64), keep_default=False)
+
+ # Adding field 'Application.proc_stack_mb'
+ db.add_column('application', 'proc_stack_mb', self.gf('django.db.models.fields.IntegerField')(default=2), keep_default=False)
+
+ # Adding field 'Application.debug'
+ db.add_column('application', 'debug', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False)
+
+ # Adding field 'Process.num_procs'
+ db.add_column('process', 'num_procs', self.gf('django.db.models.fields.IntegerField')(default=1), keep_default=False)
+
+ # Adding unique constraint on 'Process', fields ['application', 'host']
+ db.create_unique('process', ['application_id', 'host'])
+
+
+ def backwards(self, orm):
+
+ # Removing unique constraint on 'Process', fields ['application', 'host']
+ db.delete_unique('process', ['application_id', 'host'])
+
+ # Adding field 'Application.max_processes'
+ db.add_column('application', 'max_processes', self.gf('django.db.models.fields.IntegerField')(default=1), keep_default=False)
+
+ # Deleting field 'Application.proc_num_threads'
+ db.delete_column('application', 'proc_num_threads')
+
+ # Deleting field 'Application.proc_mem_mb'
+ db.delete_column('application', 'proc_mem_mb')
+
+ # Deleting field 'Application.proc_stack_mb'
+ db.delete_column('application', 'proc_stack_mb')
+
+ # Deleting field 'Application.debug'
+ db.delete_column('application', 'debug')
+
+ # Deleting field 'Process.num_procs'
+ db.delete_column('process', 'num_procs')
+
+
+ models = {
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "(('application', 'host'),)", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0006_mark_deletion.py b/src/server/master/management_database/management_database/migrations/0006_mark_deletion.py
new file mode 100644
index 0000000..2bf3f08
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0006_mark_deletion.py
@@ -0,0 +1,66 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding field 'Application.deleted'
+ db.add_column('application', 'deleted', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'Application.deleted'
+ db.delete_column('application', 'deleted')
+
+
+ models = {
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "(('application', 'host'),)", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0007_add_chargify_ids.py b/src/server/master/management_database/management_database/migrations/0007_add_chargify_ids.py
new file mode 100644
index 0000000..a2d65ed
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0007_add_chargify_ids.py
@@ -0,0 +1,81 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding field 'User.customer_id'
+ db.add_column('user', 'customer_id', self.gf('django.db.models.fields.CharField')(default=-1, max_length=255), keep_default=False)
+
+ # Adding field 'User.subscription_id'
+ db.add_column('user', 'subscription_id', self.gf('django.db.models.fields.CharField')(default=-1, max_length=255), keep_default=False)
+
+ # Adding field 'User.masked_cc'
+ db.add_column('user', 'masked_cc', self.gf('django.db.models.fields.CharField')(default=-1, max_length=255), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'User.customer_id'
+ db.delete_column('user', 'customer_id')
+
+ # Deleting field 'User.subscription_id'
+ db.delete_column('user', 'subscription_id')
+
+ # Deleting field 'User.masked_cc'
+ db.delete_column('user', 'masked_cc')
+
+
+ models = {
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "(('application', 'host'),)", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'masked_cc': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'subscription_id': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0008_remove_masked_cc.py b/src/server/master/management_database/management_database/migrations/0008_remove_masked_cc.py
new file mode 100644
index 0000000..1d29fe4
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0008_remove_masked_cc.py
@@ -0,0 +1,68 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Deleting field 'User.masked_cc'
+ db.delete_column('user', 'masked_cc')
+
+
+ def backwards(self, orm):
+
+ # Adding field 'User.masked_cc'
+ db.add_column('user', 'masked_cc', self.gf('django.db.models.fields.CharField')(default=-1, max_length=255), keep_default=False)
+
+
+ models = {
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "(('application', 'host'),)", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'subscription_id': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0009_add_allocation_change.py b/src/server/master/management_database/management_database/migrations/0009_add_allocation_change.py
new file mode 100644
index 0000000..6475f66
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0009_add_allocation_change.py
@@ -0,0 +1,85 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding model 'AllocationChange'
+ db.create_table('allocation_change', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('application', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['management_database.Application'])),
+ ('component', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ('quantity', self.gf('django.db.models.fields.IntegerField')()),
+ ('billed', self.gf('django.db.models.fields.BooleanField')(default=False)),
+ ('timestamp', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
+ ))
+ db.send_create_signal('management_database', ['AllocationChange'])
+
+
+ def backwards(self, orm):
+
+ # Deleting model 'AllocationChange'
+ db.delete_table('allocation_change')
+
+
+ models = {
+ 'management_database.allocationchange': {
+ 'Meta': {'object_name': 'AllocationChange', 'db_table': "'allocation_change'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'billed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'component': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'quantity': ('django.db.models.fields.IntegerField', [], {}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "(('application', 'host'),)", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'subscription_id': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0010_add_ProxyCache_and_VirtualHost_and_Process_port.py b/src/server/master/management_database/management_database/migrations/0010_add_ProxyCache_and_VirtualHost_and_Process_port.py
new file mode 100644
index 0000000..1c9dbca
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0010_add_ProxyCache_and_VirtualHost_and_Process_port.py
@@ -0,0 +1,112 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding model 'VirtualHost'
+ db.create_table('virtualhost', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('application', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['management_database.Application'])),
+ ('virtualhost', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ))
+ db.send_create_signal('management_database', ['VirtualHost'])
+
+ # Adding model 'ProxyCache'
+ db.create_table('proxycache', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('application', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['management_database.Application'])),
+ ('host', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ))
+ db.send_create_signal('management_database', ['ProxyCache'])
+
+ # Adding field 'Process.port'
+ db.add_column('process', 'port', self.gf('django.db.models.fields.IntegerField')(default=8080), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting model 'VirtualHost'
+ db.delete_table('virtualhost')
+
+ # Deleting model 'ProxyCache'
+ db.delete_table('proxycache')
+
+ # Deleting field 'Process.port'
+ db.delete_column('process', 'port')
+
+
+ models = {
+ 'management_database.allocationchange': {
+ 'Meta': {'object_name': 'AllocationChange', 'db_table': "'allocation_change'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'billed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'component': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'quantity': ('django.db.models.fields.IntegerField', [], {}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "(('application', 'host'),)", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '8080'})
+ },
+ 'management_database.proxycache': {
+ 'Meta': {'object_name': 'ProxyCache', 'db_table': "'proxycache'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'subscription_id': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.virtualhost': {
+ 'Meta': {'object_name': 'VirtualHost', 'db_table': "'virtualhost'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'virtualhost': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0011_add_port_to_proxycache.py b/src/server/master/management_database/management_database/migrations/0011_add_port_to_proxycache.py
new file mode 100644
index 0000000..d4cb9fc
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0011_add_port_to_proxycache.py
@@ -0,0 +1,91 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding field 'ProxyCache.port'
+ db.add_column('proxycache', 'port', self.gf('django.db.models.fields.IntegerField')(default=20000), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'ProxyCache.port'
+ db.delete_column('proxycache', 'port')
+
+
+ models = {
+ 'management_database.allocationchange': {
+ 'Meta': {'object_name': 'AllocationChange', 'db_table': "'allocation_change'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'billed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'component': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'quantity': ('django.db.models.fields.IntegerField', [], {}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "(('application', 'host'),)", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '8080'})
+ },
+ 'management_database.proxycache': {
+ 'Meta': {'object_name': 'ProxyCache', 'db_table': "'proxycache'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '20000'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'subscription_id': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.virtualhost': {
+ 'Meta': {'object_name': 'VirtualHost', 'db_table': "'virtualhost'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'virtualhost': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0012_remove_ProxyCache_port_and_add_some_uniqueness_constraints.py b/src/server/master/management_database/management_database/migrations/0012_remove_ProxyCache_port_and_add_some_uniqueness_constraints.py
new file mode 100644
index 0000000..f60f254
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0012_remove_ProxyCache_port_and_add_some_uniqueness_constraints.py
@@ -0,0 +1,108 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding unique constraint on 'Application', fields ['name']
+ db.create_unique('application', ['name'])
+
+ # Adding unique constraint on 'Process', fields ['host', 'port']
+ db.create_unique('process', ['host', 'port'])
+
+ # Deleting field 'ProxyCache.port'
+ db.delete_column('proxycache', 'port')
+
+ # Adding unique constraint on 'User', fields ['email']
+ db.create_unique('user', ['email'])
+
+
+ def backwards(self, orm):
+
+ # Removing unique constraint on 'User', fields ['email']
+ db.delete_unique('user', ['email'])
+
+ # Removing unique constraint on 'Process', fields ['host', 'port']
+ db.delete_unique('process', ['host', 'port'])
+
+ # Removing unique constraint on 'Application', fields ['name']
+ db.delete_unique('application', ['name'])
+
+ # Adding field 'ProxyCache.port'
+ db.add_column('proxycache', 'port', self.gf('django.db.models.fields.IntegerField')(default=20000), keep_default=False)
+
+
+ models = {
+ 'management_database.allocationchange': {
+ 'Meta': {'object_name': 'AllocationChange', 'db_table': "'allocation_change'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'billed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'component': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'quantity': ('django.db.models.fields.IntegerField', [], {}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "[('application', 'host'), ('host', 'port')]", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '20000'})
+ },
+ 'management_database.proxycache': {
+ 'Meta': {'object_name': 'ProxyCache', 'db_table': "'proxycache'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'subscription_id': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.virtualhost': {
+ 'Meta': {'object_name': 'VirtualHost', 'db_table': "'virtualhost'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'virtualhost': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0013_create_table_WorkerHost.py b/src/server/master/management_database/management_database/migrations/0013_create_table_WorkerHost.py
new file mode 100644
index 0000000..2fba5af
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0013_create_table_WorkerHost.py
@@ -0,0 +1,101 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding model 'WorkerHost'
+ db.create_table('worker_host', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('host', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)),
+ ('max_procs', self.gf('django.db.models.fields.IntegerField')(default=100)),
+ ))
+ db.send_create_signal('management_database', ['WorkerHost'])
+
+
+ def backwards(self, orm):
+
+ # Deleting model 'WorkerHost'
+ db.delete_table('worker_host')
+
+
+ models = {
+ 'management_database.allocationchange': {
+ 'Meta': {'object_name': 'AllocationChange', 'db_table': "'allocation_change'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'billed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'component': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'quantity': ('django.db.models.fields.IntegerField', [], {}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "[('application', 'host'), ('host', 'port')]", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '20000'})
+ },
+ 'management_database.proxycache': {
+ 'Meta': {'object_name': 'ProxyCache', 'db_table': "'proxycache'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'subscription_id': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.virtualhost': {
+ 'Meta': {'object_name': 'VirtualHost', 'db_table': "'virtualhost'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'virtualhost': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.workerhost': {
+ 'Meta': {'object_name': 'WorkerHost', 'db_table': "'worker_host'"},
+ 'host': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_procs': ('django.db.models.fields.IntegerField', [], {'default': '100'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0014_add_application_num_procs.py b/src/server/master/management_database/management_database/migrations/0014_add_application_num_procs.py
new file mode 100644
index 0000000..f0aa995
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0014_add_application_num_procs.py
@@ -0,0 +1,97 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding field 'Application.num_procs'
+ db.add_column('application', 'num_procs', self.gf('django.db.models.fields.IntegerField')(default=1), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'Application.num_procs'
+ db.delete_column('application', 'num_procs')
+
+
+ models = {
+ 'management_database.allocationchange': {
+ 'Meta': {'object_name': 'AllocationChange', 'db_table': "'allocation_change'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'billed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'component': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'quantity': ('django.db.models.fields.IntegerField', [], {}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "[('application', 'host'), ('host', 'port')]", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '20000'})
+ },
+ 'management_database.proxycache': {
+ 'Meta': {'object_name': 'ProxyCache', 'db_table': "'proxycache'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'subscription_id': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.virtualhost': {
+ 'Meta': {'object_name': 'VirtualHost', 'db_table': "'virtualhost'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'virtualhost': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.workerhost': {
+ 'Meta': {'object_name': 'WorkerHost', 'db_table': "'worker_host'"},
+ 'host': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_procs': ('django.db.models.fields.IntegerField', [], {'default': '100'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0015_default_VirtualHost.py b/src/server/master/management_database/management_database/migrations/0015_default_VirtualHost.py
new file mode 100644
index 0000000..08d8afa
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0015_default_VirtualHost.py
@@ -0,0 +1,98 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ # For each application, make sure it has a default entry in the VirtualHost table.
+ for application in orm.Application.objects.all():
+ if not orm.VirtualHost.objects.filter(application=application).all():
+ print "Adding %s.djangy.com" % application.name
+ virtualhost = orm.VirtualHost(application=application, virtualhost='%s.djangy.com' % application.name)
+ virtualhost.save()
+
+
+ def backwards(self, orm):
+ "Write your backwards methods here."
+
+
+ models = {
+ 'management_database.allocationchange': {
+ 'Meta': {'object_name': 'AllocationChange', 'db_table': "'allocation_change'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'billed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'component': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'quantity': ('django.db.models.fields.IntegerField', [], {}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "[('application', 'host'), ('host', 'port')]", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '20000'})
+ },
+ 'management_database.proxycache': {
+ 'Meta': {'object_name': 'ProxyCache', 'db_table': "'proxycache'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'subscription_id': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.virtualhost': {
+ 'Meta': {'object_name': 'VirtualHost', 'db_table': "'virtualhost'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'virtualhost': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.workerhost': {
+ 'Meta': {'object_name': 'WorkerHost', 'db_table': "'worker_host'"},
+ 'host': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_procs': ('django.db.models.fields.IntegerField', [], {'default': '100'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0016_default_ProxyCache.py b/src/server/master/management_database/management_database/migrations/0016_default_ProxyCache.py
new file mode 100644
index 0000000..471c87d
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0016_default_ProxyCache.py
@@ -0,0 +1,100 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ # For each application, make sure it has at least one entry in the
+ # ProxyCache table. We make that default "localhost", which is iffy
+ # long-term, because it means the ProxyCache server and Master
+ # server must be the same machine.
+ for application in orm.Application.objects.all():
+ if not orm.ProxyCache.objects.filter(application=application).all():
+ proxycache = orm.ProxyCache(application=application, host='localhost')
+ proxycache.save()
+
+
+ def backwards(self, orm):
+ "Write your backwards methods here."
+
+
+ models = {
+ 'management_database.allocationchange': {
+ 'Meta': {'object_name': 'AllocationChange', 'db_table': "'allocation_change'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'billed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'component': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'quantity': ('django.db.models.fields.IntegerField', [], {}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "[('application', 'host'), ('host', 'port')]", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '20000'})
+ },
+ 'management_database.proxycache': {
+ 'Meta': {'object_name': 'ProxyCache', 'db_table': "'proxycache'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'subscription_id': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.virtualhost': {
+ 'Meta': {'object_name': 'VirtualHost', 'db_table': "'virtualhost'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'virtualhost': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.workerhost': {
+ 'Meta': {'object_name': 'WorkerHost', 'db_table': "'worker_host'"},
+ 'host': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_procs': ('django.db.models.fields.IntegerField', [], {'default': '100'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0017_make_virtualhost_unique.py b/src/server/master/management_database/management_database/migrations/0017_make_virtualhost_unique.py
new file mode 100644
index 0000000..994e027
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0017_make_virtualhost_unique.py
@@ -0,0 +1,97 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding unique constraint on 'VirtualHost', fields ['application', 'virtualhost']
+ db.create_unique('virtualhost', ['application_id', 'virtualhost'])
+
+
+ def backwards(self, orm):
+
+ # Removing unique constraint on 'VirtualHost', fields ['application', 'virtualhost']
+ db.delete_unique('virtualhost', ['application_id', 'virtualhost'])
+
+
+ models = {
+ 'management_database.allocationchange': {
+ 'Meta': {'object_name': 'AllocationChange', 'db_table': "'allocation_change'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'billed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'component': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'quantity': ('django.db.models.fields.IntegerField', [], {}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "[('application', 'host'), ('host', 'port')]", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '20000'})
+ },
+ 'management_database.proxycache': {
+ 'Meta': {'object_name': 'ProxyCache', 'db_table': "'proxycache'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'subscription_id': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.virtualhost': {
+ 'Meta': {'unique_together': "[('application', 'virtualhost')]", 'object_name': 'VirtualHost', 'db_table': "'virtualhost'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'virtualhost': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.workerhost': {
+ 'Meta': {'object_name': 'WorkerHost', 'db_table': "'worker_host'"},
+ 'host': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_procs': ('django.db.models.fields.IntegerField', [], {'default': '100'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0018_add_referrers.py b/src/server/master/management_database/management_database/migrations/0018_add_referrers.py
new file mode 100644
index 0000000..a6ee430
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0018_add_referrers.py
@@ -0,0 +1,105 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding field 'WhiteList.referrer'
+ db.add_column('whitelist', 'referrer', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['management_database.User'], null=True, blank=True), keep_default=False)
+
+ # Adding field 'User.referrer'
+ db.add_column('user', 'referrer', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['management_database.User'], null=True, blank=True), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'WhiteList.referrer'
+ db.delete_column('whitelist', 'referrer_id')
+
+ # Deleting field 'User.referrer'
+ db.delete_column('user', 'referrer_id')
+
+
+ models = {
+ 'management_database.allocationchange': {
+ 'Meta': {'object_name': 'AllocationChange', 'db_table': "'allocation_change'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'billed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'component': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'quantity': ('django.db.models.fields.IntegerField', [], {}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '20'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "[('application', 'host'), ('host', 'port')]", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '20000'})
+ },
+ 'management_database.proxycache': {
+ 'Meta': {'object_name': 'ProxyCache', 'db_table': "'proxycache'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'}),
+ 'subscription_id': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.virtualhost': {
+ 'Meta': {'unique_together': "[('application', 'virtualhost')]", 'object_name': 'VirtualHost', 'db_table': "'virtualhost'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'virtualhost': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.workerhost': {
+ 'Meta': {'object_name': 'WorkerHost', 'db_table': "'worker_host'"},
+ 'host': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_procs': ('django.db.models.fields.IntegerField', [], {'default': '100'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0019_add_invite_limit.py b/src/server/master/management_database/management_database/migrations/0019_add_invite_limit.py
new file mode 100644
index 0000000..3e66d87
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0019_add_invite_limit.py
@@ -0,0 +1,100 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding field 'User.invite_limit'
+ db.add_column('user', 'invite_limit', self.gf('django.db.models.fields.IntegerField')(default=10), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'User.invite_limit'
+ db.delete_column('user', 'invite_limit')
+
+
+ models = {
+ 'management_database.allocationchange': {
+ 'Meta': {'object_name': 'AllocationChange', 'db_table': "'allocation_change'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'billed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'component': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'quantity': ('django.db.models.fields.IntegerField', [], {}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '20'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "[('application', 'host'), ('host', 'port')]", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '20000'})
+ },
+ 'management_database.proxycache': {
+ 'Meta': {'object_name': 'ProxyCache', 'db_table': "'proxycache'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_limit': ('django.db.models.fields.IntegerField', [], {'default': '10'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'}),
+ 'subscription_id': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.virtualhost': {
+ 'Meta': {'unique_together': "[('application', 'virtualhost')]", 'object_name': 'VirtualHost', 'db_table': "'virtualhost'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'virtualhost': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.workerhost': {
+ 'Meta': {'object_name': 'WorkerHost', 'db_table': "'worker_host'"},
+ 'host': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_procs': ('django.db.models.fields.IntegerField', [], {'default': '100'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0020_add_SshPublicKey_and_Collaborator.py b/src/server/master/management_database/management_database/migrations/0020_add_SshPublicKey_and_Collaborator.py
new file mode 100644
index 0000000..8dc80eb
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0020_add_SshPublicKey_and_Collaborator.py
@@ -0,0 +1,142 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Rename Application.account to Application.owner - broken
+ #db.rename_column('application', 'account_id', 'owner_id')
+
+ # Adding model 'Collaborator'
+ db.create_table('collaborator', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('application', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['management_database.Application'])),
+ ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['management_database.User'])),
+ ))
+ db.send_create_signal('management_database', ['Collaborator'])
+
+ # Adding unique constraint on 'Collaborator', fields ['application', 'user']
+ db.create_unique('collaborator', ['application_id', 'user_id'])
+
+ # Adding model 'SshPublicKey'
+ db.create_table('ssh_public_key', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['management_database.User'])),
+ ('ssh_public_key', self.gf('django.db.models.fields.CharField')(max_length=1024)),
+ ('comment', self.gf('django.db.models.fields.CharField')(max_length=64)),
+ ))
+ db.send_create_signal('management_database', ['SshPublicKey'])
+
+
+ def backwards(self, orm):
+
+ # Rename Application.owner back to Application.account - broken
+ #db.rename_column('application', 'owner_id', 'account_id')
+
+ # Removing unique constraint on 'Collaborator', fields ['application', 'user']
+ db.delete_unique('collaborator', ['application_id', 'user_id'])
+
+ # Deleting model 'Collaborator'
+ db.delete_table('collaborator')
+
+ # Deleting model 'SshPublicKey'
+ db.delete_table('ssh_public_key')
+
+
+ models = {
+ 'management_database.allocationchange': {
+ 'Meta': {'object_name': 'AllocationChange', 'db_table': "'allocation_change'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'billed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'component': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'quantity': ('django.db.models.fields.IntegerField', [], {}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '20'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.collaborator': {
+ 'Meta': {'unique_together': "[('application', 'user')]", 'object_name': 'Collaborator', 'db_table': "'collaborator'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "[('application', 'host'), ('host', 'port')]", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '20000'})
+ },
+ 'management_database.proxycache': {
+ 'Meta': {'object_name': 'ProxyCache', 'db_table': "'proxycache'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.sshpublickey': {
+ 'Meta': {'object_name': 'SshPublicKey', 'db_table': "'ssh_public_key'"},
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ssh_public_key': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_limit': ('django.db.models.fields.IntegerField', [], {'default': '10'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'}),
+ 'subscription_id': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.virtualhost': {
+ 'Meta': {'unique_together': "[('application', 'virtualhost')]", 'object_name': 'VirtualHost', 'db_table': "'virtualhost'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'virtualhost': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.workerhost': {
+ 'Meta': {'object_name': 'WorkerHost', 'db_table': "'worker_host'"},
+ 'host': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_procs': ('django.db.models.fields.IntegerField', [], {'default': '100'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0021_chargify_to_devpayments_schema.py b/src/server/master/management_database/management_database/migrations/0021_chargify_to_devpayments_schema.py
new file mode 100644
index 0000000..1e0c8b3
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0021_chargify_to_devpayments_schema.py
@@ -0,0 +1,126 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Deleting field 'User.subscription_id'
+ db.delete_column('user', 'subscription_id')
+
+ # Adding field 'User.first_name'
+ db.add_column('user', 'first_name', self.gf('django.db.models.fields.CharField')(default=None, max_length=255, null=True, blank=True), keep_default=False)
+
+ # Adding field 'User.last_name'
+ db.add_column('user', 'last_name', self.gf('django.db.models.fields.CharField')(default=None, max_length=255, null=True, blank=True), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Adding field 'User.subscription_id'
+ db.add_column('user', 'subscription_id', self.gf('django.db.models.fields.CharField')(default=-1, max_length=255), keep_default=False)
+
+ # Deleting field 'User.first_name'
+ db.delete_column('user', 'first_name')
+
+ # Deleting field 'User.last_name'
+ db.delete_column('user', 'last_name')
+
+
+ models = {
+ 'management_database.allocationchange': {
+ 'Meta': {'object_name': 'AllocationChange', 'db_table': "'allocation_change'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'billed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'component': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'quantity': ('django.db.models.fields.IntegerField', [], {}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '20'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.collaborator': {
+ 'Meta': {'unique_together': "[('application', 'user')]", 'object_name': 'Collaborator', 'db_table': "'collaborator'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "[('application', 'host'), ('host', 'port')]", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '20000'})
+ },
+ 'management_database.proxycache': {
+ 'Meta': {'object_name': 'ProxyCache', 'db_table': "'proxycache'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.sshpublickey': {
+ 'Meta': {'object_name': 'SshPublicKey', 'db_table': "'ssh_public_key'"},
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ssh_public_key': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_limit': ('django.db.models.fields.IntegerField', [], {'default': '10'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.virtualhost': {
+ 'Meta': {'unique_together': "[('application', 'virtualhost')]", 'object_name': 'VirtualHost', 'db_table': "'virtualhost'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'virtualhost': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.workerhost': {
+ 'Meta': {'object_name': 'WorkerHost', 'db_table': "'worker_host'"},
+ 'host': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_procs': ('django.db.models.fields.IntegerField', [], {'default': '100'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0022_add_chargables.py b/src/server/master/management_database/management_database/migrations/0022_add_chargables.py
new file mode 100644
index 0000000..f9a1972
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0022_add_chargables.py
@@ -0,0 +1,125 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding model 'Chargable'
+ db.create_table('chargable', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('component', self.gf('django.db.models.fields.IntegerField')(default=0)),
+ ('price', self.gf('django.db.models.fields.IntegerField')(default=0)),
+ ))
+ db.send_create_signal('management_database', ['Chargable'])
+
+
+ def backwards(self, orm):
+
+ # Deleting model 'Chargable'
+ db.delete_table('chargable')
+
+
+ models = {
+ 'management_database.allocationchange': {
+ 'Meta': {'object_name': 'AllocationChange', 'db_table': "'allocation_change'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'billed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'component': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'quantity': ('django.db.models.fields.IntegerField', [], {}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '20'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.chargable': {
+ 'Meta': {'object_name': 'Chargable', 'db_table': "'chargable'"},
+ 'component': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'price': ('django.db.models.fields.IntegerField', [], {'default': '0'})
+ },
+ 'management_database.collaborator': {
+ 'Meta': {'unique_together': "[('application', 'user')]", 'object_name': 'Collaborator', 'db_table': "'collaborator'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "[('application', 'host'), ('host', 'port')]", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '20000'})
+ },
+ 'management_database.proxycache': {
+ 'Meta': {'object_name': 'ProxyCache', 'db_table': "'proxycache'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.sshpublickey': {
+ 'Meta': {'object_name': 'SshPublicKey', 'db_table': "'ssh_public_key'"},
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ssh_public_key': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_limit': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.virtualhost': {
+ 'Meta': {'unique_together': "[('application', 'virtualhost')]", 'object_name': 'VirtualHost', 'db_table': "'virtualhost'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'virtualhost': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.workerhost': {
+ 'Meta': {'object_name': 'WorkerHost', 'db_table': "'worker_host'"},
+ 'host': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_procs': ('django.db.models.fields.IntegerField', [], {'default': '100'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0023_alter_allocation_change.py b/src/server/master/management_database/management_database/migrations/0023_alter_allocation_change.py
new file mode 100644
index 0000000..174e11a
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0023_alter_allocation_change.py
@@ -0,0 +1,126 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Deleting field 'AllocationChange.component'
+ db.delete_column('allocation_change', 'component')
+
+ # Adding field 'AllocationChange.chargable'
+ db.add_column('allocation_change', 'chargable', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['management_database.Chargable'], null=True), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Adding field 'AllocationChange.component'
+ db.add_column('allocation_change', 'component', self.gf('django.db.models.fields.CharField')(default='workers', max_length=255), keep_default=False)
+
+ # Deleting field 'AllocationChange.chargable'
+ db.delete_column('allocation_change', 'chargable_id')
+
+
+ models = {
+ 'management_database.allocationchange': {
+ 'Meta': {'object_name': 'AllocationChange', 'db_table': "'allocation_change'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'billed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'chargable': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Chargable']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'quantity': ('django.db.models.fields.IntegerField', [], {}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '20'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.chargable': {
+ 'Meta': {'object_name': 'Chargable', 'db_table': "'chargable'"},
+ 'component': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'price': ('django.db.models.fields.IntegerField', [], {'default': '0'})
+ },
+ 'management_database.collaborator': {
+ 'Meta': {'unique_together': "[('application', 'user')]", 'object_name': 'Collaborator', 'db_table': "'collaborator'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "[('application', 'host'), ('host', 'port')]", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '20000'})
+ },
+ 'management_database.proxycache': {
+ 'Meta': {'object_name': 'ProxyCache', 'db_table': "'proxycache'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.sshpublickey': {
+ 'Meta': {'object_name': 'SshPublicKey', 'db_table': "'ssh_public_key'"},
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ssh_public_key': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_limit': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.virtualhost': {
+ 'Meta': {'unique_together': "[('application', 'virtualhost')]", 'object_name': 'VirtualHost', 'db_table': "'virtualhost'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'virtualhost': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.workerhost': {
+ 'Meta': {'object_name': 'WorkerHost', 'db_table': "'worker_host'"},
+ 'host': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_procs': ('django.db.models.fields.IntegerField', [], {'default': '100'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0024_add_billing_events.py b/src/server/master/management_database/management_database/migrations/0024_add_billing_events.py
new file mode 100644
index 0000000..493a333
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0024_add_billing_events.py
@@ -0,0 +1,143 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding model 'BillingEvent'
+ db.create_table('billingevent', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('email', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ('customer_id', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ('application_name', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ('chargable_name', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ('timestamp', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
+ ('cents', self.gf('django.db.models.fields.IntegerField')()),
+ ('success', self.gf('django.db.models.fields.BooleanField')(default=False)),
+ ('memo', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
+ ))
+ db.send_create_signal('management_database', ['BillingEvent'])
+
+
+ def backwards(self, orm):
+
+ # Deleting model 'BillingEvent'
+ db.delete_table('billingevent')
+
+
+ models = {
+ 'management_database.allocationchange': {
+ 'Meta': {'object_name': 'AllocationChange', 'db_table': "'allocation_change'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'billed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'chargable': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Chargable']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'quantity': ('django.db.models.fields.IntegerField', [], {}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '20'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.billingevent': {
+ 'Meta': {'object_name': 'BillingEvent', 'db_table': "'billingevent'"},
+ 'application_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'cents': ('django.db.models.fields.IntegerField', [], {}),
+ 'chargable_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'memo': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'success': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.chargable': {
+ 'Meta': {'object_name': 'Chargable', 'db_table': "'chargable'"},
+ 'component': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'price': ('django.db.models.fields.IntegerField', [], {'default': '0'})
+ },
+ 'management_database.collaborator': {
+ 'Meta': {'unique_together': "[('application', 'user')]", 'object_name': 'Collaborator', 'db_table': "'collaborator'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "[('application', 'host'), ('host', 'port')]", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '20000'})
+ },
+ 'management_database.proxycache': {
+ 'Meta': {'object_name': 'ProxyCache', 'db_table': "'proxycache'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.sshpublickey': {
+ 'Meta': {'object_name': 'SshPublicKey', 'db_table': "'ssh_public_key'"},
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ssh_public_key': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_limit': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.virtualhost': {
+ 'Meta': {'unique_together': "[('application', 'virtualhost')]", 'object_name': 'VirtualHost', 'db_table': "'virtualhost'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'virtualhost': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.workerhost': {
+ 'Meta': {'object_name': 'WorkerHost', 'db_table': "'worker_host'"},
+ 'host': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_procs': ('django.db.models.fields.IntegerField', [], {'default': '100'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0025_add_ActiveApplicationName_table.py b/src/server/master/management_database/management_database/migrations/0025_add_ActiveApplicationName_table.py
new file mode 100644
index 0000000..76c46c8
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0025_add_ActiveApplicationName_table.py
@@ -0,0 +1,147 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Removing unique constraint on 'Application', fields ['name']
+ db.delete_unique('application', ['name'])
+
+ # Adding model 'ActiveApplicationName'
+ db.create_table('active_application_name', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)),
+ ))
+ db.send_create_signal('management_database', ['ActiveApplicationName'])
+
+
+ def backwards(self, orm):
+
+ # Deleting model 'ActiveApplicationName'
+ db.delete_table('active_application_name')
+
+ # Adding unique constraint on 'Application', fields ['name']
+ db.create_unique('application', ['name'])
+
+
+ models = {
+ 'management_database.activeapplicationname': {
+ 'Meta': {'object_name': 'ActiveApplicationName', 'db_table': "'active_application_name'"},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
+ },
+ 'management_database.allocationchange': {
+ 'Meta': {'object_name': 'AllocationChange', 'db_table': "'allocation_change'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'billed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'chargable': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Chargable']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'quantity': ('django.db.models.fields.IntegerField', [], {}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '20'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.billingevent': {
+ 'Meta': {'object_name': 'BillingEvent', 'db_table': "'billingevent'"},
+ 'application_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'cents': ('django.db.models.fields.IntegerField', [], {}),
+ 'chargable_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'memo': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'success': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.chargable': {
+ 'Meta': {'object_name': 'Chargable', 'db_table': "'chargable'"},
+ 'component': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'price': ('django.db.models.fields.IntegerField', [], {'default': '0'})
+ },
+ 'management_database.collaborator': {
+ 'Meta': {'unique_together': "[('application', 'user')]", 'object_name': 'Collaborator', 'db_table': "'collaborator'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "[('application', 'host'), ('host', 'port')]", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '20000'})
+ },
+ 'management_database.proxycache': {
+ 'Meta': {'object_name': 'ProxyCache', 'db_table': "'proxycache'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.sshpublickey': {
+ 'Meta': {'object_name': 'SshPublicKey', 'db_table': "'ssh_public_key'"},
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ssh_public_key': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_limit': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.virtualhost': {
+ 'Meta': {'unique_together': "[('application', 'virtualhost')]", 'object_name': 'VirtualHost', 'db_table': "'virtualhost'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'virtualhost': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.workerhost': {
+ 'Meta': {'object_name': 'WorkerHost', 'db_table': "'worker_host'"},
+ 'host': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_procs': ('django.db.models.fields.IntegerField', [], {'default': '100'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0026_add_subscriptions.py b/src/server/master/management_database/management_database/migrations/0026_add_subscriptions.py
new file mode 100644
index 0000000..c19f540
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0026_add_subscriptions.py
@@ -0,0 +1,173 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding model 'SubscriptionType'
+ db.create_table('subscriptiontype', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
+ ('description', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
+ ('price', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)),
+ ))
+ db.send_create_signal('management_database', ['SubscriptionType'])
+
+ # Adding model 'Subscription'
+ db.create_table('subscription', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('application', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['management_database.Application'])),
+ ('subscription_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['management_database.SubscriptionType'])),
+ ('price', self.gf('django.db.models.fields.IntegerField')(default=0, null=True, blank=True)),
+ ('enabled', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
+ ('disabled', self.gf('django.db.models.fields.DateTimeField')(default=None, null=True, blank=True)),
+ ))
+ db.send_create_signal('management_database', ['Subscription'])
+
+
+ def backwards(self, orm):
+
+ # Deleting model 'SubscriptionType'
+ db.delete_table('subscriptiontype')
+
+ # Deleting model 'Subscription'
+ db.delete_table('subscription')
+
+
+ models = {
+ 'management_database.activeapplicationname': {
+ 'Meta': {'object_name': 'ActiveApplicationName', 'db_table': "'active_application_name'"},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
+ },
+ 'management_database.allocationchange': {
+ 'Meta': {'object_name': 'AllocationChange', 'db_table': "'allocation_change'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'billed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'chargable': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Chargable']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'quantity': ('django.db.models.fields.IntegerField', [], {}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '20'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.billingevent': {
+ 'Meta': {'object_name': 'BillingEvent', 'db_table': "'billingevent'"},
+ 'application_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'cents': ('django.db.models.fields.IntegerField', [], {}),
+ 'chargable_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'memo': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'success': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.chargable': {
+ 'Meta': {'object_name': 'Chargable', 'db_table': "'chargable'"},
+ 'component': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'price': ('django.db.models.fields.IntegerField', [], {'default': '0'})
+ },
+ 'management_database.collaborator': {
+ 'Meta': {'unique_together': "[('application', 'user')]", 'object_name': 'Collaborator', 'db_table': "'collaborator'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "[('application', 'host'), ('host', 'port')]", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '20000'})
+ },
+ 'management_database.proxycache': {
+ 'Meta': {'object_name': 'ProxyCache', 'db_table': "'proxycache'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.sshpublickey': {
+ 'Meta': {'object_name': 'SshPublicKey', 'db_table': "'ssh_public_key'"},
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ssh_public_key': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"})
+ },
+ 'management_database.subscription': {
+ 'Meta': {'object_name': 'Subscription', 'db_table': "'subscription'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'disabled': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
+ 'enabled': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'price': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True', 'blank': 'True'}),
+ 'subscription_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.SubscriptionType']"})
+ },
+ 'management_database.subscriptiontype': {
+ 'Meta': {'object_name': 'SubscriptionType', 'db_table': "'subscriptiontype'"},
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'price': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_limit': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.virtualhost': {
+ 'Meta': {'unique_together': "[('application', 'virtualhost')]", 'object_name': 'VirtualHost', 'db_table': "'virtualhost'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'virtualhost': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.workerhost': {
+ 'Meta': {'object_name': 'WorkerHost', 'db_table': "'worker_host'"},
+ 'host': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_procs': ('django.db.models.fields.IntegerField', [], {'default': '100'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0027_add_cache_sizes.py b/src/server/master/management_database/management_database/migrations/0027_add_cache_sizes.py
new file mode 100644
index 0000000..b624426
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0027_add_cache_sizes.py
@@ -0,0 +1,160 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding field 'Application.cache_index_size_kb'
+ db.add_column('application', 'cache_index_size_kb', self.gf('django.db.models.fields.IntegerField')(default=0), keep_default=False)
+
+ # Adding field 'Application.cache_size_kb'
+ db.add_column('application', 'cache_size_kb', self.gf('django.db.models.fields.IntegerField')(default=0), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'Application.cache_index_size_kb'
+ db.delete_column('application', 'cache_index_size_kb')
+
+ # Deleting field 'Application.cache_size_kb'
+ db.delete_column('application', 'cache_size_kb')
+
+
+ models = {
+ 'management_database.activeapplicationname': {
+ 'Meta': {'object_name': 'ActiveApplicationName', 'db_table': "'active_application_name'"},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
+ },
+ 'management_database.allocationchange': {
+ 'Meta': {'object_name': 'AllocationChange', 'db_table': "'allocation_change'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'billed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'chargable': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Chargable']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'quantity': ('django.db.models.fields.IntegerField', [], {}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cache_index_size_kb': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'cache_size_kb': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '20'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.billingevent': {
+ 'Meta': {'object_name': 'BillingEvent', 'db_table': "'billingevent'"},
+ 'application_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'cents': ('django.db.models.fields.IntegerField', [], {}),
+ 'chargable_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'memo': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'success': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.chargable': {
+ 'Meta': {'object_name': 'Chargable', 'db_table': "'chargable'"},
+ 'component': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'price': ('django.db.models.fields.IntegerField', [], {'default': '0'})
+ },
+ 'management_database.collaborator': {
+ 'Meta': {'unique_together': "[('application', 'user')]", 'object_name': 'Collaborator', 'db_table': "'collaborator'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "[('application', 'host'), ('host', 'port')]", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '20000'})
+ },
+ 'management_database.proxycache': {
+ 'Meta': {'object_name': 'ProxyCache', 'db_table': "'proxycache'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.sshpublickey': {
+ 'Meta': {'object_name': 'SshPublicKey', 'db_table': "'ssh_public_key'"},
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ssh_public_key': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"})
+ },
+ 'management_database.subscription': {
+ 'Meta': {'object_name': 'Subscription', 'db_table': "'subscription'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'disabled': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
+ 'enabled': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'price': ('django.db.models.fields.FloatField', [], {'default': '0.0', 'null': 'True', 'blank': 'True'}),
+ 'subscription_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.SubscriptionType']"})
+ },
+ 'management_database.subscriptiontype': {
+ 'Meta': {'object_name': 'SubscriptionType', 'db_table': "'subscriptiontype'"},
+ 'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'price': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_limit': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.virtualhost': {
+ 'Meta': {'unique_together': "[('application', 'virtualhost')]", 'object_name': 'VirtualHost', 'db_table': "'virtualhost'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'virtualhost': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.workerhost': {
+ 'Meta': {'object_name': 'WorkerHost', 'db_table': "'worker_host'"},
+ 'host': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_procs': ('django.db.models.fields.IntegerField', [], {'default': '100'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0028_add_celery_procs.py b/src/server/master/management_database/management_database/migrations/0028_add_celery_procs.py
new file mode 100644
index 0000000..1de358b
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0028_add_celery_procs.py
@@ -0,0 +1,156 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding field 'Application.celery_procs'
+ db.add_column('application', 'celery_procs', self.gf('django.db.models.fields.IntegerField')(default=1), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'Application.celery_procs'
+ db.delete_column('application', 'celery_procs')
+
+
+ models = {
+ 'management_database.activeapplicationname': {
+ 'Meta': {'object_name': 'ActiveApplicationName', 'db_table': "'active_application_name'"},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
+ },
+ 'management_database.allocationchange': {
+ 'Meta': {'object_name': 'AllocationChange', 'db_table': "'allocation_change'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'billed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'chargable': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Chargable']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'quantity': ('django.db.models.fields.IntegerField', [], {}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cache_index_size_kb': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'cache_size_kb': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'celery_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '20'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.billingevent': {
+ 'Meta': {'object_name': 'BillingEvent', 'db_table': "'billingevent'"},
+ 'application_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'cents': ('django.db.models.fields.IntegerField', [], {}),
+ 'chargable_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'memo': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'success': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.chargable': {
+ 'Meta': {'object_name': 'Chargable', 'db_table': "'chargable'"},
+ 'component': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'price': ('django.db.models.fields.IntegerField', [], {'default': '0'})
+ },
+ 'management_database.collaborator': {
+ 'Meta': {'unique_together': "[('application', 'user')]", 'object_name': 'Collaborator', 'db_table': "'collaborator'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "[('application', 'host'), ('host', 'port')]", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '20000'})
+ },
+ 'management_database.proxycache': {
+ 'Meta': {'object_name': 'ProxyCache', 'db_table': "'proxycache'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.sshpublickey': {
+ 'Meta': {'object_name': 'SshPublicKey', 'db_table': "'ssh_public_key'"},
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ssh_public_key': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"})
+ },
+ 'management_database.subscription': {
+ 'Meta': {'object_name': 'Subscription', 'db_table': "'subscription'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'disabled': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
+ 'enabled': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'price': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True', 'blank': 'True'}),
+ 'subscription_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.SubscriptionType']"})
+ },
+ 'management_database.subscriptiontype': {
+ 'Meta': {'object_name': 'SubscriptionType', 'db_table': "'subscriptiontype'"},
+ 'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'price': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_limit': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.virtualhost': {
+ 'Meta': {'unique_together': "[('application', 'virtualhost')]", 'object_name': 'VirtualHost', 'db_table': "'virtualhost'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'virtualhost': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.workerhost': {
+ 'Meta': {'object_name': 'WorkerHost', 'db_table': "'worker_host'"},
+ 'host': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_procs': ('django.db.models.fields.IntegerField', [], {'default': '100'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/0029_add_proc_type_to_Process.py b/src/server/master/management_database/management_database/migrations/0029_add_proc_type_to_Process.py
new file mode 100644
index 0000000..2b82391
--- /dev/null
+++ b/src/server/master/management_database/management_database/migrations/0029_add_proc_type_to_Process.py
@@ -0,0 +1,169 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Removing unique constraint on 'Process', fields ['application', 'host']
+ db.delete_unique('process', ['application_id', 'host'])
+
+ # Adding field 'Process.proc_type'
+ db.add_column('process', 'proc_type', self.gf('django.db.models.fields.CharField')(default='gunicorn', max_length=64), keep_default=False)
+
+ # Adding unique constraint on 'Process', fields ['application', 'host', 'proc_type']
+ db.create_unique('process', ['application_id', 'host', 'proc_type'])
+
+
+ def backwards(self, orm):
+
+ # Removing unique constraint on 'Process', fields ['application', 'host', 'proc_type']
+ db.delete_unique('process', ['application_id', 'host', 'proc_type'])
+
+ # Deleting field 'Process.proc_type'
+ db.delete_column('process', 'proc_type')
+
+ # Adding unique constraint on 'Process', fields ['application', 'host']
+ db.create_unique('process', ['application_id', 'host'])
+
+
+ models = {
+ 'management_database.activeapplicationname': {
+ 'Meta': {'object_name': 'ActiveApplicationName', 'db_table': "'active_application_name'"},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
+ },
+ 'management_database.allocationchange': {
+ 'Meta': {'object_name': 'AllocationChange', 'db_table': "'allocation_change'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'billed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'chargable': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Chargable']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'quantity': ('django.db.models.fields.IntegerField', [], {}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.application': {
+ 'Meta': {'object_name': 'Application', 'db_table': "'application'"},
+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"}),
+ 'app_gid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'bundle_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'cache_index_size_kb': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'cache_size_kb': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'celery_procs': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'cron_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'db_host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_max_size_mb': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+ 'db_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'db_port': ('django.db.models.fields.IntegerField', [], {'default': '3306'}),
+ 'db_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'debug': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {'default': '64'}),
+ 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {'default': '20'}),
+ 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+ 'setup_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+ 'web_uid': ('django.db.models.fields.IntegerField', [], {'default': '-1'})
+ },
+ 'management_database.billingevent': {
+ 'Meta': {'object_name': 'BillingEvent', 'db_table': "'billingevent'"},
+ 'application_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'cents': ('django.db.models.fields.IntegerField', [], {}),
+ 'chargable_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'memo': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'success': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'management_database.chargable': {
+ 'Meta': {'object_name': 'Chargable', 'db_table': "'chargable'"},
+ 'component': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'price': ('django.db.models.fields.IntegerField', [], {'default': '0'})
+ },
+ 'management_database.collaborator': {
+ 'Meta': {'unique_together': "[('application', 'user')]", 'object_name': 'Collaborator', 'db_table': "'collaborator'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"})
+ },
+ 'management_database.process': {
+ 'Meta': {'unique_together': "[('application', 'proc_type', 'host'), ('host', 'port')]", 'object_name': 'Process', 'db_table': "'process'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'num_procs': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '20000'}),
+ 'proc_type': ('django.db.models.fields.CharField', [], {'default': "'gunicorn'", 'max_length': '64'})
+ },
+ 'management_database.proxycache': {
+ 'Meta': {'object_name': 'ProxyCache', 'db_table': "'proxycache'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'management_database.sshpublickey': {
+ 'Meta': {'object_name': 'SshPublicKey', 'db_table': "'ssh_public_key'"},
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ssh_public_key': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.User']"})
+ },
+ 'management_database.subscription': {
+ 'Meta': {'object_name': 'Subscription', 'db_table': "'subscription'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'disabled': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
+ 'enabled': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'price': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True', 'blank': 'True'}),
+ 'subscription_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.SubscriptionType']"})
+ },
+ 'management_database.subscriptiontype': {
+ 'Meta': {'object_name': 'SubscriptionType', 'db_table': "'subscriptiontype'"},
+ 'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'price': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.user': {
+ 'Meta': {'object_name': 'User', 'db_table': "'user'"},
+ 'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'customer_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_limit': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'passwd': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.virtualhost': {
+ 'Meta': {'unique_together': "[('application', 'virtualhost')]", 'object_name': 'VirtualHost', 'db_table': "'virtualhost'"},
+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['management_database.Application']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'virtualhost': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'management_database.whitelist': {
+ 'Meta': {'object_name': 'WhiteList', 'db_table': "'whitelist'"},
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invite_code': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'referrer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['management_database.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'management_database.workerhost': {
+ 'Meta': {'object_name': 'WorkerHost', 'db_table': "'worker_host'"},
+ 'host': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_procs': ('django.db.models.fields.IntegerField', [], {'default': '100'})
+ }
+ }
+
+ complete_apps = ['management_database']
diff --git a/src/server/master/management_database/management_database/migrations/__init__.py b/src/server/master/management_database/management_database/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/server/master/management_database/management_database/models.py b/src/server/master/management_database/management_database/models.py
new file mode 100644
index 0000000..53d0e99
--- /dev/null
+++ b/src/server/master/management_database/management_database/models.py
@@ -0,0 +1,369 @@
+from django.db import models
+from django.db.utils import IntegrityError
+from datetime import datetime
+
+class WhiteList(models.Model):
+ class Meta:
+ db_table = 'whitelist'
+
+ email = models.CharField(max_length=255) # Shouldn't this be unique=True?
+ invite_code = models.CharField(max_length=255)
+ referrer = models.ForeignKey('User', blank = True, default = None, null = True)
+
+ @staticmethod
+ def verify(email, invite_code):
+ try:
+ wl = WhiteList.objects.get(email=email)
+ if wl.invite_code == invite_code:
+ return True
+ except:
+ pass
+ return False
+
+class User(models.Model):
+ class Meta:
+ db_table = 'user'
+
+ email = models.CharField(max_length=255, unique=True)
+ passwd = models.CharField(max_length=255)
+ admin = models.BooleanField(default=False)
+ customer_id = models.CharField(max_length=255)
+ referrer = models.ForeignKey('User', blank = True, default = None, null = True)
+ invite_limit = models.IntegerField(default=0)
+ first_name = models.CharField(max_length=255, blank = True, default = None, null = True)
+ last_name = models.CharField(max_length=255, blank = True, default = None, null = True)
+
+ @staticmethod
+ def get_by_email(email):
+ try:
+ return User.objects.get(email=email)
+ except:
+ return None
+
+ def add_ssh_public_key(self, ssh_public_key, comment):
+ if not SshPublicKey.objects.filter(user=self, ssh_public_key=ssh_public_key).exists():
+ SshPublicKey(user=self, ssh_public_key=ssh_public_key, comment=comment).save()
+
+ def get_ssh_public_keys(self):
+ return self.sshpublickey_set.all()
+
+ def remove_ssh_public_key(self, key_id):
+ self.sshpublickey_set.filter(id=key_id).delete()
+
+ def get_accessible_applications(self):
+ owned_applications = list(self.application_set.filter(deleted=None).all())
+ collaborating_applications = filter(lambda x: x.deleted==None, \
+ [x.application for x in self.collaborator_set.select_related(depth=1)])
+ applications = owned_applications + collaborating_applications
+ applications.sort(cmp=lambda x, y: cmp(x.name, y.name))
+ return applications
+
+ def get_subscriptions(self):
+ subs = []
+ apps = self.application_set.all()
+ for app in apps:
+ subs += list(app.subscription_set.all())
+ return subs
+
+ def get_active_subscriptions(self):
+ subs = []
+ apps = self.application_set.all()
+ for app in apps:
+ subs += list(app.subscription_set.filter(disabled=None))
+ return subs
+
+class SshPublicKey(models.Model):
+ class Meta:
+ db_table = 'ssh_public_key'
+
+ user = models.ForeignKey(User)
+ ssh_public_key = models.CharField(max_length=1024)
+ comment = models.CharField(max_length=64)
+
+ @staticmethod
+ def get_users_by_public_key_id(id):
+ # Two-step process in case two users have the same SSH public key.
+ ssh_public_key = SshPublicKey.objects.get(id=id).ssh_public_key
+ return [x.user for x in SshPublicKey.objects.filter(ssh_public_key=ssh_public_key)]
+
+class ActiveApplicationName(models.Model):
+ class Meta:
+ db_table = 'active_application_name'
+
+ name = models.CharField(max_length=255, unique=True)
+
+class Application(models.Model):
+ class Meta:
+ db_table = 'application'
+
+ account = models.ForeignKey(User)
+ bundle_version = models.CharField(max_length=255,default='')
+ name = models.CharField(max_length=255)
+ db_name = models.CharField(max_length=255)
+ db_username = models.CharField(max_length=255)
+ db_password = models.CharField(max_length=255)
+ db_host = models.CharField(max_length=255)
+ db_port = models.IntegerField(default=3306)
+ db_max_size_mb = models.IntegerField(default=5)
+ setup_uid = models.IntegerField(default=-1)
+ web_uid = models.IntegerField(default=-1)
+ cron_uid = models.IntegerField(default=-1)
+ app_gid = models.IntegerField(default=-1)
+ num_procs = models.IntegerField(default=1)
+ proc_num_threads = models.IntegerField(default=20)
+ proc_mem_mb = models.IntegerField(default=64)
+ proc_stack_mb = models.IntegerField(default=2)
+ cache_index_size_kb = models.IntegerField(default=64)
+ cache_size_kb = models.IntegerField(default=16384)
+ debug = models.BooleanField(default=False)
+ deleted = models.DateTimeField(null=True, blank=True)
+ celery_procs = models.IntegerField(default=0)
+
+ @staticmethod
+ def get_by_name(name):
+ try:
+ return Application.objects.get(name=name, deleted=None)
+ except:
+ return None
+
+ def mark_deleted(self):
+ if not self.deleted:
+ self.deleted = datetime.now()
+ self.save()
+ self.process_set.all().delete()
+ self.virtualhost_set.all().delete()
+ try:
+ ActiveApplicationName.objects.get(name=self.name).delete()
+ except:
+ pass
+
+ def report_allocation_change(self, chargable, quantity):
+ alloc = AllocationChange(application = self, chargable = chargable, quantity = quantity)
+ alloc.save()
+
+ def has_collaborator(self, user):
+ return Collaborator.objects.filter(application=self, user=user).exists()
+
+ def accessible_by(self, user):
+ return (self.deleted == None) and ((user.admin == True) or (self.account == user) or self.has_collaborator(user))
+
+ def accessible_by_any_of(self, users):
+ return any([self.accessible_by(u) for u in users])
+
+ def add_collaborator(self, email):
+ user = User.get_by_email(email)
+ if not user:
+ raise NoUserException(email)
+ if not self.deleted and (self.account != user) \
+ and not Collaborator.objects.filter(application=self, user=user).exists():
+ collaborator = Collaborator(application=self, user=user)
+ collaborator.save()
+ return True
+ else:
+ return False
+
+ def remove_collaborator(self, email):
+ user = User.get_by_email(email)
+ if user and not self.deleted:
+ try:
+ Collaborator.objects.get(application=self, user=user).delete()
+ except:
+ pass
+
+ def get_collaborators(self):
+ """ Returns email addresses of collaborators on this application (not including the owner). """
+ return [c.user.email for c in self.collaborator_set.all()]
+
+ def is_server_cache_enabled(self):
+ return self.cache_size_kb > 0
+
+ def enable_server_cache(self):
+ self.cache_index_size_kb = 64
+ self.cache_size_kb = 16384
+ self.save()
+
+ def disable_server_cache(self):
+ self.cache_index_size_kb = 0
+ self.cache_size_kb = 0
+ self.save()
+
+ def add_domain_name(self, domain_name):
+ if domain_name not in VirtualHost.get_virtualhosts_by_application(self):
+ VirtualHost(application = self, virtualhost = str(domain_name)).save()
+
+ def delete_domain_name(self, domain_name):
+ VirtualHost.objects.filter(application = self, virtualhost = str(domain_name)).delete()
+
+class NoUserException(Exception):
+ def __init__(self, email):
+ self.email = email
+ def __str__(self):
+ return 'No user exists with email address %s' % self.email
+
+class Collaborator(models.Model):
+ class Meta:
+ db_table = 'collaborator'
+ unique_together = [('application', 'user')]
+
+ application = models.ForeignKey(Application)
+ user = models.ForeignKey(User)
+
+class WorkerHost(models.Model):
+ class Meta:
+ db_table = 'worker_host'
+
+ host = models.CharField(max_length=255, unique=True)
+ max_procs = models.IntegerField(default=100)
+ # In the future, we may want to distinguish between hosts used by paid
+ # users vs. free users, and offer paid users better service while packing
+ # as many free users as possible onto a host.
+
+class Process(models.Model):
+ class Meta:
+ db_table = 'process'
+ unique_together = [('application', 'proc_type', 'host'), ('host', 'port')]
+
+ application = models.ForeignKey(Application)
+ host = models.CharField(max_length=255)
+ port = models.IntegerField(default=20000)
+ num_procs = models.IntegerField(default=1)
+ proc_type = models.CharField(max_length=64, default='gunicorn')
+
+ @staticmethod
+ def get_hosts_ports_by_application(application):
+ try:
+ return [(process.host, process.port) for process in application.process_set.all()]
+ except:
+ return None
+
+class Chargable(models.Model):
+ class Meta:
+ db_table = 'chargable'
+
+ component = models.IntegerField(default=0)
+ price = models.IntegerField(default=0)
+
+ components = {
+ 'application_processes': 0,
+ 'background_processes':1
+ }
+ @staticmethod
+ def get_by_component(name):
+ try:
+ return Chargable.objects.get(component=Chargable.components[name])
+ except:
+ return None
+
+ @staticmethod
+ def get_by_id(id):
+ try:
+ return Chargable.objects.get(component=id)
+ except:
+ return None
+
+ def __str__(self):
+ reverse = dict((v,k) for k, v in self.components.iteritems())
+ return reverse[self.component]
+
+
+class AllocationChange(models.Model):
+ class Meta:
+ db_table = 'allocation_change'
+
+ application = models.ForeignKey(Application)
+ chargable = models.ForeignKey(Chargable, null=True)
+ quantity = models.IntegerField()
+ billed = models.BooleanField(default=False)
+ timestamp = models.DateTimeField(auto_now_add=True)
+
+class ProxyCache(models.Model):
+ class Meta:
+ db_table = 'proxycache'
+
+ application = models.ForeignKey(Application)
+ host = models.CharField(max_length=255)
+
+ @staticmethod
+ def get_proxycache_hosts_by_application_name(name):
+ return ProxyCache.get_proxycache_hosts_by_application(Application.get_by_name(name))
+
+ @staticmethod
+ def get_proxycache_hosts_by_application(application):
+ try:
+ return [proxycache.host for proxycache in application.proxycache_set.all()]
+ except:
+ return None
+
+class VirtualHost(models.Model):
+ class Meta:
+ db_table = 'virtualhost'
+ unique_together = [('application', 'virtualhost')]
+
+ application = models.ForeignKey(Application)
+ virtualhost = models.CharField(max_length=255)
+
+ @staticmethod
+ def get_virtualhosts_by_application_name(name):
+ return VirtualHost.get_virtualhosts_by_application(Application.get_by_name(name))
+
+ @staticmethod
+ def get_virtualhosts_by_application(application):
+ try:
+ return [virtualhost.virtualhost for virtualhost in application.virtualhost_set.all()]
+ except:
+ return None
+
+class BillingEvent(models.Model):
+ class Meta:
+ db_table = 'billingevent'
+
+ email = models.CharField(max_length=255)
+ customer_id = models.CharField(max_length=255)
+ application_name = models.CharField(max_length=255)
+ chargable_name = models.CharField(max_length=255)
+ timestamp = models.DateTimeField(auto_now_add=True)
+ cents = models.IntegerField()
+ success = models.BooleanField()
+ memo = models.CharField(max_length=255, blank=True, null=True)
+
+class SubscriptionType(models.Model):
+ class Meta:
+ db_table = 'subscriptiontype'
+
+ name = models.CharField(max_length=255, blank=True, null=True)
+ description = models.CharField(max_length=255, blank=True, null=True)
+ price = models.IntegerField(blank=True, null=True)
+
+ @staticmethod
+ def get_by_name(name):
+ return SubscriptionType.objects.get(name=name)
+
+class Subscription(models.Model):
+ class Meta:
+ db_table = 'subscription'
+
+ application = models.ForeignKey(Application)
+ subscription_type = models.ForeignKey(SubscriptionType)
+ price = models.IntegerField(blank=True, null=True, default=0)
+ enabled = models.DateTimeField(auto_now_add=True)
+ disabled = models.DateTimeField(blank=True, null=True, default=None)
+
+ @staticmethod
+ def subscribe(application, subscription_name, price=None):
+ assert not Subscription.is_subscribed(application, subscription_name)
+ subscription_type = SubscriptionType.get_by_name(subscription_name)
+ if price == None:
+ price = subscription_type.price
+ Subscription(application=application, subscription_type=subscription_type, price=price).save()
+
+ @staticmethod
+ def is_subscribed(application, subscription_name):
+ subscription_type = SubscriptionType.get_by_name(subscription_name)
+ return Subscription.objects.filter(application=application, subscription_type=subscription_type, disabled=None).exists()
+
+ @staticmethod
+ def unsubscribe(application, subscription_name):
+ subscription_type = SubscriptionType.get_by_name(subscription_name)
+ for s in Subscription.objects.filter(application=application, subscription_type=subscription_type, disabled=None):
+ s.disabled = datetime.now()
+ s.save()
diff --git a/src/server/master/management_database/management_database/settings.py b/src/server/master/management_database/management_database/settings.py
new file mode 100644
index 0000000..d746d4d
--- /dev/null
+++ b/src/server/master/management_database/management_database/settings.py
@@ -0,0 +1,15 @@
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.mysql',
+ 'NAME': 'djangy',
+ 'USER': 'djangy',
+ 'PASSWORD': 'password goes here',
+ 'HOST': '',
+ 'PORT': ''
+ }
+}
+
+INSTALLED_APPS = (
+ 'management_database',
+ 'south'
+)
diff --git a/src/server/master/management_database/setup.py b/src/server/master/management_database/setup.py
new file mode 100644
index 0000000..63b70f5
--- /dev/null
+++ b/src/server/master/management_database/setup.py
@@ -0,0 +1,13 @@
+from setuptools import setup, find_packages
+
+setup(
+ name="management_database",
+ version="0.1",
+ packages=find_packages(),
+ install_requires=['Django>=1.0', 'mysql-python', 'South'],
+ author="David J. Paola",
+ author_email="dave@djangy.com",
+ description="Djangy.com Management Database model specification",
+ keywords="djangy django",
+ url="http://www.djangy.com"
+)
diff --git a/src/server/master/master_api/.gitignore b/src/server/master/master_api/.gitignore
new file mode 100644
index 0000000..822e66a
--- /dev/null
+++ b/src/server/master/master_api/.gitignore
@@ -0,0 +1,3 @@
+build
+dist
+master_api.egg-info
diff --git a/src/server/master/master_api/master_api/__init__.py b/src/server/master/master_api/master_api/__init__.py
new file mode 100644
index 0000000..6f093fa
--- /dev/null
+++ b/src/server/master/master_api/master_api/__init__.py
@@ -0,0 +1,2 @@
+from application_api import *
+from billing_api import *
diff --git a/src/server/master/master_api/master_api/application_api.py b/src/server/master/master_api/master_api/application_api.py
new file mode 100644
index 0000000..65b9c57
--- /dev/null
+++ b/src/server/master/master_api/master_api/application_api.py
@@ -0,0 +1,125 @@
+# Standard Python libraries
+import os, re
+# Djangy libraries installed in our virtualenv
+from djangy_server_shared import *
+from management_database.models import Application, Process, Chargable, Subscription
+# Libraries within this package
+import exceptions
+
+open_log_file(os.path.join(LOGS_DIR, 'master_api.log'), 0600)
+
+def retrieve_logs(application_name):
+ return run_external_program([os.path.join(MASTER_SETUID_DIR, 'run_retrieve_logs'), 'application_name', application_name])['stdout_contents']
+
+def name_available(name):
+ """ Checks for application name availability. """
+ if Application.get_by_name(name):
+ return False
+ else:
+ return (re.match('^[A-Za-z][A-Za-z0-9]{4,14}$', name) != None) \
+ and not (name in RESERVED_APPLICATION_NAMES)
+
+def toggle_debug(application_name, debug):
+ cmd = [os.path.join(MASTER_SETUID_DIR, 'run_allocate'), 'application_name', application_name, 'debug', str(debug)]
+ run_external_program(cmd)
+
+def update_application_allocation(application_name, changes):
+ allocations = {
+ 'application_processes':'num_procs',
+ 'background_processes':'celery_procs'
+ }
+ try:
+ application = Application.get_by_name(application_name)
+ cmd = [os.path.join(MASTER_SETUID_DIR, 'run_allocate'), 'application_name', application_name]
+ for key in changes.keys():
+ if allocations.get(key):
+ cmd += [str(allocations[key]), str(changes[key])]
+
+ result = run_external_program(cmd)
+ if external_program_encountered_error(result):
+ raise exceptions.UpdateAllocationException(result['exit_code'], application_name)
+
+ for key in changes.keys():
+ if allocations.get(key):
+ try:
+ application.report_allocation_change(Chargable.get_by_component(key), str(changes[key]))
+ except Exception as e:
+ log_error_message(e)
+
+ except Exception as e:
+ log_last_exception()
+ logging.error(e)
+ return False
+ return True
+
+def _call_proxycache_manager(application_name):
+ return run_external_program([os.path.join(MASTER_SETUID_DIR, 'run_configure_proxycache'), 'application_name', application_name])
+
+def add_domain_name(application_name, domain_name):
+ Application.get_by_name(application_name).add_domain_name(domain_name)
+ result = _call_proxycache_manager(application_name)
+ if external_program_encountered_error(result):
+ raise exceptions.AddDomainException(result['exit_code'], application_name, domain_name)
+
+def delete_domain_name(application_name, domain_name):
+ Application.get_by_name(application_name).delete_domain_name(domain_name)
+ result = _call_proxycache_manager(application_name)
+ if external_program_encountered_error(result):
+ raise exceptions.DeleteDomainException(result['exit_code'], application_name, domain_name)
+
+def enable_server_cache(application_name):
+ application = Application.get_by_name(application_name)
+ application.enable_server_cache()
+ result = _call_proxycache_manager(application_name)
+ assert not external_program_encountered_error(result)
+
+def disable_server_cache(application_name):
+ application = Application.get_by_name(application_name)
+ application.disable_server_cache()
+ result = _call_proxycache_manager(application_name)
+ assert not external_program_encountered_error(result)
+
+def add_application(application_name, email, pubkey):
+ result = run_external_program([ \
+ os.path.join(MASTER_SETUID_DIR, 'run_add_application'), \
+ 'application_name', application_name, 'email', email, 'pubkey', pubkey])
+ if external_program_encountered_error(result):
+ raise exceptions.AddApplicationException(result['exit_code'], application_name, email, pubkey)
+
+def remove_application(application_name):
+ result = run_external_program([ \
+ os.path.join(MASTER_SETUID_DIR, 'run_delete_application'), \
+ 'application_name', application_name])
+ if external_program_encountered_error(result):
+ raise exceptions.RemoveApplicationException(result['exit_code'], application_name)
+
+def get_application_log(application_name, log_name='django.log'):
+ result = run_external_program([ \
+ os.path.join(MASTER_SETUID_DIR, 'run_get_application_log'), \
+ 'application_name', application_name, \
+ 'log_name', log_name])
+ if external_program_encountered_error(result):
+ return '[Log not found]'
+ else:
+ return result['stdout_contents']
+
+def command(application_name, cmd, *args):
+ result = run_external_program([ \
+ os.path.join(MASTER_SETUID_DIR, 'run_command'), \
+ 'application_name', application_name, \
+ 'command', cmd] + list(args))
+ return result['stdout_contents']
+
+def add_ssh_public_key(email, ssh_public_key):
+ result = run_external_program([ \
+ os.path.join(MASTER_SETUID_DIR, 'run_add_ssh_public_key'), \
+ 'email', email, \
+ 'ssh_public_key', ssh_public_key])
+ assert not external_program_encountered_error(result)
+
+def remove_ssh_public_key(email, ssh_public_key_id):
+ result = run_external_program([ \
+ os.path.join(MASTER_SETUID_DIR, 'run_remove_ssh_public_key'), \
+ 'email', email, \
+ 'ssh_public_key_id', ssh_public_key_id])
+ assert not external_program_encountered_error(result)
diff --git a/src/server/master/master_api/master_api/billing_api.py b/src/server/master/master_api/master_api/billing_api.py
new file mode 100644
index 0000000..a978de9
--- /dev/null
+++ b/src/server/master/master_api/master_api/billing_api.py
@@ -0,0 +1,217 @@
+# Standard Python libraries
+import datetime
+# Djangy libraries installed in our virtualenv
+from djangy_server_shared import * # referenced?
+from management_database.models import User, AllocationChange, Chargable, BillingEvent, Subscription
+# Libraries within this package
+from devpayments import DevPayException
+import exceptions, devpayments
+import application_api
+
+def update_billing_info(email, info):
+ def _unpack_customer_info():
+ return {
+ 'first_name':info.get('first_name', ''),
+ 'last_name':info.get('last_name', ''),
+ 'email':email
+ }
+
+ def _unpack_billing_info():
+ return {
+ 'number':info['cc_number'],
+ 'exp_month':info['expiration_month'],
+ 'exp_year':info['expiration_year'],
+ 'cvc':info['cvv']
+ }
+
+ def _create_new_customer():
+ devpay = devpayments.Client(DEVPAYMENTS_API_KEY)
+ try:
+ result = devpay.createCustomer(
+ mnemonic = _unpack_customer_info()['email'],
+ card = _unpack_billing_info()
+ )
+ user.customer_id = result.id
+ user.save()
+ except DevPayException as e:
+ return e.message
+ return True
+
+ def _update_customer():
+ devpay = devpayments.Client(DEVPAYMENTS_API_KEY)
+ try:
+ result = devpay.updateCustomer(
+ id = user.customer_id,
+ mnemonic = _unpack_customer_info()['email'],
+ card = _unpack_billing_info()
+ )
+ user.customer_id = result.id
+ user.save()
+ except DevPayException as e:
+ return e.message
+ return True
+
+ user = User.get_by_email(email)
+ if not user:
+ raise exceptions.UserNotFoundException(email)
+ message = True
+ if user.customer_id == '-1' or user.customer_id == '':
+ try:
+ result = _create_new_customer()
+ if True != result:
+ message = result
+ except Exception as e:
+ log_error_message(e)
+ return "Our system encountered an error. Please contact support@djangy.com."
+ else:
+ try:
+ result = _update_customer()
+ if True != result:
+ message = result
+ except Exception as e:
+ log_error_message(e)
+ return "Our system encountered an error. Please contact support@djangy.com."
+
+ try:
+ cust_info = _unpack_customer_info()
+ user.first_name = cust_info.get('first_name', '')
+ user.last_name = cust_info.get('last_name', '')
+ user.save()
+ except Exception as e:
+ log_error_message(e)
+ return "Our system encountered an error. Please contact support@djangy.com."
+ return message
+
+def retrieve_billing_info(user):
+ customer_id = user.customer_id
+ if customer_id == '-1' or customer_id == '':
+ return None
+ try:
+ devpay = devpayments.Client(DEVPAYMENTS_API_KEY)
+ result = devpay.retrieveCustomer(id=customer_id)
+ last4 = result.active_card.get('last4', '')
+ usage = ''
+ try:
+ usage = result.next_recurring_charge.get('amount', '')
+ except:
+ pass
+ bill_date = ''
+ try:
+ bill_date = result.next_recurring_charge.get('date', '')
+ except:
+ pass
+ if last4 != '':
+ last4 = "**** **** **** %s" % last4
+ return {
+ 'first_name':user.first_name,
+ 'last_name':user.last_name,
+ 'cc_number':last4,
+ 'usage':usage,
+ 'bill_date':bill_date
+ }
+ except DevPayException as e:
+ log_error_message(e.message)
+ return None
+ except Exception as e:
+ log_error_message(e)
+ return None
+
+def report_all_usage():
+ emails = [user.email for user in User.objects.all()]
+
+ for email in emails:
+ report_user_usage(email)
+
+def report_user_usage(email):
+ user = User.get_by_email(email)
+ if not user:
+ raise exceptions.UserNotFoundException(email)
+ for application in user.application_set.all():
+ changes = AllocationChange.objects.filter(application=application).filter(billed=False)
+ if changes.count() < 1:
+ continue
+ log_info_message("for application %s, reporting %s changes" % (application, changes.count()))
+ for chargable in Chargable.objects.all():
+ total_seconds = 0.0
+ total_cents = 0.0
+ # only look at allocs from before one minute ago
+ now = datetime.datetime.now() - datetime.timedelta(seconds=60)
+ allocs = list(changes.filter(chargable=chargable).filter(timestamp__lt=now).order_by('-timestamp'))
+ if len(allocs) < 1:
+ continue
+ latest = allocs[-1]
+ latest_copy = AllocationChange(application = application, chargable = chargable, quantity = latest.quantity, timestamp = now)
+ latest_copy.save()
+ allocs.insert(0, latest_copy)
+ for alloc in allocs:
+ if alloc == latest_copy:
+ continue
+ diff = (now - alloc.timestamp).seconds
+ if chargable.component == Chargable.components['application_processes']:
+ # the (alloc.quantity - 1) is to ensure the first process is free
+ price = (diff * (alloc.quantity - 1) * (chargable.price / 3600.0))
+ else:
+ price = (diff * (alloc.quantity) * (chargable.price / 3600.0))
+ total_cents += price
+ total_seconds += diff
+ now = alloc.timestamp
+ alloc.billed = True
+ total_hours = (total_seconds / 3600) + 1
+ result = report_usage(user, total_cents, memo="%s hours for %s" % (total_hours, chargable))
+ if result:
+ [alloc.save() for alloc in allocs]
+ be = BillingEvent(
+ email = email,
+ customer_id = user.customer_id,
+ application_name = application.name,
+ chargable_name = str(chargable),
+ cents = total_cents,
+ success = True,
+ memo = "devpayments dump: %s" % str(result)
+ )
+ be.save()
+ log_info_message("Reported %s cents for %s for application %s" % (total_cents, chargable, application.name))
+ else:
+ be = BillingEvent(
+ email = email,
+ customer_id = user.customer_id,
+ application_name = application.name,
+ chargable_name = str(chargable),
+ cents = total_cents,
+ success = False,
+ memo = "devpayments dump: %s" % str(result)
+ )
+ be.save()
+ log_error_message("Reporting failed for %s cents for %s for application %s: %s" % (total_cents, chargable, application.name, result))
+
+def report_usage(user, quantity, memo=""):
+ devpay = devpayments.Client(DEVPAYMENTS_API_KEY)
+ try:
+ result = devpay.billCustomer(
+ id = user.customer_id,
+ amount = int(quantity),
+ currency = 'usd'
+ )
+ return result
+ except Exception as e:
+ log_error_message(e)
+ return False
+
+def update_devpayments_subscription(user):
+ total_cents = sum([sub.price for sub in user.get_active_subscriptions()])
+ customer_id = user.customer_id
+
+ devpay = devpayments.Client(DEVPAYMENTS_API_KEY)
+ try:
+ result = devpay.updateCustomer(
+ id = user.customer_id,
+ subscription = {
+ 'amount':total_cents,
+ 'per':'month',
+ 'currency':'usd'
+ }
+ )
+ return result
+ except Exception as e:
+ log_error_message(e)
+ return False
diff --git a/src/server/master/master_api/master_api/devpayments/__init__.py b/src/server/master/master_api/master_api/devpayments/__init__.py
new file mode 100644
index 0000000..6942774
--- /dev/null
+++ b/src/server/master/master_api/master_api/devpayments/__init__.py
@@ -0,0 +1,174 @@
+# /dev/payments python bindings
+# for usage, see example.py
+# author: Patrick Collison
+
+import urllib2
+import urllib # need urlencode
+import json
+
+class Response(object):
+ def __init__(self, d):
+ self.dict = d
+
+ def __getattr__(self, name):
+ return self.dict[name]
+
+ def __str__(self):
+ return str(self.dict)
+
+class DevPayException(Exception):
+ def __init__(self, msg):
+ self.message = msg
+ super(DevPayException, self).__init__(msg)
+
+ def message(self):
+ self.message
+
+class CardException(DevPayException):
+ pass
+
+class InvalidRequestException(DevPayException):
+ pass
+
+class APIException(DevPayException):
+ pass
+
+class Client(object):
+ API_URL = 'https://api.devpayments.com/v1'
+
+ def __init__(self, key):
+ self.key = key
+
+ def retrieve(self, **params):
+ """
+ Fetch a DP charge token representing the supplied transaction as described in params, assuming the transaction has previously been prepared or executed; does not execute the transaction.
+ """
+ self.__requireParams(params, ['id'])
+ return self.__req('retrieve_charge', params)
+
+ def execute(self, **params):
+ """
+ Execute the described transaction. Transaction is specified either using a DP token or by supplying amount and currency arguments.
+
+ params:
+ {
+ * amount: integer amount to be charged in cents
+ * currency: lowercase 3-character string from set {usd, cad, ars,...} - for full specification see http://en.wikipedia.org/wiki/ISO_4217
+ }
+ AND
+ * card: dictionary object describing card details
+ {
+ * number: string representing credit card number
+ * exp_year: integer representing credit card expiry year
+ * exp_month: integer representing credit card expiry month
+ *OPTIONAL* name: string representing cardholder name
+ *OPTIONAL* address_line_1: string representing cardholder address, line 1
+ *OPTIONAL* address_line_2: string representing cardholder address, line 2
+ *OPTIONAL* address_zip: string representing cardholder zip
+ *OPTIONAL* address_state: string representing cardholder state
+ *OPTIONAL* address_country: string representing cardholder country
+ *OPTIONAL* cvc: CVC Number
+ }
+ OR
+ * customer: the id of an existing customer
+
+ """
+ self.__requireParams(params, ['amount', 'currency'])
+
+ return self.__req('execute_charge', params)
+
+ def refund(self, **params):
+ """
+ Refund a previously executed charge by passing this method the charge token
+ """
+ self.__requireParams(params, ['id'])
+ return self.__req('refund_charge', params)
+
+ def createCustomer(self, **params):
+ """
+ Create a new customer with the given token, and set the supplied
+ credit card as the active card to be their active card.
+ Used for recurring billing.
+ """
+ return self.__req('create_customer', params)
+
+ def updateCustomer(self, **params):
+ """
+ Set a credit card as the active card for a given customer. Used for recurring billing.
+ """
+ self.__requireParams(params, ['id'])
+ return self.__req('update_customer', params)
+
+ def billCustomer(self, **params):
+ """
+ Add a once-off amount to a customer's account. Used for recurring billing.
+ """
+ self.__requireParams(params, ['id', 'amount'])
+ return self.__req('bill_customer', params)
+
+ def retrieveCustomer(self, **params):
+ """
+ Retrieve billing info for the given customer. Used for recurring billing.
+ """
+ self.__requireParams(params, ['id'])
+ return self.__req('retrieve_customer', params)
+
+ def deleteCustomer(self, **params):
+ """
+ Delete the given customer. They will not be charged again, even if their is an outstanding balance on their account.
+ """
+ self.__requireParams(params, ['id'])
+ return self.__req('delete_customer', params)
+
+ def __encodeInner(self, d):
+ """
+ We want post vars of form:
+ {'foo': 'bar', 'nested': {'a': 'b', 'c': 'd'}}
+ to become:
+ foo=bar&nested[a]=b&nested[c]=d
+ """
+ stk = []
+ for key, value in d.items():
+ if isinstance(value, dict):
+ n = {}
+ for k, v in value.items():
+ n["%s[%s]" % (key, k)] = v
+ stk.extend(self.__encodeInner(n))
+ else:
+ stk.append((key, value))
+ return stk
+
+ def __encode(self, d):
+ """
+ Internal: encode a string for url representation
+ """
+ return urllib.urlencode(self.__encodeInner(d))
+
+ def __requireParams(self, params, req):
+ """
+ Internal: strict verification of parameter list
+ """
+ for r in req:
+ if not params.has_key(r):
+ raise InvalidRequestException('Missing required param: %s' % r)
+
+ def __req(self, meth, params):
+ """
+ Internal: mechanism for requesting an API call from the pay-server
+ """
+ params = params.copy()
+ params['method'] = meth
+ params['key'] = self.key
+ params['client'] = {'type':'binding', 'language':'python', 'version':'1.4.1'}
+ c = urllib2.urlopen(self.API_URL, self.__encode(params))
+ resp = json.loads(c.read())
+
+ if resp.get('error'):
+ err = {
+ 'card_error': lambda msg: CardException(msg),
+ 'invalid_request_error': lambda msg: InvalidRequestException(msg),
+ 'api_error': lambda msg: APIException(msg)
+ }
+ raise err[resp['error']['type']](resp['error']['message'])
+
+ return Response(resp)
diff --git a/src/server/master/master_api/master_api/exceptions.py b/src/server/master/master_api/master_api/exceptions.py
new file mode 100644
index 0000000..302c938
--- /dev/null
+++ b/src/server/master/master_api/master_api/exceptions.py
@@ -0,0 +1,60 @@
+class AddApplicationException(Exception):
+ """Error adding application."""
+ def __init__(self, result, application_name, email, pubkey):
+ self.result = result
+ self.application_name = application_name
+ self.email = email
+ self.pubkey = pubkey
+ def __str__(self):
+ return 'Error adding application. Return code: %i, application_name: "%s", email: "%s", pubkey: "%s"' % \
+ (self.result, self.application_name, self.email, self.pubkey)
+
+class RemoveApplicationException(Exception):
+ """Error removing application."""
+ def __init__(self, result, application_name):
+ self.result = result
+ self.application_name = application_name
+ def __str__(self):
+ return 'Error removing application. Return code: %i, application_name: "%s".' % (self.result, self.application_name)
+
+class UserNotFoundException(Exception):
+ """ Error finding user """
+ def __init__(self, email):
+ self.email = email
+ def __str__(self):
+ return "Error finding user with email: %s." % self.email
+
+class UpdateBillingException(Exception):
+ def __init__(self, message):
+ self.message = message
+ def __str__(self):
+ return self.message
+
+class ComponentNotFoundException(Exception):
+ def __init__(self, component):
+ self.component = component
+ def __str__(self):
+ return "Error looking up component: %s" % self.component
+
+class UpdateAllocationException(Exception):
+ def __init__(self, result, application_name):
+ self.result = result
+ self.application_name = application_name
+ def __str__(self):
+ return "Error updating allocation. Return code: %i, application_name: %s" % (self.result, self.application_name)
+
+class AddDomainException(Exception):
+ def __init__(self, result, application_name, domain_name):
+ self.result = result
+ self.application_name = application_name
+ self.domain_name = domain_name
+ def __str__(self):
+ return "Error adding domain '%s' to application '%s'. Return code: %i" % (self.domain_name, self.application_name, self.result)
+
+class DeleteDomainException(Exception):
+ def __init__(self, result, application_name, domain_name):
+ self.result = result
+ self.application_name = application_name
+ self.domain_name = domain_name
+ def __str__(self):
+ return "Error deleting domain '%s' to application '%s'. Return code: %i" % (self.domain_name, self.application_name, self.result)
diff --git a/src/server/master/master_api/setup.py b/src/server/master/master_api/setup.py
new file mode 100644
index 0000000..0adf8b7
--- /dev/null
+++ b/src/server/master/master_api/setup.py
@@ -0,0 +1,13 @@
+from setuptools import setup, find_packages
+
+setup(
+ name="master_api",
+ version="0.1",
+ packages=find_packages(),
+ author="David J. Paola",
+ author_email="dave@djangy.com",
+ description="Djangy.com Master API",
+ keywords="djangy django",
+ url="http://www.djangy.com",
+ license="University of Illinois/NCSA Open Source License"
+)
diff --git a/src/server/master/master_manager/__init__.py b/src/server/master/master_manager/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/server/master/master_manager/add_application.py b/src/server/master/master_manager/add_application.py
new file mode 100755
index 0000000..e2fb3d4
--- /dev/null
+++ b/src/server/master/master_manager/add_application.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python
+
+from shared import *
+import _mysql, re
+from ConfigParser import RawConfigParser
+from management_database.models import User, Application
+
+def main():
+ check_trusted_uid(sys.argv[0])
+ kwargs = check_and_return_keyword_args(sys.argv, ['application_name', 'email', 'pubkey'])
+ add_application(**kwargs)
+
+def gen_uids_gid(app_id):
+ setup_uid = (app_id * 3) + 100000
+ return {
+ 'setup_uid': setup_uid,
+ 'web_uid' : setup_uid + 1,
+ 'cron_uid' : setup_uid + 2,
+ 'app_gid' : setup_uid
+ }
+
+def add_application(application_name, email, pubkey):
+ """ Add the application specified by application_name and a corresponding database, owned by the user with the email address specified. """
+
+ # Claim the application name
+ ActiveApplicationName(name=application_name).save()
+
+ user = User.get_by_email(email)
+
+ # generate a secure password
+ db_password = gen_password()
+
+ # create the application row
+ app = Application()
+ app.name = application_name
+ app.account = user
+ app.db_name = application_name
+ app.db_username = application_name
+ app.db_password = db_password
+ app.db_host = DEFAULT_DATABASE_HOST
+ app.num_procs = 1
+ app.save()
+
+ # generate user and group ids to run as
+ uids_gid = gen_uids_gid(app.id)
+ app.setup_uid = uids_gid['setup_uid']
+ app.web_uid = uids_gid['web_uid']
+ app.cron_uid = uids_gid['cron_uid']
+ app.app_gid = uids_gid['app_gid']
+ app.save()
+
+ # enable git push
+ create_git_repository(application_name)
+ add_ssh_public_key(user, pubkey)
+
+ # allocate a proxycache host for the application -- improve on this later
+ ProxyCache(application = app, host = DEFAULT_PROXYCACHE_HOST).save()
+
+ # assign virtualhost on which to listen for application
+ VirtualHost(application = app, virtualhost = application_name + '.djangy.com').save()
+
+ # allocate the application to a worker host
+ # Note: this must happen after ProxyCache and VirtualHost are filled in.
+ allocate_workers(app)
+
+ # create the database
+ db = _mysql.connect(
+ host = DEFAULT_DATABASE_HOST,
+ user = DATABASE_ROOT_USER,
+ passwd = DATABASE_ROOT_PASSWORD)
+
+ try: # try to remove the user if it already exists
+ db.query(""" DROP USER '%s'@'%%';""" % application_name)
+ except:
+ pass
+
+ db.query("""
+ CREATE USER '%s'@'%%' IDENTIFIED BY '%s';""" % (application_name, db_password))
+
+ try: # try to drop the database in case it exists
+ db.query(""" DROP DATABASE %s;""" % application_name)
+ except:
+ pass
+
+ db.query("""
+ CREATE DATABASE %s;""" % application_name)
+
+ db.query("""
+ USE %s""" % application_name)
+
+ db.query("""
+ GRANT ALL ON %s.* TO '%s'@'%%';""" % (application_name, application_name))
+
+ return True
+
+if __name__ == '__main__':
+ main()
diff --git a/src/server/master/master_manager/add_ssh_public_key.py b/src/server/master/master_manager/add_ssh_public_key.py
new file mode 100755
index 0000000..20be5b4
--- /dev/null
+++ b/src/server/master/master_manager/add_ssh_public_key.py
@@ -0,0 +1,12 @@
+#!/usr/bin/env python
+
+from shared import *
+
+def main():
+ check_trusted_uid(sys.argv[0])
+ kwargs = check_and_return_keyword_args(sys.argv, ['email', 'ssh_public_key'])
+ user = User.get_by_email(kwargs['email'])
+ add_ssh_public_key(user, kwargs['ssh_public_key'])
+
+if __name__ == '__main__':
+ main()
diff --git a/src/server/master/master_manager/allocate.py b/src/server/master/master_manager/allocate.py
new file mode 100755
index 0000000..46f39e7
--- /dev/null
+++ b/src/server/master/master_manager/allocate.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+#
+# python application_name [num_procs ] [proc_num_threads ] [proc_mem_mb ] [proc_stack_mb ] [debug ]
+#
+
+from shared import *
+from management_database.models import Application, Process
+from django.core.exceptions import ObjectDoesNotExist
+
+def main():
+ check_trusted_uid(sys.argv[0])
+ kwargs = check_and_return_keyword_args(sys.argv, ['application_name'], \
+ ['num_procs', 'proc_num_threads', 'proc_mem_mb', \
+ 'proc_stack_mb', 'debug', 'celery_procs'])
+ try:
+ allocate_application(**kwargs)
+ except:
+ log_last_exception()
+ print 'Allocation failed for application "%s".' % kwargs['application_name']
+ sys.exit(1)
+
+def allocate_application(application_name, num_procs=None, proc_num_threads=None, proc_mem_mb=None, proc_stack_mb=None, debug=None, celery_procs=None):
+ application_info = Application.get_by_name(application_name)
+
+ if num_procs != None:
+ application_info.num_procs = int(num_procs)
+ if celery_procs != None:
+ application_info.celery_procs = int(celery_procs)
+ # Adjust allocation parameters relevant to each individual process of an
+ # application: num threads, total memory, stack size, debug
+ if proc_num_threads:
+ application_info.proc_num_threads = int(proc_num_threads)
+ if proc_mem_mb:
+ application_info.proc_mem_mb = int(proc_mem_mb)
+ if proc_stack_mb:
+ application_info.proc_stack_mb = int(proc_stack_mb)
+ if debug:
+ application_info.debug = (debug == 'True')
+
+ # Save the updated settings
+ application_info.save()
+
+ # Num processes is done differently because it requires
+ # reallocation of processes to hosts, and must directly
+ # contact hosts from which a process is removed.
+ if (num_procs != None) or (celery_procs != None):
+ allocate_workers(application_info)
+ else:
+ # Apply the settings to all deployed workers
+ call_worker_managers_allocate(application_name)
+ # (Don't need to update proxycache_managers)
+ #call_proxycache_managers_configure(application_name)
+
+if __name__ == '__main__':
+ main()
diff --git a/src/server/master/master_manager/change_password.py b/src/server/master/master_manager/change_password.py
new file mode 100644
index 0000000..d739ec3
--- /dev/null
+++ b/src/server/master/master_manager/change_password.py
@@ -0,0 +1,23 @@
+import sys
+from hashlib import md5
+from management_database import User
+
+def hash_password(email, password):
+ return md5("%s:%s" % (email, password)).hexdigest()
+
+def main(email, password):
+ try:
+ user = User.get_by_email(email)
+ user.passwd = hash_password(email, password)
+ user.save()
+ except Exception as e:
+ print "Exception: %s" % e
+
+ print "Success."
+
+if __name__ == '__main__':
+ if len(sys.argv) < 3:
+ print "Usage: python change_password.py "
+ email = str(sys.argv[1])
+ password = str(sys.argv[2])
+ main(email, password)
diff --git a/src/server/master/master_manager/command.py b/src/server/master/master_manager/command.py
new file mode 100755
index 0000000..382afee
--- /dev/null
+++ b/src/server/master/master_manager/command.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python
+#
+# Run a simple manage.py command as an application's setup_uid.
+#
+
+from shared import *
+
+ALLOWED_CMDS = [
+ 'syncdb',
+ 'migrate',
+ 'createsuperuser'
+]
+
+def main():
+ check_trusted_uid(sys.argv[0])
+
+ # Check command line arguments
+ if not (len(sys.argv) >= 5 \
+ and sys.argv[1] == 'application_name' \
+ and is_valid_django_app_name(sys.argv[2]) \
+ and sys.argv[3] == 'command'
+ and (sys.argv[4] in ALLOWED_CMDS)):
+ print_or_log_usage("Usage: %s application_name command [...]" % sys.argv[0])
+ sys.exit(1)
+
+ # Extract command line arguments
+ application_name = sys.argv[2]
+ command = ['python', 'manage.py'] + sys.argv[4:]
+ stdin_contents = None
+
+ # handle the special case of adding a superuser (piping python code to python manage.py shell)
+ if sys.argv[4] == 'createsuperuser':
+ command = ['python', 'manage.py', 'shell']
+ username = 'admin'
+ email = ''
+ password = gen_password()
+
+ stdin_contents = """
+from django.contrib.auth.models import User
+try:
+ found = User.objects.get(username='admin')
+ found.delete()
+except Exception, e:
+ pass
+
+User.objects.create_superuser('%s', '%s', '%s')
+
+""" % (username, email, password)
+ status = run_command(application_name, command, stdin_contents = stdin_contents, pass_stdout = False)
+ if status == 0:
+ print "Superuser '%s' created with password: '%s'" % (username, password)
+ sys.exit(status)
+
+ # Run the actual command as the application's setup_uid
+ sys.exit(run_command(application_name, command, stdin_contents = stdin_contents))
+
+def run_command(application_name, args, stdin_contents = None, pass_stdout = True):
+ try:
+ check_application_name(application_name)
+ # Look up application info in the database
+ application_info = Application.get_by_name(application_name)
+ bundle_version = application_info.bundle_version
+ setup_uid = application_info.setup_uid
+ app_gid = application_info.app_gid
+ # Validate UID/GID
+ check_setup_uid(setup_uid)
+ check_app_gid(app_gid)
+ # Compute the bundle path
+ bundle_name = '%s-%s' % (application_name, bundle_version)
+ bundle_path = os.path.join(BUNDLES_DIR, bundle_name)
+ # Find the django project within the repository; this is where
+ # manage.py needs to be run from
+ django_project_path = find_django_project(os.path.join(bundle_path, 'application'))
+ # Run the command
+ result = run_external_program(list(args), \
+ cwd=django_project_path, pass_stdout=pass_stdout, stderr_to_stdout=True, \
+ preexec_fn=gen_preexec(bundle_name, setup_uid, app_gid), stdin_contents = stdin_contents)
+ return result['exit_code']
+ except Exception as e:
+ log_last_exception()
+ print str(e)
+ sys.exit(2)
+
+def gen_preexec(bundle_name, uid, gid):
+ """Generate a preexec_fn to be passed to run_external_program() which (a) sets up the environment, and (b) sets the uid/gid"""
+ def command_preexec_fn():
+ os.environ.clear()
+ virtual_env_dir = os.path.join(BUNDLES_DIR, '%s/python-virtual' % bundle_name)
+ os.environ['PATH'] = '%s:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' % os.path.join(virtual_env_dir, 'bin')
+ os.environ['VIRTUAL_ENV'] = virtual_env_dir
+ set_uid_gid(uid, gid)
+ return command_preexec_fn
+
+if __name__ == '__main__':
+ main()
diff --git a/src/server/master/master_manager/configure_proxycache.py b/src/server/master/master_manager/configure_proxycache.py
new file mode 100755
index 0000000..0414048
--- /dev/null
+++ b/src/server/master/master_manager/configure_proxycache.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+#
+# python configure_proxycache.py application_name
+#
+
+from shared import *
+from management_database.models import Application, VirtualHost
+
+def main():
+ check_trusted_uid(sys.argv[0])
+ kwargs = check_and_return_keyword_args(sys.argv, ['application_name'])
+ try:
+ call_proxycache_managers_configure(kwargs['application_name'])
+ except:
+ log_last_exception()
+ print 'Configuring proxycache for application "%s" failed.' % kwargs['application_name']
+ sys.exit(1)
+
+if __name__ == '__main__':
+ main()
diff --git a/src/server/master/master_manager/copy_etc_hosts.py b/src/server/master/master_manager/copy_etc_hosts.py
new file mode 100755
index 0000000..e8b761b
--- /dev/null
+++ b/src/server/master/master_manager/copy_etc_hosts.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python
+#
+# Copy the /etc/hosts file to all other Djangy servers.
+#
+# We assume there is a line in /etc/hosts of the form "# djangy internal\n",
+# and all host lines below that specify the internal IP address of all the
+# Djangy servers. There may be duplicates, e.g., master1.srv.djangy.com and
+# worker1.srv.djangy.com might have the same IP address. We only copy the
+# /etc/hosts file to a given IP address once.
+#
+
+import re, subprocess
+
+def read_lines(path):
+ with open(path, 'r') as f:
+ return f.readlines()
+
+def get_hosts():
+ in_djangy_section = False
+ host_addresses = []
+ for etc_hosts_line in read_lines('/etc/hosts'):
+ if re.match(r'\s*#\s*djangy\s*internal.*\n', etc_hosts_line):
+ in_djangy_section = True
+ elif in_djangy_section:
+ matches = re.match(r'^\s*([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\s+', etc_hosts_line)
+ if matches:
+ host_addresses.append(matches.group(1))
+ return set(host_addresses)
+
+if __name__ == '__main__':
+ for host in get_hosts():
+ print host
+ subprocess.call(['scp', '/etc/hosts', host + ':/etc/hosts'])
diff --git a/src/server/master/master_manager/delete_application.py b/src/server/master/master_manager/delete_application.py
new file mode 100755
index 0000000..45b1644
--- /dev/null
+++ b/src/server/master/master_manager/delete_application.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+#
+# Delete an application.
+#
+
+from shared import *
+import _mysql
+from management_database.models import Application
+
+def main():
+ check_trusted_uid(program_name = sys.argv[0])
+ kwargs = check_and_return_keyword_args(sys.argv, ['application_name'])
+ application_name = kwargs['application_name']
+ try:
+ # Look up the application
+ application = Application.get_by_name(application_name)
+ # Disable the application to the outside world
+ call_proxycache_managers_delete_application(application_name)
+ # Stop running the application
+ call_worker_managers_delete_application(application_name)
+ # Remove the git repository
+ try:
+ shutil.rmtree(os.path.join(REPOS_DIR, application_name + ".git"))
+ except:
+ log_last_exception()
+ # Remove the database
+ db = _mysql.connect(
+ host = application.db_host,
+ user = DATABASE_ROOT_USER,
+ passwd = DATABASE_ROOT_PASSWORD)
+ try: # try to remove the user if it already exists
+ db.query(""" DROP USER '%s'@'%%';""" % application_name)
+ except:
+ pass
+ try: # try to drop the database in case it exists
+ db.query(""" DROP DATABASE %s;""" % application_name)
+ except:
+ pass
+ # Mark the application as deleted
+ application.mark_deleted()
+ except:
+ log_last_exception()
+ print 'Remove failed for application "%s".' % application_name
+ sys.exit(1)
+
+if __name__ == '__main__':
+ main()
diff --git a/src/server/master/master_manager/deploy.py b/src/server/master/master_manager/deploy.py
new file mode 100755
index 0000000..fdb0c45
--- /dev/null
+++ b/src/server/master/master_manager/deploy.py
@@ -0,0 +1,301 @@
+#!/usr/bin/env python
+
+from ConfigParser import RawConfigParser
+from mako.lookup import TemplateLookup
+from shared import *
+
+def main():
+ check_trusted_uid(sys.argv[0])
+ kwargs = check_and_return_keyword_args(sys.argv, ['application_name'])
+ deploy(**kwargs)
+
+def deploy(application_name):
+ print ''
+ print ''
+ print 'Welcome to Djangy!'
+ print ''
+ print 'Deploying project %s.' % application_name
+ print ''
+
+ try:
+ bundle_version = create_latest_bundle_via_db(application_name)
+ print 'Deploying to worker hosts...',
+ call_worker_managers_allocate(application_name)
+ call_proxycache_managers_configure(application_name)
+ log_info_message("Successfully deployed application '%s'!" % application_name)
+ print 'Done.'
+ print ''
+ except BundleAlreadyExistsException as e:
+ log_last_exception()
+ print 'WARNING: ' + str(e)
+ print 'Commit and push some changes to force redeployment.'
+ print ''
+ except ApplicationNotInDatabaseException as e:
+ log_last_exception()
+ print 'ERROR: ' + str(e)
+ print ''
+ except InvalidApplicationNameException as e:
+ log_last_exception()
+ print 'ERROR: ' + str(e)
+ print ''
+ except DjangoProjectNotFoundException as e:
+ log_last_exception()
+ print 'ERROR: No django project found in the git repository.'
+ print ''
+ except:
+ log_last_exception()
+ print 'Internal error, please contact support@djangy.com'
+ print ''
+
+def create_latest_bundle_via_db(application_name):
+ """Create a bundle from the latest version of an application. Fetches
+ details like administrative email address and database credentials from
+ the management database."""
+
+ check_application_name(application_name)
+
+ # Extract application info from management database
+ try:
+ application_info = Application.get_by_name(application_name)
+ user_info = application_info.account
+ bundle_params = {
+ 'application_name': application_name,
+ 'admin_email' : user_info.email,
+ 'db_host' : application_info.db_host,
+ 'db_port' : application_info.db_port,
+ 'db_name' : application_info.db_name,
+ 'db_username' : application_info.db_username,
+ 'db_password' : application_info.db_password,
+ 'setup_uid' : application_info.setup_uid,
+ 'web_uid' : application_info.web_uid,
+ 'cron_uid' : application_info.cron_uid,
+ 'app_gid' : application_info.app_gid,
+ 'celery_procs' : application_info.celery_procs,
+ }
+ # Also need to query DB for which hosts to run on; and
+ # resource allocations may be heterogenous across hosts
+ check_setup_uid(bundle_params['setup_uid'])
+ check_web_uid (bundle_params['web_uid' ])
+ check_cron_uid (bundle_params['cron_uid' ])
+ check_app_gid (bundle_params['app_gid' ])
+ except Exception as e:
+ log_last_exception()
+ print str(e)
+ # Couldn't find application_name in the management database!
+ raise ApplicationNotInDatabaseException(application_name)
+
+ # Create the bundle.
+ bundle_version = create_latest_bundle(**bundle_params)
+
+ # Update latest bundle version in the database.
+ application_info.bundle_version = bundle_version
+ application_info.save()
+
+ return bundle_version
+
+def create_latest_bundle(application_name, admin_email, db_host, db_port, db_name, db_username, db_password, \
+ setup_uid, web_uid, cron_uid, app_gid, celery_procs):
+ """Create a bundle from the latest version of an application. Requires
+ administrative email address and database credentials as arguments."""
+
+ # Put application code in /application
+ # and user-supplied config files in /config
+ print 'Cloning git repository...',
+ (bundle_version, bundle_name, bundle_application_path) = clone_repo_to_bundle(application_name)
+ print 'Done.'
+ print ''
+
+ bundle_path = os.path.join(BUNDLES_DIR, bundle_name)
+ recursive_chown_chmod(bundle_path, 0, app_gid, '0750')
+
+ # Find the Django project directory
+ django_project_path = find_django_project(os.path.join(bundle_path, 'application'))
+ django_project_module_name = os.path.basename(django_project_path)
+
+ # Rename the user's settings module to something that's unlikely to conflict
+ if os.path.isfile(os.path.join(django_project_path, 'settings', '__init__.py')):
+ user_settings_module_name = '__init__%s' % bundle_version
+ os.rename(os.path.join(django_project_path, 'settings', '__init__.py'), \
+ os.path.join(django_project_path, 'settings', user_settings_module_name + '.py'))
+ elif os.path.isfile(os.path.join(django_project_path, 'settings.py')):
+ user_settings_module_name = 'settings_%s' % bundle_version
+ os.rename(os.path.join(django_project_path, 'settings.py'), \
+ os.path.join(django_project_path, user_settings_module_name + '.py'))
+
+ # Create production settings.py file in /application/.../settings.py
+ # (code also exists in worker_manager.deploy)
+ print 'Creating production settings.py file...',
+ if os.path.isdir(os.path.join(django_project_path, 'settings')):
+ settings_path = os.path.join(django_project_path, 'settings', '__init__.py')
+ else:
+ settings_path = os.path.join(django_project_path, 'settings.py')
+ generate_config_file('generic_settings', settings_path,
+ user_settings_module_name = user_settings_module_name,
+ django_project_module_name = django_project_module_name,
+ db_host = db_host,
+ db_port = db_port,
+ db_name = db_name,
+ db_username = db_username,
+ db_password = db_password,
+ bundle_name = bundle_name,
+ debug = False,
+ celery_procs = None,
+ application_name = application_name)
+ os.chown(settings_path, 0, app_gid)
+ os.chmod(settings_path, 0750)
+ print 'Done.'
+ print ''
+
+ # The create_virtualenv.py program calls setuid() to run as setup_uid
+ python_virtual_path = os.path.join(bundle_path, 'python-virtual')
+ os.mkdir(python_virtual_path, 0770)
+ os.chown(python_virtual_path, 0, app_gid)
+ os.chmod(python_virtual_path, 0770)
+ sys.stdout.flush()
+ run_external_program([PYTHON_BIN_PATH, os.path.join(MASTER_MANAGER_SRC_DIR, 'uid_application_setup/create_virtualenv.py'), \
+ 'application_name', application_name, 'bundle_name', bundle_name, \
+ 'setup_uid', str(setup_uid), 'app_gid', str(app_gid)], \
+ pass_stdout=True, cwd=bundle_application_path)
+
+ os.umask(0227)
+
+ # Save the bundle info used by worker_manager to generate config files
+ print 'Saving bundle info...',
+ django_admin_media_path = get_django_admin_media_path(bundle_path)
+ admin_media_prefix='/admin_media'
+ BundleInfo( \
+ django_project_path = django_project_path, \
+ django_admin_media_path = django_admin_media_path, \
+ admin_media_prefix = admin_media_prefix, \
+ admin_email = admin_email, \
+ setup_uid = setup_uid, \
+ web_uid = web_uid, \
+ cron_uid = cron_uid, \
+ app_gid = app_gid, \
+ user_settings_module_name = user_settings_module_name, \
+ db_host = db_host, \
+ db_port = db_port, \
+ db_name = db_name, \
+ db_username = db_username, \
+ db_password = db_password
+ ).save_to_file(os.path.join(bundle_path, 'config', 'bundle_info.config'))
+ print 'Done.'
+ print ''
+
+ recursive_chown_chmod(bundle_path, 0, app_gid, '0750')
+ # TODO: don't chmod everything +x, only what needs it.
+
+ return bundle_version
+
+### Also exists in worker_manager.deploy ###
+def generate_config_file(__template_name__, __config_file_path__, **kwargs):
+ """Generate a bundle config file from a template, supplying arguments
+ from kwargs."""
+
+ # Load the template
+ lookup = TemplateLookup(directories = [WORKER_TEMPLATE_DIR])
+ template = lookup.get_template(__template_name__)
+ # Instantiate the template
+ instance = template.render(**kwargs)
+ # Write the instantiated template to the bundle
+ f = open(__config_file_path__, 'w')
+ f.write(instance)
+ f.close()
+
+def get_django_admin_media_path(bundle_path):
+ try:
+ # Currently assumes python2.6
+ f = open(os.path.join(bundle_path, 'python-virtual/lib/python2.6/site-packages/easy-install.pth'))
+ contents = f.read()
+ f.close()
+ django_path = re.search('^(.*/Django-.*\.egg)$', contents, flags=re.MULTILINE).group(0)
+ admin_media_path = os.path.join(django_path, 'django/contrib/admin/media')
+ return admin_media_path
+ except:
+ return os.path.join(bundle_path, 'directory_that_does_not_exist')
+
+def clone_repo_to_bundle(application_name):
+ """Try to clone an application's git repository and put the latest code
+ into a new bundle. Throws BundleAlreadyExistsException if a bundle
+ directory already exists for the latest version in the repository."""
+
+ # Create temporary directory in which to git clone
+ master_repo_path = os.path.join(REPOS_DIR, application_name + '.git')
+ temp_repo_path = tempfile.mkdtemp('.git', 'tmp-', BUNDLES_DIR)
+ os.chown(temp_repo_path, GIT_UID, GIT_UID)
+ os.chmod(temp_repo_path, 0700)
+ # git clone and read current version of git repository
+ result = run_external_program([PYTHON_BIN_PATH, os.path.join(MASTER_MANAGER_SRC_DIR, 'uid_git/clone_repo.py'), master_repo_path, temp_repo_path])
+ stdout = result['stdout_contents'].split('\n')
+ if len(stdout) < 1:
+ git_repo_version = ''
+ else:
+ git_repo_version = stdout[0]
+ # Validate git_repo_version
+ if result['exit_code'] != 0 or not validate_git_repo_version(git_repo_version):
+ raise GitCloneException(application_name, temp_repo_path)
+ # Compute bundle path
+ bundle_version = BUNDLE_VERSION_PREFIX + git_repo_version
+ bundle_name = application_name + '-' + bundle_version
+ bundle_path = os.path.join(BUNDLES_DIR, bundle_name)
+ # Check if bundle already exists
+ if os.path.exists(bundle_path):
+ shutil.rmtree(temp_repo_path)
+ raise BundleAlreadyExistsException(bundle_name)
+ # Make bundle directory
+ bundle_config_path = os.path.join(bundle_path, 'config')
+ os.makedirs(bundle_config_path)
+ os.chmod(bundle_path, 0700)
+ # Move checked-out repo to bundle
+ bundle_application_path = get_bundle_application_path(application_name, temp_repo_path, bundle_path)
+ os.makedirs(bundle_application_path)
+ os.rename(temp_repo_path, bundle_application_path)
+ # Copy the user-supplied configuration files to a deterministic location
+ copy_normal_file(os.path.join(bundle_application_path, 'djangy.config'), os.path.join(bundle_config_path, 'djangy.config'))
+ copy_normal_file(os.path.join(bundle_application_path, 'djangy.eggs' ), os.path.join(bundle_config_path, 'djangy.eggs' ))
+ copy_normal_file(os.path.join(bundle_application_path, 'djangy.pip' ), os.path.join(bundle_config_path, 'djangy.pip' ))
+ # Remove .git history which is not relevant in bundle
+ shutil.rmtree(os.path.join(bundle_application_path, '.git'))
+ # Note: bundle permissions must be adjusted by caller
+ return (bundle_version, bundle_name, bundle_application_path)
+
+def validate_git_repo_version(git_repo_version):
+ return (None != re.match('^[0-9a-f]{40}$', git_repo_version))
+
+def get_bundle_application_path(application_name, repo_path, bundle_path):
+ """Given the path to a copy of the code for an application and the path
+ to the bundle in which it needs to be inserted, determine the path where
+ the code needs to be moved to. The simple case is
+ (bundle_path)/application/(application_name), but if the user provides a
+ djangy.config file in the root of the repository, they can override
+ that, e.g., (bundle_path)/application/mydir"""
+ # Default: (bundle_path)/application/(application_name)
+ bundle_application_path = os.path.join(bundle_path, 'application', application_name)
+ # But if djangy.config file exists, look for:
+ # [application]
+ # rootdir=(some directory)
+ djangy_config_path = os.path.join(repo_path, 'djangy.config')
+ if is_normal_file(djangy_config_path):
+ parser = RawConfigParser()
+ parser.read(djangy_config_path)
+ try:
+ # Normalize the path relative to a hypothetical root directory,
+ # then remove the leftmost / to make the path relative again.
+ rootdir = os.path.normpath(os.path.join('/', parser.get('application', 'rootdir')))[1:]
+ # Put the path inside the bundle's application directory;
+ # normalizing will remove a rightmost / if rootdir == ''
+ bundle_application_path = os.path.normpath(os.path.join(bundle_path, 'application', rootdir))
+ except:
+ pass
+ return bundle_application_path
+
+def is_normal_file(path):
+ return not os.path.islink(path) and os.path.isfile(path)
+
+def copy_normal_file(src_path, dest_path):
+ if is_normal_file(src_path) and \
+ not os.path.exists(dest_path):
+ shutil.copyfile(src_path, dest_path)
+
+if __name__ == '__main__':
+ main()
diff --git a/src/server/master/master_manager/deploy_all.py b/src/server/master/master_manager/deploy_all.py
new file mode 100755
index 0000000..0a4f156
--- /dev/null
+++ b/src/server/master/master_manager/deploy_all.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+#
+# Can be used when installing/upgrading to rebuild all bundles and deploy
+# their applications. Not very fast.
+#
+
+from shared import *
+
+if __name__ == '__main__':
+ if len(sys.argv) > 1:
+ # If the user provided arguments, look up those individual
+ # applications.
+ applications = []
+ for application_name in sys.argv[1:]:
+ applications.extend(list(Application.objects.filter(deleted=None, name=application_name)))
+ else:
+ # No user arguments, so deploy all applications.
+ applications = Application.objects.filter(deleted=None)
+
+ for application in applications:
+ # Two step deployment (in case the process table is empty)
+ run_external_program(['/srv/djangy/run/master_manager/setuid/run_deploy',
+ 'application_name', application.name], pass_stdout=True)
+ allocate_workers(application)
diff --git a/src/server/master/master_manager/git_serve.py b/src/server/master/master_manager/git_serve.py
new file mode 100755
index 0000000..103b2c5
--- /dev/null
+++ b/src/server/master/master_manager/git_serve.py
@@ -0,0 +1,42 @@
+#!/srv/djangy/run/python-virtual/bin/python
+#
+# Note: doesn't import shared.ssh_and_git because this runs as the "git"
+# user, which doesn't have access to write to /srv/logs/master.log...
+#
+
+from djangy_server_shared import constants
+from management_database import *
+import os, re, sys
+
+def main():
+ try:
+ git_serve(int(sys.argv[1]))
+ except:
+ sys.stderr.write('Access denied. Please email support@djangy.com for help.\n')
+
+def git_serve(ssh_public_key_id):
+ """ Serve an incoming git push/pull request. Should only be called via ~git/.ssh/authorized_keys """
+ assert os.getuid() == constants.GIT_UID
+ # Usage: git_serve
+ # git_serve() should only be called via ~git/.ssh/authorized_keys
+ # Each line of authorized_keys specifies a particular SshPublicKey.id
+ # from the database as an argument to git_serve.
+ users = SshPublicKey.get_users_by_public_key_id(ssh_public_key_id)
+ # Look at the command git wanted to run.
+ # It should be one of git-upload-pack or git-receive-pack.
+ # (or their variants, 'git upload-pack' and 'git receive-pack')
+ # The argument to the command is .git
+ ssh_original_command = os.environ['SSH_ORIGINAL_COMMAND']
+ matches = re.match('^\s*(git(-|\s+)(?Pupload-pack|receive-pack))' \
+ + '\s+\'(?P[A-Za-z0-9]{1,15})\.git\'\s*$', ssh_original_command)
+ command = 'git-' + matches.group('command')
+ application_name = matches.group('application_name')
+ # Look up the requested application, and make sure that at least one
+ # user associated with the SSH key that was used has access to it.
+ application = Application.get_by_name(application_name)
+ if application.accessible_by_any_of(users):
+ # Finally, run the git server-side command
+ os.execvp('git', ['git', 'shell', '-c', "%s '/srv/git/repositories/%s.git'" % (command, application_name)])
+
+if __name__ == '__main__':
+ main()
diff --git a/src/server/master/master_manager/import_ssh_public_keys.py b/src/server/master/master_manager/import_ssh_public_keys.py
new file mode 100644
index 0000000..c10b0d7
--- /dev/null
+++ b/src/server/master/master_manager/import_ssh_public_keys.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+#
+# Utility script for importing gitosis-style SSH public keys.
+#
+# Must be run from a directory containing .pub files or you can
+# specify the path to such a directory on the command line.
+#
+# Will reject files containing things that don't look like SSH public keys.
+#
+
+from shared import *
+import os, sys
+
+def main():
+ if len(sys.argv) > 1:
+ os.chdir(sys.argv[1])
+ import_ssh_public_keys()
+
+def import_ssh_public_keys():
+ for filename in os.listdir('.'):
+ if filename.endswith('.pub'):
+ email = filename[:-4]
+ try:
+ user = User.get_by_email(email)
+ add_ssh_public_key(user, read_contents(filename))
+ print 'Added %s' % email
+ except Exception as e:
+ sys.stderr.write('Skipping %s (Error: %s)\n' % (filename, str(e)))
+ else:
+ sys.stderr.write('Skipping %s\n' % filename)
+
+def read_contents(filename):
+ f = open(filename)
+ contents = f.read()
+ f.close()
+ return contents
+
+if __name__ == '__main__':
+ main()
diff --git a/src/server/master/master_manager/post_receive.py b/src/server/master/master_manager/post_receive.py
new file mode 100755
index 0000000..9b84371
--- /dev/null
+++ b/src/server/master/master_manager/post_receive.py
@@ -0,0 +1,34 @@
+#!/srv/djangy/run/python-virtual/bin/python
+
+import warnings
+warnings.simplefilter("ignore")
+
+import os, re, sys
+
+excluded_repos = [
+ 'gitosis-admin',
+ 'djangy',
+ 'test',
+]
+
+if __name__ == '__main__':
+ # In practice, I've observed GIT_DIR to be '.', but it could potentially
+ # be something else. So we convert it to an absolute path and then
+ # remove it from the environment, because it tends to confuse other
+ # git operations performed later.
+ if os.environ.has_key('GIT_DIR'):
+ git_repository_path = os.path.abspath(os.environ['GIT_DIR'])
+ os.environ.pop('GIT_DIR')
+ else:
+ git_repository_path = os.getcwd()
+ # Make sure we were passed an official git project repository
+ match = re.match('^/srv/git/repositories/([A-Za-z][A-Za-z0-9]*)\.git$', git_repository_path);
+ if match == None:
+ sys.exit(1)
+ application_name = match.group(1)
+ # Ignore special repositories that aren't supposed to use the post-receive hook
+ if application_name in excluded_repos:
+ sys.exit(0)
+ # Update the deployment of this application
+ args = ['/srv/djangy/run/master_manager/setuid/run_deploy', 'application_name', application_name]
+ os.execv(args[0], args)
diff --git a/src/server/master/master_manager/purge_old_bundles.py b/src/server/master/master_manager/purge_old_bundles.py
new file mode 100644
index 0000000..be966cc
--- /dev/null
+++ b/src/server/master/master_manager/purge_old_bundles.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+#
+# Utility script to remove old, unused bundles from a master_manager host.
+#
+
+import os, shutil
+from management_database import *
+
+BUNDLES_ROOT = '/srv/bundles';
+
+def main():
+ current_bundle_names = set([x.name + '-' + x.bundle_version for x in Application.objects.filter(deleted=None)])
+ for bundle_name in os.listdir(BUNDLES_ROOT):
+ if bundle_name not in current_bundle_names:
+ print 'Removing %s ...' % bundle_name
+ shutil.rmtree(os.path.join(BUNDLES_ROOT, bundle_name))
+
+if __name__ == '__main__':
+ main()
diff --git a/src/server/master/master_manager/regenerate_ssh_authorized_keys.py b/src/server/master/master_manager/regenerate_ssh_authorized_keys.py
new file mode 100755
index 0000000..91d07ee
--- /dev/null
+++ b/src/server/master/master_manager/regenerate_ssh_authorized_keys.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+
+from shared import *
+
+def main():
+ check_trusted_uid(sys.argv[0])
+ kwargs = check_and_return_keyword_args(sys.argv, [])
+ regenerate_ssh_authorized_keys()
+
+if __name__ == '__main__':
+ main()
diff --git a/src/server/master/master_manager/remove_ssh_public_key.py b/src/server/master/master_manager/remove_ssh_public_key.py
new file mode 100755
index 0000000..104df31
--- /dev/null
+++ b/src/server/master/master_manager/remove_ssh_public_key.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python
+
+from shared import *
+
+def main():
+ check_trusted_uid(sys.argv[0])
+ kwargs = check_and_return_keyword_args(sys.argv, ['email', 'ssh_public_key_id'])
+ user = User.get_by_email(kwargs['email'])
+ user.remove_ssh_public_key(kwargs['ssh_public_key_id'])
+ regenerate_ssh_authorized_keys()
+
+if __name__ == '__main__':
+ main()
diff --git a/src/server/master/master_manager/retrieve_logs.py b/src/server/master/master_manager/retrieve_logs.py
new file mode 100755
index 0000000..ea11aa9
--- /dev/null
+++ b/src/server/master/master_manager/retrieve_logs.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+
+from ConfigParser import RawConfigParser
+from shared import *
+
+def main():
+ check_trusted_uid(sys.argv[0])
+ kwargs = check_and_return_keyword_args(sys.argv, ['application_name'])
+ retrieve_logs(**kwargs)
+
+def retrieve_logs(application_name):
+ stdout_contents_dict = call_worker_managers_retrieve_logs(application_name)
+ try:
+ print stdout_contents_dict.values()[0]
+ except:
+ pass
+
+if __name__ == '__main__':
+ main()
diff --git a/src/server/master/master_manager/setuid/.gitignore b/src/server/master/master_manager/setuid/.gitignore
new file mode 100644
index 0000000..8481f1e
--- /dev/null
+++ b/src/server/master/master_manager/setuid/.gitignore
@@ -0,0 +1,8 @@
+run_add_application
+run_add_domain_name
+run_allocate
+run_command
+run_delete_domain_name
+run_deploy
+run_retrieve_logs
+run_soft_remove_application
diff --git a/src/server/master/master_manager/setuid/Makefile b/src/server/master/master_manager/setuid/Makefile
new file mode 100644
index 0000000..63e1a95
--- /dev/null
+++ b/src/server/master/master_manager/setuid/Makefile
@@ -0,0 +1,8 @@
+TARGETS=run_add_application run_add_ssh_public_key run_allocate run_command run_configure_proxycache run_delete_application run_deploy run_regenerate_ssh_authorized_keys run_remove_ssh_public_key run_retrieve_logs run_shell_serve
+
+all: $(TARGETS)
+ -chown root.djangy $(TARGETS)
+ chmod 6710 $(TARGETS)
+
+clean:
+ rm -f $(TARGETS) *~
diff --git a/src/server/master/master_manager/setuid/config.h b/src/server/master/master_manager/setuid/config.h
new file mode 100644
index 0000000..b4c1af5
--- /dev/null
+++ b/src/server/master/master_manager/setuid/config.h
@@ -0,0 +1,6 @@
+#define ROOT_UID 0
+#define WWW_DATA_UID 33
+
+#define PATH "/srv/djangy/run/python-virtual/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+#define VIRTUAL_ENV "/srv/djangy/run/python-virtual"
+#define PROGRAM_DIR "/srv/djangy/src/server/master/master_manager/" // trailing slash is important
diff --git a/src/server/master/master_manager/setuid/run.h b/src/server/master/master_manager/setuid/run.h
new file mode 100644
index 0000000..0b890c5
--- /dev/null
+++ b/src/server/master/master_manager/setuid/run.h
@@ -0,0 +1,22 @@
+#include
+#include
+#include
+
+#include "config.h"
+
+int is_trusted_user(uid_t uid);
+
+#define MAIN(PROGRAM) \
+int main(int argc, char *argv[]) \
+{ \
+ char *envp[] = {"PATH=" PATH, \
+ "VIRTUAL_ENV=" VIRTUAL_ENV, \
+ NULL}; \
+ const char *program_name = argv[0]; \
+ if (is_trusted_user(getuid())) { \
+ setuid(0); \
+ } \
+ argv[0] = PROGRAM_DIR PROGRAM; \
+ execve(argv[0], argv, envp); \
+ perror(program_name); \
+}
diff --git a/src/server/master/master_manager/setuid/run_add_application.c b/src/server/master/master_manager/setuid/run_add_application.c
new file mode 100644
index 0000000..73a30b9
--- /dev/null
+++ b/src/server/master/master_manager/setuid/run_add_application.c
@@ -0,0 +1,11 @@
+//
+// Run deploy. Must be setuid root.
+//
+#include "run.h"
+
+MAIN("add_application.py")
+
+int is_trusted_user(uid_t uid)
+{
+ return (uid == ROOT_UID) || (uid == WWW_DATA_UID);
+}
diff --git a/src/server/master/master_manager/setuid/run_add_ssh_public_key.c b/src/server/master/master_manager/setuid/run_add_ssh_public_key.c
new file mode 100644
index 0000000..79edc2e
--- /dev/null
+++ b/src/server/master/master_manager/setuid/run_add_ssh_public_key.c
@@ -0,0 +1,11 @@
+//
+// Must be setuid root.
+//
+#include "run.h"
+
+MAIN("add_ssh_public_key.py")
+
+int is_trusted_user(uid_t uid)
+{
+ return (uid == ROOT_UID) || (uid == WWW_DATA_UID);
+}
diff --git a/src/server/master/master_manager/setuid/run_allocate.c b/src/server/master/master_manager/setuid/run_allocate.c
new file mode 100644
index 0000000..555b76c
--- /dev/null
+++ b/src/server/master/master_manager/setuid/run_allocate.c
@@ -0,0 +1,11 @@
+//
+// Run allocate. Must be setuid root.
+//
+#include "run.h"
+
+MAIN("allocate.py")
+
+int is_trusted_user(uid_t uid)
+{
+ return (uid == ROOT_UID);
+}
diff --git a/src/server/master/master_manager/setuid/run_command.c b/src/server/master/master_manager/setuid/run_command.c
new file mode 100644
index 0000000..7391181
--- /dev/null
+++ b/src/server/master/master_manager/setuid/run_command.c
@@ -0,0 +1,11 @@
+//
+// Run manage.py command. Must be setuid root.
+//
+#include "run.h"
+
+MAIN("command.py")
+
+int is_trusted_user(uid_t uid)
+{
+ return (uid == ROOT_UID) || (uid == WWW_DATA_UID);
+}
diff --git a/src/server/master/master_manager/setuid/run_configure_proxycache.c b/src/server/master/master_manager/setuid/run_configure_proxycache.c
new file mode 100644
index 0000000..b6f8cb2
--- /dev/null
+++ b/src/server/master/master_manager/setuid/run_configure_proxycache.c
@@ -0,0 +1,11 @@
+//
+// Run configure_proxycache. Must be setuid root.
+//
+#include "run.h"
+
+MAIN("configure_proxycache.py")
+
+int is_trusted_user(uid_t uid)
+{
+ return (uid == ROOT_UID);
+}
diff --git a/src/server/master/master_manager/setuid/run_delete_application.c b/src/server/master/master_manager/setuid/run_delete_application.c
new file mode 100644
index 0000000..0cd616b
--- /dev/null
+++ b/src/server/master/master_manager/setuid/run_delete_application.c
@@ -0,0 +1,11 @@
+//
+// Run delete. Must be setuid root.
+//
+#include "run.h"
+
+MAIN("delete_application.py")
+
+int is_trusted_user(uid_t uid)
+{
+ return (uid == ROOT_UID);
+}
diff --git a/src/server/master/master_manager/setuid/run_deploy.c b/src/server/master/master_manager/setuid/run_deploy.c
new file mode 100644
index 0000000..0c4897f
--- /dev/null
+++ b/src/server/master/master_manager/setuid/run_deploy.c
@@ -0,0 +1,11 @@
+//
+// Run deploy. Must be setuid root.
+//
+#include "run.h"
+
+MAIN("deploy.py")
+
+int is_trusted_user(uid_t uid)
+{
+ return (uid == ROOT_UID);
+}
diff --git a/src/server/master/master_manager/setuid/run_regenerate_ssh_authorized_keys.c b/src/server/master/master_manager/setuid/run_regenerate_ssh_authorized_keys.c
new file mode 100644
index 0000000..c24a76a
--- /dev/null
+++ b/src/server/master/master_manager/setuid/run_regenerate_ssh_authorized_keys.c
@@ -0,0 +1,11 @@
+//
+// Regenerate .ssh/authorized_keys files. Must be setuid root.
+//
+#include "run.h"
+
+MAIN("regenerate_ssh_authorized_keys.py")
+
+int is_trusted_user(uid_t uid)
+{
+ return (uid == ROOT_UID) || (uid == WWW_DATA_UID);
+}
diff --git a/src/server/master/master_manager/setuid/run_remove_ssh_public_key.c b/src/server/master/master_manager/setuid/run_remove_ssh_public_key.c
new file mode 100644
index 0000000..eaa0dd3
--- /dev/null
+++ b/src/server/master/master_manager/setuid/run_remove_ssh_public_key.c
@@ -0,0 +1,11 @@
+//
+// Must be setuid root.
+//
+#include "run.h"
+
+MAIN("remove_ssh_public_key.py")
+
+int is_trusted_user(uid_t uid)
+{
+ return (uid == ROOT_UID) || (uid == WWW_DATA_UID);
+}
diff --git a/src/server/master/master_manager/setuid/run_retrieve_logs.c b/src/server/master/master_manager/setuid/run_retrieve_logs.c
new file mode 100644
index 0000000..a86d0d0
--- /dev/null
+++ b/src/server/master/master_manager/setuid/run_retrieve_logs.c
@@ -0,0 +1,11 @@
+//
+// Run deploy. Must be setuid root.
+//
+#include "run.h"
+
+MAIN("retrieve_logs.py")
+
+int is_trusted_user(uid_t uid)
+{
+ return (uid == ROOT_UID);
+}
diff --git a/src/server/master/master_manager/setuid/run_shell_serve.c b/src/server/master/master_manager/setuid/run_shell_serve.c
new file mode 100644
index 0000000..13eacea
--- /dev/null
+++ b/src/server/master/master_manager/setuid/run_shell_serve.c
@@ -0,0 +1,26 @@
+//
+// Run deploy. Must be setuid root.
+//
+#include
+#include
+#include
+#include "config.h"
+
+int is_trusted_user(uid_t uid)
+{
+ struct passwd *pw = getpwnam("shell");
+ return (uid == ROOT_UID) || (uid == pw->pw_uid);
+}
+
+int main(int argc, char *argv[])
+{
+ const char *program_name = argv[0];
+ if (is_trusted_user(getuid())) {
+ setuid(0);
+ }
+ argv[0] = PROGRAM_DIR "shell_serve.py";
+ close(1);
+ dup2(2, 1);
+ execv(argv[0], argv);
+ perror(program_name);
+}
diff --git a/src/server/master/master_manager/shared/__init__.py b/src/server/master/master_manager/shared/__init__.py
new file mode 100644
index 0000000..f9bf97e
--- /dev/null
+++ b/src/server/master/master_manager/shared/__init__.py
@@ -0,0 +1,11 @@
+from djangy_server_shared import *
+from management_database import *
+
+from allocate_workers import *
+from call_remote import *
+from ssh_and_git import *
+
+TRUSTED_UIDS.extend(MASTER_TRUSTED_UIDS)
+
+open_log_file(os.path.join(LOGS_DIR, 'master.log'), 0600)
+
diff --git a/src/server/master/master_manager/shared/allocate_workers.py b/src/server/master/master_manager/shared/allocate_workers.py
new file mode 100644
index 0000000..477944c
--- /dev/null
+++ b/src/server/master/master_manager/shared/allocate_workers.py
@@ -0,0 +1,235 @@
+from management_database import Process, WorkerHost
+import copy, random
+from django.db.models import Sum
+from djangy_server_shared.constants import *
+from djangy_server_shared import log_info_message
+from call_remote import *
+
+def _random_worker_port():
+ return random.randrange(WORKER_PORT_LOWER, WORKER_PORT_UPPER)
+
+def _random_unique_worker_port_on_host(host):
+ port = _random_worker_port()
+ while Process.objects.filter(host=host).filter(port=port).exists():
+ port = _random_worker_port()
+ return port
+
+def allocate_workers(application):
+ # Theoretically, we should only allow one application to compute
+ # reallocation at a time, to prevent accidentally overloading workers.
+ # In practice, that seems overly conservative.
+
+ log_info_message('allocate_workers("%s", %i, %i)' % (application.name, application.num_procs, application.celery_procs))
+
+ gunicorn_updated_worker_hosts = _compute_reallocation_to_worker_hosts_update_db(application, 'gunicorn', application.num_procs)
+ celery_updated_worker_hosts = _compute_reallocation_to_worker_hosts_update_db(application, 'celery' , application.celery_procs)
+ updated_worker_hosts = list(set(gunicorn_updated_worker_hosts).union(set(celery_updated_worker_hosts)))
+
+ # Update the worker_managers whose allocations have changed
+ call_worker_managers_allocate(application.name, updated_worker_hosts)
+ # Update the proxycache_managers
+ call_proxycache_managers_configure(application.name)
+
+def _compute_reallocation_to_worker_hosts_update_db(application, proc_type, new_num_procs):
+ # Compute the allocation of workers to hosts
+ worker_hosts__num_procs = _compute_reallocation_to_worker_hosts_read_db(application, proc_type, new_num_procs)
+ # Update the Process table
+ updated_worker_hosts = []
+
+ for (worker_host, num_procs) in worker_hosts__num_procs.items():
+ try:
+ proc = Process.objects.get(application=application, proc_type=proc_type, host=worker_host)
+ if num_procs == 0:
+ proc.delete()
+ elif proc.num_procs != num_procs:
+ proc.num_procs = num_procs
+ proc.save()
+ updated_worker_hosts.append(worker_host)
+ except:
+ if num_procs != 0:
+ port = _random_unique_worker_port_on_host(worker_host)
+ proc = Process(application=application, proc_type=proc_type, host=worker_host, port=port, num_procs=num_procs)
+ proc.save()
+ updated_worker_hosts.append(worker_host)
+
+ return updated_worker_hosts
+
+# Reads the database and returns information about how processes are
+# allocated to worker hosts, structured as below.
+# Returns :: { : { 'max_procs' : int, 'total_procs': int, 'application_procs' : int } }
+def _read_worker_hosts_from_db(application, proc_type):
+ # worker_host -> max_procs
+ worker_host__max_procs = dict((row['host'], row['max_procs']) for row in WorkerHost.objects.values('host', 'max_procs').distinct())
+ # worker_host -> total num_procs
+ worker_host__total_procs = dict((row['host'], row['num_procs']) for row in Process.objects.values('host').annotate(num_procs=Sum('num_procs')))
+ # worker_host -> application's num_procs
+ worker_host__application_procs = dict((row['host'], row['num_procs']) for row in Process.objects.filter(application=application, proc_type=proc_type).values('host', 'num_procs'))
+
+ worker_hosts = { }
+ for h in worker_host__max_procs:
+ max_procs = worker_host__max_procs[h]
+ total_procs = worker_host__total_procs.get(h, 0)
+ application_procs = worker_host__application_procs.get(h, 0)
+ worker_hosts[h] = {'max_procs': max_procs, 'total_procs': total_procs, 'application_procs': application_procs}
+
+ return worker_hosts
+
+# Call this method to compute an updated allocation of an application's
+# processes to hosts. Does not touch the database or workers, simply
+# computes and returns a result.
+#
+# Tries to spread out an application's processes evenly across all available
+# worker hosts, but does not rebalance existing processes. In other words,
+# if N processes need to be added, they will be added to whichever hosts
+# have additional capacity and currently have the fewest processe for this
+# application. Similarly, if N processes need to be removed, they will be
+# removed from those hosts which have the most processes for this
+# application.
+#
+# application :: management_database.models.Application instance
+# new_num_procs :: int -- the number of processes that should be running
+# this application after reallocation (not the number of new processes)
+# Returns :: { : int }
+#
+# Note: if an entry in the return value is 0, we need to contact the
+# worker_manager to tell it to stop running this application, and when we
+# update the proxycache_manager, we should no longer list that worker.
+def _compute_reallocation_to_worker_hosts_read_db(application, proc_type, new_num_procs):
+ existing_num_procs = Process.objects.filter(application=application, proc_type=proc_type).aggregate(num_procs=Sum('num_procs'))['num_procs']
+ if not existing_num_procs:
+ existing_num_procs = 0
+ worker_hosts = _read_worker_hosts_from_db(application, proc_type)
+ # host -> application's num_procs
+ process_allocation = { }
+ # Project out the current allocations of processes to hosts
+ for h in worker_hosts:
+ application_procs = worker_hosts[h]['application_procs']
+ if application_procs > 0:
+ process_allocation[h] = application_procs
+ # More procs? Compute the added processes to hosts.
+ if new_num_procs > existing_num_procs:
+ added_num_procs = new_num_procs - existing_num_procs
+ added_procs = _compute_allocation_to_worker_hosts(added_num_procs, worker_hosts)
+ for h in added_procs:
+ process_allocation[h] = process_allocation.get(h, 0) + added_procs[h]
+ # Fewer procs? Compute the removed processes from hosts.
+ elif new_num_procs < existing_num_procs:
+ removed_num_procs = existing_num_procs - new_num_procs
+ removed_procs = _compute_deallocation_from_worker_hosts(removed_num_procs, worker_hosts)
+ for h in removed_procs:
+ if process_allocation.get(h):
+ process_allocation[h] -= removed_procs[h]
+
+ return process_allocation
+
+# Call this method to compute which hosts to allocate num_procs more
+# processes for application to. Does not touch the database or workers,
+# simply computes and returns a result. Tries to spread out an
+# application's processes evenly across all available worker hosts.
+#
+# num_procs_to_add :: int
+# worker_hosts :: { : { 'max_procs' : int, 'total_procs': int, 'application_procs' : int } }
+# Returns :: { : }
+def _compute_allocation_to_worker_hosts(num_procs_to_add, worker_hosts):
+ worker_hosts = copy.deepcopy(worker_hosts)
+
+ # Remove hosts that are maxed out
+ maxed_out_worker_hosts = []
+ for h in worker_hosts:
+ if worker_hosts[h]['total_procs'] >= worker_hosts[h]['max_procs']:
+ maxed_out_worker_hosts.append(h)
+ for h in maxed_out_worker_hosts:
+ del worker_hosts[h]
+
+ # Additional processes added to hosts :: host -> int
+ added_procs = { }
+
+ # Helper function: find the host with capacity for at least one more
+ # process, that has the fewest number of processes for this application,
+ # using total number of processes as a tie-breaker.
+ def find_min_host():
+ worker_hosts_list = list(worker_hosts)
+ # The following line will raise an exception if we're out of capacity.
+ min_host = worker_hosts_list[0]
+ min_value = worker_hosts[min_host]
+ for h in worker_hosts_list[1:]:
+ value = worker_hosts[h]
+ if (value['application_procs'] < min_value['application_procs']) \
+ or (value['application_procs'] == min_value['application_procs'] \
+ and value['total_procs'] < min_value['total_procs']):
+ min_host = h
+ min_value = value
+ return min_host
+
+ # Helper function: update state, adding one process to worker_host
+ def add_to_host(worker_host):
+ added_procs[worker_host] = added_procs.get(worker_host, 0) + 1
+ value = worker_hosts[worker_host]
+ value['total_procs'] += 1
+ value['application_procs'] += 1
+ # Remove host if maxed out
+ if value['total_procs'] >= value['max_procs']:
+ del worker_hosts[worker_host]
+
+ for i in range(0, num_procs_to_add):
+ h = find_min_host()
+ add_to_host(h)
+
+ return added_procs
+
+# Call this method to deallocate up to num_procs worker processes for
+# application. If application has fewer than num_procs worker processes,
+# that's ok, we'll just deallocate as many as we can. Does not touch the
+# database or workers, simply computes and returns a result. Tries to leave
+# the remaining processes evenly distributed across worker hosts.
+#
+# num_procs_to_remove :: int
+# worker_hosts :: { : { 'max_procs' : int, 'total_procs': int, 'application_procs' : int } }
+# Returns :: { : }
+def _compute_deallocation_from_worker_hosts(num_procs_to_remove, worker_hosts):
+ worker_hosts = copy.deepcopy(worker_hosts)
+
+ # Remove hosts that don't contain application processes
+ unused_worker_hosts = []
+ for h in worker_hosts:
+ if worker_hosts[h]['application_procs'] <= 0:
+ unused_worker_hosts.append(h)
+ for h in unused_worker_hosts:
+ del worker_hosts[h]
+
+ # Processes removed from hosts :: host -> int
+ removed_procs = { }
+
+ # Helper function: find the host with the most processes from this
+ # application, using total number of processes as a tie-breaker.
+ def find_max_host():
+ worker_hosts_list = list(worker_hosts)
+ if worker_hosts_list == []:
+ return None
+ max_host = worker_hosts_list[0]
+ max_value = worker_hosts[max_host]
+ for h in worker_hosts_list[1:]:
+ value = worker_hosts[h]
+ if (value['application_procs'] > max_value['application_procs']) \
+ or (value['application_procs'] == max_value['application_procs'] \
+ and value['total_procs'] > max_value['total_procs']):
+ max_host = h
+ max_value = value
+ return max_host
+
+ # Helper function: update state, removing one process from worker_host
+ def remove_from_host(worker_host):
+ removed_procs[worker_host] = removed_procs.get(worker_host, 0) + 1
+ value = worker_hosts[worker_host]
+ value['total_procs'] -= 1
+ value['application_procs'] -= 1
+ # Remove host if it contains no more application processes
+ if value['application_procs'] <= 0:
+ del worker_hosts[worker_host]
+
+ for i in range(0, num_procs_to_remove):
+ h = find_max_host()
+ if h:
+ remove_from_host(h)
+
+ return removed_procs
diff --git a/src/server/master/master_manager/shared/call_remote.py b/src/server/master/master_manager/shared/call_remote.py
new file mode 100644
index 0000000..9733c24
--- /dev/null
+++ b/src/server/master/master_manager/shared/call_remote.py
@@ -0,0 +1,135 @@
+import os.path, sys
+from djangy_server_shared import *
+from management_database import *
+
+def _call_remote(hosts, make_command):
+ # Run commands in parallel on all designated hosts
+ # Note: command arguments will be parsed by shell, and must not contain spaces
+ num_success = 0
+ num_failure = 0
+ stdout_contents_dict = { }
+ programs = []
+ for h in hosts:
+ p = ExternalProgram(['ssh', h] + make_command(h))
+ p.host = h
+ programs.append(p)
+ sys.stdout.flush()
+ for p in programs:
+ if p:
+ p.start()
+ for p in programs:
+ if p:
+ result = p.finish()
+ if external_program_encountered_error(result):
+ num_failure = num_failure + 1
+ else:
+ num_success = num_success + 1
+ stdout_contents_dict[p.host] = result['stdout_contents']
+ else:
+ num_failure = num_failure + 1
+
+ return (num_success, num_failure, stdout_contents_dict)
+
+def call_worker_managers_retrieve_logs(application_name, hosts=None):
+ def make_retrieve_command(application_info, host):
+ command = [os.path.join(WORKER_SETUID_DIR, 'run_retrieve_logs'),
+ 'application_name', application_info.name,
+ 'bundle_version', application_info.bundle_version
+ ]
+ return command
+
+ (num_success, num_failure, stdout_contents_dict) = _call_worker_managers(application_name, make_retrieve_command, hosts)
+ return stdout_contents_dict
+
+def call_worker_managers_allocate(application_name, hosts=None):
+ def make_allocate_command(application_info, host):
+ try:
+ p = Process.objects.get(application=application_info, proc_type='gunicorn', host=host)
+ num_procs = p.num_procs
+ port = p.port
+ except:
+ num_procs = 0
+ port = 0
+ try:
+ p = Process.objects.get(application=application_info, proc_type='celery', host=host)
+ celery_procs = p.num_procs
+ except:
+ celery_procs = 0
+ virtualhosts = VirtualHost.get_virtualhosts_by_application_name(application_name)
+ http_virtual_hosts = ','.join(virtualhosts)
+ command = [os.path.join(WORKER_SETUID_DIR, 'run_deploy'), \
+ 'application_name', application_info.name, \
+ 'bundle_version', application_info.bundle_version, \
+ 'num_procs', str(num_procs), \
+ 'proc_num_threads', str(application_info.proc_num_threads), \
+ 'proc_mem_mb', str(application_info.proc_mem_mb), \
+ 'proc_stack_mb', str(application_info.proc_stack_mb), \
+ 'debug', str(application_info.debug), \
+ 'http_virtual_hosts', http_virtual_hosts, \
+ 'host', host, \
+ 'port', str(port),
+ 'celery_procs', str(celery_procs)]
+ return command
+
+ _call_worker_managers(application_name, make_allocate_command, hosts)
+
+def call_worker_managers_delete_application(application_name, hosts=None):
+ def make_delete_application_command(application_info, host):
+ command = [os.path.join(WORKER_SETUID_DIR, 'run_delete_application'), \
+ 'application_name', application_info.name]
+ return command
+
+ _call_worker_managers(application_name, make_delete_application_command, hosts)
+
+def _call_worker_managers(application_name, make_command, hosts=None):
+ # Load global application info
+ application = Application.get_by_name(application_name)
+ bundle_version = application.bundle_version
+ if bundle_version == None or bundle_version == '':
+ return
+
+ def make_command2(host):
+ return make_command(application, host)
+
+ # Load relevant hosts from database if none specified
+ if hosts == None:
+ hosts = [h for (h, p) in Process.get_hosts_ports_by_application(application)]
+
+ (num_success, num_failure, stdout_contents_dict) = _call_remote(hosts, make_command2)
+ if num_failure > 0:
+ print ('%i success, %i failure' % (num_success, num_failure)),
+
+ return (num_success, num_failure, stdout_contents_dict)
+
+def call_proxycache_managers_configure(application_name):
+ application = Application.get_by_name(application_name)
+ # Hosts running proxycache serving this application
+ proxycache_hosts = ProxyCache.get_proxycache_hosts_by_application(application)
+ # Virtual hosts used by this application
+ virtualhosts = VirtualHost.get_virtualhosts_by_application(application)
+ # Real hosts and port numbers running instances of this application
+ worker_hosts_ports = Process.get_hosts_ports_by_application(application)
+
+ http_virtual_hosts = ','.join(virtualhosts)
+ worker_servers = ','.join(['%s:%s' % (h, p) for (h, p) in worker_hosts_ports])
+
+ command = [os.path.join(PROXYCACHE_SETUID_DIR, 'run_configure'), \
+ 'application_name', application_name, \
+ 'http_virtual_hosts', http_virtual_hosts, \
+ 'worker_servers', worker_servers, \
+ 'cache_index_size_kb', str(application.cache_index_size_kb), \
+ 'cache_size_kb', str(application.cache_size_kb)]
+
+ (num_success, num_failure, stdout_contents_dict) = _call_remote(proxycache_hosts, lambda h: command)
+ if num_failure > 0:
+ print ('%i success, %i failure' % (num_success, num_failure)),
+
+def call_proxycache_managers_delete_application(application_name):
+ # Hosts running proxycache serving this application
+ proxycache_hosts = ProxyCache.get_proxycache_hosts_by_application_name(application_name)
+
+ command = [os.path.join(PROXYCACHE_SETUID_DIR, 'run_delete_application'), 'application_name', application_name]
+
+ (num_success, num_failure, stdout_contents_dict) = _call_remote(proxycache_hosts, lambda h: command)
+ if num_failure > 0:
+ print ('%i success, %i failure' % (num_success, num_failure)),
diff --git a/src/server/master/master_manager/shared/ssh_and_git.py b/src/server/master/master_manager/shared/ssh_and_git.py
new file mode 100644
index 0000000..efde788
--- /dev/null
+++ b/src/server/master/master_manager/shared/ssh_and_git.py
@@ -0,0 +1,75 @@
+from djangy_server_shared import *
+from management_database import *
+import os, os.path, re
+
+def add_ssh_public_key(user, pubkey):
+ """ Add a user's SSH public key to mangement_database and git access. """
+ (ssh_public_key, comment) = parse_ssh_public_key(pubkey)
+ # Update the management_database
+ user.add_ssh_public_key(ssh_public_key, comment)
+ # Update ~git/.ssh/authorized_keys
+ regenerate_ssh_authorized_keys()
+
+def parse_ssh_public_key(pubkey):
+ """ Parse an SSH public key into the key proper and the optional
+ comment. Throws an exception when given a malformed key. """
+ pubkey2 = pubkey.replace('\r', '')
+ matches = re.match('^(?P(?:ssh-dss|ssh-rsa)\s+[A-Za-z0-9/+]+=*)\s+(?P.*)$', pubkey2, re.DOTALL)
+ if matches:
+ return (matches.group('ssh_public_key'), matches.group('comment'))
+ key_data_matches = re.match('\s*---- BEGIN SSH2 PUBLIC KEY ----\s*\n(?:[^:\n]*:[^\n]*\n)*(?P[A-Za-z0-9/+\s\n]+=*)\s*\n\s*---- END SSH2 PUBLIC KEY ----\s*', pubkey2, re.DOTALL)
+ key_type_matches = re.match('.*?\s*Comment\s*:\s*(?P(?:(?P[Rr][Ss][Aa])|(?P[Dd][Ss][Aa])|(?P[Dd][Ss][Ss])|[^\n])*)\s*\n.*?', pubkey2, re.DOTALL)
+ if key_data_matches:
+ if key_type_matches.group('rsa'):
+ key_type = 'rsa'
+ elif key_type_matches.group('dsa'):
+ key_type = 'dss'
+ elif key_type_matches.group('dss'):
+ key_type = 'dss'
+ else:
+ key_type = 'rsa'
+ key_data = key_data_matches.group('key_data').replace('\n', '')
+ ssh_public_key = 'ssh-%s %s' % (key_type, key_data)
+ if key_type_matches.group('comment'):
+ return (ssh_public_key, key_type_matches.group('comment'))
+ else:
+ return (ssh_public_key, '')
+ return ('', 'Invalid SSH public key: %s' % pubkey)
+
+def create_git_repository(application_name):
+ """ Create a new git repository for a given application. Performs no validation. """
+ # Location of the repository: /srv/git/repositories/.git
+ repo_path = os.path.join(REPOS_DIR, application_name + '.git')
+ # We will run "git init" as the git user/group
+ def become_git_user():
+ set_uid_gid(GIT_UID, GIT_GID)
+ # Run "git init"
+ run_external_program(['git', 'init', '--bare', repo_path], cwd='/', preexec_fn=become_git_user)
+
+def regenerate_ssh_authorized_keys():
+ """ Regenerate /srv/git/.ssh/authorized_keys and /srv/shell/.ssh/authorized_keys from the management_database. """
+ # Programs that will be run when the user connects via ssh
+ git_serve_path = GIT_SERVE_PATH
+ shell_serve_path = SHELL_SERVE_PATH
+ # Generate authorized_keys
+ git_authorized_keys = generate_ssh_authorized_keys_contents(git_serve_path)
+ shell_authorized_keys = generate_ssh_authorized_keys_contents(shell_serve_path)
+ # Write out authorized_keys
+ write_to_file(os.path.join(GIT_SSH_DIR, 'authorized_keys'), git_authorized_keys, GIT_UID, GIT_GID, AUTHORIZED_KEYS_MODE)
+ write_to_file(os.path.join(SHELL_SSH_DIR, 'authorized_keys'), shell_authorized_keys, SHELL_UID, SHELL_GID, AUTHORIZED_KEYS_MODE)
+
+def generate_ssh_authorized_keys_contents(command_path):
+ # Options to lock down ssh access
+ options = 'no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty'
+ # Create lines in authorized_keys for each ssh public key in the datbasee
+ keys = filter(lambda y: y.ssh_public_key.strip() != '', SshPublicKey.objects.all())
+ lines = ['command="%s %i",%s %s' % (command_path, x.id, options, x.ssh_public_key) for x in keys]
+ authorized_keys_contents = '\n'.join(lines) + '\n'
+ return authorized_keys_contents
+
+def write_to_file(path, contents, uid, gid, mode):
+ f = open(path, 'w')
+ f.write(contents)
+ f.close()
+ os.chown(path, uid, gid)
+ os.chmod(path, mode)
diff --git a/src/server/master/master_manager/shell_serve.py b/src/server/master/master_manager/shell_serve.py
new file mode 100755
index 0000000..eb158df
--- /dev/null
+++ b/src/server/master/master_manager/shell_serve.py
@@ -0,0 +1,55 @@
+#!/srv/djangy/run/python-virtual/bin/python
+#
+# Note: doesn't import shared.ssh_and_git because this runs as the "shell"
+# user, which doesn't have access to write to /srv/logs/master.log...
+#
+
+from djangy_server_shared import become_application_setup_uid_gid, constants, find_django_project
+from management_database import *
+import os, re, subprocess, sys
+
+def main():
+ try:
+ shell_serve(int(sys.argv[1]))
+ except:
+ sys.stderr.write('Access denied. Please email support@djangy.com for help.\n')
+
+def shell_serve(ssh_public_key_id):
+ """ Serve an incoming manage.py request. Should only be called via ~shell/.ssh/authorized_keys """
+ # Get all users who have the specified public key
+ users = SshPublicKey.get_users_by_public_key_id(ssh_public_key_id)
+ # SSH_ORIGINAL_COMMAND format:
+ # manage.py [args...]
+ ssh_original_command = os.environ['SSH_ORIGINAL_COMMAND']
+ matches = re.match('^\s*(?P[A-Za-z0-9]+)\s+manage\.py\s+(?P.*?)\s*$', ssh_original_command)
+ assert matches
+ application_name = matches.group('application_name')
+ args = matches.group('args')
+ # blocked commands
+ if args.split()[0] in constants.BLOCKED_COMMANDS:
+ sys.stderr.write('For security reasons, that command has been disallowed. Contact support@djangy.com for help.\n')
+ return None
+ # Look up the requested application, and make sure that at least one
+ # user associated with the SSH key that was used has access to it.
+ application = Application.get_by_name(application_name)
+ if application.accessible_by_any_of(users):
+ # Look up bundle information
+ bundle_version = application.bundle_version
+ bundle_name = '%s-%s' % (application_name, bundle_version)
+ bundle_path = os.path.join(constants.BUNDLES_DIR, bundle_name)
+ setup_uid = application.setup_uid
+ app_gid = application.app_gid
+ bin_path = os.path.join(bundle_path, 'python-virtual/bin')
+ python_path = os.path.join(bin_path, 'python')
+ # It might be preferable to read the bundle configuration instead, to be consistent...
+ django_project_path = find_django_project(os.path.join(bundle_path, 'application'))
+ # Get around buffered stdout
+ os.dup2(2, 1)
+ # Run the command:
+ os.chdir(django_project_path)
+ become_application_setup_uid_gid('shell_serve', setup_uid, app_gid)
+ command = '%s -u manage.py %s' % (python_path, args)
+ os.execve('/bin/bash', ['bash', '-c', command], {'PATH':'/bin:/usr/bin:%s' % bin_path})
+
+if __name__ == '__main__':
+ main()
diff --git a/src/server/master/master_manager/uid_application_setup/__init__.py b/src/server/master/master_manager/uid_application_setup/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/server/master/master_manager/uid_application_setup/create_virtualenv.py b/src/server/master/master_manager/uid_application_setup/create_virtualenv.py
new file mode 100644
index 0000000..f8beef8
--- /dev/null
+++ b/src/server/master/master_manager/uid_application_setup/create_virtualenv.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python
+#
+# Runs virtualenv create commands as an application setup UID. This program
+# is called as root, and then sets its own UID. This allows us to protect
+# the create_virtualenv.py script from end-user code.
+#
+
+from djangy_server_shared import *
+
+def main():
+ kwargs = check_and_return_keyword_args(sys.argv, ['application_name', 'bundle_name', 'setup_uid', 'app_gid'])
+ become_application_setup_uid_gid(sys.argv[0], int(kwargs['setup_uid']), int(kwargs['app_gid']))
+ os.umask(0027)
+ os.environ['PATH'] = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
+ del os.environ['VIRTUAL_ENV']
+ create_virtualenv(**kwargs)
+
+def create_virtualenv(application_name, bundle_name, setup_uid, app_gid):
+ if os.getuid() == 0 or os.getuid() != int(setup_uid):
+ print 'ERROR: setup_bundle must be run as setup_uid'
+ sys.exit(2)
+
+ bundle_path = os.path.join(BUNDLES_DIR, bundle_name)
+
+ # Create virtualenv in /python-virtual
+ print 'Installing dependencies...\n',
+ virtualenv_path = os.path.join(bundle_path, 'python-virtual')
+ generate_virtual_environment(bundle_path, virtualenv_path)
+ print 'Done.'
+ print ''
+
+def generate_virtual_environment(bundle_path, virtualenv_path):
+ # Create the virtualenv
+ sys.stdout.flush()
+ run_external_program(['virtualenv', virtualenv_path])
+ # Install eggs using easy_install
+ easy_install_eggs(bundle_path, virtualenv_path)
+ # Install other required python packages using pip
+ pip_install_requirements(bundle_path, virtualenv_path)
+
+def easy_install_eggs(bundle_path, virtualenv_path):
+ print ' Dependencies from djangy.eggs using easy_install:'
+ # read the djangy.eggs file (if it exists) and install all the packages mentioned
+ deps_path = os.path.join(bundle_path, 'config', 'djangy.eggs')
+ deps = []
+ if os.path.exists(deps_path):
+ deps = [d.strip('\n') for d in open(deps_path, 'r').readlines()]
+ elif not os.path.exists(os.path.join(bundle_path, 'config', 'djangy.pip')):
+ deps = ['Django', 'South']
+ if 'gunicorn' not in deps:
+ deps += ['gunicorn']
+ easy_install = os.path.join(virtualenv_path, 'bin', 'easy_install')
+ install_deps([easy_install, '-Z'], deps)
+
+def pip_install_requirements(bundle_path, virtualenv_path):
+ print ' Dependencies from djangy.pip using pip:'
+ # read the djangy.pip file (if it exists) and install all the packages mentioned
+ deps_path = os.path.join(bundle_path, 'config', 'djangy.pip')
+ if os.path.exists(deps_path):
+ deps = [d.strip('\n') for d in open(deps_path, 'r').readlines()]
+ else:
+ deps = []
+ pip_path = os.path.join(virtualenv_path, 'bin', 'pip')
+ install_deps([pip_path, 'install'], deps)
+
+def install_deps(install_command, deps):
+ sys.stdout.flush()
+ num_deps = 0
+ for dep in deps:
+ # Get the raw dependency, no comment.
+ dep = dep.strip()
+ # Install the dependency, but skip blank or comment lines
+ if dep != '' and dep[0] != '#':
+ num_deps = num_deps + 1
+ print ' Installing %s...' % dep,
+ sys.stdout.flush()
+ result = run_external_program(install_command + dep.split())
+ if result['exit_code'] == 0:
+ print 'Success.'
+ else:
+ print 'FAILED!'
+ if num_deps == 0:
+ print ' None found.'
+ sys.stdout.flush()
+
+if __name__ == '__main__':
+ main()
diff --git a/src/server/master/master_manager/uid_application_setup/get_admin_media_prefix.py b/src/server/master/master_manager/uid_application_setup/get_admin_media_prefix.py
new file mode 100644
index 0000000..797d52a
--- /dev/null
+++ b/src/server/master/master_manager/uid_application_setup/get_admin_media_prefix.py
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+#
+# Placeholder -- should import settings.py from the django project and print
+# out ADMIN_MEDIA_PREFIX. Needs to run as setup_uid and in the application's
+# virtual environment.
+#
+
+from djangy_server_shared import *
+
+def main():
+ kwargs = check_and_return_keyword_args(sys.argv, ['setup_uid', 'app_gid', 'virtual_env_path', 'django_project_path'])
+ os.chdir('/')
+ become_application_setup_uid_gid(sys.argv[0], int(kwargs['setup_uid']), int(kwargs['app_gid']))
+ os.chdir(kwargs['django_project_path'])
diff --git a/src/server/master/master_manager/uid_git/__init__.py b/src/server/master/master_manager/uid_git/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/server/master/master_manager/uid_git/clone_repo.py b/src/server/master/master_manager/uid_git/clone_repo.py
new file mode 100644
index 0000000..5c3e18a
--- /dev/null
+++ b/src/server/master/master_manager/uid_git/clone_repo.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+#
+# Should be run as the git user/group.
+#
+
+from djangy_server_shared import *
+
+def main():
+ program_name = sys.argv[0]
+ become_uid_gid(program_name, GIT_UID, GIT_GID)
+ args = sys.argv[1:]
+ if len(args) != 2:
+ print_or_log_usage('Usage: %s \n' % program_name)
+ sys.exit(1)
+ clone_repo(*args)
+
+def clone_repo(master_repo_path, temp_repo_path):
+ # git clone
+ run_external_program(['git', 'clone', master_repo_path, temp_repo_path], cwd=temp_repo_path)
+ if not os.path.exists(temp_repo_path):
+ log_error_message('git clone failed')
+ sys.exit(3)
+ # read current version of git repository
+ result = run_external_program(['git', 'show-ref', '--heads', '-s'], cwd=temp_repo_path)
+ stdout = result['stdout_contents'].split('\n')
+ if len(stdout) < 1:
+ git_repo_version = ''
+ else:
+ git_repo_version = stdout[0]
+ if not validate_git_repo_version(git_repo_version):
+ log_error_message('git returned invalid application version (%s)' % git_repo_version)
+ sys.exit(4)
+ # output current version of git repository
+ print git_repo_version
+ sys.exit(0)
+
+def validate_git_repo_version(git_repo_version):
+ return (None != re.match('^[0-9a-f]{40}$', git_repo_version))
+
+if __name__ == '__main__':
+ main()
diff --git a/src/server/master/web_api/application/web_api/__init__.py b/src/server/master/web_api/application/web_api/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/src/server/master/web_api/application/web_api/api/Router.py b/src/server/master/web_api/application/web_api/api/Router.py
new file mode 100644
index 0000000..d4ca9ab
--- /dev/null
+++ b/src/server/master/web_api/application/web_api/api/Router.py
@@ -0,0 +1,23 @@
+class Router(object):
+ """ Router to tell the application when to use the management_database and when to use the 'default' database.
+ see http://docs.djangoproject.com/en/1.2/topics/db/multi-db/
+ """
+
+ def check_for_md(self, model, **hints):
+ if model._meta.app_label == 'management_database':
+ return 'management_database'
+ return None
+
+ db_for_read = check_for_md
+ db_for_write = check_for_md
+
+ def allow_relation(self, obj1, obj2, **hints):
+ if (obj1._meta.app_label == obj1._meta.app_label):
+ return True
+ return False
+
+ def allow_syncdb(self, db, model):
+ """ Keep the management database from being synchronized here."""
+ if model._meta.app_label == 'management_database':
+ return False
+ return None
diff --git a/src/server/master/web_api/application/web_api/api/__init__.py b/src/server/master/web_api/application/web_api/api/__init__.py
new file mode 100755
index 0000000..a62fbc7
--- /dev/null
+++ b/src/server/master/web_api/application/web_api/api/__init__.py
@@ -0,0 +1 @@
+from Router import *
diff --git a/src/server/master/web_api/application/web_api/api/models.py b/src/server/master/web_api/application/web_api/api/models.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/server/master/web_api/application/web_api/api/tests.py b/src/server/master/web_api/application/web_api/api/tests.py
new file mode 100755
index 0000000..2247054
--- /dev/null
+++ b/src/server/master/web_api/application/web_api/api/tests.py
@@ -0,0 +1,23 @@
+"""
+This file demonstrates two different styles of tests (one doctest and one
+unittest). These will both pass when you run "manage.py test".
+
+Replace these with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+class SimpleTest(TestCase):
+ def test_basic_addition(self):
+ """
+ Tests that 1 + 1 always equals 2.
+ """
+ self.failUnlessEqual(1 + 1, 2)
+
+__test__ = {"doctest": """
+Another way to test that 1 + 1 is equal to 2.
+
+>>> 1 + 1 == 2
+True
+"""}
+
diff --git a/src/server/master/web_api/application/web_api/api/views.py b/src/server/master/web_api/application/web_api/api/views.py
new file mode 100755
index 0000000..3d97993
--- /dev/null
+++ b/src/server/master/web_api/application/web_api/api/views.py
@@ -0,0 +1,137 @@
+from django.conf import settings
+from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, \
+ HttpResponseNotFound, HttpResponseNotAllowed, HttpResponseServerError
+from master_api import name_available, add_application, remove_application, retrieve_logs, command
+from management_database import *
+import logging, json
+
+def _presence_of(arg, msg):
+ """ Decorator that returns the specified message if the specified POST variable isn't present."""
+ def presence(func):
+ def verify(*args, **kwargs):
+ if not args[0].POST.get(arg, None):
+ return HttpResponseBadRequest(msg)
+ return func(*args, **kwargs)
+ return verify
+ return presence
+
+def _auth_required(func):
+ """ Decorator for API function calls that ensures the presence of email, hashed_password, pubkey, and application_name. """
+ def auth(*args, **kwargs):
+ if args[0].method.lower() != 'post':
+ return HttpResponseNotAllowed(['POST'])
+
+ email = args[0].POST.get('email', None)
+ if email is None:
+ return HttpResponseBadRequest('No email provided.')
+
+ hashed_password = args[0].POST.get('hashed_password', None)
+ if hashed_password is None:
+ return HttpResponseBadRequest('No password provided.')
+
+ user = User.get_by_email(email)
+ if user is None:
+ return HttpResponseForbidden('Please create an account on Djangy.com first.')
+
+ if user.passwd != hashed_password:
+ return HttpResponseForbidden('Invalid password.')
+
+ return func(*args, **kwargs)
+ return auth
+
+def _check_application_access(func):
+ """ Decorator for checking that the user has access to the selected application. Use after _auth_required. """
+ def check_application_access(request):
+ email = request.REQUEST.get('email')
+ application_name = request.REQUEST.get('application_name')
+ user = User.get_by_email(email)
+ application = Application.get_by_name(application_name)
+ if application and application.accessible_by(user):
+ return func(request, email, application_name)
+ else:
+ return HttpResponseBadRequest('Access denied for user "%s" to application "%s".' % (email, application_name))
+ return check_application_access
+
+@_auth_required
+def index(request):
+ return HttpResponse('')
+
+@_presence_of('pubkey', 'No public key provided.')
+@_presence_of('application_name', 'No application name provided.')
+@_auth_required
+def create(request):
+ """ create command, called from the djangy command line client."""
+ email = request.POST.get('email')
+ application_name = request.POST.get('application_name')
+
+ # check for that application name
+ if not name_available(application_name):
+ return HttpResponseBadRequest('Error: an application named "%s" already exists.' % application_name)
+
+ # create the application
+ try:
+ pubkey = request.POST.get('pubkey')
+ add_application(application_name, email, pubkey)
+ except Exception, e:
+ return HttpResponseServerError('Exception while adding application: %s' % e)
+
+ logging.info('Application created: %s.' % application_name)
+
+ return HttpResponse('Application created.')
+
+@_presence_of('application_name', 'No application name provided.')
+@_auth_required
+@_check_application_access
+def delete(request, email, application_name):
+ """ Remove a project. Called from the djangy.py command line client. """
+ status = remove_application(application_name)
+ if not status:
+ return HttpResponseServerError('Error: %s.' % status)
+
+ return HttpResponse('Your application, %s, has been deleted.' % application_name)
+
+@_presence_of('application_name', 'No application name provided.')
+@_auth_required
+@_check_application_access
+def logs(request, email, application_name):
+ """ Return the last 100 lines of the django.log file for this application."""
+ try:
+ return HttpResponse(retrieve_logs(application_name))
+ except Exception, e:
+ return HttpResponseServerError('Error: %s.' % e)
+
+@_presence_of('application_name', 'No application name provided.')
+@_auth_required
+@_check_application_access
+def syncdb(request, email, application_name):
+ """ Run the syncdb command. """
+ try:
+ return HttpResponse(command(application_name, 'syncdb', '--noinput'))
+ except Exception, e:
+ return HttpResponseServerError('Error: %s.' % e)
+
+@_presence_of('application_name', 'No application name provided.')
+@_auth_required
+@_check_application_access
+def migrate(request, email, application_name):
+ """ Run the migrate command. """
+ raw_args = request.POST.get('args', '')
+ logging.info('[MIGRATE] got args: %s' % raw_args)
+ try:
+ args = json.loads(raw_args)
+ except:
+ args = []
+ try:
+ return HttpResponse(command(application_name, 'migrate', *args))
+ except Exception, e:
+ return HttpResponseServerError('Error: %s.' % e)
+
+@_presence_of('application_name', 'No application name provided.')
+@_auth_required
+@_check_application_access
+def createsuperuser(request, email, application_name):
+ """ Run the createsuperuser command. """
+ try:
+ return HttpResponse(command(application_name, 'createsuperuser'))
+ except Exception, e:
+ return HttpResponseServerError('Error: %s.' % e)
diff --git a/src/server/master/web_api/application/web_api/manage.py b/src/server/master/web_api/application/web_api/manage.py
new file mode 100755
index 0000000..6ce754c
--- /dev/null
+++ b/src/server/master/web_api/application/web_api/manage.py
@@ -0,0 +1,12 @@
+#!/usr/bin/env python
+
+from django.core.management import execute_manager
+try:
+ import settings # Assumed to be in the same directory.
+except ImportError:
+ import sys
+ sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+ sys.exit(1)
+
+if __name__ == "__main__":
+ execute_manager(settings)
diff --git a/src/server/master/web_api/application/web_api/settings.py b/src/server/master/web_api/application/web_api/settings.py
new file mode 100644
index 0000000..9960162
--- /dev/null
+++ b/src/server/master/web_api/application/web_api/settings.py
@@ -0,0 +1,121 @@
+# Django settings for web_api project.
+import djangy_server_shared, os.path
+
+DEBUG = False
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+ ('Bob Jones', 'bob@jones.mil')
+)
+
+MANAGERS = ADMINS
+
+DATABASES = {
+ 'default': {
+ 'ENGINE':'mysql',
+ 'NAME':'web_api',
+ 'USER':'web_api',
+ 'PASSWORD':'password goes here',
+ 'HOST':'',
+ 'PORT':'',
+ },
+ 'management_database': {
+ 'ENGINE': 'mysql', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
+ 'NAME': 'djangy', # Or path to database file if using sqlite3.
+ 'USER': 'djangy', # Not used with sqlite3.
+ 'PASSWORD': 'password goes here', # Not used with sqlite3.
+ 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
+ 'PORT': '', # Set to empty string for default. Not used with sqlite3.
+ }
+}
+
+DATABASE_ROUTERS = ['api.Router']
+# Local time zone for this installation. Choices can be found here:
+# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
+# although not all choices may be available on all operating systems.
+# On Unix systems, a value of None will cause Django to use the same
+# timezone as the operating system.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = 'America/Chicago'
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'en-us'
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# If you set this to False, Django will not format dates, numbers and
+# calendars according to the current locale
+USE_L10N = True
+
+# Absolute path to the directory that holds media.
+# Example: "/home/media/media.lawrence.com/"
+MEDIA_ROOT = ''
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash if there is a path component (optional in other cases).
+# Examples: "http://media.lawrence.com", "http://example.com/media/"
+MEDIA_URL = ''
+
+# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
+# trailing slash.
+# Examples: "http://foo.com/media/", "/media/".
+ADMIN_MEDIA_PREFIX = '/media/'
+
+# Make this unique, and don't share it with anybody.
+#SECRET_KEY =
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+ 'django.template.loaders.filesystem.Loader',
+ 'django.template.loaders.app_directories.Loader',
+# 'django.template.loaders.eggs.Loader',
+)
+
+MIDDLEWARE_CLASSES = (
+ 'django.middleware.common.CommonMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ #'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+)
+
+ROOT_URLCONF = 'web_api.urls'
+
+TEMPLATE_DIRS = (
+ # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
+ # Always use forward slashes, even on Windows.
+ # Don't forget to use absolute paths, not relative paths.
+)
+
+INSTALLED_APPS = (
+ #'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ 'django.contrib.messages',
+ 'management_database',
+ 'web_api.api',
+ 'sentry.client',
+ # Uncomment the next line to enable the admin:
+ # 'django.contrib.admin',
+)
+SENTRY_KEY = 'password goes here'
+SENTRY_REMOTE_URL = 'django logsentry remote URL goes here'
+
+import logging
+
+LOG_FILENAME = os.path.join(djangy_server_shared.LOGS_DIR, 'api.djangy.com/django.log')
+logging.basicConfig(filename=LOG_FILENAME, level=logging.DEBUG)
+from sentry.client.handlers import SentryHandler
+
+logging.getLogger().addHandler(SentryHandler())
+
+# Add StreamHandler to sentry's default so you can catch missed exceptions
+logging.getLogger('sentry').addHandler(logging.StreamHandler())
+
diff --git a/src/server/master/web_api/application/web_api/static/foo.txt b/src/server/master/web_api/application/web_api/static/foo.txt
new file mode 100644
index 0000000..e69de29
diff --git a/src/server/master/web_api/application/web_api/urls.py b/src/server/master/web_api/application/web_api/urls.py
new file mode 100755
index 0000000..f3c0433
--- /dev/null
+++ b/src/server/master/web_api/application/web_api/urls.py
@@ -0,0 +1,15 @@
+from django.conf.urls.defaults import *
+
+# Uncomment the next two lines to enable the admin:
+# from django.contrib import admin
+# admin.autodiscover()
+
+urlpatterns = patterns('',
+ (r'^$', 'web_api.api.views.index'),
+ (r'^create$', 'web_api.api.views.create'),
+ (r'^delete$', 'web_api.api.views.delete'),
+ (r'^logs$', 'web_api.api.views.logs'),
+ (r'^syncdb$', 'web_api.api.views.syncdb'),
+ (r'^migrate$', 'web_api.api.views.migrate'),
+ (r'^createsuperuser$', 'web_api.api.views.createsuperuser'),
+)
diff --git a/src/server/master/web_api/config/apache.conf b/src/server/master/web_api/config/apache.conf
new file mode 100644
index 0000000..baea7ce
--- /dev/null
+++ b/src/server/master/web_api/config/apache.conf
@@ -0,0 +1,24 @@
+
+ ServerName api.djangy.com
+ ServerAdmin support@djangy.com
+
+ DocumentRoot /srv/djangy/src/server/master/web_api/application/web_api/static
+
+ WSGIScriptAlias / /srv/djangy/src/server/master/web_api/config/production.wsgi
+ WSGIDaemonProcess web_api display-name=web_api
+
+ Order allow,deny
+ Allow from all
+
+
+ Alias /robots.txt /srv/djangy/src/server/master/web_api/application/web_api/static/robots.txt
+ Alias /favicon.ico /srv/djangy/src/server/master/web_api/application/web_api/static/favicon.ico
+ Alias /static /srv/djangy/src/server/master/web_api/application/web_api/static
+
+ ErrorLog /srv/logs/api.djangy.com/error.log
+ CustomLog /srv/logs/api.djangy.com/access.log combined
+
+ SSLEngine on
+ SSLCertificateFile /srv/djangy/install/conf/ssl_keys/djangy.com.crt
+ SSLCertificateKeyFile /srv/djangy/install/conf/ssl_keys/djangy.com.key
+
diff --git a/src/server/master/web_api/config/production.wsgi b/src/server/master/web_api/config/production.wsgi
new file mode 100644
index 0000000..a9c76d0
--- /dev/null
+++ b/src/server/master/web_api/config/production.wsgi
@@ -0,0 +1,12 @@
+import site
+site.addsitedir("/srv/djangy/run/python-virtual/lib/python2.6/site-packages")
+
+import os, sys
+
+sys.path.append('/srv/djangy/src/server/master/web_api/application/web_api')
+sys.path.append('/srv/djangy/src/server/master/web_api/application')
+
+os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
+
+import django.core.handlers.wsgi
+application = django.core.handlers.wsgi.WSGIHandler()
diff --git a/src/server/master/web_ui/application/web_ui/.eggs b/src/server/master/web_ui/application/web_ui/.eggs
new file mode 100644
index 0000000..8b7c238
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/.eggs
@@ -0,0 +1,2 @@
+Django
+Beaker
diff --git a/src/server/master/web_ui/application/web_ui/__init__.py b/src/server/master/web_ui/application/web_ui/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/server/master/web_ui/application/web_ui/docs/__init__.py b/src/server/master/web_ui/application/web_ui/docs/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/server/master/web_ui/application/web_ui/docs/admin.py b/src/server/master/web_ui/application/web_ui/docs/admin.py
new file mode 100644
index 0000000..16491c7
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/docs/admin.py
@@ -0,0 +1,6 @@
+from django.contrib import admin
+
+from models import Page
+
+
+admin.site.register(Page)
diff --git a/src/server/master/web_ui/application/web_ui/docs/dump_docs b/src/server/master/web_ui/application/web_ui/docs/dump_docs
new file mode 100755
index 0000000..af0d0d1
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/docs/dump_docs
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+source /srv/djangy/run/python-virtual/bin/activate && \
+python ../manage.py dumpdata --format=yaml docs > wiki_docs.yaml
diff --git a/src/server/master/web_ui/application/web_ui/docs/forms.py b/src/server/master/web_ui/application/web_ui/docs/forms.py
new file mode 100644
index 0000000..798478b
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/docs/forms.py
@@ -0,0 +1,23 @@
+from django import forms as forms
+
+from models import Page
+
+
+class PageForm(forms.Form):
+ name = forms.CharField(max_length=255)
+ content = forms.CharField(widget=forms.Textarea(attrs={
+ 'cols':80,
+ 'rows':30
+ }))
+
+ def clean_name(self):
+ import re
+ from templatetags.wiki import WIKI_WORD
+
+ pattern = re.compile(WIKI_WORD)
+
+ name = self.cleaned_data['name']
+ if not pattern.match(name):
+ raise forms.ValidationError('Must be a WikiWord.')
+
+ return name
diff --git a/src/server/master/web_ui/application/web_ui/docs/migrations/0001_create_tables.py b/src/server/master/web_ui/application/web_ui/docs/migrations/0001_create_tables.py
new file mode 100644
index 0000000..80f2169
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/docs/migrations/0001_create_tables.py
@@ -0,0 +1,37 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding model 'Page'
+ db.create_table('docs_page', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)),
+ ('content', self.gf('django.db.models.fields.TextField')()),
+ ('rendered', self.gf('django.db.models.fields.TextField')()),
+ ))
+ db.send_create_signal('docs', ['Page'])
+
+
+ def backwards(self, orm):
+
+ # Deleting model 'Page'
+ db.delete_table('docs_page')
+
+
+ models = {
+ 'docs.page': {
+ 'Meta': {'ordering': "('name',)", 'object_name': 'Page'},
+ 'content': ('django.db.models.fields.TextField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'rendered': ('django.db.models.fields.TextField', [], {})
+ }
+ }
+
+ complete_apps = ['docs']
diff --git a/src/server/master/web_ui/application/web_ui/docs/migrations/__init__.py b/src/server/master/web_ui/application/web_ui/docs/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/server/master/web_ui/application/web_ui/docs/models.py b/src/server/master/web_ui/application/web_ui/docs/models.py
new file mode 100644
index 0000000..e306e66
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/docs/models.py
@@ -0,0 +1,19 @@
+from django.db import models
+
+from templatetags.wiki import wikify
+
+
+class Page(models.Model):
+ name = models.CharField(max_length=255, unique=True)
+ content = models.TextField()
+ rendered = models.TextField()
+
+ class Meta:
+ ordering = ('name', )
+
+ def __unicode__(self):
+ return self.name
+
+ def save(self, *args, **kwargs):
+ self.rendered = wikify(self.content)
+ super(Page, self).save(*args, **kwargs)
diff --git a/src/server/master/web_ui/application/web_ui/docs/templates/wiki/edit.html b/src/server/master/web_ui/application/web_ui/docs/templates/wiki/edit.html
new file mode 100644
index 0000000..3b2dbcd
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/docs/templates/wiki/edit.html
@@ -0,0 +1,30 @@
+{% extends 'wiki/wiki_base.html' %}
+
+
+{% block wiki_content %}
+ Edit
+
+
+{% endblock %}
diff --git a/src/server/master/web_ui/application/web_ui/docs/templates/wiki/index.html b/src/server/master/web_ui/application/web_ui/docs/templates/wiki/index.html
new file mode 100644
index 0000000..fa6ef3a
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/docs/templates/wiki/index.html
@@ -0,0 +1,16 @@
+{% extends 'wiki/wiki_base.html' %}
+
+
+{% block wiki_content %}
+ Index
+
+ {% if pages %}
+
+ {% else %}
+ Create a new page .
+ {% endif %}
+{% endblock %}
diff --git a/src/server/master/web_ui/application/web_ui/docs/templates/wiki/view.html b/src/server/master/web_ui/application/web_ui/docs/templates/wiki/view.html
new file mode 100644
index 0000000..345392a
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/docs/templates/wiki/view.html
@@ -0,0 +1,26 @@
+{% extends 'wiki/wiki_base.html' %}
+
+
+{% block wiki_content %}
+ {% comment %}
+
+ {% endcomment %}
+ {% if not page.id %}
+ {% if admin %}
+ This page does not exist, create it now ?
+ {% endif %}
+ {% endif %}
+
+ {{ page.content|safe }}
+{% endblock %}
+
+
+{% block footer %}
+ {% if admin %}
+ {% if page.id %}
+ Edit this page
+ {% else %}
+ Create this page
+ {% endif %}
+ {% endif %}
+{% endblock %}
diff --git a/src/server/master/web_ui/application/web_ui/docs/templates/wiki/wiki_base.html b/src/server/master/web_ui/application/web_ui/docs/templates/wiki/wiki_base.html
new file mode 100644
index 0000000..a2c1401
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/docs/templates/wiki/wiki_base.html
@@ -0,0 +1,21 @@
+{% extends "base.html" %}
+{% block title %}
+Djangy Documentation - Djangy: Instant Deployment and scaling for your django applications
+{% endblock %}
+{% block pagetitle %}
+
+{% endblock %}
+{% block content %}
+
+{% block footer %}{% endblock %}
+{% block wiki_content %}{% endblock %}
+
+
+{{ navbar.content|safe }}
+
+{% endblock %}
diff --git a/src/server/master/web_ui/application/web_ui/docs/templatetags/__init__.py b/src/server/master/web_ui/application/web_ui/docs/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/server/master/web_ui/application/web_ui/docs/templatetags/wiki.py b/src/server/master/web_ui/application/web_ui/docs/templatetags/wiki.py
new file mode 100644
index 0000000..53e70a0
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/docs/templatetags/wiki.py
@@ -0,0 +1,19 @@
+import re
+
+from django import template
+
+
+WIKI_WORD = r'(?:[A-Z]+[a-z0-9]+){1,}'
+
+
+register = template.Library()
+
+
+wikifier = re.compile(r'\b(%s)\b' % WIKI_WORD)
+
+
+@register.filter
+def wikify(s):
+ from django.core.urlresolvers import reverse
+ wiki_root = reverse('docs.views.index', args=[], kwargs={})
+ return wikifier.sub(r'\1 ' % wiki_root, s)
diff --git a/src/server/master/web_ui/application/web_ui/docs/urls.py b/src/server/master/web_ui/application/web_ui/docs/urls.py
new file mode 100644
index 0000000..f66e95c
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/docs/urls.py
@@ -0,0 +1,11 @@
+from django.conf.urls.defaults import *
+
+from templatetags.wiki import WIKI_WORD
+
+
+urlpatterns = patterns('docs.views',
+ (r'^$', 'index'),
+ ('overview.html', 'index'),
+ ('(?P%s)/$' % WIKI_WORD, 'view'),
+ ('(?P%s)/edit/$' % WIKI_WORD, 'edit'),
+)
diff --git a/src/server/master/web_ui/application/web_ui/docs/views.py b/src/server/master/web_ui/application/web_ui/docs/views.py
new file mode 100644
index 0000000..3757ab5
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/docs/views.py
@@ -0,0 +1,55 @@
+from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect
+from django.shortcuts import get_object_or_404, render_to_response
+from main.views.shared import is_admin, get_user, auth_required, admin_required
+from forms import PageForm
+from models import Page
+
+def index(request):
+ return HttpResponseRedirect('/docs/Documentation')
+
+def view(request, name):
+ """Shows a single wiki page."""
+ try:
+ page = Page.objects.get(name=name)
+ except Page.DoesNotExist:
+ page = Page(name=name)
+
+ return render_to_response('wiki/view.html', {
+ 'page': page,
+ 'admin': is_admin(request),
+ 'user': get_user(request),
+ 'navbar':Page.objects.get(name='NavBar'),
+ })
+
+@auth_required
+@admin_required
+def edit(request, name):
+ """Allows users to edit wiki pages."""
+ try:
+ page = Page.objects.get(name=name)
+ except Page.DoesNotExist:
+ page = None
+
+ if request.method == 'POST':
+ form = PageForm(request.POST)
+ if form.is_valid():
+ if not page:
+ page = Page()
+ page.name = form.cleaned_data['name']
+ page.content = form.cleaned_data['content']
+
+ page.save()
+ return HttpResponseRedirect('../../%s/' % page.name)
+ else:
+ if page:
+ form = PageForm(initial=page.__dict__)
+ else:
+ form = PageForm(initial={'name': name})
+
+ return render_to_response('wiki/edit.html', {
+ 'form': form,
+ 'admin': is_admin(request),
+ 'user': get_user(request),
+ 'navbar':Page.objects.get(name='NavBar'),
+ })
diff --git a/src/server/master/web_ui/application/web_ui/docs/wiki_docs.yaml b/src/server/master/web_ui/application/web_ui/docs/wiki_docs.yaml
new file mode 100644
index 0000000..0a0f227
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/docs/wiki_docs.yaml
@@ -0,0 +1,1886 @@
+- fields: {content: "Djangy's Architecture \r\nDjangy has several key components:\r\
+ \n
\r\n
\r\n \r\n \r\
+ \nProxyCache \r\n \r\nThe ProxyCache is what sits in front of\
+ \ our server infrastructure. It knows about your application\
+ \ bundle , your custom domain names, and your caching configuration. It\
+ \ automatically balances the load across our workers \
+ \ to ensure the highest performance possible.
\r\n \r\n \r\nMasters \r\n \r\nOur masters coordinate everything\
+ \ that happens on Djangy. When you push your code, you're pushing it to one\
+ \ of our masters. The master is also responsible for updating your application's\
+ \ resource allocations: domain names, performance, debug status, and the like.\
+ \ During deployment, the master chooses which workers have the lowest load,\
+ \ then instructs the workers to build your application's bundle and to launch\
+ \ your application.
\r\n \r\n \r\nWorkers \r\
+ \n \r\nWorkers are the horsepower behind Djangy. When you deploy your\
+ \ app, you deploy it to our worker infrastructure. These are rock-solid, multi-tenant,\
+ \ fully managed machines that do one thing: run applications. When you scale\
+ \ your application's performance, these workers throw resources directly behind\
+ \ your application -- instant scalability is now possible.
\r\n \r\n\
+ \r\nBundles \r\n \r\nWhen you push code\
+ \ to Djangy, we build a self-contained, runnable version of your application\
+ \ for use on our worker infrastructure. Your application's bundle contains\
+ \ a virtual environment (including any dependencies you specify), customized\
+ \ settings, and miscellaneous configuration files required to run your application\
+ \ on Djangy. Your application's bundle is READ-ONLY. For this reason, Djangy\
+ \ does not provide filesystem access to your application. If you need to store\
+ \ uploaded files, we suggest either putting them into your database as BLOBs\
+ \ or using a third-party storage service like http://aws.amazon.com/s3/ .
\r\n \r\n \r\nDatabases \r\n \r\nDjangy provides your\
+ \ application with it's own MySQL database. This database will just work \
+ \ -- all the settings in your application are overridden to ensure seamless\
+ \ integration. You can easily see these settings in action -- they're in the\
+ \ same place, the DATABASES variable inside settings.py.
\r\nYou can interact\
+ \ with your live database using the djangy manage.py shell command.\
+ \ You also have the ability to load and dump data directly -- see the Database docs for more information.
", name: Architecture,
+ rendered: "\r\nDjangy has several key components:\r\n
\r\n\r\n \r\n \r\n\r\n \r\nThe \
+ \ ProxyCache is what sits in\
+ \ front of our server infrastructure. It \
+ \ knows about your application bundle , your custom\
+ \ domain names, and your caching configuration. It automatically balances the load across our workers \
+ \ to ensure the highest performance possible.
\r\n \r\n \r\n\r\n \r\nOur masters coordinate everything\
+ \ that happens on Djangy . When you push your code, you're pushing it to\
+ \ one of our masters. The master is\
+ \ also responsible for updating your application's resource allocations: domain\
+ \ names, performance, debug status, and the like. During deployment, the master chooses which workers have the lowest load,\
+ \ then instructs the workers to build your application's bundle and to launch\
+ \ your application.
\r\n \r\n \r\n\r\n \r\nWorkers are the horsepower behind Djangy . When you deploy your\
+ \ app, you deploy it to our worker infrastructure. These are rock-solid, multi-tenant, fully managed machines that do one\
+ \ thing: run applications. When you\
+ \ scale your application's performance, these workers throw resources directly\
+ \ behind your application -- instant scalability is now possible.
\r\n \r\n \r\n\r\n \r\nWhen \
+ \ you push code to Djangy , we build\
+ \ a self-contained, runnable version of your application for use on our worker\
+ \ infrastructure. Your application's\
+ \ bundle contains a virtual environment (including any dependencies you specify),\
+ \ customized settings, and miscellaneous configuration files required to run\
+ \ your application on Djangy . Your application's bundle is READ-ONLY.\
+ \ For this reason, Djangy does not provide filesystem access to your application. If you need to store uploaded files, we suggest\
+ \ either putting them into your database as BLOBs or using a third-party storage service like http://aws.amazon.com/s3/ .
\r\n \r\n \r\n\r\
+ \n \r\nDjangy provides your\
+ \ application with it's own MySQL database. This database will just work -- all the settings in your application\
+ \ are overridden to ensure seamless integration. You can easily see these settings in action -- they're in the same place,\
+ \ the DATABASES variable inside settings.py.
\r\nYou can interact with your live database using the djangy manage.py\
+ \ shell command. You also have\
+ \ the ability to load and dump data directly -- see the Databases \">Database docs for more information.
"}
+ model: docs.page
+ pk: 30
+- fields: {content: "Django admin panel and auth \r\nDjango's Admin app\
+ \ is a popular way to manage authentication and\r\npermissions. Djangy fully\
+ \ supports the admin panel.
\r\n\r\nUsually when you run syncdb \
+ \ and you have the auth app in your\r\nINSTALLED_APPS , you'd be prompted\
+ \ to create a superuser account.\r\nFor security purposes, we've separated this\
+ \ functionality into a djangy command;\r\nnamely, the djangy createsuperuser \
+ \ command.
\r\n\r\ncreatesuperuser \r\n\r\nAssuming you've created\
+ \ a models.py inside your Django application\r\nand you've previously\
+ \ run the djangy syncdb command, your next step\r\nis to run djangy\
+ \ createsuperuser :
\r\n\r\n\r\ndjangy createsuperuser\r\n \r\
+ \n\r\nAssuming all goes well, a django admin super user will be created and\r\
+ \nyour newfound credentials will be printed for your convenience. Now you\r\
+ \nare free to log in to the admin panel wherever you've decided to configure\
+ \ it,\r\nby default yourapp.djangy.com/admin .
\r\n", name: Auth,
+ rendered: "Django admin panel and auth \r\
+ \nDjango 's Admin \
+ \ app is a popular way to manage authentication and\r\npermissions. Djangy fully supports the admin panel.
\r\n\r\nUsually when you run syncdb and you have\
+ \ the auth app in your\r\nINSTALLED_APPS , you'd be prompted to create\
+ \ a superuser account.\r\nFor security purposes,\
+ \ we've separated this functionality into a djangy command;\r\nnamely, the djangy\
+ \ createsuperuser command.
\r\n\r\ncreatesuperuser \r\n\r\n\
+ Assuming you've created a models.py \
+ \ inside your Django application\r\nand you've\
+ \ previously run the djangy syncdb command, your next step\r\nis to\
+ \ run djangy createsuperuser :
\r\n\r\n\r\ndjangy createsuperuser\r\
+ \n \r\n\r\nAssuming all goes well, a\
+ \ django admin super user will be created and\r\nyour newfound credentials will\
+ \ be printed for your convenience. Now you\r\nare\
+ \ free to log in to the admin panel wherever you've decided to configure it,\r\
+ \nby default yourapp.djangy.com/admin .
\r\n"}
+ model: docs.page
+ pk: 12
+- fields: {content: "Backups \r\n \r\nDjangy performs daily backups\
+ \ of your application bundle, database, and user information. Interacting with\
+ \ these backups via the dashboard and client is on our roadmap. If you need\
+ \ to restore your instance from a recent backup, please email support@djangy.com \
+ \ and we'll be glad to help you.
\r\nAlso note that it's possible to run\
+ \ reset , syncdb , migrate (if necessary), and loaddata \
+ \ if you need to quickly wipe and repopulate your database. More information\
+ \ can be found in the Database documentation .
",
+ name: Backups, rendered: "\r\
+ \n \r\nDjangy performs daily\
+ \ backups of your application bundle, database, and user information. Interacting with these backups via the\
+ \ dashboard and client is on our roadmap. If you need to restore your instance from a recent backup, please email\
+ \ support@djangy.com and we'll be glad to help\
+ \ you.
\r\nAlso note that it's\
+ \ possible to run reset , syncdb , migrate (if necessary),\
+ \ and loaddata if you need to quickly wipe and repopulate your database.\
+ \ More information can be found in\
+ \ the Databases \"\
+ >Database documentation.
"}
+ model: docs.page
+ pk: 28
+- fields: {content: "Background jobs with Celery \r\n \r\n\r\n \r\n \r\nIntroduction to background jobs \r\n \r\n\
+ Background jobs are an essential component of many modern web apps. They're\
+ \ used to keep the load off client requests -- uploading data to S3, image manipulation,\
+ \ and RSS scraping are perfect candidates for background jobs.
\r\nDjangy\
+ \ gives you this ability by utilizing the popular Celery project .
\r\n \r\n \r\nUsing celery \r\n \r\nUsing celery is easy. Here's\
+ \ an example of a task:
\r\n\r\nfrom celery.decorators import task\r\
+ \n\r\n@task\r\ndef do_work(x, y):\r\n return x * y\r\n \r\nYou'd\
+ \ call this task from your view or interactive shell like so:
\r\n\r\
+ \nfrom tasks import do_work\r\nresult = do_work.delay(5, 6) # pass x=5, y=6\
+ \ to the do_work() function\r\nvalue = result.wait() # block until this task\
+ \ completes\r\nprint value\r\n \r\nTo get celery working, add your tasks\
+ \ to a file called tasks.py in the same directory as manage.py \
+ \ (for example). Then, add the following to your settings.py :
\r\
+ \n\r\nINSTALLED_APPS = (\r\n ...,\r\n djcelery,\r\n ghettoq,\r\n)\r\
+ \nimport djcelery\r\ndjcelery.setup_loader()\r\nCELERY_IMPORTS = (\r\n \"\
+ tasks\",\r\n)\r\n \r\nThe djcelery stuff integrates celery\
+ \ into your django project. The ghettoq line is the library required\
+ \ to use your database as the message queue for your tasks.
\r\nAdd the\
+ \ following to your djangy.pip :
\r\
+ \n\r\ndjango-celery\r\nghettoq\r\n \r\nDon't forget to git\
+ \ commit your changes and push!
\r\nBefore you can use the task queue,\
+ \ you need to syncdb and migrate :
\r\n\r\n$ djangy\
+ \ manage.py syncdb\r\n$ djnagy managey.py migrate\r\n \r\n...and you're\
+ \ good to go!
\r\nFor more information on how to utilize celery for background\
+ \ tasks, check out their Official documentation .
", name: Celery, rendered: "\r\n \r\n\r\n \r\n \r\n\r\n \r\nBackground jobs are an essential component of many modern web apps. They 're used to keep the load off client\
+ \ requests -- uploading data to S3 , image\
+ \ manipulation, and RSS scraping are perfect candidates for background jobs.
\r\
+ \nDjangy gives you this ability\
+ \ by utilizing the popular Celery project.
\r\
+ \n \r\n \r\n\r\n \r\nUsing celery is easy. Here 's\
+ \ an example of a task:
\r\n\r\nfrom celery.decorators import task\r\
+ \n\r\n@task\r\ndef do_work(x, y):\r\n return x * y\r\n \r\nYou 'd call this task from your view or interactive\
+ \ shell like so:
\r\n\r\nfrom tasks import do_work\r\nresult = do_work.delay(5,\
+ \ 6) # pass x=5, y=6 to the do_work() function\r\nvalue = result.wait() # block\
+ \ until this task completes\r\nprint value\r\n \r\nTo get celery working, add your tasks to a file called tasks.py \
+ \ in the same directory as manage.py (for example). Then , add the following to your settings.py :
\r\n\r\n\
+ INSTALLED_APPS = (\r\n ...,\r\n djcelery,\r\n ghettoq,\r\n)\r\nimport djcelery\r\
+ \ndjcelery.setup_loader()\r\nCELERY_IMPORTS = (\r\n \"tasks\",\r\n)\r\n \r\
+ \nThe djcelery stuff integrates\
+ \ celery into your django project. The \
+ \ ghettoq line is the library required to use your database as the\
+ \ message queue for your tasks.
\r\nAdd the following to your Dependencies \">djangy.pip :
\r\n\r\ndjango-celery\r\
+ \nghettoq\r\n \r\nDon 't forget\
+ \ to git commit your changes and push!
\r\nBefore you can use the task queue, you need to syncdb and migrate :
\r\
+ \n\r\n$ djangy manage.py syncdb\r\n$ djnagy managey.py migrate\r\n \r\
+ \n...and you're good to go!
\r\nFor more information on how to utilize celery for background tasks, check\
+ \ out their Official documentation.
"}
+ model: docs.page
+ pk: 33
+- fields: {content: "Installing the Djangy command-line client \r\n \r\
+ \nThe Djangy command-line tool is how you'll interact with our API most of\
+ \ the time. The client will allow you to create djangy applications, view logs,\
+ \ and run manage.py commands like syncdb and migrate. It will also give you\
+ \ access to the usual interactive shell you're used to for local development.\r\
+ \n
\r\n\r\n \r\n \r\nInstallation \r\n \r\nAssuming you already have a Djangy\
+ \ account, you have two options to install the djangy command-line client:\r\
+ \n
\r\n$ easy_install djangy\r\n \r\nor\r\n\r\n$ pip install djangy\r\
+ \n \r\n\r\n \r\nUsage \r\n\r\n$ djangy\
+ \ help\r\n Djangy Commands:\r\n # NOTE: all commands\
+ \ accept\r\n # the [-a app_name] argument:\r\n\
+ \ # $ djangy -a myproject create\r\n\r\ndjangy\
+ \ create # create a new djangy application\r\n\r\ndjangy manage.py\
+ \ <command> # remotely execute manage.py command\r\ndjangy manage.py\
+ \ syncdb\r\ndjangy manage.py migrate\r\ndjangy manage.py shell\r\n\r\ndjangy\
+ \ logs # display recent log output (last 100 lines)\r\n\
+ djangy help # display this message\r\n\r\n# Example:\r\n\
+ \r\n django-startproject myproject\r\n cd myproject\r\n git init\r\n\
+ \ git add .\r\n git commit -m \"my new project\"\r\n djangy create\r\
+ \n git push djangy master\r\n\r\n# http://www.djangy.com/docs/ | support@djangy.com\r\
+ \n\r\n ", name: Client, rendered: "\r\n \r\nThe Djangy command-line tool is how you'll\
+ \ interact with our API most of the time. The client will allow you to create djangy applications, view logs, and\
+ \ run manage.py commands like syncdb and migrate. It will also give you access to the usual interactive shell you're used\
+ \ to for local development.\r\n
\r\
+ \n\r\n \r\n \r\n\r\n \r\nAssuming you already have a Djangy \
+ \ account, you have two options to install the djangy command-line client:\r\
+ \n
\r\n$ easy_install djangy\r\n \r\nor\r\n\r\n$ pip install djangy\r\
+ \n \r\n\r\n \r\n\r\n\r\n$ djangy help\r\n Djangy Commands :\r\n \
+ \ # NOTE: all commands accept\r\n \
+ \ # the [-a app_name] argument:\r\n \
+ \ # $ djangy -a myproject create\r\n\r\ndjangy create \
+ \ # create a new djangy application\r\n\r\ndjangy manage.py <command>\
+ \ # remotely execute manage.py command\r\ndjangy manage.py syncdb\r\ndjangy\
+ \ manage.py migrate\r\ndjangy manage.py shell\r\n\r\ndjangy logs \
+ \ # display recent log output (last 100 lines)\r\ndjangy help \
+ \ # display this message\r\n\r\n# Example :\r\n\r\n django-startproject myproject\r\n cd myproject\r\
+ \n git init\r\n git add .\r\n git commit -m \"my new project\"\r\n\
+ \ djangy create\r\n git push djangy master\r\n\r\n# http://www.djangy.com/docs/\
+ \ | support@djangy.com\r\n\r\n "}
+ model: docs.page
+ pk: 21
+- fields: {content: "Collaboration \r\n \r\nAdding Collaborators \r\
+ \n \r\nCollaborating with other developers on your project is easy:\r\
+ \n
\r\n Navigate to the dashboard for your application \r\n Find\
+ \ the \"Collaborators\" section \r\n Enter the user's email address\
+ \ in the provided textbox \r\n Click \"Add Collaborator\"! \r\n\
+ \r\nThat user will now have access to that application via the djangy command-line\
+ \ tool, the web dashboard, and via git (provided they've provided us with their\
+ \ SSH public key).\r\n\r\nRemoving Collaborators \r\n \r\n\
+ Removing collaborators is just as easy as adding them. Simply find their\
+ \ email address in the \"Collaborators\" section of your application's web dashboard\
+ \ and click \"remove\".
", name: Collaboration, rendered: "\r\n \r\n\r\n \r\nCollaborating with other developers on your project is easy:\r\n
\r\nThat \
+ \ user will now have access to that application via the djangy command-line\
+ \ tool, the web dashboard, and via git (provided they've provided us with their\
+ \ SSH public key).\r\n\r\n\r\n \r\nRemoving collaborators\
+ \ is just as easy as adding them. Simply \
+ \ find their email address in the \"Collaborators \" section of your application's web dashboard and click \"\
+ remove\".
"}
+ model: docs.page
+ pk: 24
+- fields: {content: "Optional configuration files \r\n \r\nDjangy\
+ \ automatically creates and uses three configuration files. You may\r\nneed\
+ \ to customize these configuration files if your application has more\r\ndependencies,\
+ \ or if you make a change to the name of the directory\r\ncontaining your git\
+ \ repository.
\r\n \r\n~/.djangy \r\n \r\nContains your\
+ \ Djangy account's email address and hashed password. Used\r\nby the Djangy\
+ \ client program to authenticate with the Djangy servers. If\r\nyou change\
+ \ your password on the Djangy web\r\ndashboard , the\
+ \ next time you run djangy you'll be asked\r\nto update your\
+ \ password, and it will then be saved to\r\n~/.djangy
\r\n\
+ \r\ngit_repo/djangy.eggs \r\n \r\nSee Managing Dependencies .
\r\ngit_repo/djangy.pip \r\n \r\n\
+ See Managing Dependencies .
\r\n \r\
+ \ngit_repo/djangy.config \r\n \r\nConfigures basic information\
+ \ about your Djangy application, including the\r\napplication's name and what\
+ \ the application's git repository directory is\r\nnamed on your computer. \
+ \ A basic version of djangy.config is\r\nautomatically generated\
+ \ by “djangy create ”,\r\ndepicted below.
\r\n\
+ \r\n[application]\r\nrootdir=... \r\napplication_name=... \r\
+ \n\r\nImportant: If your application assumes that your git repository\r\
+ \nhas a particular name, you must include a\r\ndjangy.config \
+ \ file with rootdir set\r\ncorrectly.
", name: ConfigFiles,
+ rendered: "Optional configuration\
+ \ files \r\n \r\nDjangy \
+ \ automatically creates and uses three configuration files. You may\r\nneed to customize these configuration files if your application\
+ \ has more\r\ndependencies, or if you make a change to the name of the directory\r\
+ \ncontaining your git repository.
\r\n \r\n~/.djangy \r\n \r\nContains your Djangy account's email address and hashed password.\
+ \ Used \r\nby the Djangy client program to authenticate with the Djangy servers. If \r\nyou change\
+ \ your password on the Djangy web\r\ndashboard, the next time you run djangy \
+ \ you'll be asked\r\nto update your password, and it will then be saved to\r\
+ \n~/.djangy
\r\n \r\ngit_repo/djangy.eggs \r\
+ \n \r\nSee Dependencies \">Managing Dependencies .
\r\
+ \ngit_repo/djangy.pip \r\n \r\nSee Dependencies \"\
+ >Managing Dependencies .
\r\n \r\ngit_repo/djangy.config \r\n\
+ \r\nConfigures basic\
+ \ information about your Djangy application,\
+ \ including the\r\napplication's name and what the application's git repository\
+ \ directory is\r\nnamed on your computer. A basic version of djangy.config \
+ \ is\r\nautomatically generated by “djangy create ”,\r\
+ \ndepicted below.
\r\n\r\n[application]\r\nrootdir=... \r\napplication_name=... \r\
+ \n\r\nImportant : If your application assumes that your\
+ \ git repository\r\nhas a particular name, you must include a\r\ndjangy.config \
+ \ file with rootdir set\r\ncorrectly.
"}
+ model: docs.page
+ pk: 9
+- fields: {content: "Creating Djangy Applications \r\n \r\nDjangy\
+ \ is a platform for hosting your django applications. The first step is always\
+ \ to create an app. Simply run the following commands from within your git\
+ \ repository:\r\n
\r\n$ cd myapp\r\n$ djangy create\r\nUsing git repository\
+ \ \"/Users/dave/projects/myapp\"\r\n\r\nEnter your email address: dave@djangy.com\r\
+ \nPlease enter your password: \r\n \r\nSaved credentials.\r\nTo change your\
+ \ email address or password, remove \"/Users/dave/.djangy\"\r\n\r\nPlease enter\
+ \ your application name [Enter for myapp]: \r\n \r\nUsing application name \"\
+ myapp\" from user input\r\nUsing public key file \"/Users/dave/.ssh/id_rsa.pub\"\
+ \r\nApplication created.\r\n[master 83b657d] added \"djangy.config\" and \"\
+ djangy.eggs\" to repository\r\n 2 files changed, 5 insertions(+), 0 deletions(-)\r\
+ \n create mode 100644 djangy.config\r\n create mode 100644 djangy.eggs\r\n\r\
+ \nYou can now run \"git push djangy master\"\r\n \r\nNow you're ready to\
+ \ Deploy with Git .", name: CreatingApps,
+ rendered: "\r\n \r\nDjangy is a platform for hosting your django applications. The first step is always to create an app. Simply run the following commands\
+ \ from within your git repository:\r\n
\r\n$ cd myapp\r\n$ djangy create\r\
+ \nUsing git repository \"/Users /dave/projects/myapp\"\r\n\r\nEnter your email address: dave@djangy.com\r\n\
+ Please enter your password: \r\n\
+ \ \r\nSaved credentials.\r\nTo change your email address or password, remove\
+ \ \"/Users /dave/.djangy\"\r\n\r\n\
+ Please enter your application name\
+ \ [Enter for myapp]: \r\n \r\nUsing application name \"myapp\" from user input\r\
+ \nUsing public key file \"/Users /dave/.ssh/id_rsa.pub\"\r\nApplication created.\r\n[master 83b657d] added \"djangy.config\" and \"\
+ djangy.eggs\" to repository\r\n 2 files changed, 5 insertions(+), 0 deletions(-)\r\
+ \n create mode 100644 djangy.config\r\n create mode 100644 djangy.eggs\r\n\r\
+ \nYou can now run \"git push djangy\
+ \ master\"\r\n \r\nNow you're ready\
+ \ to DeployingWithGit \"\
+ >Deploy with Git ."}
+ model: docs.page
+ pk: 22
+- fields: {content: "Using a database on Djangy \r\n\r\nSooner or later\
+ \ (probably sooner), you'll want to use Django data models\r\nin your application.\
+ \ Django models require a database to store persistent\r\ndata.
\r\n\r\n\
+ Djangy automatically provides your Django application with its own MySQL\r\
+ \ndatabase. You can interact with this database using your normal workflow:\r\
+ \n
\r\n\r\n \r\n \r\nsyncdb \r\n\
+ \r\nAssuming you've created a models.py inside your Django\
+ \ application\r\nand it works for you locally, all you need to do to run the\
+ \ equivalent of\r\npython manage.py syncdb on Djangy:
\r\n\r\n\r\
+ \n$ djangy manage.py syncdb\r\n\r\nUsing git repository \"/Users/dave/myapp\"\
+ \r\nUsing application name \"myapp\" from \"/Users/dave/myapp/djangy.config\"\
+ \r\n\r\nCreating table auth_permission\r\nCreating table auth_group_permissions\r\
+ \nCreating table auth_group\r\nCreating table auth_user_user_permissions\r\n\
+ Creating table auth_user_groups\r\nCreating table auth_user\r\nCreating table\
+ \ auth_message\r\nCreating table django_content_type\r\nCreating table django_session\r\
+ \nCreating table django_site\r\n\r\nYou just installed Django's auth system,\
+ \ which means you don't have any \r\nsuperusers defined. Would you like to\
+ \ create one now? (yes/no): yes\r\nUsername (Leave blank to use 'djangy'): dave\
+ \ \r\nE-mail address: dave@djangy.com\r\nPassword:\r\nPassword (again):\r\
+ \n\r\nSuperuser created successfully.\r\nInstalling index for auth.Permission\
+ \ model\r\nInstalling index for auth.Group_permissions model\r\nInstalling index\
+ \ for auth.User_user_permissions model\r\nInstalling index for auth.User_groups\
+ \ model\r\nInstalling index for auth.Message model\r\nNo fixtures found.\r\n\
+ \r\n \r\n \r\nmigrate \r\n \r\
+ \nDjango's syncdb only creates tables that don't already exist in\r\
+ \nthe database; it doesn't even try to help when you change the structure of\r\
+ \nan existing Django model. South \
+ \ is\r\na Django extension which provides migrations that allow you to\
+ \ change\r\nthe schema of an existing database.
\r\n\r\nDjangy fully supports\
+ \ South, using the following workflow:
\r\n\r\n\r\nUpdate your Django\
+ \ models locally on your development workstation. \r\nRun python\
+ \ manage.py schemamigration , e.g.\r\n python manage.py schemamigration\
+ \ appname --auto \r\n This will generate migration files in your local\
+ \ git repository. \r\nRun git commit and git push \
+ \ to upload the\r\n migrations to Djangy.\r\n git add .\r\ngit commit\
+ \ -m \"Created migrations\"\r\ngit push djangy master \r\nRun\
+ \ djangy manage.py migrate to apply the migrations. The djangy\r\
+ \n manage.py migrate command accepts the same arguments as python\
+ \ manage.py\r\n migrate , but runs on Djangy's servers rather than your\
+ \ local\r\n development workstation.\r\n djangy manage.py migrate\
+ \ [args...] \r\n \r\n \r\n \r\nloaddata \r\
+ \n \r\nloaddata is the standard django-esque method of prepopulating\
+ \ your database. Using loaddata on Djangy is no different from using loaddata\
+ \ normally. Make sure your fixture is in your repository and pushed to Djangy,\
+ \ then run:\r\n
\r\n$ djangy manage.py loaddata <fixture-name>\r\n\
+ \r\nFor more information on loaddata and fixtures, see the Django Documentation .\r\n\r\n \r\n \r\ndumpdata \r\n \r\ndumpdata is the\
+ \ standard django-esque method of downloading all of the data currently in your\
+ \ database. Using dumpdata on Djangy is no different from using dumpdata normally.\
+ \ Simply run:\r\n
\r\n$ djangy manage.py dumpdata [app-name] > fixture-name.json\r\
+ \n \r\nNote that if no app name is provided, an entire database dump will\
+ \ be downloaded. It's also important to note that all output will be directed\
+ \ to stdout . Hence the need to use > fixture-name.json \
+ \ at the end of the command. For more information on dumpdata and fixtures,\
+ \ see the Django Documentation .\r\n \r\n \r\nreset \r\n \r\nreset is the standard django-esque\
+ \ method of dropping database tables. Usage is identical to local usage:\r\n\
+
\r\n$ djangy manage.py reset <app-name>\r\n \r\nSince this is\
+ \ such a sensitive operation, you'll be prompted before resetting any of your\
+ \ applications' data. For more information, see the official Django docs .", name: Databases, rendered: "\r\n\r\nSooner \
+ \ or later (probably sooner), you'll want to use Django data models\r\nin your application. Django models require a database to store persistent\r\ndata.
\r\n\r\
+ \nDjangy automatically provides\
+ \ your Django application with its\
+ \ own MySQL\r\ndatabase. You can interact\
+ \ with this database using your normal workflow:\r\n
\r\n\r\
+ \n \r\n \r\nsyncdb \r\n \r\nAssuming you've created a models.py \
+ \ inside your Django application\r\
+ \nand it works for you locally, all you need to do to run the equivalent of\r\
+ \npython manage.py syncdb on Djangy :
\r\n\r\n\r\n$ djangy manage.py syncdb\r\n\r\nUsing git repository \"/Users /dave/myapp\"\r\nUsing application\
+ \ name \"myapp\" from \"/Users /dave/myapp/djangy.config\"\
+ \r\n\r\nCreating table auth_permission\r\
+ \nCreating table auth_group_permissions\r\
+ \nCreating table auth_group\r\n\
+ Creating table auth_user_user_permissions\r\
+ \nCreating table auth_user_groups\r\
+ \nCreating table auth_user\r\n\
+ Creating table auth_message\r\n\
+ Creating table django_content_type\r\
+ \nCreating table django_session\r\
+ \nCreating table django_site\r\n\
+ \r\nYou just installed Django 's auth system, which means you don't have any \r\nsuperusers defined.\
+ \ Would you like to create one now?\
+ \ (yes/no): yes\r\nUsername (Leave blank to use 'djangy'): dave\
+ \ \r\nE-mail address: dave@djangy.com\r\nPassword :\r\nPassword (again):\r\
+ \n\r\nSuperuser created successfully.\r\
+ \nInstalling index for auth.Permission model\r\nInstalling index for auth.Group_permissions\
+ \ model\r\nInstalling index for\
+ \ auth.User_user_permissions model\r\nInstalling index for auth.User_groups model\r\nInstalling index for auth.Message \
+ \ model\r\nNo fixtures found.\r\n \r\
+ \n \r\n \r\nmigrate \r\n \r\nDjango 's syncdb only creates\
+ \ tables that don't already exist in\r\nthe database; it doesn't even try to\
+ \ help when you change the structure of\r\nan existing Django model. South is\r\na Django extension\
+ \ which provides migrations that allow you to change\r\nthe schema of\
+ \ an existing database.
\r\n\r\nDjangy fully supports South ,\
+ \ using the following workflow:
\r\n\r\n\r\nUpdate your Django models locally\
+ \ on your development workstation. \r\nRun python manage.py schemamigration , e.g.\r\n python\
+ \ manage.py schemamigration appname --auto \r\n This will generate migration files in your local git repository. \r\
+ \nRun git commit and git\
+ \ push to upload the\r\n migrations to Djangy .\r\n git add .\r\ngit commit -m \"Created migrations\"\r\ngit push djangy master \r\nRun djangy manage.py migrate to apply\
+ \ the migrations. The djangy\r\n\
+ \ manage.py migrate command accepts the same arguments as python\
+ \ manage.py\r\n migrate , but runs on Djangy 's servers rather than your local\r\n development workstation.\r\
+ \n djangy manage.py migrate [args...] \r\n \r\n \r\
+ \n \r\nloaddata \r\n \r\nloaddata \
+ \ is the standard django-esque method of prepopulating your database. Using loaddata on Djangy is no different from using loaddata normally. Make sure your fixture is in your repository and pushed to Djangy , then run:\r\n
\r\n$ djangy manage.py loaddata <fixture-name>\r\
+ \n \r\nFor more information on\
+ \ loaddata and fixtures, see the Django Documentation .\r\n\r\n \r\
+ \n \r\ndumpdata \r\n \r\ndumpdata \
+ \ is the standard django-esque method of downloading all of the data currently\
+ \ in your database. Using dumpdata\
+ \ on Djangy is no different from\
+ \ using dumpdata normally. Simply \
+ \ run:\r\n
\r\n$ djangy manage.py dumpdata [app-name] > fixture-name.json\r\
+ \n \r\nNote that if no app name\
+ \ is provided, an entire database dump will be downloaded. It 's also important to note that all output will be directed to stdout .\
+ \ Hence the need to use >\
+ \ fixture-name.json at the end of the command. For more information on dumpdata and fixtures, see the Django Documentation .\r\n \r\n\
+ \r\nreset \r\n \r\nreset is the\
+ \ standard django-esque method of dropping database tables. Usage is identical to local usage:\r\n
\r\n$ djangy manage.py reset\
+ \ <app-name>\r\n \r\nSince \
+ \ this is such a sensitive operation, you'll be prompted before resetting any\
+ \ of your applications' data. For more\
+ \ information, see the official Django docs."}
+ model: docs.page
+ pk: 10
+- fields: {content: "Managing Dependencies \r\n \r\nPython offers\
+ \ a wide range of installable packages. Djangy offers native support for automatically\
+ \ installing dependencies using easy_install or pip . All\
+ \ you have to do is create a file called \"djangy.eggs \" (for Python\
+ \ eggs, installed using easy_install ) or \"djangy.pip \"\
+ \ (for Python source packages, installed using pip ) in the root\
+ \ directory of your git repository, containing a list of packages you would\
+ \ like Djangy to install.
\r\n\r\nIf you don't create a djangy.pip \
+ \ or djangy.eggs file, Djangy will assume that your application's only\
+ \ dependencies are Python iself, Django , and South .
\r\n\
+ \r\nIn general, you can list any Python source package that is available\
+ \ on PyPI . There are over 12,000 \
+ \ packages available on PyPI.
\r\n\r\ndjangy.pip file format \r\n\
+ \r\nThe optional djangy.pip file is used to specify Python\
+ \ source packages to install. It follows the pip requirements file format . For example:\r\n
\r\nMyPackage==3.0\r\
+ \ne svn+http://svn.myproject.org/svn/MyProject/trunk#egg=MyProject\r\n \r\
+ \nThe main advantage of using pip over easy_install is pip's\
+ \ ability to install python libraries from remote repositories or website URLs.\r\
+ \n\r\n\r\ndjangy.eggs file format \r\n\r\nThe optional djangy.eggs \
+ \ file in the root directory of your git repository should list one egg requirement\
+ \ on each line. By default, djangy create will automatically create\
+ \ a minimal djangy.eggs file in the correct location.
\r\n\r\nFor\
+ \ example, the following example requires Django (version 1.2 or later), South,\
+ \ Mako, a custom package my_package.egg stored in a directory named\
+ \ local_eggs/ in your git repository, and another custom package located\
+ \ at http://djangy.com/example.egg (note: this file doesn't actually\
+ \ exist):
\r\n\r\nDjango>=1.2\r\nSouth\r\nMako\r\n./local_eggs/my_package.egg\r\
+ \nhttp://djangy.com/example.egg \r\n\r\nNote that we explicitly included\
+ \ Django and South in the list. If you\r\nhave a djangy.eggs file\
+ \ but it doesn't list Django or South, those\r\npackages will not be\
+ \ installed.
\r\n\r\nIn general, you can list any Python egg that is available\
+ \ on PyPI . Django, South, and Mako\
+ \ are just a few examples; there are over 12,000 packages available on\
+ \ PyPI.
\r\n\r\nYou can also construct more elaborate egg requirements\
+ \ if you'd like;\r\neach line is the same as the requirement_or_url \
+ \ you'd pass to\r\npip or easy_install on your local development\
+ \ workstation. For example,\r\nyou can use this to force installation of a\
+ \ custom version of Django or\r\nSouth or any other package. Or you can use\
+ \ a URL to force installation of a Python egg which isn't available on PyPI.
\r\
+ \n", name: Dependencies, rendered: "\r\
+ \n \r\nPython offers a wide\
+ \ range of installable packages. Djangy \
+ \ offers native support for automatically installing dependencies using easy_install \
+ \ or pip . All you have to\
+ \ do is create a file called \"djangy.eggs \" (for Python eggs, installed using easy_install ) or \"djangy.pip \"\
+ \ (for Python source packages, \
+ \ installed using pip ) in the root directory of your git repository,\
+ \ containing a list of packages you would like Djangy to install.
\r\n\r\nIf \
+ \ you don't create a djangy.pip or djangy.eggs file, Djangy will assume that your application's\
+ \ only dependencies are Python iself,\
+ \ Django , and South .
\r\n\r\nIn general, you can list any Python \
+ \ source package that is available on PyPI . There are over 12,000 \
+ \ packages available on PyPI.
\r\n\r\ndjangy.pip file format \r\n\
+ \r\nThe optional djangy.pip \
+ \ file is used to specify Python \
+ \ source packages to install. It follows\
+ \ the pip \
+ \ requirements file format . For \
+ \ example:\r\n
\r\nMyPackage ==3.0\r\
+ \ne svn+http://svn.myproject.org/svn/MyProject /trunk#egg=MyProject \r\
+ \n \r\nThe main advantage of using\
+ \ pip over easy_install is pip's ability to install python\
+ \ libraries from remote repositories or website URLs .\r\n\r\n\r\ndjangy.eggs file format \r\n\r\nThe optional djangy.eggs file in the\
+ \ root directory of your git repository should list one egg requirement on each\
+ \ line. By default, djangy create \
+ \ will automatically create a minimal djangy.eggs file in the correct\
+ \ location.
\r\n\r\nFor example,\
+ \ the following example requires Django \
+ \ (version 1.2 or later), South , Mako , a custom package my_package.egg \
+ \ stored in a directory named local_eggs/ in your git repository, and\
+ \ another custom package located at http://djangy.com/example.egg (note:\
+ \ this file doesn't actually exist):
\r\n\r\nDjango >=1.2\r\nSouth \r\nMako \r\n./local_eggs/my_package.egg\r\nhttp://djangy.com/example.egg \r\
+ \n\r\nNote that we explicitly included\
+ \ Django and South in the list. If you\r\nhave\
+ \ a djangy.eggs file but it doesn't list Django or South , those\r\npackages\
+ \ will not be installed.
\r\n\r\nIn general, you can list any Python \
+ \ egg that is available on PyPI .\
+ \ Django , South , and Mako are just a few\
+ \ examples; there are over 12,000 packages available on PyPI.
\r\n\
+ \r\nYou can also construct more elaborate\
+ \ egg requirements if you'd like;\r\neach line is the same as the requirement_or_url \
+ \ you'd pass to\r\npip or easy_install on your local development\
+ \ workstation. For example,\r\nyou\
+ \ can use this to force installation of a custom version of Django or\r\nSouth or any other\
+ \ package. Or you can use a URL to force\
+ \ installation of a Python egg which\
+ \ isn't available on PyPI.
\r\n"}
+ model: docs.page
+ pk: 25
+- fields: {content: "Deploying with Git \r\n \r\nAfter you've created\
+ \ your application with Djangy, the final step is to deploy. This is where\
+ \ Djangy really shines. There's just ONE COMMAND separating you from deployment:\r\
+ \n
\r\n$ git push djangy master\r\n\r\nCounting objects: 10, done.\r\nDelta\
+ \ compression using up to 2 threads.\r\nCompressing objects: 100% (8/8), done.\r\
+ \nWriting objects: 100% (10/10), 2.67 KiB, done.\r\nTotal 10 (delta 1), reused\
+ \ 0 (delta 0)\r\n\r\n\r\nWelcome to Djangy!\r\n\r\nDeploying project myapp.\r\
+ \n\r\nCloning git repository... Done.\r\n\r\nCreating production settings.py\
+ \ file... Done.\r\n\r\nInstalling dependencies...\r\n Dependencies from djangy.eggs\
+ \ using easy_install:\r\n Installing Django... Success.\r\n Installing\
+ \ South... Success.\r\n Installing gunicorn... Success.\r\n Dependencies\
+ \ from djangy.pip using pip:\r\n None found.\r\nDone.\r\n\r\nSaving bundle\
+ \ info... Done.\r\n\r\nDeploying to worker hosts... Done.\r\n\r\nTo git@api.djangy.com:myapp.git\r\
+ \n * [new branch] master -> master\r\n \r\n \r\nThat's it! You\
+ \ can now access your running application at http://myapp.djangy.com. However,\
+ \ you might want to remember to sync your database .\r\
+ \n", name: DeployingWithGit, rendered: "\r\n \r\nAfter you've created your\
+ \ application with Djangy , the final\
+ \ step is to deploy. This is where\
+ \ Djangy really shines. There 's just ONE COMMAND separating you from\
+ \ deployment:\r\n
\r\n$ git push djangy master\r\n\r\nCounting objects: 10, done.\r\nDelta \
+ \ compression using up to 2 threads.\r\nCompressing objects: 100% (8/8), done.\r\nWriting objects: 100% (10/10), 2.67 KiB, done.\r\nTotal 10 (delta 1), reused 0 (delta 0)\r\n\r\n\r\nWelcome to Djangy !\r\n\r\nDeploying project myapp.\r\n\r\n\
+ Cloning git repository... Done .\r\n\r\nCreating production settings.py file... Done .\r\n\r\nInstalling \
+ \ dependencies...\r\n Dependencies \
+ \ from djangy.eggs using easy_install:\r\n Installing Django ... Success .\r\n Installing South ... Success .\r\n Installing gunicorn... Success .\r\
+ \n Dependencies from djangy.pip\
+ \ using pip:\r\n None found.\r\n\
+ Done .\r\n\r\nSaving bundle info... Done .\r\n\
+ \r\nDeploying to worker hosts...\
+ \ Done .\r\n\r\nTo git@api.djangy.com:myapp.git\r\n * [new branch] master -> master\r\
+ \n \r\n \r\nThat 's it! You can now access your running application\
+ \ at http://myapp.djangy.com. However ,\
+ \ you might want to remember to Databases \">sync your database.\r\n"}
+ model: docs.page
+ pk: 23
+- fields: {content: "The pages listed on the right explain how to host Django applications\
+ \ on\r\n djangy.com. Since Djangy is currently in beta, the documentation and\
+ \ how\r\n things work are likely to change over time. Please feel free to email\r\
+ \n support@djangy.com with any\r\n\
+ \ questions or comments.
\r\n", name: Documentation, rendered: "The pages listed on the right explain how to host\
+ \ Django applications on\r\n djangy.com.\
+ \ Since Djangy is currently in beta, the documentation and how\r\n things work\
+ \ are likely to change over time. Please \
+ \ feel free to email\r\n support@djangy.com \
+ \ with any\r\n questions or comments.
\r\n"}
+ model: docs.page
+ pk: 1
+- fields: {content: "Installing dependencies \r\n\r\nPython offers a wide\
+ \ range of installable packages. Djangy offers native support for automatically\
+ \ installing dependencies using easy_install or pip . All\
+ \ you have to do is create a file called \"djangy.eggs \" (for Python\
+ \ eggs, installed using easy_install ) or \"djangy.pip \"\
+ \ (for Python source packages, installed using pip ) in the root\
+ \ directory of your git repository, containing a list of packages you would\
+ \ like Djangy to install.
\r\n\r\nIf you don't create a djangy.pip \
+ \ or djangy.eggs file, Djangy will assume that your application's only\
+ \ dependencies are Python iself, Django , and South .
\r\n\
+ \r\ndjangy.pip file format \r\n\r\nThe optional djangy.pip \
+ \ file is used to specify Python source packages to install. It follows the\
+ \ pip \
+ \ requirements file format .
\r\n\r\nIn general, you can list any Python\
+ \ source package that is available on PyPI . There are over 12,000 packages available on PyPI.
\r\n\
+ \r\ndjangy.eggs file format \r\n\r\nThe optional djangy.eggs \
+ \ file in the root directory of your git repository should list one egg requirement\
+ \ on each line. By default, djangy create will automatically create\
+ \ a minimal djangy.eggs file in the correct location.
\r\n\r\nFor\
+ \ example, the following example requires Django (version 1.2 or later), South,\
+ \ Mako, a custom package my_package.egg stored in a directory named\
+ \ local_eggs/ in your git repository, and another custom package located\
+ \ at http://djangy.com/example.egg (note: this file doesn't actually\
+ \ exist):
\r\n\r\nDjango>=1.2\r\nSouth\r\nMako\r\n./local_eggs/my_package.egg\r\
+ \nhttp://djangy.com/example.egg \r\n\r\nNote that we explicitly included\
+ \ Django and South in the list. If you\r\nhave a djangy.eggs file\
+ \ but it doesn't list Django or South, those\r\npackages will not be\
+ \ installed.
\r\n\r\nIn general, you can list any Python egg that is available\
+ \ on PyPI . Django, South, and Mako\
+ \ are just a few examples; there are over 12,000 packages available on\
+ \ PyPI.
\r\n\r\nYou can also construct more elaborate egg requirements\
+ \ if you'd like;\r\neach line is the same as the requirement_or_url \
+ \ you'd pass to\r\neasy_install on your local development workstation.\
+ \ For example,\r\nyou can use this to force installation of a custom version\
+ \ of Django or\r\nSouth or any other package. Or you can use a URL to force\
+ \ installation of a Python egg which isn't available on PyPI.
\r\n", name: Eggs,
+ rendered: "\r\
+ \n\r\nPython offers a wide range\
+ \ of installable packages. Djangy \
+ \ offers native support for automatically installing dependencies using easy_install \
+ \ or pip . All you have to\
+ \ do is create a file called \"djangy.eggs \" (for Python eggs, installed using easy_install ) or \"djangy.pip \"\
+ \ (for Python source packages, \
+ \ installed using pip ) in the root directory of your git repository,\
+ \ containing a list of packages you would like Djangy to install.
\r\n\r\nIf \
+ \ you don't create a djangy.pip or djangy.eggs file, Djangy will assume that your application's\
+ \ only dependencies are Python iself,\
+ \ Django , and South .
\r\n\r\ndjangy.pip file format \r\
+ \n\r\nThe optional djangy.pip \
+ \ file is used to specify Python \
+ \ source packages to install. It follows\
+ \ the pip \
+ \ requirements file format .
\r\n\r\nIn general, you can list any Python \
+ \ source package that is available on PyPI . There are over 12,000 \
+ \ packages available on PyPI.
\r\n\r\ndjangy.eggs file format \r\n\
+ \r\nThe optional djangy.eggs \
+ \ file in the root directory of your git repository should list one egg requirement\
+ \ on each line. By default, djangy\
+ \ create will automatically create a minimal djangy.eggs file\
+ \ in the correct location.
\r\n\r\nFor example, the following example requires Django (version 1.2 or later), South ,\
+ \ Mako , a custom package my_package.egg \
+ \ stored in a directory named local_eggs/ in your git repository, and\
+ \ another custom package located at http://djangy.com/example.egg (note:\
+ \ this file doesn't actually exist):
\r\n\r\nDjango >=1.2\r\nSouth \r\nMako \r\n./local_eggs/my_package.egg\r\nhttp://djangy.com/example.egg \r\
+ \n\r\nNote that we explicitly included\
+ \ Django and South in the list. If you\r\nhave\
+ \ a djangy.eggs file but it doesn't list Django or South , those\r\npackages\
+ \ will not be installed.
\r\n\r\nIn general, you can list any Python \
+ \ egg that is available on PyPI .\
+ \ Django , South , and Mako are just a few\
+ \ examples; there are over 12,000 packages available on PyPI.
\r\n\
+ \r\nYou can also construct more elaborate\
+ \ egg requirements if you'd like;\r\neach line is the same as the requirement_or_url \
+ \ you'd pass to\r\neasy_install on your local development workstation.\
+ \ For example,\r\nyou can use this\
+ \ to force installation of a custom version of Django or\r\nSouth or any other\
+ \ package. Or you can use a URL to force\
+ \ installation of a Python egg which\
+ \ isn't available on PyPI.
\r\n"}
+ model: docs.page
+ pk: 11
+- fields: {content: "Configuring email \r\nConfiguring email for your application\
+ \ on Djangy is the same as for any other Django application. You need, at a\
+ \ minimum, the following in your settings.py:
\r\n\r\nEMAIL_HOST\r\n\
+ EMAIL_PORT\r\n \r\nIf your email server requires authentication for\
+ \ SMTP, you must also set:
\r\n\r\nEMAIL_HOST_USER\r\nEMAIL_HOST_PASSWORD\r\
+ \n \r\n\r\nFinally,
EMAIL_USE_TLS controls whether or not\
+ \ a secure connection is used. For more information on how to send mail using\
+ \ Django, see the official documentation .\r\n\r\nWe recommend using\
+ \ Google Apps for your email service (it's what we use).", name: Email,
+ rendered: "\r\nConfiguring email for your application on\
+ \ Djangy is the same as for any other Django application. You need,\
+ \ at a minimum, the following in your settings.py:
\r\n\r\nEMAIL_HOST\r\
+ \nEMAIL_PORT\r\n \r\nIf your email server\
+ \ requires authentication for SMTP, you must also set:
\r\n\r\nEMAIL_HOST_USER\r\
+ \nEMAIL_HOST_PASSWORD\r\n \r\n\r\nFinally ,\
+ \
EMAIL_USE_TLS controls whether or not a secure connection is used.\
+ \ For more information on how to send mail using\
+ \ Django , see the official documentation .\r\n\r\nWe recommend using Google Apps for your email service (it's what we use)."}
+ model: docs.page
+ pk: 16
+- fields: {content: "Billing and Pricing \r\n\r\n\r\n \r\nWhy\
+ \ do you need my billing information? \r\n \r\nDjangy only needs\
+ \ your billing information so we can charge you for premium features. These\
+ \ premium features include things like more performance workers, background\
+ \ jobs, and dedicated databases. Running a single app instance on Djangy will\
+ \ always be free.
\r\n\r\n \r\nHow do you prorate\
+ \ charges? \r\n \r\nWorker processes are prorated per second at\
+ \ a rate of $0.05/hr. Please note that we'll charge you for how many workers\
+ \ you have allocated -- not the amount of activity per process.
\r\n\r\n\
+ \r\nHow do I delete my account? \r\n\
+ \r\nTo delete your Djangy account, send an email to support@djangy.com . Please be sure to email us from the address associated\
+ \ with your Djangy account.
", name: FAQBillingPricing, rendered: "\r\n\r\n\r\n Why do you need my billing information? \r\n How do you prorate charges? \r\
+ \n How \
+ \ do I delete my account? \r\n \r\n \r\nWhy do you need my billing information? \r\
+ \n \r\nDjangy only needs\
+ \ your billing information so we can charge you for premium features. These premium features include things like more\
+ \ performance workers, background jobs, and dedicated databases. Running a single app instance on Djangy will always be free.
\r\n\r\n \r\nHow do you prorate\
+ \ charges? \r\n \r\nWorker \
+ \ processes are prorated per second at a rate of $0.05/hr. Please note that we'll charge you for how many workers you have allocated\
+ \ -- not the amount of activity per process.
\r\n\r\n \r\nHow do I delete my account? \r\
+ \n \r\nTo delete your Djangy account, send an email to support@djangy.com . Please be sure to email us from the address associated with your Djangy account.
"}
+ model: docs.page
+ pk: 31
+- fields: {content: "Installing dependencies \r\n\r\nPython offers a wide\
+ \ range of installable packages. Djangy offers native support for automatically\
+ \ installing dependencies using easy_install or pip . All\
+ \ you have to do is create a file called \"djangy.eggs \" (for Python\
+ \ eggs, installed using easy_install ) or \"djangy.pip \"\
+ \ (for Python source packages, installed using pip ) in the root\
+ \ directory of your git repository, containing a list of packages you would\
+ \ like Djangy to install.
\r\n\r\nIf you don't create a djangy.pip \
+ \ or djangy.eggs file, Djangy will assume that your application's only\
+ \ dependencies are Python iself, Django , and South .
\r\n\
+ \r\ndjangy.pip file format \r\n\r\nThe optional djangy.pip \
+ \ file is used to specify Python source packages to install. It follows the\
+ \ pip \
+ \ requirements file format .
\r\n\r\nIn general, you can list any Python\
+ \ source package that is available on PyPI . There are over 12,000 packages available on PyPI.
\r\n\
+ \r\ndjangy.eggs file format \r\n\r\nThe optional djangy.eggs \
+ \ file in the root directory of your git repository should list one egg requirement\
+ \ on each line. By default, djangy create will automatically create\
+ \ a minimal djangy.eggs file in the correct location.
\r\n\r\nFor\
+ \ example, the following example requires Django (version 1.2 or later), South,\
+ \ Mako, a custom package my_package.egg stored in a directory named\
+ \ local_eggs/ in your git repository, and another custom package located\
+ \ at http://djangy.com/example.egg (note: this file doesn't actually\
+ \ exist):
\r\n\r\nDjango>=1.2\r\nSouth\r\nMako\r\n./local_eggs/my_package.egg\r\
+ \nhttp://djangy.com/example.egg \r\n\r\nNote that we explicitly included\
+ \ Django and South in the list. If you\r\nhave a djangy.eggs file\
+ \ but it doesn't list Django or South, those\r\npackages will not be\
+ \ installed.
\r\n\r\nIn general, you can list any Python egg that is available\
+ \ on PyPI . Django, South, and Mako\
+ \ are just a few examples; there are over 12,000 packages available on\
+ \ PyPI.
\r\n\r\nYou can also construct more elaborate egg requirements\
+ \ if you'd like;\r\neach line is the same as the requirement_or_url \
+ \ you'd pass to\r\neasy_install on your local development workstation.\
+ \ For example,\r\nyou can use this to force installation of a custom version\
+ \ of Django or\r\nSouth or any other package. Or you can use a URL to force\
+ \ installation of a Python egg which isn't available on PyPI.
\r\n", name: InstallingDependencies,
+ rendered: "\r\
+ \n\r\nPython offers a wide range\
+ \ of installable packages. Djangy \
+ \ offers native support for automatically installing dependencies using easy_install \
+ \ or pip . All you have to\
+ \ do is create a file called \"djangy.eggs \" (for Python eggs, installed using easy_install ) or \"djangy.pip \"\
+ \ (for Python source packages, \
+ \ installed using pip ) in the root directory of your git repository,\
+ \ containing a list of packages you would like Djangy to install.
\r\n\r\nIf \
+ \ you don't create a djangy.pip or djangy.eggs file, Djangy will assume that your application's\
+ \ only dependencies are Python iself,\
+ \ Django , and South .
\r\n\r\ndjangy.pip file format \r\
+ \n\r\nThe optional djangy.pip \
+ \ file is used to specify Python \
+ \ source packages to install. It follows\
+ \ the pip \
+ \ requirements file format .
\r\n\r\nIn general, you can list any Python \
+ \ source package that is available on PyPI . There are over 12,000 \
+ \ packages available on PyPI.
\r\n\r\ndjangy.eggs file format \r\n\
+ \r\nThe optional djangy.eggs \
+ \ file in the root directory of your git repository should list one egg requirement\
+ \ on each line. By default, djangy\
+ \ create will automatically create a minimal djangy.eggs file\
+ \ in the correct location.
\r\n\r\nFor example, the following example requires Django (version 1.2 or later), South ,\
+ \ Mako , a custom package my_package.egg \
+ \ stored in a directory named local_eggs/ in your git repository, and\
+ \ another custom package located at http://djangy.com/example.egg (note:\
+ \ this file doesn't actually exist):
\r\n\r\nDjango >=1.2\r\nSouth \r\nMako \r\n./local_eggs/my_package.egg\r\nhttp://djangy.com/example.egg \r\
+ \n\r\nNote that we explicitly included\
+ \ Django and South in the list. If you\r\nhave\
+ \ a djangy.eggs file but it doesn't list Django or South , those\r\npackages\
+ \ will not be installed.
\r\n\r\nIn general, you can list any Python \
+ \ egg that is available on PyPI .\
+ \ Django , South , and Mako are just a few\
+ \ examples; there are over 12,000 packages available on PyPI.
\r\n\
+ \r\nYou can also construct more elaborate\
+ \ egg requirements if you'd like;\r\neach line is the same as the requirement_or_url \
+ \ you'd pass to\r\neasy_install on your local development workstation.\
+ \ For example,\r\nyou can use this\
+ \ to force installation of a custom version of Django or\r\nSouth or any other\
+ \ package. Or you can use a URL to force\
+ \ installation of a Python egg which\
+ \ isn't available on PyPI.
\r\n"}
+ model: docs.page
+ pk: 18
+- fields: {content: "Logging \r\n \r\nThere are two options for logging\
+ \ in your Djangy app:\r\n
\r\n\r\n \r\n \r\nStandard\
+ \ Logging \r\n \r\nTo use standard logging from your applications,\
+ \ use the following code as a template:\r\n
\r\nimport logging\r\nlogger\
+ \ = logging.getLogger('djangy')\r\nlogger.setLevel(logging.DEBUG) # the default\
+ \ is logging.INFO\r\n\r\ndef myview(request):\r\n logger.error(\"my error\
+ \ message\")\r\n return \"Hello, world!\"\r\n \r\nTo access your logs,\
+ \ run the following command:\r\n\r\n$ djangy logs\r\n \r\nfrom inside\
+ \ your application's repository. Alternatively, you can find the \"View logs\"\
+ \ button from your application's web dashboard.\r\nNOTE: This is a limited\
+ \ logging facility. It will only show you the last 100 lines of your application's\
+ \ log. For a better logging solution, we recommend using django-sentry .
\r\n \r\n \r\nUsing\
+ \ django-sentry for enhanced logging \r\n \r\nIf you require more\
+ \ sophisticated logging, we recommend using django-sentry . To get sentry working for your application\
+ \ (you'll be able to access it at the \"/sentry\" URL), add the following to\
+ \ your djangy.eggs or djangy.pip file:
\r\n\r\n\r\ndjango-indexer\r\n\
+ django-paging\r\ndjango-sentry\r\n \r\n\r\nThen, add the \"sentry\"\
+ , \"sentry.client\", \"indexer\", and \"paging\" to your INSTALLED_APPS in your\
+ \ settings.py:
\r\n\r\n\r\nINSTALLED_APPS = [\r\n...\r\n 'sentry',\r\
+ \n 'sentry.client',\r\n 'indexer',\r\n 'paging'\r\n]\r\n \r\n\r\
+ \nFinally, add the sentry URLs module to your urls.py:
\r\n\r\nurlpatterns\
+ \ = patterns('',\r\n ...\r\n (r'^sentry/', include('sentry.urls')),\r\n\
+ )\r\n \r\n\r\nSentry will catch exceptions and log them. If you'd like\
+ \ to log additional messages to sentry, use the following code as a template:
\r\
+ \n\r\nimport logging\r\nfrom sentry.client.handlers import SentryHandler\r\
+ \n\r\nlogging.getLogger().addHandler(SentryHandler())\r\n\r\n# Add StreamHandler\
+ \ to sentry's default so you can catch missed exceptions\r\nlogger = logging.getLogger('sentry.errors')\r\
+ \nlogger.propagate = False\r\nlogger.addHandler(logging.StreamHandler())\r\n\
+ \r\nlogger.error(\"my error message\")\r\n \r\n\r\nFor more information,\
+ \ see the official documentation .
", name: Logging, rendered: "\r\n \r\nThere are two options for logging in your Djangy app:\r\n
\r\
+ \n\r\n \r\n \r\n\r\n \r\nTo use standard logging from\
+ \ your applications, use the following code as a template:\r\n
\r\nimport\
+ \ logging\r\nlogger = logging.getLogger('djangy')\r\nlogger.setLevel(logging.DEBUG)\
+ \ # the default is logging.INFO\r\n\r\ndef myview(request):\r\n logger.error(\"\
+ my error message\")\r\n return \"Hello ,\
+ \ world!\"\r\n \r\nTo access your\
+ \ logs, run the following command:\r\n\r\n$ djangy logs\r\n \r\nfrom\
+ \ inside your application's repository. Alternatively , you can find the \"View logs\" button from your application's web dashboard.\r\nNOTE:\
+ \ This is a limited logging facility.\
+ \ It will only show you the last 100\
+ \ lines of your application's log. For \
+ \ a better logging solution, we recommend using django-sentry .
\r\
+ \n \r\n \r\nUsing django-sentry for enhanced logging \r\n \r\nIf you require more sophisticated logging, we recommend\
+ \ using django-sentry . To get sentry working\
+ \ for your application (you'll be able to access it at the \"/sentry\" URL),\
+ \ add the following to your djangy.eggs or djangy.pip file:
\r\n\r\n\r\
+ \ndjango-indexer\r\ndjango-paging\r\ndjango-sentry\r\n \r\n\r\nThen , add the \"sentry\", \"sentry.client\",\
+ \ \"indexer\", and \"paging\" to your INSTALLED_APPS in your settings.py:
\r\
+ \n\r\n\r\nINSTALLED_APPS = [\r\n...\r\n 'sentry',\r\n 'sentry.client',\r\
+ \n 'indexer',\r\n 'paging'\r\n]\r\n \r\n\r\nFinally , add the sentry URLs module\
+ \ to your urls.py:
\r\n\r\nurlpatterns = patterns('',\r\n ...\r\n\
+ \ (r'^sentry/', include('sentry.urls')),\r\n)\r\n \r\n\r\nSentry will catch exceptions and log them.\
+ \ If you'd like to log additional messages\
+ \ to sentry, use the following code as a template:
\r\n\r\nimport logging\r\
+ \nfrom sentry.client.handlers import SentryHandler \r\n\r\nlogging.getLogger().addHandler(SentryHandler ())\r\n\r\n# Add StreamHandler to sentry's default\
+ \ so you can catch missed exceptions\r\nlogger = logging.getLogger('sentry.errors')\r\
+ \nlogger.propagate = False \r\nlogger.addHandler(logging.StreamHandler ())\r\n\r\nlogger.error(\"\
+ my error message\")\r\n \r\n\r\nFor more information, see the official documentation .
"}
+ model: docs.page
+ pk: 17
+- fields: {content: "Manage.py Commands \r\n \r\nThe Djangy command-line client supports running manage.py commands remotely.\
+ \ The behavior is designed to be identical to what you'd expect when you run\
+ \ the commands locally, except that everything happens within the context of\
+ \ your live Djangy application.
\r\n\r\n \r\n \r\nUsage \r\n \r\nExecuting remote manage.py\
+ \ commands is identical to executing them locally, except you pass everything\
+ \ to the djangy command-line client instead:\r\n
\r\n$ djangy manage.py\
+ \ \r\n \r\nAs the ability to implement your own commands is supported,\
+ \ it would be impossible to list each supported command. Instead, you'll find\
+ \ the following commands offically supported by Djangy (others will almost certainly\
+ \ work, but we can't make any guarantees):\r\n\r\n syncdb \r\
+ \n migrate \r\n shell \r\n loaddata \r\
+ \n dumpdata \r\n migrate \r\n reset \r\
+ \n \r\n\r\n \r\n \r\nExceptions \r\
+ \n \r\nThere are a few manage.py commands that don't make sense to execute\
+ \ remotely. For security and logical reasons the following commands have been\
+ \ disallowed:\r\n
\r\n changepassword \r\n compilemessages \r\
+ \n dbshell \r\n makemessages \r\n runfcgi \r\
+ \n runserver \r\n schemamigration \r\n \
+ \ datamigration \r\n test \r\n testserver \r\
+ \n \r\nIn general, it doesn't make sense to execute a remote command that\
+ \ changes your code in some way -- for example, schemamigration or makemessages.\
+ \ Running the development server is obviously disallowed for performance and\
+ \ security reasons.\r\n", name: ManagePyCommands, rendered: "\r\n \r\nThe \
+ \ Client \">Djangy command-line client supports running\
+ \ manage.py commands remotely. The \
+ \ behavior is designed to be identical to what you'd expect when you run the\
+ \ commands locally, except that everything happens within the context of your\
+ \ live Djangy application.
\r\n\
+ \r\n \r\n \r\n\r\n \r\nExecuting \
+ \ remote manage.py commands is identical to executing them locally, except you\
+ \ pass everything to the djangy command-line client instead:\r\n
\r\n$ djangy\
+ \ manage.py \r\n \r\nAs \
+ \ the ability to implement your own commands is supported, it would be impossible\
+ \ to list each supported command. Instead ,\
+ \ you'll find the following commands offically supported by Djangy (others will almost certainly work, but we can't make any guarantees):\r\
+ \n\r\n syncdb \r\n migrate \r\n shell \r\
+ \n loaddata \r\n dumpdata \r\n migrate \r\
+ \n reset \r\n \r\n\r\n \r\n \r\n\r\
+ \n \r\nThere are a few manage.py\
+ \ commands that don't make sense to execute remotely. For security and logical reasons the following commands have been disallowed:\r\
+ \n
\r\n changepassword \r\n compilemessages \r\
+ \n dbshell \r\n makemessages \r\n runfcgi \r\
+ \n runserver \r\n schemamigration \r\n \
+ \ datamigration \r\n test \r\n testserver \r\
+ \n \r\nIn general, it doesn't make\
+ \ sense to execute a remote command that changes your code in some way -- for\
+ \ example, schemamigration or makemessages. Running the development server is obviously disallowed for performance\
+ \ and security reasons.\r\n"}
+ model: docs.page
+ pk: 27
+- fields: {content: " \r\n Getting Started \r\n \r\
+ \n FAQ \r\n \r\n Using Djangy \r\n \r\n How To \r\n \r\n ", name: NavBar, rendered: " "}
+ model: docs.page
+ pk: 13
+- fields: {content: 'This document has been moved to What
+ is Djangy? .
', name: Overview, rendered: 'This
+ document has been moved to WhatIsDjangy ">What is Djangy ?.
'}
+ model: docs.page
+ pk: 2
+- fields: {content: "Djangy Quickstart Guide \r\n \r\nDjangy is the\
+ \ best way to deploy your applications. Develop your app locally like always,\
+ \ then use the djang client to deploy to Djangy's cloud infrastructure. It's\
+ \ that simple!\r\n
\r\n\r\n \r\n \r\nPrerequisites \r\n \r\nTo use Djangy, you need three\
+ \ things:\r\n
\r\n\r\n \r\n \r\nDeploying to Djangy \r\n \r\
+ \n1. Ensure your application is in a valid Git repository \r\n \r\
+ \nIf you don't already use git, run the following commands (replacing \"\
+ myapp\" with your own application's name, of course):
\r\n\r\n$ cd myapp\r\
+ \n$ git init\r\nInitialized empty Git repository in .git/\r\n$ git add .\r\n\
+ $ git commit -m \"my application\"\r\nCreated initial commit 3a9245c: my application\r\
+ \n21 files changed, 2142 insertions(+), 0 deletions(-)\r\n \r\n2. Ensure\
+ \ you have a public SSH key \r\n \r\nGit uses SSH to push and pull\
+ \ changes, so you need to have a public key. Follow these instructions for\
+ \ OS X ,\
+ \ Windows ,\
+ \ or Linux .
\r\
+ \n3. Create your app on Djangy \r\n \r\nFrom inside your application's\
+ \ git repository, run \"djangy create\". When prompted, enter your Djangy username\
+ \ and password. They are saved into ~/.djangy for future runs. The client\
+ \ will also upload your public SSH key so that you can interact with the newly\
+ \ created \"djangy\" remote.
\r\n\r\n$ cd myapp\r\n$ djangy create\r\
+ \nUsing git repository \"/Users/dave/projects/myapp\"\r\n\r\nEnter your email\
+ \ address: dave@djangy.com\r\nPlease enter your password: \r\n \r\nSaved credentials.\r\
+ \nTo change your email address or password, remove \"/Users/dave/.djangy\"\r\
+ \n\r\nPlease enter your application name [Enter for myapp]: \r\n \r\nUsing application\
+ \ name \"myapp\" from user input\r\nUsing public key file \"/Users/dave/.ssh/id_rsa.pub\"\
+ \r\nApplication created.\r\n[master 83b657d] added \"djangy.config\", \"djangy.eggs\"\
+ \ and \"djangy.pip\" to repository\r\n 2 files changed, 5 insertions(+), 0 deletions(-)\r\
+ \n create mode 100644 djangy.config\r\n create mode 100644 djangy.eggs\r\n create\
+ \ mode 100644 djangy.pip\r\n\r\nYou can now run \"git push djangy master\"\r\
+ \n \r\nYou can see that djangy added three files (djangy.config, djangy.eggs,\
+ \ and djangy.pip) to your repository. Everything is ready to go. If you'd\
+ \ like, you can customize the\
+ \ configuration files .
\r\n\r\n4. Push your application to\
+ \ Djangy \r\n \r\n\r\n$ git push djangy master\r\n\r\nCounting\
+ \ objects: 10, done.\r\nDelta compression using up to 2 threads.\r\nCompressing\
+ \ objects: 100% (8/8), done.\r\nWriting objects: 100% (10/10), 2.67 KiB, done.\r\
+ \nTotal 10 (delta 1), reused 0 (delta 0)\r\n\r\n\r\nWelcome to Djangy!\r\n\r\
+ \nDeploying project myapp.\r\n\r\nCloning git repository... Done.\r\n\r\nCreating\
+ \ production settings.py file... Done.\r\n\r\nInstalling dependencies...\r\n\
+ \ Dependencies from djangy.eggs using easy_install:\r\n Installing Django...\
+ \ Success.\r\n Installing South... Success.\r\n Installing gunicorn...\
+ \ Success.\r\n Dependencies from djangy.pip using pip:\r\n None found.\r\
+ \nDone.\r\n\r\nSaving bundle info... Done.\r\n\r\nDeploying to worker hosts...\
+ \ Done.\r\n\r\nTo git@api.djangy.com:myapp.git\r\n * [new branch] master\
+ \ -> master\r\n \r\n \r\n5. Sync your models \r\n \r\n\
+ Your app is now up and running on Djangy. However, it still has an empty\
+ \ database. To fix that, run the usual syncdb command:
\r\n\r\n$ djangy\
+ \ manage.py syncdb\r\n\r\nUsing git repository \"/Users/dave/myapp\"\r\nUsing\
+ \ application name \"myapp\" from \"/Users/dave/myapp/djangy.config\"\r\n\r\n\
+ Creating table auth_permission\r\nCreating table auth_group_permissions\r\n\
+ Creating table auth_group\r\nCreating table auth_user_user_permissions\r\nCreating\
+ \ table auth_user_groups\r\nCreating table auth_user\r\nCreating table auth_message\r\
+ \nCreating table django_content_type\r\nCreating table django_session\r\nCreating\
+ \ table django_site\r\n\r\nYou just installed Django's auth system, which means\
+ \ you don't have any \r\nsuperusers defined. Would you like to create one now?\
+ \ (yes/no): yes\r\nUsername (Leave blank to use 'djangy'): dave \r\
+ \nE-mail address: dave@djangy.com\r\nPassword:\r\nPassword (again):\r\n\r\n\
+ Superuser created successfully.\r\nInstalling index for auth.Permission model\r\
+ \nInstalling index for auth.Group_permissions model\r\nInstalling index for\
+ \ auth.User_user_permissions model\r\nInstalling index for auth.User_groups\
+ \ model\r\nInstalling index for auth.Message model\r\nNo fixtures found.\r\n\
+ \r\nAs you can see, this behavior is very familiar. In fact, most\
+ \ manage.py commands will work just as expected, including South migrations.\
+ \ For more info, see Manage.py\
+ \ Commands .
\r\n \r\n \r\nEpilogue \r\
+ \n \r\nCongratulations, your app is now up and running on Djangy! Your\
+ \ workflow is now incredibly simple. Any time you make any changes, you need\
+ \ to simply:\r\n
\r\n Commit your changes to git \r\n Push\
+ \ your changes to Djangy with \"git push djangy master\" \r\n \r\nAs\
+ \ always, you can email support@djangy.com \
+ \ if you have any questions, problems, or suggestions. We welcome your feedback!
",
+ name: Quickstart, rendered: "\r\n \r\nDjangy \
+ \ is the best way to deploy your applications. Develop your app locally like always, then use the djang client to deploy\
+ \ to Djangy 's cloud infrastructure.\
+ \ It 's that simple!\r\n
\r\n\r\n \r\n \r\n\r\n \r\nTo use Djangy , you need three things:\r\n
\r\n\r\n \r\n \r\n\r\n \r\n1.\
+ \ Ensure your application is in a\
+ \ valid Git repository \r\n \r\
+ \nIf you don't already use git, run\
+ \ the following commands (replacing \"myapp\" with your own application's name,\
+ \ of course):
\r\n\r\n$ cd myapp\r\n$ git init\r\nInitialized empty Git repository\
+ \ in .git/\r\n$ git add .\r\n$ git commit -m \"my application\"\r\nCreated initial commit 3a9245c: my application\r\
+ \n21 files changed, 2142 insertions(+), 0 deletions(-)\r\n \r\n2. Ensure you have a public SSH key \r\
+ \n \r\nGit uses SSH to push\
+ \ and pull changes, so you need to have a public key. Follow these instructions for OS X , Windows ,\
+ \ or Linux .
\r\n\r\n \r\nFrom \
+ \ inside your application's git repository, run \"djangy create\". When prompted, enter your Djangy username and password. They \
+ \ are saved into ~/.djangy for future runs. The client will also upload your public SSH key so that you can interact\
+ \ with the newly created \"djangy\" remote.
\r\n\r\n$ cd myapp\r\n$\
+ \ djangy create\r\nUsing git repository\
+ \ \"/Users /dave/projects/myapp\"\r\
+ \n\r\nEnter your email address: dave@djangy.com\r\
+ \nPlease enter your password: \r\n\
+ \ \r\nSaved credentials.\r\nTo change your email address or password, remove\
+ \ \"/Users /dave/.djangy\"\r\n\r\n\
+ Please enter your application name\
+ \ [Enter for myapp]: \r\n \r\nUsing application name \"myapp\" from user input\r\
+ \nUsing public key file \"/Users /dave/.ssh/id_rsa.pub\"\r\nApplication created.\r\n[master 83b657d] added \"djangy.config\", \"djangy.eggs\"\
+ \ and \"djangy.pip\" to repository\r\n 2 files changed, 5 insertions(+), 0 deletions(-)\r\
+ \n create mode 100644 djangy.config\r\n create mode 100644 djangy.eggs\r\n create\
+ \ mode 100644 djangy.pip\r\n\r\nYou \
+ \ can now run \"git push djangy master\"\r\n \r\nYou can see that djangy added three files (djangy.config, djangy.eggs,\
+ \ and djangy.pip) to your repository. Everything is ready to go. If you'd\
+ \ like, you can ConfigFiles \" target=\"_blank\">customize the configuration files.
\r\
+ \n\r\n4. Push your application\
+ \ to Djangy \r\n \r\n\r\
+ \n$ git push djangy master\r\n\r\nCounting \
+ \ objects: 10, done.\r\nDelta compression\
+ \ using up to 2 threads.\r\nCompressing \
+ \ objects: 100% (8/8), done.\r\nWriting \
+ \ objects: 100% (10/10), 2.67 KiB, done.\r\nTotal 10 (delta 1), reused 0 (delta 0)\r\n\r\n\r\nWelcome to Djangy !\r\n\r\nDeploying project myapp.\r\n\r\n\
+ Cloning git repository... Done .\r\n\r\nCreating production settings.py file... Done .\r\n\r\nInstalling \
+ \ dependencies...\r\n Dependencies \
+ \ from djangy.eggs using easy_install:\r\n Installing Django ... Success .\r\n Installing South ... Success .\r\n Installing gunicorn... Success .\r\
+ \n Dependencies from djangy.pip\
+ \ using pip:\r\n None found.\r\n\
+ Done .\r\n\r\nSaving bundle info... Done .\r\n\
+ \r\nDeploying to worker hosts...\
+ \ Done .\r\n\r\nTo git@api.djangy.com:myapp.git\r\n * [new branch] master -> master\r\
+ \n \r\n \r\n5. Sync your\
+ \ models \r\n \r\nYour \
+ \ app is now up and running on Djangy .\
+ \ However , it still has an empty\
+ \ database. To fix that, run the usual\
+ \ syncdb command:
\r\n\r\n$ djangy manage.py syncdb\r\n\r\nUsing git repository \"/Users /dave/myapp\"\r\nUsing application\
+ \ name \"myapp\" from \"/Users /dave/myapp/djangy.config\"\
+ \r\n\r\nCreating table auth_permission\r\
+ \nCreating table auth_group_permissions\r\
+ \nCreating table auth_group\r\n\
+ Creating table auth_user_user_permissions\r\
+ \nCreating table auth_user_groups\r\
+ \nCreating table auth_user\r\n\
+ Creating table auth_message\r\n\
+ Creating table django_content_type\r\
+ \nCreating table django_session\r\
+ \nCreating table django_site\r\n\
+ \r\nYou just installed Django 's auth system, which means you don't have any \r\nsuperusers defined.\
+ \ Would you like to create one now?\
+ \ (yes/no): yes\r\nUsername (Leave blank to use 'djangy'): dave\
+ \ \r\nE-mail address: dave@djangy.com\r\nPassword :\r\nPassword (again):\r\
+ \n\r\nSuperuser created successfully.\r\
+ \nInstalling index for auth.Permission model\r\nInstalling index for auth.Group_permissions\
+ \ model\r\nInstalling index for\
+ \ auth.User_user_permissions model\r\nInstalling index for auth.User_groups model\r\nInstalling index for auth.Message \
+ \ model\r\nNo fixtures found.\r\n \r\
+ \nAs you can see, this behavior is\
+ \ very familiar. In fact, most manage.py\
+ \ commands will work just as expected, including South migrations. For more info,\
+ \ see ManagePyCommands \"\
+ \ target=\"_blank\">Manage .py Commands .
\r\n \r\n\
+ \r\n\r\
+ \n \r\nCongratulations ,\
+ \ your app is now up and running on Djangy !\
+ \ Your workflow is now incredibly\
+ \ simple. Any time you make any changes,\
+ \ you need to simply:\r\n
\r\n Commit your changes to git \r\n Push your changes to Djangy \
+ \ with \"git push djangy master\" \r\n \r\nAs always, you can email support@djangy.com \
+ \ if you have any questions, problems, or suggestions. We welcome your feedback!
"}
+ model: docs.page
+ pk: 20
+- fields: {content: "Serving static content \r\n\r\nWeb applications consist\
+ \ of a combination of code that must run on the server and static content such\
+ \ as images, CSS files, and JavaScript files that run purely on the client web\
+ \ browser. Djangy's solution for serving static content is a custom hardened\
+ \ implementation of django.views.static.serve . Unlike the stock Django\
+ \ version of static serve, Djangy's implementation performs additional request\
+ \ validation for improved security, and automatically serves content off of\
+ \ Djangy's high-performance front end cache servers, rather than hitting your\
+ \ application for each static content request.
\r\n\r\nHere is an example\
+ \ of a simple urls.py which serves static content, assuming there is\
+ \ a subdirectory called site_media in the same directory as urls.py :
\r\
+ \n\r\n\r\nfrom django.conf.urls.defaults import *\r\n\r\nurlpatterns =\
+ \ patterns('',\r\n (r'^site_media/(?P<path>.*)$',\r\n 'django.views.static.serve',\
+ \ {'document_root': 'site_media/'}),\r\n (r'^', 'testapp.main.views.index')\r\
+ \n)\r\n \r\n\r\nFor more information on django.views.static.serve ,\
+ \ please refer to the Django documentation on serving static files . Please note that the disclaimer\
+ \ about efficiency and security in the Django documentation doesn't apply to\
+ \ Djangy's custom static content server. (You may also find other documentation\
+ \ online that describes more complicated ways to serve static content; again,\
+ \ these other approaches do not apply to Djangy.)
\r\n", name: ServingStaticContent,
+ rendered: "Serving static content \r\
+ \n\r\nWeb applications consist of\
+ \ a combination of code that must run on the server and static content such\
+ \ as images, CSS files, and JavaScript \
+ \ files that run purely on the client web browser. Djangy 's solution for serving static content is a custom hardened implementation\
+ \ of django.views.static.serve . Unlike the stock Django version\
+ \ of static serve, Djangy 's implementation\
+ \ performs additional request validation for improved security, and automatically\
+ \ serves content off of Djangy 's\
+ \ high-performance front end cache servers, rather than hitting your application\
+ \ for each static content request.
\r\n\r\nHere is an example of a simple urls.py which serves static content,\
+ \ assuming there is a subdirectory called site_media in the same directory\
+ \ as urls.py :
\r\n\r\n\r\nfrom django.conf.urls.defaults import\
+ \ *\r\n\r\nurlpatterns = patterns('',\r\n (r'^site_media/(?P<path>.*)$',\r\
+ \n 'django.views.static.serve', {'document_root': 'site_media/'}),\r\n\
+ \ (r'^', 'testapp.main.views.index')\r\n)\r\n \r\n\r\nFor more information on django.views.static.serve ,\
+ \ please refer to the Django documentation on serving\
+ \ static files. Please note\
+ \ that the disclaimer about efficiency and security in the Django documentation doesn't apply to Djangy 's custom static content server. (You may also find other documentation online that describes more complicated\
+ \ ways to serve static content; again, these other approaches do not apply to\
+ \ Djangy .)
\r\n"}
+ model: docs.page
+ pk: 14
+- fields: {content: "Serving static content \r\n\r\nWeb applications consist\
+ \ of a combination of code that must run on the server and static content such\
+ \ as images, CSS files, and JavaScript files that run purely on the client web\
+ \ browser. Djangy's solution for serving static content is a custom hardened\
+ \ implementation of django.views.static.serve . Unlike the stock Django\
+ \ version of static serve, Djangy's implementation performs additional request\
+ \ validation for improved security, and automatically serves content off of\
+ \ Djangy's high-performance front end cache servers, rather than hitting your\
+ \ application for each static content request.
\r\n\r\nHere is an example\
+ \ of a simple urls.py which serves static content, assuming there is\
+ \ a subdirectory called site_media in the same directory as urls.py :
\r\
+ \n\r\n\r\nfrom django.conf.urls.defaults import *\r\n\r\nurlpatterns =\
+ \ patterns('',\r\n (r'^site_media/(?P<path>.*)$',\r\n 'django.views.static.serve',\
+ \ {'document_root': 'site_media/'}),\r\n (r'^', 'testapp.main.views.index')\r\
+ \n)\r\n \r\n\r\nFor more information on django.views.static.serve ,\
+ \ please refer to the Django documentation on serving static files . Please note that the disclaimer\
+ \ about efficiency and security in the Django documentation doesn't apply to\
+ \ Djangy's custom static content server. (You may also find other documentation\
+ \ online that describes more complicated ways to serve static content; again,\
+ \ these other approaches do not apply to Djangy.)
\r\n", name: StaticContent,
+ rendered: "Serving static content \r\
+ \n\r\nWeb applications consist of\
+ \ a combination of code that must run on the server and static content such\
+ \ as images, CSS files, and JavaScript \
+ \ files that run purely on the client web browser. Djangy 's solution for serving static content is a custom hardened implementation\
+ \ of django.views.static.serve . Unlike the stock Django version\
+ \ of static serve, Djangy 's implementation\
+ \ performs additional request validation for improved security, and automatically\
+ \ serves content off of Djangy 's\
+ \ high-performance front end cache servers, rather than hitting your application\
+ \ for each static content request.
\r\n\r\nHere is an example of a simple urls.py which serves static content,\
+ \ assuming there is a subdirectory called site_media in the same directory\
+ \ as urls.py :
\r\n\r\n\r\nfrom django.conf.urls.defaults import\
+ \ *\r\n\r\nurlpatterns = patterns('',\r\n (r'^site_media/(?P<path>.*)$',\r\
+ \n 'django.views.static.serve', {'document_root': 'site_media/'}),\r\n\
+ \ (r'^', 'testapp.main.views.index')\r\n)\r\n \r\n\r\nFor more information on django.views.static.serve ,\
+ \ please refer to the Django documentation on serving\
+ \ static files. Please note\
+ \ that the disclaimer about efficiency and security in the Django documentation doesn't apply to Djangy 's custom static content server. (You may also find other documentation online that describes more complicated\
+ \ ways to serve static content; again, these other approaches do not apply to\
+ \ Djangy .)
\r\n"}
+ model: docs.page
+ pk: 29
+- fields: {content: "Storing File uploads with S3 \r\n \r\nWe recommend\
+ \ using django-storages along with Amazon's S3 service \
+ \ to handle file uploads.
\r\n\r\n \r\n \r\nSpecifying dependencies \r\n \r\nAdd the following\
+ \ to your djangy.pip file:\r\n
\r\nboto\r\nhg+http://code.welldev.org/django-storage\r\
+ \n \r\nboto is Amazon's S3 backend storage library, which we'll\
+ \ configure django-storages to use.\r\n\r\n \r\n \r\nRequired settings \r\n \r\nFirst, you need\
+ \ to add the 'storages' app to your INSTALLED_APPS (in settings.py ):\r\
+ \n
\r\nINSTALLED_APPS = (\r\n ...\r\n 'storages',\r\n)\r\n \r\
+ \nNow add the following to the end of your settings.py (replacing the\
+ \ appropriate values):\r\n\r\nDEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage'\r\
+ \nAWS_ACCESS_KEY_ID = 'REPLACE ME'\r\nAWS_SECRET_ACCESS_KEY = 'REPLACE ME'\r\
+ \nAWS_STORAGE_BUCKET_NAME = 'REPLACE ME'\r\nfrom S3 import CallingFormat\r\n\
+ AWS_CALLING_FORMAT = CallingFormat.SUBDOMAIN\r\n \r\n\r\n \r\n\
+ \r\nUsage \r\n \r\nNow all you need to\
+ \ do is use Django's built-in FileField in your models, like so:
\r\n\r\
+ \nfrom django.db import models\r\n\r\nclass MyModel(models.Model):\r\n data\
+ \ = models.FileField(upload_to='sub-bucket-name')\r\n \r\nThat's it!\
+ \ All uploads that happen through the MyModel.data field will be put\
+ \ into your Amazon S3 bucket automatically. For more information on using Django's\
+ \ FileField, see the Official documentation .
", name: StoringUploads,
+ rendered: "\r\n \r\nWe recommend\
+ \ using django-storages along with Amazon 's S3 service to handle\
+ \ file uploads.
\r\n\r\
+ \n \r\n \r\n\r\n \r\nAdd the following to your djangy.pip file:\r\n
\r\nboto\r\n\
+ hg+http://code.welldev.org/django-storage\r\n \r\nboto is Amazon 's S3 backend storage library, which we'll configure django-storages \
+ \ to use.\r\n\r\n \r\n \r\n\r\n \r\nFirst , you need to add the 'storages' app to your INSTALLED_APPS \
+ \ (in settings.py ):\r\n
\r\nINSTALLED_APPS = (\r\n ...\r\n \
+ \ 'storages',\r\n)\r\n \r\nNow \
+ \ add the following to the end of your settings.py (replacing the appropriate\
+ \ values):\r\n\r\nDEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage '\r\nAWS_ACCESS_KEY_ID\
+ \ = 'REPLACE ME'\r\nAWS_SECRET_ACCESS_KEY = 'REPLACE ME'\r\nAWS_STORAGE_BUCKET_NAME\
+ \ = 'REPLACE ME'\r\nfrom S3 import CallingFormat \r\nAWS_CALLING_FORMAT\
+ \ = CallingFormat .SUBDOMAIN\r\
+ \n \r\n\r\n \r\n \r\n\r\n \r\nNow \
+ \ all you need to do is use Django 's\
+ \ built-in FileField in your models,\
+ \ like so:
\r\n\r\nfrom django.db import models\r\n\r\nclass MyModel (models.Model ):\r\n data = models.FileField (upload_to='sub-bucket-name')\r\n \r\nThat 's it! All uploads that happen\
+ \ through the MyModel .data \
+ \ field will be put into your Amazon \
+ \ S3 bucket automatically. For more information on using Django 's FileField , see the\
+ \ Official documentation.
"}
+ model: docs.page
+ pk: 32
+- fields: {content: "Configuring Django TEMPLATE_DIRS \r\n\r\nA key feature\
+ \ of Django is its powerful templating system, which allows you to separate\
+ \ HTML markup and styling from the logic of your web application. The general\
+ \ idea is a Django developer creates a set of HTML, CSS, etc. template files\
+ \ with \"holes\" in them, that can then be filled in by Python code. Django\
+ \ needs to know where these template files are located in order to access them\
+ \ from Python.
\r\n\r\nThere are three recommended ways to configure the\
+ \ location of your templates when using Djangy. These are not Djangy-specific\
+ \ quirks, but rather best practices for portable Django applications.
\r\n\
+ \r\nOption 1: implicit configuration \r\nPut templates in a directory\
+ \ called templates , which is a subdirectory of a Django app directory.\
+ \ For example, you might have a directory tree that looks like this:
\r\n\
+ \r\n\r\ntestapp/\r\n djangy.config\r\n djangy.eggs\r\n manage.py\r\
+ \n settings.py\r\n testapp/\r\n __init__.py\r\n models.py\r\
+ \n templates/ \r\n template files go here \r\n\
+ \ views.py\r\n \r\n\r\nOption 2: explicit relative paths \r\
+ \nUnless you call os.chdir() , your Djangy application will run from\
+ \ the directory containing your settings.py file. As such, your settings.py \
+ \ file may contain a TEMPLATE_DIRS section listing paths relative\
+ \ to the directory containing settings.py .
\r\n\r\nFor example,\
+ \ if we assume the directory layout above, the corresponding TEMPLATE_DIRS \
+ \ section in settings.py would be:
\r\n\r\n\r\nTEMPLATE_DIRS\
+ \ = (\r\n 'testapp/templates'\r\n)\r\n \r\n\r\nNow, if you wanted\
+ \ to move the templates to some other directory, you'd just have to update settings.py\
+ \ to point to that directory. Similarly, you could add a list of several different\
+ \ directories containing groups of template files.
\r\n\r\nOption 3:\
+ \ explicit absolute paths \r\nIf you're having trouble accessing your\
+ \ templates even after configuring explicit relative paths, you might consider\
+ \ using absolute paths instead. However, a hardcoded absolute path is not a\
+ \ good idea because it means your application will only work if it is installed\
+ \ in a specific directory. The solution is to use Python's os.path \
+ \ module to build an absolute path from a relative path and the __file__ \
+ \ variable, like so:
\r\n\r\n\r\nimport os.path\r\n\r\nTEMPLATE_DIRS\
+ \ = (\r\n os.path.join(os.path.dirname(__file__), 'testapp/templates')\r\n\
+ )\r\n \r\n", name: TemplateDirs, rendered: "\r\n\
+ \r\nA key feature of Django is its powerful\
+ \ templating system, which allows you to separate HTML markup and styling from\
+ \ the logic of your web application. The general\
+ \ idea is a Django developer creates a set of\
+ \ HTML, CSS, etc. template files with \"holes\" in them, that can then be filled\
+ \ in by Python code. Django needs to know where these template files are located in order to\
+ \ access them from Python .
\r\n\r\nThere are three recommended ways to configure the location\
+ \ of your templates when using Djangy . These are not Djangy -specific\
+ \ quirks, but rather best practices for portable Django \
+ \ applications.
\r\n\r\nOption 1: implicit\
+ \ configuration \r\nPut templates in a directory\
+ \ called templates , which is a subdirectory of a Django app directory. For example, you might\
+ \ have a directory tree that looks like this:
\r\n\r\n\r\ntestapp/\r\
+ \n djangy.config\r\n djangy.eggs\r\n manage.py\r\n settings.py\r\
+ \n testapp/\r\n __init__.py\r\n models.py\r\n templates/ \r\
+ \n template files go here \r\n views.py\r\n \r\n\
+ \r\nOption 2: explicit relative paths \r\
+ \nUnless you call os.chdir() , your\
+ \ Djangy application will run from the directory\
+ \ containing your settings.py file. As such,\
+ \ your settings.py file may contain a TEMPLATE_DIRS section\
+ \ listing paths relative to the directory containing settings.py .
\r\
+ \n\r\nFor example, if we assume the directory\
+ \ layout above, the corresponding TEMPLATE_DIRS section in settings.py \
+ \ would be:
\r\n\r\n\r\nTEMPLATE_DIRS = (\r\n 'testapp/templates'\r\
+ \n)\r\n \r\n\r\nNow , if you wanted to move\
+ \ the templates to some other directory, you'd just have to update settings.py\
+ \ to point to that directory. Similarly , you\
+ \ could add a list of several different directories containing groups of template\
+ \ files.
\r\n\r\nOption 3: explicit absolute\
+ \ paths \r\nIf you're having trouble accessing\
+ \ your templates even after configuring explicit relative paths, you might consider\
+ \ using absolute paths instead. However , a hardcoded\
+ \ absolute path is not a good idea because it means your application will only\
+ \ work if it is installed in a specific directory. The \
+ \ solution is to use Python 's os.path \
+ \ module to build an absolute path from a relative path and the __file__ \
+ \ variable, like so:
\r\n\r\n\r\nimport os.path\r\n\r\nTEMPLATE_DIRS\
+ \ = (\r\n os.path.join(os.path.dirname(__file__), 'testapp/templates')\r\n\
+ )\r\n \r\n"}
+ model: docs.page
+ pk: 15
+- fields: {content: This document has been moved to Quickstart
+ Guide .
, name: Tutorial, rendered: This
+ document has been moved to Quickstart ">Quickstart Guide .
}
+ model: docs.page
+ pk: 3
+- fields: {content: Please refer to the Quickstart Guide
+ for information on getting started with Djangy.
, name: TutorialStep1, rendered: Please refer to the Quickstart ">Quickstart
+ Guide for information on getting
+ started with Djangy .
}
+ model: docs.page
+ pk: 4
+- fields: {content: Please refer to the Quickstart Guide
+ for information on getting started with Djangy.
, name: TutorialStep2, rendered: Please refer to the Quickstart ">Quickstart
+ Guide for information on getting
+ started with Djangy .
}
+ model: docs.page
+ pk: 5
+- fields: {content: Please refer to the Quickstart Guide
+ for information on getting started with Djangy.
, name: TutorialStep3, rendered: Please refer to the Quickstart ">Quickstart
+ Guide for information on getting
+ started with Djangy .
}
+ model: docs.page
+ pk: 6
+- fields: {content: Please refer to the Quickstart Guide
+ for information on getting started with Djangy.
, name: TutorialStep4, rendered: Please refer to the Quickstart ">Quickstart
+ Guide for information on getting
+ started with Djangy .
}
+ model: docs.page
+ pk: 7
+- fields: {content: Please refer to the Quickstart Guide
+ for information on getting started with Djangy.
, name: TutorialStep5, rendered: Please refer to the Quickstart ">Quickstart
+ Guide for information on getting
+ started with Djangy .
}
+ model: docs.page
+ pk: 8
+- fields: {content: "Using the Shell \r\n \r\nOne of the most rigorously\
+ \ used features of django is the ability to quickly and easily spawn an interactive\
+ \ python shell within the context of your application. The shell is useful\
+ \ for anything from debugging to testing, quickly interacting with your database,\
+ \ and the like.
\r\n\r\nTo use the shell within the context of your live\
+ \ Djangy application, simply run:\r\n
\r\n$ djangy manage.py shell\r\nUsing\
+ \ git repository \"/Users/dave/myapp\"\r\nUsing application name \"davezor\"\
+ \ from \"/Users/dave/myapp/djangy.config\"\r\n\r\nPython 2.6.5 (r265:79063,\
+ \ Apr 16 2010, 13:57:41) \r\n[GCC 4.4.3] on linux2\r\nType \"help\", \"copyright\"\
+ , \"credits\" or \"license\" for more information.\r\n(InteractiveConsole)\r\
+ \n>>> \r\n \r\nFor more information, see the official Django docs .\r\n", name: UsingTheShell, rendered: "
\r\n \r\nOne \
+ \ of the most rigorously used features of django is the ability to quickly and\
+ \ easily spawn an interactive python shell within the context of your application.\
+ \ The shell is useful for anything\
+ \ from debugging to testing, quickly interacting with your database, and the\
+ \ like.
\r\n\r\nTo use the shell\
+ \ within the context of your live Djangy \
+ \ application, simply run:\r\n
\r\n$ djangy manage.py shell\r\nUsing git repository \"/Users /dave/myapp\"\r\nUsing application\
+ \ name \"davezor\" from \"/Users /dave/myapp/djangy.config\"\
+ \r\n\r\nPython 2.6.5 (r265:79063,\
+ \ Apr 16 2010, 13:57:41) \r\n[GCC 4.4.3]\
+ \ on linux2\r\nType \"help\", \"copyright\"\
+ , \"credits\" or \"license\" for more information.\r\n(InteractiveConsole )\r\n>>> \r\n \r\nFor more information, see the official Django docs.\r\
+ \n"}
+ model: docs.page
+ pk: 26
+- fields: {content: "
Introduction to Djangy \r\n\r\nDjangy is the best\
+ \ way to host and scale Django apps. It's instant and simple. Never worry\
+ \ about servers, hosting, downtime, or system administration again!
\r\n\
+ \r\n\r\nInstant deployment \r\n \r\nDeploying your app\
+ \ is as simple as doing a familiar \"git push\". Our system does the rest.
\r\
+ \n \r\nInstant scaling \r\n \r\nQuickly throw more resources\
+ \ behind your app to handle higher loads and more traffic.
\r\n \r\n\
+ Familiar controls \r\n \r\nInteract with your app using manage.py\
+ \ in exactly the same way you'd expect. Syncdb, migrate, loaddata, dumpdata,\
+ \ and shell are all where you expect them to be!
\r\n \r\nPay only\
+ \ for what you use \r\n \r\nDjangy gives you the ability to scale\
+ \ up and down your app usage, which means we only charge you for what you choose\
+ \ to use.
\r\n \r\nFor more information on how Djangy works, check\
+ \ out Djangy's architecture .
", name: WhatIsDjangy,
+ rendered: "\r\n\r\nDjangy is the best way to host and scale Django apps. It 's instant and simple. Never \
+ \ worry about servers, hosting, downtime, or system administration again!
\r\
+ \n \r\n\r\n\r\
+ \n \r\nDeploying your\
+ \ app is as simple as doing a familiar \"git push\". Our system does the rest.
\r\n \r\n\r\n \r\nQuickly throw more resources behind your app to handle higher loads and\
+ \ more traffic.
\r\n \r\n\r\n \r\nInteract with your app using manage.py in exactly the same way you'd expect.\
+ \ Syncdb , migrate, loaddata, dumpdata,\
+ \ and shell are all where you expect them to be!
\r\n \r\nPay only for what you use \r\n \r\nDjangy gives you the ability to scale\
+ \ up and down your app usage, which means we only charge you for what you choose\
+ \ to use.
\r\n \r\nFor more\
+ \ information on how Djangy works,\
+ \ check out Architecture \"\
+ >Djangy 's architecture.
"}
+ model: docs.page
+ pk: 19
+
diff --git a/src/server/master/web_ui/application/web_ui/main/Router.py b/src/server/master/web_ui/application/web_ui/main/Router.py
new file mode 100644
index 0000000..4201a7c
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/Router.py
@@ -0,0 +1,21 @@
+class Router(object):
+ """ Router to tell the application when to use the management_database and when to use the 'default' database.
+ see http://docs.djangoproject.com/en/1.2/topics/db/multi-db/
+ """
+
+ def check_for_md(self, model, **hints):
+ if model._meta.app_label == 'management_database':
+ return 'management_database'
+ return None
+
+ db_for_read = check_for_md
+ db_for_write = check_for_md
+
+ def allow_relation(self, obj1, obj2, **hints):
+ return obj1._meta.app_label == obj1._meta.app_label
+
+ def allow_syncdb(self, db, model):
+ """ Keep the management database from being synchronized here."""
+ if model._meta.app_label == 'management_database':
+ return False
+ return None
diff --git a/src/server/master/web_ui/application/web_ui/main/__init__.py b/src/server/master/web_ui/application/web_ui/main/__init__.py
new file mode 100644
index 0000000..a62fbc7
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/__init__.py
@@ -0,0 +1 @@
+from Router import *
diff --git a/src/server/master/web_ui/application/web_ui/main/invite_code/__init__.py b/src/server/master/web_ui/application/web_ui/main/invite_code/__init__.py
new file mode 100644
index 0000000..025112e
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/invite_code/__init__.py
@@ -0,0 +1 @@
+from gen_invite_code import *
diff --git a/src/server/master/web_ui/application/web_ui/main/invite_code/adjectives.py b/src/server/master/web_ui/application/web_ui/main/invite_code/adjectives.py
new file mode 100644
index 0000000..9cee114
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/invite_code/adjectives.py
@@ -0,0 +1,357 @@
+adjectives = [
+'adorable',
+'adventurous',
+'aggressive',
+'agreeable',
+'alert',
+'amused',
+'ancient',
+'angry',
+'annoyed',
+'annoying',
+'anxious',
+'arrogant',
+'ashamed',
+'attractive',
+'average',
+'awful',
+'beautiful',
+'bewildered',
+'big',
+'bitter',
+'black',
+'bloody',
+'blue',
+'blue-eyed',
+'blushing',
+'boiling',
+'bored',
+'brainy',
+'brave',
+'breakable',
+'breezy',
+'brief',
+'bright',
+'broad',
+'broken',
+'bumpy',
+'busy',
+'calm',
+'careful',
+'cautious',
+'charming',
+'cheerful',
+'chilly',
+'chubby',
+'clean',
+'clear',
+'clever',
+'cloudy',
+'clumsy',
+'cold',
+'colorful',
+'colossal',
+'combative',
+'comfortable',
+'concerned',
+'condemned',
+'confused',
+'cooing',
+'cool',
+'cooperative',
+'courageous',
+'crazy',
+'creepy',
+'crooked',
+'crowded',
+'cruel',
+'cuddly',
+'curious',
+'curly',
+'curved',
+'cute',
+'damaged',
+'damp',
+'dangerous',
+'dark',
+'dead',
+'deafening',
+'deep',
+'defeated',
+'defiant',
+'delicious',
+'delightful',
+'depressed',
+'determined',
+'difficult',
+'dirty',
+'disgusted',
+'distinct',
+'disturbed',
+'ditzy',
+'dizzy',
+'doubtful',
+'drab',
+'dry',
+'dull',
+'dusty',
+'eager',
+'easy',
+'elated',
+'elegant',
+'embarrassed',
+'empty',
+'enchanting',
+'encouraging',
+'energetic',
+'enthusiastic',
+'envious',
+'evil',
+'excited',
+'expensive',
+'exuberant',
+'faint',
+'faithful',
+'famous',
+'fancy',
+'fantastic',
+'fast',
+'fat',
+'fierce',
+'filthy',
+'fine',
+'flaky',
+'flat',
+'fluffy',
+'fluttering',
+'foolish',
+'fragile',
+'frail',
+'frantic',
+'freezing',
+'fresh',
+'friendly',
+'frightened',
+'funny',
+'fuzzy',
+'gentle',
+'gifted',
+'gigantic',
+'glamorous',
+'gleaming',
+'glorious',
+'gorgeous',
+'graceful',
+'greasy',
+'grieving',
+'grotesque',
+'grubby',
+'grumpy',
+'handsome',
+'happy',
+'hard',
+'harsh',
+'healthy',
+'heavy',
+'helpful',
+'helpless',
+'high-pitched',
+'hilarious',
+'hissing',
+'hollow',
+'homeless',
+'homely',
+'horrible',
+'hot',
+'huge',
+'hungry',
+'hurt',
+'hushed',
+'husky',
+'icy',
+'immense',
+'important',
+'impossible',
+'inexpensive',
+'innocent',
+'inquisitive',
+'itchy',
+'jealous',
+'jittery',
+'jolly',
+'joyous',
+'juicy',
+'kind',
+'large',
+'late',
+'lazy',
+'little',
+'lively',
+'living',
+'lonely',
+'loud',
+'lovely',
+'lucky',
+'magnificent',
+'mammoth',
+'manly',
+'massive',
+'melodic',
+'melted',
+'miniature',
+'misty',
+'moaning',
+'modern',
+'motionless',
+'muddy',
+'mushy',
+'mute',
+'mysterious',
+'narrow',
+'nasty',
+'naughty',
+'nervous',
+'nice',
+'noisy',
+'nutritious',
+'nutty',
+'obedient',
+'obnoxious',
+'odd',
+'old',
+'old-fashioned',
+'outrageous',
+'outstanding',
+'panicky',
+'perfect',
+'petite',
+'plain',
+'plastic',
+'pleasant',
+'poised',
+'poor',
+'powerful',
+'precious',
+'prickly',
+'proud',
+'puny',
+'purring',
+'puzzled',
+'quaint',
+'quick',
+'quiet',
+'rainy',
+'rapid',
+'raspy',
+'real',
+'relieved',
+'repulsive',
+'resonant',
+'rich',
+'ripe',
+'rotten',
+'rough',
+'round',
+'salty',
+'scary',
+'scattered',
+'scrawny',
+'screeching',
+'selfish',
+'shaggy',
+'shaky',
+'shallow',
+'sharp',
+'shiny',
+'shivering',
+'short',
+'shrill',
+'shy',
+'silent',
+'silky',
+'silly',
+'skinny',
+'sleepy',
+'slimy',
+'slippery',
+'slow',
+'small',
+'smiling',
+'smitten',
+'smoggy',
+'smooth',
+'soft',
+'solid',
+'sore',
+'sour',
+'sparkling',
+'spicy',
+'splendid',
+'spotless',
+'square',
+'squealing',
+'stale',
+'steady',
+'steep',
+'sticky',
+'stormy',
+'straight',
+'strange',
+'strong',
+'stupid',
+'substantial',
+'successful',
+'super',
+'sweet',
+'swift',
+'talented',
+'tall',
+'tame',
+'tasteless',
+'tasty',
+'teeny',
+'teeny-tiny',
+'tender',
+'tense',
+'terrible',
+'thankful',
+'thirsty',
+'thoughtful',
+'thoughtless',
+'thundering',
+'tight',
+'tiny',
+'tired',
+'tough',
+'troubled',
+'ugly',
+'uneven',
+'uninterested',
+'unsightly',
+'unusual',
+'upset',
+'uptight',
+'victorious',
+'vivacious',
+'voiceless',
+'wandering',
+'warm',
+'weak',
+'weary',
+'wet',
+'whispering',
+'wicked',
+'wide',
+'wide-eyed',
+'wild',
+'witty',
+'wonderful',
+'wooden',
+'worldly',
+'worried',
+'young',
+'yummy',
+'zany',
+'zealous',
+'zombie',
+]
diff --git a/src/server/master/web_ui/application/web_ui/main/invite_code/gen_invite_code.py b/src/server/master/web_ui/application/web_ui/main/invite_code/gen_invite_code.py
new file mode 100644
index 0000000..ea9a5be
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/invite_code/gen_invite_code.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+
+import random, sys
+from adjectives import *
+from nouns import *
+
+def gen_invite_code(n=1):
+ if len(sys.argv) > 1:
+ n = int(sys.argv[1])
+
+ def strip_newlines(lines):
+ return map(lambda x: x[:-1], lines)
+
+ def choose_word(words):
+ return words[random.randint(0, len(words)-1)]
+
+ for i in range(0, n):
+ adj1 = choose_word(adjectives)
+ adj2 = choose_word(adjectives)
+ while adj1[-1] == adj2[-1]:
+ adj2 = choose_word(adjectives)
+ if adj1[-1] > adj2[-1]:
+ (adj1, adj2) = (adj2, adj1)
+ if adj1 == 'zombie':
+ (adj1, adj2) = (adj2, adj1)
+ noun = choose_word(nouns)
+
+ return "%s %s %s" % (adj1, adj2, noun)
+
+if __name__ == '__main__':
+ gen_invite_code()
diff --git a/src/server/master/web_ui/application/web_ui/main/invite_code/nouns.py b/src/server/master/web_ui/application/web_ui/main/invite_code/nouns.py
new file mode 100644
index 0000000..03e16d0
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/invite_code/nouns.py
@@ -0,0 +1,122 @@
+nouns = [
+'alien',
+'artist',
+'baby',
+'badger',
+'basketball',
+'basketcase',
+'bedsheet',
+'bicycle',
+'boy',
+'boyscout',
+'bratwurst',
+'camera',
+'candle',
+'captain',
+'cat',
+'caveman',
+'ceo',
+'chair',
+'cheek',
+'cheesecake',
+'chihuahua',
+'chipmunk',
+'cruller',
+'digerati',
+'dog',
+'donkey',
+'donut',
+'dork',
+'driver',
+'drunk',
+'elf',
+'eskimo',
+'fairy',
+'fan',
+'father',
+'football',
+'friend',
+'frog',
+'gangster',
+'ghost',
+'girl',
+'girlscout',
+'goalie',
+'gorilla',
+'hacker',
+'hedgehog',
+'helmet',
+'hipster',
+'hobo',
+'horse',
+'house',
+'icecream',
+'inmate',
+'insect',
+'jock',
+'kangaroo',
+'keyboard',
+'king',
+'kitten',
+'knife',
+'koala',
+'lamp',
+'llama',
+'magistrate',
+'mathematician',
+'mom',
+'monkey',
+'monologue',
+'moped',
+'narwhal',
+'nerd',
+'ninja',
+'painting',
+'panda',
+'pants',
+'pencil',
+'penguin',
+'pig',
+'pikachu',
+'pirate',
+'pizza',
+'pogostick',
+'pony',
+'priest',
+'prince',
+'princess',
+'pumpkin',
+'puppy',
+'queen',
+'rabbit',
+'racquet',
+'redditor',
+'roommate',
+'scientist',
+'sheep',
+'skateboard',
+'snail',
+'solicitor',
+'spork',
+'spring',
+'statue',
+'summer',
+'superstar',
+'swimmer',
+'teaspoon',
+'toothbrush',
+'towel',
+'train',
+'trashcan',
+'troll',
+'tulip',
+'turtle',
+'unicorn',
+'viking',
+'wallaby',
+'weather',
+'winnebago',
+'wino',
+'winter',
+'yankee',
+]
diff --git a/src/server/master/web_ui/application/web_ui/main/migrations/0001_initial.py b/src/server/master/web_ui/application/web_ui/main/migrations/0001_initial.py
new file mode 100644
index 0000000..d43a7eb
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/migrations/0001_initial.py
@@ -0,0 +1,35 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding model 'Email'
+ db.create_table('main_email', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('email', self.gf('django.db.models.fields.EmailField')(max_length=75)),
+ ('timestamp', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
+ ))
+ db.send_create_signal('main', ['Email'])
+
+
+ def backwards(self, orm):
+
+ # Deleting model 'Email'
+ db.delete_table('main_email')
+
+
+ models = {
+ 'main.email': {
+ 'Meta': {'object_name': 'Email'},
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
+ }
+ }
+
+ complete_apps = ['main']
diff --git a/src/server/master/web_ui/application/web_ui/main/migrations/0002_add_invited_field.py b/src/server/master/web_ui/application/web_ui/main/migrations/0002_add_invited_field.py
new file mode 100644
index 0000000..41c2bfb
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/migrations/0002_add_invited_field.py
@@ -0,0 +1,31 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding field 'Email.invited'
+ db.add_column('main_email', 'invited', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'Email.invited'
+ db.delete_column('main_email', 'invited')
+
+
+ models = {
+ 'main.email': {
+ 'Meta': {'object_name': 'Email'},
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'invited': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
+ }
+ }
+
+ complete_apps = ['main']
diff --git a/src/server/master/web_ui/application/web_ui/main/migrations/__init__.py b/src/server/master/web_ui/application/web_ui/main/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/server/master/web_ui/application/web_ui/main/models.py b/src/server/master/web_ui/application/web_ui/main/models.py
new file mode 100644
index 0000000..6c87c49
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/models.py
@@ -0,0 +1,7 @@
+from django.db import models
+
+class Email(models.Model):
+ email = models.EmailField()
+ timestamp = models.DateTimeField(auto_now = True)
+ invited = models.BooleanField(default = False)
+
diff --git a/src/server/master/web_ui/application/web_ui/main/templates/admin.html b/src/server/master/web_ui/application/web_ui/main/templates/admin.html
new file mode 100644
index 0000000..3468b2d
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/templates/admin.html
@@ -0,0 +1,46 @@
+{% extends "base.html" %}
+{% block pagetitle %}
+
+
Admin
+
+{% endblock %}
+{% block content %}
+ Registered Users: {{ user_count }}
+ Applications: {{ app_count }}
+
+
Users
+ {% for email, application in emails_applications %}
+
+
+ {{ email }}
+ {% for app in application %}
+
+ {{ app.name }}
+
+ {% endfor %}
+
+
+ {% endfor %}
+
+
+
+
Email Signups
+
+ (download as txt)
+
+
+
+ {% for email in emails %}
+ {% if email.invited %}
+ {{ email.email }} (already invited)
+ {% else %}
+ {{ email.email }} invite
+ {% endif %}
+ {% endfor %}
+
+
+
+{% endblock %}
diff --git a/src/server/master/web_ui/application/web_ui/main/templates/dashboard_account.html b/src/server/master/web_ui/application/web_ui/main/templates/dashboard_account.html
new file mode 100644
index 0000000..7cb7e60
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/templates/dashboard_account.html
@@ -0,0 +1,78 @@
+{% extends "base.html" %}
+{% block pagetitle %}
+
+{% endblock %}
+{% block content %}
+
+
+
+ SSH key management
+ Current SSH public keys
+
+ {% for key in ssh_public_keys %}
+
remove
+ {{ key.ssh_public_key }} {{ key.comment }}
+
+ {% endfor %}
+
+ New SSH public key
+
+{% endblock %}
+{% block scripts %}
+
+{% endblock %}
diff --git a/src/server/master/web_ui/application/web_ui/main/templates/dashboard_application.html b/src/server/master/web_ui/application/web_ui/main/templates/dashboard_application.html
new file mode 100644
index 0000000..a1bb35c
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/templates/dashboard_application.html
@@ -0,0 +1,157 @@
+{% extends "base.html" %}
+{% block pagetitle %}
+
+
{{ application_name }}
+
+
+{% endblock %}
+{% block content %}
+
+{% endblock %}
+{% block scripts %}
+
+{% endblock %}
diff --git a/src/server/master/web_ui/application/web_ui/main/templates/dashboard_applicationlist.html b/src/server/master/web_ui/application/web_ui/main/templates/dashboard_applicationlist.html
new file mode 100644
index 0000000..4a0b7ef
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/templates/dashboard_applicationlist.html
@@ -0,0 +1,38 @@
+{% extends "base.html" %}
+{% block pagetitle %}
+
+{% endblock %}
+{% block content %}
+
+ {% if applications|length_is:"0" %}
+ You have no applications. Check out the Quickstart guide to get started!
+ {% else %}
+ {% if applications|length_is:"1" %}
+ You have one application:
+ {% else %}
+ You have {{ applications|length }} applications:
+ {% endif %}
+ {% endif %}
+
+ {% for app in applications %}
+
+ {% endfor %}
+{% endblock %}
+{% block scripts %}
+
+{% endblock %}
diff --git a/src/server/master/web_ui/application/web_ui/main/templates/dashboard_billing.html b/src/server/master/web_ui/application/web_ui/main/templates/dashboard_billing.html
new file mode 100644
index 0000000..0d88da6
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/templates/dashboard_billing.html
@@ -0,0 +1,103 @@
+{% extends "base.html" %}
+{% block pagetitle %}
+
+{% endblock %}
+{% block content %}
+
+
+
+
+
+
+ {% if info.bill_date %}
+ Next bill date: {{ info.bill_date }}
+ {% endif %}
+ {% if usage %}
+ Current usage: {{ usage }}
+
+
+
+ {% endif %}
+ FAQ
+
+
+
+
+{% endblock %}
+{% block scripts %}
+
+{% endblock %}
diff --git a/src/server/master/web_ui/application/web_ui/main/templates/dashboard_invite.html b/src/server/master/web_ui/application/web_ui/main/templates/dashboard_invite.html
new file mode 100644
index 0000000..99226bc
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/templates/dashboard_invite.html
@@ -0,0 +1,39 @@
+{% extends "base.html" %}
+{% block pagetitle %}
+
+{% endblock %}
+{% block content %}
+
+ {% if num_remaining_invitations > 0 %}
+ You have {{ num_remaining_invitations }} remaining invitations.
+ Use them wisely.
+ {% else %}
+ You have no remaining invitations. Contact support@djangy.com if you think
+ you need more.
+ {% endif %}
+
+
+{% endblock %}
+{% block scripts %}
+
+{% endblock %}
diff --git a/src/server/master/web_ui/application/web_ui/main/templates/emails.txt b/src/server/master/web_ui/application/web_ui/main/templates/emails.txt
new file mode 100644
index 0000000..ffb0392
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/templates/emails.txt
@@ -0,0 +1 @@
+{% for email in emails %}{{ email }}, {% endfor %}
diff --git a/src/server/master/web_ui/application/web_ui/main/templates/hackerdojo.html b/src/server/master/web_ui/application/web_ui/main/templates/hackerdojo.html
new file mode 100644
index 0000000..686062d
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/templates/hackerdojo.html
@@ -0,0 +1,27 @@
+{% extends "base.html" %}
+{% block pagetitle %}
+
+
Join
+
+{% endblock %}
+{% block content %}
+ Welcome, hackers. Enter your invite code below:
+ {% with "/hackerdojo" as setpassword_action %}
+ {% with "Sign Up" as setpassword_button %}
+
+
+
+ {% endwith %}
+ {% endwith %}
+{% endblock %}
+{% block scripts %}
+
+{% endblock %}
diff --git a/src/server/master/web_ui/application/web_ui/main/templates/index.html b/src/server/master/web_ui/application/web_ui/main/templates/index.html
new file mode 100644
index 0000000..799e44c
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/templates/index.html
@@ -0,0 +1,51 @@
+{% extends "base.html" %}
+{% block showcase %}
+
+
+
+
+ {% if user %}
+
Welcome to Djangy!
+
Thank you for participating in the Djangy private beta.
+
Visit the Dashboard to manage your account and applications.
+
Read the Documentation to learn more about Djangy.
+
Email support@djangy.com with questions, comments, feature requests, and bug reports.
+ {% else %}
+
Sign up for free!
+
Djangy is currently in private beta. Please enter your email address and we'll
+ send you an invitation soon!
+
+
+
+ {% endif %}
+
+
+{% endblock %}
+{% block content %}
+
+
Deploy and scale Django apps instantly.
+
+ Never configure apache.
+ Forget about the headaches of virtual hosting.
+ Only pay for what you use.
+ Push your code to Djangy and we'll do the rest!
+
+
+ Django
+ Pony Powered
+
+
+
+
Need more power?
+
Only pay for what you use: $0.05 per hour
+ for each running instance past the first. Instantly scale your
+ allocations up and down, and pay at the end of the month.
+ Find out more .
+
+{% endblock %}
+{% block scripts %}
+
+{% endblock %}
diff --git a/src/server/master/web_ui/application/web_ui/main/templates/join.html b/src/server/master/web_ui/application/web_ui/main/templates/join.html
new file mode 100644
index 0000000..b2df47d
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/templates/join.html
@@ -0,0 +1,19 @@
+{% extends "base.html" %}
+{% block pagetitle %}
+
+
Join
+
+{% endblock %}
+{% block content %}
+ Your invite code is:
{{ invite_code }}
+ {% with "/join" as setpassword_action %}
+ {% with "Sign Up" as setpassword_button %}
+ {% include "setpassword.html" %}
+ {% endwith %}
+ {% endwith %}
+{% endblock %}
+{% block scripts %}
+
+{% endblock %}
diff --git a/src/server/master/web_ui/application/web_ui/main/templates/login.html b/src/server/master/web_ui/application/web_ui/main/templates/login.html
new file mode 100644
index 0000000..7334f15
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/templates/login.html
@@ -0,0 +1,26 @@
+{% extends "base.html" %}
+{% block title %}
+Login to Djangy - Instant deployment and scaling for your Django applications
+{% endblock %}
+{% block pagetitle %}
+
+
Login
+
+{% endblock %}
+{% block content %}
+
+{% endblock %}
+{% block scripts %}
+
+{% endblock %}
diff --git a/src/server/master/web_ui/application/web_ui/main/templates/pricing.html b/src/server/master/web_ui/application/web_ui/main/templates/pricing.html
new file mode 100644
index 0000000..6c2190a
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/templates/pricing.html
@@ -0,0 +1,175 @@
+{% extends "base.html" %}
+{% block pagetitle %}
+
+
Pricing
+
+{% endblock %}
+{% block content %}
+
+
+
+ Performance
+
+
+
+ An application instance handles one HTTP request for your
+ application at a time. More instances let you support more
+ concurrent users. When you enable multiple instances,
+ Djangy automatically runs them on multiple different
+ machines.
+
+
+
+
+ instances
+ price
+
+
+
+
+ first instance
+ FREE
+
+
+ additional instances
+ $0.05 / instance-hour
+
+
+
+
+
+
+ Background jobs
+
+
+
+ A background worker is a single
+ Celery process that accepts and
+ runs background jobs from your queue. More background
+ workers let you run more queued jobs faster. When you
+ enable multiple workers, Djangy automatically runs them on
+ multiple different machines.
+
+
+
+
+ workers
+ price
+
+
+
+
+ each worker
+ $0.05 / worker-hour
+
+
+
+
+
+
+ Databases
+
+
+
+ Djangy's shared database cluster provides solid
+ performance and reliability for small to medium size
+ applications. Our dedicated database plans offer higher
+ performance and capacity for large applications, with the
+ same ease of use as our shared plans.
+
+ To sign up for a paid database plan, contact support@djangy.com .
+
+ Shared database plans
+
+
+
+
+ plan
+ price
+ capacity
+
+
+
+
+ Shared
+ FREE
+ 20 MB
+
+
+ Shared Plus
+ $20 / month
+ 20 GB
+
+
+
+
+ Dedicated database plans
+
+
+
+
+ plan
+ price
+ RAM
+ cores
+ capacity
+
+
+
+
+ Basic
+ $200 / month
+ 1.7 GB
+ 1
+ 2 TB
+
+
+ Plus
+ $800 / month
+ 7.5 GB
+ 4
+ 2 TB
+
+
+ Pro
+ $1600 / month
+ 15 GB
+ 8
+ 2 TB
+
+
+
+
+
+
+
+
What's included?
+
+ Technical support
+ Instant deployment and scaling
+ 100% standard environment: no lock-in
+
+
+
+
Database
+
Every Djangy app has automatic access to a database on our shared
+ database cluster. You don't need to configure anything—it
+ just works!
+
+
+
Custom domains
+
Use any of your custom domain names with your Djangy
+ applications, at no extra cost.
+
+
+
HTTP Caching
+
Our front-end servers automatically cache your static content, so
+ your application instances can focus on running your application.
+
+{% endblock %}
+{% block scripts %}
+
+{% endblock %}
diff --git a/src/server/master/web_ui/application/web_ui/main/templates/request_reset_password.html b/src/server/master/web_ui/application/web_ui/main/templates/request_reset_password.html
new file mode 100644
index 0000000..32c79e9
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/templates/request_reset_password.html
@@ -0,0 +1,25 @@
+{% extends "base.html" %}
+{% block title %}
+Login to Djangy - Instant deployment and scaling for your Django applications
+{% endblock %}
+{% block pagetitle %}
+
+
Password Reset
+
+{% endblock %}
+{% block content %}
+
+ Enter your address and we'll email you a password reset link.
+
+
+
+
+{% endblock %}
+{% block scripts %}
+
+{% endblock %}
diff --git a/src/server/master/web_ui/application/web_ui/main/templates/reset_password_form.html b/src/server/master/web_ui/application/web_ui/main/templates/reset_password_form.html
new file mode 100644
index 0000000..af71546
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/templates/reset_password_form.html
@@ -0,0 +1,24 @@
+{% extends "base.html" %}
+{% block title %}
+Djangy Password reset - Instant deployment and scaling for your Django applications
+{% endblock %}
+{% block pagetitle %}
+
+
Password reset
+
+{% endblock %}
+{% block content %}
+
+ Enter your new password for {{ email }} below and confirm:
+ {% with '/set_password' as setpassword_action %}
+ {% with 'Set Password' as setpassword_button %}
+ {% include 'setpassword.html' %}
+ {% endwith %}
+ {% endwith %}
+
+{% endblock %}
+{% block scripts %}
+
+{% endblock %}
diff --git a/src/server/master/web_ui/application/web_ui/main/templates/setpassword.html b/src/server/master/web_ui/application/web_ui/main/templates/setpassword.html
new file mode 100644
index 0000000..245d693
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/templates/setpassword.html
@@ -0,0 +1,11 @@
+
+
+
diff --git a/src/server/master/web_ui/application/web_ui/main/tests.py b/src/server/master/web_ui/application/web_ui/main/tests.py
new file mode 100644
index 0000000..2247054
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/tests.py
@@ -0,0 +1,23 @@
+"""
+This file demonstrates two different styles of tests (one doctest and one
+unittest). These will both pass when you run "manage.py test".
+
+Replace these with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+class SimpleTest(TestCase):
+ def test_basic_addition(self):
+ """
+ Tests that 1 + 1 always equals 2.
+ """
+ self.failUnlessEqual(1 + 1, 2)
+
+__test__ = {"doctest": """
+Another way to test that 1 + 1 is equal to 2.
+
+>>> 1 + 1 == 2
+True
+"""}
+
diff --git a/src/server/master/web_ui/application/web_ui/main/utils.py b/src/server/master/web_ui/application/web_ui/main/utils.py
new file mode 100644
index 0000000..0da1e11
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/utils.py
@@ -0,0 +1,9 @@
+from hashlib import md5
+
+def hash_password(email, password):
+ return md5("%s:%s" % (email, password)).hexdigest()
+
+def check_password(email, password, hashed_password):
+ if hash_password(email, password) != hashed_password:
+ return False
+ return True
diff --git a/src/server/master/web_ui/application/web_ui/main/views/__init__.py b/src/server/master/web_ui/application/web_ui/main/views/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/server/master/web_ui/application/web_ui/main/views/admin.py b/src/server/master/web_ui/application/web_ui/main/views/admin.py
new file mode 100644
index 0000000..1fab013
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/views/admin.py
@@ -0,0 +1,96 @@
+from shared import *
+
+@http_methods('GET', 'POST')
+@auth_required
+@admin_required
+def admin(request):
+ message = get_session_message(request)
+
+ user = User.get_by_email(request.session.get('email'))
+ emails = Email.objects.all()
+ user_count = User.objects.all().count()
+ app_count = Application.objects.filter(deleted=None).all().count()
+ emails_applications = [(u.email, u.application_set.filter(deleted=None)) for u in User.objects.all()]
+ return render_to_response('admin.html', {
+ 'navbar_section':'admin',
+ 'emails_applications':emails_applications,
+ 'user':user,
+ 'message':message,
+ 'emails':emails,
+ 'user_count':user_count,
+ 'app_count':app_count
+ })
+
+# XXX - CSRF
+@http_methods('GET', 'POST')
+@auth_required
+def invite(request):
+ email = request.REQUEST.get('email')
+
+ # ensure there are invitations left
+ inviter = User.get_by_email(request.session.get('email', None))
+ invitees = User.objects.filter(referrer=inviter).count() + WhiteList.objects.filter(referrer=inviter).count()
+ if invitees > inviter.invite_limit and (not inviter.admin):
+ request.session['message'] = 'You have no invitations left.'
+ return HttpResponseRedirect('/dashboard/account')
+
+ # Prevent duplicate invitations
+ if User.get_by_email(email) != None:
+ request.session['message'] = 'User already exists, email not sent to %s.' % email
+ return HttpResponseRedirect('/dashboard/account')
+
+ invite_code = gen_invite_code()
+
+ wl = WhiteList.objects.all().filter(email=email)
+ for obj in wl:
+ WhiteList.delete(obj)
+ wl = WhiteList(email=email)
+ wl.invite_code = invite_code
+ try:
+ wl.referrer = User.get_by_email(request.session.get('email', None))
+ except:
+ logging.debug("tried to set whitelist referrer to: %s" % request.session.get("email", None))
+ wl.save()
+ referrer = 'the Djangy admin'
+ if wl.referrer:
+ referrer = wl.referrer.email
+ # mark the user as invited
+ try:
+ email_object = Email.objects.filter(email=email).all()
+ for em in email_object:
+ em.invited = True
+ em.save()
+ except Exception, e:
+ logging.debug(e)
+
+ # email the user
+ send_mail(
+ 'Your Djangy.com Private Beta Invitation',
+ """
+Congratulations, %s has invited you to join the private beta of Djangy.com,
+the hosting service that lets you deploy your Django applications instantly!
+
+Your invite code is: %s
+
+Click the following link to sign up:
+https://www.djangy.com/join?%s
+
+For more information, check out our documentation:
+http://www.djangy.com/docs
+
+Please email support@djangy.com with any feedback you may have.
+
+Love,
+Djangy.com""" % (referrer, invite_code, urlencode({'email':email, 'invite_code':invite_code})),
+ 'support@djangy.com',
+ [email, 'support@djangy.com'], fail_silently=False
+ )
+
+ request.session['message'] = 'Invitation sent to %s' % email
+ return HttpResponseRedirect('/admin')
+
+@auth_required
+@admin_required
+def get_emails(request):
+ emails = [user.email for user in User.objects.all()]
+ return render_to_response("emails.txt", {'emails':emails}, mimetype="text/plain")
diff --git a/src/server/master/web_ui/application/web_ui/main/views/create_account.py b/src/server/master/web_ui/application/web_ui/main/views/create_account.py
new file mode 100644
index 0000000..3aba82d
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/views/create_account.py
@@ -0,0 +1,110 @@
+from shared import *
+
+@http_methods('POST')
+def signup(request):
+ email = request.POST.get('email')
+ if not email:
+ return HttpResponseRedirect('/')
+
+ try:
+ validate_email(email)
+ except Exception, e:
+ request.session['message'] = 'Please enter a valid email address, or email support@djangy.com for help.'
+ return HttpResponseRedirect('/')
+ #return render_to_response('index.html', {'message':'Please enter a valid email address, or email support@djangy.com for help.'})
+
+ email_obj = Email(email = email)
+ email_obj.save()
+
+ request.session['message'] = "Thanks! We'll send you an invitation soon."
+ return HttpResponseRedirect('/')
+ #return render_to_response('index.html', {'message':"Thanks! We'll send you an invitation soon.", 'index':True})
+
+# XXX
+@http_methods('GET', 'POST')
+def join(request):
+ if request.method == 'POST':
+ email = request.POST.get('email')
+ invite_code = request.POST.get('invite_code')
+ password1 = request.POST.get('password1')
+ password2 = request.POST.get('password2')
+ if (not email) or (not password1) or (not password2):
+ return HttpResponseRedirect('/')
+
+ if (password1 != password2):
+ return render_to_response('join.html', {'message':'Whoops, looks like your passwords didn\'t match. Please try again.', 'email':email, 'invite_code':invite_code})
+ try:
+ validate_email(email)
+ except:
+ return HttpResponseRedirect('/')
+
+ user = User()
+ user.email = email
+ user.passwd = hash_password(email, password1)
+
+ wl = WhiteList.objects.get(email = email)
+ user.referrer = wl.referrer
+ user.save()
+ wl.delete()
+ user.save()
+
+ request.session['email'] = email
+ logging.info('%s joined successfully.' % email)
+ return HttpResponseRedirect('/dashboard')
+
+ elif request.method == 'GET':
+ email = request.GET.get('email')
+ invite_code = request.GET.get('invite_code')
+ if (email is None) or (invite_code is None):
+ request.session['message'] = 'Email or invite code not found.'
+ return HttpResponseRedirect('/')
+
+ if WhiteList.verify(email, invite_code):
+ return render_to_response('join.html', { 'email':email, 'invite_code':invite_code})
+ else:
+ request.session['message'] = 'Invalid invite code.'
+ return HttpResponseRedirect('/')
+
+# XXX
+@http_methods('GET', 'POST')
+def hackerdojo(request):
+ if request.method == 'GET':
+ return render_to_response('hackerdojo.html')
+
+ elif request.method == 'POST':
+ email = request.POST.get('email')
+ invite_code = request.POST.get('invite_code')
+ password1 = request.POST.get('password1')
+ password2 = request.POST.get('password2')
+
+ if (not email) or (not password1) or (not password2):
+ return render_to_response('hackerdojo.html', {'message':'Please enter a valid email address.', 'invite_code':invite_code})
+
+ if (password1 != password2):
+ return render_to_response('hackerdojo.html', {'message':'Whoops, looks like your passwords didn\'t match. Please try again.', 'email':email, 'invite_code':invite_code})
+ try:
+ validate_email(email)
+ except:
+ return render_to_response('hackerdojo.html', {'message':'Please enter a valid email address.', 'invite_code':invite_code})
+ if not invite_code:
+ return render_to_response('hackerdojo.html', {'message':'It looks like you forgot to enter an invite code. Try again.', 'email':email})
+
+ try:
+ wl = WhiteList.objects.get(invite_code = invite_code)
+ if wl.email:
+ return render_to_response('hackerdojo.html', {'message':'That invite code has already been used. Please try again.', 'email':email})
+ wl.email = email
+ wl.save()
+ except:
+ return render_to_response('hackerdojo.html', {'message':'That invite code is invalid. Please try again.', 'email':email})
+
+ user = User()
+ user.email = email
+ user.passwd = hash_password(email, password1)
+ user.save()
+
+
+ request.session['email'] = email
+ request.session['message'] = 'Thanks for signing up!'
+ logging.info('%s joined successfully.' % email)
+ return HttpResponseRedirect('/dashboard')
diff --git a/src/server/master/web_ui/application/web_ui/main/views/dashboard_account.py b/src/server/master/web_ui/application/web_ui/main/views/dashboard_account.py
new file mode 100644
index 0000000..b732b2d
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/views/dashboard_account.py
@@ -0,0 +1,106 @@
+from shared import *
+import master_api
+from django.core.mail import send_mail
+
+@http_methods('GET')
+@auth_required
+def account(request):
+ message = get_session_message(request)
+ email = request.session.get('email')
+ user = User.get_by_email(email)
+ return render_to_response('dashboard_account.html', {
+ 'navbar_section':'dashboard',
+ 'user':user,
+ 'email':email,
+ 'sessionid': request.COOKIES['sessionid'],
+ 'message':message,
+ 'ssh_public_keys':user.get_ssh_public_keys()
+ })
+
+@http_methods('POST')
+@token_required
+@auth_required
+def change_password(request):
+ email = request.session.get('email')
+ if not email:
+ return HttpResponseRedirect('/login')
+
+ user = User.get_by_email(email)
+ if not user:
+ return HttpResponseRedirect('/login')
+
+ # Check that the user knew the old password
+ old_password = request.POST.get('old_password')
+ if user.passwd != hash_password(email, old_password):
+ request.session['message'] = 'Incorrect old password.'
+ return HttpResponseRedirect('/dashboard/account')
+
+ # Confirm that the new passwords are the same and nonempty
+ new_password1 = request.POST.get('new_password1')
+ new_password2 = request.POST.get('new_password2')
+ if (not new_password1) or (not new_password2) or (new_password1 != new_password2):
+ request.session['message'] = 'New passwords do not match.'
+ return HttpResponseRedirect('/dashboard/account')
+
+ user.passwd = hash_password(email, new_password1)
+ user.save()
+
+ request.session['message'] = 'Password successfully changed.'
+ return HttpResponseRedirect('/dashboard/account')
+
+@http_methods('POST')
+@token_required
+@auth_required
+def change_email(request):
+ email = request.session.get('email')
+ user = User.get_by_email(email)
+ if not user:
+ request.session['message'] = 'There was a problem looking up your user account. Please contact support@djangy.com'
+ return HttpResponseRedirect('/dashboard/account')
+
+ new_email = request.POST.get('new_email')
+
+ if not new_email:
+ request.session['message'] = 'Invalid email address.'
+ return HttpResponseRedirect('/dashboard/account')
+
+ password = request.POST.get('password') or ''
+
+ if hash_password(email, password) != user.passwd:
+ request.session['message'] = 'Invalid password.'
+ return HttpResponseRedirect('/dashboard/account')
+
+ user.email = new_email
+ user.passwd = hash_password(new_email, password)
+ user.save()
+ request.session['email'] = new_email
+ request.session['message'] = 'Your email address has been updated.'
+ return HttpResponseRedirect('/dashboard/account')
+
+@http_methods('POST')
+@token_required
+@auth_required
+def add_ssh_public_key(request):
+ email = request.session.get('email')
+ if not User.get_by_email(email):
+ request.session['message'] = 'There was a problem looking up your user account. Please contact support@djangy.com'
+ return HttpResponseRedirect('/dashboard/account')
+
+ ssh_public_key = request.POST.get('ssh_public_key')
+ master_api.add_ssh_public_key(email, ssh_public_key)
+
+ return HttpResponseRedirect('/dashboard/account')
+
+@http_methods('GET')
+@token_required
+@auth_required
+def remove_ssh_public_key(request):
+ email = request.session.get('email')
+ if not User.get_by_email(email):
+ request.session['message'] = 'There was a problem looking up your user account. Please contact support@djangy.com'
+ return HttpResponseRedirect('/dashboard/account')
+
+ ssh_public_key_id = int(request.GET.get('id'))
+ master_api.remove_ssh_public_key(email, str(ssh_public_key_id))
+
+ return HttpResponseRedirect('/dashboard/account')
diff --git a/src/server/master/web_ui/application/web_ui/main/views/dashboard_application.py b/src/server/master/web_ui/application/web_ui/main/views/dashboard_application.py
new file mode 100644
index 0000000..834ef95
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/views/dashboard_application.py
@@ -0,0 +1,221 @@
+import re, traceback
+from shared import *
+import management_database
+from management_database import *
+
+def _check_application_access(func):
+ """ Decorator for checking that the user has access to the selected application. Use after auth_required. """
+ def check_application_access(request, application_name):
+ email = request.session.get('email')
+ user = User.get_by_email(email)
+ application = Application.get_by_name(application_name)
+ if application and application.accessible_by(user):
+ return func(request, application_name)
+ else:
+ return HttpResponseForbidden('Access denied for user "%s" to application "%s".' % (email, application_name))
+ return check_application_access
+
+# GET /dashboard/application/
+@http_methods('GET')
+@auth_required
+@_check_application_access
+def application(request, application_name):
+ email = request.session.get('email')
+ user = User.get_by_email(email)
+ application = Application.get_by_name(application_name)
+
+ message = get_session_message(request)
+ gunicorn_processes = Process.objects.filter(application__name=application_name, proc_type='gunicorn').aggregate(Sum('num_procs'))['num_procs__sum']
+ celery_processes = Process.objects.filter(application__name=application_name, proc_type='celery' ).aggregate(Sum('num_procs'))['num_procs__sum']
+ custom_domains = VirtualHost.get_virtualhosts_by_application_name(application_name)
+ custom_domains.remove('%s.djangy.com' % application.name)
+ return render_to_response('dashboard_application.html', {
+ 'navbar_section':'dashboard',
+ 'user': user,
+ 'application_name': application.name,
+ 'sessionid': request.COOKIES['sessionid'],
+ 'application_instances': gunicorn_processes,
+ 'application_instances_range': range(1, 10+1),
+ 'background_workers': celery_processes,
+ 'background_workers_range': range(0, 5+1),
+ 'message': message,
+ 'custom_domains': custom_domains,
+ 'enable_debug': application.debug,
+ 'enable_server_cache': application.is_server_cache_enabled(),
+ 'owner_email': application.account.email,
+ 'collaborator_emails': application.get_collaborators()
+ })
+
+# POST /dashboard/application//delete?really_delete=yes
+@http_methods('POST')
+@token_required
+@auth_required
+@_check_application_access
+def delete_application(request, application_name):
+ if not request.POST.get('really_delete'):
+ return HttpResponseRedirect('/dashboard/application/%s' % application_name)
+
+ try:
+ remove_application(application_name)
+ except Exception, e:
+ return HttpResponseServerError('Error deleting application.')
+
+ request.session['message'] = 'Application %s was deleted.' % application_name
+ return HttpResponseRedirect('/dashboard')
+
+# POST /dashboard/application//add_collaborator?email=
+@http_methods('POST')
+@token_required
+@auth_required
+@_check_application_access
+def add_collaborator(request, application_name):
+ email = request.POST.get('email')
+ if email != None:
+ try:
+ application = management_database.Application.get_by_name(application_name)
+ if application.add_collaborator(email):
+ request.session['message'] = 'Collaborator %s added to %s' % (email, application_name)
+ else:
+ request.session['message'] = 'Collaborator %s already has access to %s' % (email, application_name)
+ except NoUserException as e:
+ request.session['message'] = 'Error: %s does not have a Djangy account' % email
+ except Exception as e:
+ request.session['message'] = 'Error adding collaborator %s %s ' % (email, traceback.format_exc())
+
+ return HttpResponseRedirect('/dashboard/application/%s' % application_name)
+
+# GET /dashboard/application//remove_collaborator?email=
+@http_methods('GET')
+@token_required
+@auth_required
+@_check_application_access
+def remove_collaborator(request, application_name):
+ email = request.GET.get('email')
+ if email != None:
+ try:
+ application = Application.get_by_name(application_name)
+ application.remove_collaborator(email)
+ request.session['message'] = 'Collaborator %s removed from %s' % (email, application_name)
+ except Exception:
+ request.session['message'] = 'Error removing collaborator %s' % email
+
+ return HttpResponseRedirect('/dashboard/application/%s' % application_name)
+
+# GET /dashboard/application//logs
+@http_methods('GET')
+@auth_required
+@_check_application_access
+def logs(request, application_name):
+ return HttpResponse(retrieve_logs(application_name), content_type='text/plain')
+
+# POST /dashboard/application//debug?enable_debug=yes
+@http_methods('POST')
+@token_required
+@auth_required
+@_check_application_access
+def debug_redirect(request, application_name):
+ _application_debug(request, application_name)
+ return HttpResponseRedirect('/dashboard/application/%s' % application_name)
+
+# Called by application_debug_redirect()
+#@http_methods('GET', 'POST')
+#@token_required
+#@auth_required
+#@_check_application_access
+def _application_debug(request, application_name):
+ if request.method == 'POST':
+ enable_debug = not not request.POST.get('enable_debug')
+ toggle_debug(application_name, enable_debug)
+
+ elif request.method == 'GET':
+ enable_debug = Application.get_by_name(application_name).debug
+ return HttpResponse(enable_debug)
+
+# POST /dashboard/application//server_cache?enable_server_cache=yes
+@http_methods('POST')
+@token_required
+@auth_required
+@_check_application_access
+def server_cache_redirect(request, application_name):
+ server_cache = not not request.POST.get('enable_server_cache')
+ if server_cache:
+ enable_server_cache(application_name)
+ else:
+ disable_server_cache(application_name)
+ return HttpResponseRedirect('/dashboard/application/%s' % application_name)
+
+# POST /dashboard/application//allocation
+@http_methods('POST')
+@token_required
+@auth_required
+@_check_application_access
+def application_allocation_redirect(request, application_name):
+ if _has_billing_info(application_name):
+ _application_allocation(request, application_name)
+ return HttpResponseRedirect('/dashboard/application/%s' % application_name)
+ else:
+ request.session['message'] = "We need your billing info first!"
+ return HttpResponseRedirect('/dashboard/billing')
+
+def _require_int(str_int):
+ try:
+ return int(str_int)
+ except:
+ return None
+
+# Called by application_allocation_redirect()
+#@http_methods('POST')
+#@token_required
+#@auth_required
+#@_check_application_access
+def _application_allocation(request, application_name):
+ application_processes = _require_int(request.POST.get('application_instances'))
+ if application_processes == None:
+ return HttpResponseBadRequest('Missing argument: application_instances')
+ background_processes = _require_int(request.POST.get('background_workers'))
+ if background_processes == None:
+ return HttpResponseBadRequest('Missing argument: background_workers')
+ result = update_application_allocation(application_name, {'application_processes':application_processes, 'background_processes':background_processes})
+ if not result:
+ return HttpResponseServerError('There was a problem saving changes. Djangy staff has been notified.')
+
+# called by application_allocation_redirect
+def _has_billing_info(application_name):
+ app = Application.get_by_name(application_name)
+ if not app:
+ return False
+ cust_id = app.account.customer_id
+ if cust_id == '-1' or cust_id == '' or cust_id is None:
+ return False
+ return True
+
+_domain_name_regex = re.compile('^[A-Za-z0-9-][A-Za-z0-9-\.]*[A-Za-z0-9-]$')
+
+def _valid_custom_domain(domain):
+ return domain \
+ and _domain_name_regex.match(domain) != None \
+ and not domain.endswith('.djangy.com') \
+ and domain != 'djangy.com'
+
+# POST /dashboard/application//add_domain
+@http_methods('POST')
+@token_required
+@auth_required
+@_check_application_access
+def add_domain_redirect(request, application_name):
+ domain = request.REQUEST.get('domain')
+ if _valid_custom_domain(domain):
+ add_domain_name(application_name, domain)
+ return HttpResponseRedirect('/dashboard/application/%s' % application_name)
+
+# GET /dashboard/application//remove_domain?sessionid=...
+@http_methods('GET')
+@token_required
+@auth_required
+@_check_application_access
+def remove_domain_redirect(request, application_name):
+ domain = request.REQUEST.get('domain')
+ if _valid_custom_domain(domain):
+ delete_domain_name(application_name, domain)
+ return HttpResponseRedirect('/dashboard/application/%s' % application_name)
+
diff --git a/src/server/master/web_ui/application/web_ui/main/views/dashboard_applicationlist.py b/src/server/master/web_ui/application/web_ui/main/views/dashboard_applicationlist.py
new file mode 100644
index 0000000..ca266a6
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/views/dashboard_applicationlist.py
@@ -0,0 +1,17 @@
+from shared import *
+
+@http_methods('GET')
+@auth_required
+def applicationlist(request):
+ message = get_session_message(request)
+ email = request.session.get('email')
+
+ user = User.get_by_email(email)
+ applications = user.get_accessible_applications()
+ return render_to_response('dashboard_applicationlist.html', {
+ 'navbar_section':'dashboard',
+ 'applications':applications,
+ 'user':user,
+ 'email':email,
+ 'message': message
+ })
diff --git a/src/server/master/web_ui/application/web_ui/main/views/dashboard_billing.py b/src/server/master/web_ui/application/web_ui/main/views/dashboard_billing.py
new file mode 100644
index 0000000..f7f2667
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/views/dashboard_billing.py
@@ -0,0 +1,83 @@
+from shared import *
+from master_api import update_billing_info as do_update_billing_info
+
+# XXX - CSRF
+@http_methods('GET', 'POST')
+@auth_required
+def update_billing_info(request):
+ email = request.session.get('email')
+ user = User.get_by_email(email)
+ REQUIRED_KEYS = [
+ 'first_name',
+ 'last_name',
+ 'cc_number',
+ 'cvv',
+ 'expiration_month',
+ 'expiration_year'
+ ]
+ if request.method == 'GET':
+ info = retrieve_billing_info(user)
+ # if the values are in the session, restore them and remove from session
+ for key in REQUIRED_KEYS:
+ try:
+ value = request.session.get(key, None)
+ if value:
+ info[key] = value
+ del request.session[key]
+ except:
+ pass
+ message = get_session_message(request)
+ amount = None
+ usage = None
+ try:
+ amount = int(info['usage'])
+ dollars = (amount / 100)
+ cents = (amount % 100)
+ usage = "$%s.%02d" % (dollars, cents)
+ except:
+ pass
+ return render_to_response('dashboard_billing.html', {
+ 'navbar_section':'dashboard',
+ 'user':user,
+ 'info':info,
+ 'message':message,
+ 'months':cc_months(),
+ 'years':cc_years(),
+ 'usage':usage,
+ })
+ elif request.method == 'POST':
+ email = request.session.get('email')
+ if not email:
+ return HttpResponseRedirect('/dashboard')
+
+ msg_mapper = {
+ 'cc_number':'Card number',
+ 'exp_month':'Expiration month',
+ 'exp_year':'Expiration year',
+ 'cvv':'CVV',
+ 'first_name':'First name',
+ 'last_name':'Last name'
+ }
+ info = dict()
+ for k in REQUIRED_KEYS:
+ value = request.POST.get(k, None)
+ info[k] = value
+ if k != 'cc_number':
+ request.session[k] = value
+ for k in REQUIRED_KEYS:
+ if info[k] is None or info[k] == '':
+ request.session['message'] = 'Missing: %s' % msg_mapper.get(k, k)
+ return HttpResponseRedirect('/dashboard/billing')
+
+ message = do_update_billing_info(email, info)
+ if True == message:
+ for k in REQUIRED_KEYS:
+ try:
+ del request.session[k]
+ except:
+ pass
+ request.session['message'] = 'Your billing settings have been saved. Thanks!'
+ else:
+ request.session['message'] = message
+ return HttpResponseRedirect('/dashboard/billing')
+ return HttpResponseRedirect('/dashboard')
diff --git a/src/server/master/web_ui/application/web_ui/main/views/dashboard_invite.py b/src/server/master/web_ui/application/web_ui/main/views/dashboard_invite.py
new file mode 100644
index 0000000..e16fc66
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/views/dashboard_invite.py
@@ -0,0 +1,24 @@
+from shared import *
+import admin
+
+@http_methods('GET', 'POST')
+@auth_required
+def invite(request):
+ if request.method == 'POST':
+ admin.invite(request)
+
+ message = get_session_message(request)
+ email = request.session.get('email')
+ user = User.get_by_email(email)
+
+ num_invited = User.objects.filter(referrer=user).count() + WhiteList.objects.filter(referrer=user).count()
+ num_remaining_invitations = user.invite_limit - num_invited
+
+ return render_to_response('dashboard_invite.html', {
+ 'navbar_section': 'dashboard',
+ 'user': user,
+ 'email': email,
+ 'sessionid': request.COOKIES['sessionid'],
+ 'message': message,
+ 'num_remaining_invitations': num_remaining_invitations
+ })
diff --git a/src/server/master/web_ui/application/web_ui/main/views/index.py b/src/server/master/web_ui/application/web_ui/main/views/index.py
new file mode 100644
index 0000000..241902f
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/views/index.py
@@ -0,0 +1,21 @@
+from shared import *
+
+@http_methods('GET')
+def index(request):
+ message = get_session_message(request)
+ email = request.session.get('email')
+ if email:
+ user = User.get_by_email(email)
+ else:
+ user = None
+ return render_to_response('index.html', {'navbar_section':'home', 'message':message, 'user':user, 'index':True})
+
+@http_methods('GET')
+def pricing(request):
+ message = get_session_message(request)
+ email = request.session.get('email')
+ if email:
+ user = User.get_by_email(email)
+ else:
+ user = None
+ return render_to_response('pricing.html', {'navbar_section':'pricing', 'message':message, 'user':user, 'index':False})
diff --git a/src/server/master/web_ui/application/web_ui/main/views/login_logout.py b/src/server/master/web_ui/application/web_ui/main/views/login_logout.py
new file mode 100644
index 0000000..a7d1cce
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/views/login_logout.py
@@ -0,0 +1,106 @@
+from shared import *
+
+@http_methods('GET', 'POST')
+def login(request):
+ if request.session.get('email'):
+ return HttpResponseRedirect('/dashboard')
+
+ if request.method == 'GET':
+ return render_to_response('login.html', {'navbar_section':'login'})
+
+ email = request.POST.get('email')
+ password = request.POST.get('password')
+
+ # Check the login email address and hashed password
+ try:
+ validate_email(email)
+ user = User.get_by_email(email)
+ assert check_password(email, password, user.passwd)
+ except:
+ return render_to_response('login.html', {'navbar_section':'login', 'message':'Incorrect email address or password. Please try again.'})
+
+ # set session data
+ request.session['email'] = email
+
+ # redirect to the dashboard
+
+ return HttpResponseRedirect('/dashboard')
+
+# XXX - CSRF?
+@http_methods('GET', 'POST')
+def logout(request):
+ try:
+ del request.session['email']
+ except KeyError:
+ pass
+ request.session['message'] = 'You have been logged out.'
+ return HttpResponseRedirect('/login')
+
+@http_methods('GET', 'POST')
+def reset_password(request):
+ reset_hash = request.GET.get('reset', None)
+ if not reset_hash:
+ request.session['message'] = 'No reset code supplied.'
+ return HttpResponseRedirect('/')
+
+ email = request.GET.get('email', None)
+ if not email:
+ request.session['message'] = 'No email code supplied.'
+ return HttpResponseRedirect('/')
+
+ user = User.get_by_email(email)
+ if not user:
+ request.session['message'] = 'Invalid user.'
+ return HttpResponseRedirect('/')
+
+ if check_password(email, user.passwd, reset_hash):
+ # legit request, go ahead and process
+ return render_to_response('reset_password_form.html', {'email': email})
+ request.session['message'] = 'Invalid reset hash.'
+ return HttpResponseRedirect('/')
+
+@http_methods('POST')
+def set_password(request):
+ email = request.POST.get("email", None)
+ if not email:
+ return HttpResponseRedirect('/')
+ password1 = request.POST.get('password1', None)
+ password2 = request.POST.get('password2', None)
+ if not password1 or not password2 or password1 != password2:
+ request.session['message'] = 'Your passwords did not match.'
+ return render_to_response('reset_password_form.html', {'email':email})
+ user = User.get_by_email(email)
+ if not user:
+ return HttpResponseRedirect('/')
+ user.passwd = hash_password(email, password1)
+ user.save()
+ request.session['message'] = 'Your password has been reset.'
+ request.session['email'] = email
+ return HttpResponseRedirect('/dashboard')
+
+@http_methods('POST', 'GET')
+def request_reset_password(request):
+ if request.method.lower() == 'post':
+ # send the email
+ email = request.POST.get('email', None)
+ if not email:
+ return HttpResponseRedirect('/')
+ user = User.get_by_email(email)
+ if not user:
+ return HttpResponseRedirect('/')
+ reset_hash = hash_password(email, user.passwd)
+ message_body = """
+
+A password reset request has been requested for the Djangy account owned by this email address. If this is correct, please click on the following link:
+
+https://www.djangy.com/reset_password?email=%s&reset=%s
+
+If not, please simply disregard this message or contact support@djangy.com.
+
+-Djangy
+ """ % (email, reset_hash)
+ result = send_mail('Password Reset request', message_body, 'support@djangy.com', [email], fail_silently=False)
+ request.session['message'] = 'Please check your email for a link to reset your password.'
+ return HttpResponseRedirect('/')
+ else: # GET request
+ return render_to_response('request_reset_password.html')
diff --git a/src/server/master/web_ui/application/web_ui/main/views/shared.py b/src/server/master/web_ui/application/web_ui/main/views/shared.py
new file mode 100644
index 0000000..1976fd5
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/main/views/shared.py
@@ -0,0 +1,104 @@
+from django.shortcuts import render_to_response
+from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotFound, HttpResponseNotAllowed, HttpResponseRedirect, HttpResponseServerError
+from django.core.validators import validate_email
+from django.core.mail import send_mail
+from django.db.models import Sum
+from web_ui.main.utils import check_password, hash_password
+from web_ui.main.models import *
+from web_ui.main.invite_code import gen_invite_code
+from management_database import *
+from master_api import *
+import os, logging
+from urllib import urlencode
+from datetime import datetime
+
+#
+# Decorators that abstract out common checks for views.
+#
+
+# Decorator for views that require users to be logged in.
+def auth_required(func):
+ """ Decorator for views that require users to be logged in. """
+ def _auth_required(request, *args, **kwargs):
+ if not request.session.get('email'):
+ return HttpResponseRedirect('/login')
+ return func(request, *args, **kwargs)
+ return _auth_required
+
+# Decorator for views that perform an action and hence must be protected against CSRF.
+def token_required(func):
+ """ Decorator for views that perform an action and hence must be protected against CSRF. """
+ def _token_required(request, *args, **kwargs):
+ posted_session_id = request.REQUEST.get('sessionid')
+ if posted_session_id != request.COOKIES['sessionid']:
+ return HttpResponseForbidden('Invalid session information.')
+ return func(request, *args, **kwargs)
+ return _token_required
+
+# Decorator for views only accessible to admin users.
+def admin_required(func):
+ """ Decorator for views only accessible to admin users. """
+ def _admin_required(request, *args, **kwargs):
+ user = User.get_by_email(request.session.get('email'))
+
+ if not user.admin:
+ return HttpResponseRedirect('/dashboard')
+ return func(request, *args, **kwargs)
+ return _admin_required
+
+# Decorator for views that only accept certain HTTP request methods (e.g., GET, POST).
+def http_methods(*methods):
+ """ Decorator for views that only accept certain HTTP request methods (e.g., GET, POST). """
+ def http_methods_decorator(func):
+ def _http_methods(request, *args, **kwargs):
+ if not request.method in methods:
+ return HttpResponseNotAllowed(methods)
+ else:
+ return func(request, *args, **kwargs)
+ return _http_methods
+ return http_methods_decorator
+
+#
+# Helper functions
+#
+
+def get_session_message(request):
+ """ Remove and return the message stored in the session. This is a poor
+ design which can mess up if the user runs two concurrent requests in
+ the same session (race condition). """
+ message = request.session.get('message')
+ try:
+ del request.session['message']
+ except:
+ pass
+ return message
+
+# Return True or False the status of whether or not the current session is an admin
+def is_admin(request):
+ email = request.session.get("email", None)
+ if not email:
+ return False
+ user = User.get_by_email(email)
+ if not user:
+ return False
+ return user.admin
+
+def get_user(request):
+ email = request.session.get("email", None)
+ if not email:
+ return False
+ return User.get_by_email(email)
+
+def cc_years():
+ current_year = datetime.now().year
+ return range(current_year, current_year + 12)
+
+def cc_months():
+ months = []
+ for month in range(1, 13):
+ if len(str(month)) == 1:
+ numeric = '0' + str(month)
+ else:
+ numeric = str(month)
+ months.append(numeric)
+ return months
diff --git a/src/server/master/web_ui/application/web_ui/manage.py b/src/server/master/web_ui/application/web_ui/manage.py
new file mode 100644
index 0000000..6ce754c
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/manage.py
@@ -0,0 +1,12 @@
+#!/usr/bin/env python
+
+from django.core.management import execute_manager
+try:
+ import settings # Assumed to be in the same directory.
+except ImportError:
+ import sys
+ sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+ sys.exit(1)
+
+if __name__ == "__main__":
+ execute_manager(settings)
diff --git a/src/server/master/web_ui/application/web_ui/settings.py b/src/server/master/web_ui/application/web_ui/settings.py
new file mode 100644
index 0000000..0ecca56
--- /dev/null
+++ b/src/server/master/web_ui/application/web_ui/settings.py
@@ -0,0 +1,130 @@
+# Django settings for web_ui project.
+import django, os
+
+DEBUG = False
+TEMPLATE_DEBUG = DEBUG
+
+DJANGO_ROOT = os.path.dirname(os.path.realpath(django.__file__))
+SITE_ROOT = os.path.dirname(os.path.realpath(__file__))
+
+ADMINS = (
+ ('Bob Jones', 'bob@jones.mil')
+)
+
+MANAGERS = ADMINS
+
+DATABASES = {
+ 'default': {
+ 'ENGINE':'mysql',
+ 'NAME':'web_ui',
+ 'USER':'web_ui',
+ 'PASSWORD':'password goes here',
+ 'HOST':'',
+ 'PORT':'',
+ },
+ 'management_database': {
+ 'ENGINE': 'mysql', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
+ 'NAME': 'djangy', # Or path to database file if using sqlite3.
+ 'USER': 'djangy', # Not used with sqlite3.
+ 'PASSWORD': 'password goes here', # Not used with sqlite3.
+ 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
+ 'PORT': '', # Set to empty string for default. Not used with sqlite3.
+ }
+}
+
+DATABASE_ROUTERS = ['main.Router']
+
+EMAIL_HOST = 'smtp.gmail.com'
+EMAIL_PORT = 587
+EMAIL_HOST_USER = 'admin@djangy.com'
+EMAIL_HOST_PASSWORD = 'password goes here'
+EMAIL_USE_TLS = True
+
+# Local time zone for this installation. Choices can be found here:
+# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
+# although not all choices may be available on all operating systems.
+# On Unix systems, a value of None will cause Django to use the same
+# timezone as the operating system.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = 'America/Chicago'
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'en-us'
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# If you set this to False, Django will not format dates, numbers and
+# calendars according to the current locale
+USE_L10N = True
+
+# Absolute path to the directory that holds media.
+# Example: "/home/media/media.lawrence.com/"
+MEDIA_ROOT = ''
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash if there is a path component (optional in other cases).
+# Examples: "http://media.lawrence.com", "http://example.com/media/"
+MEDIA_URL = ''
+
+# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
+# trailing slash.
+# Examples: "http://foo.com/media/", "/media/".
+ADMIN_MEDIA_PREFIX = '/media/'
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = 'password goes here'
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+ 'django.template.loaders.filesystem.Loader',
+ 'django.template.loaders.app_directories.Loader',
+# 'django.template.loaders.eggs.Loader',
+)
+
+MIDDLEWARE_CLASSES = (
+ 'django.middleware.common.CommonMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ #'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+)
+
+ROOT_URLCONF = 'web_ui.urls'
+
+TEMPLATE_DIRS = (
+ # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
+ # Always use forward slashes, even on Windows.
+ # Don't forget to use absolute paths, not relative paths.
+ os.path.join(SITE_ROOT, 'templates')
+)
+
+INSTALLED_APPS = (
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ 'django.contrib.messages',
+ 'web_ui.main',
+ 'web_ui.docs',
+ 'management_database',
+ 'south',
+ 'sentry.client',
+ # Uncomment the next line to enable the admin:
+ # 'django.contrib.admin',
+)
+SENTRY_KEY = 'password goes here'
+SENTRY_REMOTE_URL = 'http://logsentry.djangy.com/sentry/store/'
+
+import logging
+from sentry.client.handlers import SentryHandler
+
+logging.getLogger().addHandler(SentryHandler())
+
+# Add StreamHandler to sentry's default so you can catch missed exceptions
+logging.getLogger('sentry').addHandler(logging.StreamHandler())
+
diff --git a/src/server/master/web_ui/application/web_ui/static/assets/fonts/DroidSans-Bold.eot b/src/server/master/web_ui/application/web_ui/static/assets/fonts/DroidSans-Bold.eot
new file mode 100644
index 0000000000000000000000000000000000000000..cd212f8a752e3e6e7fe5b8e1270eda8f63e0fbc5
GIT binary patch
literal 30374
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≥$N7pC3(TO^4kU-sPTRE#o6wy=L65bXVt0k#Y
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