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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + +
+ + + + +
+

Application Error

+
+ +
+ + + +
+ + +
+
+ +

The application you requested encountered an error.


+ + +

If you are the developer of this application, check the logs in the Djangy dashboard or using +the “djangy logs” command to diagnose the 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + +
+ + + + +
+

Application Error

+
+ +
+ + + +
+ + +
+
+ +

The application you requested is not running.


+ + +

If you are the developer of this application, the following instructions may help you fix this problem: + +

    +
  • Make sure you created a valid Django project, committed it to your git repository, and successfully deployed it using “git push djangy master”.
  • + +
  • If you already deployed this application, check the logs in the Djangy dashboard or using the “djangy logs” command.
  • + +
  • Please read the Djangy documentation or email support@djangy.com for more information.
  • +
+ +
+
+ + +
+ + + + + + +
+ + +
+ + + 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

+ +
+ +
+ + {% if form.name.errors %}{{ form.name.errors }}{% endif %} +
+ {{ form.name }} +
+
+ +
+ + {% if form.content.errors %}{{ form.content.errors }}{% endif %} +
+ {{ form.content }} +
+
+ +
+ +
+ +
+{% 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 %} +

{{ page.name }}

+ {% 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\n

Djangy has several key components:\r\ + \n

\r\n

\r\n
\r\n\r\ + \n

ProxyCache

\r\n
\r\n

The 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

Masters

\r\n
\r\n

Our 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

Workers

\r\ + \n
\r\n

Workers 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

Bundles

\r\n
\r\n

When 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

Databases

\r\n
\r\n

Djangy 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\n

You 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: "

Djangy's Architecture

\r\n

Djangy has several key components:\r\n

\r\n

\r\n
\r\n\r\n

ProxyCache

\r\n
\r\n

The\ + \ 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

Masters

\r\n\r\n

Our 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

Workers

\r\n
\r\n

Workers 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

Bundles

\r\n
\r\n

When\ + \ 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

Databases

\r\ + \n
\r\n

Djangy 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\n

You 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\n

Django's Admin app\ + \ is a popular way to manage authentication and\r\npermissions. Djangy fully\ + \ supports the admin panel.

\r\n\r\n

Usually 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\n

createsuperuser

\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\n

Assuming 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\ + \n

Django's Admin\ + \ app is a popular way to manage authentication and\r\npermissions. Djangy fully supports the admin panel.

\r\n\r\n

Usually 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\n

createsuperuser

\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\n

Assuming 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\n

Djangy 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\n

Also 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: "

Backups

\r\ + \n
\r\n

Djangy 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\n

Also 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
  • Introduction to background jobs
  • \r\n
  • Using celery
  • \r\n
\r\n
\r\n\r\n

Introduction 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\n

Djangy\ + \ gives you this ability by utilizing the popular Celery project.

\r\n
\r\n\r\n

Using celery

\r\n
\r\n

Using 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\n

You'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\n

To 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\n

The 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\n

Add the\ + \ following to your djangy.pip:

\r\ + \n
\r\ndjango-celery\r\nghettoq\r\n
\r\n

Don't forget to git\ + \ commit your changes and push!

\r\n

Before 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\n

For more information on how to utilize celery for background\ + \ tasks, check out their Official documentation.

", name: Celery, rendered: "

Background jobs with Celery

\r\n
\r\n\r\n
\r\n\r\n

Introduction\ + \ 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\ + \n

Djangy gives you this ability\ + \ by utilizing the popular Celery project.

\r\ + \n
\r\n\r\n

Using celery

\r\n
\r\n

Using 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\n

You'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\n

To 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\ + \n

The 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\n

Add the following to your Dependencies\">djangy.pip:

\r\n
\r\ndjango-celery\r\
+      \nghettoq\r\n
\r\n

Don't forget\ + \ to git commit your changes and push!

\r\n

Before 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\n

For 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\ + \n

The 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

Installation

\r\n
\r\n

Assuming 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

Usage

\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: "

Installing the Djangy command-line\ + \ client

\r\n
\r\n

The 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

Installation

\r\n
\r\n

Assuming 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

Usage

\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\n

Adding Collaborators

\r\ + \n
\r\n

Collaborating 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\n

Removing 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: "

Collaboration

\r\n
\r\n

Adding Collaborators

\r\n
\r\n

Collaborating 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

Removing\ + \ 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\".

"} + model: docs.page + pk: 24 +- fields: {content: "

Optional configuration files

\r\n
\r\n

Djangy\ + \ 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\n

Contains 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\n

git_repo/djangy.eggs

\r\n
\r\n

See Managing Dependencies.

\r\n

git_repo/djangy.pip

\r\n
\r\n\ +

See Managing Dependencies.

\r\n
\r\ + \n

git_repo/djangy.config

\r\n
\r\n

Configures 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\n

Important: 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\n

Djangy\ + \ 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\n

Contains 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\n

git_repo/djangy.eggs

\r\ + \n
\r\n

See Dependencies\">Managing Dependencies.

\r\ + \n

git_repo/djangy.pip

\r\n
\r\n

See Dependencies\"\ + >Managing Dependencies.

\r\n
\r\n

git_repo/djangy.config

\r\n\ +
\r\n

Configures 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\n

Important: 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\n

Djangy\ + \ 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: "

Creating Djangy Applications

\r\n
\r\n

Djangy 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\n

Sooner 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\n

syncdb

\r\n\ +
\r\n

Assuming 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\n

migrate

\r\n
\r\ + \n

Django'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\n

Djangy fully supports\ + \ South, using the following workflow:

\r\n\r\n
    \r\n
  1. Update your Django\ + \ models locally on your development workstation.
  2. \r\n
  3. Run 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.
  4. \r\n
  5. Run 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
  6. \r\n
  7. Run\ + \ 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...]
  8. \r\n
\r\n
\r\n\r\n

loaddata

\r\ + \n
\r\n

loaddata 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\n

dumpdata

\r\n
\r\n

dumpdata 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\n

reset

\r\n
\r\n

reset 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: "

Using a database on Djangy

\r\n\r\n

Sooner\ + \ 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\n

syncdb

\r\n
\r\n

Assuming 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\n

migrate

\r\n
\r\n

Django'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\n

Djangy fully supports South,\ + \ using the following workflow:

\r\n\r\n
    \r\n
  1. Update your Django models locally\ + \ on your development workstation.
  2. \r\n
  3. Run 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.
  4. \r\ + \n
  5. Run 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
  6. \r\n
  7. Run 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...]
  8. \r\n
\r\n
\r\ + \n\r\n

loaddata

\r\n
\r\n

loaddata\ + \ 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\n

dumpdata

\r\n
\r\n

dumpdata\ + \ 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\n

reset

\r\n
\r\n

reset 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\n

Python 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\n

If 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\n

In 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\n

djangy.pip file format

\r\n\ +
\r\n

The 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\n

djangy.eggs file format

\r\n\r\n

The 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\n

For\ + \ 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\n
Django>=1.2\r\nSouth\r\nMako\r\n./local_eggs/my_package.egg\r\
+      \nhttp://djangy.com/example.egg
\r\n\r\n

Note 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\n

In 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\n

You 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: "

Managing Dependencies

\r\ + \n
\r\n

Python 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\n

If\ + \ 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\n

In 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\n

djangy.pip file format

\r\n\ +
\r\n

The 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\n

djangy.eggs file format

\r\n\r\n

The 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\n

For 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\n
Django>=1.2\r\nSouth\r\nMako\r\n./local_eggs/my_package.egg\r\nhttp://djangy.com/example.egg
\r\ + \n\r\n

Note 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\n

In 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\n

You 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\n

After 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: "

Deploying with Git

\r\n\r\n

After 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\n

Python 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\n

If 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\n

djangy.pip file format

\r\n\r\n

The optional djangy.pip\ + \ file is used to specify Python source packages to install. It follows the\ + \ pip\ + \ requirements file format.

\r\n\r\n

In 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\n

djangy.eggs file format

\r\n\r\n

The 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\n

For\ + \ 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\n
Django>=1.2\r\nSouth\r\nMako\r\n./local_eggs/my_package.egg\r\
+      \nhttp://djangy.com/example.egg
\r\n\r\n

Note 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\n

In 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\n

You 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: "

Installing dependencies

\r\ + \n\r\n

Python 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\n

If\ + \ 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\n

djangy.pip file format

\r\ + \n\r\n

The optional djangy.pip\ + \ file is used to specify Python\ + \ source packages to install. It follows\ + \ the pip\ + \ requirements file format.

\r\n\r\n

In 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\n

djangy.eggs file format

\r\n\ + \r\n

The 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\n

For 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\n
Django>=1.2\r\nSouth\r\nMako\r\n./local_eggs/my_package.egg\r\nhttp://djangy.com/example.egg
\r\ + \n\r\n

Note 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\n

In 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\n

You 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\n

Configuring 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\n

If 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\n

Finally,

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: "

Configuring email

\r\n

Configuring 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\n

If 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\n

Finally,\ + \

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\n
Why\ + \ do you need my billing information?
\r\n
\r\n

Djangy 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\n
How do you prorate\ + \ charges?
\r\n
\r\n

Worker 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\n
How do I delete my account?
\r\n\ +
\r\n

To 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: "

Billing and Pricing

\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\n
Why do you need my billing information?
\r\ + \n
\r\n

Djangy 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\n
How do you prorate\ + \ charges?
\r\n
\r\n

Worker\ + \ 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\n
How do I delete my account?
\r\ + \n
\r\n

To 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\n

Python 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\n

If 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\n

djangy.pip file format

\r\n\r\n

The optional djangy.pip\ + \ file is used to specify Python source packages to install. It follows the\ + \ pip\ + \ requirements file format.

\r\n\r\n

In 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\n

djangy.eggs file format

\r\n\r\n

The 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\n

For\ + \ 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\n
Django>=1.2\r\nSouth\r\nMako\r\n./local_eggs/my_package.egg\r\
+      \nhttp://djangy.com/example.egg
\r\n\r\n

Note 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\n

In 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\n

You 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: "

Installing dependencies

\r\ + \n\r\n

Python 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\n

If\ + \ 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\n

djangy.pip file format

\r\ + \n\r\n

The optional djangy.pip\ + \ file is used to specify Python\ + \ source packages to install. It follows\ + \ the pip\ + \ requirements file format.

\r\n\r\n

In 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\n

djangy.eggs file format

\r\n\ + \r\n

The 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\n

For 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\n
Django>=1.2\r\nSouth\r\nMako\r\n./local_eggs/my_package.egg\r\nhttp://djangy.com/example.egg
\r\ + \n\r\n

Note 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\n

In 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\n

You 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\n

There are two options for logging\ + \ in your Djangy app:\r\n

\r\n

\r\n
\r\n\r\n

Standard\ + \ Logging

\r\n
\r\n

To 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\n

NOTE: 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\n

Using\ + \ django-sentry for enhanced logging

\r\n
\r\n

If 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\n

Then, 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\ + \n

Finally, 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\n

Sentry 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\n

For more information,\ + \ see the official documentation.

", name: Logging, rendered: "

Logging

\r\n
\r\n

There are two options for logging in your Djangy app:\r\n

\r\ + \n

\r\n
\r\n\r\n

Standard Logging

\r\n\r\n

To 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\n

NOTE:\ + \ 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\n

Using django-sentry for enhanced logging

\r\n
\r\n

If 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\n

Then, 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\n

Finally, 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\n

Sentry 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\n

For more information, see the official documentation.

"} + model: docs.page + pk: 17 +- fields: {content: "

Manage.py Commands

\r\n
\r\n

The 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

Usage

\r\n
\r\n

Executing 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

Exceptions

\r\ + \n
\r\n

There 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: "

Manage.py Commands

\r\n
\r\n

The\ + \ 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

Usage

\r\n
\r\n

Executing\ + \ 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

Exceptions

\r\ + \n
\r\n

There 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: " ", 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\n

Djangy 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

Prerequisites

\r\n
\r\n

To use Djangy, you need three\ + \ things:\r\n

\r\n

\r\n\r\n\r\n

Deploying to Djangy

\r\n
\r\ + \n
1. Ensure your application is in a valid Git repository
\r\n
\r\ + \n

If 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\n
2. Ensure\ + \ you have a public SSH key
\r\n
\r\n

Git 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
3. Create your app on Djangy
\r\n
\r\n

From 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\n

You 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\n
4. 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\n
5. 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\n

As 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\n

Epilogue

\r\ + \n
\r\n

Congratulations, 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\n

As\ + \ always, you can email support@djangy.com\ + \ if you have any questions, problems, or suggestions. We welcome your feedback!

", + name: Quickstart, rendered: "

Djangy\ + \ Quickstart Guide

\r\n
\r\n

Djangy\ + \ 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

Prerequisites

\r\n
\r\n

To use Djangy, you need three things:\r\n

\r\n

\r\n
\r\n\r\n

Deploying\ + \ to Djangy

\r\n
\r\n
1.\ + \ Ensure your application is in a\ + \ valid Git repository
\r\n
\r\ + \n

If 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\n
2. Ensure you have a public SSH key
\r\ + \n
\r\n

Git 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
3. Create your app on Djangy
\r\n
\r\n

From\ + \ 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\n

You 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\n
4. 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\n
5. 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\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

As 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

Epilogue

\r\ + \n
\r\n

Congratulations,\ + \ 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\n

As 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\n

Web 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\n

Here 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\n

For 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\n

Web 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\n

Here 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\n

For 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\n

Web 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\n

Here 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\n

For 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\n

Web 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\n

Here 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\n

For 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\n

We recommend\ + \ using django-storages along with Amazon's S3 service\ + \ to handle file uploads.

\r\n\r\n
\r\n\r\n

Specifying dependencies

\r\n
\r\n

Add 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\n

Required settings

\r\n
\r\n

First, 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\n

Usage

\r\n
\r\n

Now 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\n

That'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: "

Storing File uploads with S3

\r\n
\r\n

We recommend\ + \ using django-storages along with Amazon's S3 service to handle\ + \ file uploads.

\r\n\r\ + \n
\r\n\r\n

Specifying dependencies

\r\n
\r\n

Add 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

Required settings

\r\n
\r\n

First, 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

Usage

\r\n
\r\n

Now\ + \ 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\n

That'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\n

A 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\n

There 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\n

Option 1: implicit configuration

\r\n

Put 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\n

Option 2: explicit relative paths

\r\ + \n

Unless 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\n

For 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\n

Now, 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\n

Option 3:\ + \ explicit absolute paths

\r\n

If 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: "

Configuring Django TEMPLATE_DIRS

\r\n\ + \r\n

A 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\n

There 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\n

Option 1: implicit\ + \ configuration

\r\n

Put 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\n

Option 2: explicit relative paths

\r\ + \n

Unless 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\n

For 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\n

Now, 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\n

Option 3: explicit absolute\ + \ paths

\r\n

If 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\n

One 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\n

To 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\n

For more information, see the official Django docs.\r\n", name: UsingTheShell, rendered: "

Using the Shell

\r\n
\r\n

One\ + \ 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\n

To 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\n

For more information, see the official Django docs.\r\ + \n"} + model: docs.page + pk: 26 +- fields: {content: "

Introduction to Djangy

\r\n\r\n

Djangy 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

Instant deployment

\r\n
\r\n

Deploying your app\ + \ is as simple as doing a familiar \"git push\". Our system does the rest.

\r\ + \n
\r\n

Instant scaling

\r\n
\r\n

Quickly throw more resources\ + \ behind your app to handle higher loads and more traffic.

\r\n
\r\n\ +

Familiar controls

\r\n
\r\n

Interact 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\n

Pay only\ + \ for what you use

\r\n
\r\n

Djangy 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\n

For more information on how Djangy works, check\ + \ out Djangy's architecture.

", name: WhatIsDjangy, + rendered: "

Introduction to\ + \ Djangy

\r\n\r\n

Djangy 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

Instant deployment

\r\ + \n
\r\n

Deploying your\ + \ app is as simple as doing a familiar \"git push\". Our system does the rest.

\r\n
\r\n

Instant scaling

\r\n
\r\n

Quickly throw more resources behind your app to handle higher loads and\ + \ more traffic.

\r\n
\r\n

Familiar controls

\r\n
\r\n

Interact 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\n

Pay only for what you use

\r\n
\r\n

Djangy 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\n

For 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: + +
+

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 %} +
+

Account

+ +
+{% endblock %} +{% block content %} +
+
+

Change password

+
    +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
+
+
+
+

Change email address

+
    +
  • Current:   {{ email }}
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
+
+
+

+ +

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 %} + + + + + + + + {% comment %} + + {% endcomment %} + + + + + + + + + + + + + + + + + + +

Total Cost

+

$0/month estimated

+ +

Performance

+
+ Application instances: + +      + Background workers: + +      + + + +
+ + +
+ Crank up your application instances to support more concurrent + users. Run queued tasks using background workers. +
+

 

SNI SSL

+
+ + + $5/month   + Purchase SNI SSL to enable secure https connections using + your custom domain.
+ An SNI-compatible SSL certificate must be separately + purchased from a certificate authority. +
+ +
+
+

Domains

+ {{ application_name }}.djangy.com
+ Custom domains:
+ {% for custom_domain in custom_domains %} + {{ custom_domain }} (remove)
+ {% endfor %} +
+ + + +
+

 

Django Debug

+
+ + +    (Should be disabled for production sites) +
+ +
+
+

Server Cache

+
+ + +    (Should be enabled for production sites) +
+ +
+
+

Logs

+
+ +
+

 

Collaboration

+ Owner: {{ owner_email }}
+ Collaborators:
+ {% for collaborator_email in collaborator_emails %} + {{ collaborator_email }} (remove)
+ {% endfor %} +
+ + + +
+

 

Delete

+
+ Really delete?   + + +

+ +
+
+{% 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 %} +
+

Applications

+ +
+{% 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 %} +
+

{{ app.name }}

+ {% comment %} + Resource settings + {% endcomment %} +
+ {% 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 %} +
+

Billing

+ +
+{% endblock %} +{% block content %} + + +
+
+ + + + + + + + + + + + + + + + + {% if info.cc_number %} + + + + + {% endif %} + + + + + + + + + + + + +
+ Visa + MasterCard + American Express +
First name
Last name
Card number
(currently {{ info.cc_number }})
CVV
Expires + + / + +
+
+
+ {% 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 %} +
+

Invite

+ +
+{% 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! +

+

+ + +

+
+
+

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 %} +
+
+
    +
  • +
  • +
  • +
+
+ Forgot your password? +
+{% 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.

+ + + + + + + + + + + + + + + + + + +
instancesprice
first instanceFREE
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.

+ + + + + + + + + + + + + + +
workersprice
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


+ + + + + + + + + + + + + + + + + + + + + +
planpricecapacity
SharedFREE20 MB
Shared Plus$20 / month20 GB

 
+ +

Dedicated database plans


+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
planpriceRAMcorescapacity
Basic$200 / month1.7 GB12 TB
Plus$800 / month7.5 GB42 TB
Pro$1600 / month15 GB82 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 @@ +
+
    +
  • + {% if invite_code %} +
  • + {% endif %} +
  • +
  • +
  • +
+
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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 k 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 literal 0 HcmV?d00001 diff --git a/src/server/master/web_ui/application/web_ui/static/assets/fonts/DroidSans.eot b/src/server/master/web_ui/application/web_ui/static/assets/fonts/DroidSans.eot new file mode 100644 index 0000000000000000000000000000000000000000..aba32b09bc26217280cef14febf51a72407197cb GIT binary patch literal 30002 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 Dm8_&) literal 0 HcmV?d00001 diff --git a/src/server/master/web_ui/application/web_ui/static/assets/fonts/Google Android License.txt b/src/server/master/web_ui/application/web_ui/static/assets/fonts/Google Android License.txt new file mode 100644 index 0000000..1a96dfd --- /dev/null +++ b/src/server/master/web_ui/application/web_ui/static/assets/fonts/Google Android License.txt @@ -0,0 +1,18 @@ +Copyright (C) 2008 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +########## + +This directory contains the fonts for the platform. They are licensed +under the Apache 2 license. diff --git a/src/server/master/web_ui/application/web_ui/static/assets/fonts/demo.html b/src/server/master/web_ui/application/web_ui/static/assets/fonts/demo.html new file mode 100644 index 0000000..528d984 --- /dev/null +++ b/src/server/master/web_ui/application/web_ui/static/assets/fonts/demo.html @@ -0,0 +1,38 @@ + + + + + + + Font Face Demo + + + + + +
+

Font-face Demo for the Droid Sans Font

+ + + +

Droid Sans Regular - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

+ + + +

Droid Sans Bold - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

+ +
+ + diff --git a/src/server/master/web_ui/application/web_ui/static/assets/fonts/stylesheet.css b/src/server/master/web_ui/application/web_ui/static/assets/fonts/stylesheet.css new file mode 100644 index 0000000..226f966 --- /dev/null +++ b/src/server/master/web_ui/application/web_ui/static/assets/fonts/stylesheet.css @@ -0,0 +1,24 @@ +/* + * This CSS file has been generated by fontsquirrel.com and is based on the work of Paul Irish. + * http://paulirish.com/2009/bulletproof-font-face-implementation-syntax/ + * The fonts included are copyrighted by the vendor listed below. + * + * @vendor: Google Android + * @vendorurl: http://code.google.com/android/ + * @licenseurl: http://www.fontsquirrel.com/license/Droid-Sans + * + * + */ + +@font-face { + font-family: 'Droid Sans Regular'; + src: url('DroidSans.eot'); + src: local('Droid Sans Regular'), local('DroidSans-Regular'), url('DroidSans.ttf') format('truetype'); +} + +@font-face { + font-family: 'Droid Sans Bold'; + src: url('DroidSans-Bold.eot'); + src: local('Droid Sans Bold'), local('DroidSans-Bold'), url('DroidSans-Bold.ttf') format('truetype'); +} + diff --git a/src/server/master/web_ui/application/web_ui/static/assets/js/form.validation.js b/src/server/master/web_ui/application/web_ui/static/assets/js/form.validation.js new file mode 100644 index 0000000..7bdeb0a --- /dev/null +++ b/src/server/master/web_ui/application/web_ui/static/assets/js/form.validation.js @@ -0,0 +1,64 @@ +$(document).ready(function(){ +/* + * Contact Form Validation + */ + + $('#contactform').submit(function() { + + // Disable the submit button + $('#contactform input[type=submit]') + .attr('value', 'Sending message') + .attr('disabled', 'disabled'); + + // AJAX POST request + $.post( + $(this).attr('action'), + { + name:$('#name').val(), + email:$('#email').val(), + message:$('#message').val() + }, + function(errors) { + // No errors + if (errors == null) { + $('#contactform') + .hide() + .html('

Thank you

Your message has been sent.

') + .show(); + } + + // Errors + else { + // Re-enable the submit button + $('#contactform input[type=submit]') + .removeAttr('disabled') + .attr('value', 'Send your Question'); + + // Technical server problem, the email could not be sent + if (errors.server != null) { + alert(errors.server); + return false; + } + + // Empty the errorbox and reset the error alerts + $('#contactform .errorbox').html('
    ').show(); + $('#contactform li').removeClass('alert'); + + // Loop over the errors, mark the corresponding input fields, + // and add the error messages to the errorbox. + for (field in errors) { + if (errors[field] != null) { + $('#' + field).parent('li').addClass('alert'); + $('#contactform .errorbox ul').append('
  • ' + errors[field] + '
  • '); + } + } + } + }, + 'json' + ); + + // Prevent non-AJAX form submission + return false; + }); + +}); \ No newline at end of file diff --git a/src/server/master/web_ui/application/web_ui/static/assets/js/jquery-1.2.3.min.js b/src/server/master/web_ui/application/web_ui/static/assets/js/jquery-1.2.3.min.js new file mode 100644 index 0000000..610bbcb --- /dev/null +++ b/src/server/master/web_ui/application/web_ui/static/assets/js/jquery-1.2.3.min.js @@ -0,0 +1,32 @@ +/* + * jQuery 1.2.3 - New Wave Javascript + * + * Copyright (c) 2008 John Resig (jquery.com) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * $Date: 2008/05/23 $ + * $Rev: 4663 $ + */ +(function(){if(window.jQuery)var _jQuery=window.jQuery;var jQuery=window.jQuery=function(selector,context){return new jQuery.prototype.init(selector,context);};if(window.$)var _$=window.$;window.$=jQuery;var quickExpr=/^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/;var isSimple=/^.[^:#\[\.]*$/;jQuery.fn=jQuery.prototype={init:function(selector,context){selector=selector||document;if(selector.nodeType){this[0]=selector;this.length=1;return this;}else if(typeof selector=="string"){var match=quickExpr.exec(selector);if(match&&(match[1]||!context)){if(match[1])selector=jQuery.clean([match[1]],context);else{var elem=document.getElementById(match[3]);if(elem)if(elem.id!=match[3])return jQuery().find(selector);else{this[0]=elem;this.length=1;return this;}else +selector=[];}}else +return new jQuery(context).find(selector);}else if(jQuery.isFunction(selector))return new jQuery(document)[jQuery.fn.ready?"ready":"load"](selector);return this.setArray(selector.constructor==Array&&selector||(selector.jquery||selector.length&&selector!=window&&!selector.nodeType&&selector[0]!=undefined&&selector[0].nodeType)&&jQuery.makeArray(selector)||[selector]);},jquery:"1.2.3",size:function(){return this.length;},length:0,get:function(num){return num==undefined?jQuery.makeArray(this):this[num];},pushStack:function(elems){var ret=jQuery(elems);ret.prevObject=this;return ret;},setArray:function(elems){this.length=0;Array.prototype.push.apply(this,elems);return this;},each:function(callback,args){return jQuery.each(this,callback,args);},index:function(elem){var ret=-1;this.each(function(i){if(this==elem)ret=i;});return ret;},attr:function(name,value,type){var options=name;if(name.constructor==String)if(value==undefined)return this.length&&jQuery[type||"attr"](this[0],name)||undefined;else{options={};options[name]=value;}return this.each(function(i){for(name in options)jQuery.attr(type?this.style:this,name,jQuery.prop(this,options[name],type,i,name));});},css:function(key,value){if((key=='width'||key=='height')&&parseFloat(value)<0)value=undefined;return this.attr(key,value,"curCSS");},text:function(text){if(typeof text!="object"&&text!=null)return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(text));var ret="";jQuery.each(text||this,function(){jQuery.each(this.childNodes,function(){if(this.nodeType!=8)ret+=this.nodeType!=1?this.nodeValue:jQuery.fn.text([this]);});});return ret;},wrapAll:function(html){if(this[0])jQuery(html,this[0].ownerDocument).clone().insertBefore(this[0]).map(function(){var elem=this;while(elem.firstChild)elem=elem.firstChild;return elem;}).append(this);return this;},wrapInner:function(html){return this.each(function(){jQuery(this).contents().wrapAll(html);});},wrap:function(html){return this.each(function(){jQuery(this).wrapAll(html);});},append:function(){return this.domManip(arguments,true,false,function(elem){if(this.nodeType==1)this.appendChild(elem);});},prepend:function(){return this.domManip(arguments,true,true,function(elem){if(this.nodeType==1)this.insertBefore(elem,this.firstChild);});},before:function(){return this.domManip(arguments,false,false,function(elem){this.parentNode.insertBefore(elem,this);});},after:function(){return this.domManip(arguments,false,true,function(elem){this.parentNode.insertBefore(elem,this.nextSibling);});},end:function(){return this.prevObject||jQuery([]);},find:function(selector){var elems=jQuery.map(this,function(elem){return jQuery.find(selector,elem);});return this.pushStack(/[^+>] [^+>]/.test(selector)||selector.indexOf("..")>-1?jQuery.unique(elems):elems);},clone:function(events){var ret=this.map(function(){if(jQuery.browser.msie&&!jQuery.isXMLDoc(this)){var clone=this.cloneNode(true),container=document.createElement("div");container.appendChild(clone);return jQuery.clean([container.innerHTML])[0];}else +return this.cloneNode(true);});var clone=ret.find("*").andSelf().each(function(){if(this[expando]!=undefined)this[expando]=null;});if(events===true)this.find("*").andSelf().each(function(i){if(this.nodeType==3)return;var events=jQuery.data(this,"events");for(var type in events)for(var handler in events[type])jQuery.event.add(clone[i],type,events[type][handler],events[type][handler].data);});return ret;},filter:function(selector){return this.pushStack(jQuery.isFunction(selector)&&jQuery.grep(this,function(elem,i){return selector.call(elem,i);})||jQuery.multiFilter(selector,this));},not:function(selector){if(selector.constructor==String)if(isSimple.test(selector))return this.pushStack(jQuery.multiFilter(selector,this,true));else +selector=jQuery.multiFilter(selector,this);var isArrayLike=selector.length&&selector[selector.length-1]!==undefined&&!selector.nodeType;return this.filter(function(){return isArrayLike?jQuery.inArray(this,selector)<0:this!=selector;});},add:function(selector){return!selector?this:this.pushStack(jQuery.merge(this.get(),selector.constructor==String?jQuery(selector).get():selector.length!=undefined&&(!selector.nodeName||jQuery.nodeName(selector,"form"))?selector:[selector]));},is:function(selector){return selector?jQuery.multiFilter(selector,this).length>0:false;},hasClass:function(selector){return this.is("."+selector);},val:function(value){if(value==undefined){if(this.length){var elem=this[0];if(jQuery.nodeName(elem,"select")){var index=elem.selectedIndex,values=[],options=elem.options,one=elem.type=="select-one";if(index<0)return null;for(var i=one?index:0,max=one?index+1:options.length;i=0||jQuery.inArray(this.name,value)>=0);else if(jQuery.nodeName(this,"select")){var values=value.constructor==Array?value:[value];jQuery("option",this).each(function(){this.selected=(jQuery.inArray(this.value,values)>=0||jQuery.inArray(this.text,values)>=0);});if(!values.length)this.selectedIndex=-1;}else +this.value=value;});},html:function(value){return value==undefined?(this.length?this[0].innerHTML:null):this.empty().append(value);},replaceWith:function(value){return this.after(value).remove();},eq:function(i){return this.slice(i,i+1);},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments));},map:function(callback){return this.pushStack(jQuery.map(this,function(elem,i){return callback.call(elem,i,elem);}));},andSelf:function(){return this.add(this.prevObject);},data:function(key,value){var parts=key.split(".");parts[1]=parts[1]?"."+parts[1]:"";if(value==null){var data=this.triggerHandler("getData"+parts[1]+"!",[parts[0]]);if(data==undefined&&this.length)data=jQuery.data(this[0],key);return data==null&&parts[1]?this.data(parts[0]):data;}else +return this.trigger("setData"+parts[1]+"!",[parts[0],value]).each(function(){jQuery.data(this,key,value);});},removeData:function(key){return this.each(function(){jQuery.removeData(this,key);});},domManip:function(args,table,reverse,callback){var clone=this.length>1,elems;return this.each(function(){if(!elems){elems=jQuery.clean(args,this.ownerDocument);if(reverse)elems.reverse();}var obj=this;if(table&&jQuery.nodeName(this,"table")&&jQuery.nodeName(elems[0],"tr"))obj=this.getElementsByTagName("tbody")[0]||this.appendChild(this.ownerDocument.createElement("tbody"));var scripts=jQuery([]);jQuery.each(elems,function(){var elem=clone?jQuery(this).clone(true)[0]:this;if(jQuery.nodeName(elem,"script")){scripts=scripts.add(elem);}else{if(elem.nodeType==1)scripts=scripts.add(jQuery("script",elem).remove());callback.call(obj,elem);}});scripts.each(evalScript);});}};jQuery.prototype.init.prototype=jQuery.prototype;function evalScript(i,elem){if(elem.src)jQuery.ajax({url:elem.src,async:false,dataType:"script"});else +jQuery.globalEval(elem.text||elem.textContent||elem.innerHTML||"");if(elem.parentNode)elem.parentNode.removeChild(elem);}jQuery.extend=jQuery.fn.extend=function(){var target=arguments[0]||{},i=1,length=arguments.length,deep=false,options;if(target.constructor==Boolean){deep=target;target=arguments[1]||{};i=2;}if(typeof target!="object"&&typeof target!="function")target={};if(length==1){target=this;i=0;}for(;i-1;}},swap:function(elem,options,callback){var old={};for(var name in options){old[name]=elem.style[name];elem.style[name]=options[name];}callback.call(elem);for(var name in options)elem.style[name]=old[name];},css:function(elem,name,force){if(name=="width"||name=="height"){var val,props={position:"absolute",visibility:"hidden",display:"block"},which=name=="width"?["Left","Right"]:["Top","Bottom"];function getWH(){val=name=="width"?elem.offsetWidth:elem.offsetHeight;var padding=0,border=0;jQuery.each(which,function(){padding+=parseFloat(jQuery.curCSS(elem,"padding"+this,true))||0;border+=parseFloat(jQuery.curCSS(elem,"border"+this+"Width",true))||0;});val-=Math.round(padding+border);}if(jQuery(elem).is(":visible"))getWH();else +jQuery.swap(elem,props,getWH);return Math.max(0,val);}return jQuery.curCSS(elem,name,force);},curCSS:function(elem,name,force){var ret;function color(elem){if(!jQuery.browser.safari)return false;var ret=document.defaultView.getComputedStyle(elem,null);return!ret||ret.getPropertyValue("color")=="";}if(name=="opacity"&&jQuery.browser.msie){ret=jQuery.attr(elem.style,"opacity");return ret==""?"1":ret;}if(jQuery.browser.opera&&name=="display"){var save=elem.style.outline;elem.style.outline="0 solid black";elem.style.outline=save;}if(name.match(/float/i))name=styleFloat;if(!force&&elem.style&&elem.style[name])ret=elem.style[name];else if(document.defaultView&&document.defaultView.getComputedStyle){if(name.match(/float/i))name="float";name=name.replace(/([A-Z])/g,"-$1").toLowerCase();var getComputedStyle=document.defaultView.getComputedStyle(elem,null);if(getComputedStyle&&!color(elem))ret=getComputedStyle.getPropertyValue(name);else{var swap=[],stack=[];for(var a=elem;a&&color(a);a=a.parentNode)stack.unshift(a);for(var i=0;i]*?)\/>/g,function(all,front,tag){return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?all:front+">";});var tags=jQuery.trim(elem).toLowerCase(),div=context.createElement("div");var wrap=!tags.indexOf("",""]||!tags.indexOf("",""]||tags.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"","
    "]||!tags.indexOf("",""]||(!tags.indexOf("",""]||!tags.indexOf("",""]||jQuery.browser.msie&&[1,"div
    ","
    "]||[0,"",""];div.innerHTML=wrap[1]+elem+wrap[2];while(wrap[0]--)div=div.lastChild;if(jQuery.browser.msie){var tbody=!tags.indexOf(""&&tags.indexOf("=0;--j)if(jQuery.nodeName(tbody[j],"tbody")&&!tbody[j].childNodes.length)tbody[j].parentNode.removeChild(tbody[j]);if(/^\s/.test(elem))div.insertBefore(context.createTextNode(elem.match(/^\s*/)[0]),div.firstChild);}elem=jQuery.makeArray(div.childNodes);}if(elem.length===0&&(!jQuery.nodeName(elem,"form")&&!jQuery.nodeName(elem,"select")))return;if(elem[0]==undefined||jQuery.nodeName(elem,"form")||elem.options)ret.push(elem);else +ret=jQuery.merge(ret,elem);});return ret;},attr:function(elem,name,value){if(!elem||elem.nodeType==3||elem.nodeType==8)return undefined;var fix=jQuery.isXMLDoc(elem)?{}:jQuery.props;if(name=="selected"&&jQuery.browser.safari)elem.parentNode.selectedIndex;if(fix[name]){if(value!=undefined)elem[fix[name]]=value;return elem[fix[name]];}else if(jQuery.browser.msie&&name=="style")return jQuery.attr(elem.style,"cssText",value);else if(value==undefined&&jQuery.browser.msie&&jQuery.nodeName(elem,"form")&&(name=="action"||name=="method"))return elem.getAttributeNode(name).nodeValue;else if(elem.tagName){if(value!=undefined){if(name=="type"&&jQuery.nodeName(elem,"input")&&elem.parentNode)throw"type property can't be changed";elem.setAttribute(name,""+value);}if(jQuery.browser.msie&&/href|src/.test(name)&&!jQuery.isXMLDoc(elem))return elem.getAttribute(name,2);return elem.getAttribute(name);}else{if(name=="opacity"&&jQuery.browser.msie){if(value!=undefined){elem.zoom=1;elem.filter=(elem.filter||"").replace(/alpha\([^)]*\)/,"")+(parseFloat(value).toString()=="NaN"?"":"alpha(opacity="+value*100+")");}return elem.filter&&elem.filter.indexOf("opacity=")>=0?(parseFloat(elem.filter.match(/opacity=([^)]*)/)[1])/100).toString():"";}name=name.replace(/-([a-z])/ig,function(all,letter){return letter.toUpperCase();});if(value!=undefined)elem[name]=value;return elem[name];}},trim:function(text){return(text||"").replace(/^\s+|\s+$/g,"");},makeArray:function(array){var ret=[];if(typeof array!="array")for(var i=0,length=array.length;i*",this).remove();while(this.firstChild)this.removeChild(this.firstChild);}},function(name,fn){jQuery.fn[name]=function(){return this.each(fn,arguments);};});jQuery.each(["Height","Width"],function(i,name){var type=name.toLowerCase();jQuery.fn[type]=function(size){return this[0]==window?jQuery.browser.opera&&document.body["client"+name]||jQuery.browser.safari&&window["inner"+name]||document.compatMode=="CSS1Compat"&&document.documentElement["client"+name]||document.body["client"+name]:this[0]==document?Math.max(Math.max(document.body["scroll"+name],document.documentElement["scroll"+name]),Math.max(document.body["offset"+name],document.documentElement["offset"+name])):size==undefined?(this.length?jQuery.css(this[0],type):null):this.css(type,size.constructor==String?size:size+"px");};});var chars=jQuery.browser.safari&&parseInt(jQuery.browser.version)<417?"(?:[\\w*_-]|\\\\.)":"(?:[\\w\u0128-\uFFFF*_-]|\\\\.)",quickChild=new RegExp("^>\\s*("+chars+"+)"),quickID=new RegExp("^("+chars+"+)(#)("+chars+"+)"),quickClass=new RegExp("^([#.]?)("+chars+"*)");jQuery.extend({expr:{"":function(a,i,m){return m[2]=="*"||jQuery.nodeName(a,m[2]);},"#":function(a,i,m){return a.getAttribute("id")==m[2];},":":{lt:function(a,i,m){return im[3]-0;},nth:function(a,i,m){return m[3]-0==i;},eq:function(a,i,m){return m[3]-0==i;},first:function(a,i){return i==0;},last:function(a,i,m,r){return i==r.length-1;},even:function(a,i){return i%2==0;},odd:function(a,i){return i%2;},"first-child":function(a){return a.parentNode.getElementsByTagName("*")[0]==a;},"last-child":function(a){return jQuery.nth(a.parentNode.lastChild,1,"previousSibling")==a;},"only-child":function(a){return!jQuery.nth(a.parentNode.lastChild,2,"previousSibling");},parent:function(a){return a.firstChild;},empty:function(a){return!a.firstChild;},contains:function(a,i,m){return(a.textContent||a.innerText||jQuery(a).text()||"").indexOf(m[3])>=0;},visible:function(a){return"hidden"!=a.type&&jQuery.css(a,"display")!="none"&&jQuery.css(a,"visibility")!="hidden";},hidden:function(a){return"hidden"==a.type||jQuery.css(a,"display")=="none"||jQuery.css(a,"visibility")=="hidden";},enabled:function(a){return!a.disabled;},disabled:function(a){return a.disabled;},checked:function(a){return a.checked;},selected:function(a){return a.selected||jQuery.attr(a,"selected");},text:function(a){return"text"==a.type;},radio:function(a){return"radio"==a.type;},checkbox:function(a){return"checkbox"==a.type;},file:function(a){return"file"==a.type;},password:function(a){return"password"==a.type;},submit:function(a){return"submit"==a.type;},image:function(a){return"image"==a.type;},reset:function(a){return"reset"==a.type;},button:function(a){return"button"==a.type||jQuery.nodeName(a,"button");},input:function(a){return/input|select|textarea|button/i.test(a.nodeName);},has:function(a,i,m){return jQuery.find(m[3],a).length;},header:function(a){return/h\d/i.test(a.nodeName);},animated:function(a){return jQuery.grep(jQuery.timers,function(fn){return a==fn.elem;}).length;}}},parse:[/^(\[) *@?([\w-]+) *([!*$^~=]*) *('?"?)(.*?)\4 *\]/,/^(:)([\w-]+)\("?'?(.*?(\(.*?\))?[^(]*?)"?'?\)/,new RegExp("^([:.#]*)("+chars+"+)")],multiFilter:function(expr,elems,not){var old,cur=[];while(expr&&expr!=old){old=expr;var f=jQuery.filter(expr,elems,not);expr=f.t.replace(/^\s*,\s*/,"");cur=not?elems=f.r:jQuery.merge(cur,f.r);}return cur;},find:function(t,context){if(typeof t!="string")return[t];if(context&&context.nodeType!=1&&context.nodeType!=9)return[];context=context||document;var ret=[context],done=[],last,nodeName;while(t&&last!=t){var r=[];last=t;t=jQuery.trim(t);var foundToken=false;var re=quickChild;var m=re.exec(t);if(m){nodeName=m[1].toUpperCase();for(var i=0;ret[i];i++)for(var c=ret[i].firstChild;c;c=c.nextSibling)if(c.nodeType==1&&(nodeName=="*"||c.nodeName.toUpperCase()==nodeName))r.push(c);ret=r;t=t.replace(re,"");if(t.indexOf(" ")==0)continue;foundToken=true;}else{re=/^([>+~])\s*(\w*)/i;if((m=re.exec(t))!=null){r=[];var merge={};nodeName=m[2].toUpperCase();m=m[1];for(var j=0,rl=ret.length;j=0;if(!not&&pass||not&&!pass)tmp.push(r[i]);}return tmp;},filter:function(t,r,not){var last;while(t&&t!=last){last=t;var p=jQuery.parse,m;for(var i=0;p[i];i++){m=p[i].exec(t);if(m){t=t.substring(m[0].length);m[2]=m[2].replace(/\\/g,"");break;}}if(!m)break;if(m[1]==":"&&m[2]=="not")r=isSimple.test(m[3])?jQuery.filter(m[3],r,true).r:jQuery(r).not(m[3]);else if(m[1]==".")r=jQuery.classFilter(r,m[2],not);else if(m[1]=="["){var tmp=[],type=m[3];for(var i=0,rl=r.length;i=0)^not)tmp.push(a);}r=tmp;}else if(m[1]==":"&&m[2]=="nth-child"){var merge={},tmp=[],test=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(m[3]=="even"&&"2n"||m[3]=="odd"&&"2n+1"||!/\D/.test(m[3])&&"0n+"+m[3]||m[3]),first=(test[1]+(test[2]||1))-0,last=test[3]-0;for(var i=0,rl=r.length;i=0)add=true;if(add^not)tmp.push(node);}r=tmp;}else{var fn=jQuery.expr[m[1]];if(typeof fn=="object")fn=fn[m[2]];if(typeof fn=="string")fn=eval("false||function(a,i){return "+fn+";}");r=jQuery.grep(r,function(elem,i){return fn(elem,i,m,r);},not);}}return{r:r,t:t};},dir:function(elem,dir){var matched=[];var cur=elem[dir];while(cur&&cur!=document){if(cur.nodeType==1)matched.push(cur);cur=cur[dir];}return matched;},nth:function(cur,result,dir,elem){result=result||1;var num=0;for(;cur;cur=cur[dir])if(cur.nodeType==1&&++num==result)break;return cur;},sibling:function(n,elem){var r=[];for(;n;n=n.nextSibling){if(n.nodeType==1&&(!elem||n!=elem))r.push(n);}return r;}});jQuery.event={add:function(elem,types,handler,data){if(elem.nodeType==3||elem.nodeType==8)return;if(jQuery.browser.msie&&elem.setInterval!=undefined)elem=window;if(!handler.guid)handler.guid=this.guid++;if(data!=undefined){var fn=handler;handler=function(){return fn.apply(this,arguments);};handler.data=data;handler.guid=fn.guid;}var events=jQuery.data(elem,"events")||jQuery.data(elem,"events",{}),handle=jQuery.data(elem,"handle")||jQuery.data(elem,"handle",function(){var val;if(typeof jQuery=="undefined"||jQuery.event.triggered)return val;val=jQuery.event.handle.apply(arguments.callee.elem,arguments);return val;});handle.elem=elem;jQuery.each(types.split(/\s+/),function(index,type){var parts=type.split(".");type=parts[0];handler.type=parts[1];var handlers=events[type];if(!handlers){handlers=events[type]={};if(!jQuery.event.special[type]||jQuery.event.special[type].setup.call(elem)===false){if(elem.addEventListener)elem.addEventListener(type,handle,false);else if(elem.attachEvent)elem.attachEvent("on"+type,handle);}}handlers[handler.guid]=handler;jQuery.event.global[type]=true;});elem=null;},guid:1,global:{},remove:function(elem,types,handler){if(elem.nodeType==3||elem.nodeType==8)return;var events=jQuery.data(elem,"events"),ret,index;if(events){if(types==undefined||(typeof types=="string"&&types.charAt(0)=="."))for(var type in events)this.remove(elem,type+(types||""));else{if(types.type){handler=types.handler;types=types.type;}jQuery.each(types.split(/\s+/),function(index,type){var parts=type.split(".");type=parts[0];if(events[type]){if(handler)delete events[type][handler.guid];else +for(handler in events[type])if(!parts[1]||events[type][handler].type==parts[1])delete events[type][handler];for(ret in events[type])break;if(!ret){if(!jQuery.event.special[type]||jQuery.event.special[type].teardown.call(elem)===false){if(elem.removeEventListener)elem.removeEventListener(type,jQuery.data(elem,"handle"),false);else if(elem.detachEvent)elem.detachEvent("on"+type,jQuery.data(elem,"handle"));}ret=null;delete events[type];}}});}for(ret in events)break;if(!ret){var handle=jQuery.data(elem,"handle");if(handle)handle.elem=null;jQuery.removeData(elem,"events");jQuery.removeData(elem,"handle");}}},trigger:function(type,data,elem,donative,extra){data=jQuery.makeArray(data||[]);if(type.indexOf("!")>=0){type=type.slice(0,-1);var exclusive=true;}if(!elem){if(this.global[type])jQuery("*").add([window,document]).trigger(type,data);}else{if(elem.nodeType==3||elem.nodeType==8)return undefined;var val,ret,fn=jQuery.isFunction(elem[type]||null),event=!data[0]||!data[0].preventDefault;if(event)data.unshift(this.fix({type:type,target:elem}));data[0].type=type;if(exclusive)data[0].exclusive=true;if(jQuery.isFunction(jQuery.data(elem,"handle")))val=jQuery.data(elem,"handle").apply(elem,data);if(!fn&&elem["on"+type]&&elem["on"+type].apply(elem,data)===false)val=false;if(event)data.shift();if(extra&&jQuery.isFunction(extra)){ret=extra.apply(elem,val==null?data:data.concat(val));if(ret!==undefined)val=ret;}if(fn&&donative!==false&&val!==false&&!(jQuery.nodeName(elem,'a')&&type=="click")){this.triggered=true;try{elem[type]();}catch(e){}}this.triggered=false;}return val;},handle:function(event){var val;event=jQuery.event.fix(event||window.event||{});var parts=event.type.split(".");event.type=parts[0];var handlers=jQuery.data(this,"events")&&jQuery.data(this,"events")[event.type],args=Array.prototype.slice.call(arguments,1);args.unshift(event);for(var j in handlers){var handler=handlers[j];args[0].handler=handler;args[0].data=handler.data;if(!parts[1]&&!event.exclusive||handler.type==parts[1]){var ret=handler.apply(this,args);if(val!==false)val=ret;if(ret===false){event.preventDefault();event.stopPropagation();}}}if(jQuery.browser.msie)event.target=event.preventDefault=event.stopPropagation=event.handler=event.data=null;return val;},fix:function(event){var originalEvent=event;event=jQuery.extend({},originalEvent);event.preventDefault=function(){if(originalEvent.preventDefault)originalEvent.preventDefault();originalEvent.returnValue=false;};event.stopPropagation=function(){if(originalEvent.stopPropagation)originalEvent.stopPropagation();originalEvent.cancelBubble=true;};if(!event.target)event.target=event.srcElement||document;if(event.target.nodeType==3)event.target=originalEvent.target.parentNode;if(!event.relatedTarget&&event.fromElement)event.relatedTarget=event.fromElement==event.target?event.toElement:event.fromElement;if(event.pageX==null&&event.clientX!=null){var doc=document.documentElement,body=document.body;event.pageX=event.clientX+(doc&&doc.scrollLeft||body&&body.scrollLeft||0)-(doc.clientLeft||0);event.pageY=event.clientY+(doc&&doc.scrollTop||body&&body.scrollTop||0)-(doc.clientTop||0);}if(!event.which&&((event.charCode||event.charCode===0)?event.charCode:event.keyCode))event.which=event.charCode||event.keyCode;if(!event.metaKey&&event.ctrlKey)event.metaKey=event.ctrlKey;if(!event.which&&event.button)event.which=(event.button&1?1:(event.button&2?3:(event.button&4?2:0)));return event;},special:{ready:{setup:function(){bindReady();return;},teardown:function(){return;}},mouseenter:{setup:function(){if(jQuery.browser.msie)return false;jQuery(this).bind("mouseover",jQuery.event.special.mouseenter.handler);return true;},teardown:function(){if(jQuery.browser.msie)return false;jQuery(this).unbind("mouseover",jQuery.event.special.mouseenter.handler);return true;},handler:function(event){if(withinElement(event,this))return true;arguments[0].type="mouseenter";return jQuery.event.handle.apply(this,arguments);}},mouseleave:{setup:function(){if(jQuery.browser.msie)return false;jQuery(this).bind("mouseout",jQuery.event.special.mouseleave.handler);return true;},teardown:function(){if(jQuery.browser.msie)return false;jQuery(this).unbind("mouseout",jQuery.event.special.mouseleave.handler);return true;},handler:function(event){if(withinElement(event,this))return true;arguments[0].type="mouseleave";return jQuery.event.handle.apply(this,arguments);}}}};jQuery.fn.extend({bind:function(type,data,fn){return type=="unload"?this.one(type,data,fn):this.each(function(){jQuery.event.add(this,type,fn||data,fn&&data);});},one:function(type,data,fn){return this.each(function(){jQuery.event.add(this,type,function(event){jQuery(this).unbind(event);return(fn||data).apply(this,arguments);},fn&&data);});},unbind:function(type,fn){return this.each(function(){jQuery.event.remove(this,type,fn);});},trigger:function(type,data,fn){return this.each(function(){jQuery.event.trigger(type,data,this,true,fn);});},triggerHandler:function(type,data,fn){if(this[0])return jQuery.event.trigger(type,data,this[0],false,fn);return undefined;},toggle:function(){var args=arguments;return this.click(function(event){this.lastToggle=0==this.lastToggle?1:0;event.preventDefault();return args[this.lastToggle].apply(this,arguments)||false;});},hover:function(fnOver,fnOut){return this.bind('mouseenter',fnOver).bind('mouseleave',fnOut);},ready:function(fn){bindReady();if(jQuery.isReady)fn.call(document,jQuery);else +jQuery.readyList.push(function(){return fn.call(this,jQuery);});return this;}});jQuery.extend({isReady:false,readyList:[],ready:function(){if(!jQuery.isReady){jQuery.isReady=true;if(jQuery.readyList){jQuery.each(jQuery.readyList,function(){this.apply(document);});jQuery.readyList=null;}jQuery(document).triggerHandler("ready");}}});var readyBound=false;function bindReady(){if(readyBound)return;readyBound=true;if(document.addEventListener&&!jQuery.browser.opera)document.addEventListener("DOMContentLoaded",jQuery.ready,false);if(jQuery.browser.msie&&window==top)(function(){if(jQuery.isReady)return;try{document.documentElement.doScroll("left");}catch(error){setTimeout(arguments.callee,0);return;}jQuery.ready();})();if(jQuery.browser.opera)document.addEventListener("DOMContentLoaded",function(){if(jQuery.isReady)return;for(var i=0;i=0){var selector=url.slice(off,url.length);url=url.slice(0,off);}callback=callback||function(){};var type="GET";if(params)if(jQuery.isFunction(params)){callback=params;params=null;}else{params=jQuery.param(params);type="POST";}var self=this;jQuery.ajax({url:url,type:type,dataType:"html",data:params,complete:function(res,status){if(status=="success"||status=="notmodified")self.html(selector?jQuery("
    ');var arrPageSizes=___getPageSize();$('#jquery-overlay').css({backgroundColor:settings.overlayBgColor,opacity:settings.overlayOpacity,width:arrPageSizes[0],height:arrPageSizes[1]}).fadeIn();var arrPageScroll=___getPageScroll();$('#jquery-lightbox').css({top:arrPageScroll[1]+(arrPageSizes[3]/10),left:arrPageScroll[0]}).show();$('#jquery-overlay,#jquery-lightbox').click(function(){_finish();});$('#lightbox-loading-link,#lightbox-secNav-btnClose').click(function(){_finish();return false;});$(window).resize(function(){var arrPageSizes=___getPageSize();$('#jquery-overlay').css({width:arrPageSizes[0],height:arrPageSizes[1]});var arrPageScroll=___getPageScroll();$('#jquery-lightbox').css({top:arrPageScroll[1]+(arrPageSizes[3]/10),left:arrPageScroll[0]});});} +function _set_image_to_view(){$('#lightbox-loading').show();if(settings.fixedNavigation){$('#lightbox-image,#lightbox-container-image-data-box,#lightbox-image-details-currentNumber').hide();}else{$('#lightbox-image,#lightbox-nav,#lightbox-nav-btnPrev,#lightbox-nav-btnNext,#lightbox-container-image-data-box,#lightbox-image-details-currentNumber').hide();} +var objImagePreloader=new Image();objImagePreloader.onload=function(){$('#lightbox-image').attr('src',settings.imageArray[settings.activeImage][0]);_resize_container_image_box(objImagePreloader.width,objImagePreloader.height);objImagePreloader.onload=function(){};};objImagePreloader.src=settings.imageArray[settings.activeImage][0];};function _resize_container_image_box(intImageWidth,intImageHeight){var intCurrentWidth=$('#lightbox-container-image-box').width();var intCurrentHeight=$('#lightbox-container-image-box').height();var intWidth=(intImageWidth+(settings.containerBorderSize*2));var intHeight=(intImageHeight+(settings.containerBorderSize*2));var intDiffW=intCurrentWidth-intWidth;var intDiffH=intCurrentHeight-intHeight;$('#lightbox-container-image-box').animate({width:intWidth,height:intHeight},settings.containerResizeSpeed,function(){_show_image();});if((intDiffW==0)&&(intDiffH==0)){if($.browser.msie){___pause(250);}else{___pause(100);}} +$('#lightbox-container-image-data-box').css({width:intImageWidth});$('#lightbox-nav-btnPrev,#lightbox-nav-btnNext').css({height:intImageHeight+(settings.containerBorderSize*2)});};function _show_image(){$('#lightbox-loading').hide();$('#lightbox-image').fadeIn(function(){_show_image_data();_set_navigation();});_preload_neighbor_images();};function _show_image_data(){$('#lightbox-container-image-data-box').slideDown('fast');$('#lightbox-image-details-caption').hide();if(settings.imageArray[settings.activeImage][1]){$('#lightbox-image-details-caption').html(settings.imageArray[settings.activeImage][1]).show();} +if(settings.imageArray.length>1){$('#lightbox-image-details-currentNumber').html(settings.txtImage+' '+(settings.activeImage+1)+' '+settings.txtOf+' '+settings.imageArray.length).show();}} +function _set_navigation(){$('#lightbox-nav').show();$('#lightbox-nav-btnPrev,#lightbox-nav-btnNext').css({'background':'transparent url('+settings.imageBlank+') no-repeat'});if(settings.activeImage!=0){if(settings.fixedNavigation){$('#lightbox-nav-btnPrev').css({'background':'url('+settings.imageBtnPrev+') left 15% no-repeat'}).unbind().bind('click',function(){settings.activeImage=settings.activeImage-1;_set_image_to_view();return false;});}else{$('#lightbox-nav-btnPrev').unbind().hover(function(){$(this).css({'background':'url('+settings.imageBtnPrev+') left 15% no-repeat'});},function(){$(this).css({'background':'transparent url('+settings.imageBlank+') no-repeat'});}).show().bind('click',function(){settings.activeImage=settings.activeImage-1;_set_image_to_view();return false;});}} +if(settings.activeImage!=(settings.imageArray.length-1)){if(settings.fixedNavigation){$('#lightbox-nav-btnNext').css({'background':'url('+settings.imageBtnNext+') right 15% no-repeat'}).unbind().bind('click',function(){settings.activeImage=settings.activeImage+1;_set_image_to_view();return false;});}else{$('#lightbox-nav-btnNext').unbind().hover(function(){$(this).css({'background':'url('+settings.imageBtnNext+') right 15% no-repeat'});},function(){$(this).css({'background':'transparent url('+settings.imageBlank+') no-repeat'});}).show().bind('click',function(){settings.activeImage=settings.activeImage+1;_set_image_to_view();return false;});}} +_enable_keyboard_navigation();} +function _enable_keyboard_navigation(){$(document).keydown(function(objEvent){_keyboard_action(objEvent);});} +function _disable_keyboard_navigation(){$(document).unbind();} +function _keyboard_action(objEvent){if(objEvent==null){keycode=event.keyCode;escapeKey=27;}else{keycode=objEvent.keyCode;escapeKey=objEvent.DOM_VK_ESCAPE;} +key=String.fromCharCode(keycode).toLowerCase();if((key==settings.keyToClose)||(key=='x')||(keycode==escapeKey)){_finish();} +if((key==settings.keyToPrev)||(keycode==37)){if(settings.activeImage!=0){settings.activeImage=settings.activeImage-1;_set_image_to_view();_disable_keyboard_navigation();}} +if((key==settings.keyToNext)||(keycode==39)){if(settings.activeImage!=(settings.imageArray.length-1)){settings.activeImage=settings.activeImage+1;_set_image_to_view();_disable_keyboard_navigation();}}} +function _preload_neighbor_images(){if((settings.imageArray.length-1)>settings.activeImage){objNext=new Image();objNext.src=settings.imageArray[settings.activeImage+1][0];} +if(settings.activeImage>0){objPrev=new Image();objPrev.src=settings.imageArray[settings.activeImage-1][0];}} +function _finish(){$('#jquery-lightbox').remove();$('#jquery-overlay').fadeOut(function(){$('#jquery-overlay').remove();});$('embed, object, select').css({'visibility':'visible'});} +function ___getPageSize(){var xScroll,yScroll;if(window.innerHeight&&window.scrollMaxY){xScroll=window.innerWidth+window.scrollMaxX;yScroll=window.innerHeight+window.scrollMaxY;}else if(document.body.scrollHeight>document.body.offsetHeight){xScroll=document.body.scrollWidth;yScroll=document.body.scrollHeight;}else{xScroll=document.body.offsetWidth;yScroll=document.body.offsetHeight;} +var windowWidth,windowHeight;if(self.innerHeight){if(document.documentElement.clientWidth){windowWidth=document.documentElement.clientWidth;}else{windowWidth=self.innerWidth;} +windowHeight=self.innerHeight;}else if(document.documentElement&&document.documentElement.clientHeight){windowWidth=document.documentElement.clientWidth;windowHeight=document.documentElement.clientHeight;}else if(document.body){windowWidth=document.body.clientWidth;windowHeight=document.body.clientHeight;} +if(yScroll35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('(6($){$.2N.3g=6(4){4=23.2H({2B:\'#34\',2g:0.8,1d:F,1M:\'18/5-33-Y.16\',1v:\'18/5-1u-2Q.16\',1E:\'18/5-1u-2L.16\',1W:\'18/5-1u-2I.16\',19:\'18/5-2F.16\',1f:10,2A:3d,2s:\'1j\',2o:\'32\',2j:\'c\',2f:\'p\',2d:\'n\',h:[],9:0},4);f I=N;6 20(){1X(N,I);u F}6 1X(1e,I){$(\'1U, 1S, 1R\').l({\'1Q\':\'2E\'});1O();4.h.B=0;4.9=0;7(I.B==1){4.h.1J(v 1m(1e.17(\'J\'),1e.17(\'2v\')))}j{36(f i=0;i<1w g="5-b"><1w W="\'+4.1M+\'"><1i g="5-b-A-1t"><1i g="5-b-A-1g"><1w W="\'+4.1W+\'">\');f z=1D();$(\'#q-13\').l({2K:4.2B,2J:4.2g,S:z[0],P:z[1]}).1V();f R=1p();$(\'#q-5\').l({1T:R[1]+(z[3]/10),1c:R[0]}).E();$(\'#q-13,#q-5\').C(6(){1a()});$(\'#5-Y-29,#5-1s-22\').C(6(){1a();u F});$(G).2G(6(){f z=1D();$(\'#q-13\').l({S:z[0],P:z[1]});f R=1p();$(\'#q-5\').l({1T:R[1]+(z[3]/10),1c:R[0]})})}6 D(){$(\'#5-Y\').E();7(4.1d){$(\'#5-b,#5-s-b-T-w,#5-b-A-1g\').1b()}j{$(\'#5-b,#5-k,#5-k-V,#5-k-X,#5-s-b-T-w,#5-b-A-1g\').1b()}f Q=v 1j();Q.1P=6(){$(\'#5-b\').2D(\'W\',4.h[4.9][0]);1N(Q.S,Q.P);Q.1P=6(){}};Q.W=4.h[4.9][0]};6 1N(1o,1r){f 1L=$(\'#5-s-b-w\').S();f 1K=$(\'#5-s-b-w\').P();f 1n=(1o+(4.1f*2));f 1y=(1r+(4.1f*2));f 1I=1L-1n;f 2z=1K-1y;$(\'#5-s-b-w\').3f({S:1n,P:1y},4.2A,6(){2y()});7((1I==0)&&(2z==0)){7($.3e.3c){1H(3b)}j{1H(3a)}}$(\'#5-s-b-T-w\').l({S:1o});$(\'#5-k-V,#5-k-X\').l({P:1r+(4.1f*2)})};6 2y(){$(\'#5-Y\').1b();$(\'#5-b\').1V(6(){2u();2t()});2r()};6 2u(){$(\'#5-s-b-T-w\').38(\'35\');$(\'#5-b-A-1t\').1b();7(4.h[4.9][1]){$(\'#5-b-A-1t\').2p(4.h[4.9][1]).E()}7(4.h.B>1){$(\'#5-b-A-1g\').2p(4.2s+\' \'+(4.9+1)+\' \'+4.2o+\' \'+4.h.B).E()}}6 2t(){$(\'#5-k\').E();$(\'#5-k-V,#5-k-X\').l({\'K\':\'1C M(\'+4.19+\') L-O\'});7(4.9!=0){7(4.1d){$(\'#5-k-V\').l({\'K\':\'M(\'+4.1v+\') 1c 15% L-O\'}).11().1k(\'C\',6(){4.9=4.9-1;D();u F})}j{$(\'#5-k-V\').11().2m(6(){$(N).l({\'K\':\'M(\'+4.1v+\') 1c 15% L-O\'})},6(){$(N).l({\'K\':\'1C M(\'+4.19+\') L-O\'})}).E().1k(\'C\',6(){4.9=4.9-1;D();u F})}}7(4.9!=(4.h.B-1)){7(4.1d){$(\'#5-k-X\').l({\'K\':\'M(\'+4.1E+\') 2l 15% L-O\'}).11().1k(\'C\',6(){4.9=4.9+1;D();u F})}j{$(\'#5-k-X\').11().2m(6(){$(N).l({\'K\':\'M(\'+4.1E+\') 2l 15% L-O\'})},6(){$(N).l({\'K\':\'1C M(\'+4.19+\') L-O\'})}).E().1k(\'C\',6(){4.9=4.9+1;D();u F})}}2k()}6 2k(){$(d).30(6(12){2i(12)})}6 1G(){$(d).11()}6 2i(12){7(12==2h){U=2Z.2e;1x=27}j{U=12.2e;1x=12.2Y}14=2X.2W(U).2U();7((14==4.2j)||(14==\'x\')||(U==1x)){1a()}7((14==4.2f)||(U==37)){7(4.9!=0){4.9=4.9-1;D();1G()}}7((14==4.2d)||(U==39)){7(4.9!=(4.h.B-1)){4.9=4.9+1;D();1G()}}}6 2r(){7((4.h.B-1)>4.9){2c=v 1j();2c.W=4.h[4.9+1][0]}7(4.9>0){2b=v 1j();2b.W=4.h[4.9-1][0]}}6 1a(){$(\'#q-5\').2a();$(\'#q-13\').2T(6(){$(\'#q-13\').2a()});$(\'1U, 1S, 1R\').l({\'1Q\':\'2S\'})}6 1D(){f o,r;7(G.1h&&G.28){o=G.26+G.2R;r=G.1h+G.28}j 7(d.m.25>d.m.24){o=d.m.2P;r=d.m.25}j{o=d.m.2O;r=d.m.24}f y,H;7(Z.1h){7(d.t.1l){y=d.t.1l}j{y=Z.26}H=Z.1h}j 7(d.t&&d.t.1A){y=d.t.1l;H=d.t.1A}j 7(d.m){y=d.m.1l;H=d.m.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 N002ovPDHLkV1gA;4_E*I literal 0 HcmV?d00001 diff --git a/src/server/master/web_ui/application/web_ui/static/images/djangopowered126x54.gif b/src/server/master/web_ui/application/web_ui/static/images/djangopowered126x54.gif new file mode 100644 index 0000000000000000000000000000000000000000..acd7b153ffff1848682c63d84c6e7f118e05e787 GIT binary patch literal 3312 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

    oO$cF;xF7ytbM{gA9G~4IQp5uk9u; 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 literal 0 HcmV?d00001 diff --git a/src/server/master/web_ui/application/web_ui/static/images/lightbox-blank.gif b/src/server/master/web_ui/application/web_ui/static/images/lightbox-blank.gif new file mode 100644 index 0000000000000000000000000000000000000000..1d11fa9ada9e93505b3d736acb204083f45d5fbf GIT binary patch literal 43 scmZ?wbhEHbWMp7uX!y@?;J^U}1_s5SEQ~;kK?g*DWEhy3To@Uw0n;G|I{*Lx literal 0 HcmV?d00001 diff --git a/src/server/master/web_ui/application/web_ui/static/images/lightbox-btn-close.gif b/src/server/master/web_ui/application/web_ui/static/images/lightbox-btn-close.gif new file mode 100644 index 0000000000000000000000000000000000000000..33bcf517a35b72135b6a5b97bac72425762b8343 GIT binary patch literal 700 zcmZ?wbhEHbbYc)=xXQrr|NsBLfB*ja_3OuvA78(I{qW(#>({SezI^%Y*|R54p4`8G z|IVE|w{G3Ke*OB@t5+{wx^(*V=@TbT?Ay0*`}XZywrtt7Y18`k>sPE;v3T*~dGqGY zo;`cov}qG3PVDdR@9F7jZ*OmEYHDa`sI9FnD=RB5E-oxA%*)Hm%*;$rPftlnNlHqJ zkB^Uyjg5+m3Jnbn4h{|o2=Mds^Y-@k^z?Ldb7LR`6o0ZXurvH;&;cm~`H6w;ze7WT zhmKVLi6uoRbFAiHSg}QNM$~#2#^;SaIV^5XhYKa-WLTaw7EPSeoxUZ&W*tAf%&o4( zmMs}FnZ=U&t@`p#;`)5z0}*FgxG7?Q+eP>#}Q0kG{s^ou20;n;V<17z*6xlW|Jw?=bOR#PEpA zg=KOFx5Rw`%jtY|_3ZS` zqUjnN13nacHVT_2ml!nlJA2OW%MmP8S=ga0=sH=)An?>j$~+u1KUln%@oJ3-U|ygRY{{;{vyg#@ zVe7)XGmJPB4>%ce-gMSF@xfy;(;UM9kqL%E%RL+UjM8k(gB06(gljgeIR7Z|k07_Y Yz?%8?E)08>7@u6ee}4b|UkVJ?0I^^iLI3~& literal 0 HcmV?d00001 diff --git a/src/server/master/web_ui/application/web_ui/static/images/lightbox-btn-next.gif b/src/server/master/web_ui/application/web_ui/static/images/lightbox-btn-next.gif new file mode 100644 index 0000000000000000000000000000000000000000..a0d4fcf84a784f2cf44c33084145dde5df294ccf GIT binary patch literal 812 zcmZ?wbhEHbv}aIYc*el+|NsBLfB*je{rl(7pWnZK|N8ao=g*%%e*F0U{rk6X-@bnR z`sK@)&!0bk_Uze{Cr=(ddi3DIgM0Vx-MMq;#*G`-u3fuy>C%M@7tWnKck0xs6DLj_ zJ9g~Qp+g4_9N4pG&#qm&wr$(CWy_Y$n>TOVxN-gZ^=sFzUA=ns%9SgZFJHcN>C(lE z7tftLclPYrGiT16IB{Zse}8XpZ+CZhdwY9pYio0Jb7Ny;LqkJdU0qdGRYgTbSy@?0 zNl8ILL0(>7PEJl%R#rwvMp{~0N=iynQc^-fLR?&2OiWBvR8&MnL|9l@NJvOfP*6ZX zfS;eAudlDSx3{OKr@OnmtE;QCvop}+41)kD{$ycfX9#A{0l5VfCk*WW8-jxwT3XxM zJ370%dpeqXCrq3)dCJsj(`U?_HG9t7dGi-6T(o$}(q+q6EMSyU6K`dbQDR_IP*vNm z&M%^|u7yignomt{TLh9X-vlT%qa~3Xe4)wGVP;i|rWZ3pVTZ!3`iIq!%NnqiVmI=~4yG?*UBuy@#qi_9lOv%EOFWqjwm)IFQ?j)3`1T=zfmcO{foI3W6sLtd zS4F+)nC;}rU*j;#AyW5g3x|3eXP|{qRNj;|3pfM literal 0 HcmV?d00001 diff --git a/src/server/master/web_ui/application/web_ui/static/images/lightbox-btn-prev.gif b/src/server/master/web_ui/application/web_ui/static/images/lightbox-btn-prev.gif new file mode 100644 index 0000000000000000000000000000000000000000..040ee5992f7fdb9b51907cb4ba1c5570b1b5482a GIT binary patch literal 832 zcmZ?wbhEHbv}aIYc*el+|NsBLfB*je{rl(7pFe*5`1bAFmoHyFefsp_!-se8-o1MD z>gCIq&!0bk`t<4J$B!RAe0cx<{kwPX-oAbN)~#DNZrr$b?b?+qS1w$*aPHi>GiT16 zK7IP+$&<&AA3u8Z=;6bM4<0Ta{2P*ix)3mxNzb8`Sa(^n>T09oLRGG&6qJ`%9JS+CQRt>@9*vHZEbCBZf^0EX>c(&&|!v&d$!v%uG#9O-@cuN=iydNQjGz zi;0Peii(PehzJV{3keAc4h{|s4D|Q+_x1Jl_V)Jl^mKQ3cXD!aaBu(y1q0oI;!hSv zc7{j>9gxkSIALJ_-@wq++|t_C-qG3B78%*yKVf2j-=wM2rq7r;YxbPE^X4yDxM=Z` zrOTGDSUHZ{$*`cIU*V9G=mkL*$3~&B9F~NQE=O7f+$T8{d|+|! z5LB^I==tcv%)lM$(xcU&+%-wnv&x6z08`gQRsV((28G1VNlI>g4%W9mc+UCN{rKLQV#k0fcFk=e`pVK=5riUkZTAtTQ6isw#WRUmZ3z(a9R-j$e ze3FRFDrSywC%q{a3<^!By0Zf_LL_n(8yB!Rulm4I*~32ZbaR4DVcG)ssl5F2B6c2< z?&fr4Oo*`z)H=|_wuV)6!iF%%R#s*epBZ+t2Ny7lSgg8L*Lk3Uk?9ck4?hLV?=DPC dE8_S6+w=SThsUSq`|tm!>h(?3l#7MI8UW1vMEU># literal 0 HcmV?d00001 diff --git a/src/server/master/web_ui/application/web_ui/static/images/lightbox-ico-loading.gif b/src/server/master/web_ui/application/web_ui/static/images/lightbox-ico-loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..4f1429c06cb2ffd2910b038d06b01a4b3ee00a21 GIT binary patch literal 3990 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 literal 0 HcmV?d00001 diff --git a/src/server/master/web_ui/application/web_ui/static/images/ponypowered.png b/src/server/master/web_ui/application/web_ui/static/images/ponypowered.png new file mode 100644 index 0000000000000000000000000000000000000000..8ca1cd9976cdae1e55d5dd08f6433a6ef79951bb GIT binary patch literal 10833 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 b{}W&Up8ZL8>Cl?n00000NkvXXu0mjfKN~{K literal 0 HcmV?d00001 diff --git a/src/server/master/web_ui/application/web_ui/static/images/ponypowered_gray.png b/src/server/master/web_ui/application/web_ui/static/images/ponypowered_gray.png new file mode 100644 index 0000000000000000000000000000000000000000..6ba5ca8d11286ea933571791df96e86000da0332 GIT binary patch literal 11929 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 literal 0 HcmV?d00001 diff --git a/src/server/master/web_ui/application/web_ui/static/images/view_from_pontevecchio_florence.jpg b/src/server/master/web_ui/application/web_ui/static/images/view_from_pontevecchio_florence.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8f5606b119a31bdee0e8a99b6e17d2b24c07e316 GIT binary patch literal 272016 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°AjKYB|^{xf1WUc-Gh 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 z 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 literal 0 HcmV?d00001 diff --git a/src/server/master/web_ui/application/web_ui/templates/404.html b/src/server/master/web_ui/application/web_ui/templates/404.html new file mode 100644 index 0000000..9f412de --- /dev/null +++ b/src/server/master/web_ui/application/web_ui/templates/404.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block title %} +Page not found - Djangy: Instant deployment and scaling for your Django applications +{% endblock %} +{% block pagetitle %} +

    +{% endblock %} +{% block content %} +

    The page {{ request_path }} was not found.

    +

    {{ message }}

    +{% endblock %} +{% block scripts %} + +{% endblock %} diff --git a/src/server/master/web_ui/application/web_ui/templates/500.html b/src/server/master/web_ui/application/web_ui/templates/500.html new file mode 100644 index 0000000..6c1a3bf --- /dev/null +++ b/src/server/master/web_ui/application/web_ui/templates/500.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% block title %} +Page not found - Djangy: Instant deployment and scaling for your Django applications +{% endblock %} +{% block pagetitle %} +
    +

    Server error

    +
    +{% endblock %} +{% block content %} +{% if message %} +

    {{ message }}

    +{% endif %} +

    A server error has occurred.
    We've been notified and will fix this issue shortly!

    +{% endblock %} +{% block scripts %} + +{% endblock %} diff --git a/src/server/master/web_ui/application/web_ui/templates/base.html b/src/server/master/web_ui/application/web_ui/templates/base.html new file mode 100644 index 0000000..3dbbd9c --- /dev/null +++ b/src/server/master/web_ui/application/web_ui/templates/base.html @@ -0,0 +1,90 @@ + + + + {% block title %}Djangy - Instant deployment and scaling for your Django applications{% endblock %} + + + + + + + + + + + + + + + + {% block scripts %} + {% endblock %} + + +{% if index %} + +
    +{% else %} + + +
    +{% endif %} + + {% if index %} + +
    + {% else %} + +
    + {% endif %} + + +
    + + {% block showcase %} + {% endblock %} + {% block pagetitle %} + {% endblock %} +
    + + + +
    + + +
    +
    + {% if message %} +

    {{ message }}



    + {% endif %} + {% block content %} + {% endblock %} +
    +
    + + +
    + + + + + + +
    + + +
    + + + diff --git a/src/server/master/web_ui/application/web_ui/templates/docs_navbar.html b/src/server/master/web_ui/application/web_ui/templates/docs_navbar.html new file mode 100644 index 0000000..2393773 --- /dev/null +++ b/src/server/master/web_ui/application/web_ui/templates/docs_navbar.html @@ -0,0 +1,8 @@ + diff --git a/src/server/master/web_ui/application/web_ui/templates/docs_tutorial_navbar.html b/src/server/master/web_ui/application/web_ui/templates/docs_tutorial_navbar.html new file mode 100644 index 0000000..f6ef775 --- /dev/null +++ b/src/server/master/web_ui/application/web_ui/templates/docs_tutorial_navbar.html @@ -0,0 +1,16 @@ + diff --git a/src/server/master/web_ui/application/web_ui/templates/footer.html b/src/server/master/web_ui/application/web_ui/templates/footer.html new file mode 100644 index 0000000..d305e83 --- /dev/null +++ b/src/server/master/web_ui/application/web_ui/templates/footer.html @@ -0,0 +1,3 @@ + diff --git a/src/server/master/web_ui/application/web_ui/templates/navbar.html b/src/server/master/web_ui/application/web_ui/templates/navbar.html new file mode 100644 index 0000000..a958ff0 --- /dev/null +++ b/src/server/master/web_ui/application/web_ui/templates/navbar.html @@ -0,0 +1,16 @@ + diff --git a/src/server/master/web_ui/application/web_ui/urls.py b/src/server/master/web_ui/application/web_ui/urls.py new file mode 100644 index 0000000..aed3dc0 --- /dev/null +++ b/src/server/master/web_ui/application/web_ui/urls.py @@ -0,0 +1,47 @@ +from django.conf.urls.defaults import * +from django.views.generic.simple import redirect_to + +# Uncomment the next two lines to enable the admin: +# from django.contrib import admin +# admin.autodiscover() +urlpatterns = patterns('', + (r'^$', 'web_ui.main.views.index.index'), + (r'^pricing$', 'web_ui.main.views.index.pricing'), + # Log in/log out/request password reset + (r'^login$', 'web_ui.main.views.login_logout.login'), + (r'^logout$', 'web_ui.main.views.login_logout.logout'), + (r'^request_reset_password$', 'web_ui.main.views.login_logout.request_reset_password'), + (r'^reset_password$', 'web_ui.main.views.login_logout.reset_password'), + (r'^set_password$', 'web_ui.main.views.login_logout.set_password'), + # Request account/create account using invite code + (r'^signup$', 'web_ui.main.views.create_account.signup'), + (r'^join$', 'web_ui.main.views.create_account.join'), + (r'^hackerdojo$', 'web_ui.main.views.create_account.hackerdojo'), + # Administrative dashboard + (r'^admin$', 'web_ui.main.views.admin.admin'), + (r'^admin/invite$', 'web_ui.main.views.admin.invite'), + (r'^admin/get_emails$', 'web_ui.main.views.admin.get_emails'), + # User dashboard + (r'^dashboard$', 'web_ui.main.views.dashboard_applicationlist.applicationlist'), + (r'^dashboard/account$', 'web_ui.main.views.dashboard_account.account'), + (r'^dashboard/account/change_password$', 'web_ui.main.views.dashboard_account.change_password'), + (r'^dashboard/account/change_email$', 'web_ui.main.views.dashboard_account.change_email'), + (r'^dashboard/account/add_ssh_public_key$', 'web_ui.main.views.dashboard_account.add_ssh_public_key'), + (r'^dashboard/account/remove_ssh_public_key$', 'web_ui.main.views.dashboard_account.remove_ssh_public_key'), + (r'^dashboard/invite$', 'web_ui.main.views.dashboard_invite.invite'), + (r'^dashboard/application/(?P[^/]*)$', 'web_ui.main.views.dashboard_application.application'), + (r'^dashboard/application/(?P[^/]*)/allocation$', 'web_ui.main.views.dashboard_application.application_allocation_redirect'), + (r'^dashboard/application/(?P[^/]*)/add_domain$', 'web_ui.main.views.dashboard_application.add_domain_redirect'), + (r'^dashboard/application/(?P[^/]*)/remove_domain$', 'web_ui.main.views.dashboard_application.remove_domain_redirect'), + (r'^dashboard/application/(?P[^/]*)/debug$', 'web_ui.main.views.dashboard_application.debug_redirect'), + (r'^dashboard/application/(?P[^/]*)/server_cache$', 'web_ui.main.views.dashboard_application.server_cache_redirect'), + (r'^dashboard/application/(?P[^/]*)/logs$', 'web_ui.main.views.dashboard_application.logs'), + (r'^dashboard/application/(?P[^/]*)/add_collaborator$', 'web_ui.main.views.dashboard_application.add_collaborator'), + (r'^dashboard/application/(?P[^/]*)/remove_collaborator$', 'web_ui.main.views.dashboard_application.remove_collaborator'), + (r'^dashboard/application/(?P[^/]*)/delete$', 'web_ui.main.views.dashboard_application.delete_application'), + (r'^dashboard/billing$', 'web_ui.main.views.dashboard_billing.update_billing_info'), + # Documentation + (r'^docs/', include('docs.urls')), + # Static content -- note, we should run web_ui as a djangy application since we're using static.serve + (r'^static/(?P.*)$', 'django.views.static.serve', {'document_root':'static'}), +) diff --git a/src/server/master/web_ui/config/apache.conf b/src/server/master/web_ui/config/apache.conf new file mode 100644 index 0000000..32a9daf --- /dev/null +++ b/src/server/master/web_ui/config/apache.conf @@ -0,0 +1,40 @@ + + ServerName www.djangy.com + ServerAlias djangy.com + ServerAdmin support@djangy.com + + Redirect permanent / https://www.djangy.com/ + + + + ServerName www-test.djangy.com + ServerAdmin support@djangy.com + + Redirect permanent / https://www-test.djangy.com/ + + + + ServerName www.djangy.com + ServerAlias www-test.djangy.com + ServerAdmin support@djangy.com + + DocumentRoot /srv/djangy/src/server/master/web_ui/application/web_ui/static + + WSGIScriptAlias / /srv/djangy/src/server/master/web_ui/config/production.wsgi + WSGIDaemonProcess web_ui display-name=web_ui + + Order allow,deny + Allow from all + + + Alias /robots.txt /srv/djangy/src/server/master/web_ui/application/web_ui/static/robots.txt + Alias /favicon.ico /srv/djangy/src/server/master/web_ui/application/web_ui/static/favicon.ico + Alias /static /srv/djangy/src/server/master/web_ui/application/web_ui/static + + ErrorLog /srv/logs/djangy.com/error.log + CustomLog /srv/logs/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_ui/config/production.wsgi b/src/server/master/web_ui/config/production.wsgi new file mode 100644 index 0000000..ee7d9bf --- /dev/null +++ b/src/server/master/web_ui/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_ui/application/web_ui') +sys.path.append('/srv/djangy/src/server/master/web_ui/application') + +os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' + +import django.core.handlers.wsgi +application = django.core.handlers.wsgi.WSGIHandler() diff --git a/src/server/proxycache/nginx.conf b/src/server/proxycache/nginx.conf new file mode 100644 index 0000000..1a1a5ba --- /dev/null +++ b/src/server/proxycache/nginx.conf @@ -0,0 +1,78 @@ + +user proxycache proxycache; +worker_processes 1; + +#error_log logs/error.log; +#error_log logs/error.log notice; +#error_log logs/error.log info; + +#pid logs/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include mime.types; + default_type application/octet-stream; + server_names_hash_max_size 65536; + #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + # '$status $body_bytes_sent "$http_referer" ' + # '"$http_user_agent" "$http_x_forwarded_for"'; + + #access_log logs/access.log main; + + sendfile on; + #tcp_nopush on; + + #keepalive_timeout 0; + keepalive_timeout 65; + + #gzip on; + + # + # Allow large-ish file uploads to Djangy applications. + # + client_max_body_size 50m; + + # + # Djangy catch-all redirect + # + server { + listen 80; + server_name _; + rewrite ^(.*)$ https://www.djangy.com/ redirect; + + #access_log logs/host.access.log main; + } + + # + # Special case redirects for http://[www.]djangy.com + # + server { + listen 80; + server_name djangy.com www.djangy.com; + rewrite ^(.*)$ https://www.djangy.com$1 permanent; + } + + # + # Super-special case redirect for http://www-test.djangy.com + # + server { + listen 80; + server_name www-test.djangy.com; + rewrite ^(.*)$ https://www-test.djangy.com$1 permanent; + } + + # + # Djangy nginx cache settings + # + proxy_cache_key "$scheme://$host$request_uri"; + proxy_cache_use_stale error timeout updating http_404; + # + # Djangy include application nginx config files + # + include /srv/proxycache_manager/nginx/conf/applications/*.conf; +} diff --git a/src/server/proxycache/proxycache_manager/clear_cache.py b/src/server/proxycache/proxycache_manager/clear_cache.py new file mode 100755 index 0000000..68d9abf --- /dev/null +++ b/src/server/proxycache/proxycache_manager/clear_cache.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +# +# Erase the nginx cache for a given application +# Example usage: +# clear_cache.py application_name testapp +# + +from shared import * +import os, os.path, shutil + +def main(): + try: + check_trusted_uid(sys.argv[0]) + kwargs = check_and_return_keyword_args(sys.argv, ['application_name']) + clear_cache(**kwargs) + except: + log_last_exception() + +def clear_cache(application_name): + if is_valid_application_name(application_name): + try: + shutil.rmtree(os.path.join(NGINX_CACHE_DIR, application_name)) + except: + # Cache may not yet exist + pass + +if __name__ == '__main__': + main() diff --git a/src/server/proxycache/proxycache_manager/configure.py b/src/server/proxycache/proxycache_manager/configure.py new file mode 100755 index 0000000..a8a7729 --- /dev/null +++ b/src/server/proxycache/proxycache_manager/configure.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# +# Configure the nginx proxy cache for a given application. +# Example usage: +# configure.py application_name testapp http_virtual_hosts 'testapp.djangy.com www.testapp.com' worker_servers 'worker3.internal.djangy.com:8080' cache_index_size_kb 16 cache_size_kb 1024 +# + +import os +from shared import * +from mako.template import Template +from mako.lookup import TemplateLookup +import clear_cache + +def main(): + try: + check_trusted_uid(sys.argv[0]) + kwargs = check_and_return_keyword_args(sys.argv, ['application_name', \ + 'http_virtual_hosts', 'worker_servers', 'cache_index_size_kb', 'cache_size_kb']) + + configure(**kwargs) + except: + log_last_exception() + +def configure(application_name, http_virtual_hosts, worker_servers, cache_index_size_kb, cache_size_kb): + create_config_file(application_name, http_virtual_hosts.split(','), \ + worker_servers.split(','), int(cache_index_size_kb), int(cache_size_kb)) + clear_cache.clear_cache(application_name) + reload_nginx_conf() + +def create_config_file(application_name, http_virtual_hosts, \ + worker_servers, cache_index_size_kb, cache_size_kb): + + # Create Nginx config file in nginx/conf/applications/ + # http_virtual_hosts -- list of virtual host names + # worker_servers -- list of 'host:port' for workers + # cache_index_size -- in memory + # cache_size -- on disk/in disk cache + print 'Generating nginx configuration file...', + nginx_conf_path = os.path.join(NGINX_APP_CONF_DIR, '%s.conf' % application_name) + + # Remove the old config file + try: + os.remove(nginx_conf_path) + except: + pass + + if http_virtual_hosts != [] and worker_servers != []: + # Create new config file + upstream_servers = '\n '.join(map(lambda x: 'server %s;' % x, worker_servers)) + generate_config_file('generic_nginx_conf', nginx_conf_path, + application_name = application_name, \ + http_virtual_hosts = ' '.join(http_virtual_hosts), \ + upstream_servers = upstream_servers, \ + cache_index_size_kb = cache_index_size_kb, \ + cache_size_kb = cache_size_kb) + # Set permissions + os.chown(nginx_conf_path, PROXYCACHE_UID, PROXYCACHE_GID) + os.chmod(nginx_conf_path, 0600) + + print 'Done.' + print '' + +### Copied from master_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 = [PROXYCACHE_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() + +if __name__ == '__main__': + main() diff --git a/src/server/proxycache/proxycache_manager/delete_application.py b/src/server/proxycache/proxycache_manager/delete_application.py new file mode 100755 index 0000000..81e55bf --- /dev/null +++ b/src/server/proxycache/proxycache_manager/delete_application.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +# +# Remove a given application from the nginx proxy cache +# Example usage: +# remove_application.py application_name testapp +# + +import os +from shared import * +from mako.template import Template +from mako.lookup import TemplateLookup + +def main(): + try: + check_trusted_uid(sys.argv[0]) + kwargs = check_and_return_keyword_args(sys.argv, ['application_name']) + + delete_application(**kwargs) + except: + log_last_exception() + +def delete_application(application_name): + print 'Removing nginx configuration file for %s...' % application_name + nginx_conf_path = os.path.join(NGINX_APP_CONF_DIR, '%s.conf' % application_name) + + # Remove the old config file + try: + os.remove(nginx_conf_path) + except: + pass + + # Remove the cache + print 'Removing nginx cache for %s...' % application_name + clear_cache.clear_cache(application_name) + + reload_nginx_conf() + +if __name__ == '__main__': + main() diff --git a/src/server/proxycache/proxycache_manager/setuid/.gitignore b/src/server/proxycache/proxycache_manager/setuid/.gitignore new file mode 100644 index 0000000..22afc55 --- /dev/null +++ b/src/server/proxycache/proxycache_manager/setuid/.gitignore @@ -0,0 +1,3 @@ +run_clear_cache +run_configure +run_remove_application diff --git a/src/server/proxycache/proxycache_manager/setuid/Makefile b/src/server/proxycache/proxycache_manager/setuid/Makefile new file mode 100644 index 0000000..a64d253 --- /dev/null +++ b/src/server/proxycache/proxycache_manager/setuid/Makefile @@ -0,0 +1,8 @@ +TARGETS=run_clear_cache run_configure run_delete_application + +all: $(TARGETS) + -chown root.djangy $(TARGETS) + chmod 6710 $(TARGETS) + +clean: + rm -f $(TARGETS) *~ diff --git a/src/server/proxycache/proxycache_manager/setuid/config.h b/src/server/proxycache/proxycache_manager/setuid/config.h new file mode 100644 index 0000000..646bea1 --- /dev/null +++ b/src/server/proxycache/proxycache_manager/setuid/config.h @@ -0,0 +1,7 @@ +#define ROOT_UID 0 +#define WWW_DATA_UID 33 +#define GITOSIS_UID 105 + +#define PATH "/srv/djangy/run/python-virtual/bin:/usr/sbin:/usr/bin:/sbin:/bin" +#define VIRTUAL_ENV "/srv/djangy/run/python-virtual" +#define PROGRAM_DIR "/srv/djangy/src/server/proxycache/proxycache_manager/" // trailing slash is important diff --git a/src/server/proxycache/proxycache_manager/setuid/run.h b/src/server/proxycache/proxycache_manager/setuid/run.h new file mode 100644 index 0000000..0b890c5 --- /dev/null +++ b/src/server/proxycache/proxycache_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/proxycache/proxycache_manager/setuid/run_clear_cache.c b/src/server/proxycache/proxycache_manager/setuid/run_clear_cache.c new file mode 100644 index 0000000..3a4824b --- /dev/null +++ b/src/server/proxycache/proxycache_manager/setuid/run_clear_cache.c @@ -0,0 +1,11 @@ +// +// Run clear_cache. Must be setuid root. +// +#include "run.h" + +MAIN("clear_cache.py") + +int is_trusted_user(uid_t uid) +{ + return (uid == ROOT_UID); +} diff --git a/src/server/proxycache/proxycache_manager/setuid/run_configure.c b/src/server/proxycache/proxycache_manager/setuid/run_configure.c new file mode 100644 index 0000000..9cc792e --- /dev/null +++ b/src/server/proxycache/proxycache_manager/setuid/run_configure.c @@ -0,0 +1,11 @@ +// +// Run configure. Must be setuid root. +// +#include "run.h" + +MAIN("configure.py") + +int is_trusted_user(uid_t uid) +{ + return (uid == ROOT_UID); +} diff --git a/src/server/proxycache/proxycache_manager/setuid/run_delete_application.c b/src/server/proxycache/proxycache_manager/setuid/run_delete_application.c new file mode 100644 index 0000000..d9dd8dc --- /dev/null +++ b/src/server/proxycache/proxycache_manager/setuid/run_delete_application.c @@ -0,0 +1,11 @@ +// +// Run delete_application. 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/proxycache/proxycache_manager/shared/__init__.py b/src/server/proxycache/proxycache_manager/shared/__init__.py new file mode 100644 index 0000000..d11b5b6 --- /dev/null +++ b/src/server/proxycache/proxycache_manager/shared/__init__.py @@ -0,0 +1,6 @@ +from djangy_server_shared import * +from nginx import * + +TRUSTED_UIDS.extend(PROXYCACHE_TRUSTED_UIDS) + +open_log_file(os.path.join(LOGS_DIR, 'proxycache.log'), 0600) diff --git a/src/server/proxycache/proxycache_manager/shared/nginx.py b/src/server/proxycache/proxycache_manager/shared/nginx.py new file mode 100644 index 0000000..5de61ad --- /dev/null +++ b/src/server/proxycache/proxycache_manager/shared/nginx.py @@ -0,0 +1,8 @@ +from djangy_server_shared import * + +def reload_nginx_conf(): + result = run_external_program([NGINX_BIN_PATH, '-t']) + if result['exit_code'] != 0: # (note that 'nginx -t' outputs on stderr on success as well as failure) + # Error should already be logged by run_external_program + return + run_external_program([NGINX_BIN_PATH, '-s', 'reload']) diff --git a/src/server/proxycache/proxycache_manager/templates/generic_nginx_conf b/src/server/proxycache/proxycache_manager/templates/generic_nginx_conf new file mode 100644 index 0000000..7eae833 --- /dev/null +++ b/src/server/proxycache/proxycache_manager/templates/generic_nginx_conf @@ -0,0 +1,34 @@ +upstream upstream_${ application_name } { + ${ upstream_servers } +} + +% if cache_size_kb > 0: +proxy_cache_path /srv/proxycache_manager/nginx/cache/${ application_name } levels=2:2 keys_zone=${ application_name }:${ str(cache_index_size_kb) }k inactive=1d max_size=${ str(cache_size_kb) }k; +% endif + +server { + listen 80; + server_name ${ http_virtual_hosts }; + location / { + proxy_pass http://upstream_${ application_name }/; + proxy_redirect default; + proxy_set_header Host $host; + % if cache_size_kb > 0: + proxy_cache ${ application_name }; + % endif + } + + #access_log logs/host.access.log main; + + # Crazy naming scheme for error pages to avoid collisions with real application URLs: + + #proxy_intercept_errors on; + #error_page 500 /__500__asdfxyz1029; + #location = /__500__asdfxyz1029 { + # alias /srv/proxycache_manager/500.html; + #} + error_page 502 /__502__asdfxyz1029; + location = /__502__asdfxyz1029 { + alias /srv/proxycache_manager/502.html; + } +} diff --git a/src/server/shared/djangy_server_shared/__init__.py b/src/server/shared/djangy_server_shared/__init__.py new file mode 100644 index 0000000..89a6dc0 --- /dev/null +++ b/src/server/shared/djangy_server_shared/__init__.py @@ -0,0 +1,9 @@ +import grp, os, os.path, pwd, re, shutil, sys, tempfile +from bundle_info import * +from constants import * +from exceptions import * +from find_django_project import * +from functions import * +from json_log import * +from resource_allocation import * +from run_external_program import * diff --git a/src/server/shared/djangy_server_shared/bundle_info.py b/src/server/shared/djangy_server_shared/bundle_info.py new file mode 100644 index 0000000..91824ac --- /dev/null +++ b/src/server/shared/djangy_server_shared/bundle_info.py @@ -0,0 +1,68 @@ +from ConfigParser import RawConfigParser + +def write_params(file_path, section_name, **params): + config = RawConfigParser() + config.add_section(section_name) + for (k, v) in params.items(): + config.set(section_name, k, str(v)) + f = open(file_path, 'w') + config.write(f) + f.close() + +class BundleInfo(object): + def __init__(self, django_project_path, django_admin_media_path, \ + admin_media_prefix, admin_email, setup_uid, web_uid, cron_uid, \ + app_gid, user_settings_module_name, db_host, db_port, db_name, \ + db_username, db_password): + + self.django_project_path = django_project_path + self.django_admin_media_path = django_admin_media_path + self.admin_media_prefix = admin_media_prefix + self.admin_email = admin_email + self.setup_uid = setup_uid + self.web_uid = web_uid + self.cron_uid = cron_uid + self.app_gid = app_gid + self.user_settings_module_name = user_settings_module_name + self.db_host = db_host + self.db_port = db_port + self.db_name = db_name + self.db_username = db_username + self.db_password = db_password + + def save_to_file(self, file_path): + write_params(file_path, 'bundle_info', \ + django_project_path = self.django_project_path, \ + django_admin_media_path = self.django_admin_media_path, \ + admin_media_prefix = self.admin_media_prefix, \ + admin_email = self.admin_email, \ + setup_uid = self.setup_uid, \ + web_uid = self.web_uid, \ + cron_uid = self.cron_uid, \ + app_gid = self.app_gid, \ + user_settings_module_name = self.user_settings_module_name, \ + db_host = self.db_host, \ + db_port = self.db_port, \ + db_name = self.db_name, \ + db_username = self.db_username, \ + db_password = self.db_password) + + @staticmethod + def load_from_file(file_path): + parser = RawConfigParser() + parser.read(file_path) + return BundleInfo( \ + django_project_path = parser.get('bundle_info', 'django_project_path'), \ + django_admin_media_path = parser.get('bundle_info', 'django_admin_media_path'), \ + admin_media_prefix = parser.get('bundle_info', 'admin_media_prefix'), \ + admin_email = parser.get('bundle_info', 'admin_email'), \ + setup_uid = int(parser.get('bundle_info', 'setup_uid')), \ + web_uid = int(parser.get('bundle_info', 'web_uid')), \ + cron_uid = int(parser.get('bundle_info', 'cron_uid')), \ + app_gid = int(parser.get('bundle_info', 'app_gid')), \ + user_settings_module_name = parser.get('bundle_info', 'user_settings_module_name'), \ + db_host = parser.get('bundle_info', 'db_host'), \ + db_port = int(parser.get('bundle_info', 'db_port')), \ + db_name = parser.get('bundle_info', 'db_name'), \ + db_username = parser.get('bundle_info', 'db_username'), \ + db_password = parser.get('bundle_info', 'db_password')) diff --git a/src/server/shared/djangy_server_shared/constants.py b/src/server/shared/djangy_server_shared/constants.py new file mode 100644 index 0000000..3f5bfa4 --- /dev/null +++ b/src/server/shared/djangy_server_shared/constants.py @@ -0,0 +1,123 @@ +import grp, os, pwd +import installer_configured_constants + +# Users and groups +# DJANGY_USERNAME = 'djangy' +# TODO: we should create a 'djangy' user which has ssh access to +# worker_manager and proxycache_manager hosts and can only run +# the worker_manager and proxycache_manager methods, but can't +# do just arbitrary things on its own. (Right now we ssh as root.) +DJANGY_GROUPNAME = 'djangy' +GIT_USERNAME = 'git' +GIT_GROUPNAME = 'git' +PROXYCACHE_USERNAME = 'proxycache' +PROXYCACHE_GROUPNAME = 'proxycache' +SHELL_USERNAME = 'shell' +SHELL_GROUPNAME = 'shell' +WWW_DATA_USERNAME = 'www-data' +WWW_DATA_GROUPNAME = 'www-data' + +# UIDs and GIDs -- computed from users and groups +# DJANGY_UID = pwd.getpwnam(DJANGY_USERNAME).pw_uid +DJANGY_GID = grp.getgrnam(DJANGY_GROUPNAME).gr_gid +GIT_UID = pwd.getpwnam(GIT_USERNAME).pw_uid +GIT_GID = grp.getgrnam(GIT_GROUPNAME).gr_gid +PROXYCACHE_UID = pwd.getpwnam(PROXYCACHE_USERNAME ).pw_uid +PROXYCACHE_GID = grp.getgrnam(PROXYCACHE_GROUPNAME).gr_gid +SHELL_UID = pwd.getpwnam(SHELL_USERNAME ).pw_uid +SHELL_GID = grp.getgrnam(SHELL_GROUPNAME).gr_gid +ROOT_UID = 0 +ROOT_GID = 0 +WWW_DATA_UID = pwd.getpwnam(WWW_DATA_USERNAME).pw_uid +WWW_DATA_GID = grp.getgrnam(WWW_DATA_GROUPNAME).gr_gid + +# Other shared constants +INSTALL_ROOT_DIR = '/srv' +BUNDLES_DIR = os.path.join(INSTALL_ROOT_DIR, 'bundles') +DJANGY_DIR = os.path.join(INSTALL_ROOT_DIR, 'djangy') +LOGS_DIR = os.path.join(INSTALL_ROOT_DIR, 'logs') +PYTHON_BIN_PATH = os.path.join(DJANGY_DIR, 'run/python-virtual/bin/python') +BUNDLE_VERSION_PREFIX = 'v1g' + +#GITOSIS_ADMIN_DIR = os.path.join(INSTALL_ROOT_DIR, 'scratch') +#GITOSIS_ADMIN_REPO = 'git@%s:gitosis-admin.git' % installer_configured_constants.MASTER_MANAGER_HOST + +DATABASE_ROOT_USER = 'root' +DATABASE_ROOT_PASSWORD = 'password goes here' + +DEFAULT_DATABASE_HOST = installer_configured_constants.DEFAULT_DATABASE_HOST # XXX +DEFAULT_PROXYCACHE_HOST = installer_configured_constants.DEFAULT_PROXYCACHE_HOST # XXX + +TRUSTED_UIDS = [] + +# Master constants +MASTER_TRUSTED_UIDS = [ROOT_UID, GIT_UID, WWW_DATA_UID] + +MASTER_SETUID_DIR = os.path.join(DJANGY_DIR, 'run/master_manager/setuid') +MASTER_MANAGER_SRC_DIR = os.path.join(DJANGY_DIR, 'src/server/master/master_manager') +GIT_SSH_DIR = os.path.join(INSTALL_ROOT_DIR, 'git/.ssh') +SHELL_SSH_DIR = os.path.join(INSTALL_ROOT_DIR, 'shell/.ssh') +AUTHORIZED_KEYS_MODE = 0644 +REPOS_DIR = os.path.join(INSTALL_ROOT_DIR, 'git/repositories') +MASTER_MANAGER_HOST = installer_configured_constants.MASTER_MANAGER_HOST # XXX - used to define BUNDLES_SRC_HOST for worker_manager below +DEVPAYMENTS_API_KEY = installer_configured_constants.DEVPAYMENTS_API_KEY + +GIT_SERVE_PATH = '/srv/djangy/run/python-virtual/bin/git_serve.py' +SHELL_SERVE_PATH = '/srv/djangy/run/master_manager/setuid/run_shell_serve' + +# XXX deprecate chargify constants +CHARGIFY_SUBDOMAIN = 'subdomain goes here' +CHARGIFY_API_KEY = 'password goes here' +CHARGIFY_PRODUCT_ID = 14215 +CHARGIFY_COMPONENTS = [ + ('worker_processes',1537) +] + +# List of specific names applications can't have +RESERVED_APPLICATION_NAMES = [ 'djangy', 'www', 'www-s', 'https', 'ssl', 'secure', 'api', 'mail', 'localhost', 'web' ] + +# blocked remote manage.py commands +BLOCKED_COMMANDS = [ + 'runserver', + 'dbshell', + 'test', + 'testserver', + 'runfcgi', + 'changepassword', + 'compilemessages', + 'makemessages', + 'schemamigration', + 'datamigration' +] + +# Worker constants +WORKER_TRUSTED_UIDS = [ROOT_UID] + +WORKER_SETUID_DIR = os.path.join(DJANGY_DIR, 'run/worker_manager/setuid') +WORKER_MANAGER_SRC_DIR = os.path.join(DJANGY_DIR, 'src/server/worker/worker_manager') +WORKER_MANAGER_VAR_DIR = os.path.join(INSTALL_ROOT_DIR, 'worker_manager') +WORKER_TEMPLATE_DIR = os.path.join(WORKER_MANAGER_SRC_DIR, 'templates') + +BUNDLES_SRC_HOST = MASTER_MANAGER_HOST +BUNDLES_SRC_DIR = BUNDLES_DIR +BUNDLES_DEST_DIR = BUNDLES_DIR + +APACHE_SITES_AVAILABLE = '/etc/apache2/sites-available' + +LOGS = ['django.log', 'error.log', 'access.log', 'celery.log'] + +MAX_PROCS_PER_WORKER = 100 +WORKER_PORT_LOWER = 20000 +WORKER_PORT_UPPER = 40000 +DEFAULT_WORKER_PORT = 20000 + +# Proxycache constants +PROXYCACHE_TRUSTED_UIDS = [ROOT_UID] + +PROXYCACHE_SETUID_DIR = os.path.join(DJANGY_DIR, 'run/proxycache_manager/setuid') +PROXYCACHE_TEMPLATE_DIR = os.path.join(DJANGY_DIR, 'src/server/proxycache/proxycache_manager/templates') + +NGINX_DIR = os.path.join(INSTALL_ROOT_DIR, 'proxycache_manager/nginx') +NGINX_BIN_PATH = os.path.join(NGINX_DIR, 'sbin/nginx') +NGINX_APP_CONF_DIR = os.path.join(NGINX_DIR, 'conf/applications') +NGINX_CACHE_DIR = os.path.join(NGINX_DIR, 'cache') diff --git a/src/server/shared/djangy_server_shared/exceptions.py b/src/server/shared/djangy_server_shared/exceptions.py new file mode 100644 index 0000000..60fb6b7 --- /dev/null +++ b/src/server/shared/djangy_server_shared/exceptions.py @@ -0,0 +1,85 @@ +class BundleAlreadyExistsException(Exception): + """Could not create a bundle because it already exists.""" + def __init__(self, bundle_name): + self.bundle_name = bundle_name + def __str__(self): + return 'Could not create bundle "%s" because it already exists.' % self.bundle_name + +class InvalidBundleException(Exception): + """Invalid bundle.""" + def __init__(self, bundle_name): + self.bundle_name = bundle_name + def __str__(self): + return 'Invalid bundle name "%s".' % self.bundle_name + +class GitCloneException(Exception): + """Error in git clone.""" + def __init__(self, application_name, temp_repo_path): + self.application_name = application_name + self.temp_repo_path = temp_repo_path + def __str__(self): + return 'Error in git clone: application_name="%s" and temp_repo_path="%s"' % (self.application_name, self.temp_repo_path) + +class CheckApplicationUidGidException(Exception): + """Checking application uid/gid failed.""" + def __init__(self, id_type, id_value): + self.id_type = id_type + self.id_value = id_value + def __str__(self): + return 'Checking application uid/gid failed: %i is not a valid %s.' % (self.id_value, self.id_type) + +class SetUidGidFailedException(Exception): + """Set uid/gid failed.""" + def __init__(self): + pass + def __str__(self): + return self.__doc__ + +class InvalidApplicationNameException(Exception): + """The requested application name does not comply with Djangy's application naming guidelines.""" + def __init__(self, application_name): + self.application_name = application_name + def __str__(self): + return 'The application name "%s" does not comply with Djangy\'s application naming guidelines.' % self.application_name + +class ApplicationNotInDatabaseException(Exception): + """The requested application was not found in the management database.""" + def __init__(self, application_name): + self.application_name = application_name + def __str__(self): + return 'Could not find application "%s" in management database.' % self.application_name + +class ArgumentException(Exception): + """Error parsing command-line argument list.""" + def __init__(self): + pass + def __str__(self): + return self.__doc__ + +class RepeatedArgumentException(ArgumentException): + """The same key was used for multiple command-line arguments.""" + def __init__(self, key): + self.key = key + def __str__(self): + return 'The key "%s" was used for multiple command-line arguments.' % self.key + +class UnexpectedArgumentException(ArgumentException): + """An unknown key was used for a command-line argument.""" + def __init__(self, key): + self.key = key + def __str__(self): + return 'Unknown key "%s" was used for a command-line argument.' % self.key + +class MissingArgumentException(ArgumentException): + """A command-line argument was missing.""" + def __init__(self): + pass + def __str__(self): + return self.__doc__ + +class PasswordGenerationException(Exception): + """Password generation failed.""" + def __init__(self): + pass + def __str__(self): + return self.__doc__ diff --git a/src/server/shared/djangy_server_shared/find_django_project.py b/src/server/shared/djangy_server_shared/find_django_project.py new file mode 100644 index 0000000..6c98803 --- /dev/null +++ b/src/server/shared/djangy_server_shared/find_django_project.py @@ -0,0 +1,38 @@ +import os.path + +__DJANGO_PROJECT_DIR_FILES__ = set(['__init__.py', 'manage.py', 'settings.py', 'urls.py']) + +def find_django_project(repo_path): + """Finds a django project within the given repository. If the + repository contains more than one django project, an arbitrary one will + be chosen. Raises a NoDjangoProjectFoundException if the repository + does not contain any django projects.""" + + # Traverse a git repository, but don't follow symbolic links because we + # don't know where they might point. + for (dir_path, sub_dir_names, file_names) in os.walk(repo_path, topdown=True, followlinks=False): + # Don't bother stepping into the .git directory + if '.git' in sub_dir_names: + sub_dir_names.remove('.git') + # Check if we've found a django project + if '__init__.py' in file_names and \ + dir_contains_module(dir_path, 'manage') and \ + dir_contains_module(dir_path, 'settings') and \ + dir_contains_module(dir_path, 'urls'): + return dir_path + + # If we got here, we did an exhaustive search of the repository and + # couldn't find a django project. + raise DjangoProjectNotFoundException(repo_path) + +def dir_contains_module(dir_path, module_name): + return os.path.isfile(os.path.join(dir_path, module_name + '.py')) or \ + os.path.isdir(os.path.join(dir_path, module_name)) and \ + os.path.isfile(os.path.join(dir_path, module_name, '__init__.py')) + +class DjangoProjectNotFoundException(Exception): + """No django project found in the specified repository.""" + def __init__(self, repo_path): + self.repo_path = repo_path + def __str__(self): + return 'No django project found in the repository "%s".' % self.repo_path diff --git a/src/server/shared/djangy_server_shared/functions.py b/src/server/shared/djangy_server_shared/functions.py new file mode 100644 index 0000000..d58ecd3 --- /dev/null +++ b/src/server/shared/djangy_server_shared/functions.py @@ -0,0 +1,133 @@ +import binascii, os, re, sys +from constants import * +from exceptions import * +from json_log import * +from run_external_program import * + +def may_not_be_run_as(program_name, uid, gid): + print_or_log_usage('%s may not be run by uid %i gid %i' % (program_name, uid, gid)) + sys.exit(1) + +def check_trusted_uid(program_name): + if not os.getuid() in TRUSTED_UIDS: + may_not_be_run_as(program_name, os.getuid(), os.getgid()) + set_uid_gid(0, 0) + +def check_setup_uid(setup_uid): + if setup_uid < 100000 \ + or (setup_uid - 100000) % 3 != 0: + raise CheckApplicationUidGidException('setup_uid', setup_uid) + +def check_web_uid(web_uid): + if web_uid < 100000 \ + or (web_uid - 100000) % 3 != 1: + raise CheckApplicationUidGidException('web_uid', web_uid) + +def check_cron_uid(cron_uid): + if cron_uid < 100000 \ + or (cron_uid - 100000) % 3 != 2: + raise CheckApplicationUidGidException('cron_uid', cron_uid) + +def check_app_gid(app_gid): + if app_gid < 100000 \ + or (app_gid - 100000) % 3 != 0: + raise CheckApplicationUidGidException('app_gid', app_gid) + +def become_application_setup_uid_gid(program_name, setup_uid, app_gid): + try: + check_setup_uid(setup_uid) + check_app_gid(app_gid) + set_uid_gid(setup_uid, setup_uid) + except Exception as e: + may_not_be_run_as(program_name, setup_uid, app_gid) + +def become_uid_gid(program_name, uid, gid, groups=[]): + try: + set_uid_gid(uid, gid, groups) + except Exception as e: + may_not_be_run_as(program_name, os.getuid(), os.getgid()) + +def check_positional_args(args, expected_num_args, help_string): + if len(args) != expected_num_args+1: + print_or_log_usage('Usage: %s %s' % (args[0], help_string)) + sys.exit(1) + +def check_and_return_keyword_args(args, required_keys, optional_keys=None): + try: + return arg_list_to_dict(args[1:], required_keys, optional_keys) + except ArgumentException as e: + required_keys_message = ' '.join(map(lambda key: key + ' ', required_keys)) + if optional_keys: + optional_keys_message = ' '.join(map(lambda key: '[%s ]' % key, optional_keys)) + print_or_log_usage('Usage: %s %s %s' % (args[0], required_keys_message, optional_keys_message)) + else: + print_or_log_usage('Usage: %s %s' % (args[0], required_keys_message)) + sys.exit(1) + +def arg_list_to_dict(args, required_keys, optional_keys=None): + """Converts a list of alternating key-value pairs to a dictionary. + Checks that the dictionary keys are equal to a given list of expected + keys.""" + dict = {} + required_keys_used = [] + if optional_keys: + allowed_keys = required_keys + optional_keys + else: + allowed_keys = required_keys + for i in range(0, len(args)/2*2, 2): + key = args[i] + value = args[i+1] + if dict.has_key(key): + raise RepeatedArgumentException() + if not key in allowed_keys: + raise UnexpectedArgumentException(key) + dict[key] = value + if key in required_keys: + required_keys_used.append(key) + if sort(required_keys_used) != sort(required_keys): + raise MissingArgumentException() + return dict + +def sort(list): + sorted_list = list + [] + sorted_list.sort() + return sorted_list + +def set_uid_gid(uid, gid, groups=[]): + os.setgid(gid) + os.setegid(gid) + os.setgroups(groups) + os.setuid(uid) + os.seteuid(uid) + if os.getgid() != gid or os.getegid() != gid \ + or os.getuid() != uid or os.geteuid() != uid \ + or (os.getgroups() != [] and os.getgroups != [gid]): + raise SetUidGidFailedException() + +# Validation for djangy projects/applications +def is_valid_application_name(application_name): + return (re.match('^[A-Za-z][A-Za-z0-9]*$', application_name) != None) + +def check_application_name(application_name): + if not is_valid_application_name(application_name): + raise InvalidApplicationNameException() + +# Validation for django apps within a djangy project/application +def is_valid_django_app_name(django_app_name): + return (re.match('^[A-Za-z_][A-Za-z0-9_]*$', django_app_name) != None) + +def recursive_chown_chmod(bundle_path, uid, gid, mode): + # Make sure we're not changing BUNDLES_DIR itself! + if bundle_path.strip(' /') == '': + raise InvalidBundleException() + # chown/chmod bundle to setup_uid + run_external_program(['chown', '-R', str(uid) + ':' + str(gid), bundle_path], cwd=bundle_path) + run_external_program(['chmod', '-R', mode, bundle_path], cwd=bundle_path) + +def gen_password(): + """ Generate a random 24-character password. """ + password1 = binascii.b2a_base64(os.urandom(9))[:-1] + password2 = binascii.b2a_base64(os.urandom(9))[:-1] + if len(password1) != 12 or len(password2) != 12 or password1 == password2: + raise PasswordGenerationException() + return password1 + password2 diff --git a/src/server/shared/djangy_server_shared/installer_configured_constants.py b/src/server/shared/djangy_server_shared/installer_configured_constants.py new file mode 100644 index 0000000..a8f6983 --- /dev/null +++ b/src/server/shared/djangy_server_shared/installer_configured_constants.py @@ -0,0 +1,8 @@ +# This is a placeholder version of this file; in an actual installation, it +# is generated from scratch by the installer. The constants defined here +# are exposed by constants.py + +DEFAULT_DATABASE_HOST = None # XXX +DEFAULT_PROXYCACHE_HOST = None # XXX +MASTER_MANAGER_HOST = None # XXX - used to define BUNDLES_SRC_HOST for worker_manager below +DEVPAYMENTS_API_KEY = None diff --git a/src/server/shared/djangy_server_shared/json_log.py b/src/server/shared/djangy_server_shared/json_log.py new file mode 100644 index 0000000..5fc3487 --- /dev/null +++ b/src/server/shared/djangy_server_shared/json_log.py @@ -0,0 +1,109 @@ +import json, os, socket, sys, time, traceback +import logging +#from sentry.client.base import SentryClient + +__log_file_path__ = None +__log_file__ = None + +class LogFileAlreadyOpenException(Exception): + """Tried to open a different log file when the log file was already open.""" + def __init__(self, old_log_file, new_log_file): + self.old_log_file = old_log_file + self.new_log_file = new_log_file + def __str__(self): + return 'Tried to open new log file "%s" when old log file "%s%" was already open.' % (self.new_log_file, self.old_log_file) + +def open_log_file(log_file_path, mode): + global __log_file_path__ + global __log_file__ + if __log_file__ != None: + if __log_file_path__ != log_file_path: + raise LogFileAlreadyOpenException(__log_file_path__, log_file_path) + else: + __log_file_path__ = log_file_path + __log_file__ = open(log_file_path, 'a') + os.chmod(log_file_path, mode) + +def __format_time_utc__(time_struct): + return "%04i-%02i-%02i %02i:%02i:%02i.%03i UTC" % \ + (time_struct['tm_year'], time_struct['tm_mon'], time_struct['tm_mday'], \ + time_struct['tm_hour'], time_struct['tm_min'], time_struct['tm_sec'], time_struct['tm_msec']) + +def __current_time_utc__(): + time_struct = time.gmtime() + tm_msec = int(time.time() * 1000) % 1000 + time_dict = { \ + 'tm_year': time_struct.tm_year, \ + 'tm_mon' : time_struct.tm_mon, \ + 'tm_mday': time_struct.tm_mday, \ + 'tm_hour': time_struct.tm_hour, \ + 'tm_min' : time_struct.tm_min, \ + 'tm_sec' : time_struct.tm_sec, \ + 'tm_msec': tm_msec \ + } + return __format_time_utc__(time_dict) + +def __format_list_as_struct__(list): + out = '{' + if len(list) % 2 == 1: + list = list[:-1] + for i in range(0, len(list), 2): + if i > 0: + out = out + ', ' + out = out + json.dumps(list[i]) + ':' + json.dumps(list[i+1]) + out = out + '}' + return out + +def __make_log_entry__(*args): + ip = socket.gethostbyname(socket.gethostname()) + return __format_list_as_struct__(['date_time_utc', __current_time_utc__(), 'epoch_time', time.time(), \ + 'host_ip', ip, 'program', sys.argv[0], 'pid', os.getpid(), 'uid', os.getuid(), 'gid', os.getgid()] \ + + list(args)) + +def log(*args): + log_file = __log_file__ + if log_file == None: + log_file = sys.stderr + log_entry = __make_log_entry__(*args) + log_file.write(log_entry + '\n') + log_file.flush() + +def print_or_log_usage(usage): + if os.isatty(1): + print usage + else: + log('type', 'USAGE_ERROR', 'usage', usage) + +def log_info_message(message, *args): + #SentryClient().create_from_text(message, level = logging.INFO) + log('type', 'INFO_MESSAGE', 'message', message, *args) + +def log_error_message(message, *args): + #SentryClient().create_from_text(message, level = logging.ERROR) + log('type', 'ERROR_MESSAGE', 'message', message, *args) + +def log_warning_message(message, *args): + #SentryClient().create_from_text(message, level = logging.WARN) + log('type', 'WARNING_MESSAGE', 'message', message, *args) + +def log_external_program(external_program_args, result, *args): + #SentryClient().create_from_text("EXTERNAL PROGRAM ERROR: %s" % external_program_args, level = logging.ERROR) + log('type', 'EXTERNAL_PROGRAM_ERROR', 'external_program_args', external_program_args, \ + 'external_program_pid', result['pid'], 'exit_code', result['exit_code'], \ + 'stdout', result['stdout_contents'], 'stderr', result['stderr_contents'], *args) + +def log_external_program_log_stderr(external_program_args, result, *args): + log('type', 'BEGIN_EXTERNAL_PROGRAM_LOG', 'external_program_args', external_program_args, \ + 'external_program_pid', result['pid'], 'exit_code', result['exit_code'], \ + 'stdout', result['stdout_contents'], 'stderr_follows_in_log', True, *args) + for message in result['stderr_contents'].split('\n'): + log(message) + log('type', 'END_EXTERNAL_PROGRAM_LOG', 'external_program_pid', result['pid']) + +def __log_exception__(exception, *args): + log('type', 'EXCEPTION', 'class', exception.__class__.__name__, 'message', str(exception), *args) + +def log_last_exception(*args): + #SentryClient().create_from_exception() + (_, exception, _) = sys.exc_info() + __log_exception__(exception, 'traceback', traceback.format_exc(), *args) diff --git a/src/server/shared/djangy_server_shared/resource_allocation.py b/src/server/shared/djangy_server_shared/resource_allocation.py new file mode 100644 index 0000000..6a6f36b --- /dev/null +++ b/src/server/shared/djangy_server_shared/resource_allocation.py @@ -0,0 +1,35 @@ +class ResourceAllocation(object): + def __init__(self, num_procs, proc_num_threads, proc_mem_mb, proc_stack_mb, debug, http_virtual_hosts, host, port, celery_procs): + self.num_procs = num_procs + self.proc_num_threads = proc_num_threads + self.proc_mem_mb = proc_mem_mb + self.proc_stack_mb = proc_stack_mb + self.debug = debug + self.http_virtual_hosts = http_virtual_hosts + self.host = host + self.port = port + self.celery_procs = celery_procs + + def to_command_line(self): + return ['num_procs', str(self.num_procs), \ + 'proc_num_threads', str(self.proc_num_threads), \ + 'proc_mem_mb', str(self.proc_mem_mb), \ + 'proc_stack_mb', str(self.proc_stack_mb), \ + 'debug', str(self.debug), \ + 'http_virtual_hosts', ','.join(http_virtual_hosts), \ + 'host', self.host, \ + 'port', str(self.port), \ + 'celery_procs', str(self.celery_procs)] + + @staticmethod + def from_command_line_dict(dict): + return ResourceAllocation( + num_procs = int(dict['num_procs']), + proc_num_threads = int(dict['proc_num_threads']), + proc_mem_mb = int(dict['proc_mem_mb']), + proc_stack_mb = int(dict['proc_stack_mb']), + debug = (dict['debug'] == 'True'), + http_virtual_hosts = dict['http_virtual_hosts'].split(','), + host = dict['host'], + port = int(dict['port']), + celery_procs = int(dict['celery_procs'])) diff --git a/src/server/shared/djangy_server_shared/run_external_program.py b/src/server/shared/djangy_server_shared/run_external_program.py new file mode 100644 index 0000000..cfa5baf --- /dev/null +++ b/src/server/shared/djangy_server_shared/run_external_program.py @@ -0,0 +1,106 @@ +import os, subprocess, sys, tempfile +from json_log import * + +class RunExternalProgramException(Exception): + """Exception trying to run external program.""" + def __init__(self, args, cause_exception): + self.args = args + self.cause_exception = cause_exception + def __str__(self): + return 'Exception %s trying to run external program %s.' % (str(self.cause_exception), str(self.args)) + +def external_program_encountered_error(result): + return result['exit_code'] != 0 # or len(result['stderr_contents']) > 0 + +class ExternalProgram(object): + def __init__(self, args, cwd=None, preexec_fn=None, pass_stdin=False, pass_stdout=False, pass_stderr=False, stdin_contents=None, stderr_to_stdout=False, log_stderr=False): + self._args = args + self._cwd = cwd + self._preexec_fn = preexec_fn + self._pass_stdin = pass_stdin + self._pass_stdout = pass_stdout + self._pass_stderr = pass_stderr + self._stdin_contents = stdin_contents + self._stderr_to_stdout = stderr_to_stdout + self._log_stderr = log_stderr + self._has_started = False + self._has_finished = False + def run(self): + self.start() + return self.finish() + def start(self): + if self._has_started: + return + self._has_started = True + try: + # Flush output if necessary + if self._pass_stdout: + sys.stdout.flush() + if self._pass_stderr: + sys.stderr.flush() + # Create temporary files to redirect stdin/stdout/stderr + temp_stdin = tempfile.NamedTemporaryFile() + self._temp_stdout = tempfile.NamedTemporaryFile() + self._temp_stderr = tempfile.NamedTemporaryFile() + # After fork(), we will run this to redirect stdin/stdout/stderr + def run_process_preexec_fn(): + if not self._pass_stdin and self._stdin_contents == None: + os.dup2(os.open(temp_stdin.name, os.O_RDONLY), 0) + if not self._pass_stdout: + os.dup2(os.open(self._temp_stdout.name, os.O_WRONLY), 1) + if not self._pass_stderr: + if self._stderr_to_stdout: + os.dup2(1, 2) + else: + os.dup2(os.open(self._temp_stderr.name, os.O_WRONLY), 2) + if self._preexec_fn != None: + self._preexec_fn() + # Start the subprocess--calls above function. + self._stdin_flag = None + if self._stdin_contents != None: + self._stdin_flag = subprocess.PIPE + self._process = subprocess.Popen(self._args, preexec_fn=run_process_preexec_fn, \ + executable=self._args[0], close_fds=True, shell=False, cwd=self._cwd, stdin=self._stdin_flag) + if self._stdin_contents != None: + self._process.stdin.write(self._stdin_contents) + self._process.stdin.close() + except Exception as e: + # Couldn't run external program. Log the exception. + log_last_exception('external_program_args', self._args) + raise RunExternalProgramException(self._args, e) + + def finish(self): + if self._has_finished or not self._has_started: + return None + self._has_finished = True + try: + # Run the subprocess to completion + pid = self._process.pid + exit_code = self._process.wait() + # Read out stdout/stderr + stdout_contents = self._temp_stdout.read() + stderr_contents = self._temp_stderr.read() + # Close stdout/stderr + self._temp_stdout.close() + self._temp_stderr.close() + # Return full results + result = { \ + 'pid' : pid, \ + 'exit_code' : exit_code, \ + 'stdout_contents': stdout_contents, \ + 'stderr_contents': stderr_contents \ + } + # (but first, log any error) + if external_program_encountered_error(result): + if self._log_stderr: + log_external_program_log_stderr(self._args, result) + else: + log_external_program(self._args, result) + return result + except Exception as e: + # Couldn't run external program. Log the exception. + log_last_exception('external_program_args', self._args) + raise RunExternalProgramException(self._args, e) + +def run_external_program(*args, **kwargs): + return ExternalProgram(*args, **kwargs).run() diff --git a/src/server/shared/setup.py b/src/server/shared/setup.py new file mode 100644 index 0000000..14ba5e3 --- /dev/null +++ b/src/server/shared/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup, find_packages + +setup( + name="djangy_server_shared", + version="0.1", + packages=find_packages(), + author="Sameer Sundresh", + author_email="sameer@djangy.com", + description="Djangy.com server shared code", + keywords="djangy django", + url="http://www.djangy.com", + license="University of Illinois/NCSA Open Source License" +) diff --git a/src/server/worker/worker_manager/__init__.py b/src/server/worker/worker_manager/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/server/worker/worker_manager/delete_application.py b/src/server/worker/worker_manager/delete_application.py new file mode 100755 index 0000000..c80cd63 --- /dev/null +++ b/src/server/worker/worker_manager/delete_application.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# +# Stops and removes application from LocalApplication table but doesn't +# remove bundles or logs. +# + +from shared import * + +def main(): + try: + check_trusted_uid(program_name = sys.argv[0]) + kwargs = check_and_return_keyword_args(sys.argv, ['application_name']) + + application_name = kwargs['application_name'] + + delete_application(application_name) + except: + log_last_exception() + +@lock_application +def delete_application(application_name): + stop_application(application_name) + application_info = LocalApplication.objects.get(application_name = application_name) + application_info.delete() + +if __name__ == '__main__': + main() diff --git a/src/server/worker/worker_manager/deploy.py b/src/server/worker/worker_manager/deploy.py new file mode 100755 index 0000000..d02aec7 --- /dev/null +++ b/src/server/worker/worker_manager/deploy.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python + +from shared import * +from mako.lookup import TemplateLookup +from django.core.exceptions import ObjectDoesNotExist +import re + +def main(): + try: + check_trusted_uid(sys.argv[0]) + kwargs = check_and_return_keyword_args(sys.argv, ['application_name', 'bundle_version', + 'num_procs', 'proc_num_threads', 'proc_mem_mb', 'proc_stack_mb', 'debug', 'http_virtual_hosts', + 'host', 'port', 'celery_procs']) + + application_name = kwargs['application_name'] + bundle_version = kwargs['bundle_version'] + resource_allocation = ResourceAllocation.from_command_line_dict(kwargs) + + set_application_allocation(application_name, resource_allocation) + deploy_application(application_name = application_name, + bundle_version = bundle_version, + resource_allocation = resource_allocation) + except: + log_last_exception() + +def set_application_allocation(application_name, resource_allocation): + # Try to find an existing application + try: + application_info = LocalApplication.objects.get(application_name = application_name) + # Uses manual locking rather than @lock_application because we have + # to gracefully handle the case of creating a new object. + lock = LocalApplicationLocks.lock(application_name) + except ObjectDoesNotExist: + # Or create a new one + # XXX race condition if two LocalApplications with the same application_name are created concurrently... + application_info = LocalApplication(application_name = application_name, is_stopped = False) + lock = None + + ra = resource_allocation + + # Set fields + application_info.num_procs = ra.num_procs + application_info.proc_num_threads = ra.proc_num_threads + application_info.proc_mem_mb = ra.proc_mem_mb + application_info.proc_stack_mb = ra.proc_stack_mb + application_info.port = ra.port + application_info.celery_procs = ra.celery_procs + + application_info.save() + + print lock + LocalApplicationLocks.unlock(lock) + +@lock_application +def deploy_application(application_name, bundle_version, resource_allocation): + # Note that because this function is wrapped in @_lock_application, + # it will raise an exception if called before set_application_allocation() + + # Compose bundle_name + bundle_name = application_name + '-' + bundle_version + + # Copy bundle + application_info = LocalApplication.objects.get(application_name=application_name) + old_bundle_version = application_info.bundle_version + bundle_info = copy_bundle_from_remote_host(application_name, bundle_version, old_bundle_version) + + # Stop old version of application + should_restart = stop_application(application_name) + # Either delete an application with zero processes or restart one with nonzero. + if resource_allocation.num_procs == 0 and resource_allocation.celery_procs == 0: + application_info.delete() + else: + try: + # Create configuration files from templates + create_config_files(application_name, bundle_version, bundle_name, bundle_info, resource_allocation) + # Update current bundle version in database + application_info.bundle_version = bundle_version + application_info.save() + # Create log files + create_log_files(bundle_name, bundle_info.web_uid, bundle_info.app_gid) + finally: + # Start new version of application + if should_restart: + start_application(application_name) + + # If it all worked, we can delete the old bundle now... + # For now, we just mark it by creating a file called 'old' + if old_bundle_version and old_bundle_version != bundle_version: + try: + make_file(os.path.join(BUNDLES_DEST_DIR, application_name + '-' + old_bundle_version, 'old'), 0600) + except: + # Old bundle doesn't exist + log_last_exception() + +# Note: we should make as much of this as possible run without root +# permissions. The tricky part is we need to know what permissions are +# needed, because the remote bundle's bundle_info.config file specifies +# its UIDs and GID... +def copy_bundle_from_remote_host(application_name, bundle_version, old_bundle_version): + bundle_name = application_name + '-' + bundle_version + local_bundle_path = os.path.join(BUNDLES_DEST_DIR, bundle_name) + remote_bundle_path = BUNDLES_SRC_HOST + ':' + os.path.join(BUNDLES_SRC_DIR, bundle_name) + #remote_bundle_path = os.path.join(BUNDLES_SRC_DIR, bundle_name) + if old_bundle_version != None: + old_bundle_name = application_name + '-' + old_bundle_version + local_old_bundle_path = os.path.join(BUNDLES_DEST_DIR, old_bundle_name) + else: + old_bundle_name = '' + local_old_bundle_path = '' + + # Check if bundle already exists + if os.path.exists(local_bundle_path): + # Assume the bundle is ok--but log a warning! + log_warning_message('Warning: bundle "%s" already exists' % bundle_name) + return BundleInfo.load_from_file(os.path.join(local_bundle_path, 'config', 'bundle_info.config')) + + # Copy bundle: + # 1. Create a temporary directory to copy the new bundle into + local_temp_bundle_path = tempfile.mkdtemp(dir=BUNDLES_DEST_DIR, prefix='tmp-' + bundle_name + '-', suffix='.download') + + # 2. Make a hard link "copy" of the existing bundle (speeds up rsync) + if local_old_bundle_path != None and local_old_bundle_path != '': + sys.stdout.flush() + result = run_external_program(['cp', '-alT', local_old_bundle_path, local_temp_bundle_path]) + if external_program_encountered_error(result): + log_error_message('Error copying old bundle "%s" as baseline for new bundle "%s"' % (old_bundle_name, bundle_name)) + + # 3. Use rsync to copy over the changes. Trailing / on remote_bundle_path is critical. + sys.stdout.flush() + result = run_external_program(['rsync', '-a', '--delete', remote_bundle_path + '/', local_temp_bundle_path]) + if external_program_encountered_error(result): + log_error_message('Error downloading bundle "%s"' % bundle_name) + + # 4. Change ownership from bundles to setup_uid:app_gid + bundle_info = BundleInfo.load_from_file(os.path.join(local_temp_bundle_path, 'config', 'bundle_info.config')) + recursive_chown_chmod(local_temp_bundle_path, 0, bundle_info.app_gid, '0750') + + # 5. Rename temporary directory to final bundle directory + try: + os.rename(local_temp_bundle_path, local_bundle_path) + except OSError as e: + # This might happen if another process created the bundle while we + # were busy. Assume the bundle is ok--but log a warning! + log_last_exception('custom_message', 'Warning: error copying downloaded bundle "%s"' % bundle_name) + # TODO: it would be nice if we could mark the above as a warning. + + return bundle_info + +def create_config_files(application_name, bundle_version, bundle_name, bundle_info, resource_allocation): + bi = bundle_info + ra = resource_allocation + + # Compute bundle path + bundle_path = os.path.join(BUNDLES_DEST_DIR, bundle_name) + config_path = os.path.join(bundle_path, 'config') + + (django_project_parent_path, django_project_module_name) = os.path.split(bi.django_project_path) + + # Remove old config files (if any) + remove_no_exception(os.path.join(config_path, 'gunicorn.conf')) + remove_no_exception(os.path.join(config_path, 'runnable.py')) + remove_no_exception(os.path.join(bi.django_project_path, 'settings.py')) + remove_no_exception(os.path.join(bi.django_project_path, 'settings/__init__.py')) + + os.umask(0227) + + # Create production settings.py file in /application/.../settings.py + # (code also exists in master_manager.deploy) + print 'Creating production settings.py file...', + if os.path.isdir(os.path.join(bi.django_project_path, 'settings')): + settings_path = os.path.join(bi.django_project_path, 'settings', '__init__.py') + else: + settings_path = os.path.join(bi.django_project_path, 'settings.py') + generate_config_file('generic_settings', settings_path, + user_settings_module_name = bi.user_settings_module_name, + db_host = bi.db_host, + db_port = bi.db_port, + db_name = bi.db_name, + db_username = bi.db_username, + db_password = bi.db_password, + bundle_name = bundle_name, + application_name = application_name, + celery_procs = ra.celery_procs, + debug = ra.debug) + os.chown(settings_path, 0, bi.app_gid) + os.chmod(settings_path, 0750) + print 'Done.' + print '' + + # Create Django WSGI file in /runnable_.py + print 'Generating django wsgi script for your project...', + django_wsgi_path = os.path.join(config_path, 'runnable_%s.py' % bundle_version) + if is_nonempty_file(os.path.join(bi.django_project_path, '__init__.py')): + settings_module = '%s.settings' % os.path.basename(bi.django_project_path) + else: + settings_module = 'settings' + # XXX - Try reenabling RLIMIT_NOFILE in generic_django_wsgi + generate_config_file('generic_django_wsgi', django_wsgi_path, \ + django_project_path = escape(bi.django_project_path), \ + django_project_parent_path = escape(django_project_parent_path), \ + application_name = application_name, \ + bundle_name = bundle_name, \ + rlimit_data = str(ra.proc_mem_mb - ra.proc_stack_mb), \ + rlimit_stack = str(ra.proc_stack_mb), \ + rlimit_rss = str(ra.proc_mem_mb), \ + rlimit_nproc = str(ra.proc_num_threads * ra.num_procs), \ + settings_module = settings_module) + os.chown(django_wsgi_path, 0, bi.app_gid) + os.chmod(django_wsgi_path, 0750) + print 'Done.' + print '' + + # Create Gunicorn config file in /config/gunicorn.conf + print 'Generating gunicorn configuration file...', + gunicorn_conf_path = os.path.join(config_path, 'gunicorn.conf') + generate_config_file('generic_gunicorn_conf', gunicorn_conf_path, + application_name = application_name, \ + bundle_name = bundle_name, \ + web_uid = str(bi.web_uid), \ + app_gid = str(bi.app_gid), \ + num_processes = str(ra.num_procs), \ + host = ra.host, \ + port = ra.port) + os.chown(gunicorn_conf_path, 0, bi.app_gid) + os.chmod(gunicorn_conf_path, 0750) + print 'Done.' + print '' + +def is_nonempty_file(path): + if not os.path.isfile(path): + return False + return os.stat(path).st_size > 0 + +def escape(text): + text1 = re.sub('(\'|\"|\\\\)', '\\\\\\1', text) + text2 = re.sub('\n', '\\\\n', text1) + return text2 + +def remove_no_exception(path): + try: + os.remove(path) + except: + pass + +### Copied from master_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 relink(src_path, link_path): + """Equivalent to ln -sf: create a symbolic link, overriding any existing + target link or file.""" + try: + os.remove(link_path) + except OSError as e: + # Old link didn't exist, not a problem. + pass + os.symlink(src_path, link_path) + +def create_log_files(bundle_name, web_uid, app_gid): + """Create the /srv/logs/ directory and initially empty + django and web server log files.""" + logdir_path = os.path.join(LOGS_DIR, bundle_name) + try: + os.mkdir(logdir_path, 0770) + os.chown(logdir_path, 0, app_gid) + os.chmod(logdir_path, 0770) + except OSError as e: + # Should log this--ok if it's just because the file exists. + if not os.path.exists(logdir_path): + log_last_exception('custom_message', 'Error: could not create log directory "%s"' % logdir_path) + return + for log_name in LOGS: + logfile_path = os.path.join(logdir_path, log_name) + try: + make_file(logfile_path, 0660) + os.chown(logfile_path, web_uid, app_gid) + os.chmod(logfile_path, 0660) + except OSError as e: + log_last_exception('custom_message', 'Error: could not create log file "%s"' % logfile_path) + +def make_file(path, mode): + os.close(os.open(path, os.O_CREAT, mode)) + +if __name__ == '__main__': + main() diff --git a/src/server/worker/worker_manager/orm/__init__.py b/src/server/worker/worker_manager/orm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/server/worker/worker_manager/orm/manage.py b/src/server/worker/worker_manager/orm/manage.py new file mode 100755 index 0000000..6ce754c --- /dev/null +++ b/src/server/worker/worker_manager/orm/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/worker/worker_manager/orm/migrations/0001_initial.py b/src/server/worker/worker_manager/orm/migrations/0001_initial.py new file mode 100644 index 0000000..f3f703e --- /dev/null +++ b/src/server/worker/worker_manager/orm/migrations/0001_initial.py @@ -0,0 +1,64 @@ +# 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 'LocalApplication' + db.create_table('local_application', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('application_name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)), + ('bundle_version', self.gf('django.db.models.fields.CharField')(max_length=255, null=True)), + ('is_stopped', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('num_procs', self.gf('django.db.models.fields.IntegerField')()), + ('proc_num_threads', self.gf('django.db.models.fields.IntegerField')()), + ('proc_mem_mb', self.gf('django.db.models.fields.IntegerField')()), + ('proc_stack_mb', self.gf('django.db.models.fields.IntegerField')()), + )) + db.send_create_signal('orm', ['LocalApplication']) + + # Adding model 'LocalApplicationLocks' + db.create_table('local_application_locks', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('application', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['orm.LocalApplication'], unique=True)), + ('pid', self.gf('django.db.models.fields.IntegerField')()), + ('time', self.gf('django.db.models.fields.DateField')(auto_now_add=True, blank=True)), + )) + db.send_create_signal('orm', ['LocalApplicationLocks']) + + + def backwards(self, orm): + + # Deleting model 'LocalApplication' + db.delete_table('local_application') + + # Deleting model 'LocalApplicationLocks' + db.delete_table('local_application_locks') + + + models = { + 'orm.localapplication': { + 'Meta': {'object_name': 'LocalApplication', 'db_table': "'local_application'"}, + 'application_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'bundle_version': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_stopped': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'num_procs': ('django.db.models.fields.IntegerField', [], {}), + 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {}), + 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {}), + 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {}) + }, + 'orm.localapplicationlocks': { + 'Meta': {'object_name': 'LocalApplicationLocks', 'db_table': "'local_application_locks'"}, + 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['orm.LocalApplication']", 'unique': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pid': ('django.db.models.fields.IntegerField', [], {}), + 'time': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}) + } + } + + complete_apps = ['orm'] diff --git a/src/server/worker/worker_manager/orm/migrations/0002_add_celery_procs.py b/src/server/worker/worker_manager/orm/migrations/0002_add_celery_procs.py new file mode 100644 index 0000000..c954622 --- /dev/null +++ b/src/server/worker/worker_manager/orm/migrations/0002_add_celery_procs.py @@ -0,0 +1,43 @@ +# 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 'LocalApplication.celery_procs' + db.add_column('local_application', 'celery_procs', self.gf('django.db.models.fields.IntegerField')(default=0), keep_default=False) + + + def backwards(self, orm): + + # Deleting field 'LocalApplication.celery_procs' + db.delete_column('local_application', 'celery_procs') + + + models = { + 'orm.localapplication': { + 'Meta': {'object_name': 'LocalApplication', 'db_table': "'local_application'"}, + 'application_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'bundle_version': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'celery_procs': ('django.db.models.fields.IntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_stopped': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'num_procs': ('django.db.models.fields.IntegerField', [], {}), + 'proc_mem_mb': ('django.db.models.fields.IntegerField', [], {}), + 'proc_num_threads': ('django.db.models.fields.IntegerField', [], {}), + 'proc_stack_mb': ('django.db.models.fields.IntegerField', [], {}) + }, + 'orm.localapplicationlocks': { + 'Meta': {'object_name': 'LocalApplicationLocks', 'db_table': "'local_application_locks'"}, + 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['orm.LocalApplication']", 'unique': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pid': ('django.db.models.fields.IntegerField', [], {}), + 'time': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}) + } + } + + complete_apps = ['orm'] diff --git a/src/server/worker/worker_manager/orm/migrations/__init__.py b/src/server/worker/worker_manager/orm/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/server/worker/worker_manager/orm/models.py b/src/server/worker/worker_manager/orm/models.py new file mode 100644 index 0000000..53f3a6f --- /dev/null +++ b/src/server/worker/worker_manager/orm/models.py @@ -0,0 +1,62 @@ +import os +from django.db import models + +class LocalApplication(models.Model): + + class Meta: + db_table = 'local_application' + + application_name = models.CharField(max_length=255, unique=True) + bundle_version = models.CharField(max_length=255, null=True) + is_stopped = models.BooleanField(default=False) + num_procs = models.IntegerField() + proc_num_threads = models.IntegerField() + proc_mem_mb = models.IntegerField() + proc_stack_mb = models.IntegerField() + celery_procs = models.IntegerField() + +class LocalApplicationLocks(models.Model): + + class Meta: + db_table = 'local_application_locks' + + application = models.ForeignKey(LocalApplication, unique=True) + # Diagnostic information about the lock in case it is not properly + # unlocked and we need to track down the problem. + pid = models.IntegerField() + time = models.DateField(auto_now_add=True) + + @staticmethod + def lock(application_name): + """Locks an application. Throws an exception if it is already locked.""" + try: + application = LocalApplication.objects.get(application_name=application_name) + lock = LocalApplicationLocks(application=application, pid=os.getpid()) + lock.save() + return lock + except Exception as e: + raise LockFailedException(application_name) + + @staticmethod + def unlock(lock): + """Unlocks an application. The argument must be the return value of + a previous call to lock() which has not already been unlocked.""" + try: + if lock != None: + lock.delete() + except Exception as e: + raise UnlockFailedException(lock.application.application_name) + +class LockFailedException(Exception): + """Could not lock the application.""" + def __init__(self, application_name): + self.application_name = application_name + def __str__(self): + return 'Could not lock the application "%s".' % self.application_name + +class UnlockFailedException(Exception): + """Could not unlock the application.""" + def __init__(self, application_name): + self.application_name = application_name + def __str__(self): + return 'Could not unlock the application "%s".' % self.application_name diff --git a/src/server/worker/worker_manager/orm/settings.py b/src/server/worker/worker_manager/orm/settings.py new file mode 100644 index 0000000..bcff153 --- /dev/null +++ b/src/server/worker/worker_manager/orm/settings.py @@ -0,0 +1,17 @@ +import djangy_server_shared, os.path + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(djangy_server_shared.WORKER_MANAGER_VAR_DIR, 'worker_manager.db'), + 'USER': '', + 'PASSWORD': '', + 'HOST': '', + 'PORT': '' + } +} + +INSTALLED_APPS = ( + 'orm', + 'south', +) diff --git a/src/server/worker/worker_manager/purge_old_bundles.py b/src/server/worker/worker_manager/purge_old_bundles.py new file mode 100644 index 0000000..b52e1c0 --- /dev/null +++ b/src/server/worker/worker_manager/purge_old_bundles.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +# +# Utility script to remove old, unused bundles from a worker_manager host. +# + +import os, shutil +from shared import * + +BUNDLES_ROOT = '/srv/bundles'; + +def main(): + current_bundle_names = set([x.application_name + '-' + x.bundle_version for x in LocalApplication.objects.all()]) + 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/worker/worker_manager/purge_old_logs.py b/src/server/worker/worker_manager/purge_old_logs.py new file mode 100644 index 0000000..f678247 --- /dev/null +++ b/src/server/worker/worker_manager/purge_old_logs.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# +# Utility script to remove old, unused bundles from a worker_manager host. +# + +import os, shutil +from shared import * + +LOGS_ROOT = '/srv/logs'; + +def main(): + current_bundle_names = set([x.application_name + '-' + x.bundle_version for x in LocalApplication.objects.all()]) + for log_name in os.listdir(LOGS_ROOT): + log_path = os.path.join(LOGS_ROOT, log_name) + if log_name.find('-v1g') >= 0 and os.path.isdir(log_path) and log_name not in current_bundle_names: + print 'Removing %s ...' % log_name + shutil.rmtree(log_path) + +if __name__ == '__main__': + main() diff --git a/src/server/worker/worker_manager/retrieve_logs.py b/src/server/worker/worker_manager/retrieve_logs.py new file mode 100755 index 0000000..894d087 --- /dev/null +++ b/src/server/worker/worker_manager/retrieve_logs.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python + +from shared import * +from mako.template import Template +from mako.lookup import TemplateLookup +import os + +def main(): + try: + check_trusted_uid(sys.argv[0]) + kwargs = check_and_return_keyword_args(sys.argv, ['application_name', 'bundle_version']) + + application_name = kwargs['application_name'] + bundle_version = kwargs['bundle_version'] + retrieve_logs(application_name, bundle_version) + except: + log_last_exception() + +def retrieve_logs(application_name, bundle_version): + bundle_name = application_name + '-' + bundle_version + django_log_path = os.path.join(LOGS_DIR, bundle_name, "django.log") + error_log_path = os.path.join(LOGS_DIR, bundle_name, "error.log") + + django_log = open(django_log_path).read() + error_log = open(error_log_path).read() + + lookup = TemplateLookup(directories = [WORKER_TEMPLATE_DIR]) + template = lookup.get_template('logs.txt') + instance = template.render( + django_log = django_log, + error_log = error_log + ) + print instance + +if __name__ == '__main__': + main() diff --git a/src/server/worker/worker_manager/setuid/.gitignore b/src/server/worker/worker_manager/setuid/.gitignore new file mode 100644 index 0000000..81af2bf --- /dev/null +++ b/src/server/worker/worker_manager/setuid/.gitignore @@ -0,0 +1,6 @@ +run_deploy +run_soft_remove +run_retrieve_logs +run_soft_remove_application +run_start +run_stop diff --git a/src/server/worker/worker_manager/setuid/Makefile b/src/server/worker/worker_manager/setuid/Makefile new file mode 100644 index 0000000..9b6c292 --- /dev/null +++ b/src/server/worker/worker_manager/setuid/Makefile @@ -0,0 +1,8 @@ +TARGETS=run_delete_application run_deploy run_start run_stop run_retrieve_logs + +all: $(TARGETS) + -chown root.djangy $(TARGETS) + chmod 6710 $(TARGETS) + +clean: + rm -f $(TARGETS) *~ diff --git a/src/server/worker/worker_manager/setuid/config.h b/src/server/worker/worker_manager/setuid/config.h new file mode 100644 index 0000000..19fb00c --- /dev/null +++ b/src/server/worker/worker_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/sbin:/usr/bin:/sbin:/bin" +#define VIRTUAL_ENV "/srv/djangy/run/python-virtual" +#define PROGRAM_DIR "/srv/djangy/src/server/worker/worker_manager/" // trailing slash is important diff --git a/src/server/worker/worker_manager/setuid/run.h b/src/server/worker/worker_manager/setuid/run.h new file mode 100644 index 0000000..0b890c5 --- /dev/null +++ b/src/server/worker/worker_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/worker/worker_manager/setuid/run_delete_application.c b/src/server/worker/worker_manager/setuid/run_delete_application.c new file mode 100644 index 0000000..0cd616b --- /dev/null +++ b/src/server/worker/worker_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/worker/worker_manager/setuid/run_deploy.c b/src/server/worker/worker_manager/setuid/run_deploy.c new file mode 100644 index 0000000..0c4897f --- /dev/null +++ b/src/server/worker/worker_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/worker/worker_manager/setuid/run_retrieve_logs.c b/src/server/worker/worker_manager/setuid/run_retrieve_logs.c new file mode 100644 index 0000000..a86d0d0 --- /dev/null +++ b/src/server/worker/worker_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/worker/worker_manager/setuid/run_start.c b/src/server/worker/worker_manager/setuid/run_start.c new file mode 100644 index 0000000..9cbd3c3 --- /dev/null +++ b/src/server/worker/worker_manager/setuid/run_start.c @@ -0,0 +1,11 @@ +// +// Run start. Must be setuid root. +// +#include "run.h" + +MAIN("start.py") + +int is_trusted_user(uid_t uid) +{ + return (uid == ROOT_UID); +} diff --git a/src/server/worker/worker_manager/setuid/run_stop.c b/src/server/worker/worker_manager/setuid/run_stop.c new file mode 100644 index 0000000..149dee4 --- /dev/null +++ b/src/server/worker/worker_manager/setuid/run_stop.c @@ -0,0 +1,11 @@ +// +// Run stop. Must be setuid root. +// +#include "run.h" + +MAIN("stop.py") + +int is_trusted_user(uid_t uid) +{ + return (uid == ROOT_UID); +} diff --git a/src/server/worker/worker_manager/shared/__init__.py b/src/server/worker/worker_manager/shared/__init__.py new file mode 100644 index 0000000..e821f8c --- /dev/null +++ b/src/server/worker/worker_manager/shared/__init__.py @@ -0,0 +1,10 @@ +import grp, pwd, os, subprocess, sys, tempfile +os.environ['DJANGO_SETTINGS_MODULE'] = 'orm.settings' +from orm.models import * +from djangy_server_shared import * +from lock_application import * +from start_stop import * + +TRUSTED_UIDS.extend(WORKER_TRUSTED_UIDS) + +open_log_file(os.path.join(LOGS_DIR, 'worker.log'), 0600) diff --git a/src/server/worker/worker_manager/shared/lock_application.py b/src/server/worker/worker_manager/shared/lock_application.py new file mode 100644 index 0000000..e98b353 --- /dev/null +++ b/src/server/worker/worker_manager/shared/lock_application.py @@ -0,0 +1,12 @@ +import os +os.environ['DJANGO_SETTINGS_MODULE'] = 'orm.settings' +from orm.models import * + +def lock_application(func): + def lock_and_call(application_name, *args, **kwargs): + lock = LocalApplicationLocks.lock(application_name) + try: + func(application_name, *args, **kwargs) + finally: + LocalApplicationLocks.unlock(lock) + return lock_and_call diff --git a/src/server/worker/worker_manager/shared/start_stop.py b/src/server/worker/worker_manager/shared/start_stop.py new file mode 100644 index 0000000..276b70a --- /dev/null +++ b/src/server/worker/worker_manager/shared/start_stop.py @@ -0,0 +1,81 @@ +from shared import * +from socket import gethostname +import os, sys, pwd + +def start_application(application_name): + # Set the started status in the local database + application_info = LocalApplication.objects.get(application_name=application_name) + application_info.is_stopped = False + application_info.save() + # Start the application in gunicorn + bundle_version = application_info.bundle_version + bundle_name = application_name + "-" + bundle_version + bundle_path = os.path.join('/srv/bundles', bundle_name) + config_path = os.path.join(bundle_path, 'config') + virtualenv_path = os.path.join(bundle_path, 'python-virtual') + bundle_info = BundleInfo.load_from_file(os.path.join(config_path, 'bundle_info.config')) + def become_web_uid(): + os.environ.clear() + os.environ['PATH'] = '%s:/usr/bin:/bin' % os.path.join(virtualenv_path, 'bin') + os.environ['VIRTUAL_ENV'] = virtualenv_path + os.chdir(config_path) + os.setgid(bundle_info.app_gid) + os.setuid(bundle_info.web_uid) + def become_cron_uid(): + os.environ.clear() + os.environ['PATH'] = '%s:/usr/bin:/bin' % os.path.join(virtualenv_path, 'bin') + os.environ['VIRTUAL_ENV'] = virtualenv_path + os.chdir(bundle_info.django_project_path) + os.setgid(bundle_info.app_gid) + os.setuid(bundle_info.cron_uid) + # Start gunicorn process + sys.stdout.flush() + run_external_program(['gunicorn', '-c', os.path.join(config_path, 'gunicorn.conf'), 'runnable_%s:application' % bundle_version], \ + cwd = config_path, \ + preexec_fn = become_web_uid) + try: + celery_procs = int(application_info.celery_procs) + # only start celery if there is more than one process being allocated + assert celery_procs > 0 + except: + return + pid = os.fork() + if pid == 0: + # child process + os.closerange(1, 1024) + become_cron_uid() + hostname = "%s.%s" % (application_name, gethostname()) + os.execvp('python', [ + 'python', + os.path.join(bundle_info.django_project_path, 'manage.py'), + 'celeryd', + '-n', hostname, + ]) + + +def stop_application(application_name): + # Set the stopped status in the local database + application_info = LocalApplication.objects.get(application_name=application_name) + if application_info.is_stopped: + return False + application_info.is_stopped = True + application_info.save() + if not application_info.bundle_version: + # There is no current running version + return True + try: + bundle_name = application_name + "-" + application_info.bundle_version + bundle_path = os.path.join('/srv/bundles', bundle_name) + config_path = os.path.join(bundle_path, 'config') + bundle_info = BundleInfo.load_from_file(os.path.join(config_path, 'bundle_info.config')) + # Stop the running gunicorn process + web_user = pwd.getpwuid(int(bundle_info.web_uid)).pw_name + cron_user = pwd.getpwuid(int(bundle_info.cron_uid)).pw_name + # send SIGKILL + sys.stdout.flush() + run_external_program(['killall', '-s', 'SIGKILL', '-u', str(web_user)]) + run_external_program(['killall', '-s', 'SIGKILL', '-u', str(cron_user)]) + except: + # Database had a stale entry + log_last_exception() + return True diff --git a/src/server/worker/worker_manager/start.py b/src/server/worker/worker_manager/start.py new file mode 100755 index 0000000..450c8b6 --- /dev/null +++ b/src/server/worker/worker_manager/start.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +from shared import * + +def main(): + check_trusted_uid(sys.argv[0]) + kwargs = check_and_return_keyword_args(sys.argv, ['application_name']) + start(**kwargs) + +@lock_application +def start(application_name): + return start_application(application_name) + +if __name__ == '__main__': + main() diff --git a/src/server/worker/worker_manager/stop.py b/src/server/worker/worker_manager/stop.py new file mode 100755 index 0000000..744bb02 --- /dev/null +++ b/src/server/worker/worker_manager/stop.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +from shared import * + +def main(): + check_trusted_uid(sys.argv[0]) + kwargs = check_and_return_keyword_args(sys.argv, ['application_name']) + stop(**kwargs) + +@lock_application +def stop(application_name): + return stop_application(application_name) + +if __name__ == '__main__': + main() diff --git a/src/server/worker/worker_manager/templates/generic_django_wsgi b/src/server/worker/worker_manager/templates/generic_django_wsgi new file mode 100644 index 0000000..08bc171 --- /dev/null +++ b/src/server/worker/worker_manager/templates/generic_django_wsgi @@ -0,0 +1,26 @@ +import resource + +def dup(x): + return (x, x) + +resource.setrlimit(resource.RLIMIT_CORE, dup(0)) +resource.setrlimit(resource.RLIMIT_DATA, dup(${rlimit_data}*1024*1024)) +resource.setrlimit(resource.RLIMIT_STACK, dup(${rlimit_stack}*1024*1024)) +resource.setrlimit(resource.RLIMIT_RSS, dup(${rlimit_rss}*1024*1024)) +resource.setrlimit(resource.RLIMIT_NPROC, dup(${rlimit_nproc})) +#resource.setrlimit(resource.RLIMIT_NOFILE, dup(64)) +resource.setrlimit(resource.RLIMIT_MEMLOCK, dup(0)) + +import site +site.addsitedir("/srv/bundles/${ bundle_name }/python-virtual/lib/python2.6/site-packages") + +import os, sys + +sys.path.append('${ django_project_parent_path }') +sys.path.append('${ django_project_path }') +os.chdir('${ django_project_path }') + +os.environ['DJANGO_SETTINGS_MODULE'] = '${ settings_module }' + +import django.core.handlers.wsgi +application = django.core.handlers.wsgi.WSGIHandler() diff --git a/src/server/worker/worker_manager/templates/generic_gunicorn_conf b/src/server/worker/worker_manager/templates/generic_gunicorn_conf new file mode 100644 index 0000000..95a1a72 --- /dev/null +++ b/src/server/worker/worker_manager/templates/generic_gunicorn_conf @@ -0,0 +1,8 @@ +bind = "${ host }:${ port }" +workers = ${ num_processes } +daemon = True +user = "${ web_uid }" +group = "${ app_gid }" +umask = "0077" +proc_name = "${ application_name }" +logfile = "/srv/logs/${ bundle_name }/error.log" diff --git a/src/server/worker/worker_manager/templates/generic_settings b/src/server/worker/worker_manager/templates/generic_settings new file mode 100644 index 0000000..b27cf0c --- /dev/null +++ b/src/server/worker/worker_manager/templates/generic_settings @@ -0,0 +1,132 @@ +import django, django.http, django.utils.http, django.views.static, imp, logging, os.path, sys, time +import django.conf.urls.defaults as urls + +ADMIN_MEDIA_PREFIX = '/admin_media/' + +# Configure logging to go to our log file +LOG_FILENAME = "/srv/logs/${ bundle_name }/django.log" + +_old_logging_basicConfig = logging.basicConfig + +def _new_logging_basicConfig(**kwargs): + if kwargs.has_key('filename'): + kwargs['filename'] = LOG_FILENAME + return _old_logging_basicConfig(**kwargs) + +logging.basicConfig = _new_logging_basicConfig +logger = logging.getLogger("djangy") +glogger = logging.getLogger("gunicorn") +for handler in glogger.handlers: + logger.addHandler(handler) +logger.setLevel(logging.INFO) + +# Extend django.views.static.serve with extra validation and cache-control +# headers. Perhaps _new_static_serve should be defined in an egg? +_old_static_serve = django.views.static.serve + +def _new_static_serve(request, path, document_root=None, show_indexes=False): + # Only allow GET requests for static content + if not request.method == 'GET': + return django.http.HttpResponseNotAllowed(['GET']) + # The document_root must be within the application + abs_document_root = os.path.abspath(document_root) + if not \ + (abs_document_root.startswith('/srv/bundles/${ bundle_name }/application/') + or abs_document_root.startswith('/srv/bundles/${ bundle_name }/python-virtual/') + or abs_document_root.startswith('/usr/local/lib/python2.6/dist-packages/')): + return django.http.HttpResponseServerError('HTTP server error') + # The requested path must remain within the document_root + abs_path = os.path.abspath(os.path.join(abs_document_root, path)) + if abs_path == abs_document_root: + path = '' + elif abs_path.startswith(abs_document_root + '/'): + path = abs_path[len(abs_document_root + '/'):] + else: + return django.http.HttpResponseNotFound('HTTP not found') + # Create a sanitized HttpRequest object + request = django.http.HttpRequest() + request.method = 'GET' + request.path = path + request.path_info = path + # Pass off sanitized request to django.views.static.serve + response = _old_static_serve(request, path, document_root=document_root, show_indexes=show_indexes) + # Set Expires and Cache-Control headers (should check if it's already + # set?) + # We use: expires in 1 day, allow 10 minutes of staleness past that. + response['Expires'] = django.utils.http.http_date(time.time() + 24*60*60) + response['Cache-Control'] = 'max-stale=%i' % (10*60) + # Finally return the response. + return response + +django.views.static.serve = _new_static_serve + +# We don't yet support caching +CACHE_BACKEND = 'dummy://' + +DJANGO_ROOT = os.path.dirname(os.path.realpath(django.__file__)) + +DEBUG = ${ debug } +TEMPLATE_DEBUG = DEBUG + +_DATABASES = { + 'default' : { + 'ENGINE':'django.db.backends.mysql', + 'HOST':'${ db_host }', + 'PORT':'${ db_port }', + 'NAME':'${ db_name }', + 'USER':'${ db_username }', + 'PASSWORD':'${ db_password }' + } +} + + +# Save settings +_LOG_FILENAME = LOG_FILENAME +_CACHE_BACKEND = CACHE_BACKEND +_DJANGO_ROOT = DJANGO_ROOT +_DEBUG = DEBUG +_TEMPLATE_DEBUG = TEMPLATE_DEBUG + + +# Call user's settings.py +from ${ user_settings_module_name } import * + + +# Restore settings +LOG_FILENAME = _LOG_FILENAME +CACHE_BACKEND = _CACHE_BACKEND +DJANGO_ROOT = _DJANGO_ROOT +DEBUG = _DEBUG +TEMPLATE_DEBUG = _TEMPLATE_DEBUG +DATABASES = _DATABASES + +# celery configuration +CARROT_BACKEND = 'ghettoq.taproot.Database' +CELERYD_CONCURRENCY = ${ celery_procs } +CELERYD_LOG_FILE = "/srv/logs/${ bundle_name }/celery.log" +CELERYD_LOG_LEVEL = logging.INFO + +def _enable_admin_media_serving(): + dir_path = os.path.dirname(os.path.abspath(__file__)) + path = [dir_path, os.path.dirname(dir_path)] + sys.path + (file, pathname, description) = imp.find_module(ROOT_URLCONF.replace('.', '/'), path) + urls_module = imp.load_module(ROOT_URLCONF, file, pathname, description) + if file: + file.close() + if not urls_module.urlpatterns: + urls_module.urlpatterns = [] + prefix = ADMIN_MEDIA_PREFIX + if prefix.startswith('/'): + prefix = prefix[1:] + urls_module.urlpatterns.reverse() + urls_module.urlpatterns += urls.patterns('', + ('^%s(?P.*)$' % prefix, 'django.views.static.serve', {'document_root': '/usr/local/lib/python2.6/dist-packages/Django-1.2.1-py2.6.egg/django/contrib/admin/media/'})) + urls_module.urlpatterns.reverse() + +# Only run _enable_admin_media_serving() if we're serving a web app; don't +# do it if we're running manage.py, since loading ROOT_URLCONF doesn't work +# correctly in that context (some sort of path issue). +# Note: the uid check makes sure it's a web uid +if ((os.getuid() - 100000) % 3) == 1 \ +and os.environ.has_key('DJANGO_SETTINGS_MODULE'): + _enable_admin_media_serving() diff --git a/src/server/worker/worker_manager/templates/logs.txt b/src/server/worker/worker_manager/templates/logs.txt new file mode 100644 index 0000000..81f2b65 --- /dev/null +++ b/src/server/worker/worker_manager/templates/logs.txt @@ -0,0 +1,4 @@ +***** BEGIN DJANGY LOG OUTPUT ***** +${ error_log } + +***** END DJANGY LOG OUTPUT ***** diff --git a/test/data/testapp-v1/__init__.py b/test/data/testapp-v1/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/test/data/testapp-v1/main/__init__.py b/test/data/testapp-v1/main/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/test/data/testapp-v1/main/models.py b/test/data/testapp-v1/main/models.py new file mode 100755 index 0000000..71a8362 --- /dev/null +++ b/test/data/testapp-v1/main/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/test/data/testapp-v1/main/tests.py b/test/data/testapp-v1/main/tests.py new file mode 100755 index 0000000..2247054 --- /dev/null +++ b/test/data/testapp-v1/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/test/data/testapp-v1/main/views.py b/test/data/testapp-v1/main/views.py new file mode 100755 index 0000000..0a5e083 --- /dev/null +++ b/test/data/testapp-v1/main/views.py @@ -0,0 +1,5 @@ +import os +from django.http import HttpResponse + +def index(request): + return HttpResponse('testapp.main') diff --git a/test/data/testapp-v1/manage.py b/test/data/testapp-v1/manage.py new file mode 100755 index 0000000..5e78ea9 --- /dev/null +++ b/test/data/testapp-v1/manage.py @@ -0,0 +1,11 @@ +#!/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/test/data/testapp-v1/settings.py b/test/data/testapp-v1/settings.py new file mode 100755 index 0000000..0ccef24 --- /dev/null +++ b/test/data/testapp-v1/settings.py @@ -0,0 +1,94 @@ +# Django settings for testapp project. + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + # ('Your Name', 'your_email@domain.com'), +) + +MANAGERS = ADMINS + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': '', # Or path to database file if using sqlite3. + 'USER': '', # Not used with sqlite3. + 'PASSWORD': '', # 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. + } +} + +# 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 = '7=y^^6+_1&sqo*n=07pu@7(3=t&2rxv#-+4#ote0jo=a8f0jox' + +# 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 = 'testapp.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', + # Uncomment the next line to enable the admin: + # 'django.contrib.admin', +) diff --git a/test/data/testapp-v1/urls.py b/test/data/testapp-v1/urls.py new file mode 100755 index 0000000..0c57947 --- /dev/null +++ b/test/data/testapp-v1/urls.py @@ -0,0 +1,19 @@ +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'^', 'testapp.main.views.index'), + + # Example: + # (r'^testapp/', include('testapp.foo.urls')), + + # Uncomment the admin/doc line below and add 'django.contrib.admindocs' + # to INSTALLED_APPS to enable admin documentation: + # (r'^admin/doc/', include('django.contrib.admindocs.urls')), + + # Uncomment the next line to enable the admin: + # (r'^admin/', include(admin.site.urls)), +) diff --git a/test/data/testapp-v2/__init__.py b/test/data/testapp-v2/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/test/data/testapp-v2/main/__init__.py b/test/data/testapp-v2/main/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/test/data/testapp-v2/main/models.py b/test/data/testapp-v2/main/models.py new file mode 100755 index 0000000..71a8362 --- /dev/null +++ b/test/data/testapp-v2/main/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/test/data/testapp-v2/main/tests.py b/test/data/testapp-v2/main/tests.py new file mode 100755 index 0000000..2247054 --- /dev/null +++ b/test/data/testapp-v2/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/test/data/testapp-v2/main/views.py b/test/data/testapp-v2/main/views.py new file mode 100755 index 0000000..20700cc --- /dev/null +++ b/test/data/testapp-v2/main/views.py @@ -0,0 +1,5 @@ +import os +from django.http import HttpResponse + +def index(request): + return HttpResponse('testapp.main second edition') diff --git a/test/data/testapp-v2/manage.py b/test/data/testapp-v2/manage.py new file mode 100755 index 0000000..5e78ea9 --- /dev/null +++ b/test/data/testapp-v2/manage.py @@ -0,0 +1,11 @@ +#!/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/test/data/testapp-v2/settings.py b/test/data/testapp-v2/settings.py new file mode 100755 index 0000000..0ccef24 --- /dev/null +++ b/test/data/testapp-v2/settings.py @@ -0,0 +1,94 @@ +# Django settings for testapp project. + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + # ('Your Name', 'your_email@domain.com'), +) + +MANAGERS = ADMINS + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': '', # Or path to database file if using sqlite3. + 'USER': '', # Not used with sqlite3. + 'PASSWORD': '', # 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. + } +} + +# 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 = '7=y^^6+_1&sqo*n=07pu@7(3=t&2rxv#-+4#ote0jo=a8f0jox' + +# 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 = 'testapp.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', + # Uncomment the next line to enable the admin: + # 'django.contrib.admin', +) diff --git a/test/data/testapp-v2/site_media/index.html b/test/data/testapp-v2/site_media/index.html new file mode 100644 index 0000000..cda674a --- /dev/null +++ b/test/data/testapp-v2/site_media/index.html @@ -0,0 +1 @@ +this page should be cached. diff --git a/test/data/testapp-v2/urls.py b/test/data/testapp-v2/urls.py new file mode 100755 index 0000000..2ca89d6 --- /dev/null +++ b/test/data/testapp-v2/urls.py @@ -0,0 +1,20 @@ +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'^site_media/(?P.*)$', 'django.views.static.serve', {'document_root': 'site_media/'}), + (r'^', 'testapp.main.views.index') + + # Example: + # (r'^testapp/', include('testapp.foo.urls')), + + # Uncomment the admin/doc line below and add 'django.contrib.admindocs' + # to INSTALLED_APPS to enable admin documentation: + # (r'^admin/doc/', include('django.contrib.admindocs.urls')), + + # Uncomment the next line to enable the admin: + # (r'^admin/', include(admin.site.urls)), +) diff --git a/test/data/testapp-v3/__init__.py b/test/data/testapp-v3/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/test/data/testapp-v3/main/__init__.py b/test/data/testapp-v3/main/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/test/data/testapp-v3/main/models.py b/test/data/testapp-v3/main/models.py new file mode 100755 index 0000000..859b1d6 --- /dev/null +++ b/test/data/testapp-v3/main/models.py @@ -0,0 +1,6 @@ +from django.db import models + +# Create your models here. + +class Foo(models.Model): + name = models.CharField(max_length=255) diff --git a/test/data/testapp-v3/main/tests.py b/test/data/testapp-v3/main/tests.py new file mode 100755 index 0000000..3b31148 --- /dev/null +++ b/test/data/testapp-v3/main/tests.py @@ -0,0 +1,22 @@ +""" +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/test/data/testapp-v3/main/views.py b/test/data/testapp-v3/main/views.py new file mode 100755 index 0000000..c3088f4 --- /dev/null +++ b/test/data/testapp-v3/main/views.py @@ -0,0 +1,14 @@ +import os +from django.http import HttpResponse +from main.models import * + +def index(request): + return HttpResponse('testapp.main second edition') + +def add_foo(request): + f = Foo(name="bar") + f.save() + return HttpResponse("bar") + +def count_rows(request): + return HttpResponse(Foo.objects.all.count()) diff --git a/test/data/testapp-v3/manage.py b/test/data/testapp-v3/manage.py new file mode 100755 index 0000000..5e78ea9 --- /dev/null +++ b/test/data/testapp-v3/manage.py @@ -0,0 +1,11 @@ +#!/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/test/data/testapp-v3/settings.py b/test/data/testapp-v3/settings.py new file mode 100755 index 0000000..fbeafd9 --- /dev/null +++ b/test/data/testapp-v3/settings.py @@ -0,0 +1,95 @@ +# Django settings for testapp project. + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + # ('Your Name', 'your_email@domain.com'), +) + +MANAGERS = ADMINS + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': '', # Or path to database file if using sqlite3. + 'USER': '', # Not used with sqlite3. + 'PASSWORD': '', # 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. + } +} + +# 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 = '7=y^^6+_1&sqo*n=07pu@7(3=t&2rxv#-+4#ote0jo=a8f0jox' + +# 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 = 'testapp.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', + 'main', + # Uncomment the next line to enable the admin: + # 'django.contrib.admin', +) diff --git a/test/data/testapp-v3/urls.py b/test/data/testapp-v3/urls.py new file mode 100755 index 0000000..616d53c --- /dev/null +++ b/test/data/testapp-v3/urls.py @@ -0,0 +1,20 @@ +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'^add_foo$', 'testapp.main.views.add_foo'), + (r'^count_rows$', 'testapp.main.views.count_rows'), + (r'^$', 'testapp.main.views.index'), + # Example: + # (r'^testapp/', include('testapp.foo.urls')), + + # Uncomment the admin/doc line below and add 'django.contrib.admindocs' + # to INSTALLED_APPS to enable admin documentation: + # (r'^admin/doc/', include('django.contrib.admindocs.urls')), + + # Uncomment the next line to enable the admin: + # (r'^admin/', include(admin.site.urls)), +) diff --git a/test/data/testapp-v4/__init__.py b/test/data/testapp-v4/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/test/data/testapp-v4/main/__init__.py b/test/data/testapp-v4/main/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/test/data/testapp-v4/main/migrations/0001_initial.py b/test/data/testapp-v4/main/migrations/0001_initial.py new file mode 100644 index 0000000..676a8d5 --- /dev/null +++ b/test/data/testapp-v4/main/migrations/0001_initial.py @@ -0,0 +1,33 @@ +# 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 'Foo' + db.create_table('main_foo', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=255)), + )) + db.send_create_signal('main', ['Foo']) + + + def backwards(self, orm): + + # Deleting model 'Foo' + db.delete_table('main_foo') + + + models = { + 'main.foo': { + 'Meta': {'object_name': 'Foo'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['main'] diff --git a/test/data/testapp-v4/main/migrations/__init__.py b/test/data/testapp-v4/main/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/data/testapp-v4/main/models.py b/test/data/testapp-v4/main/models.py new file mode 100755 index 0000000..859b1d6 --- /dev/null +++ b/test/data/testapp-v4/main/models.py @@ -0,0 +1,6 @@ +from django.db import models + +# Create your models here. + +class Foo(models.Model): + name = models.CharField(max_length=255) diff --git a/test/data/testapp-v4/main/tests.py b/test/data/testapp-v4/main/tests.py new file mode 100755 index 0000000..3b31148 --- /dev/null +++ b/test/data/testapp-v4/main/tests.py @@ -0,0 +1,22 @@ +""" +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/test/data/testapp-v4/main/views.py b/test/data/testapp-v4/main/views.py new file mode 100755 index 0000000..c3088f4 --- /dev/null +++ b/test/data/testapp-v4/main/views.py @@ -0,0 +1,14 @@ +import os +from django.http import HttpResponse +from main.models import * + +def index(request): + return HttpResponse('testapp.main second edition') + +def add_foo(request): + f = Foo(name="bar") + f.save() + return HttpResponse("bar") + +def count_rows(request): + return HttpResponse(Foo.objects.all.count()) diff --git a/test/data/testapp-v4/manage.py b/test/data/testapp-v4/manage.py new file mode 100755 index 0000000..5e78ea9 --- /dev/null +++ b/test/data/testapp-v4/manage.py @@ -0,0 +1,11 @@ +#!/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/test/data/testapp-v4/settings.py b/test/data/testapp-v4/settings.py new file mode 100755 index 0000000..1c537c0 --- /dev/null +++ b/test/data/testapp-v4/settings.py @@ -0,0 +1,96 @@ +# Django settings for testapp project. + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + # ('Your Name', 'your_email@domain.com'), +) + +MANAGERS = ADMINS + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': 'dev.db', # Or path to database file if using sqlite3. + 'USER': '', # Not used with sqlite3. + 'PASSWORD': '', # 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. + } +} + +# 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 = '7=y^^6+_1&sqo*n=07pu@7(3=t&2rxv#-+4#ote0jo=a8f0jox' + +# 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 = 'testapp.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', + 'main', + 'south', + # Uncomment the next line to enable the admin: + # 'django.contrib.admin', +) diff --git a/test/data/testapp-v4/urls.py b/test/data/testapp-v4/urls.py new file mode 100755 index 0000000..616d53c --- /dev/null +++ b/test/data/testapp-v4/urls.py @@ -0,0 +1,20 @@ +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'^add_foo$', 'testapp.main.views.add_foo'), + (r'^count_rows$', 'testapp.main.views.count_rows'), + (r'^$', 'testapp.main.views.index'), + # Example: + # (r'^testapp/', include('testapp.foo.urls')), + + # Uncomment the admin/doc line below and add 'django.contrib.admindocs' + # to INSTALLED_APPS to enable admin documentation: + # (r'^admin/doc/', include('django.contrib.admindocs.urls')), + + # Uncomment the next line to enable the admin: + # (r'^admin/', include(admin.site.urls)), +) diff --git a/test/fetch_url.py b/test/fetch_url.py new file mode 100644 index 0000000..2583984 --- /dev/null +++ b/test/fetch_url.py @@ -0,0 +1,35 @@ +import socket + +def fetch_url(host, url): + """Fetch the headers and body of a URL from a given host. Useful when a + host recognizes virtual hosts that don't actually point to it in DNS.""" + + s = socket.create_connection((host, 80)).makefile() + s.write('GET %s HTTP/1.0\n\n' % url) + s.flush() + response = s.read() + s.close() + return response + +def fetch_url_body(host, url): + """Return just the body from fetch_url(host, url)""" + + response = fetch_url(host, url) + n = response.index('\r\n\r\n') + 4 + return response[n:] + +def fetch_url_headers(host, url): + """Return just the headers from fetch_url(host, url), as a dictionary.""" + + response = fetch_url(host, url) + n = response.index('\r\n\r\n') + headers_list = response[:n].split('\r\n') + headers_dict = {'STATUS': headers_list[0].split(' ', 2)[1]} + for header in headers_list: + try: + (k, v) = header.split(':', 1) + headers_dict[k.strip()] = v.strip() + except: + pass + + return headers_dict diff --git a/test/test_cases.py b/test/test_cases.py new file mode 100755 index 0000000..3a0b40f --- /dev/null +++ b/test/test_cases.py @@ -0,0 +1,184 @@ +#! /usr/bin/env python + +""" + Djangy system correctness test cases. + + When you run this file, additional output will go to log files in the + test_logs directory. +""" + +import os, os.path, random, shutil, sys, time +#from fabric.api import * +import fetch_url, urls +from testlib import * +from time import sleep + +TEST_DIR = os.path.dirname(os.path.abspath(__file__)) +DJANGY_SRC_DIR = os.path.dirname(TEST_DIR) +os.environ['PATH'] = os.environ['PATH'] + ':' + os.path.join(DJANGY_SRC_DIR, 'src/client') + +def random_application_name(): + return 'testapp' + str(random.randrange(10000000, 99999999)) + +@in_temp_dir +def test_cases(): + create_djangy_client_virtualenv() + test_urls(urls.urls) + assert test_create() # If test_create() fails, don't run the other test cases + test_recreate() + assert test_update() + test_cache() + test_logs() + test_syncdb() + test_migrate() + +def create_djangy_client_virtualenv(): + cwd = os.getcwd() + call('virtualenv', 'python-virtual') + os.environ['PATH'] = '%s:/usr/bin:/bin' % os.path.join(cwd, "python-virtual", "bin") + os.chdir(os.path.join(DJANGY_SRC_DIR, 'src', 'client')) + call('make') + os.chdir(cwd) + +@testcase +def test_create(): + global application_name, repository_path + # Make a new application + log('[%s]' % time.ctime()) + (application_name, repository_path) = make_application('testapp') + # Copy in code (as a subdirectory -- also need to test as direct contents) + log('[%s]' % time.ctime()) + shutil.copytree(os.path.join(TEST_DIR, 'data', 'testapp-v1'), os.path.join(repository_path, 'testapp')) + # Add code and djangy.config to git repository + log('[%s]' % time.ctime()) + commit_code('initial version') + # Push application to djangy + log('[%s]' % time.ctime()) + push_code() + # Check the website. + log('[%s]' % time.ctime()) + check_website(application_name, 'testapp.main') + log('[%s]' % time.ctime()) + return True + +@testcase +def test_recreate(): + # Try to make the application a second time (should fail) + call('djangy', 'create', should_fail=True) + +@testcase +def test_update(): + global application_name, repository_path + # Update code, move it to root level + #shutil.rmtree(os.path.join(repository_path, 'testapp')) + #shutil.copytree(, ) + call_list(['git', 'mv'] + listdir_path('testapp') + ['.']) + call('/bin/bash', '-c', 'cp -r %s/* %s' % (os.path.join(TEST_DIR, 'data', 'testapp-v2'), repository_path)) + commit_code('updated version') + # Push application to djangy + push_code() + # Check the website. + check_website(application_name, 'testapp.main second edition') + return True + +@testcase +def test_cache(): + global application_name, repository_path + # Check the website. + log('Checking static web data is cached...') + url = 'http://%s.djangy.com/site_media/index.html' % application_name + log('url: %s' % url) + headers1 = fetch_url.fetch_url_headers('api.djangy.com', url) + log('headers1: %s' % str(headers1)) + time.sleep(2) + headers2 = fetch_url.fetch_url_headers('api.djangy.com', url) + log('headers2: %s' % str(headers2)) + assert headers1['Cache-Control'] == 'max-stale=600' + assert headers1['Date'] != headers2['Date'] + assert headers1['Last-Modified'] == headers2['Last-Modified'] + assert headers1['Expires'] == headers2['Expires'] + +@testcase +def test_logs(): + # Check logs + logs = call('djangy', 'logs') + logs.index('DJANGY LOG') + +@testcase +def test_syncdb(): + # Check syncdb + call('/bin/bash', '-c', 'cp -r %s/* %s' % (os.path.join(TEST_DIR, 'data', 'testapp-v3'), repository_path)) + commit_code('test syncdb') + # Push application to djangy + push_code() + # run syncdb + output = call('djangy', 'manage.py', 'syncdb', stdin_contents="no") + # Check the website. + check_website(application_name, 'bar', resource="add_foo") + return (application_name, repository_path) + +@testcase +def test_migrate(): + # Check migrate + (application_name, repository_path) = make_application('testapp') + # Copy in code (as a subdirectory -- also need to test as direct contents) + shutil.copytree(os.path.join(TEST_DIR, 'data', 'testapp-v4'), os.path.join(repository_path, 'testapp')) + # Add code and djangy.config to git repository + commit_code('initial version') + # Push application to djangy + push_code() + # run syncdb + output = call('djangy', 'manage.py', 'syncdb', stdin_contents="no") + # run migrate + output = call('djangy', 'manage.py', 'migrate') + # Check the website. + check_website(application_name, 'bar', resource="add_foo") + return (application_name, repository_path) + +def make_application(rootdir): + # Choose an application name + application_name = random_application_name() + repository_path = os.path.join(os.getcwd(), application_name) + os.mkdir(repository_path) + os.chdir(repository_path) + # Create a git repository + call('git', 'init') + # Create a djangy.config file + create_file('djangy.config', '[application]\napplication_name=%s\nrootdir=%s\n' % (application_name, rootdir)) + commit_code('djangy.config') + # Create a djangy application + call('djangy', 'create') + return (application_name, repository_path) + +def create_file(file_path, file_contents): + file = open(file_path, 'w') + file.write(file_contents) + file.close() + +def commit_code(commit_message): + call('git', 'add', '.') + call('git', 'commit', '-m', commit_message) + +def push_code(): + # Push application to djangy + call('git', 'push', 'djangy', 'master') + sleep(1) + +def check_website(application_name, expected_output, resource=""): + log('Checking website output...',) + url = "http://%s.djangy.com/%s" % (application_name, resource) + log("Using URL: %s" % url) + result = fetch_url.fetch_url_body('api.djangy.com', url) + log("Expected output: %s" % expected_output) + log("Actual Output: %s" % result) + assert result == expected_output + log('Website output matched.') + +def listdir_path(dir_path): + return map(lambda x: os.path.join(dir_path, x), os.listdir(dir_path)) + +if __name__ == '__main__': + try: + test_cases() + except KeyboardInterrupt: + pass diff --git a/test/testlib.py b/test/testlib.py new file mode 100644 index 0000000..a2dae0f --- /dev/null +++ b/test/testlib.py @@ -0,0 +1,105 @@ +""" + Library of functions useful for writing test cases. +""" + +import os, os.path, shutil, subprocess, sys, tempfile, traceback, urllib2 + +log_dir = os.path.join(os.getcwd(), 'test_logs') +if not os.path.isdir(log_dir): + os.makedirs(log_dir) +test_log_file = None + +def log(message): + global test_log_file + if test_log_file: + test_log_file.write(message + '\n') + else: + print message + +def testcase(func): + """Decorator for test cases""" + test_case_name = func.func_name + def handle_exception(e): + log(traceback.format_exc(e)) + print 'FAILED' + return False + def test_case_func(*args, **kwargs): + global test_log_file + try: + test_log_file = open(os.path.join(log_dir, test_case_name + '.log'), 'w') + print ('%s...' % test_case_name), + sys.stdout.flush() + log('BEGIN TEST CASE %s' % test_case_name) + log('') + func(*args, **kwargs) + print 'OK' + return True + except Exception as e: + return handle_exception(e) + except KeyboardInterrupt as e: + return handle_exception(e) + except AssertionError as e: + return handle_exception(e) + finally: + log('') + log('END TEST CASE %s' % test_case_name) + test_log_file.close() + return test_case_func + +def in_temp_dir(func): + """Decorator for functions which need to run in a temporary scratch directory""" + def in_temp_dir_func(*args, **kwargs): + tempdir = tempfile.mkdtemp() + #homedir = os.environ['HOME'] + olddir = os.getcwd() + try: + os.chdir(tempdir) + #os.environ['HOME'] = tempdir + #ssh_dir = os.path.join(tempdir, '.ssh') + #os.mkdir(ssh_dir) + #subprocess.call(['ssh-keygen', '-N', '', '-f', os.path.join(tempdir, '.ssh', 'id_rsa')]) + #subprocess.call(['cp', os.path.join(TEST_DIR, test.djangy), os.path.join(tempdir, '.djangy')]) + return func(*args, **kwargs) + finally: + os.chdir(olddir) + #os.environ['HOME'] = homedir + if os.path.dirname(tempdir) == '/tmp': + shutil.rmtree(tempdir) + return in_temp_dir_func + +def call(*args, **kwargs): + return call_list(list(args), **kwargs) + +def call_list(args, should_fail=False, stdin_contents=None): + log('Calling %s' % ' '.join(args)) + p = subprocess.Popen(list(args), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE) + if stdin_contents: + p.stdin.write(stdin_contents) + p.stdin.close() + output = p.stdout.read() + log(output) + if should_fail: + assert p.wait() != 0 + else: + assert p.wait() == 0 + return output + +def test_url(url, expected_status_code=200, validation_function=None): + try: + response = urllib2.urlopen(url) + if expected_status_code != 200: + return False + if validation_function: + return validation_function(response.read()) + else: + return True + except urllib2.HTTPError as error: + return error.code == expected_status_code + +def test_urls(urls): + for url in urls: + print ('%s...' % url), + if test_url(url): + print 'OK' + else: + print 'FAILED' diff --git a/test/update_billing.py b/test/update_billing.py new file mode 100644 index 0000000..ea77d91 --- /dev/null +++ b/test/update_billing.py @@ -0,0 +1,29 @@ +from master_api import update_billing_info +from management_database import User + +def test_update_billing_info(): + user = User.get_by_email("bob@jones.mil") + user.customer_id = '-1' + user.subscription_id = '-1' + user.save() + info = { + 'first_name':'Bob', + 'last_name':'Jones', + 'addr1':'1234 Fast Lane', + 'addr2':'', + 'city':'San Francisco', + 'state':'CA', + 'zip':'94103', + 'expiration_month':'05', + 'expiration_year':'2015', + 'cc_number':'1', + 'cvv':'734' + } + + assert (user.customer_id == '-1') + assert (user.subscription_id == '-1') + update_billing_info("bob@jones.mil", info) + user = User.get_by_email("bob@jones.mil") + assert (user.customer_id != '-1') + assert (user.subscription_id != '-1') + diff --git a/test/urls.py b/test/urls.py new file mode 100644 index 0000000..84bbf99 --- /dev/null +++ b/test/urls.py @@ -0,0 +1,8 @@ +urls = [ + 'https://www.djangy.com/', + 'https://www.djangy.com/login', + 'https://www.djangy.com/docs', + 'https://www.djangy.com/docs/Documentation', + 'https://www.djangy.com/docs/Tutorial', + #'https://www.djangy.com/signup' # Should test this as a post request... +]
    ").append(res.responseText.replace(//g,"")).find(selector):res.responseText);self.each(callback,[res.responseText,status,res]);}});return this;},serialize:function(){return jQuery.param(this.serializeArray());},serializeArray:function(){return this.map(function(){return jQuery.nodeName(this,"form")?jQuery.makeArray(this.elements):this;}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password/i.test(this.type));}).map(function(i,elem){var val=jQuery(this).val();return val==null?null:val.constructor==Array?jQuery.map(val,function(val,i){return{name:elem.name,value:val};}):{name:elem.name,value:val};}).get();}});jQuery.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(i,o){jQuery.fn[o]=function(f){return this.bind(o,f);};});var jsc=(new Date).getTime();jQuery.extend({get:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data=null;}return jQuery.ajax({type:"GET",url:url,data:data,success:callback,dataType:type});},getScript:function(url,callback){return jQuery.get(url,null,callback,"script");},getJSON:function(url,data,callback){return jQuery.get(url,data,callback,"json");},post:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data={};}return jQuery.ajax({type:"POST",url:url,data:data,success:callback,dataType:type});},ajaxSetup:function(settings){jQuery.extend(jQuery.ajaxSettings,settings);},ajaxSettings:{global:true,type:"GET",timeout:0,contentType:"application/x-www-form-urlencoded",processData:true,async:true,data:null,username:null,password:null,accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(s){var jsonp,jsre=/=\?(&|$)/g,status,data;s=jQuery.extend(true,s,jQuery.extend(true,{},jQuery.ajaxSettings,s));if(s.data&&s.processData&&typeof s.data!="string")s.data=jQuery.param(s.data);if(s.dataType=="jsonp"){if(s.type.toLowerCase()=="get"){if(!s.url.match(jsre))s.url+=(s.url.match(/\?/)?"&":"?")+(s.jsonp||"callback")+"=?";}else if(!s.data||!s.data.match(jsre))s.data=(s.data?s.data+"&":"")+(s.jsonp||"callback")+"=?";s.dataType="json";}if(s.dataType=="json"&&(s.data&&s.data.match(jsre)||s.url.match(jsre))){jsonp="jsonp"+jsc++;if(s.data)s.data=(s.data+"").replace(jsre,"="+jsonp+"$1");s.url=s.url.replace(jsre,"="+jsonp+"$1");s.dataType="script";window[jsonp]=function(tmp){data=tmp;success();complete();window[jsonp]=undefined;try{delete window[jsonp];}catch(e){}if(head)head.removeChild(script);};}if(s.dataType=="script"&&s.cache==null)s.cache=false;if(s.cache===false&&s.type.toLowerCase()=="get"){var ts=(new Date()).getTime();var ret=s.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+ts+"$2");s.url=ret+((ret==s.url)?(s.url.match(/\?/)?"&":"?")+"_="+ts:"");}if(s.data&&s.type.toLowerCase()=="get"){s.url+=(s.url.match(/\?/)?"&":"?")+s.data;s.data=null;}if(s.global&&!jQuery.active++)jQuery.event.trigger("ajaxStart");if((!s.url.indexOf("http")||!s.url.indexOf("//"))&&s.dataType=="script"&&s.type.toLowerCase()=="get"){var head=document.getElementsByTagName("head")[0];var script=document.createElement("script");script.src=s.url;if(s.scriptCharset)script.charset=s.scriptCharset;if(!jsonp){var done=false;script.onload=script.onreadystatechange=function(){if(!done&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){done=true;success();complete();head.removeChild(script);}};}head.appendChild(script);return undefined;}var requestDone=false;var xml=window.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest();xml.open(s.type,s.url,s.async,s.username,s.password);try{if(s.data)xml.setRequestHeader("Content-Type",s.contentType);if(s.ifModified)xml.setRequestHeader("If-Modified-Since",jQuery.lastModified[s.url]||"Thu, 01 Jan 1970 00:00:00 GMT");xml.setRequestHeader("X-Requested-With","XMLHttpRequest");xml.setRequestHeader("Accept",s.dataType&&s.accepts[s.dataType]?s.accepts[s.dataType]+", */*":s.accepts._default);}catch(e){}if(s.beforeSend)s.beforeSend(xml);if(s.global)jQuery.event.trigger("ajaxSend",[xml,s]);var onreadystatechange=function(isTimeout){if(!requestDone&&xml&&(xml.readyState==4||isTimeout=="timeout")){requestDone=true;if(ival){clearInterval(ival);ival=null;}status=isTimeout=="timeout"&&"timeout"||!jQuery.httpSuccess(xml)&&"error"||s.ifModified&&jQuery.httpNotModified(xml,s.url)&&"notmodified"||"success";if(status=="success"){try{data=jQuery.httpData(xml,s.dataType);}catch(e){status="parsererror";}}if(status=="success"){var modRes;try{modRes=xml.getResponseHeader("Last-Modified");}catch(e){}if(s.ifModified&&modRes)jQuery.lastModified[s.url]=modRes;if(!jsonp)success();}else +jQuery.handleError(s,xml,status);complete();if(s.async)xml=null;}};if(s.async){var ival=setInterval(onreadystatechange,13);if(s.timeout>0)setTimeout(function(){if(xml){xml.abort();if(!requestDone)onreadystatechange("timeout");}},s.timeout);}try{xml.send(s.data);}catch(e){jQuery.handleError(s,xml,null,e);}if(!s.async)onreadystatechange();function success(){if(s.success)s.success(data,status);if(s.global)jQuery.event.trigger("ajaxSuccess",[xml,s]);}function complete(){if(s.complete)s.complete(xml,status);if(s.global)jQuery.event.trigger("ajaxComplete",[xml,s]);if(s.global&&!--jQuery.active)jQuery.event.trigger("ajaxStop");}return xml;},handleError:function(s,xml,status,e){if(s.error)s.error(xml,status,e);if(s.global)jQuery.event.trigger("ajaxError",[xml,s,e]);},active:0,httpSuccess:function(r){try{return!r.status&&location.protocol=="file:"||(r.status>=200&&r.status<300)||r.status==304||r.status==1223||jQuery.browser.safari&&r.status==undefined;}catch(e){}return false;},httpNotModified:function(xml,url){try{var xmlRes=xml.getResponseHeader("Last-Modified");return xml.status==304||xmlRes==jQuery.lastModified[url]||jQuery.browser.safari&&xml.status==undefined;}catch(e){}return false;},httpData:function(r,type){var ct=r.getResponseHeader("content-type");var xml=type=="xml"||!type&&ct&&ct.indexOf("xml")>=0;var data=xml?r.responseXML:r.responseText;if(xml&&data.documentElement.tagName=="parsererror")throw"parsererror";if(type=="script")jQuery.globalEval(data);if(type=="json")data=eval("("+data+")");return data;},param:function(a){var s=[];if(a.constructor==Array||a.jquery)jQuery.each(a,function(){s.push(encodeURIComponent(this.name)+"="+encodeURIComponent(this.value));});else +for(var j in a)if(a[j]&&a[j].constructor==Array)jQuery.each(a[j],function(){s.push(encodeURIComponent(j)+"="+encodeURIComponent(this));});else +s.push(encodeURIComponent(j)+"="+encodeURIComponent(a[j]));return s.join("&").replace(/%20/g,"+");}});jQuery.fn.extend({show:function(speed,callback){return speed?this.animate({height:"show",width:"show",opacity:"show"},speed,callback):this.filter(":hidden").each(function(){this.style.display=this.oldblock||"";if(jQuery.css(this,"display")=="none"){var elem=jQuery("<"+this.tagName+" />").appendTo("body");this.style.display=elem.css("display");if(this.style.display=="none")this.style.display="block";elem.remove();}}).end();},hide:function(speed,callback){return speed?this.animate({height:"hide",width:"hide",opacity:"hide"},speed,callback):this.filter(":visible").each(function(){this.oldblock=this.oldblock||jQuery.css(this,"display");this.style.display="none";}).end();},_toggle:jQuery.fn.toggle,toggle:function(fn,fn2){return jQuery.isFunction(fn)&&jQuery.isFunction(fn2)?this._toggle(fn,fn2):fn?this.animate({height:"toggle",width:"toggle",opacity:"toggle"},fn,fn2):this.each(function(){jQuery(this)[jQuery(this).is(":hidden")?"show":"hide"]();});},slideDown:function(speed,callback){return this.animate({height:"show"},speed,callback);},slideUp:function(speed,callback){return this.animate({height:"hide"},speed,callback);},slideToggle:function(speed,callback){return this.animate({height:"toggle"},speed,callback);},fadeIn:function(speed,callback){return this.animate({opacity:"show"},speed,callback);},fadeOut:function(speed,callback){return this.animate({opacity:"hide"},speed,callback);},fadeTo:function(speed,to,callback){return this.animate({opacity:to},speed,callback);},animate:function(prop,speed,easing,callback){var optall=jQuery.speed(speed,easing,callback);return this[optall.queue===false?"each":"queue"](function(){if(this.nodeType!=1)return false;var opt=jQuery.extend({},optall);var hidden=jQuery(this).is(":hidden"),self=this;for(var p in prop){if(prop[p]=="hide"&&hidden||prop[p]=="show"&&!hidden)return jQuery.isFunction(opt.complete)&&opt.complete.apply(this);if(p=="height"||p=="width"){opt.display=jQuery.css(this,"display");opt.overflow=this.style.overflow;}}if(opt.overflow!=null)this.style.overflow="hidden";opt.curAnim=jQuery.extend({},prop);jQuery.each(prop,function(name,val){var e=new jQuery.fx(self,opt,name);if(/toggle|show|hide/.test(val))e[val=="toggle"?hidden?"show":"hide":val](prop);else{var parts=val.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),start=e.cur(true)||0;if(parts){var end=parseFloat(parts[2]),unit=parts[3]||"px";if(unit!="px"){self.style[name]=(end||1)+unit;start=((end||1)/e.cur(true))*start;self.style[name]=start+unit;}if(parts[1])end=((parts[1]=="-="?-1:1)*end)+start;e.custom(start,end,unit);}else +e.custom(start,val,"");}});return true;});},queue:function(type,fn){if(jQuery.isFunction(type)||(type&&type.constructor==Array)){fn=type;type="fx";}if(!type||(typeof type=="string"&&!fn))return queue(this[0],type);return this.each(function(){if(fn.constructor==Array)queue(this,type,fn);else{queue(this,type).push(fn);if(queue(this,type).length==1)fn.apply(this);}});},stop:function(clearQueue,gotoEnd){var timers=jQuery.timers;if(clearQueue)this.queue([]);this.each(function(){for(var i=timers.length-1;i>=0;i--)if(timers[i].elem==this){if(gotoEnd)timers[i](true);timers.splice(i,1);}});if(!gotoEnd)this.dequeue();return this;}});var queue=function(elem,type,array){if(!elem)return undefined;type=type||"fx";var q=jQuery.data(elem,type+"queue");if(!q||array)q=jQuery.data(elem,type+"queue",array?jQuery.makeArray(array):[]);return q;};jQuery.fn.dequeue=function(type){type=type||"fx";return this.each(function(){var q=queue(this,type);q.shift();if(q.length)q[0].apply(this);});};jQuery.extend({speed:function(speed,easing,fn){var opt=speed&&speed.constructor==Object?speed:{complete:fn||!fn&&easing||jQuery.isFunction(speed)&&speed,duration:speed,easing:fn&&easing||easing&&easing.constructor!=Function&&easing};opt.duration=(opt.duration&&opt.duration.constructor==Number?opt.duration:{slow:600,fast:200}[opt.duration])||400;opt.old=opt.complete;opt.complete=function(){if(opt.queue!==false)jQuery(this).dequeue();if(jQuery.isFunction(opt.old))opt.old.apply(this);};return opt;},easing:{linear:function(p,n,firstNum,diff){return firstNum+diff*p;},swing:function(p,n,firstNum,diff){return((-Math.cos(p*Math.PI)/2)+0.5)*diff+firstNum;}},timers:[],timerId:null,fx:function(elem,options,prop){this.options=options;this.elem=elem;this.prop=prop;if(!options.orig)options.orig={};}});jQuery.fx.prototype={update:function(){if(this.options.step)this.options.step.apply(this.elem,[this.now,this]);(jQuery.fx.step[this.prop]||jQuery.fx.step._default)(this);if(this.prop=="height"||this.prop=="width")this.elem.style.display="block";},cur:function(force){if(this.elem[this.prop]!=null&&this.elem.style[this.prop]==null)return this.elem[this.prop];var r=parseFloat(jQuery.css(this.elem,this.prop,force));return r&&r>-10000?r:parseFloat(jQuery.curCSS(this.elem,this.prop))||0;},custom:function(from,to,unit){this.startTime=(new Date()).getTime();this.start=from;this.end=to;this.unit=unit||this.unit||"px";this.now=this.start;this.pos=this.state=0;this.update();var self=this;function t(gotoEnd){return self.step(gotoEnd);}t.elem=this.elem;jQuery.timers.push(t);if(jQuery.timerId==null){jQuery.timerId=setInterval(function(){var timers=jQuery.timers;for(var i=0;ithis.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var done=true;for(var i in this.options.curAnim)if(this.options.curAnim[i]!==true)done=false;if(done){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(jQuery.css(this.elem,"display")=="none")this.elem.style.display="block";}if(this.options.hide)this.elem.style.display="none";if(this.options.hide||this.options.show)for(var p in this.options.curAnim)jQuery.attr(this.elem.style,p,this.options.orig[p]);}if(done&&jQuery.isFunction(this.options.complete))this.options.complete.apply(this.elem);return false;}else{var n=t-this.startTime;this.state=n/this.options.duration;this.pos=jQuery.easing[this.options.easing||(jQuery.easing.swing?"swing":"linear")](this.state,n,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update();}return true;}};jQuery.fx.step={scrollLeft:function(fx){fx.elem.scrollLeft=fx.now;},scrollTop:function(fx){fx.elem.scrollTop=fx.now;},opacity:function(fx){jQuery.attr(fx.elem.style,"opacity",fx.now);},_default:function(fx){fx.elem.style[fx.prop]=fx.now+fx.unit;}};jQuery.fn.offset=function(){var left=0,top=0,elem=this[0],results;if(elem)with(jQuery.browser){var parent=elem.parentNode,offsetChild=elem,offsetParent=elem.offsetParent,doc=elem.ownerDocument,safari2=safari&&parseInt(version)<522&&!/adobeair/i.test(userAgent),fixed=jQuery.css(elem,"position")=="fixed";if(elem.getBoundingClientRect){var box=elem.getBoundingClientRect();add(box.left+Math.max(doc.documentElement.scrollLeft,doc.body.scrollLeft),box.top+Math.max(doc.documentElement.scrollTop,doc.body.scrollTop));add(-doc.documentElement.clientLeft,-doc.documentElement.clientTop);}else{add(elem.offsetLeft,elem.offsetTop);while(offsetParent){add(offsetParent.offsetLeft,offsetParent.offsetTop);if(mozilla&&!/^t(able|d|h)$/i.test(offsetParent.tagName)||safari&&!safari2)border(offsetParent);if(!fixed&&jQuery.css(offsetParent,"position")=="fixed")fixed=true;offsetChild=/^body$/i.test(offsetParent.tagName)?offsetChild:offsetParent;offsetParent=offsetParent.offsetParent;}while(parent&&parent.tagName&&!/^body|html$/i.test(parent.tagName)){if(!/^inline|table.*$/i.test(jQuery.css(parent,"display")))add(-parent.scrollLeft,-parent.scrollTop);if(mozilla&&jQuery.css(parent,"overflow")!="visible")border(parent);parent=parent.parentNode;}if((safari2&&(fixed||jQuery.css(offsetChild,"position")=="absolute"))||(mozilla&&jQuery.css(offsetChild,"position")!="absolute"))add(-doc.body.offsetLeft,-doc.body.offsetTop);if(fixed)add(Math.max(doc.documentElement.scrollLeft,doc.body.scrollLeft),Math.max(doc.documentElement.scrollTop,doc.body.scrollTop));}results={top:top,left:left};}function border(elem){add(jQuery.curCSS(elem,"borderLeftWidth",true),jQuery.curCSS(elem,"borderTopWidth",true));}function add(l,t){left+=parseInt(l)||0;top+=parseInt(t)||0;}return results;};})(); \ No newline at end of file diff --git a/src/server/master/web_ui/application/web_ui/static/assets/js/jquery-1.4.2.min.js b/src/server/master/web_ui/application/web_ui/static/assets/js/jquery-1.4.2.min.js new file mode 100644 index 0000000..7c24308 --- /dev/null +++ b/src/server/master/web_ui/application/web_ui/static/assets/js/jquery-1.4.2.min.js @@ -0,0 +1,154 @@ +/*! + * jQuery JavaScript Library v1.4.2 + * http://jquery.com/ + * + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2010, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Sat Feb 13 22:33:48 2010 -0500 + */ +(function(A,w){function ma(){if(!c.isReady){try{s.documentElement.doScroll("left")}catch(a){setTimeout(ma,1);return}c.ready()}}function Qa(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)}function X(a,b,d,f,e,j){var i=a.length;if(typeof b==="object"){for(var o in b)X(a,o,b[o],f,e,d);return a}if(d!==w){f=!j&&f&&c.isFunction(d);for(o=0;o)[^>]*$|^#([\w-]+)$/,Ua=/^.[^:#\[\.,]*$/,Va=/\S/, +Wa=/^(\s|\u00A0)+|(\s|\u00A0)+$/g,Xa=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,P=navigator.userAgent,xa=false,Q=[],L,$=Object.prototype.toString,aa=Object.prototype.hasOwnProperty,ba=Array.prototype.push,R=Array.prototype.slice,ya=Array.prototype.indexOf;c.fn=c.prototype={init:function(a,b){var d,f;if(!a)return this;if(a.nodeType){this.context=this[0]=a;this.length=1;return this}if(a==="body"&&!b){this.context=s;this[0]=s.body;this.selector="body";this.length=1;return this}if(typeof a==="string")if((d=Ta.exec(a))&& +(d[1]||!b))if(d[1]){f=b?b.ownerDocument||b:s;if(a=Xa.exec(a))if(c.isPlainObject(b)){a=[s.createElement(a[1])];c.fn.attr.call(a,b,true)}else a=[f.createElement(a[1])];else{a=sa([d[1]],[f]);a=(a.cacheable?a.fragment.cloneNode(true):a.fragment).childNodes}return c.merge(this,a)}else{if(b=s.getElementById(d[2])){if(b.id!==d[2])return T.find(a);this.length=1;this[0]=b}this.context=s;this.selector=a;return this}else if(!b&&/^\w+$/.test(a)){this.selector=a;this.context=s;a=s.getElementsByTagName(a);return c.merge(this, +a)}else return!b||b.jquery?(b||T).find(a):c(b).find(a);else if(c.isFunction(a))return T.ready(a);if(a.selector!==w){this.selector=a.selector;this.context=a.context}return c.makeArray(a,this)},selector:"",jquery:"1.4.2",length:0,size:function(){return this.length},toArray:function(){return R.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this.slice(a)[0]:this[a]},pushStack:function(a,b,d){var f=c();c.isArray(a)?ba.apply(f,a):c.merge(f,a);f.prevObject=this;f.context=this.context;if(b=== +"find")f.selector=this.selector+(this.selector?" ":"")+d;else if(b)f.selector=this.selector+"."+b+"("+d+")";return f},each:function(a,b){return c.each(this,a,b)},ready:function(a){c.bindReady();if(c.isReady)a.call(s,c);else Q&&Q.push(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(R.apply(this,arguments),"slice",R.call(arguments).join(","))},map:function(a){return this.pushStack(c.map(this, +function(b,d){return a.call(b,d,b)}))},end:function(){return this.prevObject||c(null)},push:ba,sort:[].sort,splice:[].splice};c.fn.init.prototype=c.fn;c.extend=c.fn.extend=function(){var a=arguments[0]||{},b=1,d=arguments.length,f=false,e,j,i,o;if(typeof a==="boolean"){f=a;a=arguments[1]||{};b=2}if(typeof a!=="object"&&!c.isFunction(a))a={};if(d===b){a=this;--b}for(;b
    a"; +var e=d.getElementsByTagName("*"),j=d.getElementsByTagName("a")[0];if(!(!e||!e.length||!j)){c.support={leadingWhitespace:d.firstChild.nodeType===3,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/red/.test(j.getAttribute("style")),hrefNormalized:j.getAttribute("href")==="/a",opacity:/^0.55$/.test(j.style.opacity),cssFloat:!!j.style.cssFloat,checkOn:d.getElementsByTagName("input")[0].value==="on",optSelected:s.createElement("select").appendChild(s.createElement("option")).selected, +parentNode:d.removeChild(d.appendChild(s.createElement("div"))).parentNode===null,deleteExpando:true,checkClone:false,scriptEval:false,noCloneEvent:true,boxModel:null};b.type="text/javascript";try{b.appendChild(s.createTextNode("window."+f+"=1;"))}catch(i){}a.insertBefore(b,a.firstChild);if(A[f]){c.support.scriptEval=true;delete A[f]}try{delete b.test}catch(o){c.support.deleteExpando=false}a.removeChild(b);if(d.attachEvent&&d.fireEvent){d.attachEvent("onclick",function k(){c.support.noCloneEvent= +false;d.detachEvent("onclick",k)});d.cloneNode(true).fireEvent("onclick")}d=s.createElement("div");d.innerHTML="";a=s.createDocumentFragment();a.appendChild(d.firstChild);c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var k=s.createElement("div");k.style.width=k.style.paddingLeft="1px";s.body.appendChild(k);c.boxModel=c.support.boxModel=k.offsetWidth===2;s.body.removeChild(k).style.display="none"});a=function(k){var n= +s.createElement("div");k="on"+k;var r=k in n;if(!r){n.setAttribute(k,"return;");r=typeof n[k]==="function"}return r};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=e=j=null}})();c.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var G="jQuery"+J(),Ya=0,za={};c.extend({cache:{},expando:G,noData:{embed:true,object:true, +applet:true},data:function(a,b,d){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var f=a[G],e=c.cache;if(!f&&typeof b==="string"&&d===w)return null;f||(f=++Ya);if(typeof b==="object"){a[G]=f;e[f]=c.extend(true,{},b)}else if(!e[f]){a[G]=f;e[f]={}}a=e[f];if(d!==w)a[b]=d;return typeof b==="string"?a[b]:a}},removeData:function(a,b){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var d=a[G],f=c.cache,e=f[d];if(b){if(e){delete e[b];c.isEmptyObject(e)&&c.removeData(a)}}else{if(c.support.deleteExpando)delete a[c.expando]; +else a.removeAttribute&&a.removeAttribute(c.expando);delete f[d]}}}});c.fn.extend({data:function(a,b){if(typeof a==="undefined"&&this.length)return c.data(this[0]);else if(typeof a==="object")return this.each(function(){c.data(this,a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===w){var f=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(f===w&&this.length)f=c.data(this[0],a);return f===w&&d[1]?this.data(d[0]):f}else return this.trigger("setData"+d[1]+"!",[d[0],b]).each(function(){c.data(this, +a,b)})},removeData:function(a){return this.each(function(){c.removeData(this,a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var f=c.data(a,b);if(!d)return f||[];if(!f||c.isArray(d))f=c.data(a,b,c.makeArray(d));else f.push(d);return f}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),f=d.shift();if(f==="inprogress")f=d.shift();if(f){b==="fx"&&d.unshift("inprogress");f.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b=== +w)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this,a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var Aa=/[\n\t]/g,ca=/\s+/,Za=/\r/g,$a=/href|src|style/,ab=/(button|input)/i,bb=/(button|input|object|select|textarea)/i, +cb=/^(a|area)$/i,Ba=/radio|checkbox/;c.fn.extend({attr:function(a,b){return X(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this,a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(n){var r=c(this);r.addClass(a.call(this,n,r.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(ca),d=0,f=this.length;d-1)return true;return false},val:function(a){if(a===w){var b=this[0];if(b){if(c.nodeName(b,"option"))return(b.attributes.value||{}).specified?b.value:b.text;if(c.nodeName(b,"select")){var d=b.selectedIndex,f=[],e=b.options;b=b.type==="select-one";if(d<0)return null;var j=b?d:0;for(d=b?d+1:e.length;j=0;else if(c.nodeName(this,"select")){var u=c.makeArray(r);c("option",this).each(function(){this.selected= +c.inArray(c(this).val(),u)>=0});if(!u.length)this.selectedIndex=-1}else this.value=r}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,f){if(!a||a.nodeType===3||a.nodeType===8)return w;if(f&&b in c.attrFn)return c(a)[b](d);f=a.nodeType!==1||!c.isXMLDoc(a);var e=d!==w;b=f&&c.props[b]||b;if(a.nodeType===1){var j=$a.test(b);if(b in a&&f&&!j){if(e){b==="type"&&ab.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed"); +a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:bb.test(a.nodeName)||cb.test(a.nodeName)&&a.href?0:w;return a[b]}if(!c.support.style&&f&&b==="style"){if(e)a.style.cssText=""+d;return a.style.cssText}e&&a.setAttribute(b,""+d);a=!c.support.hrefNormalized&&f&&j?a.getAttribute(b,2):a.getAttribute(b);return a===null?w:a}return c.style(a,b,d)}});var O=/\.(.*)$/,db=function(a){return a.replace(/[^\w\s\.\|`]/g, +function(b){return"\\"+b})};c.event={add:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){if(a.setInterval&&a!==A&&!a.frameElement)a=A;var e,j;if(d.handler){e=d;d=e.handler}if(!d.guid)d.guid=c.guid++;if(j=c.data(a)){var i=j.events=j.events||{},o=j.handle;if(!o)j.handle=o=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(o.elem,arguments):w};o.elem=a;b=b.split(" ");for(var k,n=0,r;k=b[n++];){j=e?c.extend({},e):{handler:d,data:f};if(k.indexOf(".")>-1){r=k.split("."); +k=r.shift();j.namespace=r.slice(0).sort().join(".")}else{r=[];j.namespace=""}j.type=k;j.guid=d.guid;var u=i[k],z=c.event.special[k]||{};if(!u){u=i[k]=[];if(!z.setup||z.setup.call(a,f,r,o)===false)if(a.addEventListener)a.addEventListener(k,o,false);else a.attachEvent&&a.attachEvent("on"+k,o)}if(z.add){z.add.call(a,j);if(!j.handler.guid)j.handler.guid=d.guid}u.push(j);c.event.global[k]=true}a=null}}},global:{},remove:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){var e,j=0,i,o,k,n,r,u,z=c.data(a), +C=z&&z.events;if(z&&C){if(b&&b.type){d=b.handler;b=b.type}if(!b||typeof b==="string"&&b.charAt(0)==="."){b=b||"";for(e in C)c.event.remove(a,e+b)}else{for(b=b.split(" ");e=b[j++];){n=e;i=e.indexOf(".")<0;o=[];if(!i){o=e.split(".");e=o.shift();k=new RegExp("(^|\\.)"+c.map(o.slice(0).sort(),db).join("\\.(?:.*\\.)?")+"(\\.|$)")}if(r=C[e])if(d){n=c.event.special[e]||{};for(B=f||0;B=0){a.type= +e=e.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();c.event.global[e]&&c.each(c.cache,function(){this.events&&this.events[e]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===8)return w;a.result=w;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(f=c.data(d,"handle"))&&f.apply(d,b);f=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+e]&&d["on"+e].apply(d,b)===false)a.result=false}catch(j){}if(!a.isPropagationStopped()&& +f)c.event.trigger(a,b,f,true);else if(!a.isDefaultPrevented()){f=a.target;var i,o=c.nodeName(f,"a")&&e==="click",k=c.event.special[e]||{};if((!k._default||k._default.call(d,a)===false)&&!o&&!(f&&f.nodeName&&c.noData[f.nodeName.toLowerCase()])){try{if(f[e]){if(i=f["on"+e])f["on"+e]=null;c.event.triggered=true;f[e]()}}catch(n){}if(i)f["on"+e]=i;c.event.triggered=false}}},handle:function(a){var b,d,f,e;a=arguments[0]=c.event.fix(a||A.event);a.currentTarget=this;b=a.type.indexOf(".")<0&&!a.exclusive; +if(!b){d=a.type.split(".");a.type=d.shift();f=new RegExp("(^|\\.)"+d.slice(0).sort().join("\\.(?:.*\\.)?")+"(\\.|$)")}e=c.data(this,"events");d=e[a.type];if(e&&d){d=d.slice(0);e=0;for(var j=d.length;e-1?c.map(a.options,function(f){return f.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d},fa=function(a,b){var d=a.target,f,e;if(!(!da.test(d.nodeName)||d.readOnly)){f=c.data(d,"_change_data");e=Fa(d);if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data", +e);if(!(f===w||e===f))if(f!=null||e){a.type="change";return c.event.trigger(a,b,d)}}};c.event.special.change={filters:{focusout:fa,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return fa.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return fa.call(this,a)},beforeactivate:function(a){a=a.target;c.data(a, +"_change_data",Fa(a))}},setup:function(){if(this.type==="file")return false;for(var a in ea)c.event.add(this,a+".specialChange",ea[a]);return da.test(this.nodeName)},teardown:function(){c.event.remove(this,".specialChange");return da.test(this.nodeName)}};ea=c.event.special.change.filters}s.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(f){f=c.event.fix(f);f.type=b;return c.event.handle.call(this,f)}c.event.special[b]={setup:function(){this.addEventListener(a, +d,true)},teardown:function(){this.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,f,e){if(typeof d==="object"){for(var j in d)this[b](j,f,d[j],e);return this}if(c.isFunction(f)){e=f;f=w}var i=b==="one"?c.proxy(e,function(k){c(this).unbind(k,i);return e.apply(this,arguments)}):e;if(d==="unload"&&b!=="one")this.one(d,f,e);else{j=0;for(var o=this.length;j0){y=t;break}}t=t[g]}m[q]=y}}}var f=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, +e=0,j=Object.prototype.toString,i=false,o=true;[0,0].sort(function(){o=false;return 0});var k=function(g,h,l,m){l=l||[];var q=h=h||s;if(h.nodeType!==1&&h.nodeType!==9)return[];if(!g||typeof g!=="string")return l;for(var p=[],v,t,y,S,H=true,M=x(h),I=g;(f.exec(""),v=f.exec(I))!==null;){I=v[3];p.push(v[1]);if(v[2]){S=v[3];break}}if(p.length>1&&r.exec(g))if(p.length===2&&n.relative[p[0]])t=ga(p[0]+p[1],h);else for(t=n.relative[p[0]]?[h]:k(p.shift(),h);p.length;){g=p.shift();if(n.relative[g])g+=p.shift(); +t=ga(g,t)}else{if(!m&&p.length>1&&h.nodeType===9&&!M&&n.match.ID.test(p[0])&&!n.match.ID.test(p[p.length-1])){v=k.find(p.shift(),h,M);h=v.expr?k.filter(v.expr,v.set)[0]:v.set[0]}if(h){v=m?{expr:p.pop(),set:z(m)}:k.find(p.pop(),p.length===1&&(p[0]==="~"||p[0]==="+")&&h.parentNode?h.parentNode:h,M);t=v.expr?k.filter(v.expr,v.set):v.set;if(p.length>0)y=z(t);else H=false;for(;p.length;){var D=p.pop();v=D;if(n.relative[D])v=p.pop();else D="";if(v==null)v=h;n.relative[D](y,v,M)}}else y=[]}y||(y=t);y||k.error(D|| +g);if(j.call(y)==="[object Array]")if(H)if(h&&h.nodeType===1)for(g=0;y[g]!=null;g++){if(y[g]&&(y[g]===true||y[g].nodeType===1&&E(h,y[g])))l.push(t[g])}else for(g=0;y[g]!=null;g++)y[g]&&y[g].nodeType===1&&l.push(t[g]);else l.push.apply(l,y);else z(y,l);if(S){k(S,q,l,m);k.uniqueSort(l)}return l};k.uniqueSort=function(g){if(B){i=o;g.sort(B);if(i)for(var h=1;h":function(g,h){var l=typeof h==="string";if(l&&!/\W/.test(h)){h=h.toLowerCase();for(var m=0,q=g.length;m=0))l||m.push(v);else if(l)h[p]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()}, +CHILD:function(g){if(g[1]==="nth"){var h=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=h[1]+(h[2]||1)-0;g[3]=h[3]-0}g[0]=e++;return g},ATTR:function(g,h,l,m,q,p){h=g[1].replace(/\\/g,"");if(!p&&n.attrMap[h])g[1]=n.attrMap[h];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,h,l,m,q){if(g[1]==="not")if((f.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=k(g[3],null,null,h);else{g=k.filter(g[3],h,l,true^q);l||m.push.apply(m, +g);return false}else if(n.match.POS.test(g[0])||n.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled===true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,h,l){return!!k(l[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)}, +text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"===g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}}, +setFilters:{first:function(g,h){return h===0},last:function(g,h,l,m){return h===m.length-1},even:function(g,h){return h%2===0},odd:function(g,h){return h%2===1},lt:function(g,h,l){return hl[3]-0},nth:function(g,h,l){return l[3]-0===h},eq:function(g,h,l){return l[3]-0===h}},filter:{PSEUDO:function(g,h,l,m){var q=h[1],p=n.filters[q];if(p)return p(g,l,h,m);else if(q==="contains")return(g.textContent||g.innerText||a([g])||"").indexOf(h[3])>=0;else if(q==="not"){h= +h[3];l=0;for(m=h.length;l=0}},ID:function(g,h){return g.nodeType===1&&g.getAttribute("id")===h},TAG:function(g,h){return h==="*"&&g.nodeType===1||g.nodeName.toLowerCase()===h},CLASS:function(g,h){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(h)>-1},ATTR:function(g,h){var l=h[1];g=n.attrHandle[l]?n.attrHandle[l](g):g[l]!=null?g[l]:g.getAttribute(l);l=g+"";var m=h[2];h=h[4];return g==null?m==="!=":m=== +"="?l===h:m==="*="?l.indexOf(h)>=0:m==="~="?(" "+l+" ").indexOf(h)>=0:!h?l&&g!==false:m==="!="?l!==h:m==="^="?l.indexOf(h)===0:m==="$="?l.substr(l.length-h.length)===h:m==="|="?l===h||l.substr(0,h.length+1)===h+"-":false},POS:function(g,h,l,m){var q=n.setFilters[h[2]];if(q)return q(g,l,h,m)}}},r=n.match.POS;for(var u in n.match){n.match[u]=new RegExp(n.match[u].source+/(?![^\[]*\])(?![^\(]*\))/.source);n.leftMatch[u]=new RegExp(/(^(?:.|\r|\n)*?)/.source+n.match[u].source.replace(/\\(\d+)/g,function(g, +h){return"\\"+(h-0+1)}))}var z=function(g,h){g=Array.prototype.slice.call(g,0);if(h){h.push.apply(h,g);return h}return g};try{Array.prototype.slice.call(s.documentElement.childNodes,0)}catch(C){z=function(g,h){h=h||[];if(j.call(g)==="[object Array]")Array.prototype.push.apply(h,g);else if(typeof g.length==="number")for(var l=0,m=g.length;l";var l=s.documentElement;l.insertBefore(g,l.firstChild);if(s.getElementById(h)){n.find.ID=function(m,q,p){if(typeof q.getElementById!=="undefined"&&!p)return(q=q.getElementById(m[1]))?q.id===m[1]||typeof q.getAttributeNode!=="undefined"&& +q.getAttributeNode("id").nodeValue===m[1]?[q]:w:[]};n.filter.ID=function(m,q){var p=typeof m.getAttributeNode!=="undefined"&&m.getAttributeNode("id");return m.nodeType===1&&p&&p.nodeValue===q}}l.removeChild(g);l=g=null})();(function(){var g=s.createElement("div");g.appendChild(s.createComment(""));if(g.getElementsByTagName("*").length>0)n.find.TAG=function(h,l){l=l.getElementsByTagName(h[1]);if(h[1]==="*"){h=[];for(var m=0;l[m];m++)l[m].nodeType===1&&h.push(l[m]);l=h}return l};g.innerHTML=""; +if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")n.attrHandle.href=function(h){return h.getAttribute("href",2)};g=null})();s.querySelectorAll&&function(){var g=k,h=s.createElement("div");h.innerHTML="

    ";if(!(h.querySelectorAll&&h.querySelectorAll(".TEST").length===0)){k=function(m,q,p,v){q=q||s;if(!v&&q.nodeType===9&&!x(q))try{return z(q.querySelectorAll(m),p)}catch(t){}return g(m,q,p,v)};for(var l in g)k[l]=g[l];h=null}}(); +(function(){var g=s.createElement("div");g.innerHTML="
    ";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length===0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){n.order.splice(1,0,"CLASS");n.find.CLASS=function(h,l,m){if(typeof l.getElementsByClassName!=="undefined"&&!m)return l.getElementsByClassName(h[1])};g=null}}})();var E=s.compareDocumentPosition?function(g,h){return!!(g.compareDocumentPosition(h)&16)}: +function(g,h){return g!==h&&(g.contains?g.contains(h):true)},x=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false},ga=function(g,h){var l=[],m="",q;for(h=h.nodeType?[h]:h;q=n.match.PSEUDO.exec(g);){m+=q[0];g=g.replace(n.match.PSEUDO,"")}g=n.relative[g]?g+"*":g;q=0;for(var p=h.length;q=0===d})};c.fn.extend({find:function(a){for(var b=this.pushStack("","find",a),d=0,f=0,e=this.length;f0)for(var j=d;j0},closest:function(a,b){if(c.isArray(a)){var d=[],f=this[0],e,j= +{},i;if(f&&a.length){e=0;for(var o=a.length;e-1:c(f).is(e)){d.push({selector:i,elem:f});delete j[i]}}f=f.parentNode}}return d}var k=c.expr.match.POS.test(a)?c(a,b||this.context):null;return this.map(function(n,r){for(;r&&r.ownerDocument&&r!==b;){if(k?k.index(r)>-1:c(r).is(a))return r;r=r.parentNode}return null})},index:function(a){if(!a||typeof a=== +"string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){a=typeof a==="string"?c(a,b||this.context):c.makeArray(a);b=c.merge(this.get(),a);return this.pushStack(qa(a[0])||qa(b[0])?b:c.unique(b))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode", +d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")? +a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,f){var e=c.map(this,b,d);eb.test(a)||(f=d);if(f&&typeof f==="string")e=c.filter(f,e);e=this.length>1?c.unique(e):e;if((this.length>1||gb.test(f))&&fb.test(a))e=e.reverse();return this.pushStack(e,a,R.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return c.find.matches(a,b)},dir:function(a,b,d){var f=[];for(a=a[b];a&&a.nodeType!==9&&(d===w||a.nodeType!==1||!c(a).is(d));){a.nodeType=== +1&&f.push(a);a=a[b]}return f},nth:function(a,b,d){b=b||1;for(var f=0;a;a=a[d])if(a.nodeType===1&&++f===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==b&&d.push(a);return d}});var Ja=/ jQuery\d+="(?:\d+|null)"/g,V=/^\s+/,Ka=/(<([\w:]+)[^>]*?)\/>/g,hb=/^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,La=/<([\w:]+)/,ib=/"},F={option:[1,""],legend:[1,"
    ","
    "],thead:[1,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],col:[2,"","
    "],area:[1,"",""],_default:[0,"",""]};F.optgroup=F.option;F.tbody=F.tfoot=F.colgroup=F.caption=F.thead;F.th=F.td;if(!c.support.htmlSerialize)F._default=[1,"div
    ","
    "];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d= +c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==w)return this.empty().append((this[0]&&this[0].ownerDocument||s).createTextNode(a));return c.text(this)},wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this}, +wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})}, +prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b, +this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},remove:function(a,b){for(var d=0,f;(f=this[d])!=null;d++)if(!a||c.filter(a,[f]).length){if(!b&&f.nodeType===1){c.cleanData(f.getElementsByTagName("*"));c.cleanData([f])}f.parentNode&&f.parentNode.removeChild(f)}return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++)for(b.nodeType===1&&c.cleanData(b.getElementsByTagName("*"));b.firstChild;)b.removeChild(b.firstChild); +return this},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&!c.isXMLDoc(this)){var d=this.outerHTML,f=this.ownerDocument;if(!d){d=f.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(Ja,"").replace(/=([^="'>\s]+\/)>/g,'="$1">').replace(V,"")],f)[0]}else return this.cloneNode(true)});if(a===true){ra(this,b);ra(this.find("*"),b.find("*"))}return b},html:function(a){if(a===w)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Ja, +""):null;else if(typeof a==="string"&&!ta.test(a)&&(c.support.leadingWhitespace||!V.test(a))&&!F[(La.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Ka,Ma);try{for(var b=0,d=this.length;b0||e.cacheable||this.length>1?k.cloneNode(true):k)}o.length&&c.each(o,Qa)}return this}});c.fragments={};c.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]=function(d){var f=[];d=c(d);var e=this.length===1&&this[0].parentNode;if(e&&e.nodeType===11&&e.childNodes.length===1&&d.length===1){d[b](this[0]); +return this}else{e=0;for(var j=d.length;e0?this.clone(true):this).get();c.fn[b].apply(c(d[e]),i);f=f.concat(i)}return this.pushStack(f,a,d.selector)}}});c.extend({clean:function(a,b,d,f){b=b||s;if(typeof b.createElement==="undefined")b=b.ownerDocument||b[0]&&b[0].ownerDocument||s;for(var e=[],j=0,i;(i=a[j])!=null;j++){if(typeof i==="number")i+="";if(i){if(typeof i==="string"&&!jb.test(i))i=b.createTextNode(i);else if(typeof i==="string"){i=i.replace(Ka,Ma);var o=(La.exec(i)||["", +""])[1].toLowerCase(),k=F[o]||F._default,n=k[0],r=b.createElement("div");for(r.innerHTML=k[1]+i+k[2];n--;)r=r.lastChild;if(!c.support.tbody){n=ib.test(i);o=o==="table"&&!n?r.firstChild&&r.firstChild.childNodes:k[1]===""&&!n?r.childNodes:[];for(k=o.length-1;k>=0;--k)c.nodeName(o[k],"tbody")&&!o[k].childNodes.length&&o[k].parentNode.removeChild(o[k])}!c.support.leadingWhitespace&&V.test(i)&&r.insertBefore(b.createTextNode(V.exec(i)[0]),r.firstChild);i=r.childNodes}if(i.nodeType)e.push(i);else e= +c.merge(e,i)}}if(d)for(j=0;e[j];j++)if(f&&c.nodeName(e[j],"script")&&(!e[j].type||e[j].type.toLowerCase()==="text/javascript"))f.push(e[j].parentNode?e[j].parentNode.removeChild(e[j]):e[j]);else{e[j].nodeType===1&&e.splice.apply(e,[j+1,0].concat(c.makeArray(e[j].getElementsByTagName("script"))));d.appendChild(e[j])}return e},cleanData:function(a){for(var b,d,f=c.cache,e=c.event.special,j=c.support.deleteExpando,i=0,o;(o=a[i])!=null;i++)if(d=o[c.expando]){b=f[d];if(b.events)for(var k in b.events)e[k]? +c.event.remove(o,k):Ca(o,k,b.handle);if(j)delete o[c.expando];else o.removeAttribute&&o.removeAttribute(c.expando);delete f[d]}}});var kb=/z-?index|font-?weight|opacity|zoom|line-?height/i,Na=/alpha\([^)]*\)/,Oa=/opacity=([^)]*)/,ha=/float/i,ia=/-([a-z])/ig,lb=/([A-Z])/g,mb=/^-?\d+(?:px)?$/i,nb=/^-?\d/,ob={position:"absolute",visibility:"hidden",display:"block"},pb=["Left","Right"],qb=["Top","Bottom"],rb=s.defaultView&&s.defaultView.getComputedStyle,Pa=c.support.cssFloat?"cssFloat":"styleFloat",ja= +function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){return X(this,a,b,true,function(d,f,e){if(e===w)return c.curCSS(d,f);if(typeof e==="number"&&!kb.test(f))e+="px";c.style(d,f,e)})};c.extend({style:function(a,b,d){if(!a||a.nodeType===3||a.nodeType===8)return w;if((b==="width"||b==="height")&&parseFloat(d)<0)d=w;var f=a.style||a,e=d!==w;if(!c.support.opacity&&b==="opacity"){if(e){f.zoom=1;b=parseInt(d,10)+""==="NaN"?"":"alpha(opacity="+d*100+")";a=f.filter||c.curCSS(a,"filter")||"";f.filter= +Na.test(a)?a.replace(Na,b):b}return f.filter&&f.filter.indexOf("opacity=")>=0?parseFloat(Oa.exec(f.filter)[1])/100+"":""}if(ha.test(b))b=Pa;b=b.replace(ia,ja);if(e)f[b]=d;return f[b]},css:function(a,b,d,f){if(b==="width"||b==="height"){var e,j=b==="width"?pb:qb;function i(){e=b==="width"?a.offsetWidth:a.offsetHeight;f!=="border"&&c.each(j,function(){f||(e-=parseFloat(c.curCSS(a,"padding"+this,true))||0);if(f==="margin")e+=parseFloat(c.curCSS(a,"margin"+this,true))||0;else e-=parseFloat(c.curCSS(a, +"border"+this+"Width",true))||0})}a.offsetWidth!==0?i():c.swap(a,ob,i);return Math.max(0,Math.round(e))}return c.curCSS(a,b,d)},curCSS:function(a,b,d){var f,e=a.style;if(!c.support.opacity&&b==="opacity"&&a.currentStyle){f=Oa.test(a.currentStyle.filter||"")?parseFloat(RegExp.$1)/100+"":"";return f===""?"1":f}if(ha.test(b))b=Pa;if(!d&&e&&e[b])f=e[b];else if(rb){if(ha.test(b))b="float";b=b.replace(lb,"-$1").toLowerCase();e=a.ownerDocument.defaultView;if(!e)return null;if(a=e.getComputedStyle(a,null))f= +a.getPropertyValue(b);if(b==="opacity"&&f==="")f="1"}else if(a.currentStyle){d=b.replace(ia,ja);f=a.currentStyle[b]||a.currentStyle[d];if(!mb.test(f)&&nb.test(f)){b=e.left;var j=a.runtimeStyle.left;a.runtimeStyle.left=a.currentStyle.left;e.left=d==="fontSize"?"1em":f||0;f=e.pixelLeft+"px";e.left=b;a.runtimeStyle.left=j}}return f},swap:function(a,b,d){var f={};for(var e in b){f[e]=a.style[e];a.style[e]=b[e]}d.call(a);for(e in b)a.style[e]=f[e]}});if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b= +a.offsetWidth,d=a.offsetHeight,f=a.nodeName.toLowerCase()==="tr";return b===0&&d===0&&!f?true:b>0&&d>0&&!f?false:c.curCSS(a,"display")==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var sb=J(),tb=//gi,ub=/select|textarea/i,vb=/color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,N=/=\?(&|$)/,ka=/\?/,wb=/(\?|&)_=.*?(&|$)/,xb=/^(\w+:)?\/\/([^\/?#]+)/,yb=/%20/g,zb=c.fn.load;c.fn.extend({load:function(a,b,d){if(typeof a!== +"string")return zb.call(this,a);else if(!this.length)return this;var f=a.indexOf(" ");if(f>=0){var e=a.slice(f,a.length);a=a.slice(0,f)}f="GET";if(b)if(c.isFunction(b)){d=b;b=null}else if(typeof b==="object"){b=c.param(b,c.ajaxSettings.traditional);f="POST"}var j=this;c.ajax({url:a,type:f,dataType:"html",data:b,complete:function(i,o){if(o==="success"||o==="notmodified")j.html(e?c("
    ").append(i.responseText.replace(tb,"")).find(e):i.responseText);d&&j.each(d,[i.responseText,o,i])}});return this}, +serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||ub.test(this.nodeName)||vb.test(this.type))}).map(function(a,b){a=c(this).val();return a==null?null:c.isArray(a)?c.map(a,function(d){return{name:b.name,value:d}}):{name:b.name,value:a}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "), +function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:f})},getScript:function(a,b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:f})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href, +global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:A.XMLHttpRequest&&(A.location.protocol!=="file:"||!A.ActiveXObject)?function(){return new A.XMLHttpRequest}:function(){try{return new A.ActiveXObject("Microsoft.XMLHTTP")}catch(a){}},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},etag:{},ajax:function(a){function b(){e.success&& +e.success.call(k,o,i,x);e.global&&f("ajaxSuccess",[x,e])}function d(){e.complete&&e.complete.call(k,x,i);e.global&&f("ajaxComplete",[x,e]);e.global&&!--c.active&&c.event.trigger("ajaxStop")}function f(q,p){(e.context?c(e.context):c.event).trigger(q,p)}var e=c.extend(true,{},c.ajaxSettings,a),j,i,o,k=a&&a.context||e,n=e.type.toUpperCase();if(e.data&&e.processData&&typeof e.data!=="string")e.data=c.param(e.data,e.traditional);if(e.dataType==="jsonp"){if(n==="GET")N.test(e.url)||(e.url+=(ka.test(e.url)? +"&":"?")+(e.jsonp||"callback")+"=?");else if(!e.data||!N.test(e.data))e.data=(e.data?e.data+"&":"")+(e.jsonp||"callback")+"=?";e.dataType="json"}if(e.dataType==="json"&&(e.data&&N.test(e.data)||N.test(e.url))){j=e.jsonpCallback||"jsonp"+sb++;if(e.data)e.data=(e.data+"").replace(N,"="+j+"$1");e.url=e.url.replace(N,"="+j+"$1");e.dataType="script";A[j]=A[j]||function(q){o=q;b();d();A[j]=w;try{delete A[j]}catch(p){}z&&z.removeChild(C)}}if(e.dataType==="script"&&e.cache===null)e.cache=false;if(e.cache=== +false&&n==="GET"){var r=J(),u=e.url.replace(wb,"$1_="+r+"$2");e.url=u+(u===e.url?(ka.test(e.url)?"&":"?")+"_="+r:"")}if(e.data&&n==="GET")e.url+=(ka.test(e.url)?"&":"?")+e.data;e.global&&!c.active++&&c.event.trigger("ajaxStart");r=(r=xb.exec(e.url))&&(r[1]&&r[1]!==location.protocol||r[2]!==location.host);if(e.dataType==="script"&&n==="GET"&&r){var z=s.getElementsByTagName("head")[0]||s.documentElement,C=s.createElement("script");C.src=e.url;if(e.scriptCharset)C.charset=e.scriptCharset;if(!j){var B= +false;C.onload=C.onreadystatechange=function(){if(!B&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){B=true;b();d();C.onload=C.onreadystatechange=null;z&&C.parentNode&&z.removeChild(C)}}}z.insertBefore(C,z.firstChild);return w}var E=false,x=e.xhr();if(x){e.username?x.open(n,e.url,e.async,e.username,e.password):x.open(n,e.url,e.async);try{if(e.data||a&&a.contentType)x.setRequestHeader("Content-Type",e.contentType);if(e.ifModified){c.lastModified[e.url]&&x.setRequestHeader("If-Modified-Since", +c.lastModified[e.url]);c.etag[e.url]&&x.setRequestHeader("If-None-Match",c.etag[e.url])}r||x.setRequestHeader("X-Requested-With","XMLHttpRequest");x.setRequestHeader("Accept",e.dataType&&e.accepts[e.dataType]?e.accepts[e.dataType]+", */*":e.accepts._default)}catch(ga){}if(e.beforeSend&&e.beforeSend.call(k,x,e)===false){e.global&&!--c.active&&c.event.trigger("ajaxStop");x.abort();return false}e.global&&f("ajaxSend",[x,e]);var g=x.onreadystatechange=function(q){if(!x||x.readyState===0||q==="abort"){E|| +d();E=true;if(x)x.onreadystatechange=c.noop}else if(!E&&x&&(x.readyState===4||q==="timeout")){E=true;x.onreadystatechange=c.noop;i=q==="timeout"?"timeout":!c.httpSuccess(x)?"error":e.ifModified&&c.httpNotModified(x,e.url)?"notmodified":"success";var p;if(i==="success")try{o=c.httpData(x,e.dataType,e)}catch(v){i="parsererror";p=v}if(i==="success"||i==="notmodified")j||b();else c.handleError(e,x,i,p);d();q==="timeout"&&x.abort();if(e.async)x=null}};try{var h=x.abort;x.abort=function(){x&&h.call(x); +g("abort")}}catch(l){}e.async&&e.timeout>0&&setTimeout(function(){x&&!E&&g("timeout")},e.timeout);try{x.send(n==="POST"||n==="PUT"||n==="DELETE"?e.data:null)}catch(m){c.handleError(e,x,null,m);d()}e.async||g();return x}},handleError:function(a,b,d,f){if(a.error)a.error.call(a.context||a,b,d,f);if(a.global)(a.context?c(a.context):c.event).trigger("ajaxError",[b,a,f])},active:0,httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status=== +1223||a.status===0}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"),f=a.getResponseHeader("Etag");if(d)c.lastModified[b]=d;if(f)c.etag[b]=f;return a.status===304||a.status===0},httpData:function(a,b,d){var f=a.getResponseHeader("content-type")||"",e=b==="xml"||!b&&f.indexOf("xml")>=0;a=e?a.responseXML:a.responseText;e&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b=== +"json"||!b&&f.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&f.indexOf("javascript")>=0)c.globalEval(a);return a},param:function(a,b){function d(i,o){if(c.isArray(o))c.each(o,function(k,n){b||/\[\]$/.test(i)?f(i,n):d(i+"["+(typeof n==="object"||c.isArray(n)?k:"")+"]",n)});else!b&&o!=null&&typeof o==="object"?c.each(o,function(k,n){d(i+"["+k+"]",n)}):f(i,o)}function f(i,o){o=c.isFunction(o)?o():o;e[e.length]=encodeURIComponent(i)+"="+encodeURIComponent(o)}var e=[];if(b===w)b=c.ajaxSettings.traditional; +if(c.isArray(a)||a.jquery)c.each(a,function(){f(this.name,this.value)});else for(var j in a)d(j,a[j]);return e.join("&").replace(yb,"+")}});var la={},Ab=/toggle|show|hide/,Bb=/^([+-]=)?([\d+-.]+)(.*)$/,W,va=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b){if(a||a===0)return this.animate(K("show",3),a,b);else{a=0;for(b=this.length;a").appendTo("body");f=e.css("display");if(f==="none")f="block";e.remove();la[d]=f}c.data(this[a],"olddisplay",f)}}a=0;for(b=this.length;a=0;f--)if(d[f].elem===this){b&&d[f](true);d.splice(f,1)}});b||this.dequeue();return this}});c.each({slideDown:K("show",1),slideUp:K("hide",1),slideToggle:K("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(a,b){c.fn[a]=function(d,f){return this.animate(b,d,f)}});c.extend({speed:function(a,b,d){var f=a&&typeof a==="object"?a:{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};f.duration=c.fx.off?0:typeof f.duration=== +"number"?f.duration:c.fx.speeds[f.duration]||c.fx.speeds._default;f.old=f.complete;f.complete=function(){f.queue!==false&&c(this).dequeue();c.isFunction(f.old)&&f.old.call(this)};return f},easing:{linear:function(a,b,d,f){return d+f*a},swing:function(a,b,d,f){return(-Math.cos(a*Math.PI)/2+0.5)*f+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]|| +c.fx.step._default)(this);if((this.prop==="height"||this.prop==="width")&&this.elem.style)this.elem.style.display="block"},cur:function(a){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];return(a=parseFloat(c.css(this.elem,this.prop,a)))&&a>-10000?a:parseFloat(c.curCSS(this.elem,this.prop))||0},custom:function(a,b,d){function f(j){return e.step(j)}this.startTime=J();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start; +this.pos=this.state=0;var e=this;f.elem=this.elem;if(f()&&c.timers.push(f)&&!W)W=setInterval(c.fx.tick,13)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(a){var b=J(),d=true;if(a||b>=this.options.duration+this.startTime){this.now= +this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var f in this.options.curAnim)if(this.options.curAnim[f]!==true)d=false;if(d){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;a=c.data(this.elem,"olddisplay");this.elem.style.display=a?a:this.options.display;if(c.css(this.elem,"display")==="none")this.elem.style.display="block"}this.options.hide&&c(this.elem).hide();if(this.options.hide||this.options.show)for(var e in this.options.curAnim)c.style(this.elem, +e,this.options.orig[e]);this.options.complete.call(this.elem)}return false}else{e=b-this.startTime;this.state=e/this.options.duration;a=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||a](this.state,e,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a=c.timers,b=0;b
    "; +a.insertBefore(b,a.firstChild);d=b.firstChild;f=d.firstChild;e=d.nextSibling.firstChild.firstChild;this.doesNotAddBorder=f.offsetTop!==5;this.doesAddBorderForTableAndCells=e.offsetTop===5;f.style.position="fixed";f.style.top="20px";this.supportsFixedPosition=f.offsetTop===20||f.offsetTop===15;f.style.position=f.style.top="";d.style.overflow="hidden";d.style.position="relative";this.subtractsBorderForOverflowNotVisible=f.offsetTop===-5;this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==j;a.removeChild(b); +c.offset.initialize=c.noop},bodyOffset:function(a){var b=a.offsetTop,d=a.offsetLeft;c.offset.initialize();if(c.offset.doesNotIncludeMarginInBodyOffset){b+=parseFloat(c.curCSS(a,"marginTop",true))||0;d+=parseFloat(c.curCSS(a,"marginLeft",true))||0}return{top:b,left:d}},setOffset:function(a,b,d){if(/static/.test(c.curCSS(a,"position")))a.style.position="relative";var f=c(a),e=f.offset(),j=parseInt(c.curCSS(a,"top",true),10)||0,i=parseInt(c.curCSS(a,"left",true),10)||0;if(c.isFunction(b))b=b.call(a, +d,e);d={top:b.top-e.top+j,left:b.left-e.left+i};"using"in b?b.using.call(a,d):f.css(d)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),f=/^body|html$/i.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.curCSS(a,"marginTop",true))||0;d.left-=parseFloat(c.curCSS(a,"marginLeft",true))||0;f.top+=parseFloat(c.curCSS(b[0],"borderTopWidth",true))||0;f.left+=parseFloat(c.curCSS(b[0],"borderLeftWidth",true))||0;return{top:d.top- +f.top,left:d.left-f.left}},offsetParent:function(){return this.map(function(){for(var a=this.offsetParent||s.body;a&&!/^body|html$/i.test(a.nodeName)&&c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(f){var e=this[0],j;if(!e)return null;if(f!==w)return this.each(function(){if(j=wa(this))j.scrollTo(!a?f:c(j).scrollLeft(),a?f:c(j).scrollTop());else this[d]=f});else return(j=wa(e))?"pageXOffset"in j?j[a?"pageYOffset": +"pageXOffset"]:c.support.boxModel&&j.document.documentElement[d]||j.document.body[d]:e[d]}});c.each(["Height","Width"],function(a,b){var d=b.toLowerCase();c.fn["inner"+b]=function(){return this[0]?c.css(this[0],d,false,"padding"):null};c.fn["outer"+b]=function(f){return this[0]?c.css(this[0],d,false,f?"margin":"border"):null};c.fn[d]=function(f){var e=this[0];if(!e)return f==null?null:this;if(c.isFunction(f))return this.each(function(j){var i=c(this);i[d](f.call(this,j,i[d]()))});return"scrollTo"in +e&&e.document?e.document.compatMode==="CSS1Compat"&&e.document.documentElement["client"+b]||e.document.body["client"+b]:e.nodeType===9?Math.max(e.documentElement["client"+b],e.body["scroll"+b],e.documentElement["scroll"+b],e.body["offset"+b],e.documentElement["offset"+b]):f===w?c.css(e,d):this.css(d,typeof f==="string"?f:f+"px")}});A.jQuery=A.$=c})(window); diff --git a/src/server/master/web_ui/application/web_ui/static/assets/js/jquery.functions.js b/src/server/master/web_ui/application/web_ui/static/assets/js/jquery.functions.js new file mode 100644 index 0000000..f2e03f4 --- /dev/null +++ b/src/server/master/web_ui/application/web_ui/static/assets/js/jquery.functions.js @@ -0,0 +1,53 @@ +/* + * jQuery Innerfade + * This plugin is used on the homepage. +*/ + + +/* Application Showcase */ +jQuery.noConflict(); + jQuery(document).ready(function(){ + /*jQuery('#slider').innerfade({ + animationtype: 'fade', + speed: '3000', + timeout: 3000, + type: 'sequence', + containerheight: 'auto' + });*/ +}); + +/* Client Testimonials */ +jQuery.noConflict(); + jQuery(document).ready(function(){ + jQuery('#slider2').innerfade({ + animationtype: 'fade', + speed: '3000', + timeout: 5000, + type: 'sequence', + containerheight: 'auto' + }); +}); + +/* Screenshots */ +jQuery.noConflict(); + jQuery(document).ready(function(){ + jQuery('.boxgrid.captionfull').hover(function(){ //On hover... + jQuery(".cover", this).fadeIn("fast"); + }, + function() { //On hover out... + jQuery(".cover", this).fadeOut("fast"); + }); + +}); + +/* Table Style */ +jQuery.noConflict(); + jQuery(document).ready(function(){ + jQuery(".pricing-table tr:odd").addClass("alt"); + }); + +/* Lightbox */ +jQuery.noConflict(); + jQuery(document).ready(function(){ + jQuery('.boxgrid a').lightBox(); + }); diff --git a/src/server/master/web_ui/application/web_ui/static/assets/js/jquery.infieldlabel.js b/src/server/master/web_ui/application/web_ui/static/assets/js/jquery.infieldlabel.js new file mode 100644 index 0000000..343ad8c --- /dev/null +++ b/src/server/master/web_ui/application/web_ui/static/assets/js/jquery.infieldlabel.js @@ -0,0 +1,141 @@ +/* + * In-Field Label jQuery Plugin + * http://fuelyourcoding.com/scripts/infield.html + * + * Copyright (c) 2009 Doug Neiner + * Dual licensed under the MIT and GPL licenses. + * Uses the same license as jQuery, see: + * http://docs.jquery.com/License + * + * @version 0.1 + */ +(function($){ + + $.InFieldLabels = function(label,field, options){ + // To avoid scope issues, use 'base' instead of 'this' + // to reference this class from internal events and functions. + var base = this; + + // Access to jQuery and DOM versions of each element + base.$label = $(label); + base.label = label; + + base.$field = $(field); + base.field = field; + + base.$label.data("InFieldLabels", base); + base.showing = true; + + base.init = function(){ + // Merge supplied options with default options + base.options = $.extend({},$.InFieldLabels.defaultOptions, options); + + // Check if the field is already filled in + if(base.$field.val() != ""){ + base.$label.hide(); + base.showing = false; + }; + + base.$field.focus(function(){ + base.fadeOnFocus(); + }).blur(function(){ + base.checkForEmpty(true); + }).bind('keydown.infieldlabel',function(e){ + // Use of a namespace (.infieldlabel) allows us to + // unbind just this method later + base.hideOnChange(e); + }).change(function(e){ + base.checkForEmpty(); + }).bind('onPropertyChange', function(){ + base.checkForEmpty(); + }); + }; + + // If the label is currently showing + // then fade it down to the amount + // specified in the settings + base.fadeOnFocus = function(){ + if(base.showing){ + base.setOpacity(base.options.fadeOpacity); + }; + }; + + base.setOpacity = function(opacity){ + base.label.style.opacity = opacity; + // base.$label.stop().animate({ opacity: opacity }, base.options.fadeDuration); + base.showing = (opacity > 0.0); + }; + + // Checks for empty as a fail safe + // set blur to true when passing from + // the blur event + base.checkForEmpty = function(blur){ + if(base.$field.val() == ""){ + base.prepForShow(); + base.setOpacity( blur ? 1.0 : base.options.fadeOpacity ); + } else { + base.setOpacity(0.0); + }; + }; + + base.prepForShow = function(e){ + if(!base.showing) { + // Prepare for a animate in... + base.$label.css({opacity: 0.0}).show(); + + // Reattach the keydown event + base.$field.bind('keydown.infieldlabel',function(e){ + base.hideOnChange(e); + }); + }; + }; + + base.hideOnChange = function(e){ + if( + (e.keyCode == 16) || // Skip Shift + (e.keyCode == 9) // Skip Tab + ) return; + + if(base.showing){ + base.$label.hide(); + base.showing = false; + }; + + // Remove keydown event to save on CPU processing + base.$field.unbind('keydown.infieldlabel'); + }; + + // Run the initialization method + base.init(); + }; + + $.InFieldLabels.defaultOptions = { + fadeOpacity: 0.5, // Once a field has focus, how transparent should the label be + // fadeDuration: 300 // How long should it take to animate from 1.0 opacity to the fadeOpacity + }; + + + $.fn.inFieldLabels = function(options){ + return this.each(function(){ + // Find input or textarea based on for= attribute + // The for attribute on the label must contain the ID + // of the input or textarea element + var for_attr = $(this).attr('for'); + if( !for_attr ) return; // Nothing to attach, since the for field wasn't used + + + // Find the referenced input or textarea element + var $field = $( + "input#" + for_attr + "[type='text']," + + "input#" + for_attr + "[type='password']," + + "textarea#" + for_attr + ); + + if( $field.length == 0) return; // Again, nothing to attach + + // Only create object for input[text], input[password], or textarea + (new $.InFieldLabels(this, $field[0], options)); + }); + }; + +})(jQuery); diff --git a/src/server/master/web_ui/application/web_ui/static/assets/js/jquery.innerfade.js b/src/server/master/web_ui/application/web_ui/static/assets/js/jquery.innerfade.js new file mode 100644 index 0000000..c6e5181 --- /dev/null +++ b/src/server/master/web_ui/application/web_ui/static/assets/js/jquery.innerfade.js @@ -0,0 +1,128 @@ +/* ========================================================= + +// jquery.innerfade.js + +// Datum: 2008-02-14 +// Firma: Medienfreunde Hofmann & Baldes GbR +// Author: Torsten Baldes +// Mail: t.baldes@medienfreunde.com +// Web: http://medienfreunde.com + +// based on the work of Matt Oakes http://portfolio.gizone.co.uk/applications/slideshow/ +// and Ralf S. Engelschall http://trainofthoughts.org/ + + * + *
      + *
    • content 1
    • + *
    • content 2
    • + *
    • content 3
    • + *
    + * + * $('#news').innerfade({ + * animationtype: Type of animation 'fade' or 'slide' (Default: 'fade'), + * speed: Fading-/Sliding-Speed in milliseconds or keywords (slow, normal or fast) (Default: 'normal'), + * timeout: Time between the fades in milliseconds (Default: '2000'), + * type: Type of slideshow: 'sequence', 'random' or 'random_start' (Default: 'sequence'), + * containerheight: Height of the containing element in any css-height-value (Default: 'auto'), + * runningclass: CSS-Class which the container getÕs applied (Default: 'innerfade'), + * children: optional children selector (Default: null) + * }); + * + +// ========================================================= */ + + +(function($) { + + $.fn.innerfade = function(options) { + return this.each(function() { + $.innerfade(this, options); + }); + }; + + $.innerfade = function(container, options) { + var settings = { + 'animationtype': 'fade', + 'speed': 'normal', + 'type': 'sequence', + 'timeout': 2000, + 'containerheight': 'auto', + 'runningclass': 'innerfade', + 'children': null + }; + if (options) + $.extend(settings, options); + if (settings.children === null) + var elements = $(container).children(); + else + var elements = $(container).children(settings.children); + if (elements.length > 1) { + $(container).css('position', 'relative').css('height', settings.containerheight).addClass(settings.runningclass); + for (var i = 0; i < elements.length; i++) { + $(elements[i]).css('z-index', String(elements.length-i)).css('position', 'absolute').hide(); + }; + if (settings.type == "sequence") { + setTimeout(function() { + $.innerfade.next(elements, settings, 1, 0); + }, settings.timeout); + $(elements[0]).show(); + } else if (settings.type == "random") { + var last = Math.floor ( Math.random () * ( elements.length ) ); + setTimeout(function() { + do { + current = Math.floor ( Math.random ( ) * ( elements.length ) ); + } while (last == current ); + $.innerfade.next(elements, settings, current, last); + }, settings.timeout); + $(elements[last]).show(); + } else if ( settings.type == 'random_start' ) { + settings.type = 'sequence'; + var current = Math.floor ( Math.random () * ( elements.length ) ); + setTimeout(function(){ + $.innerfade.next(elements, settings, (current + 1) % elements.length, current); + }, settings.timeout); + $(elements[current]).show(); + } else { + alert('Innerfade-Type must either be \'sequence\', \'random\' or \'random_start\''); + } + } + }; + + $.innerfade.next = function(elements, settings, current, last) { + if (settings.animationtype == 'slide') { + $(elements[last]).slideUp(settings.speed); + $(elements[current]).slideDown(settings.speed); + } else if (settings.animationtype == 'fade') { + $(elements[last]).fadeOut(settings.speed); + $(elements[current]).fadeIn(settings.speed, function() { + removeFilter($(this)[0]); + }); + } else + alert('Innerfade-animationtype must either be \'slide\' or \'fade\''); + if (settings.type == "sequence") { + if ((current + 1) < elements.length) { + current = current + 1; + last = current - 1; + } else { + current = 0; + last = elements.length - 1; + } + } else if (settings.type == "random") { + last = current; + while (current == last) + current = Math.floor(Math.random() * elements.length); + } else + alert('Innerfade-Type must either be \'sequence\', \'random\' or \'random_start\''); + setTimeout((function() { + $.innerfade.next(elements, settings, current, last); + }), settings.timeout); + }; + +})(jQuery); + +// **** remove Opacity-Filter in ie **** +function removeFilter(element) { + if(element.style.removeAttribute){ + element.style.removeAttribute('filter'); + } +} diff --git a/src/server/master/web_ui/application/web_ui/static/assets/js/pngfix.js b/src/server/master/web_ui/application/web_ui/static/assets/js/pngfix.js new file mode 100644 index 0000000..700a68c --- /dev/null +++ b/src/server/master/web_ui/application/web_ui/static/assets/js/pngfix.js @@ -0,0 +1,330 @@ +/** +* DD_belatedPNG: Adds IE6 support: PNG images for CSS background-image and HTML . +* Author: Drew Diller +* Email: drew.diller@gmail.com +* URL: http://www.dillerdesign.com/experiment/DD_belatedPNG/ +* Version: 0.0.8a +* Licensed under the MIT License: http://dillerdesign.com/experiment/DD_belatedPNG/#license +* +* Example usage: +* DD_belatedPNG.fix('.png_bg'); // argument is a CSS selector +* DD_belatedPNG.fixPng( someNode ); // argument is an HTMLDomElement +**/ + +/* +PLEASE READ: +Absolutely everything in this script is SILLY. I know this. IE's rendering of certain pixels doesn't make sense, so neither does this code! +*/ + +var DD_belatedPNG = { + ns: 'DD_belatedPNG', + imgSize: {}, + delay: 10, + nodesFixed: 0, + createVmlNameSpace: function () { /* enable VML */ + if (document.namespaces && !document.namespaces[this.ns]) { + document.namespaces.add(this.ns, 'urn:schemas-microsoft-com:vml'); + } + }, + createVmlStyleSheet: function () { /* style VML, enable behaviors */ + /* + Just in case lots of other developers have added + lots of other stylesheets using document.createStyleSheet + and hit the 31-limit mark, let's not use that method! + further reading: http://msdn.microsoft.com/en-us/library/ms531194(VS.85).aspx + */ + var screenStyleSheet, printStyleSheet; + screenStyleSheet = document.createElement('style'); + screenStyleSheet.setAttribute('media', 'screen'); + document.documentElement.firstChild.insertBefore(screenStyleSheet, document.documentElement.firstChild.firstChild); + if (screenStyleSheet.styleSheet) { + screenStyleSheet = screenStyleSheet.styleSheet; + screenStyleSheet.addRule(this.ns + '\\:*', '{behavior:url(#default#VML)}'); + screenStyleSheet.addRule(this.ns + '\\:shape', 'position:absolute;'); + screenStyleSheet.addRule('img.' + this.ns + '_sizeFinder', 'behavior:none; border:none; position:absolute; z-index:-1; top:-10000px; visibility:hidden;'); /* large negative top value for avoiding vertical scrollbars for large images, suggested by James O'Brien, http://www.thanatopsic.org/hendrik/ */ + this.screenStyleSheet = screenStyleSheet; + + /* Add a print-media stylesheet, for preventing VML artifacts from showing up in print (including preview). */ + /* Thanks to RŽmi PrŽvost for automating this! */ + printStyleSheet = document.createElement('style'); + printStyleSheet.setAttribute('media', 'print'); + document.documentElement.firstChild.insertBefore(printStyleSheet, document.documentElement.firstChild.firstChild); + printStyleSheet = printStyleSheet.styleSheet; + printStyleSheet.addRule(this.ns + '\\:*', '{display: none !important;}'); + printStyleSheet.addRule('img.' + this.ns + '_sizeFinder', '{display: none !important;}'); + } + }, + readPropertyChange: function () { + var el, display, v; + el = event.srcElement; + if (!el.vmlInitiated) { + return; + } + if (event.propertyName.search('background') != -1 || event.propertyName.search('border') != -1) { + DD_belatedPNG.applyVML(el); + } + if (event.propertyName == 'style.display') { + display = (el.currentStyle.display == 'none') ? 'none' : 'block'; + for (v in el.vml) { + if (el.vml.hasOwnProperty(v)) { + el.vml[v].shape.style.display = display; + } + } + } + if (event.propertyName.search('filter') != -1) { + DD_belatedPNG.vmlOpacity(el); + } + }, + vmlOpacity: function (el) { + if (el.currentStyle.filter.search('lpha') != -1) { + var trans = el.currentStyle.filter; + trans = parseInt(trans.substring(trans.lastIndexOf('=')+1, trans.lastIndexOf(')')), 10)/100; + el.vml.color.shape.style.filter = el.currentStyle.filter; /* complete guesswork */ + el.vml.image.fill.opacity = trans; /* complete guesswork */ + } + }, + handlePseudoHover: function (el) { + setTimeout(function () { /* wouldn't work as intended without setTimeout */ + DD_belatedPNG.applyVML(el); + }, 1); + }, + /** + * This is the method to use in a document. + * @param {String} selector - REQUIRED - a CSS selector, such as '#doc .container' + **/ + fix: function (selector) { + if (this.screenStyleSheet) { + var selectors, i; + selectors = selector.split(','); /* multiple selectors supported, no need for multiple calls to this anymore */ + for (i=0; i size.H) { + c.B = size.H; + } + el.vml.image.shape.style.clip = 'rect('+c.T+'px '+(c.R+fudge)+'px '+c.B+'px '+(c.L+fudge)+'px)'; + } + else { + el.vml.image.shape.style.clip = 'rect('+dC.T+'px '+dC.R+'px '+dC.B+'px '+dC.L+'px)'; + } + }, + figurePercentage: function (bg, size, axis, position) { + var horizontal, fraction; + fraction = true; + horizontal = (axis == 'X'); + switch(position) { + case 'left': + case 'top': + bg[axis] = 0; + break; + case 'center': + bg[axis] = 0.5; + break; + case 'right': + case 'bottom': + bg[axis] = 1; + break; + default: + if (position.search('%') != -1) { + bg[axis] = parseInt(position, 10) / 100; + } + else { + fraction = false; + } + } + bg[axis] = Math.ceil( fraction ? ( (size[horizontal?'W': 'H'] * bg[axis]) - (size[horizontal?'w': 'h'] * bg[axis]) ) : parseInt(position, 10) ); + if (bg[axis] % 2 === 0) { + bg[axis]++; + } + return bg[axis]; + }, + fixPng: function (el) { + el.style.behavior = 'none'; + var lib, els, nodeStr, v, e; + if (el.nodeName == 'BODY' || el.nodeName == 'TD' || el.nodeName == 'TR') { /* elements not supported yet */ + return; + } + el.isImg = false; + if (el.nodeName == 'IMG') { + if(el.src.toLowerCase().search(/\.png$/) != -1) { + el.isImg = true; + el.style.visibility = 'hidden'; + } + else { + return; + } + } + else if (el.currentStyle.backgroundImage.toLowerCase().search('.png') == -1) { + return; + } + lib = DD_belatedPNG; + el.vml = {color: {}, image: {}}; + els = {shape: {}, fill: {}}; + for (v in el.vml) { + if (el.vml.hasOwnProperty(v)) { + for (e in els) { + if (els.hasOwnProperty(e)) { + nodeStr = lib.ns + ':' + e; + el.vml[v][e] = document.createElement(nodeStr); + } + } + el.vml[v].shape.stroked = false; + el.vml[v].shape.appendChild(el.vml[v].fill); + el.parentNode.insertBefore(el.vml[v].shape, el); + } + } + el.vml.image.shape.fillcolor = 'none'; /* Don't show blank white shapeangle when waiting for image to load. */ + el.vml.image.fill.type = 'tile'; /* Makes image show up. */ + el.vml.color.fill.on = false; /* Actually going to apply vml element's style.backgroundColor, so hide the whiteness. */ + lib.attachHandlers(el); + lib.giveLayout(el); + lib.giveLayout(el.offsetParent); + el.vmlInitiated = true; + lib.applyVML(el); /* Render! */ + } +}; +try { + document.execCommand("BackgroundImageCache", false, true); /* TredoSoft Multiple IE doesn't like this, so try{} it */ +} catch(r) {} +DD_belatedPNG.createVmlNameSpace(); +DD_belatedPNG.createVmlStyleSheet(); + + DD_belatedPNG.fix('html, body, h1, h2, h3, h4, a, img, div.container, div.header h1, div.showcase, div.page-title, div.laptop, div.content-top, div.content, div.content-btm, div.footer, div.twitter'); diff --git a/src/server/master/web_ui/application/web_ui/static/assets/lightbox/css/jquery.lightbox-0.5.css b/src/server/master/web_ui/application/web_ui/static/assets/lightbox/css/jquery.lightbox-0.5.css new file mode 100644 index 0000000..a0c5336 --- /dev/null +++ b/src/server/master/web_ui/application/web_ui/static/assets/lightbox/css/jquery.lightbox-0.5.css @@ -0,0 +1,101 @@ +/** + * jQuery lightBox plugin + * This jQuery plugin was inspired and based on Lightbox 2 by Lokesh Dhakar (http://www.huddletogether.com/projects/lightbox2/) + * and adapted to me for use like a plugin from jQuery. + * @name jquery-lightbox-0.5.css + * @author Leandro Vieira Pinho - http://leandrovieira.com + * @version 0.5 + * @date April 11, 2008 + * @category jQuery plugin + * @copyright (c) 2008 Leandro Vieira Pinho (leandrovieira.com) + * @license CC Attribution-No Derivative Works 2.5 Brazil - http://creativecommons.org/licenses/by-nd/2.5/br/deed.en_US + * @example Visit http://leandrovieira.com/projects/jquery/lightbox/ for more informations about this jQuery plugin + */ +#jquery-overlay { + position: absolute; + top: 0; + left: 0; + z-index: 90; + width: 100%; + height: 500px; +} +#jquery-lightbox { + position: absolute; + top: 0; + left: 0; + width: 100%; + z-index: 100; + text-align: center; + line-height: 0; +} +#jquery-lightbox a img, #jquery-lightbox a:hover { border: none !important; } +#lightbox-container-image-box { + position: relative; + background-color: #fff; + width: 250px; + height: 250px; + margin: 0 auto; +} +#lightbox-container-image { padding: 10px; } +#lightbox-loading { + position: absolute; + top: 40%; + left: 0%; + height: 25%; + width: 100%; + text-align: center; + line-height: 0; +} +#lightbox-nav { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + z-index: 10; +} +#lightbox-container-image-box > #lightbox-nav { left: 0; } +#lightbox-nav a { outline: none;} +#lightbox-nav-btnPrev, #lightbox-nav-btnNext { + width: 49%; + height: 100%; + zoom: 1; + display: block; +} +#lightbox-nav-btnPrev { + left: 0; + float: left; +} +#lightbox-nav-btnNext { + right: 0; + float: right; +} +#lightbox-container-image-data-box { + font: 12px Verdana, Helvetica, sans-serif; + background-color: #fff; + margin: 0 auto; + line-height: 1.4em; + overflow: auto; + width: 100%; + padding: 0 10px 0; +} +#lightbox-container-image-data { + padding: 0 10px; + color: #666; +} +#lightbox-container-image-data #lightbox-image-details { + width: 70%; + float: left; + text-align: left; +} +#lightbox-image-details-caption { font-weight: bold; } +#lightbox-image-details-currentNumber { + display: block; + clear: left; + padding-bottom: 1.0em; +} +#lightbox-secNav-btnClose { + width: 66px; + float: right; + padding-bottom: 0.7em; +} \ No newline at end of file diff --git a/src/server/master/web_ui/application/web_ui/static/assets/lightbox/js/jquery.lightbox-0.5.js b/src/server/master/web_ui/application/web_ui/static/assets/lightbox/js/jquery.lightbox-0.5.js new file mode 100644 index 0000000..f664240 --- /dev/null +++ b/src/server/master/web_ui/application/web_ui/static/assets/lightbox/js/jquery.lightbox-0.5.js @@ -0,0 +1,472 @@ +/** + * jQuery lightBox plugin + * This jQuery plugin was inspired and based on Lightbox 2 by Lokesh Dhakar (http://www.huddletogether.com/projects/lightbox2/) + * and adapted to me for use like a plugin from jQuery. + * @name jquery-lightbox-0.5.js + * @author Leandro Vieira Pinho - http://leandrovieira.com + * @version 0.5 + * @date April 11, 2008 + * @category jQuery plugin + * @copyright (c) 2008 Leandro Vieira Pinho (leandrovieira.com) + * @license CC Attribution-No Derivative Works 2.5 Brazil - http://creativecommons.org/licenses/by-nd/2.5/br/deed.en_US + * @example Visit http://leandrovieira.com/projects/jquery/lightbox/ for more informations about this jQuery plugin + */ + +// Offering a Custom Alias suport - More info: http://docs.jquery.com/Plugins/Authoring#Custom_Alias +(function($) { + /** + * $ is an alias to jQuery object + * + */ + $.fn.lightBox = function(settings) { + // Settings to configure the jQuery lightBox plugin how you like + settings = jQuery.extend({ + // Configuration related to overlay + overlayBgColor: '#000', // (string) Background color to overlay; inform a hexadecimal value like: #RRGGBB. Where RR, GG, and BB are the hexadecimal values for the red, green, and blue values of the color. + overlayOpacity: 0.8, // (integer) Opacity value to overlay; inform: 0.X. Where X are number from 0 to 9 + // Configuration related to navigation + fixedNavigation: false, // (boolean) Boolean that informs if the navigation (next and prev button) will be fixed or not in the interface. + // Configuration related to images + imageLoading: 'images/lightbox-ico-loading.gif', // (string) Path and the name of the loading icon + imageBtnPrev: 'images/lightbox-btn-prev.gif', // (string) Path and the name of the prev button image + imageBtnNext: 'images/lightbox-btn-next.gif', // (string) Path and the name of the next button image + imageBtnClose: 'images/lightbox-btn-close.gif', // (string) Path and the name of the close btn + imageBlank: 'images/lightbox-blank.gif', // (string) Path and the name of a blank image (one pixel) + // Configuration related to container image box + containerBorderSize: 10, // (integer) If you adjust the padding in the CSS for the container, #lightbox-container-image-box, you will need to update this value + containerResizeSpeed: 400, // (integer) Specify the resize duration of container image. These number are miliseconds. 400 is default. + // Configuration related to texts in caption. For example: Image 2 of 8. You can alter either "Image" and "of" texts. + txtImage: 'Image', // (string) Specify text "Image" + txtOf: 'of', // (string) Specify text "of" + // Configuration related to keyboard navigation + keyToClose: 'c', // (string) (c = close) Letter to close the jQuery lightBox interface. Beyond this letter, the letter X and the SCAPE key is used to. + keyToPrev: 'p', // (string) (p = previous) Letter to show the previous image + keyToNext: 'n', // (string) (n = next) Letter to show the next image. + // Don´t alter these variables in any way + imageArray: [], + activeImage: 0 + },settings); + // Caching the jQuery object with all elements matched + var jQueryMatchedObj = this; // This, in this context, refer to jQuery object + /** + * Initializing the plugin calling the start function + * + * @return boolean false + */ + function _initialize() { + _start(this,jQueryMatchedObj); // This, in this context, refer to object (link) which the user have clicked + return false; // Avoid the browser following the link + } + /** + * Start the jQuery lightBox plugin + * + * @param object objClicked The object (link) whick the user have clicked + * @param object jQueryMatchedObj The jQuery object with all elements matched + */ + function _start(objClicked,jQueryMatchedObj) { + // Hime some elements to avoid conflict with overlay in IE. These elements appear above the overlay. + $('embed, object, select').css({ 'visibility' : 'hidden' }); + // Call the function to create the markup structure; style some elements; assign events in some elements. + _set_interface(); + // Unset total images in imageArray + settings.imageArray.length = 0; + // Unset image active information + settings.activeImage = 0; + // We have an image set? Or just an image? Let´s see it. + if ( jQueryMatchedObj.length == 1 ) { + settings.imageArray.push(new Array(objClicked.getAttribute('href'),objClicked.getAttribute('title'))); + } else { + // Add an Array (as many as we have), with href and title atributes, inside the Array that storage the images references + for ( var i = 0; i < jQueryMatchedObj.length; i++ ) { + settings.imageArray.push(new Array(jQueryMatchedObj[i].getAttribute('href'),jQueryMatchedObj[i].getAttribute('title'))); + } + } + while ( settings.imageArray[settings.activeImage][0] != objClicked.getAttribute('href') ) { + settings.activeImage++; + } + // Call the function that prepares image exibition + _set_image_to_view(); + } + /** + * Create the jQuery lightBox plugin interface + * + * The HTML markup will be like that: +
    +
    + + +
    + * + */ + function _set_interface() { + // Apply the HTML markup into body tag + $('body').append('
    '); + // Get page sizes + var arrPageSizes = ___getPageSize(); + // Style overlay and show it + $('#jquery-overlay').css({ + backgroundColor: settings.overlayBgColor, + opacity: settings.overlayOpacity, + width: arrPageSizes[0], + height: arrPageSizes[1] + }).fadeIn(); + // Get page scroll + var arrPageScroll = ___getPageScroll(); + // Calculate top and left offset for the jquery-lightbox div object and show it + $('#jquery-lightbox').css({ + top: arrPageScroll[1] + (arrPageSizes[3] / 10), + left: arrPageScroll[0] + }).show(); + // Assigning click events in elements to close overlay + $('#jquery-overlay,#jquery-lightbox').click(function() { + _finish(); + }); + // Assign the _finish function to lightbox-loading-link and lightbox-secNav-btnClose objects + $('#lightbox-loading-link,#lightbox-secNav-btnClose').click(function() { + _finish(); + return false; + }); + // If window was resized, calculate the new overlay dimensions + $(window).resize(function() { + // Get page sizes + var arrPageSizes = ___getPageSize(); + // Style overlay and show it + $('#jquery-overlay').css({ + width: arrPageSizes[0], + height: arrPageSizes[1] + }); + // Get page scroll + var arrPageScroll = ___getPageScroll(); + // Calculate top and left offset for the jquery-lightbox div object and show it + $('#jquery-lightbox').css({ + top: arrPageScroll[1] + (arrPageSizes[3] / 10), + left: arrPageScroll[0] + }); + }); + } + /** + * Prepares image exibition; doing a image´s preloader to calculate it´s size + * + */ + function _set_image_to_view() { // show the loading + // Show the loading + $('#lightbox-loading').show(); + if ( settings.fixedNavigation ) { + $('#lightbox-image,#lightbox-container-image-data-box,#lightbox-image-details-currentNumber').hide(); + } else { + // Hide some elements + $('#lightbox-image,#lightbox-nav,#lightbox-nav-btnPrev,#lightbox-nav-btnNext,#lightbox-container-image-data-box,#lightbox-image-details-currentNumber').hide(); + } + // Image preload process + var objImagePreloader = new Image(); + objImagePreloader.onload = function() { + $('#lightbox-image').attr('src',settings.imageArray[settings.activeImage][0]); + // Perfomance an effect in the image container resizing it + _resize_container_image_box(objImagePreloader.width,objImagePreloader.height); + // clear onLoad, IE behaves irratically with animated gifs otherwise + objImagePreloader.onload=function(){}; + }; + objImagePreloader.src = settings.imageArray[settings.activeImage][0]; + }; + /** + * Perfomance an effect in the image container resizing it + * + * @param integer intImageWidth The image´s width that will be showed + * @param integer intImageHeight The image´s height that will be showed + */ + function _resize_container_image_box(intImageWidth,intImageHeight) { + // Get current width and height + var intCurrentWidth = $('#lightbox-container-image-box').width(); + var intCurrentHeight = $('#lightbox-container-image-box').height(); + // Get the width and height of the selected image plus the padding + var intWidth = (intImageWidth + (settings.containerBorderSize * 2)); // Plus the image´s width and the left and right padding value + var intHeight = (intImageHeight + (settings.containerBorderSize * 2)); // Plus the image´s height and the left and right padding value + // Diferences + var intDiffW = intCurrentWidth - intWidth; + var intDiffH = intCurrentHeight - intHeight; + // Perfomance the effect + $('#lightbox-container-image-box').animate({ width: intWidth, height: intHeight },settings.containerResizeSpeed,function() { _show_image(); }); + if ( ( intDiffW == 0 ) && ( intDiffH == 0 ) ) { + if ( $.browser.msie ) { + ___pause(250); + } else { + ___pause(100); + } + } + $('#lightbox-container-image-data-box').css({ width: intImageWidth }); + $('#lightbox-nav-btnPrev,#lightbox-nav-btnNext').css({ height: intImageHeight + (settings.containerBorderSize * 2) }); + }; + /** + * Show the prepared image + * + */ + function _show_image() { + $('#lightbox-loading').hide(); + $('#lightbox-image').fadeIn(function() { + _show_image_data(); + _set_navigation(); + }); + _preload_neighbor_images(); + }; + /** + * Show the image information + * + */ + function _show_image_data() { + $('#lightbox-container-image-data-box').slideDown('fast'); + $('#lightbox-image-details-caption').hide(); + if ( settings.imageArray[settings.activeImage][1] ) { + $('#lightbox-image-details-caption').html(settings.imageArray[settings.activeImage][1]).show(); + } + // If we have a image set, display 'Image X of X' + if ( settings.imageArray.length > 1 ) { + $('#lightbox-image-details-currentNumber').html(settings.txtImage + ' ' + ( settings.activeImage + 1 ) + ' ' + settings.txtOf + ' ' + settings.imageArray.length).show(); + } + } + /** + * Display the button navigations + * + */ + function _set_navigation() { + $('#lightbox-nav').show(); + + // Instead to define this configuration in CSS file, we define here. And it´s need to IE. Just. + $('#lightbox-nav-btnPrev,#lightbox-nav-btnNext').css({ 'background' : 'transparent url(' + settings.imageBlank + ') no-repeat' }); + + // Show the prev button, if not the first image in set + if ( settings.activeImage != 0 ) { + if ( settings.fixedNavigation ) { + $('#lightbox-nav-btnPrev').css({ 'background' : 'url(' + settings.imageBtnPrev + ') left 15% no-repeat' }) + .unbind() + .bind('click',function() { + settings.activeImage = settings.activeImage - 1; + _set_image_to_view(); + return false; + }); + } else { + // Show the images button for Next buttons + $('#lightbox-nav-btnPrev').unbind().hover(function() { + $(this).css({ 'background' : 'url(' + settings.imageBtnPrev + ') left 15% no-repeat' }); + },function() { + $(this).css({ 'background' : 'transparent url(' + settings.imageBlank + ') no-repeat' }); + }).show().bind('click',function() { + settings.activeImage = settings.activeImage - 1; + _set_image_to_view(); + return false; + }); + } + } + + // Show the next button, if not the last image in set + if ( settings.activeImage != ( settings.imageArray.length -1 ) ) { + if ( settings.fixedNavigation ) { + $('#lightbox-nav-btnNext').css({ 'background' : 'url(' + settings.imageBtnNext + ') right 15% no-repeat' }) + .unbind() + .bind('click',function() { + settings.activeImage = settings.activeImage + 1; + _set_image_to_view(); + return false; + }); + } else { + // Show the images button for Next buttons + $('#lightbox-nav-btnNext').unbind().hover(function() { + $(this).css({ 'background' : 'url(' + settings.imageBtnNext + ') right 15% no-repeat' }); + },function() { + $(this).css({ 'background' : 'transparent url(' + settings.imageBlank + ') no-repeat' }); + }).show().bind('click',function() { + settings.activeImage = settings.activeImage + 1; + _set_image_to_view(); + return false; + }); + } + } + // Enable keyboard navigation + _enable_keyboard_navigation(); + } + /** + * Enable a support to keyboard navigation + * + */ + function _enable_keyboard_navigation() { + $(document).keydown(function(objEvent) { + _keyboard_action(objEvent); + }); + } + /** + * Disable the support to keyboard navigation + * + */ + function _disable_keyboard_navigation() { + $(document).unbind(); + } + /** + * Perform the keyboard actions + * + */ + function _keyboard_action(objEvent) { + // To ie + if ( objEvent == null ) { + keycode = event.keyCode; + escapeKey = 27; + // To Mozilla + } else { + keycode = objEvent.keyCode; + escapeKey = objEvent.DOM_VK_ESCAPE; + } + // Get the key in lower case form + key = String.fromCharCode(keycode).toLowerCase(); + // Verify the keys to close the ligthBox + if ( ( key == settings.keyToClose ) || ( key == 'x' ) || ( keycode == escapeKey ) ) { + _finish(); + } + // Verify the key to show the previous image + if ( ( key == settings.keyToPrev ) || ( keycode == 37 ) ) { + // If we´re not showing the first image, call the previous + if ( settings.activeImage != 0 ) { + settings.activeImage = settings.activeImage - 1; + _set_image_to_view(); + _disable_keyboard_navigation(); + } + } + // Verify the key to show the next image + if ( ( key == settings.keyToNext ) || ( keycode == 39 ) ) { + // If we´re not showing the last image, call the next + if ( settings.activeImage != ( settings.imageArray.length - 1 ) ) { + settings.activeImage = settings.activeImage + 1; + _set_image_to_view(); + _disable_keyboard_navigation(); + } + } + } + /** + * Preload prev and next images being showed + * + */ + function _preload_neighbor_images() { + if ( (settings.imageArray.length -1) > settings.activeImage ) { + objNext = new Image(); + objNext.src = settings.imageArray[settings.activeImage + 1][0]; + } + if ( settings.activeImage > 0 ) { + objPrev = new Image(); + objPrev.src = settings.imageArray[settings.activeImage -1][0]; + } + } + /** + * Remove jQuery lightBox plugin HTML markup + * + */ + function _finish() { + $('#jquery-lightbox').remove(); + $('#jquery-overlay').fadeOut(function() { $('#jquery-overlay').remove(); }); + // Show some elements to avoid conflict with overlay in IE. These elements appear above the overlay. + $('embed, object, select').css({ 'visibility' : 'visible' }); + } + /** + / THIRD FUNCTION + * getPageSize() by quirksmode.com + * + * @return Array Return an array with page width, height and window width, height + */ + function ___getPageSize() { + var xScroll, yScroll; + if (window.innerHeight && window.scrollMaxY) { + xScroll = window.innerWidth + window.scrollMaxX; + yScroll = window.innerHeight + window.scrollMaxY; + } else if (document.body.scrollHeight > document.body.offsetHeight){ // all but Explorer Mac + xScroll = document.body.scrollWidth; + yScroll = document.body.scrollHeight; + } else { // Explorer Mac...would also work in Explorer 6 Strict, Mozilla and Safari + xScroll = document.body.offsetWidth; + yScroll = document.body.offsetHeight; + } + var windowWidth, windowHeight; + if (self.innerHeight) { // all except Explorer + if(document.documentElement.clientWidth){ + windowWidth = document.documentElement.clientWidth; + } else { + windowWidth = self.innerWidth; + } + windowHeight = self.innerHeight; + } else if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode + windowWidth = document.documentElement.clientWidth; + windowHeight = document.documentElement.clientHeight; + } else if (document.body) { // other Explorers + windowWidth = document.body.clientWidth; + windowHeight = document.body.clientHeight; + } + // for small pages with total height less then height of the viewport + if(yScroll < windowHeight){ + pageHeight = windowHeight; + } else { + pageHeight = yScroll; + } + // for small pages with total width less then width of the viewport + if(xScroll < windowWidth){ + pageWidth = xScroll; + } else { + pageWidth = windowWidth; + } + arrayPageSize = new Array(pageWidth,pageHeight,windowWidth,windowHeight); + return arrayPageSize; + }; + /** + / THIRD FUNCTION + * getPageScroll() by quirksmode.com + * + * @return Array Return an array with x,y page scroll values. + */ + function ___getPageScroll() { + var xScroll, yScroll; + if (self.pageYOffset) { + yScroll = self.pageYOffset; + xScroll = self.pageXOffset; + } else if (document.documentElement && document.documentElement.scrollTop) { // Explorer 6 Strict + yScroll = document.documentElement.scrollTop; + xScroll = document.documentElement.scrollLeft; + } else if (document.body) {// all other Explorers + yScroll = document.body.scrollTop; + xScroll = document.body.scrollLeft; + } + arrayPageScroll = new Array(xScroll,yScroll); + return arrayPageScroll; + }; + /** + * Stop the code execution from a escified time in milisecond + * + */ + function ___pause(ms) { + var date = new Date(); + curDate = null; + do { var curDate = new Date(); } + while ( curDate - date < ms); + }; + // Return the jQuery object for chaining. The unbind method is used to avoid click conflict when the plugin is called more than once + return this.unbind('click').click(_initialize); + }; +})(jQuery); // Call and execute the function immediately passing the jQuery object \ No newline at end of file diff --git a/src/server/master/web_ui/application/web_ui/static/assets/lightbox/js/jquery.lightbox-0.5.min.js b/src/server/master/web_ui/application/web_ui/static/assets/lightbox/js/jquery.lightbox-0.5.min.js new file mode 100644 index 0000000..7e771b4 --- /dev/null +++ b/src/server/master/web_ui/application/web_ui/static/assets/lightbox/js/jquery.lightbox-0.5.min.js @@ -0,0 +1,42 @@ +/** + * jQuery lightBox plugin + * This jQuery plugin was inspired and based on Lightbox 2 by Lokesh Dhakar (http://www.huddletogether.com/projects/lightbox2/) + * and adapted to me for use like a plugin from jQuery. + * @name jquery-lightbox-0.5.js + * @author Leandro Vieira Pinho - http://leandrovieira.com + * @version 0.5 + * @date April 11, 2008 + * @category jQuery plugin + * @copyright (c) 2008 Leandro Vieira Pinho (leandrovieira.com) + * @license CC Attribution-No Derivative Works 2.5 Brazil - http://creativecommons.org/licenses/by-nd/2.5/br/deed.en_US + * @example Visit http://leandrovieira.com/projects/jquery/lightbox/ for more informations about this jQuery plugin + */ +(function($){$.fn.lightBox=function(settings){settings=jQuery.extend({overlayBgColor:'#000',overlayOpacity:0.8,fixedNavigation:false,imageLoading:'images/lightbox-ico-loading.gif',imageBtnPrev:'images/lightbox-btn-prev.gif',imageBtnNext:'images/lightbox-btn-next.gif',imageBtnClose:'images/lightbox-btn-close.gif',imageBlank:'images/lightbox-blank.gif',containerBorderSize:10,containerResizeSpeed:400,txtImage:'Image',txtOf:'of',keyToClose:'c',keyToPrev:'p',keyToNext:'n',imageArray:[],activeImage:0},settings);var jQueryMatchedObj=this;function _initialize(){_start(this,jQueryMatchedObj);return false;} +function _start(objClicked,jQueryMatchedObj){$('embed, object, select').css({'visibility':'hidden'});_set_interface();settings.imageArray.length=0;settings.activeImage=0;if(jQueryMatchedObj.length==1){settings.imageArray.push(new Array(objClicked.getAttribute('href'),objClicked.getAttribute('title')));}else{for(var i=0;i