From 9742842795e964c7f260aec831665d5cb28cd420 Mon Sep 17 00:00:00 2001 From: Gabriel Hurley Date: Mon, 31 Oct 2011 11:31:05 -0700 Subject: Re-architects the OpenStack Dashboard for modularity and extensibility. Implements blueprint extensible-architecture. Implements blueprint improve-dev-documentation. Implements blueprint gettext-everywhere. Implements blueprint sphinx-docs. Complete re-architecture of the dashboard to transform it from a standalone django-openstack app to a Horizon framework for building dashboards. See the docs for more information. Incidentally fixes the following bugs: Fixes bug 845868 -- no PEP8 violations. Fixes bug 766096 -- the dashboard can now be installed at any arbitrary URL. Fixes bug 879111 -- tenant id is now controlled solely by the tenant switcher, not the url (which was disregarded anyway) Fixes bug 794754 -- output of venv installation is considerably reduced. Due to the scale and scope of this patch I recommend reviewing it on github: https://github.com/gabrielhurley/horizon/tree/extensible_architecture Change-Id: I8e63f7ea235f904247df40c33cb66338d973df9e --- .bzrignore | 16 +- .gitignore | 23 +- README | 34 +- django-openstack/LICENSE | 176 -- django-openstack/Makefile | 43 - django-openstack/README | 56 - django-openstack/bootstrap.py | 260 --- django-openstack/buildout.cfg | 125 -- django-openstack/django_openstack/__init__.py | 0 django-openstack/django_openstack/api.py | 1093 ---------- django-openstack/django_openstack/auth/__init__.py | 0 django-openstack/django_openstack/auth/urls.py | 30 - django-openstack/django_openstack/auth/views.py | 176 -- .../django_openstack/context_processors.py | 48 - django-openstack/django_openstack/dash/__init__.py | 0 django-openstack/django_openstack/dash/urls.py | 118 - .../django_openstack/dash/views/__init__.py | 0 .../django_openstack/dash/views/containers.py | 95 - .../django_openstack/dash/views/floating_ips.py | 181 -- .../django_openstack/dash/views/images.py | 330 --- .../django_openstack/dash/views/instances.py | 321 --- .../django_openstack/dash/views/keypairs.py | 138 -- .../django_openstack/dash/views/networks.py | 252 --- .../django_openstack/dash/views/objects.py | 195 -- .../django_openstack/dash/views/ports.py | 208 -- .../django_openstack/dash/views/security_groups.py | 203 -- .../django_openstack/dash/views/snapshots.py | 120 - .../django_openstack/dash/views/volumes.py | 182 -- django-openstack/django_openstack/decorators.py | 42 - django-openstack/django_openstack/exceptions.py | 8 - django-openstack/django_openstack/forms.py | 187 -- .../locale/es/LC_MESSAGES/django.mo | Bin 425 -> 0 bytes .../locale/es/LC_MESSAGES/django.po | 1948 ----------------- .../locale/fr/LC_MESSAGES/django.mo | Bin 420 -> 0 bytes .../locale/fr/LC_MESSAGES/django.po | 1948 ----------------- .../locale/ja/LC_MESSAGES/django.mo | Bin 2564 -> 0 bytes .../locale/ja/LC_MESSAGES/django.po | 2268 ------------------- .../locale/pl/LC_MESSAGES/django.mo | Bin 936 -> 0 bytes .../locale/pl/LC_MESSAGES/django.po | 2260 ------------------- .../locale/pt/LC_MESSAGES/django.mo | Bin 382 -> 0 bytes .../locale/pt/LC_MESSAGES/django.po | 1947 ----------------- .../locale/zh-cn/LC_MESSAGES/django.mo | Bin 382 -> 0 bytes .../locale/zh-cn/LC_MESSAGES/django.po | 1947 ----------------- .../locale/zh-tw/LC_MESSAGES/django.mo | Bin 382 -> 0 bytes .../locale/zh-tw/LC_MESSAGES/django.po | 1947 ----------------- .../django_openstack/middleware/__init__.py | 0 .../django_openstack/middleware/keystone.py | 78 - django-openstack/django_openstack/models.py | 23 - django-openstack/django_openstack/signals.py | 50 - .../django_openstack/syspanel/__init__.py | 0 .../django_openstack/syspanel/forms.py | 29 - django-openstack/django_openstack/syspanel/urls.py | 78 - .../django_openstack/syspanel/views/__init__.py | 0 .../django_openstack/syspanel/views/flavors.py | 122 -- .../django_openstack/syspanel/views/images.py | 251 --- .../django_openstack/syspanel/views/instances.py | 263 --- .../django_openstack/syspanel/views/quotas.py | 48 - .../django_openstack/syspanel/views/services.py | 118 - .../django_openstack/syspanel/views/tenants.py | 308 --- .../django_openstack/syspanel/views/users.py | 249 --- .../templates/django_openstack/auth/_login.html | 17 - .../templates/django_openstack/auth/_switch.html | 17 - .../django_openstack/common/_page_header.html | 21 - .../django_openstack/common/_sidebar_module.html | 10 - .../django_openstack/common/instances/_reboot.html | 9 - .../common/instances/_terminate.html | 9 - .../templates/django_openstack/dash/_sidebar.html | 28 - .../templates/django_openstack/dash/base.html | 20 - .../django_openstack/dash/containers/_delete.html | 9 - .../django_openstack/dash/containers/_form.html | 11 - .../django_openstack/dash/containers/_list.html | 29 - .../django_openstack/dash/containers/create.html | 28 - .../django_openstack/dash/containers/index.html | 19 - .../dash/floating_ips/_allocate.html | 8 - .../dash/floating_ips/_associate.html | 17 - .../dash/floating_ips/_disassociate.html | 9 - .../django_openstack/dash/floating_ips/_list.html | 34 - .../dash/floating_ips/_release.html | 9 - .../dash/floating_ips/associate.html | 27 - .../django_openstack/dash/floating_ips/index.html | 26 - .../django_openstack/dash/images/_delete.html | 9 - .../django_openstack/dash/images/_form.html | 11 - .../django_openstack/dash/images/_launch.html | 6 - .../django_openstack/dash/images/_launch_form.html | 17 - .../django_openstack/dash/images/_list.html | 29 - .../django_openstack/dash/images/index.html | 25 - .../django_openstack/dash/images/launch.html | 52 - .../django_openstack/dash/images/update.html | 26 - .../django_openstack/dash/instances/_form.html | 11 - .../django_openstack/dash/instances/_list.html | 75 - .../django_openstack/dash/instances/detail.html | 124 -- .../django_openstack/dash/instances/index.html | 66 - .../django_openstack/dash/instances/update.html | 50 - .../django_openstack/dash/instances/usage.csv | 11 - .../django_openstack/dash/instances/usage.html | 102 - .../django_openstack/dash/keypairs/_delete.html | 9 - .../django_openstack/dash/keypairs/_form.html | 11 - .../django_openstack/dash/keypairs/_list.html | 19 - .../django_openstack/dash/keypairs/create.html | 43 - .../django_openstack/dash/keypairs/import.html | 33 - .../django_openstack/dash/keypairs/index.html | 29 - .../django_openstack/dash/networks/_delete.html | 9 - .../dash/networks/_delete_port.html | 10 - .../dash/networks/_detach_port.html | 10 - .../django_openstack/dash/networks/_detail.html | 49 - .../django_openstack/dash/networks/_form.html | 11 - .../django_openstack/dash/networks/_list.html | 28 - .../django_openstack/dash/networks/_rename.html | 18 - .../dash/networks/_rename_form.html | 12 - .../dash/networks/_toggle_port.html | 16 - .../django_openstack/dash/networks/create.html | 29 - .../django_openstack/dash/networks/detail.html | 31 - .../django_openstack/dash/networks/index.html | 28 - .../django_openstack/dash/networks/rename.html | 37 - .../django_openstack/dash/objects/_copy.html | 11 - .../django_openstack/dash/objects/_delete.html | 9 - .../django_openstack/dash/objects/_filter.html | 8 - .../django_openstack/dash/objects/_form.html | 11 - .../django_openstack/dash/objects/_list.html | 29 - .../django_openstack/dash/objects/_paging.html | 1 - .../django_openstack/dash/objects/copy.html | 33 - .../django_openstack/dash/objects/index.html | 36 - .../django_openstack/dash/objects/upload.html | 31 - .../django_openstack/dash/ports/_attach.html | 12 - .../django_openstack/dash/ports/_create.html | 11 - .../django_openstack/dash/ports/attach.html | 48 - .../django_openstack/dash/ports/create.html | 29 - .../dash/security_groups/_delete.html | 9 - .../dash/security_groups/_delete_rule.html | 9 - .../dash/security_groups/_form.html | 13 - .../dash/security_groups/_list.html | 22 - .../dash/security_groups/create.html | 26 - .../dash/security_groups/edit_rules.html | 67 - .../dash/security_groups/index.html | 28 - .../templates/django_openstack/dash/settings.html | 44 - .../django_openstack/dash/snapshots/_form.html | 14 - .../django_openstack/dash/snapshots/create.html | 38 - .../django_openstack/dash/snapshots/index.html | 27 - .../dash/volumes/_attach_form.html | 14 - .../django_openstack/dash/volumes/_delete.html | 10 - .../dash/volumes/_detach_form.html | 11 - .../django_openstack/dash/volumes/_form.html | 13 - .../django_openstack/dash/volumes/_list.html | 58 - .../django_openstack/dash/volumes/attach.html | 27 - .../django_openstack/dash/volumes/create.html | 35 - .../django_openstack/dash/volumes/detail.html | 52 - .../django_openstack/dash/volumes/index.html | 26 - .../django_openstack/syspanel/_sidebar.html | 18 - .../templates/django_openstack/syspanel/base.html | 20 - .../django_openstack/syspanel/flavors/_create.html | 6 - .../django_openstack/syspanel/flavors/_delete.html | 9 - .../django_openstack/syspanel/flavors/_form.html | 17 - .../django_openstack/syspanel/flavors/_list.html | 25 - .../django_openstack/syspanel/flavors/create.html | 41 - .../django_openstack/syspanel/flavors/index.html | 19 - .../django_openstack/syspanel/images/_delete.html | 9 - .../django_openstack/syspanel/images/_form.html | 11 - .../django_openstack/syspanel/images/_list.html | 47 - .../django_openstack/syspanel/images/_toggle.html | 9 - .../django_openstack/syspanel/images/index.html | 18 - .../django_openstack/syspanel/images/update.html | 26 - .../django_openstack/syspanel/instances/_list.html | 54 - .../syspanel/instances/detail.html | 104 - .../django_openstack/syspanel/instances/index.html | 66 - .../syspanel/instances/tenant_usage.csv | 11 - .../syspanel/instances/tenant_usage.html | 98 - .../django_openstack/syspanel/instances/usage.csv | 8 - .../django_openstack/syspanel/instances/usage.html | 99 - .../django_openstack/syspanel/quotas/index.html | 29 - .../django_openstack/syspanel/services/_list.html | 65 - .../syspanel/services/_toggle.html | 22 - .../django_openstack/syspanel/services/index.html | 19 - .../syspanel/tenants/_add_user.html | 10 - .../syspanel/tenants/_create_form.html | 6 - .../django_openstack/syspanel/tenants/_delete.html | 9 - .../django_openstack/syspanel/tenants/_form.html | 11 - .../django_openstack/syspanel/tenants/_list.html | 26 - .../syspanel/tenants/_quotas_form.html | 12 - .../syspanel/tenants/_remove_user.html | 10 - .../syspanel/tenants/_update_form.html | 6 - .../syspanel/tenants/_update_quotas_form.html | 6 - .../django_openstack/syspanel/tenants/create.html | 26 - .../django_openstack/syspanel/tenants/index.html | 25 - .../django_openstack/syspanel/tenants/quotas.html | 30 - .../django_openstack/syspanel/tenants/update.html | 29 - .../django_openstack/syspanel/tenants/users.html | 72 - .../syspanel/users/_create_form.html | 7 - .../django_openstack/syspanel/users/_delete.html | 9 - .../syspanel/users/_enable_disable.html | 9 - .../django_openstack/syspanel/users/_form.html | 12 - .../syspanel/users/_toggle_enabled.html | 20 - .../syspanel/users/_update_form.html | 7 - .../django_openstack/syspanel/users/create.html | 27 - .../django_openstack/syspanel/users/index.html | 45 - .../django_openstack/syspanel/users/update.html | 27 - .../django_openstack/templatetags/__init__.py | 0 .../templatetags/templatetags/__init__.py | 0 .../templatetags/templatetags/branding.py | 62 - .../templatetags/templatetags/parse_date.py | 73 - .../templatetags/templatetags/sidebar_modules.py | 46 - .../templatetags/templatetags/sizeformat.py | 76 - .../templatetags/templatetags/swift_paging.py | 15 - .../templatetags/templatetags/truncate_filter.py | 35 - django-openstack/django_openstack/test.py | 99 - .../django_openstack/tests/__init__.py | 21 - .../django_openstack/tests/api_tests.py | 1587 -------------- .../django_openstack/tests/broken/README | 2 - .../django_openstack/tests/broken/base.py | 81 - .../tests/broken/credential_tests.py | 70 - .../django_openstack/tests/broken/image_tests.py | 237 -- .../tests/broken/instance_tests.py | 69 - .../django_openstack/tests/broken/keypair_tests.py | 93 - .../django_openstack/tests/broken/region_tests.py | 41 - .../django_openstack/tests/broken/test_models.py | 187 -- .../django_openstack/tests/broken/volume_tests.py | 171 -- .../tests/context_processor_tests.py | 23 - .../django_openstack/tests/dependency_tests.py | 39 - .../tests/templates/base-sidebar.html | 0 .../django_openstack/tests/templatetag_tests.py | 71 - .../django_openstack/tests/testsettings.py | 84 - .../django_openstack/tests/testurls.py | 40 - .../django_openstack/tests/view_tests/__init__.py | 0 .../tests/view_tests/auth_tests.py | 230 -- .../django_openstack/tests/view_tests/base.py | 77 - .../tests/view_tests/dash/__init__.py | 0 .../tests/view_tests/dash/container_tests.py | 121 -- .../tests/view_tests/dash/floating_ip_tests.py | 221 -- .../tests/view_tests/dash/images_tests.py | 374 ---- .../tests/view_tests/dash/instance_tests.py | 470 ---- .../tests/view_tests/dash/keypair_tests.py | 171 -- .../tests/view_tests/dash/network_tests.py | 198 -- .../tests/view_tests/dash/object_tests.py | 228 -- .../tests/view_tests/dash/port_tests.py | 104 - .../tests/view_tests/dash/security_groups_tests.py | 372 ---- .../tests/view_tests/dash/snapshots_tests.py | 213 -- .../tests/view_tests/syspanel/__init__.py | 0 .../tests/view_tests/syspanel/users_tests.py | 107 - django-openstack/django_openstack/tests/views.py | 31 - django-openstack/django_openstack/urls.py | 38 - django-openstack/django_openstack/utils.py | 48 - django-openstack/django_openstack/version.py | 35 - django-openstack/setup.py | 52 - doc/Makefile | 153 -- doc/generate_autodoc_index.py | 68 - doc/source/conf.py | 301 --- doc/source/index.rst | 69 - doc/source/testing.rst | 32 - docs/Makefile | 153 ++ docs/source/_static/.gitignore | 0 docs/source/conf.py | 391 ++++ docs/source/faq.rst | 37 + docs/source/glossary.rst | 19 + docs/source/index.rst | 101 + docs/source/intro.rst | 124 ++ docs/source/quickstart.rst | 146 ++ docs/source/ref/context_processors.rst | 6 + docs/source/ref/decorators.rst | 6 + docs/source/ref/exceptions.rst | 6 + docs/source/ref/forms.rst | 17 + docs/source/ref/horizon.rst | 42 + docs/source/ref/middleware.rst | 6 + docs/source/ref/run_tests.rst | 100 + docs/source/ref/users.rst | 6 + docs/source/ref/views.rst | 12 + docs/source/testing.rst | 62 + horizon/LICENSE | 176 ++ horizon/Makefile | 43 + horizon/README | 59 + horizon/bootstrap.py | 260 +++ horizon/buildout.cfg | 126 ++ horizon/horizon/__init__.py | 50 + horizon/horizon/api/__init__.py | 39 + horizon/horizon/api/base.py | 118 + horizon/horizon/api/deprecated.py | 95 + horizon/horizon/api/glance.py | 89 + horizon/horizon/api/keystone.py | 256 +++ horizon/horizon/api/nova.py | 357 +++ horizon/horizon/api/quantum.py | 133 ++ horizon/horizon/api/swift.py | 132 ++ horizon/horizon/base.py | 594 +++++ horizon/horizon/context_processors.py | 69 + horizon/horizon/dashboards/__init__.py | 0 horizon/horizon/dashboards/nova/__init__.py | 0 .../horizon/dashboards/nova/containers/__init__.py | 0 .../horizon/dashboards/nova/containers/forms.py | 142 ++ .../horizon/dashboards/nova/containers/panel.py | 35 + .../horizon/dashboards/nova/containers/tests.py | 307 +++ horizon/horizon/dashboards/nova/containers/urls.py | 36 + .../horizon/dashboards/nova/containers/views.py | 134 ++ horizon/horizon/dashboards/nova/dashboard.py | 33 + .../dashboards/nova/floating_ips/__init__.py | 0 .../horizon/dashboards/nova/floating_ips/forms.py | 123 ++ .../horizon/dashboards/nova/floating_ips/panel.py | 30 + .../horizon/dashboards/nova/floating_ips/tests.py | 216 ++ .../horizon/dashboards/nova/floating_ips/urls.py | 28 + .../horizon/dashboards/nova/floating_ips/views.py | 88 + horizon/horizon/dashboards/nova/images/__init__.py | 0 horizon/horizon/dashboards/nova/images/forms.py | 200 ++ horizon/horizon/dashboards/nova/images/panel.py | 30 + horizon/horizon/dashboards/nova/images/tests.py | 370 ++++ horizon/horizon/dashboards/nova/images/urls.py | 27 + horizon/horizon/dashboards/nova/images/views.py | 163 ++ .../horizon/dashboards/nova/instances/__init__.py | 0 horizon/horizon/dashboards/nova/instances/forms.py | 101 + horizon/horizon/dashboards/nova/instances/panel.py | 30 + horizon/horizon/dashboards/nova/instances/tests.py | 448 ++++ horizon/horizon/dashboards/nova/instances/urls.py | 33 + horizon/horizon/dashboards/nova/instances/views.py | 253 +++ .../horizon/dashboards/nova/keypairs/__init__.py | 0 horizon/horizon/dashboards/nova/keypairs/forms.py | 91 + horizon/horizon/dashboards/nova/keypairs/panel.py | 30 + horizon/horizon/dashboards/nova/keypairs/tests.py | 161 ++ horizon/horizon/dashboards/nova/keypairs/urls.py | 28 + horizon/horizon/dashboards/nova/keypairs/views.py | 80 + horizon/horizon/dashboards/nova/models.py | 23 + .../horizon/dashboards/nova/networks/__init__.py | 0 horizon/horizon/dashboards/nova/networks/forms.py | 208 ++ horizon/horizon/dashboards/nova/networks/panel.py | 33 + horizon/horizon/dashboards/nova/networks/tests.py | 268 +++ horizon/horizon/dashboards/nova/networks/urls.py | 31 + horizon/horizon/dashboards/nova/networks/views.py | 231 ++ .../horizon/dashboards/nova/overview/__init__.py | 0 horizon/horizon/dashboards/nova/overview/panel.py | 30 + horizon/horizon/dashboards/nova/overview/urls.py | 26 + .../dashboards/nova/security_groups/__init__.py | 0 .../dashboards/nova/security_groups/forms.py | 128 ++ .../dashboards/nova/security_groups/panel.py | 30 + .../dashboards/nova/security_groups/tests.py | 331 +++ .../dashboards/nova/security_groups/urls.py | 28 + .../dashboards/nova/security_groups/views.py | 103 + .../horizon/dashboards/nova/snapshots/__init__.py | 0 horizon/horizon/dashboards/nova/snapshots/forms.py | 57 + horizon/horizon/dashboards/nova/snapshots/panel.py | 30 + horizon/horizon/dashboards/nova/snapshots/tests.py | 202 ++ horizon/horizon/dashboards/nova/snapshots/urls.py | 26 + horizon/horizon/dashboards/nova/snapshots/views.py | 92 + .../dashboards/nova/templates/nova/base.html | 13 + .../nova/templates/nova/containers/_delete.html | 9 + .../nova/templates/nova/containers/_form.html | 11 + .../nova/templates/nova/containers/_list.html | 29 + .../nova/templates/nova/containers/create.html | 28 + .../nova/templates/nova/containers/index.html | 19 + .../templates/nova/floating_ips/_allocate.html | 8 + .../templates/nova/floating_ips/_associate.html | 17 + .../templates/nova/floating_ips/_disassociate.html | 9 + .../nova/templates/nova/floating_ips/_list.html | 34 + .../nova/templates/nova/floating_ips/_release.html | 9 + .../templates/nova/floating_ips/associate.html | 27 + .../nova/templates/nova/floating_ips/index.html | 26 + .../nova/templates/nova/images/_delete.html | 9 + .../nova/templates/nova/images/_form.html | 11 + .../nova/templates/nova/images/_launch.html | 6 + .../nova/templates/nova/images/_launch_form.html | 17 + .../nova/templates/nova/images/_list.html | 29 + .../nova/templates/nova/images/index.html | 25 + .../nova/templates/nova/images/launch.html | 52 + .../nova/templates/nova/images/update.html | 26 + .../nova/templates/nova/instances/_form.html | 11 + .../nova/templates/nova/instances/_list.html | 75 + .../nova/templates/nova/instances/detail.html | 124 ++ .../nova/templates/nova/instances/index.html | 66 + .../nova/templates/nova/instances/update.html | 50 + .../nova/templates/nova/instances/usage.csv | 11 + .../nova/templates/nova/instances/usage.html | 102 + .../nova/templates/nova/keypairs/_delete.html | 9 + .../nova/templates/nova/keypairs/_form.html | 11 + .../nova/templates/nova/keypairs/_list.html | 19 + .../nova/templates/nova/keypairs/create.html | 43 + .../nova/templates/nova/keypairs/import.html | 33 + .../nova/templates/nova/keypairs/index.html | 29 + .../nova/templates/nova/networks/_delete.html | 9 + .../nova/templates/nova/networks/_delete_port.html | 10 + .../nova/templates/nova/networks/_detach_port.html | 10 + .../nova/templates/nova/networks/_detail.html | 49 + .../nova/templates/nova/networks/_form.html | 11 + .../nova/templates/nova/networks/_list.html | 28 + .../nova/templates/nova/networks/_rename.html | 18 + .../nova/templates/nova/networks/_rename_form.html | 12 + .../nova/templates/nova/networks/_toggle_port.html | 16 + .../nova/templates/nova/networks/create.html | 29 + .../nova/templates/nova/networks/detail.html | 31 + .../nova/templates/nova/networks/index.html | 28 + .../nova/templates/nova/networks/rename.html | 37 + .../nova/templates/nova/objects/_copy.html | 11 + .../nova/templates/nova/objects/_delete.html | 9 + .../nova/templates/nova/objects/_filter.html | 8 + .../nova/templates/nova/objects/_form.html | 11 + .../nova/templates/nova/objects/_list.html | 29 + .../nova/templates/nova/objects/_paging.html | 1 + .../nova/templates/nova/objects/copy.html | 33 + .../nova/templates/nova/objects/index.html | 36 + .../nova/templates/nova/objects/upload.html | 31 + .../nova/templates/nova/ports/_attach.html | 12 + .../nova/templates/nova/ports/_create.html | 11 + .../nova/templates/nova/ports/attach.html | 48 + .../nova/templates/nova/ports/create.html | 29 + .../templates/nova/security_groups/_delete.html | 9 + .../nova/security_groups/_delete_rule.html | 9 + .../nova/templates/nova/security_groups/_form.html | 13 + .../nova/templates/nova/security_groups/_list.html | 22 + .../templates/nova/security_groups/create.html | 26 + .../templates/nova/security_groups/edit_rules.html | 67 + .../nova/templates/nova/security_groups/index.html | 28 + .../dashboards/nova/templates/nova/settings.html | 44 + .../nova/templates/nova/snapshots/_form.html | 14 + .../nova/templates/nova/snapshots/create.html | 38 + .../nova/templates/nova/snapshots/index.html | 27 + .../nova/templates/nova/volumes/_attach_form.html | 14 + .../nova/templates/nova/volumes/_delete.html | 10 + .../nova/templates/nova/volumes/_detach_form.html | 11 + .../nova/templates/nova/volumes/_form.html | 13 + .../nova/templates/nova/volumes/_list.html | 58 + .../nova/templates/nova/volumes/attach.html | 27 + .../nova/templates/nova/volumes/create.html | 35 + .../nova/templates/nova/volumes/detail.html | 52 + .../nova/templates/nova/volumes/index.html | 26 + .../horizon/dashboards/nova/volumes/__init__.py | 0 horizon/horizon/dashboards/nova/volumes/forms.py | 106 + horizon/horizon/dashboards/nova/volumes/panel.py | 26 + horizon/horizon/dashboards/nova/volumes/tests.py | 0 horizon/horizon/dashboards/nova/volumes/urls.py | 25 + horizon/horizon/dashboards/nova/volumes/views.py | 110 + horizon/horizon/dashboards/settings/__init__.py | 0 horizon/horizon/dashboards/settings/dashboard.py | 30 + horizon/horizon/dashboards/settings/models.py | 23 + .../settings/templates/settings/base.html | 13 + .../settings/templates/settings/user/settings.html | 32 + .../horizon/dashboards/settings/user/__init__.py | 0 horizon/horizon/dashboards/settings/user/panel.py | 26 + horizon/horizon/dashboards/settings/user/urls.py | 28 + horizon/horizon/dashboards/syspanel/__init__.py | 0 horizon/horizon/dashboards/syspanel/dashboard.py | 32 + .../dashboards/syspanel/flavors/__init__.py | 0 .../horizon/dashboards/syspanel/flavors/forms.py | 69 + .../horizon/dashboards/syspanel/flavors/panel.py | 30 + .../horizon/dashboards/syspanel/flavors/tests.py | 0 .../horizon/dashboards/syspanel/flavors/urls.py | 26 + .../horizon/dashboards/syspanel/flavors/views.py | 77 + .../horizon/dashboards/syspanel/images/__init__.py | 0 .../horizon/dashboards/syspanel/images/forms.py | 83 + .../horizon/dashboards/syspanel/images/panel.py | 30 + .../horizon/dashboards/syspanel/images/tests.py | 0 horizon/horizon/dashboards/syspanel/images/urls.py | 26 + .../horizon/dashboards/syspanel/images/views.py | 192 ++ .../dashboards/syspanel/instances/__init__.py | 0 .../horizon/dashboards/syspanel/instances/panel.py | 31 + .../horizon/dashboards/syspanel/instances/tests.py | 0 .../horizon/dashboards/syspanel/instances/urls.py | 36 + .../horizon/dashboards/syspanel/instances/views.py | 337 +++ horizon/horizon/dashboards/syspanel/models.py | 23 + .../dashboards/syspanel/overview/__init__.py | 0 .../horizon/dashboards/syspanel/overview/panel.py | 31 + .../horizon/dashboards/syspanel/overview/urls.py | 26 + .../horizon/dashboards/syspanel/quotas/__init__.py | 0 .../horizon/dashboards/syspanel/quotas/panel.py | 30 + .../horizon/dashboards/syspanel/quotas/tests.py | 0 horizon/horizon/dashboards/syspanel/quotas/urls.py | 25 + .../horizon/dashboards/syspanel/quotas/views.py | 36 + .../dashboards/syspanel/services/__init__.py | 0 .../horizon/dashboards/syspanel/services/forms.py | 58 + .../horizon/dashboards/syspanel/services/panel.py | 30 + .../horizon/dashboards/syspanel/services/tests.py | 0 .../horizon/dashboards/syspanel/services/urls.py | 25 + .../horizon/dashboards/syspanel/services/views.py | 81 + .../syspanel/templates/syspanel/base.html | 13 + .../templates/syspanel/flavors/_create.html | 6 + .../templates/syspanel/flavors/_delete.html | 9 + .../syspanel/templates/syspanel/flavors/_form.html | 17 + .../syspanel/templates/syspanel/flavors/_list.html | 25 + .../templates/syspanel/flavors/create.html | 41 + .../syspanel/templates/syspanel/flavors/index.html | 19 + .../templates/syspanel/images/_delete.html | 9 + .../syspanel/templates/syspanel/images/_form.html | 11 + .../syspanel/templates/syspanel/images/_list.html | 47 + .../templates/syspanel/images/_toggle.html | 9 + .../syspanel/templates/syspanel/images/index.html | 18 + .../syspanel/templates/syspanel/images/update.html | 26 + .../templates/syspanel/instances/_list.html | 54 + .../templates/syspanel/instances/detail.html | 104 + .../templates/syspanel/instances/index.html | 66 + .../templates/syspanel/instances/tenant_usage.csv | 11 + .../templates/syspanel/instances/tenant_usage.html | 98 + .../templates/syspanel/instances/usage.csv | 8 + .../templates/syspanel/instances/usage.html | 99 + .../syspanel/templates/syspanel/quotas/index.html | 29 + .../templates/syspanel/services/_list.html | 65 + .../templates/syspanel/services/_toggle.html | 22 + .../templates/syspanel/services/index.html | 19 + .../templates/syspanel/tenants/_add_user.html | 10 + .../templates/syspanel/tenants/_create_form.html | 6 + .../templates/syspanel/tenants/_delete.html | 9 + .../syspanel/templates/syspanel/tenants/_form.html | 11 + .../syspanel/templates/syspanel/tenants/_list.html | 26 + .../templates/syspanel/tenants/_quotas_form.html | 12 + .../templates/syspanel/tenants/_remove_user.html | 10 + .../templates/syspanel/tenants/_update_form.html | 6 + .../syspanel/tenants/_update_quotas_form.html | 6 + .../templates/syspanel/tenants/create.html | 26 + .../syspanel/templates/syspanel/tenants/index.html | 25 + .../templates/syspanel/tenants/quotas.html | 30 + .../templates/syspanel/tenants/update.html | 29 + .../syspanel/templates/syspanel/tenants/users.html | 72 + .../templates/syspanel/users/_create_form.html | 7 + .../syspanel/templates/syspanel/users/_delete.html | 9 + .../templates/syspanel/users/_enable_disable.html | 9 + .../syspanel/templates/syspanel/users/_form.html | 12 + .../templates/syspanel/users/_toggle_enabled.html | 20 + .../templates/syspanel/users/_update_form.html | 7 + .../syspanel/templates/syspanel/users/create.html | 27 + .../syspanel/templates/syspanel/users/index.html | 45 + .../syspanel/templates/syspanel/users/update.html | 27 + .../dashboards/syspanel/tenants/__init__.py | 0 .../horizon/dashboards/syspanel/tenants/forms.py | 183 ++ .../horizon/dashboards/syspanel/tenants/panel.py | 30 + .../horizon/dashboards/syspanel/tenants/tests.py | 0 .../horizon/dashboards/syspanel/tenants/urls.py | 29 + .../horizon/dashboards/syspanel/tenants/views.py | 143 ++ .../horizon/dashboards/syspanel/users/__init__.py | 0 horizon/horizon/dashboards/syspanel/users/forms.py | 102 + horizon/horizon/dashboards/syspanel/users/panel.py | 30 + horizon/horizon/dashboards/syspanel/users/tests.py | 111 + horizon/horizon/dashboards/syspanel/users/urls.py | 26 + horizon/horizon/dashboards/syspanel/users/views.py | 163 ++ horizon/horizon/decorators.py | 86 + horizon/horizon/exceptions.py | 40 + horizon/horizon/forms.py | 220 ++ horizon/horizon/locale/es/LC_MESSAGES/django.mo | Bin 0 -> 425 bytes horizon/horizon/locale/es/LC_MESSAGES/django.po | 1971 +++++++++++++++++ horizon/horizon/locale/fr/LC_MESSAGES/django.mo | Bin 0 -> 420 bytes horizon/horizon/locale/fr/LC_MESSAGES/django.po | 1971 +++++++++++++++++ horizon/horizon/locale/ja/LC_MESSAGES/django.mo | Bin 0 -> 2528 bytes horizon/horizon/locale/ja/LC_MESSAGES/django.po | 2294 ++++++++++++++++++++ horizon/horizon/locale/pl/LC_MESSAGES/django.mo | Bin 0 -> 936 bytes horizon/horizon/locale/pl/LC_MESSAGES/django.po | 2282 +++++++++++++++++++ horizon/horizon/locale/pt/LC_MESSAGES/django.mo | Bin 0 -> 382 bytes horizon/horizon/locale/pt/LC_MESSAGES/django.po | 1970 +++++++++++++++++ horizon/horizon/locale/zh-cn/LC_MESSAGES/django.mo | Bin 0 -> 382 bytes horizon/horizon/locale/zh-cn/LC_MESSAGES/django.po | 1970 +++++++++++++++++ horizon/horizon/locale/zh-tw/LC_MESSAGES/django.mo | Bin 0 -> 382 bytes horizon/horizon/locale/zh-tw/LC_MESSAGES/django.po | 1970 +++++++++++++++++ horizon/horizon/middleware.py | 63 + horizon/horizon/models.py | 23 + horizon/horizon/site_urls.py | 31 + horizon/horizon/templates/horizon/_nav_list.html | 10 + .../horizon/templates/horizon/_subnav_list.html | 14 + horizon/horizon/templates/horizon/auth/_login.html | 17 + .../horizon/templates/horizon/auth/_switch.html | 17 + .../templates/horizon/common/_page_header.html | 21 + .../horizon/templates/horizon/common/_sidebar.html | 5 + .../templates/horizon/common/_sidebar_module.html | 10 + .../horizon/common/instances/_reboot.html | 9 + .../horizon/common/instances/_terminate.html | 9 + horizon/horizon/templatetags/__init__.py | 0 horizon/horizon/templatetags/branding.py | 62 + horizon/horizon/templatetags/horizon.py | 79 + horizon/horizon/templatetags/parse_date.py | 73 + horizon/horizon/templatetags/sizeformat.py | 76 + horizon/horizon/templatetags/swift_paging.py | 35 + horizon/horizon/templatetags/truncate_filter.py | 35 + horizon/horizon/test.py | 212 ++ horizon/horizon/tests/__init__.py | 21 + horizon/horizon/tests/api_tests/__init__.py | 29 + horizon/horizon/tests/api_tests/base.py | 112 + horizon/horizon/tests/api_tests/glance.py | 174 ++ horizon/horizon/tests/api_tests/keystone.py | 366 ++++ horizon/horizon/tests/api_tests/nova.py | 697 ++++++ horizon/horizon/tests/api_tests/swift.py | 260 +++ horizon/horizon/tests/api_tests/utils.py | 99 + horizon/horizon/tests/auth_tests.py | 229 ++ horizon/horizon/tests/base_tests.py | 133 ++ horizon/horizon/tests/context_processor_tests.py | 45 + horizon/horizon/tests/templates/base-sidebar.html | 0 horizon/horizon/tests/templates/base.html | 0 horizon/horizon/tests/templates/splash.html | 0 .../horizon/tests/templates/switch_tenants.html | 0 horizon/horizon/tests/templatetag_tests.py | 38 + horizon/horizon/tests/testsettings.py | 104 + horizon/horizon/tests/testurls.py | 33 + horizon/horizon/tests/views.py | 31 + horizon/horizon/users.py | 123 ++ horizon/horizon/version.py | 35 + horizon/horizon/views/__init__.py | 0 horizon/horizon/views/auth.py | 174 ++ horizon/horizon/views/auth_forms.py | 141 ++ horizon/setup.py | 51 + openstack-dashboard/README | 49 +- openstack-dashboard/dashboard/manage.py | 3 + openstack-dashboard/dashboard/middleware.py | 1 + openstack-dashboard/dashboard/settings.py | 20 +- .../dashboard/static/dashboard/images/favicon.ico | Bin 0 -> 1150 bytes .../dashboard/templates/_login.html | 2 +- .../dashboard/templates/_switch.html | 2 +- .../dashboard/templates/_topbar.html | 22 +- openstack-dashboard/dashboard/templates/base.html | 1 + .../dashboard/templates/switch_tenants.html | 4 +- openstack-dashboard/dashboard/tests.py | 38 - openstack-dashboard/dashboard/urls.py | 15 +- openstack-dashboard/dashboard/views.py | 20 +- .../local/local_settings.py.example | 97 +- openstack-dashboard/tools/install_venv.py | 11 +- openstack-dashboard/tools/pip-requires | 22 +- openstack-dashboard/tools/with_venv.sh | 1 - run_tests.sh | 75 +- 603 files changed, 33540 insertions(+), 31153 deletions(-) delete mode 100644 django-openstack/LICENSE delete mode 100644 django-openstack/Makefile delete mode 100644 django-openstack/README delete mode 100644 django-openstack/bootstrap.py delete mode 100644 django-openstack/buildout.cfg delete mode 100644 django-openstack/django_openstack/__init__.py delete mode 100644 django-openstack/django_openstack/api.py delete mode 100644 django-openstack/django_openstack/auth/__init__.py delete mode 100644 django-openstack/django_openstack/auth/urls.py delete mode 100644 django-openstack/django_openstack/auth/views.py delete mode 100644 django-openstack/django_openstack/context_processors.py delete mode 100644 django-openstack/django_openstack/dash/__init__.py delete mode 100644 django-openstack/django_openstack/dash/urls.py delete mode 100644 django-openstack/django_openstack/dash/views/__init__.py delete mode 100644 django-openstack/django_openstack/dash/views/containers.py delete mode 100644 django-openstack/django_openstack/dash/views/floating_ips.py delete mode 100644 django-openstack/django_openstack/dash/views/images.py delete mode 100644 django-openstack/django_openstack/dash/views/instances.py delete mode 100644 django-openstack/django_openstack/dash/views/keypairs.py delete mode 100644 django-openstack/django_openstack/dash/views/networks.py delete mode 100644 django-openstack/django_openstack/dash/views/objects.py delete mode 100644 django-openstack/django_openstack/dash/views/ports.py delete mode 100644 django-openstack/django_openstack/dash/views/security_groups.py delete mode 100644 django-openstack/django_openstack/dash/views/snapshots.py delete mode 100644 django-openstack/django_openstack/dash/views/volumes.py delete mode 100644 django-openstack/django_openstack/decorators.py delete mode 100644 django-openstack/django_openstack/exceptions.py delete mode 100644 django-openstack/django_openstack/forms.py delete mode 100644 django-openstack/django_openstack/locale/es/LC_MESSAGES/django.mo delete mode 100644 django-openstack/django_openstack/locale/es/LC_MESSAGES/django.po delete mode 100644 django-openstack/django_openstack/locale/fr/LC_MESSAGES/django.mo delete mode 100644 django-openstack/django_openstack/locale/fr/LC_MESSAGES/django.po delete mode 100644 django-openstack/django_openstack/locale/ja/LC_MESSAGES/django.mo delete mode 100644 django-openstack/django_openstack/locale/ja/LC_MESSAGES/django.po delete mode 100644 django-openstack/django_openstack/locale/pl/LC_MESSAGES/django.mo delete mode 100644 django-openstack/django_openstack/locale/pl/LC_MESSAGES/django.po delete mode 100644 django-openstack/django_openstack/locale/pt/LC_MESSAGES/django.mo delete mode 100644 django-openstack/django_openstack/locale/pt/LC_MESSAGES/django.po delete mode 100644 django-openstack/django_openstack/locale/zh-cn/LC_MESSAGES/django.mo delete mode 100644 django-openstack/django_openstack/locale/zh-cn/LC_MESSAGES/django.po delete mode 100644 django-openstack/django_openstack/locale/zh-tw/LC_MESSAGES/django.mo delete mode 100644 django-openstack/django_openstack/locale/zh-tw/LC_MESSAGES/django.po delete mode 100644 django-openstack/django_openstack/middleware/__init__.py delete mode 100644 django-openstack/django_openstack/middleware/keystone.py delete mode 100644 django-openstack/django_openstack/models.py delete mode 100644 django-openstack/django_openstack/signals.py delete mode 100644 django-openstack/django_openstack/syspanel/__init__.py delete mode 100644 django-openstack/django_openstack/syspanel/forms.py delete mode 100644 django-openstack/django_openstack/syspanel/urls.py delete mode 100644 django-openstack/django_openstack/syspanel/views/__init__.py delete mode 100644 django-openstack/django_openstack/syspanel/views/flavors.py delete mode 100644 django-openstack/django_openstack/syspanel/views/images.py delete mode 100644 django-openstack/django_openstack/syspanel/views/instances.py delete mode 100644 django-openstack/django_openstack/syspanel/views/quotas.py delete mode 100644 django-openstack/django_openstack/syspanel/views/services.py delete mode 100644 django-openstack/django_openstack/syspanel/views/tenants.py delete mode 100644 django-openstack/django_openstack/syspanel/views/users.py delete mode 100644 django-openstack/django_openstack/templates/django_openstack/auth/_login.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/auth/_switch.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/common/_page_header.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/common/_sidebar_module.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/common/instances/_reboot.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/common/instances/_terminate.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/_sidebar.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/base.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/containers/_delete.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/containers/_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/containers/_list.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/containers/create.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/containers/index.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/_allocate.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/_associate.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/_disassociate.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/_list.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/_release.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/associate.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/index.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/images/_delete.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/images/_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/images/_launch.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/images/_launch_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/images/_list.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/images/index.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/images/launch.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/images/update.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/instances/_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/instances/_list.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/instances/detail.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/instances/index.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/instances/update.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/instances/usage.csv delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/instances/usage.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/keypairs/_delete.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/keypairs/_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/keypairs/_list.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/keypairs/create.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/keypairs/import.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/keypairs/index.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/networks/_delete.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/networks/_delete_port.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/networks/_detach_port.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/networks/_detail.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/networks/_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/networks/_list.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/networks/_rename.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/networks/_rename_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/networks/_toggle_port.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/networks/create.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/networks/detail.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/networks/index.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/networks/rename.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/objects/_copy.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/objects/_delete.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/objects/_filter.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/objects/_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/objects/_list.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/objects/_paging.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/objects/copy.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/objects/index.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/objects/upload.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/ports/_attach.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/ports/_create.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/ports/attach.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/ports/create.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/security_groups/_delete.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/security_groups/_delete_rule.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/security_groups/_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/security_groups/_list.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/security_groups/create.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/security_groups/edit_rules.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/security_groups/index.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/settings.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/snapshots/_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/snapshots/create.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/snapshots/index.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/volumes/_attach_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/volumes/_delete.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/volumes/_detach_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/volumes/_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/volumes/_list.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/volumes/attach.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/volumes/create.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/volumes/detail.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/volumes/index.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/_sidebar.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/base.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/_create.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/_delete.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/_list.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/create.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/index.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/images/_delete.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/images/_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/images/_list.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/images/_toggle.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/images/index.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/images/update.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/instances/_list.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/instances/detail.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/instances/index.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/instances/tenant_usage.csv delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/instances/tenant_usage.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/instances/usage.csv delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/instances/usage.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/quotas/index.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/services/_list.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/services/_toggle.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/services/index.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_add_user.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_create_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_delete.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_list.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_quotas_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_remove_user.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_update_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_update_quotas_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/create.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/index.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/quotas.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/update.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/users.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/users/_create_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/users/_delete.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/users/_enable_disable.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/users/_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/users/_toggle_enabled.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/users/_update_form.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/users/create.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/users/index.html delete mode 100644 django-openstack/django_openstack/templates/django_openstack/syspanel/users/update.html delete mode 100644 django-openstack/django_openstack/templatetags/__init__.py delete mode 100644 django-openstack/django_openstack/templatetags/templatetags/__init__.py delete mode 100644 django-openstack/django_openstack/templatetags/templatetags/branding.py delete mode 100644 django-openstack/django_openstack/templatetags/templatetags/parse_date.py delete mode 100644 django-openstack/django_openstack/templatetags/templatetags/sidebar_modules.py delete mode 100644 django-openstack/django_openstack/templatetags/templatetags/sizeformat.py delete mode 100644 django-openstack/django_openstack/templatetags/templatetags/swift_paging.py delete mode 100644 django-openstack/django_openstack/templatetags/templatetags/truncate_filter.py delete mode 100644 django-openstack/django_openstack/test.py delete mode 100644 django-openstack/django_openstack/tests/__init__.py delete mode 100644 django-openstack/django_openstack/tests/api_tests.py delete mode 100644 django-openstack/django_openstack/tests/broken/README delete mode 100644 django-openstack/django_openstack/tests/broken/base.py delete mode 100644 django-openstack/django_openstack/tests/broken/credential_tests.py delete mode 100644 django-openstack/django_openstack/tests/broken/image_tests.py delete mode 100644 django-openstack/django_openstack/tests/broken/instance_tests.py delete mode 100644 django-openstack/django_openstack/tests/broken/keypair_tests.py delete mode 100644 django-openstack/django_openstack/tests/broken/region_tests.py delete mode 100644 django-openstack/django_openstack/tests/broken/test_models.py delete mode 100644 django-openstack/django_openstack/tests/broken/volume_tests.py delete mode 100644 django-openstack/django_openstack/tests/context_processor_tests.py delete mode 100644 django-openstack/django_openstack/tests/dependency_tests.py delete mode 100644 django-openstack/django_openstack/tests/templates/base-sidebar.html delete mode 100644 django-openstack/django_openstack/tests/templatetag_tests.py delete mode 100644 django-openstack/django_openstack/tests/testsettings.py delete mode 100644 django-openstack/django_openstack/tests/testurls.py delete mode 100644 django-openstack/django_openstack/tests/view_tests/__init__.py delete mode 100644 django-openstack/django_openstack/tests/view_tests/auth_tests.py delete mode 100644 django-openstack/django_openstack/tests/view_tests/base.py delete mode 100644 django-openstack/django_openstack/tests/view_tests/dash/__init__.py delete mode 100644 django-openstack/django_openstack/tests/view_tests/dash/container_tests.py delete mode 100644 django-openstack/django_openstack/tests/view_tests/dash/floating_ip_tests.py delete mode 100644 django-openstack/django_openstack/tests/view_tests/dash/images_tests.py delete mode 100644 django-openstack/django_openstack/tests/view_tests/dash/instance_tests.py delete mode 100644 django-openstack/django_openstack/tests/view_tests/dash/keypair_tests.py delete mode 100644 django-openstack/django_openstack/tests/view_tests/dash/network_tests.py delete mode 100644 django-openstack/django_openstack/tests/view_tests/dash/object_tests.py delete mode 100644 django-openstack/django_openstack/tests/view_tests/dash/port_tests.py delete mode 100644 django-openstack/django_openstack/tests/view_tests/dash/security_groups_tests.py delete mode 100644 django-openstack/django_openstack/tests/view_tests/dash/snapshots_tests.py delete mode 100644 django-openstack/django_openstack/tests/view_tests/syspanel/__init__.py delete mode 100644 django-openstack/django_openstack/tests/view_tests/syspanel/users_tests.py delete mode 100644 django-openstack/django_openstack/tests/views.py delete mode 100644 django-openstack/django_openstack/urls.py delete mode 100644 django-openstack/django_openstack/utils.py delete mode 100644 django-openstack/django_openstack/version.py delete mode 100755 django-openstack/setup.py delete mode 100644 doc/Makefile delete mode 100755 doc/generate_autodoc_index.py delete mode 100644 doc/source/conf.py delete mode 100644 doc/source/index.rst delete mode 100644 doc/source/testing.rst create mode 100644 docs/Makefile create mode 100644 docs/source/_static/.gitignore create mode 100644 docs/source/conf.py create mode 100644 docs/source/faq.rst create mode 100644 docs/source/glossary.rst create mode 100644 docs/source/index.rst create mode 100644 docs/source/intro.rst create mode 100644 docs/source/quickstart.rst create mode 100644 docs/source/ref/context_processors.rst create mode 100644 docs/source/ref/decorators.rst create mode 100644 docs/source/ref/exceptions.rst create mode 100644 docs/source/ref/forms.rst create mode 100644 docs/source/ref/horizon.rst create mode 100644 docs/source/ref/middleware.rst create mode 100644 docs/source/ref/run_tests.rst create mode 100644 docs/source/ref/users.rst create mode 100644 docs/source/ref/views.rst create mode 100644 docs/source/testing.rst create mode 100644 horizon/LICENSE create mode 100644 horizon/Makefile create mode 100644 horizon/README create mode 100644 horizon/bootstrap.py create mode 100644 horizon/buildout.cfg create mode 100644 horizon/horizon/__init__.py create mode 100644 horizon/horizon/api/__init__.py create mode 100644 horizon/horizon/api/base.py create mode 100644 horizon/horizon/api/deprecated.py create mode 100644 horizon/horizon/api/glance.py create mode 100644 horizon/horizon/api/keystone.py create mode 100644 horizon/horizon/api/nova.py create mode 100644 horizon/horizon/api/quantum.py create mode 100644 horizon/horizon/api/swift.py create mode 100644 horizon/horizon/base.py create mode 100644 horizon/horizon/context_processors.py create mode 100644 horizon/horizon/dashboards/__init__.py create mode 100644 horizon/horizon/dashboards/nova/__init__.py create mode 100644 horizon/horizon/dashboards/nova/containers/__init__.py create mode 100644 horizon/horizon/dashboards/nova/containers/forms.py create mode 100644 horizon/horizon/dashboards/nova/containers/panel.py create mode 100644 horizon/horizon/dashboards/nova/containers/tests.py create mode 100644 horizon/horizon/dashboards/nova/containers/urls.py create mode 100644 horizon/horizon/dashboards/nova/containers/views.py create mode 100644 horizon/horizon/dashboards/nova/dashboard.py create mode 100644 horizon/horizon/dashboards/nova/floating_ips/__init__.py create mode 100644 horizon/horizon/dashboards/nova/floating_ips/forms.py create mode 100644 horizon/horizon/dashboards/nova/floating_ips/panel.py create mode 100644 horizon/horizon/dashboards/nova/floating_ips/tests.py create mode 100644 horizon/horizon/dashboards/nova/floating_ips/urls.py create mode 100644 horizon/horizon/dashboards/nova/floating_ips/views.py create mode 100644 horizon/horizon/dashboards/nova/images/__init__.py create mode 100644 horizon/horizon/dashboards/nova/images/forms.py create mode 100644 horizon/horizon/dashboards/nova/images/panel.py create mode 100644 horizon/horizon/dashboards/nova/images/tests.py create mode 100644 horizon/horizon/dashboards/nova/images/urls.py create mode 100644 horizon/horizon/dashboards/nova/images/views.py create mode 100644 horizon/horizon/dashboards/nova/instances/__init__.py create mode 100644 horizon/horizon/dashboards/nova/instances/forms.py create mode 100644 horizon/horizon/dashboards/nova/instances/panel.py create mode 100644 horizon/horizon/dashboards/nova/instances/tests.py create mode 100644 horizon/horizon/dashboards/nova/instances/urls.py create mode 100644 horizon/horizon/dashboards/nova/instances/views.py create mode 100644 horizon/horizon/dashboards/nova/keypairs/__init__.py create mode 100644 horizon/horizon/dashboards/nova/keypairs/forms.py create mode 100644 horizon/horizon/dashboards/nova/keypairs/panel.py create mode 100644 horizon/horizon/dashboards/nova/keypairs/tests.py create mode 100644 horizon/horizon/dashboards/nova/keypairs/urls.py create mode 100644 horizon/horizon/dashboards/nova/keypairs/views.py create mode 100644 horizon/horizon/dashboards/nova/models.py create mode 100644 horizon/horizon/dashboards/nova/networks/__init__.py create mode 100644 horizon/horizon/dashboards/nova/networks/forms.py create mode 100644 horizon/horizon/dashboards/nova/networks/panel.py create mode 100644 horizon/horizon/dashboards/nova/networks/tests.py create mode 100644 horizon/horizon/dashboards/nova/networks/urls.py create mode 100644 horizon/horizon/dashboards/nova/networks/views.py create mode 100644 horizon/horizon/dashboards/nova/overview/__init__.py create mode 100644 horizon/horizon/dashboards/nova/overview/panel.py create mode 100644 horizon/horizon/dashboards/nova/overview/urls.py create mode 100644 horizon/horizon/dashboards/nova/security_groups/__init__.py create mode 100644 horizon/horizon/dashboards/nova/security_groups/forms.py create mode 100644 horizon/horizon/dashboards/nova/security_groups/panel.py create mode 100644 horizon/horizon/dashboards/nova/security_groups/tests.py create mode 100644 horizon/horizon/dashboards/nova/security_groups/urls.py create mode 100644 horizon/horizon/dashboards/nova/security_groups/views.py create mode 100644 horizon/horizon/dashboards/nova/snapshots/__init__.py create mode 100644 horizon/horizon/dashboards/nova/snapshots/forms.py create mode 100644 horizon/horizon/dashboards/nova/snapshots/panel.py create mode 100644 horizon/horizon/dashboards/nova/snapshots/tests.py create mode 100644 horizon/horizon/dashboards/nova/snapshots/urls.py create mode 100644 horizon/horizon/dashboards/nova/snapshots/views.py create mode 100644 horizon/horizon/dashboards/nova/templates/nova/base.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/containers/_delete.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/containers/_form.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/containers/_list.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/containers/create.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/containers/index.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/floating_ips/_allocate.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/floating_ips/_associate.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/floating_ips/_disassociate.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/floating_ips/_list.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/floating_ips/_release.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/floating_ips/associate.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/floating_ips/index.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/images/_delete.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/images/_form.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/images/_launch.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/images/_launch_form.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/images/_list.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/images/index.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/images/launch.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/images/update.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/instances/_form.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/instances/_list.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/instances/detail.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/instances/index.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/instances/update.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/instances/usage.csv create mode 100644 horizon/horizon/dashboards/nova/templates/nova/instances/usage.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/keypairs/_delete.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/keypairs/_form.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/keypairs/_list.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/keypairs/create.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/keypairs/import.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/keypairs/index.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/networks/_delete.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/networks/_delete_port.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/networks/_detach_port.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/networks/_detail.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/networks/_form.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/networks/_list.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/networks/_rename.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/networks/_rename_form.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/networks/_toggle_port.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/networks/create.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/networks/detail.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/networks/index.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/networks/rename.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/objects/_copy.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/objects/_delete.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/objects/_filter.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/objects/_form.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/objects/_list.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/objects/_paging.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/objects/copy.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/objects/index.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/objects/upload.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/ports/_attach.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/ports/_create.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/ports/attach.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/ports/create.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/security_groups/_delete.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/security_groups/_delete_rule.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/security_groups/_form.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/security_groups/_list.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/security_groups/create.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/security_groups/edit_rules.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/security_groups/index.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/settings.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/snapshots/_form.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/snapshots/create.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/snapshots/index.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/volumes/_attach_form.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/volumes/_delete.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/volumes/_detach_form.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/volumes/_form.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/volumes/_list.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/volumes/attach.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/volumes/create.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/volumes/detail.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/volumes/index.html create mode 100644 horizon/horizon/dashboards/nova/volumes/__init__.py create mode 100644 horizon/horizon/dashboards/nova/volumes/forms.py create mode 100644 horizon/horizon/dashboards/nova/volumes/panel.py create mode 100644 horizon/horizon/dashboards/nova/volumes/tests.py create mode 100644 horizon/horizon/dashboards/nova/volumes/urls.py create mode 100644 horizon/horizon/dashboards/nova/volumes/views.py create mode 100644 horizon/horizon/dashboards/settings/__init__.py create mode 100644 horizon/horizon/dashboards/settings/dashboard.py create mode 100644 horizon/horizon/dashboards/settings/models.py create mode 100644 horizon/horizon/dashboards/settings/templates/settings/base.html create mode 100644 horizon/horizon/dashboards/settings/templates/settings/user/settings.html create mode 100644 horizon/horizon/dashboards/settings/user/__init__.py create mode 100644 horizon/horizon/dashboards/settings/user/panel.py create mode 100644 horizon/horizon/dashboards/settings/user/urls.py create mode 100644 horizon/horizon/dashboards/syspanel/__init__.py create mode 100644 horizon/horizon/dashboards/syspanel/dashboard.py create mode 100644 horizon/horizon/dashboards/syspanel/flavors/__init__.py create mode 100644 horizon/horizon/dashboards/syspanel/flavors/forms.py create mode 100644 horizon/horizon/dashboards/syspanel/flavors/panel.py create mode 100644 horizon/horizon/dashboards/syspanel/flavors/tests.py create mode 100644 horizon/horizon/dashboards/syspanel/flavors/urls.py create mode 100644 horizon/horizon/dashboards/syspanel/flavors/views.py create mode 100644 horizon/horizon/dashboards/syspanel/images/__init__.py create mode 100644 horizon/horizon/dashboards/syspanel/images/forms.py create mode 100644 horizon/horizon/dashboards/syspanel/images/panel.py create mode 100644 horizon/horizon/dashboards/syspanel/images/tests.py create mode 100644 horizon/horizon/dashboards/syspanel/images/urls.py create mode 100644 horizon/horizon/dashboards/syspanel/images/views.py create mode 100644 horizon/horizon/dashboards/syspanel/instances/__init__.py create mode 100644 horizon/horizon/dashboards/syspanel/instances/panel.py create mode 100644 horizon/horizon/dashboards/syspanel/instances/tests.py create mode 100644 horizon/horizon/dashboards/syspanel/instances/urls.py create mode 100644 horizon/horizon/dashboards/syspanel/instances/views.py create mode 100644 horizon/horizon/dashboards/syspanel/models.py create mode 100644 horizon/horizon/dashboards/syspanel/overview/__init__.py create mode 100644 horizon/horizon/dashboards/syspanel/overview/panel.py create mode 100644 horizon/horizon/dashboards/syspanel/overview/urls.py create mode 100644 horizon/horizon/dashboards/syspanel/quotas/__init__.py create mode 100644 horizon/horizon/dashboards/syspanel/quotas/panel.py create mode 100644 horizon/horizon/dashboards/syspanel/quotas/tests.py create mode 100644 horizon/horizon/dashboards/syspanel/quotas/urls.py create mode 100644 horizon/horizon/dashboards/syspanel/quotas/views.py create mode 100644 horizon/horizon/dashboards/syspanel/services/__init__.py create mode 100644 horizon/horizon/dashboards/syspanel/services/forms.py create mode 100644 horizon/horizon/dashboards/syspanel/services/panel.py create mode 100644 horizon/horizon/dashboards/syspanel/services/tests.py create mode 100644 horizon/horizon/dashboards/syspanel/services/urls.py create mode 100644 horizon/horizon/dashboards/syspanel/services/views.py create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/base.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/_create.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/_delete.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/_form.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/_list.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/create.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/index.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/images/_delete.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/images/_form.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/images/_list.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/images/_toggle.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/images/index.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/images/update.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/instances/_list.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/instances/detail.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/instances/index.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/instances/tenant_usage.csv create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/instances/tenant_usage.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/instances/usage.csv create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/instances/usage.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/quotas/index.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/services/_list.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/services/_toggle.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/services/index.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_add_user.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_create_form.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_delete.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_form.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_list.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_quotas_form.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_remove_user.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_update_form.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_update_quotas_form.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/create.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/index.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/quotas.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/update.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/users.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/users/_create_form.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/users/_delete.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/users/_enable_disable.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/users/_form.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/users/_toggle_enabled.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/users/_update_form.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/users/create.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/users/index.html create mode 100644 horizon/horizon/dashboards/syspanel/templates/syspanel/users/update.html create mode 100644 horizon/horizon/dashboards/syspanel/tenants/__init__.py create mode 100644 horizon/horizon/dashboards/syspanel/tenants/forms.py create mode 100644 horizon/horizon/dashboards/syspanel/tenants/panel.py create mode 100644 horizon/horizon/dashboards/syspanel/tenants/tests.py create mode 100644 horizon/horizon/dashboards/syspanel/tenants/urls.py create mode 100644 horizon/horizon/dashboards/syspanel/tenants/views.py create mode 100644 horizon/horizon/dashboards/syspanel/users/__init__.py create mode 100644 horizon/horizon/dashboards/syspanel/users/forms.py create mode 100644 horizon/horizon/dashboards/syspanel/users/panel.py create mode 100644 horizon/horizon/dashboards/syspanel/users/tests.py create mode 100644 horizon/horizon/dashboards/syspanel/users/urls.py create mode 100644 horizon/horizon/dashboards/syspanel/users/views.py create mode 100644 horizon/horizon/decorators.py create mode 100644 horizon/horizon/exceptions.py create mode 100644 horizon/horizon/forms.py create mode 100644 horizon/horizon/locale/es/LC_MESSAGES/django.mo create mode 100644 horizon/horizon/locale/es/LC_MESSAGES/django.po create mode 100644 horizon/horizon/locale/fr/LC_MESSAGES/django.mo create mode 100644 horizon/horizon/locale/fr/LC_MESSAGES/django.po create mode 100644 horizon/horizon/locale/ja/LC_MESSAGES/django.mo create mode 100644 horizon/horizon/locale/ja/LC_MESSAGES/django.po create mode 100644 horizon/horizon/locale/pl/LC_MESSAGES/django.mo create mode 100644 horizon/horizon/locale/pl/LC_MESSAGES/django.po create mode 100644 horizon/horizon/locale/pt/LC_MESSAGES/django.mo create mode 100644 horizon/horizon/locale/pt/LC_MESSAGES/django.po create mode 100644 horizon/horizon/locale/zh-cn/LC_MESSAGES/django.mo create mode 100644 horizon/horizon/locale/zh-cn/LC_MESSAGES/django.po create mode 100644 horizon/horizon/locale/zh-tw/LC_MESSAGES/django.mo create mode 100644 horizon/horizon/locale/zh-tw/LC_MESSAGES/django.po create mode 100644 horizon/horizon/middleware.py create mode 100644 horizon/horizon/models.py create mode 100644 horizon/horizon/site_urls.py create mode 100644 horizon/horizon/templates/horizon/_nav_list.html create mode 100644 horizon/horizon/templates/horizon/_subnav_list.html create mode 100644 horizon/horizon/templates/horizon/auth/_login.html create mode 100644 horizon/horizon/templates/horizon/auth/_switch.html create mode 100644 horizon/horizon/templates/horizon/common/_page_header.html create mode 100644 horizon/horizon/templates/horizon/common/_sidebar.html create mode 100644 horizon/horizon/templates/horizon/common/_sidebar_module.html create mode 100644 horizon/horizon/templates/horizon/common/instances/_reboot.html create mode 100644 horizon/horizon/templates/horizon/common/instances/_terminate.html create mode 100644 horizon/horizon/templatetags/__init__.py create mode 100644 horizon/horizon/templatetags/branding.py create mode 100644 horizon/horizon/templatetags/horizon.py create mode 100644 horizon/horizon/templatetags/parse_date.py create mode 100644 horizon/horizon/templatetags/sizeformat.py create mode 100644 horizon/horizon/templatetags/swift_paging.py create mode 100644 horizon/horizon/templatetags/truncate_filter.py create mode 100644 horizon/horizon/test.py create mode 100644 horizon/horizon/tests/__init__.py create mode 100644 horizon/horizon/tests/api_tests/__init__.py create mode 100644 horizon/horizon/tests/api_tests/base.py create mode 100644 horizon/horizon/tests/api_tests/glance.py create mode 100644 horizon/horizon/tests/api_tests/keystone.py create mode 100644 horizon/horizon/tests/api_tests/nova.py create mode 100644 horizon/horizon/tests/api_tests/swift.py create mode 100644 horizon/horizon/tests/api_tests/utils.py create mode 100644 horizon/horizon/tests/auth_tests.py create mode 100644 horizon/horizon/tests/base_tests.py create mode 100644 horizon/horizon/tests/context_processor_tests.py create mode 100644 horizon/horizon/tests/templates/base-sidebar.html create mode 100644 horizon/horizon/tests/templates/base.html create mode 100644 horizon/horizon/tests/templates/splash.html create mode 100644 horizon/horizon/tests/templates/switch_tenants.html create mode 100644 horizon/horizon/tests/templatetag_tests.py create mode 100644 horizon/horizon/tests/testsettings.py create mode 100644 horizon/horizon/tests/testurls.py create mode 100644 horizon/horizon/tests/views.py create mode 100644 horizon/horizon/users.py create mode 100644 horizon/horizon/version.py create mode 100644 horizon/horizon/views/__init__.py create mode 100644 horizon/horizon/views/auth.py create mode 100644 horizon/horizon/views/auth_forms.py create mode 100644 horizon/setup.py create mode 100644 openstack-dashboard/dashboard/static/dashboard/images/favicon.ico delete mode 100644 openstack-dashboard/dashboard/tests.py diff --git a/.bzrignore b/.bzrignore index cdd77172..cda7dea6 100644 --- a/.bzrignore +++ b/.bzrignore @@ -1,11 +1,11 @@ -django-openstack/.installed.cfg -django-openstack/bin -django-openstack/develop-eggs/ -django-openstack/downloads/ -django-openstack/eggs/ -django-openstack/parts/ -django-openstack/src/django_nova.egg-info -django-openstack/src/django_openstack.egg-info +horizon/.installed.cfg +horizon/bin +horizon/develop-eggs/ +horizon/downloads/ +horizon/eggs/ +horizon/parts/ +horizon/src/django_nova.egg-info +horizon/src/django_openstack.egg-info django-nova-syspanel/src/django_nova_syspanel.egg-info openstack-dashboard/.dashboard-venv openstack-dashboard/local/dashboard_openstack.sqlite3 diff --git a/.gitignore b/.gitignore index e6bdfce9..b797b11f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,19 +5,20 @@ coverage.xml pep8.txt pylint.txt reports -django-openstack/.installed.cfg -django-openstack/bin -django-openstack/develop-eggs/ -django-openstack/downloads/ -django-openstack/eggs/ -django-openstack/htmlcov -django-openstack/launchpad -django-openstack/parts/ -django-openstack/django_nova.egg-info -django-openstack/django_openstack.egg-info +horizon/.installed.cfg +horizon/bin +horizon/develop-eggs/ +horizon/downloads/ +horizon/eggs/ +horizon/htmlcov +horizon/launchpad +horizon/parts/ +horizon/django_nova.egg-info +horizon/horizon.egg-info +horizon/django_openstack.egg-info django-nova-syspanel/src/django_nova_syspanel.egg-info openstack-dashboard/.dashboard-venv openstack-dashboard/local/dashboard_openstack.sqlite3 openstack-dashboard/local/local_settings.py build/ -doc/source/sourcecode +docs/source/sourcecode diff --git a/README b/README index edbfb156..2492bc59 100644 --- a/README +++ b/README @@ -4,14 +4,14 @@ OpenStack Dashboard (Horizon) The OpenStack Dashboard is a Django based reference implementation of a web based management interface for OpenStack. -It is based on django-openstack, which is designed to be a generic Django -module that can be re-used in other sites. +It is based on the ``horizon`` module, which is designed to be a generic Django +app that can be re-used in other projects. For more information about how to get started with the OpenStack Dashboard, view the README file in the openstack-dashboard folder. -For more information about working directly with django-openstack, see the -README file in the django-openstack folder. +For more information about working directly with ``horizon``, see the +README file in the ``horizon`` folder. For release management: @@ -29,21 +29,21 @@ Project Structure and Testing: ------------------------------ This project is a bit different from other Openstack projects in that it has -two very distinct components underneath it: django-openstack, and -openstack-dashboard. +two very distinct components underneath it: ``horizon``, and +``openstack-dashboard``. -django-openstack holds the generic libraries and components that can be -used in any Django project. In testing, this component is set up with -buildout (see run_tests.sh), and any dependencies that get added need to -be added to the django-openstack/buildout.cfg file. +The ``horizon`` directory holds the generic libraries and components that can +be used in any Django project. In testing, this component is set up with +buildout (see ``run_tests.sh``), and any dependencies that get added need to +be added to the ``horizon/buildout.cfg`` file. -openstack-dashboard is a reference django project that uses django-openstack -and is built with a virtualenv and tested through that environment. If -depdendencies are added that the reference django project needs, they -should be added to openstack-dashboard/tools/pip-requires. +The ``openstack-dashboard`` directory contains a reference Django project that +uses ``horizon`` and is built with a virtualenv and tested through that +environment. If dependencies are added that ``openstack-dashboard`` requires +they should be added to ``openstack-dashboard/tools/pip-requires``. -The run_tests.sh script invokes tests and analysis on both of these -components in it's process, and is what Jenkins uses to verify the +The ``run_tests.sh`` script invokes tests and analyses on both of these +components in its process, and is what Jenkins uses to verify the stability of the project. To run the tests:: @@ -55,7 +55,7 @@ Building Contributor Documentation This documentation is written by contributors, for contributors. -The source is maintained in the `doc/source` folder using +The source is maintained in the ``docs/source`` folder using `reStructuredText`_ and built by `Sphinx`_ .. _reStructuredText: http://docutils.sourceforge.net/rst.html diff --git a/django-openstack/LICENSE b/django-openstack/LICENSE deleted file mode 100644 index 68c771a0..00000000 --- a/django-openstack/LICENSE +++ /dev/null @@ -1,176 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - diff --git a/django-openstack/Makefile b/django-openstack/Makefile deleted file mode 100644 index 8496df21..00000000 --- a/django-openstack/Makefile +++ /dev/null @@ -1,43 +0,0 @@ -PYTHON=`which python` -DESTDIR=/ -BUILDIR=$(CURDIR)/debian/django-openstack -PROJECT=django-openstack - -all: - @echo "make buildout - Run through buildout" - @echo "make test - Run tests" - @echo "make source - Create source package" - @echo "make install - Install on local system" - @echo "make buildrpm - Generate a rpm package" - @echo "make builddeb - Generate a deb package" - @echo "make clean - Get rid of scratch and byte files" - -buildout: ./bin/buildout - ./bin/buildout - -./bin/buildout: - $(PYTHON) bootstrap.py - -source: - $(PYTHON) setup.py sdist $(COMPILE) - -install: - $(PYTHON) setup.py install --root $(DESTDIR) $(COMPILE) - -buildrpm: - $(PYTHON) setup.py bdist_rpm --post-install=rpm/postinstall --pre-uninstall=rpm/preuninstall - -builddeb: - # build the source package in the parent directory - # then rename it to project_version.orig.tar.gz - $(PYTHON) setup.py sdist $(COMPILE) --dist-dir=../ - rename -f 's/$(PROJECT)-(.*)\.tar\.gz/$(PROJECT)_$$1\.orig\.tar\.gz/' ../* - # build the package - #dpkg-buildpackage -i -I -rfakeroot - dpkg-buildpackage -b -rfakeroot -tc -uc -D - -clean: - $(PYTHON) setup.py clean - $(MAKE) -f $(CURDIR)/debian/rules clean - rm -rf build/ MANIFEST - find . -name '*.pyc' -delete diff --git a/django-openstack/README b/django-openstack/README deleted file mode 100644 index 0323e220..00000000 --- a/django-openstack/README +++ /dev/null @@ -1,56 +0,0 @@ -Django-OpenStack ---------------------- - -The Django-OpenStack project is a Django module that is used to provide web based -interactions with an OpenStack cloud. - -There is a reference implementation that uses this module located at: - - http://launchpad.net/horizon - -It is highly recommended that you make use of this reference implementation -so that changes you make can be visualized effectively and are consistent. -Using this reference implementation as a development environment will greatly -simplify development of the django-openstack module. - -Of course, if you are developing your own Django site using django-openstack, then -you can disregard this advice. - - - -Getting Started ---------------- - -Django-OpenStack uses Buildout (http://www.buildout.org/) to manage local -development. To configure your local Buildout environment first install the following -system-level dependencies: - * python-dev - * git - * bzr - -Then instantiate buildout with - - $ python bootstrap.py - $ bin/buildout - -This will install all the dependencies of django-openstack and provide some useful -scripts in the bin/ directory: - - bin/python provides a python shell for the current buildout. - bin/django provides django functions for the current buildout. - - -You should now be able to run unit tests as follows: - - $ bin/django test -or - $ bin/test - -You can run unit tests with code coverage on django_openstack by setting -NOSE_WITH_COVERAGE: - - $ NOSE_WITH_COVERAGE=true bin/test - -Get even better coverage info by running coverage directly: - - $ coverage run --branch --source django_openstack bin/django test django_openstack && coverage html diff --git a/django-openstack/bootstrap.py b/django-openstack/bootstrap.py deleted file mode 100644 index 5f2cb083..00000000 --- a/django-openstack/bootstrap.py +++ /dev/null @@ -1,260 +0,0 @@ -############################################################################## -# -# Copyright (c) 2006 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## -"""Bootstrap a buildout-based project - -Simply run this script in a directory containing a buildout.cfg. -The script accepts buildout command-line options, so you can -use the -c option to specify an alternate configuration file. -""" - -import os, shutil, sys, tempfile, textwrap, urllib, urllib2, subprocess -from optparse import OptionParser - -if sys.platform == 'win32': - def quote(c): - if ' ' in c: - return '"%s"' % c # work around spawn lamosity on windows - else: - return c -else: - quote = str - -# See zc.buildout.easy_install._has_broken_dash_S for motivation and comments. -stdout, stderr = subprocess.Popen( - [sys.executable, '-Sc', - 'try:\n' - ' import ConfigParser\n' - 'except ImportError:\n' - ' print 1\n' - 'else:\n' - ' print 0\n'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() -has_broken_dash_S = bool(int(stdout.strip())) - -# In order to be more robust in the face of system Pythons, we want to -# run without site-packages loaded. This is somewhat tricky, in -# particular because Python 2.6's distutils imports site, so starting -# with the -S flag is not sufficient. However, we'll start with that: -if not has_broken_dash_S and 'site' in sys.modules: - # We will restart with python -S. - args = sys.argv[:] - args[0:0] = [sys.executable, '-S'] - args = map(quote, args) - os.execv(sys.executable, args) -# Now we are running with -S. We'll get the clean sys.path, import site -# because distutils will do it later, and then reset the path and clean -# out any namespace packages from site-packages that might have been -# loaded by .pth files. -clean_path = sys.path[:] -import site -sys.path[:] = clean_path -for k, v in sys.modules.items(): - if k in ('setuptools', 'pkg_resources') or ( - hasattr(v, '__path__') and - len(v.__path__)==1 and - not os.path.exists(os.path.join(v.__path__[0],'__init__.py'))): - # This is a namespace package. Remove it. - sys.modules.pop(k) - -is_jython = sys.platform.startswith('java') - -setuptools_source = 'http://peak.telecommunity.com/dist/ez_setup.py' -distribute_source = 'http://python-distribute.org/distribute_setup.py' - -# parsing arguments -def normalize_to_url(option, opt_str, value, parser): - if value: - if '://' not in value: # It doesn't smell like a URL. - value = 'file://%s' % ( - urllib.pathname2url( - os.path.abspath(os.path.expanduser(value))),) - if opt_str == '--download-base' and not value.endswith('/'): - # Download base needs a trailing slash to make the world happy. - value += '/' - else: - value = None - name = opt_str[2:].replace('-', '_') - setattr(parser.values, name, value) - -usage = '''\ -[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] - -Bootstraps a buildout-based project. - -Simply run this script in a directory containing a buildout.cfg, using the -Python that you want bin/buildout to use. - -Note that by using --setup-source and --download-base to point to -local resources, you can keep this script from going over the network. -''' - -parser = OptionParser(usage=usage) -parser.add_option("-v", "--version", dest="version", - help="use a specific zc.buildout version") -parser.add_option("-d", "--distribute", - action="store_true", dest="use_distribute", default=False, - help="Use Distribute rather than Setuptools.") -parser.add_option("--setup-source", action="callback", dest="setup_source", - callback=normalize_to_url, nargs=1, type="string", - help=("Specify a URL or file location for the setup file. " - "If you use Setuptools, this will default to " + - setuptools_source + "; if you use Distribute, this " - "will default to " + distribute_source +".")) -parser.add_option("--download-base", action="callback", dest="download_base", - callback=normalize_to_url, nargs=1, type="string", - help=("Specify a URL or directory for downloading " - "zc.buildout and either Setuptools or Distribute. " - "Defaults to PyPI.")) -parser.add_option("--eggs", - help=("Specify a directory for storing eggs. Defaults to " - "a temporary directory that is deleted when the " - "bootstrap script completes.")) -parser.add_option("-t", "--accept-buildout-test-releases", - dest='accept_buildout_test_releases', - action="store_true", default=False, - help=("Normally, if you do not specify a --version, the " - "bootstrap script and buildout gets the newest " - "*final* versions of zc.buildout and its recipes and " - "extensions for you. If you use this flag, " - "bootstrap and buildout will get the newest releases " - "even if they are alphas or betas.")) -parser.add_option("-c", None, action="store", dest="config_file", - help=("Specify the path to the buildout configuration " - "file to be used.")) - -options, args = parser.parse_args() - -# if -c was provided, we push it back into args for buildout's main function -if options.config_file is not None: - args += ['-c', options.config_file] - -if options.eggs: - eggs_dir = os.path.abspath(os.path.expanduser(options.eggs)) -else: - eggs_dir = tempfile.mkdtemp() - -if options.setup_source is None: - if options.use_distribute: - options.setup_source = distribute_source - else: - options.setup_source = setuptools_source - -if options.accept_buildout_test_releases: - args.append('buildout:accept-buildout-test-releases=true') -args.append('bootstrap') - -try: - import pkg_resources - import setuptools # A flag. Sometimes pkg_resources is installed alone. - if not hasattr(pkg_resources, '_distribute'): - raise ImportError -except ImportError: - ez_code = urllib2.urlopen( - options.setup_source).read().replace('\r\n', '\n') - ez = {} - exec ez_code in ez - setup_args = dict(to_dir=eggs_dir, download_delay=0) - if options.download_base: - setup_args['download_base'] = options.download_base - if options.use_distribute: - setup_args['no_fake'] = True - ez['use_setuptools'](**setup_args) - if 'pkg_resources' in sys.modules: - reload(sys.modules['pkg_resources']) - import pkg_resources - # This does not (always?) update the default working set. We will - # do it. - for path in sys.path: - if path not in pkg_resources.working_set.entries: - pkg_resources.working_set.add_entry(path) - -cmd = [quote(sys.executable), - '-c', - quote('from setuptools.command.easy_install import main; main()'), - '-mqNxd', - quote(eggs_dir)] - -if not has_broken_dash_S: - cmd.insert(1, '-S') - -find_links = options.download_base -if not find_links: - find_links = os.environ.get('bootstrap-testing-find-links') -if find_links: - cmd.extend(['-f', quote(find_links)]) - -if options.use_distribute: - setup_requirement = 'distribute' -else: - setup_requirement = 'setuptools' -ws = pkg_resources.working_set -setup_requirement_path = ws.find( - pkg_resources.Requirement.parse(setup_requirement)).location -env = dict( - os.environ, - PYTHONPATH=setup_requirement_path) - -requirement = 'zc.buildout' -version = options.version -if version is None and not options.accept_buildout_test_releases: - # Figure out the most recent final version of zc.buildout. - import setuptools.package_index - _final_parts = '*final-', '*final' - def _final_version(parsed_version): - for part in parsed_version: - if (part[:1] == '*') and (part not in _final_parts): - return False - return True - index = setuptools.package_index.PackageIndex( - search_path=[setup_requirement_path]) - if find_links: - index.add_find_links((find_links,)) - req = pkg_resources.Requirement.parse(requirement) - if index.obtain(req) is not None: - best = [] - bestv = None - for dist in index[req.project_name]: - distv = dist.parsed_version - if _final_version(distv): - if bestv is None or distv > bestv: - best = [dist] - bestv = distv - elif distv == bestv: - best.append(dist) - if best: - best.sort() - version = best[-1].version -if version: - requirement = '=='.join((requirement, version)) -cmd.append(requirement) - -if is_jython: - import subprocess - exitcode = subprocess.Popen(cmd, env=env).wait() -else: # Windows prefers this, apparently; otherwise we would prefer subprocess - exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env])) -if exitcode != 0: - sys.stdout.flush() - sys.stderr.flush() - print ("An error occurred when trying to install zc.buildout. " - "Look above this message for any errors that " - "were output by easy_install.") - sys.exit(exitcode) - -ws.add_entry(eggs_dir) -ws.require(requirement) -import zc.buildout.buildout -zc.buildout.buildout.main(args) -if not options.eggs: # clean up temporary egg directory - shutil.rmtree(eggs_dir) diff --git a/django-openstack/buildout.cfg b/django-openstack/buildout.cfg deleted file mode 100644 index 0858569f..00000000 --- a/django-openstack/buildout.cfg +++ /dev/null @@ -1,125 +0,0 @@ -[buildout] -parts = - django - launchpad - openstack-compute - openstackx - python-novaclient - python-keystoneclient -develop = . -versions = versions - - -[versions] -django = 1.3 -# the following are for glance-dependencies -eventlet = 0.9.12 -greenlet = 0.3.1 -pep8 = 0.5.0 -sqlalchemy = 0.6.3 -sqlalchemy-migrate = 0.6 -webob = 1.0.8 - - -[dependencies] -# dependencies that are found locally ${buildout:directory}/module -# or can be fetched from pypi -recipe = zc.recipe.egg -eggs = - django-mailer - httplib2 - python-cloudfiles - coverage - glance - quantum -interpreter = python - - -# glance doesn't have a client, and installing -# from bzr doesn't install deps -[glance-dependencies] -recipe = zc.recipe.egg -eggs = - PasteDeploy - anyjson - argparse - eventlet - greenlet - kombu - paste - pep8 - routes - sqlalchemy - sqlalchemy-migrate - webob - xattr -interpreter = python - - -[django-openstack] -recipe = zc.recipe.egg -eggs = django-openstack -interpreter = python - - -[django] -# defines settings for django -# any dependencies that cannot be satisifed via the dependencies -# recipe above will need to be added to the extra-paths here. -# IE, dependencies fetch from a git repo will not auto-populate -# like the zc.recipe.egg ones will -recipe = djangorecipe -project = django_openstack -projectegg = django_openstack -settings = tests -test = django_openstack -eggs = - ${dependencies:eggs} - ${django-openstack:eggs} - ${glance-dependencies:eggs} -extra-paths = - ${buildout:directory}/parts/openstack-compute - ${buildout:directory}/parts/openstackx - ${buildout:directory}/parts/python-novaclient - ${buildout:directory}/parts/python-keystoneclient - - -## Dependencies fetch from git -# git dependencies end up as a subdirectory of ${buildout:directory}/parts/ -[openstack-compute] -recipe = zerokspot.recipe.git -repository = git://github.com/jacobian/openstack.compute.git -as_egg = True - -[openstackx] -recipe = zerokspot.recipe.git -repository = git://github.com/cloudbuilders/openstackx.git -as_egg = True - -[python-novaclient] -recipe = zerokspot.recipe.git -repository = git://github.com/rackspace/python-novaclient.git -as_egg = True - -[python-keystoneclient] -recipe = zerokspot.recipe.git -repository = git://github.com/4P/python-keystoneclient.git -as_egg = True - -## Dependencies fetched from launchpad -# launchpad dependencies will appear as subfolders of -# ${buildout:directory}/launchpad/ -# multiple urls can be specified, format is -# branch_url subfolder_name -# don't forget to add directory to extra_paths in [django] -[launchpad] -recipe = bazaarrecipe -urls = - https://launchpad.net/~hudson-openstack/glance/trunk/ glance - - -## Dependencies fetch from other bzr locations -#[bzrdeps] -#recipe = bazaarrecipe -#urls = -# https://launchpad.net/~hudson-openstack/glance/trunk/ glance diff --git a/django-openstack/django_openstack/__init__.py b/django-openstack/django_openstack/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/django-openstack/django_openstack/api.py b/django-openstack/django_openstack/api.py deleted file mode 100644 index 60b9f16e..00000000 --- a/django-openstack/django_openstack/api.py +++ /dev/null @@ -1,1093 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Methods and interface objects used to interact with external apis. - -API method calls return objects that are in many cases objects with -attributes that are direct maps to the data returned from the API http call. -Unfortunately, these objects are also often constructed dynamically, making -it difficult to know what data is available from the API object. Because of -this, all API calls should wrap their returned object in one defined here, -using only explicitly defined atributes and/or methods. - -In other words, django_openstack developers not working on django_openstack.api -shouldn't need to understand the finer details of APIs for -Keystone/Nova/Glance/Swift et. al. -""" - -import httplib -import json -import logging -import urlparse - -from django.conf import settings -from django.contrib import messages - -import cloudfiles -from django_openstack import exceptions -import openstack.compute -import openstackx.admin -import openstackx.api.exceptions as api_exceptions -import openstackx.extras -import openstackx.auth -from glance import client as glance_client -from glance.common import exception as glance_exceptions -from novaclient import client as base_nova_client -from keystoneclient import exceptions as keystone_exceptions -from keystoneclient.v2_0 import client as keystone_client -from novaclient.v1_1 import client as nova_client -from quantum import client as quantum_client - -LOG = logging.getLogger('django_openstack.api') - - -class APIResourceWrapper(object): - """ Simple wrapper for api objects - - Define _attrs on the child class and pass in the - api object as the only argument to the constructor - """ - _attrs = [] - - def __init__(self, apiresource): - self._apiresource = apiresource - - def __getattr__(self, attr): - if attr in self._attrs: - # __getattr__ won't find properties - return self._apiresource.__getattribute__(attr) - else: - LOG.debug('Attempted to access unknown attribute "%s" on' - ' APIResource object of type "%s" wrapping resource of' - ' type "%s"' % (attr, self.__class__, - self._apiresource.__class__)) - raise AttributeError(attr) - - -class APIDictWrapper(object): - """ Simple wrapper for api dictionaries - - Some api calls return dictionaries. This class provides identical - behavior as APIResourceWrapper, except that it will also behave as a - dictionary, in addition to attribute accesses. - - Attribute access is the preferred method of access, to be - consistent with api resource objects from openstackx - """ - def __init__(self, apidict): - self._apidict = apidict - - def __getattr__(self, attr): - if attr in self._attrs: - try: - return self._apidict[attr] - except KeyError, e: - raise AttributeError(e) - - else: - LOG.debug('Attempted to access unknown item "%s" on' - 'APIResource object of type "%s"' - % (attr, self.__class__)) - raise AttributeError(attr) - - def __getitem__(self, item): - try: - return self.__getattr__(item) - except AttributeError, e: - # caller is expecting a KeyError - raise KeyError(e) - - def get(self, item, default=None): - try: - return self.__getattr__(item) - except AttributeError: - return default - - -class Container(APIResourceWrapper): - """Simple wrapper around cloudfiles.container.Container""" - _attrs = ['name'] - - -class Console(APIResourceWrapper): - """Simple wrapper around openstackx.extras.consoles.Console""" - _attrs = ['id', 'output', 'type'] - - -class Flavor(APIResourceWrapper): - """Simple wrapper around openstackx.admin.flavors.Flavor""" - _attrs = ['disk', 'id', 'links', 'name', 'ram', 'vcpus'] - - -class FloatingIp(APIResourceWrapper): - """Simple wrapper for floating ips""" - _attrs = ['ip', 'fixed_ip', 'instance_id', 'id'] - - -class Image(APIDictWrapper): - """Simple wrapper around glance image dictionary""" - _attrs = ['checksum', 'container_format', 'created_at', 'deleted', - 'deleted_at', 'disk_format', 'id', 'is_public', 'location', - 'name', 'properties', 'size', 'status', 'updated_at', 'owner'] - - def __getattr__(self, attrname): - if attrname == "properties": - return ImageProperties(super(Image, self).__getattr__(attrname)) - else: - return super(Image, self).__getattr__(attrname) - - -class ImageProperties(APIDictWrapper): - """Simple wrapper around glance image properties dictionary""" - _attrs = ['architecture', 'image_location', 'image_state', 'kernel_id', - 'project_id', 'ramdisk_id'] - - -class KeyPair(APIResourceWrapper): - """Simple wrapper around openstackx.extras.keypairs.Keypair""" - _attrs = ['fingerprint', 'name', 'private_key'] - - -class Volume(APIResourceWrapper): - """Nova Volume representation""" - _attrs = ['id', 'status', 'displayName', 'size', 'volumeType', 'createdAt', - 'attachments', 'displayDescription'] - - -class Server(APIResourceWrapper): - """Simple wrapper around openstackx.extras.server.Server - - Preserves the request info so image name can later be retrieved - """ - _attrs = ['addresses', 'attrs', 'hostId', 'id', 'image', 'links', - 'metadata', 'name', 'private_ip', 'public_ip', 'status', 'uuid', - 'image_name', 'VirtualInterfaces'] - - def __init__(self, apiresource, request): - super(Server, self).__init__(apiresource) - self.request = request - - def __getattr__(self, attr): - if attr == "attrs": - return ServerAttributes(super(Server, self).__getattr__(attr)) - else: - return super(Server, self).__getattr__(attr) - - @property - def image_name(self): - try: - image = image_get(self.request, self.image['id']) - return image.name - except glance_exceptions.NotFound: - return "(not found)" - - def reboot(self, hardness=openstack.compute.servers.REBOOT_HARD): - compute_api(self.request).servers.reboot(self.id, hardness) - - -class ServerAttributes(APIDictWrapper): - """Simple wrapper around openstackx.extras.server.Server attributes - - Preserves the request info so image name can later be retrieved - """ - _attrs = ['description', 'disk_gb', 'host', 'image_ref', 'kernel_id', - 'key_name', 'launched_at', 'mac_address', 'memory_mb', 'name', - 'os_type', 'tenant_id', 'ramdisk_id', 'scheduled_at', - 'terminated_at', 'user_data', 'user_id', 'vcpus', 'hostname', - 'security_groups'] - - -class Services(APIResourceWrapper): - _attrs = ['disabled', 'host', 'id', 'last_update', 'stats', 'type', 'up', - 'zone'] - - -class SwiftObject(APIResourceWrapper): - _attrs = ['name'] - - -class Tenant(APIResourceWrapper): - """Simple wrapper around keystoneclient.tenants.Tenant""" - _attrs = ['id', 'description', 'enabled', 'name'] - - -class Token(APIResourceWrapper): - """Simple wrapper around keystoneclient.tokens.Tenant""" - _attrs = ['id', 'user', 'serviceCatalog', 'tenant'] - - -class Usage(APIResourceWrapper): - """Simple wrapper around openstackx.extras.usage.Usage""" - _attrs = ['begin', 'instances', 'stop', 'tenant_id', - 'total_active_disk_size', 'total_active_instances', - 'total_active_ram_size', 'total_active_vcpus', 'total_cpu_usage', - 'total_disk_usage', 'total_hours', 'total_ram_usage'] - - -class User(APIResourceWrapper): - """Simple wrapper around keystoneclient.users.User""" - _attrs = ['email', 'enabled', 'id', 'tenantId', 'name'] - - -class Role(APIResourceWrapper): - """Wrapper around keystoneclient.roles.role""" - _attrs = ['id', 'name', 'description', 'service_id'] - - -class SecurityGroup(APIResourceWrapper): - """Simple wrapper around openstackx.extras.security_groups.SecurityGroup""" - _attrs = ['id', 'name', 'description', 'tenant_id', 'rules'] - - -class SecurityGroupRule(APIResourceWrapper): - """Simple wrapper around - openstackx.extras.security_groups.SecurityGroupRule""" - _attrs = ['id', 'parent_group_id', 'group_id', 'ip_protocol', - 'from_port', 'to_port', 'groups', 'ip_ranges'] - - -class SecurityGroupRule(APIResourceWrapper): - """Simple wrapper around openstackx.extras.users.User""" - _attrs = ['id', 'name', 'description', 'tenant_id', 'security_group_rules'] - - -class SwiftAuthentication(object): - """Auth container to pass CloudFiles storage URL and token from - session. - """ - def __init__(self, storage_url, auth_token): - self.storage_url = storage_url - self.auth_token = auth_token - - def authenticate(self): - return (self.storage_url, '', self.auth_token) - - -class ServiceCatalogException(api_exceptions.ApiException): - def __init__(self, service_name): - message = 'Invalid service catalog service: %s' % service_name - super(ServiceCatalogException, self).__init__(404, message) - - -class VirtualInterface(APIResourceWrapper): - _attrs = ['id', 'mac_address'] - - -def get_service_from_catalog(catalog, service_type): - for service in catalog: - if service['type'] == service_type: - return service - return None - - -def url_for(request, service_type, admin=False): - catalog = request.user.service_catalog - service = get_service_from_catalog(catalog, service_type) - if service: - try: - if admin: - return service['endpoints'][0]['adminURL'] - else: - return service['endpoints'][0]['internalURL'] - except (IndexError, KeyError): - raise ServiceCatalogException(service_type) - else: - raise ServiceCatalogException(service_type) - - -def check_openstackx(f): - """Decorator that adds extra info to api exceptions - - The OpenStack Dashboard currently depends on openstackx extensions - being present in Nova. Error messages depending for views depending - on these extensions do not lead to the conclusion that Nova is missing - extensions. - - This decorator should be dropped and removed after Keystone and - Horizon more gracefully handle extensions and openstackx extensions - aren't required by Horizon in Nova. - """ - def inner(*args, **kwargs): - try: - return f(*args, **kwargs) - except api_exceptions.NotFound, e: - e.message = e.details or '' - e.message += ' This error may be caused by a misconfigured' \ - ' Nova url in keystone\'s service catalog, or ' \ - ' by missing openstackx extensions in Nova. ' \ - ' See the Horizon README.' - raise - - return inner - - -def compute_api(request): - compute = openstack.compute.Compute( - auth_token=request.user.token, - management_url=url_for(request, 'compute')) - # this below hack is necessary to make the jacobian compute client work - # TODO(mgius): It looks like this is unused now? - compute.client.auth_token = request.user.token - compute.client.management_url = url_for(request, 'compute') - LOG.debug('compute_api connection created using token "%s"' - ' and url "%s"' % - (request.user.token, url_for(request, 'compute'))) - return compute - - -def glance_api(request): - o = urlparse.urlparse(url_for(request, 'image')) - LOG.debug('glance_api connection created for host "%s:%d"' % - (o.hostname, o.port)) - return glance_client.Client(o.hostname, - o.port, - auth_tok=request.user.token) - - -def admin_api(request): - LOG.debug('admin_api connection created using token "%s"' - ' and url "%s"' % - (request.user.token, url_for(request, 'compute', True))) - return openstackx.admin.Admin(auth_token=request.user.token, - management_url=url_for(request, 'compute', True)) - - -def extras_api(request): - LOG.debug('extras_api connection created using token "%s"' - ' and url "%s"' % - (request.user.token, url_for(request, 'compute'))) - return openstackx.extras.Extras(auth_token=request.user.token, - management_url=url_for(request, 'compute')) - - -def novaclient(request): - LOG.debug('novaclient connection created using token "%s" and url "%s"' % - (request.user.token, url_for(request, 'compute'))) - c = nova_client.Client(username=request.user.username, - api_key=request.user.token, - project_id=request.user.tenant_id, - auth_url=url_for(request, 'compute')) - c.client.auth_token = request.user.token - c.client.management_url = url_for(request, 'compute') - return c - - -def keystoneclient(request, username=None, password=None, tenant_id=None, - token_id=None, endpoint=None): - """Returns a client connected to the Keystone backend. - - Several forms of authentication are supported: - - * Username + password -> Unscoped authentication - * Username + password + tenant id -> Scoped authentication - * Unscoped token -> Unscoped authentication - * Unscoped token + tenant id -> Scoped authentication - * Scoped token -> Scoped authentication - - Available services and data from the backend will vary depending on - whether the authentication was scoped or unscoped. - - Lazy authentication if an ``endpoint`` parameter is provided. - - The client is cached so that subsequent API calls during the same - request/response cycle don't have to be re-authenticated. - """ - # Take care of client connection caching/fetching a new client - user = request.user - if (hasattr(request, '_keystone') and - request._keystone.auth_token == user.token): - conn = request._keystone - else: - conn = keystone_client.Client(username=username or user.username, - password=password, - project_id=tenant_id or user.tenant_id, - token=token_id or user.token, - auth_url=settings.OPENSTACK_KEYSTONE_URL, - endpoint=endpoint) - request._keystone = conn - - # Fetch the correct endpoint for the user type - catalog = getattr(conn, 'service_catalog', None) - if catalog and "serviceCatalog" in catalog.catalog.keys(): - if user.is_admin(): - endpoint = catalog.url_for(service_type='identity', - endpoint_type='adminURL') - else: - endpoint = catalog.url_for(service_type='identity', - endpoint_type='publicURL') - else: - endpoint = settings.OPENSTACK_KEYSTONE_URL - conn.management_url = endpoint - - return conn - - -def swift_api(request): - LOG.debug('object store connection created using token "%s"' - ' and url "%s"' % - (request.session['token'], url_for(request, 'object-store'))) - auth = SwiftAuthentication(url_for(request, 'object-store'), - request.session['token']) - return cloudfiles.get_connection(auth=auth) - - -def quantum_api(request): - tenant = None - if hasattr(request, 'user'): - tenant = request.user.tenant_id - else: - tenant = settings.QUANTUM_TENANT - - return quantum_client.Client(settings.QUANTUM_URL, settings.QUANTUM_PORT, - False, tenant, 'json') - - -def console_create(request, instance_id, kind='text'): - return Console(extras_api(request).consoles.create(instance_id, kind)) - - -def flavor_create(request, name, memory, vcpu, disk, flavor_id): - # TODO -- convert to novaclient when novaclient adds create support - return Flavor(admin_api(request).flavors.create( - name, int(memory), int(vcpu), int(disk), flavor_id)) - - -def flavor_delete(request, flavor_id, purge=False): - # TODO -- convert to novaclient when novaclient adds delete support - admin_api(request).flavors.delete(flavor_id, purge) - - -def flavor_get(request, flavor_id): - return Flavor(novaclient(request).flavors.get(flavor_id)) - - -def flavor_list(request): - return [Flavor(f) for f in novaclient(request).flavors.list()] - - -def tenant_floating_ip_list(request): - """ - Fetches a list of all floating ips. - """ - return [FloatingIp(ip) for ip in novaclient(request).floating_ips.list()] - - -def tenant_floating_ip_get(request, floating_ip_id): - """ - Fetches a floating ip. - """ - return novaclient(request).floating_ips.get(floating_ip_id) - - -def tenant_floating_ip_allocate(request): - """ - Allocates a floating ip to tenant. - """ - return novaclient(request).floating_ips.create() - - -def tenant_floating_ip_release(request, floating_ip_id): - """ - Releases floating ip from the pool of a tenant. - """ - return novaclient(request).floating_ips.delete(floating_ip_id) - - -def image_create(request, image_meta, image_file): - return Image(glance_api(request).add_image(image_meta, image_file)) - - -def image_delete(request, image_id): - return glance_api(request).delete_image(image_id) - - -def image_get(request, image_id): - return Image(glance_api(request).get_image(image_id)[0]) - - -def image_list_detailed(request): - return [Image(i) for i in glance_api(request).get_images_detailed()] - - -def snapshot_list_detailed(request): - filters = {} - filters['property-image_type'] = 'snapshot' - filters['is_public'] = 'none' - return [Image(i) for i in glance_api(request) - .get_images_detailed(filters=filters)] - - -def snapshot_create(request, instance_id, name): - return novaclient(request).servers.create_image(instance_id, name) - - -def image_update(request, image_id, image_meta=None): - image_meta = image_meta and image_meta or {} - return Image(glance_api(request).update_image(image_id, - image_meta=image_meta)) - - -def keypair_create(request, name): - return KeyPair(novaclient(request).keypairs.create(name)) - - -def keypair_import(request, name, public_key): - return KeyPair(novaclient(request).keypairs.create(name, public_key)) - - -def keypair_delete(request, keypair_id): - novaclient(request).keypairs.delete(keypair_id) - - -def keypair_list(request): - return [KeyPair(key) for key in novaclient(request).keypairs.list()] - - -def volume_list(request): - return [Volume(vol) for vol in novaclient(request).volumes.list()] - - -def volume_get(request, volume_id): - return Volume(novaclient(request).volumes.get(volume_id)) - - -def volume_instance_list(request, instance_id): - return novaclient(request).volumes.get_server_volumes(instance_id) - - -def volume_create(request, size, name, description): - return Volume(novaclient(request).volumes.create( - size, name, description)) - - -def volume_delete(request, volume_id): - novaclient(request).volumes.delete(volume_id) - - -def volume_attach(request, volume_id, instance_id, device): - novaclient(request).volumes.create_server_volume( - instance_id, volume_id, device) - - -def volume_detach(request, instance_id, attachment_id): - novaclient(request).volumes.delete_server_volume( - instance_id, attachment_id) - - -def server_create(request, name, image, flavor, - key_name, user_data, security_groups): - return Server(novaclient(request).servers.create( - name, image, flavor, userdata=user_data, - security_groups=security_groups, - key_name=key_name), request) - - -def server_delete(request, instance): - compute_api(request).servers.delete(instance) - - -def server_get(request, instance_id): - return Server(extras_api(request).servers.get(instance_id), request) - - -@check_openstackx -def server_list(request): - return [Server(s, request) for s in extras_api(request).servers.list()] - - -@check_openstackx -def admin_server_list(request): - return [Server(s, request) for s in admin_api(request).servers.list()] - - -def server_reboot(request, - instance_id, - hardness=openstack.compute.servers.REBOOT_HARD): - server = server_get(request, instance_id) - server.reboot(hardness) - - -def server_update(request, instance_id, name, description): - return extras_api(request).servers.update(instance_id, - name=name, - description=description) - - -def server_add_floating_ip(request, server, address): - """ - Associates floating IP to server's fixed IP. - """ - server = novaclient(request).servers.get(server) - fip = novaclient(request).floating_ips.get(address) - - return novaclient(request).servers.add_floating_ip(server, fip) - - -def server_remove_floating_ip(request, server, address): - """ - Removes relationship between floating and server's fixed ip. - """ - fip = novaclient(request).floating_ips.get(address) - server = novaclient(request).servers.get(fip.instance_id) - - return novaclient(request).servers.remove_floating_ip(server, fip) - - -def service_get(request, name): - return Services(admin_api(request).services.get(name)) - - -@check_openstackx -def service_list(request): - return [Services(s) for s in admin_api(request).services.list()] - - -def service_update(request, name, enabled): - return Services(admin_api(request).services.update(name, enabled)) - - -def tenant_create(request, tenant_name, description, enabled): - return Tenant(keystoneclient(request).tenants.create(tenant_name, - description, - enabled)) - - -def tenant_get(request, tenant_id): - return Tenant(keystoneclient(request).tenants.get(tenant_id)) - - -def tenant_delete(request, tenant_id): - keystoneclient(request).tenants.delete(tenant_id) - - -def tenant_list(request): - return [Tenant(t) for t in keystoneclient(request).tenants.list()] - - -def tenant_update(request, tenant_id, tenant_name, description, enabled): - return Tenant(keystoneclient(request).tenants.update(tenant_id, - tenant_name, - description, - enabled)) - - -def tenant_delete(request, tenant_id): - keystoneclient(request).tenants.delete(tenant_id) - - -def tenant_list_for_token(request, token): - c = keystoneclient(request, token_id=token, - endpoint=settings.OPENSTACK_KEYSTONE_URL) - return [Tenant(t) for t in c.tenants.list()] - - -def token_create(request, tenant, username, password): - ''' - Creates a token using the username and password provided. If tenant - is provided it will retrieve a scoped token and the service catalog for - the given tenant. Otherwise it will return an unscoped token and without - a service catalog. - ''' - c = keystoneclient(request, username=username, password=password, - tenant_id=tenant, - endpoint=settings.OPENSTACK_KEYSTONE_URL) - token = c.tokens.authenticate(username=username, - password=password, - tenant=tenant) - return Token(token) - - -def token_create_scoped(request, tenant, token): - ''' - Creates a scoped token using the tenant id and unscoped token; retrieves - the service catalog for the given tenant. - ''' - if hasattr(request, '_keystone'): - del request._keystone - c = keystoneclient(request, tenant_id=tenant, token_id=token, - endpoint=settings.OPENSTACK_KEYSTONE_URL) - scoped_token = c.tokens.authenticate(tenant=tenant, token=token) - return Token(scoped_token) - - -def tenant_quota_get(request, tenant): - return novaclient(request).quotas.get(tenant) - - -@check_openstackx -def usage_get(request, tenant_id, start, end): - return Usage(extras_api(request).usage.get(tenant_id, start, end)) - - -@check_openstackx -def usage_list(request, start, end): - return [Usage(u) for u in extras_api(request).usage.list(start, end)] - - -def security_group_list(request): - return [SecurityGroup(g) for g in novaclient(request).\ - security_groups.list()] - - -def security_group_get(request, security_group_id): - return SecurityGroup(novaclient(request).\ - security_groups.get(security_group_id)) - - -def security_group_create(request, name, description): - return SecurityGroup(novaclient(request).\ - security_groups.create(name, description)) - - -def security_group_delete(request, security_group_id): - novaclient(request).security_groups.delete(security_group_id) - - -def security_group_rule_create(request, parent_group_id, ip_protocol=None, - from_port=None, to_port=None, cidr=None, - group_id=None): - return SecurityGroup(novaclient(request).\ - security_group_rules.create(parent_group_id, - ip_protocol, - from_port, - to_port, - cidr, - group_id)) - - -def security_group_rule_delete(request, security_group_rule_id): - novaclient(request).security_group_rules.delete(security_group_rule_id) - - -def user_list(request, tenant_id=None): - return [User(u) for u in - keystoneclient(request).users.list(tenant_id=tenant_id)] - - -def user_create(request, user_id, email, password, tenant_id, enabled): - return User(keystoneclient(request).users.create( - user_id, password, email, tenant_id, enabled)) - - -def user_delete(request, user_id): - keystoneclient(request).users.delete(user_id) - - -def user_get(request, user_id): - return User(keystoneclient(request).users.get(user_id)) - - -def user_update_email(request, user_id, email): - return User(keystoneclient(request).users.update_email(user_id, email)) - - -def user_update_enabled(request, user_id, enabled): - return User(keystoneclient(request).users.update_enabled(user_id, enabled)) - - -def user_update_password(request, user_id, password): - return User(keystoneclient(request).users.update_password(user_id, - password)) - - -def user_update_tenant(request, user_id, tenant_id): - return User(keystoneclient(request).users.update_tenant(user_id, - tenant_id)) - - -def _get_role(request, name): - roles = keystoneclient(request).roles.list() - for role in roles: - if role.name.lower() == name.lower(): - return role - - raise Exception(_('Role does not exist: %s') % name) - - -def _get_roleref(request, user_id, tenant_id, role): - rolerefs = keystoneclient(request).roles.get_user_role_refs(user_id) - for roleref in rolerefs: - if roleref.roleId == role.id and roleref.tenantId == tenant_id: - return roleref - raise Exception(_('Role "%s" does not exist for that user on this tenant.') - % role.name) - - -def role_add_for_tenant_user(request, tenant_id, user_id, role): - role = _get_role(request, role) - return keystoneclient(request).roles.add_user_to_tenant(tenant_id, - user_id, - role.id) - - -def role_delete_for_tenant_user(request, tenant_id, user_id, role): - role = _get_role(request, role) - roleref = _get_roleref(request, user_id, tenant_id, role) - return keystoneclient(request).roles.remove_user_from_tenant(tenant_id, - user_id, - roleref.id) - - -def swift_container_exists(request, container_name): - try: - swift_api(request).get_container(container_name) - return True - except cloudfiles.errors.NoSuchContainer: - return False - - -def swift_object_exists(request, container_name, object_name): - container = swift_api(request).get_container(container_name) - - try: - container.get_object(object_name) - return True - except cloudfiles.errors.NoSuchObject: - return False - - -def swift_get_containers(request, marker=None): - return [Container(c) for c in swift_api(request).get_all_containers( - limit=getattr(settings, 'SWIFT_PAGINATE_LIMIT', 10000), - marker=marker)] - - -def swift_create_container(request, name): - if swift_container_exists(request, name): - raise Exception('Container with name %s already exists.' % (name)) - - return Container(swift_api(request).create_container(name)) - - -def swift_delete_container(request, name): - swift_api(request).delete_container(name) - - -def swift_get_objects(request, container_name, prefix=None, marker=None): - container = swift_api(request).get_container(container_name) - objects = container.get_objects(prefix=prefix, marker=marker, - limit=getattr(settings, 'SWIFT_PAGINATE_LIMIT', 10000)) - return [SwiftObject(o) for o in objects] - - -def swift_copy_object(request, orig_container_name, orig_object_name, - new_container_name, new_object_name): - - container = swift_api(request).get_container(orig_container_name) - - if swift_object_exists(request, - new_container_name, - new_object_name) == True: - raise Exception('Object with name %s already exists in container %s' - % (new_object_name, new_container_name)) - - orig_obj = container.get_object(orig_object_name) - return orig_obj.copy_to(new_container_name, new_object_name) - - -def swift_upload_object(request, container_name, object_name, object_data): - container = swift_api(request).get_container(container_name) - obj = container.create_object(object_name) - obj.write(object_data) - - -def swift_delete_object(request, container_name, object_name): - container = swift_api(request).get_container(container_name) - container.delete_object(object_name) - - -def swift_get_object_data(request, container_name, object_name): - container = swift_api(request).get_container(container_name) - return container.get_object(object_name).stream() - - -def quantum_list_networks(request): - return quantum_api(request).list_networks() - - -def quantum_network_details(request, network_id): - return quantum_api(request).show_network_details(network_id) - - -def quantum_list_ports(request, network_id): - return quantum_api(request).list_ports(network_id) - - -def quantum_port_details(request, network_id, port_id): - return quantum_api(request).show_port_details(network_id, port_id) - - -def quantum_create_network(request, data): - return quantum_api(request).create_network(data) - - -def quantum_delete_network(request, network_id): - return quantum_api(request).delete_network(network_id) - - -def quantum_update_network(request, network_id, data): - return quantum_api(request).update_network(network_id, data) - - -def quantum_create_port(request, network_id): - return quantum_api(request).create_port(network_id) - - -def quantum_delete_port(request, network_id, port_id): - return quantum_api(request).delete_port(network_id, port_id) - - -def quantum_attach_port(request, network_id, port_id, data): - return quantum_api(request).attach_resource(network_id, port_id, data) - - -def quantum_detach_port(request, network_id, port_id): - return quantum_api(request).detach_resource(network_id, port_id) - - -def quantum_set_port_state(request, network_id, port_id, data): - return quantum_api(request).set_port_state(network_id, port_id, data) - - -def quantum_port_attachment(request, network_id, port_id): - return quantum_api(request).show_port_attachment(network_id, port_id) - - -def get_vif_ids(request): - vifs = [] - attached_vifs = [] - # Get a list of all networks - networks_list = quantum_api(request).list_networks() - for network in networks_list['networks']: - ports = quantum_api(request).list_ports(network['id']) - # Get port attachments - for port in ports['ports']: - port_attachment = quantum_api(request).show_port_attachment( - network['id'], - port['id']) - if port_attachment['attachment']: - attached_vifs.append( - port_attachment['attachment']['id'].encode('ascii')) - # Get all instances - instances = server_list(request) - # Get virtual interface ids by instance - for instance in instances: - id = instance.id - instance_vifs = extras_api(request).virtual_interfaces.list(id) - for vif in instance_vifs: - # Check if this VIF is already connected to any port - if str(vif.id) in attached_vifs: - vifs.append({ - 'id': vif.id, - 'instance': instance.id, - 'instance_name': instance.name, - 'available': False}) - else: - vifs.append({ - 'id': vif.id, - 'instance': instance.id, - 'instance_name': instance.name, - 'available': True}) - return vifs - - -class GlobalSummary(object): - node_resources = ['vcpus', 'disk_size', 'ram_size'] - unit_mem_size = {'disk_size': ['GiB', 'TiB'], 'ram_size': ['MiB', 'GiB']} - node_resource_info = ['', 'active_', 'avail_'] - - def __init__(self, request): - self.summary = {} - for rsrc in GlobalSummary.node_resources: - for info in GlobalSummary.node_resource_info: - self.summary['total_' + info + rsrc] = 0 - self.request = request - self.service_list = [] - self.usage_list = [] - - def service(self): - try: - self.service_list = service_list(self.request) - except api_exceptions.ApiException, e: - self.service_list = [] - LOG.exception('ApiException fetching service list in instance ' - 'usage') - messages.error(self.request, - _('Unable to get service info: %s') % e.message) - return - - for service in self.service_list: - if service.type == 'nova-compute': - self.summary['total_vcpus'] += min(service.stats['max_vcpus'], - service.stats.get('vcpus', 0)) - self.summary['total_disk_size'] += min( - service.stats['max_gigabytes'], - service.stats.get('local_gb', 0)) - self.summary['total_ram_size'] += min( - service.stats['max_ram'], - service.stats['memory_mb']) if 'max_ram' \ - in service.stats \ - else service.stats.get('memory_mb', 0) - - def usage(self, datetime_start, datetime_end): - try: - self.usage_list = usage_list(self.request, datetime_start, - datetime_end) - except api_exceptions.ApiException, e: - self.usage_list = [] - LOG.exception('ApiException fetching usage list in instance usage' - ' on date range "%s to %s"' % (datetime_start, - datetime_end)) - messages.error(self.request, - _('Unable to get usage info: %s') % e.message) - return - - for usage in self.usage_list: - # FIXME: api needs a simpler dict interface (with iteration) - # - anthony - # NOTE(mgius): Changed this on the api end. Not too much - # neater, but at least its not going into private member - # data of an external class anymore - # usage = usage._info - for k in usage._attrs: - v = usage.__getattr__(k) - if type(v) in [float, int]: - if not k in self.summary: - self.summary[k] = 0 - self.summary[k] += v - - def human_readable(self, rsrc): - if self.summary['total_' + rsrc] > 1023: - self.summary['unit_' + rsrc] = GlobalSummary.unit_mem_size[rsrc][1] - mult = 1024.0 - else: - self.summary['unit_' + rsrc] = GlobalSummary.unit_mem_size[rsrc][0] - mult = 1.0 - - for kind in GlobalSummary.node_resource_info: - self.summary['total_' + kind + rsrc + '_hr'] = \ - self.summary['total_' + kind + rsrc] / mult - - def avail(self): - for rsrc in GlobalSummary.node_resources: - self.summary['total_avail_' + rsrc] = \ - self.summary['total_' + rsrc] - \ - self.summary['total_active_' + rsrc] diff --git a/django-openstack/django_openstack/auth/__init__.py b/django-openstack/django_openstack/auth/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/django-openstack/django_openstack/auth/urls.py b/django-openstack/django_openstack/auth/urls.py deleted file mode 100644 index ba9b04dc..00000000 --- a/django-openstack/django_openstack/auth/urls.py +++ /dev/null @@ -1,30 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from django.conf.urls.defaults import * -from django.conf import settings - - -urlpatterns = patterns('django_openstack.auth.views', - url(r'login/$', 'login', name='auth_login'), - url(r'logout/$', 'logout', name='auth_logout'), - url(r'switch/(?P[^/]+)/$', 'switch_tenants', - name='auth_switch'), -) diff --git a/django-openstack/django_openstack/auth/views.py b/django-openstack/django_openstack/auth/views.py deleted file mode 100644 index 73dc7622..00000000 --- a/django-openstack/django_openstack/auth/views.py +++ /dev/null @@ -1,176 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import logging - -from django.conf import settings -from django import template -from django import shortcuts -from django.contrib import messages -from django.utils.translation import ugettext as _ - -from django_openstack import api -from django_openstack import exceptions -from django_openstack import forms -from openstackx.api import exceptions as api_exceptions -from django_openstack import exceptions - - -LOG = logging.getLogger('django_openstack.auth') - - -def _is_admin(token): - for role in token.user['roles']: - if role['name'].lower() == 'admin': - return True - return False - - -def _set_session_data(request, token): - request.session['admin'] = _is_admin(token) - request.session['serviceCatalog'] = token.serviceCatalog - request.session['tenant'] = token.tenant['name'] - request.session['tenant_id'] = token.tenant['id'] - request.session['token'] = token.id - request.session['user'] = token.user['name'] - - -class Login(forms.SelfHandlingForm): - username = forms.CharField(max_length="20", label=_("User Name")) - password = forms.CharField(max_length="20", label=_("Password"), - widget=forms.PasswordInput(render_value=False)) - - def handle(self, request, data): - try: - if data.get('tenant'): - token = api.token_create(request, - data.get('tenant'), - data['username'], - data['password']) - - tenants = api.tenant_list_for_token(request, token.id) - tenant = None - for t in tenants: - if t.id == data.get('tenant'): - tenant = t - else: - token = api.token_create(request, - '', - data['username'], - data['password']) - - # Unscoped token - request.session['unscoped_token'] = token.id - request.user.username = data['username'] - - # Get the tenant list, and log in using first tenant - # FIXME (anthony): add tenant chooser here? - tenants = api.tenant_list_for_token(request, token.id) - - # Abort if there are no valid tenants for this user - if not tenants: - messages.error(request, - _('No tenants present for user: %(user)s') % - {"user": data['username']}) - return - - # Create a token. - # NOTE(gabriel): Keystone can return tenants that you're - # authorized to administer but not to log into as a user, so in - # the case of an Unauthorized error we should iterate through - # the tenants until one succeeds or we've failed them all. - while tenants: - tenant = tenants.pop() - try: - token = api.token_create_scoped(request, - tenant.id, - token.id) - break - except exceptions.Unauthorized as e: - token = None - if token is None: - raise exceptions.Unauthorized( - _("You are not authorized for any available tenants.")) - - LOG.info('Login form for user "%s". Service Catalog data:\n%s' % - (data['username'], token.serviceCatalog)) - _set_session_data(request, token) - - return shortcuts.redirect('dash_overview') - - except api_exceptions.Unauthorized as e: - msg = _('Error authenticating: %s') % e.message - LOG.exception(msg) - messages.error(request, msg) - except api_exceptions.ApiException as e: - messages.error(request, - _('Error authenticating with keystone: %s') % - e.message) - - -class LoginWithTenant(Login): - username = forms.CharField(max_length="20", - widget=forms.TextInput(attrs={'readonly': 'readonly'})) - tenant = forms.CharField(widget=forms.HiddenInput()) - - -def login(request): - if request.user and request.user.is_authenticated(): - if request.user.is_admin(): - return shortcuts.redirect('syspanel_overview') - else: - return shortcuts.redirect('dash_overview') - - form, handled = Login.maybe_handle(request) - if handled: - return handled - - return shortcuts.render_to_response('splash.html', { - 'form': form, - }, context_instance=template.RequestContext(request)) - - -def switch_tenants(request, tenant_id): - form, handled = LoginWithTenant.maybe_handle( - request, initial={'tenant': tenant_id, - 'username': request.user.username}) - if handled: - return handled - - unscoped_token = request.session.get('unscoped_token', None) - if unscoped_token: - try: - token = api.token_create_scoped(request, - tenant_id, - unscoped_token) - _set_session_data(request, token) - return shortcuts.redirect('dash_overview') - except exceptions.Unauthorized as e: - messages.error(_("You are not authorized for that tenant.")) - - return shortcuts.render_to_response('switch_tenants.html', { - 'to_tenant': tenant_id, - 'form': form, - }, context_instance=template.RequestContext(request)) - - -def logout(request): - request.session.clear() - return shortcuts.redirect('splash') diff --git a/django-openstack/django_openstack/context_processors.py b/django-openstack/django_openstack/context_processors.py deleted file mode 100644 index 1d1b0a5b..00000000 --- a/django-openstack/django_openstack/context_processors.py +++ /dev/null @@ -1,48 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from django.conf import settings -from django_openstack import api -from django.contrib import messages -from openstackx.api import exceptions as api_exceptions - - -def tenants(request): - if not request.user or not request.user.is_authenticated(): - return {} - - try: - return {'tenants': api.tenant_list_for_token(request, - request.user.token)} - except api_exceptions.BadRequest, e: - messages.error(request, _("Unable to retrieve tenant list from\ - keystone: %s") % e.message) - return {'tenants': []} - - -def object_store(request): - catalog = getattr(request.user, 'service_catalog', []) - object_store = catalog and api.get_service_from_catalog(catalog, - 'object-store') - return {'object_store_configured': object_store} - - -def quantum(request): - return {'quantum_configured': settings.QUANTUM_ENABLED} diff --git a/django-openstack/django_openstack/dash/__init__.py b/django-openstack/django_openstack/dash/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/django-openstack/django_openstack/dash/urls.py b/django-openstack/django_openstack/dash/urls.py deleted file mode 100644 index 82c3a5f2..00000000 --- a/django-openstack/django_openstack/dash/urls.py +++ /dev/null @@ -1,118 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from django.conf.urls.defaults import * - -SECURITY_GROUPS = r'^(?P[^/]+)/security_groups/' \ - '(?P[^/]+)/%s$' -INSTANCES = r'^(?P[^/]+)/instances/(?P[^/]+)/%s$' -IMAGES = r'^(?P[^/]+)/images/(?P[^/]+)/%s$' -KEYPAIRS = r'^(?P[^/]+)/keypairs/%s$' -SNAPSHOTS = r'^(?P[^/]+)/snapshots/(?P[^/]+)/%s$' -VOLUMES = r'^(?P[^/]+)/volumes/(?P[^/]+)/%s$' -CONTAINERS = r'^(?P[^/]+)/containers/%s$' -FLOATING_IPS = r'^(?P[^/]+)/floating_ips/(?P[^/]+)/%s$' -OBJECTS = r'^(?P[^/]+)/containers/(?P[^/]+)/%s$' -NETWORKS = r'^(?P[^/]+)/networks/%s$' -PORTS = r'^(?P[^/]+)/networks/(?P[^/]+)/ports/%s$' - -urlpatterns = patterns('django_openstack.dash.views.instances', - url(r'^(?P[^/]+)/$', 'usage', name='dash_usage'), - url(r'^(?P[^/]+)/instances/$', 'index', name='dash_instances'), - url(r'^(?P[^/]+)/instances/refresh$', 'refresh', - name='dash_instances_refresh'), - url(INSTANCES % 'detail', 'detail', name='dash_instances_detail'), - url(INSTANCES % 'console', 'console', name='dash_instances_console'), - url(INSTANCES % 'vnc', 'vnc', name='dash_instances_vnc'), - url(INSTANCES % 'update', 'update', name='dash_instances_update'), -) - -urlpatterns += patterns('django_openstack.dash.views.security_groups', - url(r'^(?P[^/]+)/security_groups/$', 'index', - name='dash_security_groups'), - url(r'^(?P[^/]+)/security_groups/create$', 'create', - name='dash_security_groups_create'), - url(SECURITY_GROUPS % 'edit_rules', 'edit_rules', - name='dash_security_groups_edit_rules'), -) - -urlpatterns += patterns('django_openstack.dash.views.images', - url(r'^(?P[^/]+)/images/$', 'index', name='dash_images'), - url(IMAGES % 'launch', 'launch', name='dash_images_launch'), - url(IMAGES % 'update', 'update', name='dash_images_update'), -) - -urlpatterns += patterns('django_openstack.dash.views.keypairs', - url(r'^(?P[^/]+)/keypairs/$', 'index', name='dash_keypairs'), - url(KEYPAIRS % 'create', 'create', name='dash_keypairs_create'), - url(KEYPAIRS % 'import', 'import_keypair', name='dash_keypairs_import'), -) - -urlpatterns += patterns('django_openstack.dash.views.floating_ips', - url(r'^(?P[^/]+)/floating_ips/$', 'index', - name='dash_floating_ips'), - url(FLOATING_IPS % 'associate', 'associate', - name='dash_floating_ips_associate'), - url(FLOATING_IPS % 'disassociate', 'disassociate', - name='dash_floating_ips_disassociate'), -) - -urlpatterns += patterns('django_openstack.dash.views.snapshots', - url(r'^(?P[^/]+)/snapshots/$', 'index', name='dash_snapshots'), - url(SNAPSHOTS % 'create', 'create', name='dash_snapshots_create'), -) - -urlpatterns += patterns('django_openstack.dash.views.volumes', - url(r'^(?P[^/]+)/volumes/$', 'index', name='dash_volumes'), - url(r'^(?P[^/]+)/volumes/create', 'create', - name='dash_volumes_create'), - url(VOLUMES % 'attach', 'attach', name='dash_volumes_attach'), - url(VOLUMES % 'detail', 'detail', name='dash_volumes_detail'), -) - -# Swift containers and objects. -urlpatterns += patterns('django_openstack.dash.views.containers', - url(CONTAINERS % '', 'index', name='dash_containers'), - url(CONTAINERS % 'create', 'create', name='dash_containers_create'), -) - -urlpatterns += patterns('django_openstack.dash.views.objects', - url(OBJECTS % '', 'index', name='dash_objects'), - url(OBJECTS % 'upload', 'upload', name='dash_objects_upload'), - url(OBJECTS % '(?P[^/]+)/copy', - 'copy', name='dash_object_copy'), - url(OBJECTS % '(?P[^/]+)/download', - 'download', name='dash_objects_download'), -) - -urlpatterns += patterns('django_openstack.dash.views.networks', - url(r'^(?P[^/]+)/networks/$', 'index', name='dash_networks'), - url(NETWORKS % 'create', 'create', name='dash_network_create'), - url(NETWORKS % '(?P[^/]+)/detail', 'detail', - name='dash_networks_detail'), - url(NETWORKS % '(?P[^/]+)/rename', 'rename', - name='dash_network_rename'), -) - -urlpatterns += patterns('django_openstack.dash.views.ports', - url(PORTS % 'create', 'create', name='dash_ports_create'), - url(PORTS % '(?P[^/]+)/attach', 'attach', - name='dash_ports_attach'), -) diff --git a/django-openstack/django_openstack/dash/views/__init__.py b/django-openstack/django_openstack/dash/views/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/django-openstack/django_openstack/dash/views/containers.py b/django-openstack/django_openstack/dash/views/containers.py deleted file mode 100644 index fada57e5..00000000 --- a/django-openstack/django_openstack/dash/views/containers.py +++ /dev/null @@ -1,95 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Views for managing Swift containers. -""" -import logging - -from django import template -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django import shortcuts -from django.utils.translation import ugettext as _ - -from django_openstack import api -from django_openstack import forms - -from cloudfiles.errors import ContainerNotEmpty - - -LOG = logging.getLogger('django_openstack.dash') - - -class DeleteContainer(forms.SelfHandlingForm): - container_name = forms.CharField(widget=forms.HiddenInput()) - - def handle(self, request, data): - try: - api.swift_delete_container(request, data['container_name']) - except ContainerNotEmpty, e: - messages.error(request, - _('Unable to delete non-empty container: %s') % - data['container_name']) - LOG.exception('Unable to delete container "%s". Exception: "%s"' % - (data['container_name'], str(e))) - else: - messages.info(request, - _('Successfully deleted container: %s') % \ - data['container_name']) - return shortcuts.redirect(request.build_absolute_uri()) - - -class CreateContainer(forms.SelfHandlingForm): - name = forms.CharField(max_length="255", label=_("Container Name")) - - def handle(self, request, data): - api.swift_create_container(request, data['name']) - messages.success(request, _("Container was successfully created.")) - return shortcuts.redirect("dash_containers", request.user.tenant_id) - - -@login_required -def index(request, tenant_id): - marker = request.GET.get('marker', None) - - delete_form, handled = DeleteContainer.maybe_handle(request) - if handled: - return handled - - containers = api.swift_get_containers(request, marker=marker) - - return shortcuts.render_to_response( - 'django_openstack/dash/containers/index.html', { - 'containers': containers, - 'delete_form': delete_form, - }, context_instance=template.RequestContext(request)) - - -@login_required -def create(request, tenant_id): - form, handled = CreateContainer.maybe_handle(request) - if handled: - return handled - - return shortcuts.render_to_response( - 'django_openstack/dash/containers/create.html', { - 'create_form': form, - }, context_instance=template.RequestContext(request)) diff --git a/django-openstack/django_openstack/dash/views/floating_ips.py b/django-openstack/django_openstack/dash/views/floating_ips.py deleted file mode 100644 index 6678f8a8..00000000 --- a/django-openstack/django_openstack/dash/views/floating_ips.py +++ /dev/null @@ -1,181 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Views for managing Nova floating ips. -""" -import logging - -from django import template -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django import shortcuts -from django.utils.translation import ugettext as _ - -from django_openstack import api -from django_openstack import forms -from novaclient import exceptions as novaclient_exceptions - - -LOG = logging.getLogger('django_openstack.dash.views.floating_ip') - - -class ReleaseFloatingIp(forms.SelfHandlingForm): - floating_ip_id = forms.CharField(widget=forms.HiddenInput()) - - def handle(self, request, data): - try: - LOG.info('Releasing Floating IP "%s"' % data['floating_ip_id']) - api.tenant_floating_ip_release(request, data['floating_ip_id']) - messages.info(request, _('Successfully released Floating IP: %s') - % data['floating_ip_id']) - except novaclient_exceptions.ClientException, e: - LOG.exception("ClientException in ReleaseFloatingIp") - messages.error(request, _('Error releasing Floating IP ' - 'from tenant: %s') % e.message) - return shortcuts.redirect(request.build_absolute_uri()) - - -class FloatingIpAssociate(forms.SelfHandlingForm): - floating_ip_id = forms.CharField(widget=forms.HiddenInput()) - floating_ip = forms.CharField(widget=forms.TextInput( - attrs={'readonly': 'readonly'})) - instance_id = forms.ChoiceField() - - def __init__(self, *args, **kwargs): - super(FloatingIpAssociate, self).__init__(*args, **kwargs) - instancelist = kwargs.get('initial', {}).get('instances', []) - self.fields['instance_id'] = forms.ChoiceField( - choices=instancelist, - label=_("Instance")) - - def handle(self, request, data): - try: - api.server_add_floating_ip(request, - data['instance_id'], - data['floating_ip_id']) - LOG.info('Associating Floating IP "%s" with Instance "%s"' - % (data['floating_ip'], data['instance_id'])) - messages.info(request, _('Successfully associated Floating IP: \ - %(ip)s with Instance: %(inst)s' - % {"ip": data['floating_ip'], - "inst": data['instance_id']})) - except novaclient_exceptions.ClientException, e: - LOG.exception("ClientException in FloatingIpAssociate") - messages.error(request, _('Error associating Floating IP: %s') - % e.message) - return shortcuts.redirect('dash_floating_ips', request.user.tenant_id) - - -class FloatingIpDisassociate(forms.SelfHandlingForm): - floating_ip_id = forms.CharField(widget=forms.HiddenInput()) - - def handle(self, request, data): - try: - fip = api.tenant_floating_ip_get(request, data['floating_ip_id']) - api.server_remove_floating_ip(request, fip.instance_id, fip.id) - - LOG.info('Disassociating Floating IP "%s"' - % data['floating_ip_id']) - - messages.info(request, - _('Successfully disassociated Floating IP: %s') - % data['floating_ip_id']) - except novaclient_exceptions.ClientException, e: - LOG.exception("ClientException in FloatingIpAssociate") - messages.error(request, _('Error disassociating Floating IP: %s') - % e.message) - return shortcuts.redirect('dash_floating_ips', request.user.tenant_id) - - -class FloatingIpAllocate(forms.SelfHandlingForm): - tenant_id = forms.CharField(widget=forms.HiddenInput()) - - def handle(self, request, data): - try: - fip = api.tenant_floating_ip_allocate(request) - LOG.info('Allocating Floating IP "%s" to tenant "%s"' - % (fip.ip, data['tenant_id'])) - - messages.success(request, - _('Successfully allocated Floating IP "%(ip)s"\ - to tenant "%(tenant)s"') - % {"ip": fip.ip, "tenant": data['tenant_id']}) - - except novaclient_exceptions.ClientException, e: - LOG.exception("ClientException in FloatingIpAllocate") - messages.error(request, _('Error allocating Floating IP "%(ip)s"\ - to tenant "%(tenant)s": %(msg)s') % - {"ip": fip.ip, "tenant": data['tenant_id'], "msg": e.message}) - return shortcuts.redirect('dash_floating_ips', request.user.tenant_id) - - -@login_required -def index(request, tenant_id): - for f in (ReleaseFloatingIp, FloatingIpDisassociate, FloatingIpAllocate): - _unused, handled = f.maybe_handle(request) - if handled: - return handled - try: - floating_ips = api.tenant_floating_ip_list(request) - except novaclient_exceptions.ClientException, e: - floating_ips = [] - LOG.exception("ClientException in floating ip index") - messages.error(request, - _('Error fetching floating ips: %s') % e.message) - - return shortcuts.render_to_response( - 'django_openstack/dash/floating_ips/index.html', { - 'allocate_form': FloatingIpAllocate( - initial={'tenant_id': request.user.tenant_id}), - 'disassociate_form': FloatingIpDisassociate(), - 'floating_ips': floating_ips, - 'release_form': ReleaseFloatingIp(), - }, context_instance=template.RequestContext(request)) - - -@login_required -def associate(request, tenant_id, ip_id): - instancelist = [(server.id, 'id: %s, name: %s' % - (server.id, server.name)) - for server in api.server_list(request)] - - form, handled = FloatingIpAssociate().maybe_handle(request, initial={ - 'floating_ip_id': ip_id, - 'floating_ip': api.tenant_floating_ip_get(request, ip_id).ip, - 'instances': instancelist}) - if handled: - return handled - - return shortcuts.render_to_response( - 'django_openstack/dash/floating_ips/associate.html', { - 'associate_form': form, - }, context_instance=template.RequestContext(request)) - - -@login_required -def disassociate(request, tenant_id, ip_id): - form, handled = FloatingIpDisassociate().maybe_handle(request) - if handled: - return handled - - return shortcuts.render_to_response( - 'django_openstack/dash/floating_ips/associate.html', { - }, context_instance=template.RequestContext(request)) diff --git a/django-openstack/django_openstack/dash/views/images.py b/django-openstack/django_openstack/dash/views/images.py deleted file mode 100644 index 3ff1d61b..00000000 --- a/django-openstack/django_openstack/dash/views/images.py +++ /dev/null @@ -1,330 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Views for managing Nova images. -""" - -import logging - -from django import template -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.shortcuts import redirect, render_to_response -from django.utils.text import normalize_newlines -from django.utils.translation import ugettext as _ - -from django_openstack import api -from django_openstack import forms -from openstackx.api import exceptions as api_exceptions -from glance.common import exception as glance_exception -from novaclient import exceptions as novaclient_exceptions - - -LOG = logging.getLogger('django_openstack.dash.views.images') - - -class UpdateImageForm(forms.SelfHandlingForm): - image_id = forms.CharField(widget=forms.HiddenInput()) - name = forms.CharField(max_length="25", label=_("Name")) - kernel = forms.CharField(max_length="25", label=_("Kernel ID"), - required=False) - ramdisk = forms.CharField(max_length="25", label=_("Ramdisk ID"), - required=False) - architecture = forms.CharField(label=_("Architecture"), required=False) - container_format = forms.CharField(label=_("Container Format"), - required=False) - disk_format = forms.CharField(label=_("Disk Format")) - - def handle(self, request, data): - image_id = data['image_id'] - tenant_id = request.user.tenant_id - error_retrieving = _('Unable to retreive image info from glance: %s' - % image_id) - error_updating = _('Error updating image with id: %s' % image_id) - - try: - image = api.image_get(request, image_id) - except glance_exception.ClientConnectionError, e: - LOG.exception(_('Error connecting to glance')) - messages.error(request, error_retrieving) - except glance_exception.Error, e: - LOG.exception(error_retrieving) - messages.error(request, error_retrieving) - - if image.owner == request.user.username: - try: - meta = { - 'is_public': True, - 'disk_format': data['disk_format'], - 'container_format': data['container_format'], - 'name': data['name'], - } - # TODO add public flag to properties - meta['properties'] = {} - if data['kernel']: - meta['properties']['kernel_id'] = data['kernel'] - - if data['ramdisk']: - meta['properties']['ramdisk_id'] = data['ramdisk'] - - if data['architecture']: - meta['properties']['architecture'] = data['architecture'] - - api.image_update(request, image_id, meta) - messages.success(request, _('Image was successfully updated.')) - - except glance_exception.ClientConnectionError, e: - LOG.exception(_('Error connecting to glance')) - messages.error(request, error_retrieving) - except glance_exception.Error, e: - LOG.exception(error_updating) - messages.error(request, error_updating) - except: - LOG.exception(_('Unspecified Exception in image update')) - messages.error(request, error_updating) - return redirect('dash_images_update', tenant_id, image_id) - else: - messages.info(request, _('Unable to update image. You are not its \ - owner.')) - return redirect('dash_images_update', tenant_id, image_id) - - -class LaunchForm(forms.SelfHandlingForm): - name = forms.CharField(max_length=80, label=_("Server Name")) - image_id = forms.CharField(widget=forms.HiddenInput()) - tenant_id = forms.CharField(widget=forms.HiddenInput()) - user_data = forms.CharField(widget=forms.Textarea, - label=_("User Data"), - required=False) - - # make the dropdown populate when the form is loaded not when django is - # started - def __init__(self, *args, **kwargs): - super(LaunchForm, self).__init__(*args, **kwargs) - flavorlist = kwargs.get('initial', {}).get('flavorlist', []) - self.fields['flavor'] = forms.ChoiceField( - choices=flavorlist, - label=_("Flavor"), - help_text="Size of Image to launch") - - keynamelist = kwargs.get('initial', {}).get('keynamelist', []) - self.fields['key_name'] = forms.ChoiceField(choices=keynamelist, - label=_("Key Name"), - required=False, - help_text="Which keypair to use for authentication") - - securitygrouplist = kwargs.get('initial', {}).get( - 'securitygrouplist', []) - self.fields['security_groups'] = forms.MultipleChoiceField( - choices=securitygrouplist, - label=_("Security Groups"), - required=True, - initial=['default'], - widget=forms.SelectMultiple( - attrs={'class': 'chzn-select', - 'style': "min-width: 200px"}), - help_text="Launch instance in these Security Groups") - # setting self.fields.keyOrder seems to break validation, - # so ordering fields manually - field_list = ( - 'name', - 'user_data', - 'flavor', - 'key_name') - for field in field_list[::-1]: - self.fields.insert(0, field, self.fields.pop(field)) - - def handle(self, request, data): - image_id = data['image_id'] - tenant_id = data['tenant_id'] - try: - image = api.image_get(request, image_id) - flavor = api.flavor_get(request, data['flavor']) - api.server_create(request, - data['name'], - image, - flavor, - data.get('key_name'), - normalize_newlines(data.get('user_data')), - data.get('security_groups')) - - msg = _('Instance was successfully launched') - LOG.info(msg) - messages.success(request, msg) - return redirect('dash_instances', tenant_id) - - except api_exceptions.ApiException, e: - LOG.exception('ApiException while creating instances of image "%s"' - % image_id) - messages.error(request, - _('Unable to launch instance: %s') % e.message) - - -class DeleteImage(forms.SelfHandlingForm): - image_id = forms.CharField(required=True) - - def handle(self, request, data): - image_id = data['image_id'] - tenant_id = request.user.tenant_id - try: - image = api.image_get(request, image_id) - if image.owner == request.user.username: - api.image_delete(request, image_id) - else: - messages.info(request, _("Unable to delete image, you are not \ - its owner.")) - return redirect('dash_images_update', tenant_id, image_id) - except glance_exception.ClientConnectionError, e: - LOG.exception("Error connecting to glance") - messages.error(request, _("Error connecting to glance: %s") - % e.message) - except glance_exception.Error, e: - LOG.exception('Error deleting image with id "%s"' % image_id) - messages.error(request, - _("Error deleting image: %(image)s: %i(msg)s") - % {"image": image_id, "msg": e.message}) - return redirect(request.build_absolute_uri()) - - -@login_required -def index(request, tenant_id): - for f in (DeleteImage, ): - unused, handled = f.maybe_handle(request) - if handled: - return handled - delete_form = DeleteImage() - - all_images = [] - try: - all_images = api.image_list_detailed(request) - if not all_images: - messages.info(request, _("There are currently no images.")) - except glance_exception.ClientConnectionError, e: - LOG.exception("Error connecting to glance") - messages.error(request, _("Error connecting to glance: %s") % str(e)) - except glance_exception.Error, e: - LOG.exception("Error retrieving image list") - messages.error(request, _("Error retrieving image list: %s") % str(e)) - except api_exceptions.ApiException, e: - msg = _("Unable to retreive image info from glance: %s") % str(e) - LOG.exception(msg) - messages.error(request, msg) - - images = [im for im in all_images - if im['container_format'] not in ['aki', 'ari']] - - return render_to_response( - 'django_openstack/dash/images/index.html', { - 'delete_form': delete_form, - 'images': images, - }, context_instance=template.RequestContext(request)) - - -@login_required -def launch(request, tenant_id, image_id): - - def flavorlist(): - try: - fl = api.flavor_list(request) - - # TODO add vcpu count to flavors - sel = [(f.id, '%s (%svcpu / %sGB Disk / %sMB Ram )' % - (f.name, f.vcpus, f.disk, f.ram)) for f in fl] - return sorted(sel) - except api_exceptions.ApiException: - LOG.exception('Unable to retrieve list of instance types') - return [(1, 'm1.tiny')] - - def keynamelist(): - try: - fl = api.keypair_list(request) - sel = [(f.name, f.name) for f in fl] - return sel - except api_exceptions.ApiException: - LOG.exception('Unable to retrieve list of keypairs') - return [] - - def securitygrouplist(): - try: - fl = api.security_group_list(request) - sel = [(f.name, f.name) for f in fl] - return sel - except novaclient_exceptions.ClientException, e: - LOG.exception('Unable to retrieve list of security groups') - return [] - - # TODO(mgius): Any reason why these can't be after the launchform logic? - # If The form is valid, we've just wasted these two api calls - image = api.image_get(request, image_id) - quotas = api.tenant_quota_get(request, request.user.tenant_id) - try: - quotas.ram = int(quotas.ram) - except Exception, e: - messages.error(request, - _('Error parsing quota for %(image)s: %(msg)s') % - {"image": image_id, "msg": e.message}) - return redirect('dash_instances', tenant_id) - - form, handled = LaunchForm.maybe_handle( - request, initial={'flavorlist': flavorlist(), - 'keynamelist': keynamelist(), - 'securitygrouplist': securitygrouplist(), - 'image_id': image_id, - 'tenant_id': tenant_id}) - if handled: - return handled - - return render_to_response( - 'django_openstack/dash/images/launch.html', { - 'image': image, - 'form': form, - 'quotas': quotas, - }, context_instance=template.RequestContext(request)) - - -@login_required -def update(request, tenant_id, image_id): - try: - image = api.image_get(request, image_id) - except glance_exception.ClientConnectionError, e: - LOG.exception("Error connecting to glance") - messages.error(request, _("Error connecting to glance: %s") - % e.message) - except glance_exception.Error, e: - LOG.exception('Error retrieving image with id "%s"' % image_id) - messages.error(request, - _("Error retrieving image %(image)s: %(msg)s") - % {"image": image_id, "msg": e.message}) - - form, handled = UpdateImageForm().maybe_handle(request, initial={ - 'image_id': image_id, - 'name': image.get('name', ''), - 'kernel': image['properties'].get('kernel_id', ''), - 'ramdisk': image['properties'].get('ramdisk_id', ''), - 'architecture': image['properties'].get('architecture', ''), - 'container_format': image.get('container_format', ''), - 'disk_format': image.get('disk_format', ''), }) - if handled: - return handled - - return render_to_response('django_openstack/dash/images/update.html', { - 'form': form, - }, context_instance=template.RequestContext(request)) diff --git a/django-openstack/django_openstack/dash/views/instances.py b/django-openstack/django_openstack/dash/views/instances.py deleted file mode 100644 index 6a726129..00000000 --- a/django-openstack/django_openstack/dash/views/instances.py +++ /dev/null @@ -1,321 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Views for managing Nova instances. -""" -import datetime -import logging - -from django import http -from django import shortcuts -from django import template -from django.conf import settings -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.utils.translation import ugettext as _ - -from django_openstack import api -from django_openstack import forms -from django_openstack import utils -import openstack.compute.servers -import openstackx.api.exceptions as api_exceptions -import StringIO - -LOG = logging.getLogger('django_openstack.dash') - - -class TerminateInstance(forms.SelfHandlingForm): - instance = forms.CharField(required=True) - - def handle(self, request, data): - instance_id = data['instance'] - instance = api.server_get(request, instance_id) - - try: - api.server_delete(request, instance) - except api_exceptions.ApiException, e: - LOG.exception(_('ApiException while terminating instance "%s"') % - instance_id) - messages.error(request, - _('Unable to terminate %(inst)s: %(message)s') % - {"inst": instance_id, "message": e.message}) - else: - msg = _('Instance %s has been terminated.') % instance_id - LOG.info(msg) - messages.success(request, msg) - - return shortcuts.redirect(request.build_absolute_uri()) - - -class RebootInstance(forms.SelfHandlingForm): - instance = forms.CharField(required=True) - - def handle(self, request, data): - instance_id = data['instance'] - try: - server = api.server_reboot(request, instance_id) - messages.success(request, _("Instance rebooting")) - except api_exceptions.ApiException, e: - LOG.exception(_('ApiException while rebooting instance "%s"') % - instance_id) - messages.error(request, - _('Unable to reboot instance: %s') % e.message) - - else: - msg = _('Instance %s has been rebooted.') % instance_id - LOG.info(msg) - messages.success(request, msg) - - return shortcuts.redirect(request.build_absolute_uri()) - - -class UpdateInstance(forms.SelfHandlingForm): - tenant_id = forms.CharField(widget=forms.HiddenInput()) - instance = forms.CharField(widget=forms.TextInput( - attrs={'readonly': 'readonly'})) - name = forms.CharField(required=True) - description = forms.CharField(required=False) - - def handle(self, request, data): - tenant_id = data['tenant_id'] - description = data.get('description', '') - try: - api.server_update(request, - data['instance'], - data['name'], - description) - messages.success(request, _("Instance '%s' updated") % - data['name']) - except api_exceptions.ApiException, e: - messages.error(request, - _('Unable to update instance: %s') % e.message) - - return shortcuts.redirect('dash_instances', tenant_id) - - -@login_required -def index(request, tenant_id): - for f in (TerminateInstance, RebootInstance): - form, handled = f.maybe_handle(request) - if handled: - return handled - instances = [] - try: - instances = api.server_list(request) - except api_exceptions.ApiException as e: - LOG.exception(_('Exception in instance index')) - messages.error(request, _('Unable to get instance list: %s') - % e.message) - - # We don't have any way of showing errors for these, so don't bother - # trying to reuse the forms from above - terminate_form = TerminateInstance() - reboot_form = RebootInstance() - - return shortcuts.render_to_response( - 'django_openstack/dash/instances/index.html', { - 'instances': instances, - 'terminate_form': terminate_form, - 'reboot_form': reboot_form, - }, context_instance=template.RequestContext(request)) - - -@login_required -def refresh(request, tenant_id): - instances = [] - try: - instances = api.server_list(request) - except Exception as e: - messages.error(request, - _('Unable to get instance list: %s') % e.message) - - # We don't have any way of showing errors for these, so don't bother - # trying to reuse the forms from above - terminate_form = TerminateInstance() - reboot_form = RebootInstance() - - return shortcuts.render_to_response( - 'django_openstack/dash/instances/_list.html', { - 'instances': instances, - 'terminate_form': terminate_form, - 'reboot_form': reboot_form, - }, context_instance=template.RequestContext(request)) - - -@login_required -def usage(request, tenant_id=None): - today = utils.today() - date_start = datetime.date(today.year, today.month, 1) - datetime_start = datetime.datetime.combine(date_start, utils.time()) - datetime_end = utils.utcnow() - - show_terminated = request.GET.get('show_terminated', False) - - usage = {} - if not tenant_id: - tenant_id = request.user.tenant_id - - try: - usage = api.usage_get(request, tenant_id, datetime_start, datetime_end) - except api_exceptions.ApiException, e: - LOG.exception(_('ApiException in instance usage')) - - messages.error(request, _('Unable to get usage info: %s') % e.message) - - ram_unit = "MB" - total_ram = 0 - if hasattr(usage, 'total_active_ram_size'): - total_ram = usage.total_active_ram_size - if total_ram > 999: - ram_unit = "GB" - total_ram /= float(1024) - - running_instances = [] - terminated_instances = [] - if hasattr(usage, 'instances'): - now = datetime.datetime.now() - for i in usage.instances: - # this is just a way to phrase uptime in a way that is compatible - # with the 'timesince' filter. Use of local time intentional - i['uptime_at'] = now - datetime.timedelta(seconds=i['uptime']) - if i['ended_at']: - terminated_instances.append(i) - else: - running_instances.append(i) - - instances = running_instances - if show_terminated: - instances += terminated_instances - - if request.GET.get('format', 'html') == 'csv': - template_name = 'django_openstack/dash/instances/usage.csv' - mimetype = "text/csv" - else: - template_name = 'django_openstack/dash/instances/usage.html' - mimetype = "text/html" - - return shortcuts.render_to_response(template_name, { - 'usage': usage, - 'ram_unit': ram_unit, - 'total_ram': total_ram, - # there are no date selection caps yet so keeping csv_link simple - 'csv_link': '?format=csv', - 'show_terminated': show_terminated, - 'datetime_start': datetime_start, - 'datetime_end': datetime_end, - 'instances': instances - }, context_instance=template.RequestContext(request), mimetype=mimetype) - - -@login_required -def console(request, tenant_id, instance_id): - try: - # TODO(jakedahn): clean this up once the api supports tailing. - length = request.GET.get('length', '') - console = api.console_create(request, instance_id, 'text') - response = http.HttpResponse(mimetype='text/plain') - if length: - response.write('\n'.join(console.output.split('\n') - [-int(length):])) - else: - response.write(console.output) - response.flush() - return response - except api_exceptions.ApiException, e: - LOG.exception(_('ApiException while fetching instance console')) - messages.error(request, - _('Unable to get log for instance %(inst)s: %(msg)s') % - {"inst": instance_id, "msg": e.message}) - return shortcuts.redirect('dash_instances', tenant_id) - - -@login_required -def vnc(request, tenant_id, instance_id): - try: - console = api.console_create(request, instance_id, 'vnc') - instance = api.server_get(request, instance_id) - return shortcuts.redirect(console.output + - ("&title=%s(%s)" % (instance.name, instance_id))) - except api_exceptions.ApiException, e: - LOG.exception(_('ApiException while fetching instance vnc connection')) - messages.error(request, - _('Unable to get vnc console for instance %(inst)s: %(message)s') % - {"inst": instance_id, "message": e.message}) - return shortcuts.redirect('dash_instances', tenant_id) - - -@login_required -def update(request, tenant_id, instance_id): - try: - instance = api.server_get(request, instance_id) - except api_exceptions.ApiException, e: - LOG.exception(_('ApiException while fetching instance info')) - messages.error(request, - _('Unable to get information for instance %(inst)s: %(message)s') % - {"inst": instance_id, "message": e.message}) - return shortcuts.redirect('dash_instances', tenant_id) - - form, handled = UpdateInstance.maybe_handle(request, initial={ - 'instance': instance_id, - 'tenant_id': tenant_id, - 'name': instance.name, - 'description': instance.attrs['description']}) - - if handled: - return handled - - return shortcuts.render_to_response( - 'django_openstack/dash/instances/update.html', { - 'instance': instance, - 'form': form, - }, context_instance=template.RequestContext(request)) - - -@login_required -def detail(request, tenant_id, instance_id): - try: - instance = api.server_get(request, instance_id) - volumes = api.volume_instance_list(request, instance_id) - try: - console = api.console_create(request, instance_id, 'vnc') - vnc_url = "%s&title=%s(%s)" % (console.output, - instance.name, - instance_id) - except api_exceptions.ApiException, e: - LOG.exception(_('ApiException while fetching instance vnc \ - connection')) - messages.error(request, - _('Unable to get vnc console for instance %(inst)s: %(msg)s') % - {"inst": instance_id, "msg": e.message}) - return shortcuts.redirect('dash_instances', tenant_id) - except api_exceptions.ApiException, e: - LOG.exception(_('ApiException while fetching instance info')) - messages.error(request, - _('Unable to get information for instance %(inst)s: %(msg)s') % - {"inst": instance_id, "msg": e.message}) - return shortcuts.redirect('dash_instances', tenant_id) - - return shortcuts.render_to_response( - 'django_openstack/dash/instances/detail.html', { - 'instance': instance, - 'vnc_url': vnc_url, - 'volumes': volumes - }, context_instance=template.RequestContext(request)) diff --git a/django-openstack/django_openstack/dash/views/keypairs.py b/django-openstack/django_openstack/dash/views/keypairs.py deleted file mode 100644 index ef077f4b..00000000 --- a/django-openstack/django_openstack/dash/views/keypairs.py +++ /dev/null @@ -1,138 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Views for managing Nova instances. -""" -import logging - -from django import http -from django import template -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.core import validators -from django.shortcuts import redirect, render_to_response -from django.utils.translation import ugettext as _ - -from django_openstack import api -from django_openstack import forms -from novaclient import exceptions as novaclient_exceptions - - -LOG = logging.getLogger('django_openstack.dash.views.keypairs') - - -class DeleteKeypair(forms.SelfHandlingForm): - keypair_id = forms.CharField(widget=forms.HiddenInput()) - - def handle(self, request, data): - try: - LOG.info('Deleting keypair "%s"' % data['keypair_id']) - api.keypair_delete(request, data['keypair_id']) - messages.info(request, _('Successfully deleted keypair: %s') - % data['keypair_id']) - except novaclient_exceptions.ClientException, e: - LOG.exception("ClientException in DeleteKeypair") - messages.error(request, - _('Error deleting keypair: %s') % e.message) - return redirect(request.build_absolute_uri()) - - -class CreateKeypair(forms.SelfHandlingForm): - - name = forms.CharField(max_length="20", label=_("Keypair Name"), - validators=[validators.RegexValidator('\w+')]) - - def handle(self, request, data): - try: - LOG.info('Creating keypair "%s"' % data['name']) - keypair = api.keypair_create(request, data['name']) - response = http.HttpResponse(mimetype='application/binary') - response['Content-Disposition'] = \ - 'attachment; filename=%s.pem' % keypair.name - response.write(keypair.private_key) - return response - except novaclient_exceptions.ClientException, e: - LOG.exception("ClientException in CreateKeyPair") - messages.error(request, - _('Error Creating Keypair: %s') % e.message) - return redirect(request.build_absolute_uri()) - - -class ImportKeypair(forms.SelfHandlingForm): - - name = forms.CharField(max_length="20", label=_("Keypair Name"), - validators=[validators.RegexValidator('\w+')]) - public_key = forms.CharField(label=_("Public Key"), widget=forms.Textarea) - - def handle(self, request, data): - try: - LOG.info('Importing keypair "%s"' % data['name']) - api.keypair_import(request, data['name'], data['public_key']) - messages.success(request, _('Successfully imported public key: %s') - % data['name']) - return redirect('dash_keypairs', request.user.tenant_id) - except novaclient_exceptions.ClientException, e: - LOG.exception("ClientException in ImportKeypair") - messages.error(request, - _('Error Importing Keypair: %s') % e.message) - return redirect(request.build_absolute_uri()) - - -@login_required -def index(request, tenant_id): - delete_form, handled = DeleteKeypair.maybe_handle(request) - - if handled: - return handled - - try: - keypairs = api.keypair_list(request) - except novaclient_exceptions.ClientException, e: - keypairs = [] - LOG.exception("ClientException in keypair index") - messages.error(request, _('Error fetching keypairs: %s') % e.message) - - return render_to_response('django_openstack/dash/keypairs/index.html', { - 'keypairs': keypairs, - 'delete_form': delete_form, - }, context_instance=template.RequestContext(request)) - - -@login_required -def create(request, tenant_id): - form, handled = CreateKeypair.maybe_handle(request) - if handled: - return handled - - return render_to_response('django_openstack/dash/keypairs/create.html', { - 'create_form': form, - }, context_instance=template.RequestContext(request)) - - -@login_required -def import_keypair(request, tenant_id): - form, handled = ImportKeypair.maybe_handle(request) - if handled: - return handled - - return render_to_response('django_openstack/dash/keypairs/import.html', { - 'create_form': form, - }, context_instance=template.RequestContext(request)) diff --git a/django-openstack/django_openstack/dash/views/networks.py b/django-openstack/django_openstack/dash/views/networks.py deleted file mode 100644 index 95682197..00000000 --- a/django-openstack/django_openstack/dash/views/networks.py +++ /dev/null @@ -1,252 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Views for managing api.quantum_api(request) networks. -""" -import logging - -from django import http -from django import shortcuts -from django import template -from django.conf import settings -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.utils import simplejson -from django.utils.translation import ugettext as _ - -from django_openstack import forms -from django_openstack import api - -from django_openstack.dash.views.ports import DeletePort -from django_openstack.dash.views.ports import DetachPort -from django_openstack.dash.views.ports import TogglePort - -import warnings - - -LOG = logging.getLogger('django_openstack.dash.views.networks') - - -class CreateNetwork(forms.SelfHandlingForm): - name = forms.CharField(required=True, label=_("Network Name")) - - def handle(self, request, data): - network_name = data['name'] - - try: - LOG.info('Creating network %s ' % network_name) - send_data = {'network': {'name': '%s' % network_name}} - api.quantum_create_network(request, send_data) - except Exception, e: - messages.error(request, - _('Unable to create network %(network)s: %(msg)s') % - {"network": network_name, "msg": e.message}) - return shortcuts.redirect(request.build_absolute_uri()) - else: - msg = _('Network %s has been created.') % network_name - LOG.info(msg) - messages.success(request, msg) - return shortcuts.redirect('dash_networks', - tenant_id=request.user.tenant_id) - - -class DeleteNetwork(forms.SelfHandlingForm): - network = forms.CharField(widget=forms.HiddenInput()) - - def handle(self, request, data): - try: - LOG.info('Deleting network %s ' % data['network']) - api.quantum_delete_network(request, data['network']) - except Exception, e: - messages.error(request, - _('Unable to delete network %(network)s: %(msg)s') % - {"network": data['network'], "msg": e.message}) - else: - msg = _('Network %s has been deleted.') % data['network'] - LOG.info(msg) - messages.success(request, msg) - - return shortcuts.redirect(request.build_absolute_uri()) - - -class RenameNetwork(forms.SelfHandlingForm): - network = forms.CharField(widget=forms.HiddenInput()) - new_name = forms.CharField(required=True) - - def handle(self, request, data): - try: - LOG.info('Renaming network %s to %s' % - (data['network'], data['new_name'])) - send_data = {'network': {'name': '%s' % data['new_name']}} - api.quantum_update_network(request, data['network'], send_data) - except Exception, e: - messages.error(request, - _('Unable to rename network %(network)s: %(msg)s') % - {"network": data['network'], "msg": e.message}) - else: - msg = _('Network %(net)s has been renamed to %(new_name)s.') % { - "net": data['network'], "new_name": data['new_name']} - LOG.info(msg) - messages.success(request, msg) - - return shortcuts.redirect(request.build_absolute_uri()) - - -@login_required -def index(request, tenant_id): - delete_form, delete_handled = DeleteNetwork.maybe_handle(request) - - networks = [] - instances = [] - - try: - networks_list = api.quantum_list_networks(request) - details = [] - for network in networks_list['networks']: - net_stats = _calc_network_stats(request, tenant_id, network['id']) - # Get network details like name and id - details = api.quantum_network_details(request, network['id']) - networks.append({ - 'name': details['network']['name'], - 'id': network['id'], - 'total': net_stats['total'], - 'available': net_stats['available'], - 'used': net_stats['used'], - 'tenant': tenant_id - }) - - except Exception, e: - messages.error(request, - _('Unable to get network list: %s') % e.message) - - return shortcuts.render_to_response( - 'django_openstack/dash/networks/index.html', { - 'networks': networks, - 'delete_form': delete_form, - }, context_instance=template.RequestContext(request)) - - -@login_required -def create(request, tenant_id): - network_form, handled = CreateNetwork.maybe_handle(request) - if handled: - return shortcuts.redirect('dash_networks', request.user.tenant_id) - - return shortcuts.render_to_response( - 'django_openstack/dash/networks/create.html', { - 'network_form': network_form - }, context_instance=template.RequestContext(request)) - - -@login_required -def detail(request, tenant_id, network_id): - delete_port_form, delete_handled = DeletePort.maybe_handle(request) - detach_port_form, detach_handled = DetachPort.maybe_handle(request) - toggle_port_form, port_toggle_handled = TogglePort.maybe_handle(request) - - network = {} - - try: - network_details = api.quantum_network_details(request, network_id) - network['name'] = network_details['network']['name'] - network['id'] = network_id - network['ports'] = _get_port_states(request, tenant_id, network_id) - except Exception, e: - messages.error(request, - _('Unable to get network details: %s') % e.message) - - return shortcuts.render_to_response( - 'django_openstack/dash/networks/detail.html', { - 'network': network, - 'tenant': tenant_id, - 'delete_port_form': delete_port_form, - 'detach_port_form': detach_port_form, - 'toggle_port_form': toggle_port_form - }, context_instance=template.RequestContext(request)) - - -@login_required -def rename(request, tenant_id, network_id): - rename_form, handled = RenameNetwork.maybe_handle(request) - network_details = api.quantum_network_details(request, network_id) - - if handled: - return shortcuts.redirect('dash_networks', request.user.tenant_id) - - return shortcuts.render_to_response( - 'django_openstack/dash/networks/rename.html', { - 'network': network_details, - 'rename_form': rename_form - }, context_instance=template.RequestContext(request)) - - -def _get_port_states(request, tenant_id, network_id): - """ - Helper method to find port states for a network - """ - network_ports = [] - # Get all vifs for comparison with port attachments - vifs = api.get_vif_ids(request) - - # Get all ports on this network - ports = api.quantum_list_ports(request, network_id) - for port in ports['ports']: - port_details = api.quantum_port_details(request, - network_id, port['id']) - # Get port attachments - port_attachment = api.quantum_port_attachment(request, - network_id, port['id']) - # Find instance the attachment belongs to - connected_instance = None - if port_attachment['attachment']: - for vif in vifs: - if str(vif['id']) == str(port_attachment['attachment']['id']): - connected_instance = vif['instance_name'] - break - network_ports.append({ - 'id': port_details['port']['id'], - 'state': port_details['port']['state'], - 'attachment': port_attachment['attachment'], - 'instance': connected_instance - }) - return network_ports - - -def _calc_network_stats(request, tenant_id, network_id): - """ - Helper method to calculate statistics for a network - """ - # Get all ports statistics for the network - total = 0 - available = 0 - used = 0 - ports = api.quantum_list_ports(request, network_id) - for port in ports['ports']: - total += 1 - # Get port attachment - port_attachment = api.quantum_port_attachment(request, - network_id, port['id']) - if port_attachment['attachment']: - used += 1 - else: - available += 1 - - return {'total': total, 'used': used, 'available': available} diff --git a/django-openstack/django_openstack/dash/views/objects.py b/django-openstack/django_openstack/dash/views/objects.py deleted file mode 100644 index f8d34e55..00000000 --- a/django-openstack/django_openstack/dash/views/objects.py +++ /dev/null @@ -1,195 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Views for managing Swift containers. -""" -import logging - -from django import http -from django import template -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django import shortcuts -from django.shortcuts import render_to_response -from django.utils.translation import ugettext as _ - -from django_openstack import api -from django_openstack import forms - - -LOG = logging.getLogger('django_openstack.dash') - - -class FilterObjects(forms.SelfHandlingForm): - container_name = forms.CharField(widget=forms.HiddenInput()) - object_prefix = forms.CharField(required=False) - - def handle(self, request, data): - object_prefix = data['object_prefix'] or None - - objects = api.swift_get_objects(request, - data['container_name'], - prefix=object_prefix) - - if not objects: - messages.info(request, - _('There are no objects matching that prefix in %s') % - data['container_name']) - - return objects - - -class DeleteObject(forms.SelfHandlingForm): - object_name = forms.CharField(widget=forms.HiddenInput()) - container_name = forms.CharField(widget=forms.HiddenInput()) - - def handle(self, request, data): - api.swift_delete_object( - request, - data['container_name'], - data['object_name']) - messages.info(request, - _('Successfully deleted object: %s') % - data['object_name']) - return shortcuts.redirect(request.build_absolute_uri()) - - -class UploadObject(forms.SelfHandlingForm): - name = forms.CharField(max_length="255", label=_("Object Name")) - object_file = forms.FileField(label=_("File")) - container_name = forms.CharField(widget=forms.HiddenInput()) - - def handle(self, request, data): - api.swift_upload_object( - request, - data['container_name'], - data['name'], - self.files['object_file'].read()) - - messages.success(request, _("Object was successfully uploaded.")) - return shortcuts.redirect(request.build_absolute_uri()) - - -class CopyObject(forms.SelfHandlingForm): - new_container_name = forms.ChoiceField( - label=_("Container to store object in")) - - new_object_name = forms.CharField(max_length="255", - label=_("New object name")) - orig_container_name = forms.CharField(widget=forms.HiddenInput()) - orig_object_name = forms.CharField(widget=forms.HiddenInput()) - - def __init__(self, *args, **kwargs): - containers = kwargs.pop('containers') - - super(CopyObject, self).__init__(*args, **kwargs) - - self.fields['new_container_name'].choices = containers - - def handle(self, request, data): - orig_container_name = data['orig_container_name'] - orig_object_name = data['orig_object_name'] - new_container_name = data['new_container_name'] - new_object_name = data['new_object_name'] - - api.swift_copy_object(request, orig_container_name, - orig_object_name, new_container_name, - new_object_name) - - messages.success(request, - _('Object was successfully copied to %(container)s\%(obj)s') % - {"container": new_container_name, "obj": new_object_name}) - - return shortcuts.redirect(request.build_absolute_uri()) - - -@login_required -def index(request, tenant_id, container_name): - marker = request.GET.get('marker', None) - - delete_form, handled = DeleteObject.maybe_handle(request) - if handled: - return handled - - filter_form, objects = FilterObjects.maybe_handle(request) - - if objects is None: - filter_form.fields['container_name'].initial = container_name - objects = api.swift_get_objects(request, container_name, marker=marker) - - delete_form.fields['container_name'].initial = container_name - return render_to_response( - 'django_openstack/dash/objects/index.html', { - 'container_name': container_name, - 'objects': objects, - 'delete_form': delete_form, - 'filter_form': filter_form, - }, context_instance=template.RequestContext(request)) - - -@login_required -def upload(request, tenant_id, container_name): - form, handled = UploadObject.maybe_handle(request) - if handled: - return handled - - form.fields['container_name'].initial = container_name - return render_to_response( - 'django_openstack/dash/objects/upload.html', { - 'container_name': container_name, - 'upload_form': form, - }, context_instance=template.RequestContext(request)) - - -@login_required -def download(request, tenant_id, container_name, object_name): - object_data = api.swift_get_object_data( - request, container_name, object_name) - - response = http.HttpResponse() - response['Content-Disposition'] = 'attachment; filename=%s' % \ - object_name - for data in object_data: - response.write(data) - return response - - -@login_required -def copy(request, tenant_id, container_name, object_name): - containers = \ - [(c.name, c.name) for c in api.swift_get_containers( - request)] - form, handled = CopyObject.maybe_handle(request, - containers=containers) - - if handled: - return handled - - form.fields['new_container_name'].initial = container_name - form.fields['orig_container_name'].initial = container_name - form.fields['orig_object_name'].initial = object_name - - return render_to_response( - 'django_openstack/dash/objects/copy.html', - {'container_name': container_name, - 'object_name': object_name, - 'copy_form': form}, - context_instance=template.RequestContext(request)) diff --git a/django-openstack/django_openstack/dash/views/ports.py b/django-openstack/django_openstack/dash/views/ports.py deleted file mode 100644 index ad690cfe..00000000 --- a/django-openstack/django_openstack/dash/views/ports.py +++ /dev/null @@ -1,208 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Views for managing api.quantum_api(request) network ports. -""" -import logging - -from django import http -from django import shortcuts -from django import template -from django.conf import settings -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.utils.translation import ugettext as _ - -from django_openstack import forms -from django_openstack import api - - -LOG = logging.getLogger('django_openstack.dash.views.ports') - - -class CreatePort(forms.SelfHandlingForm): - network = forms.CharField(widget=forms.HiddenInput()) - ports_num = forms.IntegerField(required=True, label=_("Number of Ports")) - - def handle(self, request, data): - try: - LOG.info('Creating %s ports on network %s' % - (data['ports_num'], data['network'])) - for i in range(0, data['ports_num']): - api.quantum_create_port(request, data['network']) - except Exception, e: - messages.error(request, - _('Unable to create ports on network %(network)s: %(msg)s') % - {"network": data['network'], "msg": e.message}) - else: - msg = _('%(num_ports)s ports created on network %(network)s.') % { - "num_ports": data['ports_num'], "network": data['network']} - LOG.info(msg) - messages.success(request, msg) - - return shortcuts.redirect(request.build_absolute_uri()) - - -class DeletePort(forms.SelfHandlingForm): - network = forms.CharField(widget=forms.HiddenInput()) - port = forms.CharField(widget=forms.HiddenInput()) - - def handle(self, request, data): - try: - LOG.info('Deleting %s ports on network %s' % - (data['port'], data['network'])) - api.quantum_delete_port(request, data['network'], data['port']) - except Exception, e: - messages.error(request, - _('Unable to delete port %(port)s: %(msg)s') % - {"port": data['port'], "msg": e.message}) - else: - msg = _('Port %(port)s deleted from network %(network)s.') % { - "port": data['port'], "network": data['network']} - LOG.info(msg) - messages.success(request, msg) - return shortcuts.redirect(request.build_absolute_uri()) - - -class AttachPort(forms.SelfHandlingForm): - network = forms.CharField(widget=forms.HiddenInput()) - port = forms.CharField(widget=forms.HiddenInput()) - vif_id = forms.CharField(widget=forms.Select(), - label=_("Select VIF to connect")) - - def handle(self, request, data): - try: - LOG.info('Attaching %s port to VIF %s' % - (data['port'], data['vif_id'])) - body = {'attachment': {'id': '%s' % data['vif_id']}} - api.quantum_attach_port(request, - data['network'], data['port'], body) - except Exception, e: - messages.error(request, - _('Unable to attach port %(port)s to VIF %(vif)s: %(msg)s') % - {"port": data['port'], - "vif": data['vif_id'], - "msg": e.message}) - else: - msg = _('Port %(port)s connected to VIF %(vif)s.') % \ - {"port": data['port'], "vif": data['vif_id']} - LOG.info(msg) - messages.success(request, msg) - return shortcuts.redirect(request.build_absolute_uri()) - - -class DetachPort(forms.SelfHandlingForm): - network = forms.CharField(widget=forms.HiddenInput()) - port = forms.CharField(widget=forms.HiddenInput()) - - def handle(self, request, data): - try: - LOG.info('Detaching port %s' % data['port']) - api.quantum_detach_port(request, data['network'], data['port']) - except Exception, e: - messages.error(request, - _('Unable to detach port %(port)s: %(message)s') % - {"port": data['port'], "message": e.message}) - else: - msg = _('Port %s detached.') % (data['port']) - LOG.info(msg) - messages.success(request, msg) - return shortcuts.redirect(request.build_absolute_uri()) - - -class TogglePort(forms.SelfHandlingForm): - network = forms.CharField(widget=forms.HiddenInput()) - port = forms.CharField(widget=forms.HiddenInput()) - state = forms.CharField(widget=forms.HiddenInput()) - - def handle(self, request, data): - try: - LOG.info('Toggling port state to %s' % data['state']) - body = {'port': {'state': '%s' % data['state']}} - api.quantum_set_port_state(request, - data['network'], data['port'], body) - except Exception, e: - messages.error(request, - _('Unable to set port state to %(state)s: %(message)s') % - {"state": data['state'], "message": e.message}) - else: - msg = _('Port %(port)s state set to %(state)s.') % { - "port": data['port'], "state": data['state']} - LOG.info(msg) - messages.success(request, msg) - return shortcuts.redirect(request.build_absolute_uri()) - - -@login_required -def create(request, tenant_id, network_id): - create_form, handled = CreatePort.maybe_handle(request) - - if handled: - return shortcuts.redirect( - 'dash_networks_detail', - tenant_id=request.user.tenant_id, - network_id=network_id - ) - - return shortcuts.render_to_response( - 'django_openstack/dash/ports/create.html', { - 'network_id': network_id, - 'create_form': create_form - }, context_instance=template.RequestContext(request)) - - -@login_required -def attach(request, tenant_id, network_id, port_id): - attach_form, handled = AttachPort.maybe_handle(request) - - if handled: - return shortcuts.redirect('dash_networks_detail', - request.user.tenant_id, network_id) - - # Get all avaliable vifs - vifs = _get_available_vifs(request) - - return shortcuts.render_to_response( - 'django_openstack/dash/ports/attach.html', { - 'network': network_id, - 'port': port_id, - 'attach_form': attach_form, - 'vifs': vifs, - }, context_instance=template.RequestContext(request)) - - -def _get_available_vifs(request): - """ - Method to get a list of available virtual interfaces - """ - vif_choices = [] - vifs = api.get_vif_ids(request) - - for vif in vifs: - if vif['available']: - name = "Instance %s VIF %s" % \ - (str(vif['instance_name']), str(vif['id'])) - vif_choices.append({ - 'name': str(name), - 'id': str(vif['id']) - }) - - return vif_choices diff --git a/django-openstack/django_openstack/dash/views/security_groups.py b/django-openstack/django_openstack/dash/views/security_groups.py deleted file mode 100644 index 8b88331d..00000000 --- a/django-openstack/django_openstack/dash/views/security_groups.py +++ /dev/null @@ -1,203 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Views for managing Nova instances. -""" -import logging - -from django import http -from django import template -from django.conf import settings -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.core import validators -from django import shortcuts -from django.shortcuts import redirect, render_to_response -from django.utils.translation import ugettext as _ - -from django_openstack import api -from django_openstack import forms -from novaclient import exceptions as novaclient_exceptions - - -LOG = logging.getLogger('django_openstack.dash.views.security_groups') - - -class CreateGroup(forms.SelfHandlingForm): - name = forms.CharField(validators=[validators.validate_slug]) - description = forms.CharField() - tenant_id = forms.CharField(widget=forms.HiddenInput()) - - def handle(self, request, data): - try: - LOG.info('Add security_group: "%s"' % data) - - security_group = api.security_group_create(request, - data['name'], - data['description']) - messages.info(request, _('Successfully created security_group: %s') - % data['name']) - return shortcuts.redirect('dash_security_groups', - data['tenant_id']) - except novaclient_exceptions.ClientException, e: - LOG.exception("ClientException in CreateGroup") - messages.error(request, _('Error creating security group: %s') % - e.message) - - -class DeleteGroup(forms.SelfHandlingForm): - tenant_id = forms.CharField(widget=forms.HiddenInput()) - security_group_id = forms.CharField(widget=forms.HiddenInput()) - - def handle(self, request, data): - try: - LOG.info('Delete security_group: "%s"' % data) - - security_group = api.security_group_delete(request, - data['security_group_id']) - messages.info(request, _('Successfully deleted security_group: %s') - % data['security_group_id']) - except novaclient_exceptions.ClientException, e: - LOG.exception("ClientException in DeleteGroup") - messages.error(request, _('Error deleting security group: %s') - % e.message) - return shortcuts.redirect('dash_security_groups', data['tenant_id']) - - -class AddRule(forms.SelfHandlingForm): - ip_protocol = forms.ChoiceField(choices=[('tcp', 'tcp'), - ('udp', 'udp'), - ('icmp', 'icmp')]) - from_port = forms.CharField() - to_port = forms.CharField() - cidr = forms.CharField() - # TODO (anthony) source group support - # group_id = forms.CharField() - - security_group_id = forms.CharField(widget=forms.HiddenInput()) - tenant_id = forms.CharField(widget=forms.HiddenInput()) - - def handle(self, request, data): - tenant_id = data['tenant_id'] - try: - LOG.info('Add security_group_rule: "%s"' % data) - - rule = api.security_group_rule_create(request, - data['security_group_id'], - data['ip_protocol'], - data['from_port'], - data['to_port'], - data['cidr']) - messages.info(request, _('Successfully added rule: %s') \ - % rule.id) - except novaclient_exceptions.ClientException, e: - LOG.exception("ClientException in AddRule") - messages.error(request, _('Error adding rule security group: %s') - % e.message) - return shortcuts.redirect(request.build_absolute_uri()) - - -class DeleteRule(forms.SelfHandlingForm): - security_group_rule_id = forms.CharField(widget=forms.HiddenInput()) - tenant_id = forms.CharField(widget=forms.HiddenInput()) - - def handle(self, request, data): - security_group_rule_id = data['security_group_rule_id'] - tenant_id = data['tenant_id'] - try: - LOG.info('Delete security_group_rule: "%s"' % data) - - security_group = api.security_group_rule_delete( - request, - security_group_rule_id) - messages.info(request, _('Successfully deleted rule: %s') - % security_group_rule_id) - except novaclient_exceptions.ClientException, e: - LOG.exception("ClientException in DeleteRule") - messages.error(request, _('Error authorizing security group: %s') - % e.message) - return shortcuts.redirect(request.build_absolute_uri()) - - -@login_required -def index(request, tenant_id): - delete_form, handled = DeleteGroup.maybe_handle(request, - initial={'tenant_id': tenant_id}) - - if handled: - return handled - - try: - security_groups = api.security_group_list(request) - except novaclient_exceptions.ClientException, e: - security_groups = [] - LOG.exception("ClientException in security_groups index") - messages.error(request, _('Error fetching security_groups: %s') - % e.message) - - return shortcuts.render_to_response( - 'django_openstack/dash/security_groups/index.html', { - 'security_groups': security_groups, - 'delete_form': delete_form, - }, context_instance=template.RequestContext(request)) - - -@login_required -def edit_rules(request, tenant_id, security_group_id): - add_form, handled = AddRule.maybe_handle(request, - initial={'tenant_id': tenant_id, - 'security_group_id': security_group_id}) - if handled: - return handled - - delete_form, handled = DeleteRule.maybe_handle(request, - initial={'tenant_id': tenant_id, - 'security_group_id': security_group_id}) - if handled: - return handled - - try: - security_group = api.security_group_get(request, security_group_id) - except novaclient_exceptions.ClientException, e: - LOG.exception("ClientException in security_groups rules edit") - messages.error(request, _('Error getting security_group: %s') - % e.message) - return shortcuts.redirect('dash_security_groups', tenant_id) - - return shortcuts.render_to_response( - 'django_openstack/dash/security_groups/edit_rules.html', { - 'security_group': security_group, - 'delete_form': delete_form, - 'form': add_form, - }, context_instance=template.RequestContext(request)) - - -@login_required -def create(request, tenant_id): - form, handled = CreateGroup.maybe_handle(request, - initial={'tenant_id': tenant_id}) - if handled: - return handled - - return shortcuts.render_to_response( - 'django_openstack/dash/security_groups/create.html', { - 'form': form, - }, context_instance=template.RequestContext(request)) diff --git a/django-openstack/django_openstack/dash/views/snapshots.py b/django-openstack/django_openstack/dash/views/snapshots.py deleted file mode 100644 index 47b5a7b0..00000000 --- a/django-openstack/django_openstack/dash/views/snapshots.py +++ /dev/null @@ -1,120 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Views for managing Nova instance snapshots. -""" - -import datetime -import logging -import re - -from django import http -from django import template -from django.conf import settings -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.shortcuts import redirect, render_to_response -from django.utils.translation import ugettext as _ -from django import shortcuts - -from django_openstack import api -from django_openstack import forms -from openstackx.api import exceptions as api_exceptions -from glance.common import exception as glance_exception - - -LOG = logging.getLogger('django_openstack.dash.views.snapshots') - - -class CreateSnapshot(forms.SelfHandlingForm): - tenant_id = forms.CharField(widget=forms.HiddenInput()) - instance_id = forms.CharField(widget=forms.TextInput( - attrs={'readonly': 'readonly'})) - name = forms.CharField(max_length="20", label=_("Snapshot Name")) - - def handle(self, request, data): - try: - LOG.info('Creating snapshot "%s"' % data['name']) - snapshot = api.snapshot_create(request, - data['instance_id'], - data['name']) - instance = api.server_get(request, data['instance_id']) - - messages.info(request, - _('Snapshot "%(name)s" created for instance "%(inst)s"') % - {"name": data['name'], "inst": instance.name}) - return shortcuts.redirect('dash_snapshots', data['tenant_id']) - except api_exceptions.ApiException, e: - msg = _('Error Creating Snapshot: %s') % e.message - LOG.exception(msg) - messages.error(request, msg) - return shortcuts.redirect(request.build_absolute_uri()) - - -@login_required -def index(request, tenant_id): - images = [] - - try: - images = api.snapshot_list_detailed(request) - except glance_exception.ClientConnectionError, e: - msg = _('Error connecting to glance: %s') % str(e) - LOG.exception(msg) - messages.error(request, msg) - except glance_exception.Error, e: - msg = _('Error retrieving image list: %s') % str(e) - LOG.exception(msg) - messages.error(request, msg) - - return render_to_response( - 'django_openstack/dash/snapshots/index.html', { - 'images': images, - }, context_instance=template.RequestContext(request)) - - -@login_required -def create(request, tenant_id, instance_id): - form, handled = CreateSnapshot.maybe_handle(request, - initial={'tenant_id': tenant_id, - 'instance_id': instance_id}) - if handled: - return handled - - try: - instance = api.server_get(request, instance_id) - except api_exceptions.ApiException, e: - msg = _("Unable to retreive instance: %s") % e - LOG.exception(msg) - messages.error(request, msg) - return shortcuts.redirect('dash_instances', tenant_id) - - valid_states = ['ACTIVE'] - if instance.status not in valid_states: - messages.error(request, _("To snapshot, instance state must be\ - one of the following: %s") % - ', '.join(valid_states)) - return shortcuts.redirect('dash_instances', tenant_id) - - return shortcuts.render_to_response( - 'django_openstack/dash/snapshots/create.html', { - 'instance': instance, - 'create_form': form, - }, context_instance=template.RequestContext(request)) diff --git a/django-openstack/django_openstack/dash/views/volumes.py b/django-openstack/django_openstack/dash/views/volumes.py deleted file mode 100644 index 69b225b3..00000000 --- a/django-openstack/django_openstack/dash/views/volumes.py +++ /dev/null @@ -1,182 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Nebula, Inc. -# All rights reserved. - -""" -Views for managing Nova volumes. -""" - -import logging - -from django import template -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.shortcuts import redirect, render_to_response -from django.utils.translation import ugettext as _ - -from django_openstack import api -from django_openstack import forms -from novaclient import exceptions as novaclient_exceptions - - -LOG = logging.getLogger('django_openstack.dash.views.volumes') - - -class CreateForm(forms.SelfHandlingForm): - name = forms.CharField(max_length="255", label="Volume Name") - description = forms.CharField(widget=forms.Textarea, - label=_("Description"), required=False) - size = forms.IntegerField(min_value=1, label="Size (GB)") - - def handle(self, request, data): - try: - api.volume_create(request, data['size'], data['name'], - data['description']) - message = 'Creating volume "%s"' % data['name'] - LOG.info(message) - messages.info(request, message) - except novaclient_exceptions.ClientException, e: - LOG.exception("ClientException in CreateVolume") - messages.error(request, - _('Error Creating Volume: %s') % e.message) - return redirect(request.build_absolute_uri()) - - -class DeleteForm(forms.SelfHandlingForm): - volume_id = forms.CharField(widget=forms.HiddenInput()) - volume_name = forms.CharField(widget=forms.HiddenInput()) - - def handle(self, request, data): - try: - api.volume_delete(request, data['volume_id']) - message = 'Deleting volume "%s"' % data['volume_id'] - LOG.info(message) - messages.info(request, message) - except novaclient_exceptions.ClientException, e: - LOG.exception("ClientException in DeleteVolume") - messages.error(request, - _('Error deleting volume: %s') % e.message) - return redirect(request.build_absolute_uri()) - - -class AttachForm(forms.SelfHandlingForm): - volume_id = forms.CharField(widget=forms.HiddenInput()) - device = forms.CharField(label="Device Name", initial="/dev/vdb") - - def __init__(self, *args, **kwargs): - super(AttachForm, self).__init__(*args, **kwargs) - instance_list = kwargs.get('initial', {}).get('instance_list', []) - self.fields['instance'] = forms.ChoiceField( - choices=instance_list, - label="Attach to Instance", - help_text="Select an instance to attach to.") - - def handle(self, request, data): - try: - api.volume_attach(request, data['volume_id'], data['instance'], - data['device']) - message = (_('Attaching volume %s to instance %s at %s') % - (data['volume_id'], data['instance'], - data['device'])) - LOG.info(message) - messages.info(request, message) - except novaclient_exceptions.ClientException, e: - LOG.exception("ClientException in AttachVolume") - messages.error(request, - _('Error attaching volume: %s') % e.message) - return redirect(request.build_absolute_uri()) - - -class DetachForm(forms.SelfHandlingForm): - volume_id = forms.CharField(widget=forms.HiddenInput()) - instance_id = forms.CharField(widget=forms.HiddenInput()) - attachment_id = forms.CharField(widget=forms.HiddenInput()) - - def handle(self, request, data): - try: - api.volume_detach(request, data['instance_id'], - data['attachment_id']) - message = (_('Detaching volume %s from instance %s') % - (data['volume_id'], data['instance_id'])) - LOG.info(message) - messages.info(request, message) - except novaclient_exceptions.ClientException, e: - LOG.exception("ClientException in DetachVolume") - messages.error(request, - _('Error detaching volume: %s') % e.message) - return redirect(request.build_absolute_uri()) - - -@login_required -def index(request, tenant_id): - delete_form, handled = DeleteForm.maybe_handle(request) - detach_form, handled = DetachForm.maybe_handle(request) - - if handled: - return handled - - try: - volumes = api.volume_list(request) - except novaclient_exceptions.ClientException, e: - volumes = [] - LOG.exception("ClientException in volume index") - messages.error(request, _('Error fetching volumes: %s') % e.message) - - return render_to_response('django_openstack/dash/volumes/index.html', { - 'volumes': volumes, 'delete_form': delete_form, - 'detach_form': detach_form - }, context_instance=template.RequestContext(request)) - - -@login_required -def detail(request, tenant_id, volume_id): - try: - volume = api.volume_get(request, volume_id) - attachment = volume.attachments[0] - if attachment: - instance = api.server_get( - request, volume.attachments[0]['serverId']) - else: - instance = None - except novaclient_exceptions.ClientException, e: - LOG.exception("ClientException in volume get") - messages.error(request, _('Error fetching volume: %s') % e.message) - return redirect('dash_volumes', tenant_id) - - return render_to_response('django_openstack/dash/volumes/detail.html', { - 'volume': volume, - 'attachment': attachment, - 'instance': instance - }, context_instance=template.RequestContext(request)) - - -@login_required -def create(request, tenant_id): - create_form, handled = CreateForm.maybe_handle(request) - - if handled: - return handled - - return render_to_response('django_openstack/dash/volumes/create.html', { - 'create_form': create_form - }, context_instance=template.RequestContext(request)) - - -@login_required -def attach(request, tenant_id, volume_id): - - def instances(): - insts = api.server_list(request) - return [(inst.id, '%s (Instance %s)' % (inst.name, inst.id)) - for inst in insts] - - attach_form, handled = AttachForm.maybe_handle( - request, initial={'instance_list': instances()}) - - if handled: - return handled - - return render_to_response('django_openstack/dash/volumes/attach.html', { - 'attach_form': attach_form, 'volume_id': volume_id - }, context_instance=template.RequestContext(request)) diff --git a/django-openstack/django_openstack/decorators.py b/django-openstack/django_openstack/decorators.py deleted file mode 100644 index a56e819c..00000000 --- a/django-openstack/django_openstack/decorators.py +++ /dev/null @@ -1,42 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 CRS4 -# -# 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. - -""" -Simple decorator container for general purpose -""" - -from django.shortcuts import redirect -import logging - - -LOG = logging.getLogger('django_openstack.syspanel') - - -def enforce_admin_access(fn): - """ Preserve unauthorized bypass typing directly the URL and redirects to - the overview dash page """ - def dec(*args, **kwargs): - if args[0].user.is_admin(): - return fn(*args, **kwargs) - else: - LOG.warn('Redirecting user "%s" from syspanel to dash ( %s )' % - (args[0].user.username, fn.__name__)) - return redirect('dash_overview') - return dec diff --git a/django-openstack/django_openstack/exceptions.py b/django-openstack/django_openstack/exceptions.py deleted file mode 100644 index 09856350..00000000 --- a/django-openstack/django_openstack/exceptions.py +++ /dev/null @@ -1,8 +0,0 @@ -""" Standardized exception classes for the OpenStack Dashboard. """ - -from novaclient import exceptions as nova_exceptions - - -class Unauthorized(nova_exceptions.Unauthorized): - """ A wrapper around novaclient's Unauthorized exception. """ - pass diff --git a/django-openstack/django_openstack/forms.py b/django-openstack/django_openstack/forms.py deleted file mode 100644 index 201add43..00000000 --- a/django-openstack/django_openstack/forms.py +++ /dev/null @@ -1,187 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import datetime -import logging -import re - -from django import utils -from django.conf import settings -from django.contrib import messages -from django.forms import widgets -from django.utils import dates -from django.utils import safestring -from django.utils import formats - -from django.forms import * - -LOG = logging.getLogger('django_openstack.forms') -RE_DATE = re.compile(r'(\d{4})-(\d\d?)-(\d\d?)$') - - -class SelectDateWidget(widgets.Widget): - """ - A Widget that splits date input into three - {% endblock %} - - diff --git a/django-openstack/django_openstack/templates/django_openstack/auth/_switch.html b/django-openstack/django_openstack/templates/django_openstack/auth/_switch.html deleted file mode 100644 index af8d4d20..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/auth/_switch.html +++ /dev/null @@ -1,17 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} -
- {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - {% for field in form.visible_fields %} - {{field.label_tag}} - {{field.errors}} - {{field}} - {% endfor %} - {% block submit %} - - {% endblock %} -
-
diff --git a/django-openstack/django_openstack/templates/django_openstack/common/_page_header.html b/django-openstack/django_openstack/templates/django_openstack/common/_page_header.html deleted file mode 100644 index 277c289b..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/common/_page_header.html +++ /dev/null @@ -1,21 +0,0 @@ -{%load i18n%} -{% block page_header %} - -{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/common/_sidebar_module.html b/django-openstack/django_openstack/templates/django_openstack/common/_sidebar_module.html deleted file mode 100644 index 661108dc..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/common/_sidebar_module.html +++ /dev/null @@ -1,10 +0,0 @@ -{% for module in modules %} -

{{module.title}}

- - -{% endfor %} - diff --git a/django-openstack/django_openstack/templates/django_openstack/common/instances/_reboot.html b/django-openstack/django_openstack/templates/django_openstack/common/instances/_reboot.html deleted file mode 100644 index 3758e1d8..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/common/instances/_reboot.html +++ /dev/null @@ -1,9 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/common/instances/_terminate.html b/django-openstack/django_openstack/templates/django_openstack/common/instances/_terminate.html deleted file mode 100644 index aea97246..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/common/instances/_terminate.html +++ /dev/null @@ -1,9 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/_sidebar.html b/django-openstack/django_openstack/templates/django_openstack/dash/_sidebar.html deleted file mode 100644 index 3a398a3f..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/_sidebar.html +++ /dev/null @@ -1,28 +0,0 @@ -{% load sidebar_modules %} -{%load i18n%} - - diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/base.html b/django-openstack/django_openstack/templates/django_openstack/dash/base.html deleted file mode 100644 index 430a4d99..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/base.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends 'base.html' %} - -{% block topbar %} - {% with current_topbar="dash" %} - {{block.super}} - {% endwith %} -{% endblock %} -{% block sidebar %} - {% include 'django_openstack/dash/_sidebar.html' %} -{% endblock %} - -{% block main %} - {% block page_header %} - {% endblock %} - -
- {% include "_messages.html" %} - {% block dash_main %}{% endblock %} -
-{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/containers/_delete.html b/django-openstack/django_openstack/templates/django_openstack/dash/containers/_delete.html deleted file mode 100644 index 97ee0609..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/containers/_delete.html +++ /dev/null @@ -1,9 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/containers/_form.html b/django-openstack/django_openstack/templates/django_openstack/dash/containers/_form.html deleted file mode 100644 index 155a20d2..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/containers/_form.html +++ /dev/null @@ -1,11 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} - {% for field in form.visible_fields %} - {{ field.label_tag }} - {{ field.errors }} - {{ field }} - {% endfor %} - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/containers/_list.html b/django-openstack/django_openstack/templates/django_openstack/dash/containers/_list.html deleted file mode 100644 index f15496ea..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/containers/_list.html +++ /dev/null @@ -1,29 +0,0 @@ -{% load i18n swift_paging %} - - - - - - - - - - {% for container in containers %} - - - - - {% endfor %} - - - - - - -
{% trans "Name" %}{% trans "Actions" %}
{{ container.name }} - -
{% object_paging containers %}
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/containers/create.html b/django-openstack/django_openstack/templates/django_openstack/dash/containers/create.html deleted file mode 100644 index f0405480..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/containers/create.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="containers" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% include "django_openstack/common/_page_header.html" with title=_("Create Tenant") %} -{% endblock page_header %} - -{% block dash_main %} -
-
- {% include 'django_openstack/dash/containers/_form.html' with form=create_form %} -
- -
-

Description:

-

{% trans "A container is a storage compartment for your data and provides a way for you to organize your data. You can think of a container as a folder in Windows® or a directory in UNIX®. The primary difference between a container and these other file system concepts is that containers cannot be nested. You can, however, create an unlimited number of containers within your account. Data must be stored in a container so you must have at least one container defined in your account prior to uploading data."%}

-
-
 
-
-{% endblock %} - - diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/containers/index.html b/django-openstack/django_openstack/templates/django_openstack/dash/containers/index.html deleted file mode 100644 index 6230c26f..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/containers/index.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="containers" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% url dash_images request.user.tenant_id as refresh_link %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Containers") refresh_link=refresh_link searchable="true" %} -{% endblock page_header %} - -{% block dash_main %} - {% include 'django_openstack/dash/containers/_list.html' %} - {% trans "Create New Container"%} >> -{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/_allocate.html b/django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/_allocate.html deleted file mode 100644 index f18b70b3..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/_allocate.html +++ /dev/null @@ -1,8 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/_associate.html b/django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/_associate.html deleted file mode 100644 index 4e8a3ead..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/_associate.html +++ /dev/null @@ -1,17 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} -
- {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - {% for field in form.visible_fields %} - {{field.label_tag}} - {{field.errors}} - {{field}} - {% endfor %} - {% block submit %} - - {% endblock %} -
-
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/_disassociate.html b/django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/_disassociate.html deleted file mode 100644 index ca5fae02..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/_disassociate.html +++ /dev/null @@ -1,9 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/_list.html b/django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/_list.html deleted file mode 100644 index 156d625e..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/_list.html +++ /dev/null @@ -1,34 +0,0 @@ -{%load i18n%} - - - - - - - {% for ip in floating_ips %} - - - - - - - {% endfor %} -
IPInstanceActions
{{ ip.ip }} - {% if ip.fixed_ip %} -
    -
  • {%trans "Instance ID:"%} {{ip.instance_id}}
  • -
  • {%trans "Fixed IP:"%} {{ip.fixed_ip}}
  • -
- {% else %} - None - {% endif %} -
-
    -
  • {% include "django_openstack/dash/floating_ips/_release.html" with form=release_form %}
  • - {% if ip.fixed_ip %} -
  • {% include "django_openstack/dash/floating_ips/_disassociate.html" with form=disassociate_form %}
  • - {% else %} -
  • {%trans "Associate to instance"%}
  • - {% endif %} -
-
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/_release.html b/django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/_release.html deleted file mode 100644 index ae13601d..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/_release.html +++ /dev/null @@ -1,9 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/associate.html b/django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/associate.html deleted file mode 100644 index ff05fa27..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/associate.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="floatingips" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Associate Floating IP") %} -{% endblock page_header %} - -{% block dash_main %} -
-
- {% include 'django_openstack/dash/floating_ips/_associate.html' with form=associate_form %} -
- -
-

{% trans "Description:"%}

-

{% trans "Associate a floating ip with an instance."%}

-
-
 
-
-{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/index.html b/django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/index.html deleted file mode 100644 index fa224670..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/floating_ips/index.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="floatingips" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% url dash_floating_ips request.user.tenant_id as refresh_link %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Floating IPs") refresh_link=refresh_link searchable="true" %} -{% endblock page_header %} - -{% block dash_main %} - {% if floating_ips %} - {% include 'django_openstack/dash/floating_ips/_list.html' %} - {% else %} -
-

{%trans "Info"%}

-

{%trans "There are currently no floating ips assigned to your tenant."%}

-
- {% endif %} - {% include "django_openstack/dash/floating_ips/_allocate.html" with form=allocate_form %} -{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/images/_delete.html b/django-openstack/django_openstack/templates/django_openstack/dash/images/_delete.html deleted file mode 100644 index cc8def96..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/images/_delete.html +++ /dev/null @@ -1,9 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/images/_form.html b/django-openstack/django_openstack/templates/django_openstack/dash/images/_form.html deleted file mode 100644 index 92dbbade..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/images/_form.html +++ /dev/null @@ -1,11 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} - {% for field in form.visible_fields %} - {{ field.label_tag }} - {{ field.errors }} - {{ field }} - {% endfor %} - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/images/_launch.html b/django-openstack/django_openstack/templates/django_openstack/dash/images/_launch.html deleted file mode 100644 index d0947c23..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/images/_launch.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends 'django_openstack/dash/images/_launch_form.html' %} -{%load i18n%} - -{% block submit %} - -{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/images/_launch_form.html b/django-openstack/django_openstack/templates/django_openstack/dash/images/_launch_form.html deleted file mode 100644 index 8eab67f6..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/images/_launch_form.html +++ /dev/null @@ -1,17 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} -
- {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - {% for field in form.visible_fields %} - {{field.label_tag}} - {{field.errors}} - {{field}} - {% endfor %} - {% block submit %} - - {% endblock %} -
-
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/images/_list.html b/django-openstack/django_openstack/templates/django_openstack/dash/images/_list.html deleted file mode 100644 index 0a0ad21e..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/images/_list.html +++ /dev/null @@ -1,29 +0,0 @@ -{%load i18n%} -{% load parse_date %} - - - - - - - - - {% for image in images %} - - - - - - - - - {% endfor %} -
{%trans "ID"%}{%trans "Name"%}{%trans "Created"%}{%trans "Updated"%}{%trans "Status"%}
{{image.id}}{{image.name}}{{image.created_at|parse_date}}{{image.updated_at|parse_date}}{{image.status|capfirst}} -
    - {% if image.owner == request.user.username %} -
  • {% include "django_openstack/dash/images/_delete.html" with form=delete_form %}
  • -
  • {%trans "Edit"%}
  • - {% endif %} -
  • {%trans "Launch"%}
  • -
-
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/images/index.html b/django-openstack/django_openstack/templates/django_openstack/dash/images/index.html deleted file mode 100644 index 421d7cf2..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/images/index.html +++ /dev/null @@ -1,25 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} -{% block sidebar %} - {% with current_sidebar="images" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% url dash_images request.user.tenant_id as refresh_link %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Images") refresh_link=refresh_link searchable="true" %} -{% endblock page_header %} - -{% block dash_main %} - {% if images %} - {% include 'django_openstack/dash/images/_list.html' %} - - {% else %} -
-

{% trans "Info"%}

-

{% trans "There are currently no images."%}

-
- {% endif %} -{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/images/launch.html b/django-openstack/django_openstack/templates/django_openstack/dash/images/launch.html deleted file mode 100644 index 35015950..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/images/launch.html +++ /dev/null @@ -1,52 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="images" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Launch Instance") %} -{% endblock page_header %} - -{% block dash_main %} -
-
- {% include 'django_openstack/dash/images/_launch.html' %} -
-
-

{% trans "Description:"%}

-

{% trans "Specify the details for launching an instance. Also please make note of the table below; all tenants have quotas which define the limit of resources you are allowed to provision."%}

- - - - - - - - - - - - - - - - - - - - - - - - - -
{% trans "Quota Name"%}{% trans "Limit"%}
{% trans "RAM (MB)"%}{{quotas.ram}}MB
{% trans "Floating IPs"%}{{quotas.floating_ips}}
{% trans "Instances"%}{{quotas.instances}}
{% trans "Volumes"%}{{quotas.volumes}}
{% trans "Gigabytes"%}{{quotas.gigabytes}}GB
-
-
 
-
-{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/images/update.html b/django-openstack/django_openstack/templates/django_openstack/dash/images/update.html deleted file mode 100644 index 9abf192b..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/images/update.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="images" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% include "django_openstack/common/_page_header.html" with title=_("Update Image") %} -{% endblock page_header %} - -{% block dash_main %} -
-
- {% include 'django_openstack/dash/images/_form.html' %} -
- -
-

{% trans "Description:"%}

-

{% trans "From here you can modify different properties of an image."%}

-
-
 
-
-{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/instances/_form.html b/django-openstack/django_openstack/templates/django_openstack/dash/instances/_form.html deleted file mode 100644 index 5e267eff..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/instances/_form.html +++ /dev/null @@ -1,11 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} - {% for field in form.visible_fields %} - {{ field.label_tag }} - {{ field.errors }} - {{ field }} - {% endfor %} - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/instances/_list.html b/django-openstack/django_openstack/templates/django_openstack/dash/instances/_list.html deleted file mode 100644 index ab6c0374..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/instances/_list.html +++ /dev/null @@ -1,75 +0,0 @@ - -{% load sizeformat %} -{%load i18n%} - - - - - - - - - - - - - {% for instance in instances %} - - - - - - - - - - - {% endfor %} - -
{% trans "ID"%}{% trans "Name"%}{% trans "Groups"%}{% trans "Image"%}{% trans "Size"%}{% trans "IPs"%}{% trans "State"%}{% trans "Actions"%}
{{instance.id}} - - {{instance.name}} - {% if instance.attrs.key_name %} -
- ({{instance.attrs.key_name}}) - {% endif %} -
-
-
    - {% for group in instance.attrs.security_groups %} -
  • {{group}}
  • - {% endfor %} -
      -
{{instance.image_name}} -
    -
  • {{instance.attrs.memory_mb|mbformat}} Ram
  • -
  • {{instance.attrs.vcpus}} VCPU
  • -
  • {{instance.attrs.disk_gb}}GB Disk
  • -
-
- {% for ip_group, addresses in instance.addresses.items %} - {% if instance.addresses.items|length > 1 %} -

{{ip_group}}

-
    - {% for address in addresses %} -
  • {{address.addr}}
  • - {% endfor %} -
- {% else %} -
    - {% for address in addresses %} -
  • {{address.addr}}
  • - {% endfor %} -
- {% endif %} - {% endfor %} -
{{instance.status|lower|capfirst}} - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/instances/detail.html b/django-openstack/django_openstack/templates/django_openstack/dash/instances/detail.html deleted file mode 100644 index 414017f8..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/instances/detail.html +++ /dev/null @@ -1,124 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{% load i18n %} - -{% block sidebar %} - {% with current_sidebar="instances" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title="Instance Detail: "|add:instance.name %} -{% endblock page_header %} - -{% block dash_main %} - - -
-
-
    -
  • -
    -

    {% trans "Status" %}

    -
      -
    • {% trans "Status:" %} {{instance.status}}
    • -
    • {% trans "Instance Name:" %} {{instance.name}}
    • -
    • {% trans "Instance ID:" %} {{instance.id}}
    • -
    -
    -
  • - -
  • -
    -

    {% trans "Specs" %}

    -
      -
    • {% trans "RAM:" %} {{instance.attrs.memory_mb}}
    • -
    • {% trans "VCPUs:" %} {{instance.attrs.vcpus}} {% trans "VCPU" %}
    • -
    • {% trans "Disk:" %} {{instance.attrs.disk_gb}}{% trans "GB Disk" %}
    • -
    -
    -
  • - -
  • -
    -

    {% trans "Meta" %}

    -
      -
    • {% trans "Key name:" %} {{instance.attrs.key_name}}
    • -
    • {% trans "Security Group(s):" %} {% for group in instance.attrs.security_groups %}{{group}}, {% endfor %}
    • -
    • {% trans "Image Name:" %} {{instance.image_name}}
    • -
    -
    -
  • -
  • -
    -

    {% trans "Volumes" %}

    - -
    -
  • -
-
- - - -
- -
- -
-{% endblock %} - -{% block footer_js %} - -{% endblock footer_js %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/instances/index.html b/django-openstack/django_openstack/templates/django_openstack/dash/instances/index.html deleted file mode 100644 index 60a2c7ab..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/instances/index.html +++ /dev/null @@ -1,66 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="instances" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% url dash_instances request.user.tenant_id as refresh_link %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Instances") refresh_link=refresh_link searchable="true" %} -{% endblock page_header %} - -{% block dash_main %} - {% if instances %} - {% include 'django_openstack/dash/instances/_list.html' %} - {% else %} -
- {% url dash_images request.user.tenant_id as dash_img_url %} -

{% trans "Info"%}

-

{% blocktrans %}There are currently no instances. You can launch an instance from the Images Page.{% endblocktrans %}

-
- {% endif %} -{% endblock %} - -{% block footer_js %} - -{% endblock footer_js %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/instances/update.html b/django-openstack/django_openstack/templates/django_openstack/dash/instances/update.html deleted file mode 100644 index d497d2be..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/instances/update.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="instances" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Update Instance") %} -{% endblock page_header %} - -{% block dash_main %} -
-
- {% include 'django_openstack/dash/instances/_form.html' with form=form %} -

<< {% trans "Return to Instances List"%}

-
- -
-

{% trans "Description:"%}

-

{% trans "Update the name and description of your instance"%}

-
-
-
-{% endblock %} - -{% block footer_js %} - -{% endblock footer_js %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/instances/usage.csv b/django-openstack/django_openstack/templates/django_openstack/dash/instances/usage.csv deleted file mode 100644 index d618b637..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/instances/usage.csv +++ /dev/null @@ -1,11 +0,0 @@ -Usage Report For Period:,{{datetime_start|date:"b. d Y H:i"}},/,{{datetime_end|date:"b. d Y H:i"}} -Tenant ID:,{{usage.tenant_id}} -Total Active VCPUs:,{{usage.total_active_vcpus}} -CPU-HRs Used:,{{usage.total_cpu_usage}} -Total Active Ram (MB):,{{usage.total_active_ram_size}} -Total Disk Size:,{{usage.total_active_disk_size}} -Total Disk Usage:,{{usage.total_disk_usage}} - -ID,Name,UserId,VCPUs,RamMB,DiskGB,Flavor,Usage(Hours),Uptime(Seconds),State -{% for instance in usage.instances %}{{instance.id}},{{instance.name|addslashes}},{{instance.user_id|addslashes}},{{instance.vcpus|addslashes}},{{instance.ram_size|addslashes}},{{instance.disk_size|addslashes}},{{instance.flavor|addslashes}},{{instance.hours}},{{instance.uptime}},{{instance.state|capfirst|addslashes}} -{% endfor %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/instances/usage.html b/django-openstack/django_openstack/templates/django_openstack/dash/instances/usage.html deleted file mode 100644 index b2290440..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/instances/usage.html +++ /dev/null @@ -1,102 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{% load parse_date %} -{% load sizeformat %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="overview" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Overview") %} -{% endblock page_header %} - -{% block dash_main %} -
- - {% if usage.instances %} -
-

CPU

-
    -
  • {{usage.total_active_vcpus|default:0}}Cores Active
  • -
  • {{usage.total_cpu_usage|floatformat|default:0}}CPU-HR Used
  • -
-
- -
-

RAM

-
    -
  • {{total_ram|default:0}}{{ram_unit}} Active
  • -
-
- -
-

Disk

-
    -
  • {{usage.total_active_disk_size|default:0}}GB Active
  • -
  • {{usage.total_disk_usage|floatformat|default:0}}GB-HR Used
  • -
-
-
- -
- {% trans "Download CSV"%} » -

Server Usage Summary - - {% if show_terminated %} - ( {% trans "Hide Terminated"%} ) - {% else %} - ( {% trans "Show Terminated"%} ) - {% endif %} - -

-
- - - - - - - - - - - - - - - {% for instance in instances %} - {% if instance.ended_at %} - - {% else %} - - {% endif %} - - - - - - - - - - - {% empty %} - - - - {% endfor %} - -
{% trans "ID"%}{% trans "Name"%}{% trans "User"%}{% trans "VCPUs"%}{% trans "Ram Size"%}{% trans "Disk Size"%}{% trans "Flavor"%}{% trans "Uptime"%}{% trans "Status"%}
{{instance.id}}{{instance.name}}{{instance.user_id}}{{instance.vcpus}}{{instance.ram_size|mbformat}}{{instance.disk_size}}GB{{instance.flavor}}{{instance.uptime_at|timesince}}{{instance.state|lower|capfirst}}
{% trans "No active instances."%}
- {% else %} -
- {% url dash_images request.user.tenant_id as dash_img_url%} -

{% trans "Info"%}

-

{% blocktrans %}There are currently no instances.

You can launch an instance from the Images Page.{% endblocktrans %}

-
- {% endif %} - -{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/keypairs/_delete.html b/django-openstack/django_openstack/templates/django_openstack/dash/keypairs/_delete.html deleted file mode 100644 index 5e821bdc..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/keypairs/_delete.html +++ /dev/null @@ -1,9 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/keypairs/_form.html b/django-openstack/django_openstack/templates/django_openstack/dash/keypairs/_form.html deleted file mode 100644 index 6e32d507..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/keypairs/_form.html +++ /dev/null @@ -1,11 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} - {% for field in form.visible_fields %} - {{ field.label_tag }} - {{ field.errors }} - {{ field }} - {% endfor %} - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/keypairs/_list.html b/django-openstack/django_openstack/templates/django_openstack/dash/keypairs/_list.html deleted file mode 100644 index d5288245..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/keypairs/_list.html +++ /dev/null @@ -1,19 +0,0 @@ -{%load i18n%} - - - - - - - {% for keypair in keypairs %} - - - - - - {% endfor %} -
{% trans "Name"%}{% trans "Fingerprint"%}{% trans "Actions"%}
{{ keypair.name }}{{ keypair.fingerprint }} -
    -
  • {% include "django_openstack/dash/keypairs/_delete.html" with form=delete_form %}
  • -
-
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/keypairs/create.html b/django-openstack/django_openstack/templates/django_openstack/dash/keypairs/create.html deleted file mode 100644 index f6fb09aa..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/keypairs/create.html +++ /dev/null @@ -1,43 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="keypairs" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block headerjs %} - -{% endblock %} - -{% block page_header %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Create Keypair") %} -{% endblock page_header %} - -{% block dash_main %} -
-
-

{% trans "Your private key is being downloaded."%}

- {% include 'django_openstack/dash/keypairs/_form.html' with form=create_form %} -

<< {% trans "Return to keypairs list"%}

-
- -
-

{% trans "Description"%}:

-

{% trans "Keypairs are ssh credentials which are injected into images when they are launched. Creating a new key pair registers the public key and downloads the private key (a .pem file)."%}

-

{% trans "Protect and use the key as you would any normal ssh private key."%}

-
-
 
-
-{% endblock %} - diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/keypairs/import.html b/django-openstack/django_openstack/templates/django_openstack/dash/keypairs/import.html deleted file mode 100644 index 01544c6d..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/keypairs/import.html +++ /dev/null @@ -1,33 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="keypairs" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block headerjs %} -{% endblock %} - -{% block page_header %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Create Keypair") %} -{% endblock page_header %} - -{% block dash_main %} -
-
- {% include 'django_openstack/dash/keypairs/_form.html' with form=create_form %} -

<< {% trans "Return to keypairs list"%}

-
- -
-

{% trans "Description"%}:

-

{% trans "Keypairs are ssh credentials which are injected into images when they are launched. Creating a new key pair registers the public key and downloads the private key (a .pem file)."%}

-

{% trans "Protect and use the key as you would any normal ssh private key."%}

-
-
 
-
-{% endblock %} - diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/keypairs/index.html b/django-openstack/django_openstack/templates/django_openstack/dash/keypairs/index.html deleted file mode 100644 index ea95b8ca..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/keypairs/index.html +++ /dev/null @@ -1,29 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="keypairs" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% url dash_keypairs request.user.tenant_id as refresh_link %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Keypairs") refresh_link=refresh_link searchable="true" %} -{% endblock page_header %} - -{% block dash_main %} - {% if keypairs %} - {% include 'django_openstack/dash/keypairs/_list.html' %} - {% trans "Add New Keypair"%} - {% trans "Import Keypair"%} - {% else %} -
-

{% trans "Info"%}

-

{% trans "There are currently no keypairs."%}

-
- {% trans "Add New Keypair"%} - {% trans "Import Keypair"%} - {% endif %} -{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/networks/_delete.html b/django-openstack/django_openstack/templates/django_openstack/dash/networks/_delete.html deleted file mode 100644 index d009dbb2..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/networks/_delete.html +++ /dev/null @@ -1,9 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/networks/_delete_port.html b/django-openstack/django_openstack/templates/django_openstack/dash/networks/_delete_port.html deleted file mode 100644 index c9e69d09..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/networks/_delete_port.html +++ /dev/null @@ -1,10 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/networks/_detach_port.html b/django-openstack/django_openstack/templates/django_openstack/dash/networks/_detach_port.html deleted file mode 100644 index f1597e52..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/networks/_detach_port.html +++ /dev/null @@ -1,10 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/networks/_detail.html b/django-openstack/django_openstack/templates/django_openstack/dash/networks/_detail.html deleted file mode 100644 index 7990f4e2..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/networks/_detail.html +++ /dev/null @@ -1,49 +0,0 @@ -{%load i18n%} - - - - - - - - - - {% for port in network.ports %} - - - - - - - - {% endfor %} - -
{% trans "ID"%}{% trans "State"%}{% trans "Attachment"%}{% trans "Actions"%}{% trans "Extensions"%}
{{port.id}}{{port.state}} - {% if port.attachment %} - - - - - - - - - -
{% trans "Instance"%}{% trans "VIF Id"%}
{{port.instance}} {{port.attachment.id}}
- {% else %} - -- - {% endif %} -
-
    - {% if port.attachment %} -
  • {% include "django_openstack/dash/networks/_detach_port.html" with form=detach_port_form %}
  • - {% else %} -
  • {% trans "Attach"%}
  • - {% endif %} -
  • {% include "django_openstack/dash/networks/_delete_port.html" with form=delete_port_form %}
  • -
  • {% include "django_openstack/dash/networks/_toggle_port.html" with form=toggle_port_form %}
  • -
-
-
    -
-
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/networks/_form.html b/django-openstack/django_openstack/templates/django_openstack/dash/networks/_form.html deleted file mode 100644 index 71e3e760..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/networks/_form.html +++ /dev/null @@ -1,11 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} - {% for field in form.visible_fields %} - {{ field.label_tag }} - {{ field.errors }} - {{ field }} - {% endfor %} - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/networks/_list.html b/django-openstack/django_openstack/templates/django_openstack/dash/networks/_list.html deleted file mode 100644 index f5823822..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/networks/_list.html +++ /dev/null @@ -1,28 +0,0 @@ -{%load i18n%} - - - - - - - - - - - {% for network in networks %} - - - - - - - - - {% endfor %} - -
{% trans "ID"%}{% trans "Name"%}{% trans "Ports"%}{% trans "Available"%}{% trans "Used"%}{% trans "Action"%}
{{network.id}}{{network.name}}{{network.total}}{{network.available}}{{network.used}} -
    -
  • {% include "django_openstack/dash/networks/_delete.html" with form=delete_form %}
  • -
  • {% trans "Rename"%}
  • -
-
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/networks/_rename.html b/django-openstack/django_openstack/templates/django_openstack/dash/networks/_rename.html deleted file mode 100644 index 7a915070..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/networks/_rename.html +++ /dev/null @@ -1,18 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - - - - - -
-

- -
- -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/networks/_rename_form.html b/django-openstack/django_openstack/templates/django_openstack/dash/networks/_rename_form.html deleted file mode 100644 index cc3368ed..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/networks/_rename_form.html +++ /dev/null @@ -1,12 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} - {% for field in form.visible_fields %} - {{ field.label_tag }} - {{ field.errors }} - {{ field }} - {% endfor %} - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/networks/_toggle_port.html b/django-openstack/django_openstack/templates/django_openstack/dash/networks/_toggle_port.html deleted file mode 100644 index 0be11240..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/networks/_toggle_port.html +++ /dev/null @@ -1,16 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - - {% if port.state == 'DOWN' %} - - - {% else %} - - - {% endif %} -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/networks/create.html b/django-openstack/django_openstack/templates/django_openstack/dash/networks/create.html deleted file mode 100644 index 05d4aa48..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/networks/create.html +++ /dev/null @@ -1,29 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="networks" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Create Network") %} -{% endblock page_header %} - -{% block dash_main %} -
-
- {% include 'django_openstack/dash/networks/_form.html' with form=network_form %} -

<< {% trans "Return to networks list"%}

-
- -
-

{% trans "Description"%}:

-

{% trans "Networks provide layer 2 connectivity to your instances."%}

-
-
 
-
-{% endblock %} - diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/networks/detail.html b/django-openstack/django_openstack/templates/django_openstack/dash/networks/detail.html deleted file mode 100644 index 2898d822..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/networks/detail.html +++ /dev/null @@ -1,31 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="networks" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% url dash_networks_detail request.user.tenant_id network.id as refresh_link %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=network.name refresh_link=refresh_link searchable="true" %} -{% endblock page_header %} - -{% block breadcrumbs %} - Networks »  - {{network.name}} -{% endblock %} - -{% block dash_main %} - {% if network.ports %} - {% include 'django_openstack/dash/networks/_detail.html' %} - {% trans "Create Ports"%} - {% else %} -
-

{% trans "Info"%}

-

{% trans "There are currently no ports in this network."%} {% trans "Create Ports"%} >>

-
- {% endif %} -{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/networks/index.html b/django-openstack/django_openstack/templates/django_openstack/dash/networks/index.html deleted file mode 100644 index 16ef1eeb..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/networks/index.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="networks" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% url dash_networks request.user.tenant_id as refresh_link %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Networks") refresh_link=refresh_link searchable="true" %} -{% endblock page_header %} - -{% block dash_main %} - {% if networks %} - {% include 'django_openstack/dash/networks/_list.html' %} - {% url dash_network_create request.user.tenant_id as dash_net_url %} - {% trans "Create New Network"%} - {% else %} -
-

{% trans "Info"%}

-

{% trans "There are currently no networks."%} {% trans "Create A Network"%} >>

-
- {% endif %} - -{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/networks/rename.html b/django-openstack/django_openstack/templates/django_openstack/dash/networks/rename.html deleted file mode 100644 index 1a63096a..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/networks/rename.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="networks" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Rename Network") %} -{% endblock page_header %} - -{% block headerjs %} - -{% endblock headerjs %} - -{% block dash_main %} -
-
- {% include 'django_openstack/dash/networks/_rename_form.html' with form=rename_form %} -

<< {% trans "Return to networks list"%}

-
- -
-

{% trans "Rename"%}:

-

{% trans "Enter a new name for your network."%}

-
-
 
-
-{% endblock %} - diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/objects/_copy.html b/django-openstack/django_openstack/templates/django_openstack/dash/objects/_copy.html deleted file mode 100644 index 4544bce9..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/objects/_copy.html +++ /dev/null @@ -1,11 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} - {% for field in form.visible_fields %} - {{ field.label_tag }} - {{ field.errors }} - {{ field }} - {% endfor %} - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/objects/_delete.html b/django-openstack/django_openstack/templates/django_openstack/dash/objects/_delete.html deleted file mode 100644 index 4a0af7bf..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/objects/_delete.html +++ /dev/null @@ -1,9 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/objects/_filter.html b/django-openstack/django_openstack/templates/django_openstack/dash/objects/_filter.html deleted file mode 100644 index 542439ed..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/objects/_filter.html +++ /dev/null @@ -1,8 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %}{{hidden}}{% endfor %} - {% for field in form.visible_fields %}{{field}}{% endfor %} - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/objects/_form.html b/django-openstack/django_openstack/templates/django_openstack/dash/objects/_form.html deleted file mode 100644 index 0a6de49f..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/objects/_form.html +++ /dev/null @@ -1,11 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} - {% for field in form.visible_fields %} - {{ field.label_tag }} - {{ field.errors }} - {{ field }} - {% endfor %} - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/objects/_list.html b/django-openstack/django_openstack/templates/django_openstack/dash/objects/_list.html deleted file mode 100644 index a73ea177..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/objects/_list.html +++ /dev/null @@ -1,29 +0,0 @@ -{% load i18n swift_paging %} - - - - - - - - - - {% for object in objects %} - - - - - {% endfor %} - - - - - - -
{% trans "Name"%}{% trans "Actions"%}
{{ object.name }} - -
{% object_paging objects %}
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/objects/_paging.html b/django-openstack/django_openstack/templates/django_openstack/dash/objects/_paging.html deleted file mode 100644 index 99299bdd..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/objects/_paging.html +++ /dev/null @@ -1 +0,0 @@ -{% if marker %}More{% endif %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/objects/copy.html b/django-openstack/django_openstack/templates/django_openstack/dash/objects/copy.html deleted file mode 100644 index 38f5e272..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/objects/copy.html +++ /dev/null @@ -1,33 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="containers" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% include "django_openstack/common/_page_header.html" with title=_("Copy Object") %} -{% endblock page_header %} - -{% block dash_main %} -

Container: {{container_name}}

- -
-
-

Copy Object: '{{object_name}}'

- {% include 'django_openstack/dash/objects/_copy.html' with form=copy_form greeting="HI" %} -

<< {% trans "Return to objects list"%}

-
- -
-

{% trans "Description"%}:

-

{% trans "You may make a new copy of an existing object to store in this or another container."%}

-
-
 
-
- -{% endblock %} - - diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/objects/index.html b/django-openstack/django_openstack/templates/django_openstack/dash/objects/index.html deleted file mode 100644 index aacefb1e..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/objects/index.html +++ /dev/null @@ -1,36 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="containers" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - -{% endblock %} - -{% block dash_main %} -

Container: {{ container_name }}

- - {% if objects %} - {% include 'django_openstack/dash/objects/_list.html' %} - {% else %} -
- {% url dash_objects_upload request.user.tenant_id container_name as dash_obj_up_url %} -

Info

-

{% blocktrans %}There are currently no objects in the container {{container_name}}. You can upload a new object from the Object Upload Page >>{% endblocktrans %}

-
- {% endif %} - {% trans "Upload New Object >>"%} - -{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/objects/upload.html b/django-openstack/django_openstack/templates/django_openstack/dash/objects/upload.html deleted file mode 100644 index 8bb7b510..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/objects/upload.html +++ /dev/null @@ -1,31 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="containers" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% include "django_openstack/common/_page_header.html" with title=_("Upload Objects") %} -{% endblock page_header %} - -{% block dash_main %} -

Container: {{ container_name }}

- -
-
- {% include 'django_openstack/dash/objects/_form.html' with form=upload_form %} -

<< {% trans "Return to objects list"%}

-
- -
-

{% trans "Description"%}:

-

{% trans "An object is the basic storage entity and any optional metadata that represents the files you store in the OpenStack Object Storage system. When you upload data to OpenStack Object Storage, the data is stored as-is (no compression or encryption) and consists of a location (container), the object's name, and any metadata consisting of key/value pairs."%}

-
-
 
-
- -{% endblock %} - diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/ports/_attach.html b/django-openstack/django_openstack/templates/django_openstack/dash/ports/_attach.html deleted file mode 100644 index 5ff6f73a..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/ports/_attach.html +++ /dev/null @@ -1,12 +0,0 @@ -
- {% csrf_token %} - {% for hidden in form.hidden_fields %}{{hidden}}{% endfor %} - {% for field in form.visible_fields %} - {{ field.label_tag }} - {{ field.errors }} - {{ field }} - {% endfor %} - - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/ports/_create.html b/django-openstack/django_openstack/templates/django_openstack/dash/ports/_create.html deleted file mode 100644 index c8f3fc3d..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/ports/_create.html +++ /dev/null @@ -1,11 +0,0 @@ -
- {% csrf_token %} - {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} - {% for field in form.visible_fields %} - {{ field.label_tag }} - {{ field.errors }} - {{ field }} - {% endfor %} - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/ports/attach.html b/django-openstack/django_openstack/templates/django_openstack/dash/ports/attach.html deleted file mode 100644 index 0155bb3a..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/ports/attach.html +++ /dev/null @@ -1,48 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="networks" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Attach Port") %} -{% endblock page_header %} - -{% block headerjs %} - -{% endblock headerjs %} - -{% block dash_main %} -
-
- {% include 'django_openstack/dash/ports/_attach.html' with form=attach_form %} -

<< {% trans "Return to network detail"%}

-
- -
- {% blocktrans %}

Select an interface from the list on the left to attach it to this port.

-

Only interfaces that are not connected to any existing port are shown

-

If you want to reconnect a connected interface, please detach it first

{% endblocktrans %} -
-
 
-
-{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/ports/create.html b/django-openstack/django_openstack/templates/django_openstack/dash/ports/create.html deleted file mode 100644 index 381fb3d1..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/ports/create.html +++ /dev/null @@ -1,29 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="networks" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Create Network") %} -{% endblock page_header %} - -{% block dash_main %} -
-
- {% include 'django_openstack/dash/ports/_create.html' with form=create_form %} -

<< {% trans "Return to network detail"%}

-
- -
-

{% trans "Description"%}:

-

{% trans "You can plug virtual interfaces from your instances to ports created in the network"%}

-
-
 
-
-{% endblock %} - diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/security_groups/_delete.html b/django-openstack/django_openstack/templates/django_openstack/dash/security_groups/_delete.html deleted file mode 100644 index 2059d502..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/security_groups/_delete.html +++ /dev/null @@ -1,9 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/security_groups/_delete_rule.html b/django-openstack/django_openstack/templates/django_openstack/dash/security_groups/_delete_rule.html deleted file mode 100644 index cba1f9c4..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/security_groups/_delete_rule.html +++ /dev/null @@ -1,9 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/security_groups/_form.html b/django-openstack/django_openstack/templates/django_openstack/dash/security_groups/_form.html deleted file mode 100644 index 0a8f7e40..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/security_groups/_form.html +++ /dev/null @@ -1,13 +0,0 @@ -{%load i18n%} -
-
- {% csrf_token %} - {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} - {% for field in form.visible_fields %} - {{ field.label_tag }} - {{ field.errors }} - {{ field }} - {% endfor %} - -
-
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/security_groups/_list.html b/django-openstack/django_openstack/templates/django_openstack/dash/security_groups/_list.html deleted file mode 100644 index d8a1da57..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/security_groups/_list.html +++ /dev/null @@ -1,22 +0,0 @@ -{%load i18n%} - - - - - - - {% for security_group in security_groups %} - - - - - - {% endfor %} -
{% trans "Name"%}{% trans "Description"%}{% trans "Actions"%}
{{ security_group.name }}{{ security_group.description }} -
    -
  • {% trans "Edit Rules"%}
  • - {% if security_group.name != 'default' %} -
  • {% include "django_openstack/dash/security_groups/_delete.html" with form=delete_form %}
  • - {% endif %} -
-
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/security_groups/create.html b/django-openstack/django_openstack/templates/django_openstack/dash/security_groups/create.html deleted file mode 100644 index 6e9d591f..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/security_groups/create.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="security_groups" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% include "django_openstack/common/_page_header.html" with title=_("Create Security Group") %} -{% endblock page_header %} - -{% block dash_main %} -
-
- {% include 'django_openstack/dash/security_groups/_form.html' %} -
- -
-

{% trans "Description"%}:

-

{% trans "From here you can create a new security group"%}

-
-
 
-
-{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/security_groups/edit_rules.html b/django-openstack/django_openstack/templates/django_openstack/dash/security_groups/edit_rules.html deleted file mode 100644 index 39986042..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/security_groups/edit_rules.html +++ /dev/null @@ -1,67 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="security_groups" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% include "django_openstack/common/_page_header.html" with title=_("Edit Security Group Rules") %} -{% endblock page_header %} - -{% block dash_main %} -
-
-

{% trans "Rules for Security Group"%} '{{security_group.name}}'

- - - - - - - - - {% for rule in security_group.rules %} - - - - - - - - {% empty %} - - - - {% endfor %} -
{% trans "IP Protocol"%}{% trans "From Port"%}{% trans "To Port"%}{% trans "CIDR"%}{% trans "Actions"%}
{{ rule.ip_protocol }}{{ rule.from_port }}{{ rule.to_port }}{{rule.ip_range.cidr}} -
    -
  • {% include "django_openstack/dash/security_groups/_delete_rule.html" with form=delete_form %}
  • -
-
- {% trans "No rules for this security group"%} -
-
-
-
-

{% trans "Add a rule"%}

-
-
- {% csrf_token %} - {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} - {% for field in form.visible_fields %} - {{ field.label_tag }} - {{ field.errors }} - {{ field }} - {% endfor %} - - -
-
-
-
-
-
-{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/security_groups/index.html b/django-openstack/django_openstack/templates/django_openstack/dash/security_groups/index.html deleted file mode 100644 index 48e9fa49..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/security_groups/index.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="security_groups" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% url dash_security_groups request.user.tenant_id as refresh_link %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Security Groups") refresh_link=refresh_link searchable="true" %} -{% endblock page_header %} - -{% block dash_main %} - {% if security_groups %} - {% include 'django_openstack/dash/security_groups/_list.html' %} - {% url dash_security_groups_create request.user.tenant_id as create_sec_url %} - {% trans "Create Security Group"%} - {% else %} -
- {% url dash_security_groups_create request.user.tenant_id as dash_sec_url %} -

{% trans "Info"%}

-

{% blocktrans %}There are currently no security groups. Create A Security Group >>{% endblocktrans %}

-
- {% endif %} -{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/settings.html b/django-openstack/django_openstack/templates/django_openstack/dash/settings.html deleted file mode 100644 index 658e2f50..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/settings.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends 'base.html' %} -{%load i18n%} - -{% block topbar %} - {% with current_topbar="settings" %} - {{block.super}} - {% endwith %} -{% endblock %} - - -{% block sidebar %} - {% include 'django_openstack/dash/_sidebar.html' %} -{% endblock %} - - -{% block main %} - {% block page_header %} - {% url dash_instances request.user.tenant_id as refresh_link %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Dashboard Settings") %} - {% endblock page_header %} - {% include "_messages.html" %} -
-
-
-

{% trans "Dashboard User Interface Language"%}

-
{% csrf_token %} -

- -
-
-
 
-
-
-{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/snapshots/_form.html b/django-openstack/django_openstack/templates/django_openstack/dash/snapshots/_form.html deleted file mode 100644 index fea72672..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/snapshots/_form.html +++ /dev/null @@ -1,14 +0,0 @@ -{%load i18n%} -
-
- {% csrf_token %} - {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} - {% for field in form.visible_fields %} - {{ field.label_tag }} - {{ field.errors }} - {{ field }} - {% endfor %} - -
-
- diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/snapshots/create.html b/django-openstack/django_openstack/templates/django_openstack/dash/snapshots/create.html deleted file mode 100644 index 2f059ab9..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/snapshots/create.html +++ /dev/null @@ -1,38 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="snapshots" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block headerjs %} - -{% endblock %} - -{% block page_header %} - {% include "django_openstack/common/_page_header.html" with title=_("Create a Snapshot") %} -{% endblock page_header %} - -{% block dash_main %} -
-
-

{% trans "Choose a name for your snapshot."%}

- {% include 'django_openstack/dash/snapshots/_form.html' with form=create_form %} -

<< {% trans "Return to snapshots list"%}

-
- -
-

{% trans "Description"%}:

-

{% trans "Snapshots preserve the disk state of a running instance."%}

-
-
 
-
-{% endblock %} - - diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/snapshots/index.html b/django-openstack/django_openstack/templates/django_openstack/dash/snapshots/index.html deleted file mode 100644 index 437a504e..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/snapshots/index.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="snapshots" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% url dash_snapshots request.user.tenant_id as refresh_link %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Snapshots") refresh_link=refresh_link searchable="true" %} -{% endblock page_header %} - -{% block dash_main %} - {% if images %} - {% include 'django_openstack/dash/images/_list.html' %} - {% else %} -
- {% url dash_instances request.user.tenant_id as inst_url %} -

{% trans "Info"%}

-

{% blocktrans %}There are currently no snapshots. You can create snapshots from running instances. View Running Instances >>{% endblocktrans %}

-
- {% endif %} -{% endblock %} - diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/volumes/_attach_form.html b/django-openstack/django_openstack/templates/django_openstack/dash/volumes/_attach_form.html deleted file mode 100644 index 6ad7d603..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/volumes/_attach_form.html +++ /dev/null @@ -1,14 +0,0 @@ -{% load i18n %} -
-
- {% csrf_token %} - {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} - {% for field in form.visible_fields %} - {{ field.label_tag }} - {{ field.errors }} - {{ field }} - {% endfor %} - - -
-
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/volumes/_delete.html b/django-openstack/django_openstack/templates/django_openstack/dash/volumes/_delete.html deleted file mode 100644 index 77310986..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/volumes/_delete.html +++ /dev/null @@ -1,10 +0,0 @@ -{% load i18n %} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{ hidden }} - {% endfor %} - - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/volumes/_detach_form.html b/django-openstack/django_openstack/templates/django_openstack/dash/volumes/_detach_form.html deleted file mode 100644 index 0f383a87..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/volumes/_detach_form.html +++ /dev/null @@ -1,11 +0,0 @@ -{% load i18n %} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{ hidden }} - {% endfor %} - - - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/volumes/_form.html b/django-openstack/django_openstack/templates/django_openstack/dash/volumes/_form.html deleted file mode 100644 index 758fb185..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/volumes/_form.html +++ /dev/null @@ -1,13 +0,0 @@ -{% load i18n %} -
-
- {% csrf_token %} - {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} - {% for field in form.visible_fields %} - {{ field.label_tag }} - {{ field.errors }} - {{ field }} - {% endfor %} - -
-
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/volumes/_list.html b/django-openstack/django_openstack/templates/django_openstack/dash/volumes/_list.html deleted file mode 100644 index 18910ff7..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/volumes/_list.html +++ /dev/null @@ -1,58 +0,0 @@ -{% load i18n %} -{% load parse_date %} - - - - - - - - - - - {% for volume in volumes %} - - - - - - - - - - {% endfor %} -
{% trans "ID" %}{% trans "Name" %}{% trans "Status" %}{% trans "Size" %}{% trans "Created" %}{% trans "Attached To" %}{% trans "Actions" %}
{{ volume.id }} - - {{ volume.displayName }} - - {{ volume.status|capfirst }}{{ volume.size }} {% trans "GB" %}{{ volume.createdAt|parse_date }} - {% for attachment in volume.attachments %} - {% if attachment %} - - - Instance {{ attachment.serverId }} - ({{ attachment.device }}) - - {% else %} - {% trans "Not Attached" %} - {% endif %} - {% endfor %} - -
    - {% if volume.status == "in-use" %} - {% for attachment in volume.attachments %} -
  • - {% include "django_openstack/dash/volumes/_detach_form.html" with form=detach_form %} -
  • - {% endfor %} - {% endif %} - {% if volume.status == "available" %} -
  • - {% trans "Attach" %} -
  • -
  • - {% include "django_openstack/dash/volumes/_delete.html" with form=delete_form %} -
  • - {% endif %} -
-
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/volumes/attach.html b/django-openstack/django_openstack/templates/django_openstack/dash/volumes/attach.html deleted file mode 100644 index ec0456f7..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/volumes/attach.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{% load i18n %} - -{% block sidebar %} - {% with current_sidebar="volumes" %} - {{ block.super }} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% include "django_openstack/common/_page_header.html" with title=_("Attach a Volume") %} -{% endblock page_header %} - -{% block dash_main %} -
-
- {% include 'django_openstack/dash/volumes/_attach_form.html' with form=attach_form %} -

<< {% trans "Return to volumes list" %}

-
- -
-

{% trans "Description" %}:

-

{% trans "Attach a volume to an instance." %}

-
-
 
-
-{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/volumes/create.html b/django-openstack/django_openstack/templates/django_openstack/dash/volumes/create.html deleted file mode 100644 index 9f57c8b6..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/volumes/create.html +++ /dev/null @@ -1,35 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="volumes" %} - {{ block.super }} - {% endwith %} -{% endblock %} - -{% block headerjs %} - -{% endblock %} - -{% block page_header %} - {% include "django_openstack/common/_page_header.html" with title=_("Create a Volume") %} -{% endblock page_header %} - -{% block dash_main %} -
-
- {% include 'django_openstack/dash/volumes/_form.html' with form=create_form %} -

<< {% trans "Return to volumes list"%}

-
- -
-

{% trans "Description" %}:

-

{% trans "Volumes are block devices that can be attached to instances." %}

-
-
 
-
-{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/volumes/detail.html b/django-openstack/django_openstack/templates/django_openstack/dash/volumes/detail.html deleted file mode 100644 index edd67348..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/volumes/detail.html +++ /dev/null @@ -1,52 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{% load i18n %} -{% load parse_date %} - -{% block sidebar %} - {% with current_sidebar="volumes" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title="Volume Detail: "|add:volume.displayName %} -{% endblock page_header %} - -{% block dash_main %} - - -
-
-
    -
  • -
    -

    {% trans "Details" %}

    -
      -
    • {% trans "ID:" %} {{ volume.id }}
    • -
    • {% trans "Name:" %} {{ volume.displayName }}
    • -
    • {% trans "Size:" %} {{ volume.size }} {% trans "GB" %}
    • -
    • {% trans "Description:" %} {{ volume.displayDescription }}
    • -
    • {% trans "Status:" %} {{ volume.status|capfirst }}
    • -
    • {% trans "Created:" %} {{ volume.createdAt|parse_date }}
    • -
    • - {% trans "Attached To:" %} - {% if instance %} - - {% trans "Instance" %} {{ instance.id }} - ({{ instance.name }}) - - {% trans "on" %} {{ attachment.device }} - {% else %} - {% trans "Not Attached" %} - {% endif %} -
    • -
    -
    -
  • -
-
-
-{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/volumes/index.html b/django-openstack/django_openstack/templates/django_openstack/dash/volumes/index.html deleted file mode 100644 index e6fbf983..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/dash/volumes/index.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends 'django_openstack/dash/base.html' %} -{% load i18n %} - -{% block sidebar %} - {% with current_sidebar="volumes" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% url dash_volumes request.user.tenant_id as refresh_link %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Volumes") refresh_link=refresh_link searchable="true" %} -{% endblock page_header %} - -{% block dash_main %} - {% if volumes %} - {% include 'django_openstack/dash/volumes/_list.html' %} - {% else %} -
-

{% trans "Info"%}

-

{% blocktrans %}There are currently no volumes.{% endblocktrans %}

-
- {% endif %} - {% trans "Create New Volume" %} -{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/_sidebar.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/_sidebar.html deleted file mode 100644 index 4068f8c3..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/_sidebar.html +++ /dev/null @@ -1,18 +0,0 @@ -{% load sidebar_modules %} -{%load i18n%} - - diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/base.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/base.html deleted file mode 100644 index 9f5d3055..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/base.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends 'base.html' %} - -{% block topbar %} - {% with current_topbar="syspanel" %} - {{block.super}} - {% endwith %} -{% endblock %} -{% block sidebar %} - {% include 'django_openstack/syspanel/_sidebar.html' %} -{% endblock %} - -{% block main %} - {% block page_header %} - {% endblock %} - -
- {% include "_messages.html" %} - {% block syspanel_main %}{% endblock %} -
-{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/_create.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/_create.html deleted file mode 100644 index 04f8cad9..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/_create.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends 'django_openstack/syspanel/flavors/_form.html' %} -{%load i18n%} - -{% block submit %} - -{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/_delete.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/_delete.html deleted file mode 100644 index 9d00e04e..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/_delete.html +++ /dev/null @@ -1,9 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/_form.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/_form.html deleted file mode 100644 index 2ec0f4cd..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/_form.html +++ /dev/null @@ -1,17 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} -
- {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - {% for field in form.visible_fields %} - {{field.label_tag}} - {{field.errors}} - {{field}} - {% endfor %} - {% block submit %} - - {% endblock %} -
-
diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/_list.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/_list.html deleted file mode 100644 index 05a5ddc8..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/_list.html +++ /dev/null @@ -1,25 +0,0 @@ -{%load i18n%} - - - - - - - - - - {% for flavor in flavors %} - - - - - - - - - {% endfor %} -
{% trans "Id"%}{% trans "Name"%}{% trans "VCPUs"%}{% trans "Memory"%}{% trans "Disk"%}{% trans "Actions"%}
{{flavor.id}}{{flavor.name}}{{flavor.vcpus}}{{flavor.ram}}MB{{flavor.disk}}GB -
    -
  • {% include "django_openstack/syspanel/flavors/_delete.html" with form=delete_form %}
  • -
-
diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/create.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/create.html deleted file mode 100644 index 5802dda0..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/create.html +++ /dev/null @@ -1,41 +0,0 @@ -{% extends 'django_openstack/syspanel/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="flavors" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% include "django_openstack/common/_page_header.html" with title=_("Create Flavor") %} -{% endblock page_header %} - -{% block syspanel_main %} -
-
-
    -
  • -

    {{global_summary.total_vcpus}} Cores

    -

    {{global_summary.total_avail_vcpus}} Avail

    -
  • -
  • -

    {{global_summary.total_ram_size_hr|floatformat}} {{global_summary.unit_ram_size}} Ram

    -

    {{global_summary.total_avail_ram_size_hr|floatformat}} {{global_summary.unit_ram_size}} Avail

    -
  • -
  • -

    {{global_summary.total_disk_size_hr|floatformat}} {{global_summary.unit_disk_size}} Disk

    -

    {{global_summary.total_avail_disk_size_hr|floatformat}} {{global_summary.unit_disk_size}} Avail

    -
  • -
-
-
- {% include "django_openstack/syspanel/flavors/_create.html" %} -
-
-

{% trans "Description"%}:

-

{% trans "From here you can define the sizing of a new flavor."%}

-
-
 
-
-{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/index.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/index.html deleted file mode 100644 index f592f654..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/flavors/index.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends 'django_openstack/syspanel/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="flavors" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% url syspanel_flavors as refresh_link %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Flavors") refresh_link=refresh_link searchable="true" %} -{% endblock page_header %} - -{% block syspanel_main %} - {% include "django_openstack/syspanel/flavors/_list.html" %} - {% trans "Create New Flavor"%} -{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/images/_delete.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/images/_delete.html deleted file mode 100644 index 3ec7a2e5..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/images/_delete.html +++ /dev/null @@ -1,9 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/images/_form.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/images/_form.html deleted file mode 100644 index c2c5e7dd..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/images/_form.html +++ /dev/null @@ -1,11 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} - {% for field in form.visible_fields %} - {{ field.label_tag }} - {{ field.errors }} - {{ field }} - {% endfor %} - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/images/_list.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/images/_list.html deleted file mode 100644 index adc85e5b..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/images/_list.html +++ /dev/null @@ -1,47 +0,0 @@ -{% load parse_date %} -{%load i18n%} - - - - - - - - - - - - {% for image in images %} - - - - - - - - - - - - - - {% endfor %} -
{% trans "ID"%}{% trans "Name"%}{% trans "Size"%}{% trans "Public"%}{% trans "Created"%}{% trans "Updated"%}{% trans "Status"%}
{{image.id}}{{image.name}}{{image.size|filesizeformat}}{{image.is_public}}{{image.created_at|parse_date}}{{image.updated_at|parse_date}}{{image.status|capfirst}} -
    -
  • {% include "django_openstack/syspanel/images/_delete.html" with form=delete_form %}
  • - {#
  • {% include "django_openstack/syspanel/images/_toggle.html" with form=toggle_form %}
  • #} - -
  • {% trans "Edit"%}
  • -
-
-
    -
  • {% trans "Location"%}: {{image.properties.image_location}}
  • -
  • {% trans "State"%}: {{image.properties.image_state}}
  • -
  • {% trans "Kernel ID"%}: {{image.properties.kernel_id}}
  • -
  • {% trans "Ramdisk ID"%}: {{image.properties.ramdisk_id}}
  • -
  • {% trans "Architecture"%}: {{image.properties.architecture}}
  • -
  • {% trans "Project ID"%}: {{image.properties.project_id}}
  • -
  • {% trans "Container Format"%}: {{image.container_format}}
  • -
  • {% trans "Disk Format"%}: {{image.disk_format}}
  • -
-
diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/images/_toggle.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/images/_toggle.html deleted file mode 100644 index 9aee6370..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/images/_toggle.html +++ /dev/null @@ -1,9 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/images/index.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/images/index.html deleted file mode 100644 index 9c0092cd..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/images/index.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends 'django_openstack/syspanel/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="images" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% url syspanel_images as refresh_link %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Images") refresh_link=refresh_link searchable="true" %} -{% endblock page_header %} - -{% block syspanel_main %} - {% include "django_openstack/syspanel/images/_list.html" %} -{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/images/update.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/images/update.html deleted file mode 100644 index 805c4c3c..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/images/update.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends 'django_openstack/syspanel/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="images" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% include "django_openstack/common/_page_header.html" with title=_("Update Image") %} -{% endblock page_header %} - -{% block syspanel_main %} -
-
- {% include 'django_openstack/syspanel/images/_form.html' %} -
- -
-

{% trans "Description"%}:

-

{% trans "From here you can modify different properties of an image."%}

-
-
 
-
-{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/instances/_list.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/instances/_list.html deleted file mode 100644 index 9c20dba6..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/instances/_list.html +++ /dev/null @@ -1,54 +0,0 @@ -{% load parse_date %} -{%load i18n%} - - - - - - - - - - - - - {% for instance in instances %} - - - - - - - - - - - - - - {% endfor %} -
{% trans "Name"%}{% trans "Tenant"%}{% trans "User"%}{% trans "Host"%}{% trans "Created"%}{% trans "Image"%}{% trans "IPs"%}{% trans "State"%}{% trans "Actions"%}
{{instance.name}} (id: {{instance.id}}){{instance.attrs.tenant_id}}{{instance.attrs.user_id}}{{instance.attrs.host}}{{instance.attrs.launched_at|parse_date}}{{instance.image_name}} - {% for ip_group, addresses in instance.addresses.items %} - {% if instance.addresses.items|length > 1 %} -

{{ip_group}}

-
    - {% for address in addresses %} -
  • {{address.addr}}
  • - {% endfor %} -
- {% else %} -
    - {% for address in addresses %} -
  • {{address.addr}}
  • - {% endfor %} -
- {% endif %} - {% endfor %} -
{{instance.status|lower|capfirst}} - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/instances/detail.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/instances/detail.html deleted file mode 100644 index 89d4723a..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/instances/detail.html +++ /dev/null @@ -1,104 +0,0 @@ -{% extends 'django_openstack/syspanel/base.html' %} - -{% block sidebar %} - {% with current_sidebar="instances" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title="Instance Detail: "|add:instance.name %} -{% endblock page_header %} - -{% block syspanel_main %} - - -
-
-
    -
  • -
    -

    Status

    -
      -
    • Status: {{instance.status}}
    • -
    • Instance Name: {{instance.name}}
    • -
    • Instance ID: {{instance.id}}
    • -
    -
    -
  • - -
  • -
    -

    Specs

    -
      -
    • RAM: {{instance.attrs.memory_mb}} MB
    • -
    • VCPUs: {{instance.attrs.vcpus}} VCPU
    • -
    • Disk: {{instance.attrs.disk_gb}}GB Disk
    • -
    -
    -
  • - -
  • -
    -

    Meta

    -
      -
    • Key name: {{instance.attrs.key_name}}
    • -
    • Security Group(s): {% for group in instance.attrs.security_groups %}{{group}}, {% endfor %}
    • -
    • Image Name: {{instance.image_name}}
    • -
    -
    -
  • -
-
- - - -
- -
- -
-{% endblock %} - -{% block footer_js %} - -{% endblock footer_js %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/instances/index.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/instances/index.html deleted file mode 100644 index 228ff2dd..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/instances/index.html +++ /dev/null @@ -1,66 +0,0 @@ -{% extends 'django_openstack/syspanel/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="instances" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% url syspanel_instances as refresh_link %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Instances") refresh_link=refresh_link searchable="true" %} -{% endblock page_header %} - -{% block syspanel_main %} - {% if instances %} - {% include 'django_openstack/syspanel/instances/_list.html' %} - {% else %} -
- {% url dash_images request.user.tenant_id as dash_image_url%} -

{% trans "Info"%}

-

{% blocktrans %}There are currently no instances. You can launch an instance from the Images Page.{% endblocktrans %}

-
- {% endif %} -{% endblock %} - -{% block footer_js %} - -{% endblock footer_js %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/instances/tenant_usage.csv b/django-openstack/django_openstack/templates/django_openstack/syspanel/instances/tenant_usage.csv deleted file mode 100644 index d618b637..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/instances/tenant_usage.csv +++ /dev/null @@ -1,11 +0,0 @@ -Usage Report For Period:,{{datetime_start|date:"b. d Y H:i"}},/,{{datetime_end|date:"b. d Y H:i"}} -Tenant ID:,{{usage.tenant_id}} -Total Active VCPUs:,{{usage.total_active_vcpus}} -CPU-HRs Used:,{{usage.total_cpu_usage}} -Total Active Ram (MB):,{{usage.total_active_ram_size}} -Total Disk Size:,{{usage.total_active_disk_size}} -Total Disk Usage:,{{usage.total_disk_usage}} - -ID,Name,UserId,VCPUs,RamMB,DiskGB,Flavor,Usage(Hours),Uptime(Seconds),State -{% for instance in usage.instances %}{{instance.id}},{{instance.name|addslashes}},{{instance.user_id|addslashes}},{{instance.vcpus|addslashes}},{{instance.ram_size|addslashes}},{{instance.disk_size|addslashes}},{{instance.flavor|addslashes}},{{instance.hours}},{{instance.uptime}},{{instance.state|capfirst|addslashes}} -{% endfor %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/instances/tenant_usage.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/instances/tenant_usage.html deleted file mode 100644 index 934830d7..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/instances/tenant_usage.html +++ /dev/null @@ -1,98 +0,0 @@ -{% extends 'django_openstack/syspanel/base.html' %} -{% load parse_date %} -{% load sizeformat %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="tenants" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("System Panel Overview") %} -{% endblock page_header %} - -{% block syspanel_main %} -
- -

Select a month to query its usage:

-
- {{dateform.date}} - -
-
- -
-
-

CPU

-
    -
  • {{usage.total_active_vcpus}}Cores Active
  • -
  • {{usage.total_cpu_usage|floatformat}}CPU-HR Used
  • -
-
- -
-

RAM

-
    -
  • {{usage.total_active_ram_size}}MB Active
  • -
-
- -
-

Disk

-
    -
  • {{usage.total_active_disk_size}}GB Active
  • -
  • {{usage.total_disk_usage|floatformat}}GB-HR Used
  • -
-
-
-

- {% trans "Active Instances"%}: {{usage.total_active_instances}} - {% trans "This month's VCPU-Hours"%}: {{usage.total_cpu_usage|floatformat}} - {% trans "This month's GB-Hours"%}: {{usage.total_disk_usage|floatformat}} -

- - - {% if usage.instances %} -
- {% trans "Download CSV"%} » -

{% trans "Tenant Usage"%}: {{tenant_id}}

-
- - - - - - - - - - - - - - - {% for instance in instances %} - {% if instance.ended_at %} - - {% else %} - - {% endif %} - - - - - - - - - - - {% endfor %} - -
{% trans "ID"%}{% trans "Name"%}{% trans "User"%}{% trans "VCPUs"%}{% trans "Ram Size"%}{% trans "Disk Size"%}{% trans "Flavor"%}{% trans "Uptime"%}{% trans "Status"%}
{{instance.id}}{{instance.name}}{{instance.user_id}}{{instance.vcpus}}{{instance.ram_size|mbformat}}{{instance.disk_size}}GB{{instance.flavor}}{{instance.uptime_at|timesince}}{{instance.state|capfirst}}
- {% endif %} - -{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/instances/usage.csv b/django-openstack/django_openstack/templates/django_openstack/syspanel/instances/usage.csv deleted file mode 100644 index 79acd2c7..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/instances/usage.csv +++ /dev/null @@ -1,8 +0,0 @@ -Usage Report For Period:,{{datetime_start|date:"b. d Y H:i"}},/,{{datetime_end|date:"b. d Y H:i"}} -Active Instances:,{{global_summary.total_active_instances|default:'-'}} -This month's VCPU-Hours:,{{global_summary.total_cpu_usage|floatformat|default:'-'}} -This month's GB-Hours:,{{global_summary.total_disk_usage|floatformat|default:'-'}} - -Name,UserId,VCPUs,RamMB,DiskGB,Flavor,Usage(Hours),Uptime(Seconds),State -{% for usage in usage_list %}{% for instance in usage.instances %}{{instance.name|addslashes}},{{instance.user_id|addslashes}},{{instance.vcpus|addslashes}},{{instance.ram_size|addslashes}},{{instance.disk_size|addslashes}},{{instance.flavor|addslashes}},{{instance.hours}},{{instance.uptime}},{{instance.state|capfirst|addslashes}}{% endfor %} -{% endfor %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/instances/usage.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/instances/usage.html deleted file mode 100644 index 905d19d7..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/instances/usage.html +++ /dev/null @@ -1,99 +0,0 @@ -{% extends 'django_openstack/syspanel/base.html' %} -{% load sizeformat %} -{%load i18n%} - -{# default landing page for a admin user #} -{# nav bar on top, sidebar, overview info in main #} - -{% block sidebar %} - {% with current_sidebar="overview" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("System Panel Overview") %} -{% endblock page_header %} - -{% block syspanel_main %} -
- {% if external_links %} -
-

{% trans "Monitoring"%}:

- -
- {% endif %} - -
- -

{% trans "Select a month to query its usage"%}:

-
- {{ dateform.date }} - -
-
- -
    -
  • Status: Good
  • -
  • -

    {{global_summary.total_vcpus}} Cores

    -

    {{global_summary.total_active_vcpus}} Used

    -

    {{global_summary.total_avail_vcpus}} Avail

    -
  • -
  • -

    {{global_summary.total_ram_size_hr|floatformat}} {{global_summary.unit_ram_size}} Ram

    -

    {{global_summary.total_active_ram_size_hr|floatformat}} {{global_summary.unit_ram_size}} Used

    -

    {{global_summary.total_avail_ram_size_hr|floatformat}} {{global_summary.unit_ram_size}} Avail

    -
  • -
  • -

    {{global_summary.total_disk_size_hr|floatformat}} {{global_summary.unit_disk_size}} Disk

    -

    {{global_summary.total_active_disk_size_hr|floatformat}} {{global_summary.unit_disk_size}} Used

    -

    {{global_summary.total_avail_disk_size_hr|floatformat}} {{global_summary.unit_disk_size}} Avail

    -
  • -
- -

- {% trans "Active Instances"%}: {{global_summary.total_active_instances|default:'-'}} - {% trans "This month's VCPU-Hours"%}: {{global_summary.total_cpu_usage|floatformat|default:'-'}} - {% trans "This month's GB-Hours"%}: {{global_summary.total_disk_usage|floatformat|default:'-'}} -

-
- - {% if usage_list %} -
-
- {% trans "Download CSV"%} » -

{% trans "Server Usage Summary"%}

-
- - - - - - - - - - - - {% for usage in usage_list %} - - - - - - - - - - {% endfor %} - - {% endif %} - - -{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/quotas/index.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/quotas/index.html deleted file mode 100644 index 8e471285..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/quotas/index.html +++ /dev/null @@ -1,29 +0,0 @@ -{% extends 'django_openstack/syspanel/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="quotas" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% url syspanel_quotas as refresh_link %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Default Quotas") refresh_link=refresh_link searchable="true" %} -{% endblock page_header %} - -{% block syspanel_main %} -
{% trans "Tenant"%}{% trans "Instances"%}{% trans "VCPUs"%}{% trans "Disk"%}{% trans "RAM"%}{% trans "VCPU CPU-Hours"%}{% trans "Disk GB-Hours"%}
{{usage.tenant_id}}{{usage.total_active_instances}}{{usage.total_active_vcpus}}{{usage.total_active_disk_size|diskgbformat}}{{usage.total_active_ram_size|mbformat}}{{usage.total_cpu_usage|floatformat}}{{usage.total_disk_usage|floatformat}}
- - - - - {% for name,value in quotas.items %} - - - - - {% endfor %} -
{% trans "Quota Name"%}{% trans "Limit"%}
{{name}}{{value}}
-{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/services/_list.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/services/_list.html deleted file mode 100644 index a158c8e8..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/services/_list.html +++ /dev/null @@ -1,65 +0,0 @@ -{%load i18n%} -{% load sizeformat %} - - - - - - - - - - {% for service in services %} - - - {% if service.type == 'nova-compute' %} - - {% else %} - - {% endif %} - - - - - {% endfor %} - {% for service in other_services %} - - - - - - - - {% endfor %} -
{% trans "Service"%}{% trans "System Stats"%}{% trans "Enabled"%}{% trans "Up"%}{% trans "Actions"%}
- {{service.type}}
- ( {{service.host}} ) -
-
    -
  • - {% trans "Hypervisor"%}: {{service.stats.hypervisor_type}}( {{service.stats.cpu_info.features|join:', '}}) -
  • -
  • - {% trans "Allocable Cores"%}: - {{service.stats.max_vcpus}} - ({{service.stats.vcpus_used}} Used, {{service.stats.vcpus}} Physical/Virtual) -
  • -
  • - {% trans "Allocable Storage"%}: - {{service.stats.max_gigabytes|diskgbformat}} - ({{service.stats.local_gb_used|diskgbformat}} Used, {{service.stats.local_gb|diskgbformat}} Physical) -
  • -
  • - {% trans "System Ram"%}: - {{service.stats.memory_mb|mbformat}} - ({{service.stats.memory_mb_used|mbformat}} Used) -
  • -
-
- {{service.disabled|yesno:"Disabled,Enabled"}}{{service.up}} -
    -
  • {% include "django_openstack/syspanel/services/_toggle.html" with form=service_toggle_enabled_form %}
  • -
-
- {{service.type}}
- ( {{service.host}} ) -
- {{service.disabled|yesno:"Disabled,Enabled"}}{{service.up}}
diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/services/_toggle.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/services/_toggle.html deleted file mode 100644 index 43c5bfb3..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/services/_toggle.html +++ /dev/null @@ -1,22 +0,0 @@ -{%load i18n%} -{% if service.disabled %} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - - -
-{% else %} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - - -
-{% endif %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/services/index.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/services/index.html deleted file mode 100644 index 551e9018..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/services/index.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends 'django_openstack/syspanel/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="services" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% url syspanel_services as refresh_link %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Services") refresh_link=refresh_link searchable="true" %} -{% endblock page_header %} - -{% block syspanel_main %} - {% include "django_openstack/syspanel/services/_list.html" %} -{% endblock %} - diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_add_user.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_add_user.html deleted file mode 100644 index a326e825..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_add_user.html +++ /dev/null @@ -1,10 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_create_form.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_create_form.html deleted file mode 100644 index 20c58988..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_create_form.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends "django_openstack/syspanel/tenants/_form.html" %} -{%load i18n%} - -{% block submit %} - -{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_delete.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_delete.html deleted file mode 100644 index e933f95b..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_delete.html +++ /dev/null @@ -1,9 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_form.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_form.html deleted file mode 100644 index 69276c0f..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_form.html +++ /dev/null @@ -1,11 +0,0 @@ -
- {% csrf_token %} - {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} - {% for field in form.visible_fields %} - {{ field.label_tag }} - {{ field.errors }} - {{ field }} - {% endfor %} - {% block submit %} - {% endblock %} -
diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_list.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_list.html deleted file mode 100644 index dbd44e80..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_list.html +++ /dev/null @@ -1,26 +0,0 @@ -{%load i18n%} - - - - - - - - - {% for tenant in tenants %} - - - - - - - - {% endfor %} -
{% trans "Id"%}{% trans "Name"%}{% trans "Description"%}{% trans "Enabled"%}{% trans "Options"%}
{{tenant.id}}{{tenant.name}}{{tenant.description}}{{tenant.enabled}} - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_quotas_form.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_quotas_form.html deleted file mode 100644 index a2168618..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_quotas_form.html +++ /dev/null @@ -1,12 +0,0 @@ -
- {% csrf_token %} - {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} - {% for field in form.visible_fields %} - {{ field.label_tag }} - {{ field.errors }} - {{ field }} - {% endfor %} - {% block submit %} - {% endblock %} -
- diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_remove_user.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_remove_user.html deleted file mode 100644 index 828c48c7..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_remove_user.html +++ /dev/null @@ -1,10 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_update_form.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_update_form.html deleted file mode 100644 index 5c17a8c2..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_update_form.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends "django_openstack/syspanel/tenants/_form.html" %} -{%load i18n%} - -{% block submit %} - -{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_update_quotas_form.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_update_quotas_form.html deleted file mode 100644 index cfa0e601..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/_update_quotas_form.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends "django_openstack/syspanel/tenants/_quotas_form.html" %} -{%load i18n%} - -{% block submit %} - -{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/create.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/create.html deleted file mode 100644 index d681c8be..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/create.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends 'django_openstack/syspanel/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="tenants" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% include "django_openstack/common/_page_header.html" with title=_("Create Tenant") %} -{% endblock page_header %} - -{% block syspanel_main %} -
-
- {% include 'django_openstack/syspanel/tenants/_create_form.html' %} -
- -
-

{% trans "Description"%}:

-

{% trans "From here you can create a new tenant (aka project) to organize users."%}

-
-
 
-
-{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/index.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/index.html deleted file mode 100644 index a6d76c07..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/index.html +++ /dev/null @@ -1,25 +0,0 @@ -{% extends 'django_openstack/syspanel/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="tenants" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% url syspanel_tenants as refresh_link %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Tenants") refresh_link=refresh_link searchable="true" %} -{% endblock page_header %} - -{% block syspanel_main %} - {% include "django_openstack/syspanel/tenants/_list.html" %} - {% trans "Create New Tenant"%} -{% endblock %} - - - - - - diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/quotas.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/quotas.html deleted file mode 100644 index 63b865fe..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/quotas.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends 'django_openstack/syspanel/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="tenants" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% include "django_openstack/common/_page_header.html" with title=_("Update Tenant Quotas") %} -{% endblock page_header %} - -{% block syspanel_main %} -
-
- {% include 'django_openstack/syspanel/tenants/_update_quotas_form.html' with form=form %} -
- -
-

{% trans "Description"%}:

-

{% trans "From here you can edit quotas (max limits) for the tenant {{tenant_id}}."%}

-
-
 
-
-{% endblock %} - - - - diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/update.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/update.html deleted file mode 100644 index 18da7c39..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/update.html +++ /dev/null @@ -1,29 +0,0 @@ -{% extends 'django_openstack/syspanel/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="tenants" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% include "django_openstack/common/_page_header.html" with title=_("Update Tenant") %} -{% endblock page_header %} - -{% block syspanel_main %} -
-
- {% include 'django_openstack/syspanel/tenants/_update_form.html' %} -
- -
-

{% trans "Description"%}:

-

{% trans "From here you can edit a tenant."%}

-
-
 
-
-{% endblock %} - - - diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/users.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/users.html deleted file mode 100644 index 5f7da9c2..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/tenants/users.html +++ /dev/null @@ -1,72 +0,0 @@ -{% extends 'django_openstack/syspanel/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="tenants" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - -{% endblock %} - -{% block syspanel_main %} -
- - {% if users %} - - - - - - - - - {% for user in users %} - - - - - - - {% endfor %} - -
{% trans "ID"%}{% trans "Name"%}{% trans "Email"%}{% trans "Actions"%}
{{user.id}}{{user.name}}{{user.email}} -
    -
  • {% include "django_openstack/syspanel/tenants/_remove_user.html" with form=remove_user_form %}
  • -
-
- {% else %} -
-

{% trans "Info"%}

-

T{% trans "here are currently no users for this tenant"%}

-
- {% endif %} - {% if new_users %} -

{% trans "Add new users"%}

- - - - - - - - {% for user in new_users %} - - - - - - {% endfor %} - -
{% trans "ID"%}{% trans "Name"%}{% trans "Actions"%}
{{user.id}}{{user.name}} -
    -
  • {% include "django_openstack/syspanel/tenants/_add_user.html" with form=add_user_form %}
  • -
-
- {% endif %} - -{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/users/_create_form.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/users/_create_form.html deleted file mode 100644 index 20221f23..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/users/_create_form.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "django_openstack/syspanel/users/_form.html" %} -{%load i18n%} - -{% block submit %} - -{% endblock %} - diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/users/_delete.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/users/_delete.html deleted file mode 100644 index 408f27e2..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/users/_delete.html +++ /dev/null @@ -1,9 +0,0 @@ -{%load i18n%} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/users/_enable_disable.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/users/_enable_disable.html deleted file mode 100644 index 69f3ac5f..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/users/_enable_disable.html +++ /dev/null @@ -1,9 +0,0 @@ -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - - -
diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/users/_form.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/users/_form.html deleted file mode 100644 index 3caecdb6..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/users/_form.html +++ /dev/null @@ -1,12 +0,0 @@ -
- {% csrf_token %} - {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} - {% for field in form.visible_fields %} - {{ field.label_tag }} - {{ field.errors }} - {{ field }} - {% endfor %} - {% block submit %} - {% endblock %} -
- diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/users/_toggle_enabled.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/users/_toggle_enabled.html deleted file mode 100644 index 7436648e..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/users/_toggle_enabled.html +++ /dev/null @@ -1,20 +0,0 @@ -{%load i18n%} -{% if user.enabled %} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - -
-{% else %} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{hidden}} - {% endfor %} - - -
-{% endif %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/users/_update_form.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/users/_update_form.html deleted file mode 100644 index 32117fb3..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/users/_update_form.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "django_openstack/syspanel/users/_form.html" %} -{%load i18n%} - -{% block submit %} - -{% endblock %} - diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/users/create.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/users/create.html deleted file mode 100644 index c54d26cb..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/users/create.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends 'django_openstack/syspanel/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="users" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Create User") %} -{% endblock page_header %} - -{% block syspanel_main %} -
-
- {% include 'django_openstack/syspanel/users/_create_form.html' %} -
- -
-

{% trans "Description"%}:

-

{% trans "From here you can create a new user and assign them to a tenant (aka project)."%}

-
-
 
-
-{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/users/index.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/users/index.html deleted file mode 100644 index 4783b70b..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/users/index.html +++ /dev/null @@ -1,45 +0,0 @@ -{% extends 'django_openstack/syspanel/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="users" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {% url syspanel_users as refresh_link %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Users") refresh_link=refresh_link searchable="true" %} -{% endblock page_header %} - -{% block syspanel_main %} - - - - - - - - - - {% for user in users %} - - - - - - - - {% endfor %} -
{% trans "ID"%}{% trans "Name"%}{% trans "Email"%}{% trans "Default Tenant"%}{% trans "Options"%}
{{user.id}}{% if not user.enabled %} (disabled){% endif %}{{user.name}}{{user.email}}{{user.tenantId}} -
    -
  • {% include "django_openstack/syspanel/users/_enable_disable.html" with form=user_enable_disable_form %}
  • -
  • {% include "django_openstack/syspanel/users/_delete.html" with form=user_delete_form %}
  • -
  • {% trans "Edit"%}
  • -
-
- {% trans "Create New User"%} -
- -{% endblock %} diff --git a/django-openstack/django_openstack/templates/django_openstack/syspanel/users/update.html b/django-openstack/django_openstack/templates/django_openstack/syspanel/users/update.html deleted file mode 100644 index 301446c5..00000000 --- a/django-openstack/django_openstack/templates/django_openstack/syspanel/users/update.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends 'django_openstack/syspanel/base.html' %} -{%load i18n%} - -{% block sidebar %} - {% with current_sidebar="users" %} - {{block.super}} - {% endwith %} -{% endblock %} - -{% block page_header %} - {# to make searchable false, just remove it from the include statement #} - {% include "django_openstack/common/_page_header.html" with title=_("Update User") %} -{% endblock page_header %} - -{% block syspanel_main %} -
-
- {% include 'django_openstack/syspanel/users/_update_form.html' %} -
- -
-

{% trans "Description"%}:

-

{% trans "From here you can edit users by changing their usernames, emails, passwords, and tenants."%}

-
-
 
-
-{% endblock %} diff --git a/django-openstack/django_openstack/templatetags/__init__.py b/django-openstack/django_openstack/templatetags/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/django-openstack/django_openstack/templatetags/templatetags/__init__.py b/django-openstack/django_openstack/templatetags/templatetags/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/django-openstack/django_openstack/templatetags/templatetags/branding.py b/django-openstack/django_openstack/templatetags/templatetags/branding.py deleted file mode 100644 index 79e5a91d..00000000 --- a/django-openstack/django_openstack/templatetags/templatetags/branding.py +++ /dev/null @@ -1,62 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Template tags for working with django_openstack. -""" - -from django import template -from django.conf import settings - - -register = template.Library() - - -class SiteBrandingNode(template.Node): - def render(self, context): - return settings.SITE_BRANDING - - -@register.tag -def site_branding(parser, token): - return SiteBrandingNode() - - -@register.tag -def site_title(parser, token): - return settings.SITE_BRANDING - - -# TODO(jeffjapan): This is just an assignment tag version of the above, replace -# when the dashboard is upgraded to a django version that -# supports the @assignment_tag decorator syntax instead. -class SaveBrandingNode(template.Node): - def __init__(self, var_name): - self.var_name = var_name - - def render(self, context): - context[self.var_name] = settings.SITE_BRANDING - return "" - - -@register.tag -def save_site_branding(parser, token): - tagname = token.contents.split() - return SaveBrandingNode(tagname[-1]) diff --git a/django-openstack/django_openstack/templatetags/templatetags/parse_date.py b/django-openstack/django_openstack/templatetags/templatetags/parse_date.py deleted file mode 100644 index 6adff07b..00000000 --- a/django-openstack/django_openstack/templatetags/templatetags/parse_date.py +++ /dev/null @@ -1,73 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Template tags for parsing date strings. -""" - -import datetime -from django import template -from dateutil import tz - - -register = template.Library() - - -def _parse_datetime(dtstr): - fmts = ["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%d %H:%M:%S.%f", - "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"] - for fmt in fmts: - try: - return datetime.datetime.strptime(dtstr, fmt) - except: - pass - - -class ParseDateNode(template.Node): - def render(self, context): - """Turn an iso formatted time back into a datetime.""" - if context == None: - return "None" - date_obj = _parse_datetime(context) - return date_obj.strftime("%m/%d/%y at %H:%M:%S") - - -@register.filter(name='parse_date') -def parse_date(value): - return ParseDateNode().render(value) - - -@register.filter(name='parse_datetime') -def parse_datetime(value): - return _parse_datetime(value) - - -@register.filter(name='parse_local_datetime') -def parse_local_datetime(value): - dt = _parse_datetime(value) - local_tz = tz.tzlocal() - utc = tz.gettz('UTC') - local_dt = dt.replace(tzinfo=utc) - return local_dt.astimezone(local_tz) - - -@register.filter(name='pretty_date') -def pretty_date(value): - return value.strftime("%d/%m/%y at %H:%M:%S") diff --git a/django-openstack/django_openstack/templatetags/templatetags/sidebar_modules.py b/django-openstack/django_openstack/templatetags/templatetags/sidebar_modules.py deleted file mode 100644 index c4172e22..00000000 --- a/django-openstack/django_openstack/templatetags/templatetags/sidebar_modules.py +++ /dev/null @@ -1,46 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from django import template -from django_openstack import signals - -register = template.Library() - - -@register.inclusion_tag('django_openstack/common/_sidebar_module.html') -def dash_sidebar_modules(request): - signals_call = signals.dash_modules_detect() - if signals_call: - return {'modules': [module[1] for module in signals_call - if module[1]['type'] == "dash"], - 'request': request} - else: - return {} - - -@register.inclusion_tag('django_openstack/common/_sidebar_module.html') -def syspanel_sidebar_modules(request): - signals_call = signals.dash_modules_detect() - if signals_call: - return {'modules': [module[1] for module in signals_call - if module[1]['type'] == "syspanel"], - 'request': request} - else: - return {} diff --git a/django-openstack/django_openstack/templatetags/templatetags/sizeformat.py b/django-openstack/django_openstack/templatetags/templatetags/sizeformat.py deleted file mode 100644 index 6e7e9ecc..00000000 --- a/django-openstack/django_openstack/templatetags/templatetags/sizeformat.py +++ /dev/null @@ -1,76 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Template tags for displaying sizes -""" - -import datetime -from django import template -from django.utils import translation -from django.utils import formats - - -register = template.Library() - - -def int_format(value): - return int(value) - - -def float_format(value): - return formats.number_format(round(value, 1), 0) - - -def filesizeformat(bytes, filesize_number_format): - try: - bytes = float(bytes) - except (TypeError, ValueError, UnicodeDecodeError): - return translation.ungettext("%(size)d byte", - "%(size)d bytes", 0) % {'size': 0} - - if bytes < 1024: - return translation.ungettext("%(size)d", - "%(size)d", bytes) % {'size': bytes} - if bytes < 1024 * 1024: - return translation.ugettext("%s KB") % \ - filesize_number_format(bytes / 1024) - if bytes < 1024 * 1024 * 1024: - return translation.ugettext("%s MB") % \ - filesize_number_format(bytes / (1024 * 1024)) - if bytes < 1024 * 1024 * 1024 * 1024: - return translation.ugettext("%s GB") % \ - filesize_number_format(bytes / (1024 * 1024 * 1024)) - if bytes < 1024 * 1024 * 1024 * 1024 * 1024: - return translation.ugettext("%s TB") % \ - filesize_number_format(bytes / (1024 * 1024 * 1024 * 1024)) - return translation.ugettext("%s PB") % \ - filesize_number_format(bytes / (1024 * 1024 * 1024 * 1024 * 1024)) - - -@register.filter(name='mbformat') -def mbformat(mb): - return filesizeformat(mb * 1024 * 1024, int_format).replace(' ', '') - - -@register.filter(name='diskgbformat') -def diskgbformat(gb): - return filesizeformat(gb * 1024 * 1024 * 1024, - float_format).replace(' ', '') diff --git a/django-openstack/django_openstack/templatetags/templatetags/swift_paging.py b/django-openstack/django_openstack/templatetags/templatetags/swift_paging.py deleted file mode 100644 index 774f6040..00000000 --- a/django-openstack/django_openstack/templatetags/templatetags/swift_paging.py +++ /dev/null @@ -1,15 +0,0 @@ -from django import template -from django.conf import settings -from django.utils import http - -register = template.Library() - - -@register.inclusion_tag('django_openstack/dash/objects/_paging.html') -def object_paging(objects): - marker = None - if objects and not \ - len(objects) < getattr(settings, 'SWIFT_PAGINATE_LIMIT', 10000): - last_object = objects[-1] - marker = http.urlquote_plus(last_object.name) - return {'marker': marker} diff --git a/django-openstack/django_openstack/templatetags/templatetags/truncate_filter.py b/django-openstack/django_openstack/templatetags/templatetags/truncate_filter.py deleted file mode 100644 index c8b4e4a3..00000000 --- a/django-openstack/django_openstack/templatetags/templatetags/truncate_filter.py +++ /dev/null @@ -1,35 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Template tags for truncating strings. -""" - -from django import template - -register = template.Library() - - -@register.filter("truncate") -def truncate(value, size): - if len(value) > size and size > 3: - return value[0:(size - 3)] + '...' - else: - return value[0:size] diff --git a/django-openstack/django_openstack/test.py b/django-openstack/django_openstack/test.py deleted file mode 100644 index 3cc4b716..00000000 --- a/django-openstack/django_openstack/test.py +++ /dev/null @@ -1,99 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from django import http -from django import test -import mox - -from django_openstack.middleware import keystone - - -class TestCase(test.TestCase): - TEST_STAFF_USER = 'staffUser' - TEST_TENANT = '1' - TEST_TENANT_NAME = 'aTenant' - TEST_TOKEN = 'aToken' - TEST_USER = 'test' - - TEST_SERVICE_CATALOG = [{ - "endpoints": [{ - "adminURL": "http://cdn.admin-nets.local:8774/v1.0", - "region": "RegionOne", - "internalURL": "http://127.0.0.1:8774/v1.0", - "publicURL": "http://cdn.admin-nets.local:8774/v1.0/" - }], - "type": "nova_compat", - "name": "nova_compat" - }, { - "endpoints": [{ - "adminURL": "http://nova/novapi/admin", - "region": "RegionOne", - "internalURL": "http://nova/novapi/internal", - "publicURL": "http://nova/novapi/public" - }], - "type": "compute", - "name": "nova" - }, { - "endpoints": [{ - "adminURL": "http://glance/glanceapi/admin", - "region": "RegionOne", - "internalURL": "http://glance/glanceapi/internal", - "publicURL": "http://glance/glanceapi/public" - }], - "type": "image", - "name": "glance" - }, { - "endpoints": [{ - "adminURL": "http://cdn.admin-nets.local:35357/v2.0", - "region": "RegionOne", - "internalURL": "http://127.0.0.1:5000/v2.0", - "publicURL": "http://cdn.admin-nets.local:5000/v2.0" - }], - "type": "identity", - "name": "identity" - }, { - "endpoints": [{ - "adminURL": "http://swift/swiftapi/admin", - "region": "RegionOne", - "internalURL": "http://swift/swiftapi/internal", - "publicURL": "http://swift/swiftapi/public" - }], - "type": "object-store", - "name": "swift" - }] - - def setUp(self): - self.mox = mox.Mox() - - self._real_get_user_from_request = keystone.get_user_from_request - self.setActiveUser(self.TEST_TOKEN, self.TEST_USER, self.TEST_TENANT, - True, self.TEST_SERVICE_CATALOG) - self.request = http.HttpRequest() - keystone.AuthenticationMiddleware().process_request(self.request) - - def tearDown(self): - self.mox.UnsetStubs() - keystone.get_user_from_request = self._real_get_user_from_request - - def setActiveUser(self, token=None, username=None, tenant_id=None, - is_admin=None, service_catalog=None, tenant_name=None): - keystone.get_user_from_request = \ - lambda x: keystone.User(token, username, tenant_id, - is_admin, service_catalog, tenant_name) diff --git a/django-openstack/django_openstack/tests/__init__.py b/django-openstack/django_openstack/tests/__init__.py deleted file mode 100644 index 8dc4fb67..00000000 --- a/django-openstack/django_openstack/tests/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from testsettings import * diff --git a/django-openstack/django_openstack/tests/api_tests.py b/django-openstack/django_openstack/tests/api_tests.py deleted file mode 100644 index f50d214a..00000000 --- a/django-openstack/django_openstack/tests/api_tests.py +++ /dev/null @@ -1,1587 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import cloudfiles -import httplib -import json -import mox - -from django import http -from django.conf import settings -from django_openstack import api -from glance import client as glance_client -from mox import IsA -from novaclient import service_catalog, client as base_client -from novaclient.keystone import client as keystone_client -from novaclient.v1_1 import client as nova_client -from openstack import compute as OSCompute -from openstackx import admin as OSAdmin -from openstackx import auth as OSAuth -from openstackx import extras as OSExtras - - -from django_openstack import test -from django_openstack.middleware import keystone - - -TEST_CONSOLE_KIND = 'vnc' -TEST_EMAIL = 'test@test.com' -TEST_HOSTNAME = 'hostname' -TEST_INSTANCE_ID = '2' -TEST_PASSWORD = '12345' -TEST_PORT = 8000 -TEST_RETURN = 'retValue' -TEST_TENANT_DESCRIPTION = 'tenantDescription' -TEST_TENANT_ID = '1234' -TEST_TENANT_NAME = 'foo' -TEST_TOKEN = 'aToken' -TEST_TOKEN_ID = 'userId' -TEST_URL = 'http://%s:%s/something/v1.0' % (TEST_HOSTNAME, TEST_PORT) -TEST_USERNAME = 'testUser' - - -class Server(object): - """ More or less fakes what the api is looking for """ - def __init__(self, id, image, attrs=None): - self.id = id - - self.image = image - if attrs is not None: - self.attrs = attrs - - def __eq__(self, other): - if self.id != other.id or \ - self.image['id'] != other.image['id']: - return False - - for k in self.attrs: - if other.attrs.__getattr__(k) != v: - return False - - return True - - def __ne__(self, other): - return not self == other - - -class Tenant(object): - """ More or less fakes what the api is looking for """ - def __init__(self, id, description, enabled): - self.id = id - self.description = description - self.enabled = enabled - - def __eq__(self, other): - return self.id == other.id and \ - self.description == other.description and \ - self.enabled == other.enabled - - def __ne__(self, other): - return not self == other - - -class Token(object): - """ More or less fakes what the api is looking for """ - def __init__(self, id, username, tenant_id, tenant_name, - serviceCatalog=None): - self.id = id - self.user = {'name': username} - self.tenant = {'id': tenant_id, 'name': tenant_name} - self.serviceCatalog = serviceCatalog - - def __eq__(self, other): - return self.id == other.id and \ - self.user['name'] == other.user['name'] and \ - self.tenant_id == other.tenant_id and \ - self.serviceCatalog == other.serviceCatalog - - def __ne__(self, other): - return not self == other - - -class APIResource(api.APIResourceWrapper): - """ Simple APIResource for testing """ - _attrs = ['foo', 'bar', 'baz'] - - @staticmethod - def get_instance(innerObject=None): - if innerObject is None: - class InnerAPIResource(object): - pass - innerObject = InnerAPIResource() - innerObject.foo = 'foo' - innerObject.bar = 'bar' - return APIResource(innerObject) - - -class APIDict(api.APIDictWrapper): - """ Simple APIDict for testing """ - _attrs = ['foo', 'bar', 'baz'] - - @staticmethod - def get_instance(innerDict=None): - if innerDict is None: - innerDict = {'foo': 'foo', - 'bar': 'bar'} - return APIDict(innerDict) - - -class APITestCase(test.TestCase): - def setUp(self): - def fake_keystoneclient(request, username=None, password=None, - tenant_id=None, token_id=None, endpoint=None): - return self.stub_keystoneclient() - super(APITestCase, self).setUp() - self._original_keystoneclient = api.keystoneclient - self._original_novaclient = api.novaclient - api.keystoneclient = fake_keystoneclient - api.novaclient = lambda request: self.stub_novaclient() - - def stub_novaclient(self): - if not hasattr(self, "novaclient"): - self.mox.StubOutWithMock(nova_client, 'Client') - self.novaclient = self.mox.CreateMock(nova_client.Client) - return self.novaclient - - def stub_keystoneclient(self): - if not hasattr(self, "keystoneclient"): - self.mox.StubOutWithMock(keystone_client, 'Client') - self.keystoneclient = self.mox.CreateMock(keystone_client.Client) - return self.keystoneclient - - def tearDown(self): - super(APITestCase, self).tearDown() - api.novaclient = self._original_novaclient - api.keystoneclient = self._original_keystoneclient - - -class APIResourceWrapperTests(test.TestCase): - def test_get_attribute(self): - resource = APIResource.get_instance() - self.assertEqual(resource.foo, 'foo') - - def test_get_invalid_attribute(self): - resource = APIResource.get_instance() - self.assertNotIn('missing', resource._attrs, - msg="Test assumption broken. Find new missing attribute") - with self.assertRaises(AttributeError): - resource.missing - - def test_get_inner_missing_attribute(self): - resource = APIResource.get_instance() - with self.assertRaises(AttributeError): - resource.baz - - -class APIDictWrapperTests(test.TestCase): - # APIDict allows for both attribute access and dictionary style [element] - # style access. Test both - def test_get_item(self): - resource = APIDict.get_instance() - self.assertEqual(resource.foo, 'foo') - self.assertEqual(resource['foo'], 'foo') - - def test_get_invalid_item(self): - resource = APIDict.get_instance() - self.assertNotIn('missing', resource._attrs, - msg="Test assumption broken. Find new missing attribute") - with self.assertRaises(AttributeError): - resource.missing - with self.assertRaises(KeyError): - resource['missing'] - - def test_get_inner_missing_attribute(self): - resource = APIDict.get_instance() - with self.assertRaises(AttributeError): - resource.baz - with self.assertRaises(KeyError): - resource['baz'] - - def test_get_with_default(self): - resource = APIDict.get_instance() - - self.assertEqual(resource.get('foo'), 'foo') - - self.assertIsNone(resource.get('baz')) - - self.assertEqual('retValue', resource.get('baz', 'retValue')) - - -# Wrapper classes that only define _attrs don't need extra testing. -# Wrapper classes that have other attributes or methods need testing -class ImageWrapperTests(test.TestCase): - dict_with_properties = { - 'properties': - {'image_state': 'running'}, - 'size': 100, - } - dict_without_properties = { - 'size': 100, - } - - def test_get_properties(self): - image = api.Image(self.dict_with_properties) - image_props = image.properties - self.assertIsInstance(image_props, api.ImageProperties) - self.assertEqual(image_props.image_state, 'running') - - def test_get_other(self): - image = api.Image(self.dict_with_properties) - self.assertEqual(image.size, 100) - - def test_get_properties_missing(self): - image = api.Image(self.dict_without_properties) - with self.assertRaises(AttributeError): - image.properties - - def test_get_other_missing(self): - image = api.Image(self.dict_without_properties) - with self.assertRaises(AttributeError): - self.assertNotIn('missing', image._attrs, - msg="Test assumption broken. Find new missing attribute") - image.missing - - -class ServerWrapperTests(test.TestCase): - HOST = 'hostname' - ID = '1' - IMAGE_NAME = 'imageName' - IMAGE_OBJ = {'id': '3', 'links': [{'href': '3', u'rel': u'bookmark'}]} - - def setUp(self): - super(ServerWrapperTests, self).setUp() - - # these are all objects "fetched" from the api - self.inner_attrs = {'host': self.HOST} - - self.inner_server = Server(self.ID, self.IMAGE_OBJ, self.inner_attrs) - self.inner_server_no_attrs = Server(self.ID, self.IMAGE_OBJ) - - #self.request = self.mox.CreateMock(http.HttpRequest) - - def test_get_attrs(self): - server = api.Server(self.inner_server, self.request) - attrs = server.attrs - # for every attribute in the "inner" object passed to the api wrapper, - # see if it can be accessed through the api.ServerAttribute instance - for k in self.inner_attrs: - self.assertEqual(attrs.__getattr__(k), self.inner_attrs[k]) - - def test_get_other(self): - server = api.Server(self.inner_server, self.request) - self.assertEqual(server.id, self.ID) - - def test_get_attrs_missing(self): - server = api.Server(self.inner_server_no_attrs, self.request) - with self.assertRaises(AttributeError): - server.attrs - - def test_get_other_missing(self): - server = api.Server(self.inner_server, self.request) - with self.assertRaises(AttributeError): - self.assertNotIn('missing', server._attrs, - msg="Test assumption broken. Find new missing attribute") - server.missing - - def test_image_name(self): - self.mox.StubOutWithMock(api, 'image_get') - api.image_get(IsA(http.HttpRequest), - self.IMAGE_OBJ['id'] - ).AndReturn(api.Image({'name': self.IMAGE_NAME})) - - server = api.Server(self.inner_server, self.request) - - self.mox.ReplayAll() - - image_name = server.image_name - - self.assertEqual(image_name, self.IMAGE_NAME) - - self.mox.VerifyAll() - - -class ApiHelperTests(test.TestCase): - """ Tests for functions that don't use one of the api objects """ - - def test_url_for(self): - GLANCE_URL = 'http://glance/glanceapi/' - NOVA_URL = 'http://nova/novapi/' - - url = api.url_for(self.request, 'image') - self.assertEqual(url, GLANCE_URL + 'internal') - - url = api.url_for(self.request, 'image', admin=False) - self.assertEqual(url, GLANCE_URL + 'internal') - - url = api.url_for(self.request, 'image', admin=True) - self.assertEqual(url, GLANCE_URL + 'admin') - - url = api.url_for(self.request, 'compute') - self.assertEqual(url, NOVA_URL + 'internal') - - url = api.url_for(self.request, 'compute', admin=False) - self.assertEqual(url, NOVA_URL + 'internal') - - url = api.url_for(self.request, 'compute', admin=True) - self.assertEqual(url, NOVA_URL + 'admin') - - self.assertNotIn('notAnApi', self.request.user.service_catalog, - 'Select a new nonexistent service catalog key') - with self.assertRaises(api.ServiceCatalogException): - url = api.url_for(self.request, 'notAnApi') - - -class TenantAPITests(APITestCase): - def test_tenant_create(self): - DESCRIPTION = 'aDescription' - ENABLED = True - - keystoneclient = self.stub_keystoneclient() - - keystoneclient.tenants = self.mox.CreateMockAnything() - keystoneclient.tenants.create(TEST_TENANT_ID, DESCRIPTION, - ENABLED).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.tenant_create(self.request, TEST_TENANT_ID, - DESCRIPTION, ENABLED) - - self.assertIsInstance(ret_val, api.Tenant) - self.assertEqual(ret_val._apiresource, TEST_RETURN) - - self.mox.VerifyAll() - - def test_tenant_get(self): - keystoneclient = self.stub_keystoneclient() - - keystoneclient.tenants = self.mox.CreateMockAnything() - keystoneclient.tenants.get(TEST_TENANT_ID).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.tenant_get(self.request, TEST_TENANT_ID) - - self.assertIsInstance(ret_val, api.Tenant) - self.assertEqual(ret_val._apiresource, TEST_RETURN) - - self.mox.VerifyAll() - - def test_tenant_list(self): - tenants = (TEST_RETURN, TEST_RETURN + '2') - - keystoneclient = self.stub_keystoneclient() - - keystoneclient.tenants = self.mox.CreateMockAnything() - keystoneclient.tenants.list().AndReturn(tenants) - - self.mox.ReplayAll() - - ret_val = api.tenant_list(self.request) - - self.assertEqual(len(ret_val), len(tenants)) - for tenant in ret_val: - self.assertIsInstance(tenant, api.Tenant) - self.assertIn(tenant._apiresource, tenants) - - self.mox.VerifyAll() - - def test_tenant_update(self): - DESCRIPTION = 'aDescription' - ENABLED = True - - keystoneclient = self.stub_keystoneclient() - - keystoneclient.tenants = self.mox.CreateMockAnything() - keystoneclient.tenants.update(TEST_TENANT_ID, TEST_TENANT_NAME, - DESCRIPTION, ENABLED).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.tenant_update(self.request, TEST_TENANT_ID, - TEST_TENANT_NAME, DESCRIPTION, ENABLED) - - self.assertIsInstance(ret_val, api.Tenant) - self.assertEqual(ret_val._apiresource, TEST_RETURN) - - self.mox.VerifyAll() - - -class UserAPITests(APITestCase): - def test_user_create(self): - keystoneclient = self.stub_keystoneclient() - - keystoneclient.users = self.mox.CreateMockAnything() - keystoneclient.users.create(TEST_USERNAME, TEST_PASSWORD, TEST_EMAIL, - TEST_TENANT_ID, True).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.user_create(self.request, TEST_USERNAME, TEST_EMAIL, - TEST_PASSWORD, TEST_TENANT_ID, True) - - self.assertIsInstance(ret_val, api.User) - self.assertEqual(ret_val._apiresource, TEST_RETURN) - - self.mox.VerifyAll() - - def test_user_delete(self): - keystoneclient = self.stub_keystoneclient() - - keystoneclient.users = self.mox.CreateMockAnything() - keystoneclient.users.delete(TEST_USERNAME).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.user_delete(self.request, TEST_USERNAME) - - self.assertIsNone(ret_val) - - self.mox.VerifyAll() - - def test_user_get(self): - keystoneclient = self.stub_keystoneclient() - - keystoneclient.users = self.mox.CreateMockAnything() - keystoneclient.users.get(TEST_USERNAME).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.user_get(self.request, TEST_USERNAME) - - self.assertIsInstance(ret_val, api.User) - self.assertEqual(ret_val._apiresource, TEST_RETURN) - - self.mox.VerifyAll() - - def test_user_list(self): - users = (TEST_USERNAME, TEST_USERNAME + '2') - - keystoneclient = self.stub_keystoneclient() - keystoneclient.users = self.mox.CreateMockAnything() - keystoneclient.users.list(tenant_id=None).AndReturn(users) - - self.mox.ReplayAll() - - ret_val = api.user_list(self.request) - - self.assertEqual(len(ret_val), len(users)) - for user in ret_val: - self.assertIsInstance(user, api.User) - self.assertIn(user._apiresource, users) - - self.mox.VerifyAll() - - def test_user_update_email(self): - keystoneclient = self.stub_keystoneclient() - keystoneclient.users = self.mox.CreateMockAnything() - keystoneclient.users.update_email(TEST_USERNAME, - TEST_EMAIL).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.user_update_email(self.request, TEST_USERNAME, - TEST_EMAIL) - - self.assertIsInstance(ret_val, api.User) - self.assertEqual(ret_val._apiresource, TEST_RETURN) - - self.mox.VerifyAll() - - def test_user_update_password(self): - keystoneclient = self.stub_keystoneclient() - keystoneclient.users = self.mox.CreateMockAnything() - keystoneclient.users.update_password(TEST_USERNAME, - TEST_PASSWORD).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.user_update_password(self.request, TEST_USERNAME, - TEST_PASSWORD) - - self.assertIsInstance(ret_val, api.User) - self.assertEqual(ret_val._apiresource, TEST_RETURN) - - self.mox.VerifyAll() - - def test_user_update_tenant(self): - keystoneclient = self.stub_keystoneclient() - keystoneclient.users = self.mox.CreateMockAnything() - keystoneclient.users.update_tenant(TEST_USERNAME, - TEST_TENANT_ID).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.user_update_tenant(self.request, TEST_USERNAME, - TEST_TENANT_ID) - - self.assertIsInstance(ret_val, api.User) - self.assertEqual(ret_val._apiresource, TEST_RETURN) - - self.mox.VerifyAll() - - -class RoleAPITests(APITestCase): - def test_role_add_for_tenant_user(self): - keystoneclient = self.stub_keystoneclient() - - role = api.Role(APIResource.get_instance()) - role.id = TEST_RETURN - role.name = TEST_RETURN - - keystoneclient.roles = self.mox.CreateMockAnything() - keystoneclient.roles.add_user_to_tenant(TEST_TENANT_ID, - TEST_USERNAME, - TEST_RETURN).AndReturn(role) - api._get_role = self.mox.CreateMockAnything() - api._get_role(IsA(http.HttpRequest), IsA(str)).AndReturn(role) - - self.mox.ReplayAll() - ret_val = api.role_add_for_tenant_user(self.request, - TEST_TENANT_ID, - TEST_USERNAME, - TEST_RETURN) - self.assertEqual(ret_val, role) - - self.mox.VerifyAll() - - -class AdminApiTests(APITestCase): - def stub_admin_api(self, count=1): - self.mox.StubOutWithMock(api, 'admin_api') - admin_api = self.mox.CreateMock(OSAdmin.Admin) - for i in range(count): - api.admin_api(IsA(http.HttpRequest)).AndReturn(admin_api) - return admin_api - - def test_get_admin_api(self): - self.mox.StubOutClassWithMocks(OSAdmin, 'Admin') - OSAdmin.Admin(auth_token=TEST_TOKEN, management_url=TEST_URL) - - self.mox.StubOutWithMock(api, 'url_for') - api.url_for(IsA(http.HttpRequest), 'compute', True).AndReturn(TEST_URL) - api.url_for(IsA(http.HttpRequest), 'compute', True).AndReturn(TEST_URL) - - self.mox.ReplayAll() - - self.assertIsNotNone(api.admin_api(self.request)) - - self.mox.VerifyAll() - - def test_flavor_create(self): - FLAVOR_DISK = 1000 - FLAVOR_ID = 6 - FLAVOR_MEMORY = 1024 - FLAVOR_NAME = 'newFlavor' - FLAVOR_VCPU = 2 - - admin_api = self.stub_admin_api() - - admin_api.flavors = self.mox.CreateMockAnything() - admin_api.flavors.create(FLAVOR_NAME, FLAVOR_MEMORY, FLAVOR_VCPU, - FLAVOR_DISK, FLAVOR_ID).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.flavor_create(self.request, FLAVOR_NAME, - str(FLAVOR_MEMORY), str(FLAVOR_VCPU), - str(FLAVOR_DISK), FLAVOR_ID) - - self.assertIsInstance(ret_val, api.Flavor) - self.assertEqual(ret_val._apiresource, TEST_RETURN) - - self.mox.VerifyAll() - - def test_flavor_delete(self): - FLAVOR_ID = 6 - - admin_api = self.stub_admin_api(count=2) - - admin_api.flavors = self.mox.CreateMockAnything() - admin_api.flavors.delete(FLAVOR_ID, False).AndReturn(TEST_RETURN) - admin_api.flavors.delete(FLAVOR_ID, True).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.flavor_delete(self.request, FLAVOR_ID) - self.assertIsNone(ret_val) - - ret_val = api.flavor_delete(self.request, FLAVOR_ID, purge=True) - self.assertIsNone(ret_val) - - def test_service_get(self): - NAME = 'serviceName' - - admin_api = self.stub_admin_api() - admin_api.services = self.mox.CreateMockAnything() - admin_api.services.get(NAME).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.service_get(self.request, NAME) - - self.assertIsInstance(ret_val, api.Services) - self.assertEqual(ret_val._apiresource, TEST_RETURN) - - self.mox.VerifyAll() - - def test_service_list(self): - services = (TEST_RETURN, TEST_RETURN + '2') - - admin_api = self.stub_admin_api() - admin_api.services = self.mox.CreateMockAnything() - admin_api.services.list().AndReturn(services) - - self.mox.ReplayAll() - - ret_val = api.service_list(self.request) - - for service in ret_val: - self.assertIsInstance(service, api.Services) - self.assertIn(service._apiresource, services) - - self.mox.VerifyAll() - - def test_service_update(self): - ENABLED = True - NAME = 'serviceName' - - admin_api = self.stub_admin_api() - admin_api.services = self.mox.CreateMockAnything() - admin_api.services.update(NAME, ENABLED).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.service_update(self.request, NAME, ENABLED) - - self.assertIsInstance(ret_val, api.Services) - self.assertEqual(ret_val._apiresource, TEST_RETURN) - - self.mox.VerifyAll() - - -class TokenApiTests(APITestCase): - def setUp(self): - super(TokenApiTests, self).setUp() - self._prev_OPENSTACK_KEYSTONE_URL = getattr(settings, - 'OPENSTACK_KEYSTONE_URL', - None) - settings.OPENSTACK_KEYSTONE_URL = TEST_URL - - def tearDown(self): - super(TokenApiTests, self).tearDown() - settings.OPENSTACK_KEYSTONE_URL = self._prev_OPENSTACK_KEYSTONE_URL - - def test_token_create(self): - catalog = { - 'access': { - 'token': { - 'id': TEST_TOKEN_ID, - }, - 'user': { - 'roles': [], - } - } - } - test_token = Token(TEST_TOKEN_ID, TEST_USERNAME, - TEST_TENANT_ID, TEST_TENANT_NAME) - - keystoneclient = self.stub_keystoneclient() - - keystoneclient.tokens = self.mox.CreateMockAnything() - keystoneclient.tokens.authenticate(username=TEST_USERNAME, - password=TEST_PASSWORD, - tenant=TEST_TENANT_ID - ).AndReturn(test_token) - - self.mox.ReplayAll() - - ret_val = api.token_create(self.request, TEST_TENANT_ID, - TEST_USERNAME, TEST_PASSWORD) - - self.assertEqual(test_token.tenant['id'], ret_val.tenant['id']) - - self.mox.VerifyAll() - - -class ComputeApiTests(APITestCase): - def stub_compute_api(self, count=1): - self.mox.StubOutWithMock(api, 'compute_api') - compute_api = self.mox.CreateMock(OSCompute.Compute) - for i in range(count): - api.compute_api(IsA(http.HttpRequest)).AndReturn(compute_api) - return compute_api - - def test_get_compute_api(self): - class ComputeClient(object): - __slots__ = ['auth_token', 'management_url'] - - self.mox.StubOutClassWithMocks(OSCompute, 'Compute') - compute_api = OSCompute.Compute(auth_token=TEST_TOKEN, - management_url=TEST_URL) - - compute_api.client = ComputeClient() - - self.mox.StubOutWithMock(api, 'url_for') - # called three times? Looks like a good place for optimization - api.url_for(IsA(http.HttpRequest), 'compute').AndReturn(TEST_URL) - api.url_for(IsA(http.HttpRequest), 'compute').AndReturn(TEST_URL) - api.url_for(IsA(http.HttpRequest), 'compute').AndReturn(TEST_URL) - - self.mox.ReplayAll() - - compute_api = api.compute_api(self.request) - - self.assertIsNotNone(compute_api) - self.assertEqual(compute_api.client.auth_token, TEST_TOKEN) - self.assertEqual(compute_api.client.management_url, TEST_URL) - - self.mox.VerifyAll() - - def test_flavor_get(self): - FLAVOR_ID = 6 - - novaclient = self.stub_novaclient() - - novaclient.flavors = self.mox.CreateMockAnything() - novaclient.flavors.get(FLAVOR_ID).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.flavor_get(self.request, FLAVOR_ID) - self.assertIsInstance(ret_val, api.Flavor) - self.assertEqual(ret_val._apiresource, TEST_RETURN) - - self.mox.VerifyAll() - - def test_server_delete(self): - INSTANCE = 'anInstance' - - compute_api = self.stub_compute_api() - - compute_api.servers = self.mox.CreateMockAnything() - compute_api.servers.delete(INSTANCE).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.server_delete(self.request, INSTANCE) - - self.assertIsNone(ret_val) - - self.mox.VerifyAll() - - def test_server_reboot(self): - INSTANCE_ID = '2' - HARDNESS = 'diamond' - - self.mox.StubOutWithMock(api, 'server_get') - - server = self.mox.CreateMock(OSCompute.Server) - server.reboot(OSCompute.servers.REBOOT_HARD).AndReturn(TEST_RETURN) - api.server_get(IsA(http.HttpRequest), INSTANCE_ID).AndReturn(server) - - server = self.mox.CreateMock(OSCompute.Server) - server.reboot(HARDNESS).AndReturn(TEST_RETURN) - api.server_get(IsA(http.HttpRequest), INSTANCE_ID).AndReturn(server) - - self.mox.ReplayAll() - - ret_val = api.server_reboot(self.request, INSTANCE_ID) - self.assertIsNone(ret_val) - - ret_val = api.server_reboot(self.request, INSTANCE_ID, - hardness=HARDNESS) - self.assertIsNone(ret_val) - - self.mox.VerifyAll() - - def test_server_create(self): - NAME = 'server' - IMAGE = 'anImage' - FLAVOR = 'cherry' - USER_DATA = {'nuts': 'berries'} - KEY = 'user' - SECGROUP = self.mox.CreateMock(api.SecurityGroup) - - server = self.mox.CreateMock(OSCompute.Server) - novaclient = self.stub_novaclient() - novaclient.servers = self.mox.CreateMockAnything() - novaclient.servers.create(NAME, IMAGE, FLAVOR, userdata=USER_DATA, - security_groups=[SECGROUP], key_name=KEY)\ - .AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.server_create(self.request, NAME, IMAGE, FLAVOR, - KEY, USER_DATA, [SECGROUP]) - - self.assertIsInstance(ret_val, api.Server) - self.assertEqual(ret_val._apiresource, TEST_RETURN) - - self.mox.VerifyAll() - - -class ExtrasApiTests(APITestCase): - - def stub_extras_api(self, count=1): - self.mox.StubOutWithMock(api, 'extras_api') - extras_api = self.mox.CreateMock(OSExtras.Extras) - for i in range(count): - api.extras_api(IsA(http.HttpRequest)).AndReturn(extras_api) - return extras_api - - def test_get_extras_api(self): - self.mox.StubOutClassWithMocks(OSExtras, 'Extras') - OSExtras.Extras(auth_token=TEST_TOKEN, management_url=TEST_URL) - - self.mox.StubOutWithMock(api, 'url_for') - api.url_for(IsA(http.HttpRequest), 'compute').AndReturn(TEST_URL) - api.url_for(IsA(http.HttpRequest), 'compute').AndReturn(TEST_URL) - - self.mox.ReplayAll() - - self.assertIsNotNone(api.extras_api(self.request)) - - self.mox.VerifyAll() - - def test_console_create(self): - extras_api = self.stub_extras_api(count=2) - extras_api.consoles = self.mox.CreateMockAnything() - extras_api.consoles.create( - TEST_INSTANCE_ID, TEST_CONSOLE_KIND).AndReturn(TEST_RETURN) - extras_api.consoles.create( - TEST_INSTANCE_ID, 'text').AndReturn(TEST_RETURN + '2') - - self.mox.ReplayAll() - - ret_val = api.console_create(self.request, - TEST_INSTANCE_ID, - TEST_CONSOLE_KIND) - self.assertIsInstance(ret_val, api.Console) - self.assertEqual(ret_val._apiresource, TEST_RETURN) - - ret_val = api.console_create(self.request, TEST_INSTANCE_ID) - self.assertIsInstance(ret_val, api.Console) - self.assertEqual(ret_val._apiresource, TEST_RETURN + '2') - - self.mox.VerifyAll() - - def test_flavor_list(self): - flavors = (TEST_RETURN, TEST_RETURN + '2') - novaclient = self.stub_novaclient() - novaclient.flavors = self.mox.CreateMockAnything() - novaclient.flavors.list().AndReturn(flavors) - - self.mox.ReplayAll() - - ret_val = api.flavor_list(self.request) - - self.assertEqual(len(ret_val), len(flavors)) - for flavor in ret_val: - self.assertIsInstance(flavor, api.Flavor) - self.assertIn(flavor._apiresource, flavors) - - self.mox.VerifyAll() - - def test_server_list(self): - servers = (TEST_RETURN, TEST_RETURN + '2') - - extras_api = self.stub_extras_api() - - extras_api.servers = self.mox.CreateMockAnything() - extras_api.servers.list().AndReturn(servers) - - self.mox.ReplayAll() - - ret_val = api.server_list(self.request) - - self.assertEqual(len(ret_val), len(servers)) - for server in ret_val: - self.assertIsInstance(server, api.Server) - self.assertIn(server._apiresource, servers) - - self.mox.VerifyAll() - - def test_usage_get(self): - extras_api = self.stub_extras_api() - - extras_api.usage = self.mox.CreateMockAnything() - extras_api.usage.get(TEST_TENANT_ID, 'start', - 'end').AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.usage_get(self.request, TEST_TENANT_ID, 'start', 'end') - - self.assertIsInstance(ret_val, api.Usage) - self.assertEqual(ret_val._apiresource, TEST_RETURN) - - self.mox.VerifyAll() - - def test_usage_list(self): - usages = (TEST_RETURN, TEST_RETURN + '2') - - extras_api = self.stub_extras_api() - - extras_api.usage = self.mox.CreateMockAnything() - extras_api.usage.list('start', 'end').AndReturn(usages) - - self.mox.ReplayAll() - - ret_val = api.usage_list(self.request, 'start', 'end') - - self.assertEqual(len(ret_val), len(usages)) - for usage in ret_val: - self.assertIsInstance(usage, api.Usage) - self.assertIn(usage._apiresource, usages) - - self.mox.VerifyAll() - - def test_server_get(self): - INSTANCE_ID = '2' - - extras_api = self.stub_extras_api() - extras_api.servers = self.mox.CreateMockAnything() - extras_api.servers.get(INSTANCE_ID).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.server_get(self.request, INSTANCE_ID) - - self.assertIsInstance(ret_val, api.Server) - self.assertEqual(ret_val._apiresource, TEST_RETURN) - - self.mox.VerifyAll() - - -class VolumeTests(APITestCase): - def setUp(self): - super(VolumeTests, self).setUp() - volume = api.Volume(APIResource.get_instance()) - volume.id = 1 - volume.displayName = "displayName" - volume.attachments = [{"device": "/dev/vdb", - "serverId": 1, - "id": 1, - "volumeId": 1}] - self.volume = volume - self.volumes = [volume, ] - - self.novaclient = self.stub_novaclient() - self.novaclient.volumes = self.mox.CreateMockAnything() - - def test_volume_list(self): - self.novaclient.volumes.list().AndReturn(self.volumes) - self.mox.ReplayAll() - - volumes = api.volume_list(self.request) - - self.assertIsInstance(volumes[0], api.Volume) - self.mox.VerifyAll() - - def test_volume_get(self): - self.novaclient.volumes.get(IsA(int)).AndReturn(self.volume) - self.mox.ReplayAll() - - volume = api.volume_get(self.request, 1) - - self.assertIsInstance(volume, api.Volume) - self.mox.VerifyAll() - - def test_volume_instance_list(self): - self.novaclient.volumes.get_server_volumes(IsA(int)).AndReturn( - self.volume.attachments) - self.mox.ReplayAll() - - attachments = api.volume_instance_list(self.request, 1) - - self.assertEqual(attachments, self.volume.attachments) - self.mox.VerifyAll() - - def test_volume_create(self): - self.novaclient.volumes.create(IsA(int), IsA(str), IsA(str)).AndReturn( - self.volume) - self.mox.ReplayAll() - - new_volume = api.volume_create(self.request, - 10, - "new volume", - "new description") - - self.assertIsInstance(new_volume, api.Volume) - self.mox.VerifyAll() - - def test_volume_delete(self): - self.novaclient.volumes.delete(IsA(int)) - self.mox.ReplayAll() - - ret_val = api.volume_delete(self.request, 1) - - self.assertIsNone(ret_val) - self.mox.VerifyAll() - - def test_volume_attach(self): - self.novaclient.volumes.create_server_volume( - IsA(int), IsA(int), IsA(str)) - self.mox.ReplayAll() - - ret_val = api.volume_attach(self.request, 1, 1, "/dev/vdb") - - self.assertIsNone(ret_val) - self.mox.VerifyAll() - - def test_volume_detach(self): - self.novaclient.volumes.delete_server_volume(IsA(int), IsA(int)) - self.mox.ReplayAll() - - ret_val = api.volume_detach(self.request, 1, 1) - - self.assertIsNone(ret_val) - self.mox.VerifyAll() - - -class APIExtensionTests(APITestCase): - - def setUp(self): - super(APIExtensionTests, self).setUp() - keypair = api.KeyPair(APIResource.get_instance()) - keypair.id = 1 - keypair.name = TEST_RETURN - - self.keypair = keypair - self.keypairs = [keypair, ] - - floating_ip = api.FloatingIp(APIResource.get_instance()) - floating_ip.id = 1 - floating_ip.fixed_ip = '10.0.0.4' - floating_ip.instance_id = 1 - floating_ip.ip = '58.58.58.58' - - self.floating_ip = floating_ip - self.floating_ips = [floating_ip, ] - - server = api.Server(APIResource.get_instance(), self.request) - server.id = 1 - - self.server = server - self.servers = [server, ] - - def test_server_snapshot_create(self): - novaclient = self.stub_novaclient() - - novaclient.servers = self.mox.CreateMockAnything() - novaclient.servers.create_image(IsA(int), IsA(str)).\ - AndReturn(self.server) - self.mox.ReplayAll() - - server = api.snapshot_create(self.request, 1, 'test-snapshot') - - self.assertIsInstance(server, api.Server) - self.mox.VerifyAll() - - def test_tenant_floating_ip_list(self): - novaclient = self.stub_novaclient() - - novaclient.floating_ips = self.mox.CreateMockAnything() - novaclient.floating_ips.list().AndReturn(self.floating_ips) - self.mox.ReplayAll() - - floating_ips = api.tenant_floating_ip_list(self.request) - - self.assertEqual(len(floating_ips), len(self.floating_ips)) - self.assertIsInstance(floating_ips[0], api.FloatingIp) - self.mox.VerifyAll() - - def test_tenant_floating_ip_get(self): - novaclient = self.stub_novaclient() - - novaclient.floating_ips = self.mox.CreateMockAnything() - novaclient.floating_ips.get(IsA(int)).AndReturn(self.floating_ip) - self.mox.ReplayAll() - - floating_ip = api.tenant_floating_ip_get(self.request, 1) - - self.assertIsInstance(floating_ip, api.FloatingIp) - self.mox.VerifyAll() - - def test_tenant_floating_ip_allocate(self): - novaclient = self.stub_novaclient() - - novaclient.floating_ips = self.mox.CreateMockAnything() - novaclient.floating_ips.create().AndReturn(self.floating_ip) - self.mox.ReplayAll() - - floating_ip = api.tenant_floating_ip_allocate(self.request) - - self.assertIsInstance(floating_ip, api.FloatingIp) - self.mox.VerifyAll() - - def test_tenant_floating_ip_release(self): - novaclient = self.stub_novaclient() - - novaclient.floating_ips = self.mox.CreateMockAnything() - novaclient.floating_ips.delete(1).AndReturn(self.floating_ip) - self.mox.ReplayAll() - - floating_ip = api.tenant_floating_ip_release(self.request, 1) - - self.assertIsInstance(floating_ip, api.FloatingIp) - self.mox.VerifyAll() - - def test_server_remove_floating_ip(self): - novaclient = self.stub_novaclient() - - novaclient.servers = self.mox.CreateMockAnything() - novaclient.floating_ips = self.mox.CreateMockAnything() - - novaclient.servers.get(IsA(int)).AndReturn(self.server) - novaclient.floating_ips.get(IsA(int)).AndReturn(self.floating_ip) - novaclient.servers.remove_floating_ip(IsA(self.server.__class__), - IsA(self.floating_ip.__class__)) \ - .AndReturn(self.server) - self.mox.ReplayAll() - - server = api.server_remove_floating_ip(self.request, 1, 1) - - self.assertIsInstance(server, api.Server) - self.mox.VerifyAll() - - def test_server_add_floating_ip(self): - novaclient = self.stub_novaclient() - - novaclient.floating_ips = self.mox.CreateMockAnything() - novaclient.servers = self.mox.CreateMockAnything() - - novaclient.servers.get(IsA(int)).AndReturn(self.server) - novaclient.floating_ips.get(IsA(int)).AndReturn(self.floating_ip) - novaclient.servers.add_floating_ip(IsA(self.server.__class__), - IsA(self.floating_ip.__class__)) \ - .AndReturn(self.server) - self.mox.ReplayAll() - - server = api.server_add_floating_ip(self.request, 1, 1) - - self.assertIsInstance(server, api.Server) - self.mox.VerifyAll() - - def test_keypair_create(self): - novaclient = self.stub_novaclient() - - novaclient.keypairs = self.mox.CreateMockAnything() - novaclient.keypairs.create(IsA(str)).AndReturn(self.keypair) - self.mox.ReplayAll() - - ret_val = api.keypair_create(self.request, TEST_RETURN) - self.assertIsInstance(ret_val, api.KeyPair) - self.assertEqual(ret_val.name, self.keypair.name) - - self.mox.VerifyAll() - - def test_keypair_import(self): - novaclient = self.stub_novaclient() - - novaclient.keypairs = self.mox.CreateMockAnything() - novaclient.keypairs.create(IsA(str), IsA(str)).AndReturn(self.keypair) - self.mox.ReplayAll() - - ret_val = api.keypair_import(self.request, TEST_RETURN, TEST_RETURN) - self.assertIsInstance(ret_val, api.KeyPair) - self.assertEqual(ret_val.name, self.keypair.name) - - self.mox.VerifyAll() - - def test_keypair_delete(self): - novaclient = self.stub_novaclient() - - novaclient.keypairs = self.mox.CreateMockAnything() - novaclient.keypairs.delete(IsA(int)) - - self.mox.ReplayAll() - - ret_val = api.keypair_delete(self.request, self.keypair.id) - self.assertIsNone(ret_val) - - self.mox.VerifyAll() - - def test_keypair_list(self): - novaclient = self.stub_novaclient() - - novaclient.keypairs = self.mox.CreateMockAnything() - novaclient.keypairs.list().AndReturn(self.keypairs) - - self.mox.ReplayAll() - - ret_val = api.keypair_list(self.request) - - self.assertEqual(len(ret_val), len(self.keypairs)) - for keypair in ret_val: - self.assertIsInstance(keypair, api.KeyPair) - - self.mox.VerifyAll() - - -class GlanceApiTests(APITestCase): - def stub_glance_api(self, count=1): - self.mox.StubOutWithMock(api, 'glance_api') - glance_api = self.mox.CreateMock(glance_client.Client) - glance_api.token = TEST_TOKEN - for i in range(count): - api.glance_api(IsA(http.HttpRequest)).AndReturn(glance_api) - return glance_api - - def test_get_glance_api(self): - self.mox.StubOutClassWithMocks(glance_client, 'Client') - client_instance = glance_client.Client(TEST_HOSTNAME, TEST_PORT, - auth_tok=TEST_TOKEN) - # Normally ``auth_tok`` is set in ``Client.__init__``, but mox doesn't - # duplicate that behavior so we set it manually. - client_instance.auth_tok = TEST_TOKEN - - self.mox.StubOutWithMock(api, 'url_for') - api.url_for(IsA(http.HttpRequest), 'image').AndReturn(TEST_URL) - - self.mox.ReplayAll() - - ret_val = api.glance_api(self.request) - self.assertIsNotNone(ret_val) - self.assertEqual(ret_val.auth_tok, TEST_TOKEN) - - self.mox.VerifyAll() - - def test_image_create(self): - IMAGE_FILE = 'someData' - IMAGE_META = {'metadata': 'foo'} - - glance_api = self.stub_glance_api() - glance_api.add_image(IMAGE_META, IMAGE_FILE).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.image_create(self.request, IMAGE_META, IMAGE_FILE) - - self.assertIsInstance(ret_val, api.Image) - self.assertEqual(ret_val._apidict, TEST_RETURN) - - self.mox.VerifyAll() - - def test_image_delete(self): - IMAGE_ID = '1' - - glance_api = self.stub_glance_api() - glance_api.delete_image(IMAGE_ID).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.image_delete(self.request, IMAGE_ID) - - self.assertEqual(ret_val, TEST_RETURN) - - self.mox.VerifyAll() - - def test_image_get(self): - IMAGE_ID = '1' - - glance_api = self.stub_glance_api() - glance_api.get_image(IMAGE_ID).AndReturn([TEST_RETURN]) - - self.mox.ReplayAll() - - ret_val = api.image_get(self.request, IMAGE_ID) - - self.assertIsInstance(ret_val, api.Image) - self.assertEqual(ret_val._apidict, TEST_RETURN) - - def test_image_list_detailed(self): - images = (TEST_RETURN, TEST_RETURN + '2') - glance_api = self.stub_glance_api() - glance_api.get_images_detailed().AndReturn(images) - - self.mox.ReplayAll() - - ret_val = api.image_list_detailed(self.request) - - self.assertEqual(len(ret_val), len(images)) - for image in ret_val: - self.assertIsInstance(image, api.Image) - self.assertIn(image._apidict, images) - - self.mox.VerifyAll() - - def test_image_update(self): - IMAGE_ID = '1' - IMAGE_META = {'metadata': 'foobar'} - - glance_api = self.stub_glance_api(count=2) - glance_api.update_image(IMAGE_ID, image_meta={}).AndReturn(TEST_RETURN) - glance_api.update_image(IMAGE_ID, - image_meta=IMAGE_META).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.image_update(self.request, IMAGE_ID) - - self.assertIsInstance(ret_val, api.Image) - self.assertEqual(ret_val._apidict, TEST_RETURN) - - ret_val = api.image_update(self.request, - IMAGE_ID, - image_meta=IMAGE_META) - - self.assertIsInstance(ret_val, api.Image) - self.assertEqual(ret_val._apidict, TEST_RETURN) - - self.mox.VerifyAll() - - -class SwiftApiTests(APITestCase): - def setUp(self): - self.mox = mox.Mox() - - self.request = http.HttpRequest() - self.request.session = dict() - self.request.session['token'] = TEST_TOKEN - - def tearDown(self): - self.mox.UnsetStubs() - - def stub_swift_api(self, count=1): - self.mox.StubOutWithMock(api, 'swift_api') - swift_api = self.mox.CreateMock(cloudfiles.connection.Connection) - for i in range(count): - api.swift_api(IsA(http.HttpRequest)).AndReturn(swift_api) - return swift_api - - def test_swift_get_containers(self): - containers = (TEST_RETURN, TEST_RETURN + '2') - - swift_api = self.stub_swift_api() - - swift_api.get_all_containers(limit=10000, - marker=None).AndReturn(containers) - - self.mox.ReplayAll() - - ret_val = api.swift_get_containers(self.request) - - self.assertEqual(len(ret_val), len(containers)) - for container in ret_val: - self.assertIsInstance(container, api.Container) - self.assertIn(container._apiresource, containers) - - self.mox.VerifyAll() - - def test_swift_create_container(self): - NAME = 'containerName' - - swift_api = self.stub_swift_api() - self.mox.StubOutWithMock(api, 'swift_container_exists') - - api.swift_container_exists(self.request, - NAME).AndReturn(False) - swift_api.create_container(NAME).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.swift_create_container(self.request, NAME) - - self.assertIsInstance(ret_val, api.Container) - self.assertEqual(ret_val._apiresource, TEST_RETURN) - - self.mox.VerifyAll() - - def test_swift_delete_container(self): - NAME = 'containerName' - - swift_api = self.stub_swift_api() - - swift_api.delete_container(NAME).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.swift_delete_container(self.request, NAME) - - self.assertIsNone(ret_val) - - self.mox.VerifyAll() - - def test_swift_get_objects(self): - NAME = 'containerName' - - swift_objects = (TEST_RETURN, TEST_RETURN + '2') - container = self.mox.CreateMock(cloudfiles.container.Container) - container.get_objects(limit=10000, - marker=None, - prefix=None).AndReturn(swift_objects) - - swift_api = self.stub_swift_api() - - swift_api.get_container(NAME).AndReturn(container) - - self.mox.ReplayAll() - - ret_val = api.swift_get_objects(self.request, NAME) - - self.assertEqual(len(ret_val), len(swift_objects)) - for swift_object in ret_val: - self.assertIsInstance(swift_object, api.SwiftObject) - self.assertIn(swift_object._apiresource, swift_objects) - - self.mox.VerifyAll() - - def test_swift_get_objects_with_prefix(self): - NAME = 'containerName' - PREFIX = 'prefacedWith' - - swift_objects = (TEST_RETURN, TEST_RETURN + '2') - container = self.mox.CreateMock(cloudfiles.container.Container) - container.get_objects(limit=10000, - marker=None, - prefix=PREFIX).AndReturn(swift_objects) - - swift_api = self.stub_swift_api() - - swift_api.get_container(NAME).AndReturn(container) - - self.mox.ReplayAll() - - ret_val = api.swift_get_objects(self.request, - NAME, - prefix=PREFIX) - - self.assertEqual(len(ret_val), len(swift_objects)) - for swift_object in ret_val: - self.assertIsInstance(swift_object, api.SwiftObject) - self.assertIn(swift_object._apiresource, swift_objects) - - self.mox.VerifyAll() - - def test_swift_upload_object(self): - CONTAINER_NAME = 'containerName' - OBJECT_NAME = 'objectName' - OBJECT_DATA = 'someData' - - swift_api = self.stub_swift_api() - container = self.mox.CreateMock(cloudfiles.container.Container) - swift_object = self.mox.CreateMock(cloudfiles.storage_object.Object) - - swift_api.get_container(CONTAINER_NAME).AndReturn(container) - container.create_object(OBJECT_NAME).AndReturn(swift_object) - swift_object.write(OBJECT_DATA).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.swift_upload_object(self.request, - CONTAINER_NAME, - OBJECT_NAME, - OBJECT_DATA) - - self.assertIsNone(ret_val) - - self.mox.VerifyAll() - - def test_swift_delete_object(self): - CONTAINER_NAME = 'containerName' - OBJECT_NAME = 'objectName' - - swift_api = self.stub_swift_api() - container = self.mox.CreateMock(cloudfiles.container.Container) - - swift_api.get_container(CONTAINER_NAME).AndReturn(container) - container.delete_object(OBJECT_NAME).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - ret_val = api.swift_delete_object(self.request, - CONTAINER_NAME, - OBJECT_NAME) - - self.assertIsNone(ret_val) - - self.mox.VerifyAll() - - def test_swift_get_object_data(self): - CONTAINER_NAME = 'containerName' - OBJECT_NAME = 'objectName' - OBJECT_DATA = 'objectData' - - swift_api = self.stub_swift_api() - container = self.mox.CreateMock(cloudfiles.container.Container) - swift_object = self.mox.CreateMock(cloudfiles.storage_object.Object) - - swift_api.get_container(CONTAINER_NAME).AndReturn(container) - container.get_object(OBJECT_NAME).AndReturn(swift_object) - swift_object.stream().AndReturn(OBJECT_DATA) - - self.mox.ReplayAll() - - ret_val = api.swift_get_object_data(self.request, - CONTAINER_NAME, - OBJECT_NAME) - - self.assertEqual(ret_val, OBJECT_DATA) - - self.mox.VerifyAll() - - def test_swift_object_exists(self): - CONTAINER_NAME = 'containerName' - OBJECT_NAME = 'objectName' - - swift_api = self.stub_swift_api() - container = self.mox.CreateMock(cloudfiles.container.Container) - swift_object = self.mox.CreateMock(cloudfiles.Object) - - swift_api.get_container(CONTAINER_NAME).AndReturn(container) - container.get_object(OBJECT_NAME).AndReturn(swift_object) - - self.mox.ReplayAll() - - ret_val = api.swift_object_exists(self.request, - CONTAINER_NAME, - OBJECT_NAME) - self.assertTrue(ret_val) - - self.mox.VerifyAll() - - def test_swift_copy_object(self): - CONTAINER_NAME = 'containerName' - OBJECT_NAME = 'objectName' - - swift_api = self.stub_swift_api() - container = self.mox.CreateMock(cloudfiles.container.Container) - self.mox.StubOutWithMock(api, 'swift_object_exists') - - swift_object = self.mox.CreateMock(cloudfiles.Object) - - swift_api.get_container(CONTAINER_NAME).AndReturn(container) - api.swift_object_exists(self.request, - CONTAINER_NAME, - OBJECT_NAME).AndReturn(False) - - container.get_object(OBJECT_NAME).AndReturn(swift_object) - swift_object.copy_to(CONTAINER_NAME, OBJECT_NAME) - - self.mox.ReplayAll() - - ret_val = api.swift_copy_object(self.request, CONTAINER_NAME, - OBJECT_NAME, CONTAINER_NAME, - OBJECT_NAME) - - self.assertIsNone(ret_val) - self.mox.VerifyAll() diff --git a/django-openstack/django_openstack/tests/broken/README b/django-openstack/django_openstack/tests/broken/README deleted file mode 100644 index a2eec652..00000000 --- a/django-openstack/django_openstack/tests/broken/README +++ /dev/null @@ -1,2 +0,0 @@ -Intentionally not a python module so that test runner won't find -these broken tests diff --git a/django-openstack/django_openstack/tests/broken/base.py b/django-openstack/django_openstack/tests/broken/base.py deleted file mode 100644 index 9947931f..00000000 --- a/django-openstack/django_openstack/tests/broken/base.py +++ /dev/null @@ -1,81 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# 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. - -""" -Base classes for view based unit tests. -""" - -import mox -import nova_adminclient as adminclient - -from django import test -from django.conf import settings -from django.contrib.auth import models as auth_models - - -TEST_PROJECT = 'test' -TEST_USER = 'test' -TEST_REGION = 'test' - - -class BaseViewTests(test.TestCase): - def setUp(self): - self.mox = mox.Mox() - - def tearDown(self): - self.mox.UnsetStubs() - - def assertRedirectsNoFollow(self, response, expected_url): - self.assertEqual(response._headers['location'], - ('Location', settings.TESTSERVER + expected_url)) - self.assertEqual(response.status_code, 302) - - def authenticateTestUser(self): - user = auth_models.User.objects.create_user(TEST_USER, - 'test@test.com', - password='test') - login = self.client.login(username=TEST_USER, password='test') - self.failUnless(login, 'Unable to login') - return user - - -class BaseProjectViewTests(BaseViewTests): - def setUp(self): - super(BaseProjectViewTests, self).setUp() - - project = adminclient.ProjectInfo() - project.projectname = TEST_PROJECT - project.projectManagerId = TEST_USER - - self.user = self.authenticateTestUser() - self.region = adminclient.RegionInfo(name=TEST_REGION, - endpoint='http://test:8773/') - - def create_key_pair_choices(self, key_names): - return [(k, k) for k in key_names] - - def create_instance_type_choices(self): - return [('m1.medium', 'm1.medium'), - ('m1.large', 'm1.large')] - - def create_instance_choices(self, instance_ids): - return [(id, id) for id in instance_ids] - - def create_available_volume_choices(self, volumes): - return [(v.id, '%s %s - %dGB' % (v.id, v.displayName, v.size)) \ - for v in volumes] diff --git a/django-openstack/django_openstack/tests/broken/credential_tests.py b/django-openstack/django_openstack/tests/broken/credential_tests.py deleted file mode 100644 index 23ac9777..00000000 --- a/django-openstack/django_openstack/tests/broken/credential_tests.py +++ /dev/null @@ -1,70 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# 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. - -""" -Unit tests for credential views. -""" - -from django.conf import settings -from django.core.urlresolvers import reverse -from django_openstack import models -from django_openstack.nova.tests.base import BaseViewTests - - -class CredentialViewTests(BaseViewTests): - def test_download_expired_credentials(self): - auth_token = 'expired' - self.mox.StubOutWithMock(models.CredentialsAuthorization, - 'get_by_token') - models.CredentialsAuthorization.get_by_token(auth_token) \ - .AndReturn(None) - self.mox.ReplayAll() - - res = self.client.get(reverse('nova_credentials_authorize', - args=[auth_token])) - self.assertTemplateUsed(res, - 'django_openstack/nova/credentials/expired.html') - - self.mox.VerifyAll() - - def test_download_good_credentials(self): - auth_token = 'good' - - creds = models.CredentialsAuthorization() - creds.username = 'test' - creds.project = 'test' - creds.auth_token = auth_token - - self.mox.StubOutWithMock(models.CredentialsAuthorization, - 'get_by_token') - self.mox.StubOutWithMock(creds, 'get_zip') - models.CredentialsAuthorization.get_by_token(auth_token) \ - .AndReturn(creds) - creds.get_zip().AndReturn('zip') - - self.mox.ReplayAll() - - res = self.client.get(reverse('nova_credentials_authorize', - args=[auth_token])) - self.assertEqual(res.status_code, 200) - self.assertEqual(res['Content-Disposition'], - 'attachment; filename=%s-test-test-x509.zip' % - settings.SITE_NAME) - self.assertContains(res, 'zip') - - self.mox.VerifyAll() diff --git a/django-openstack/django_openstack/tests/broken/image_tests.py b/django-openstack/django_openstack/tests/broken/image_tests.py deleted file mode 100644 index a2b6769d..00000000 --- a/django-openstack/django_openstack/tests/broken/image_tests.py +++ /dev/null @@ -1,237 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# 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. - -""" -Unit tests for image views. -""" - -import boto.ec2.image -import boto.ec2.instance -import mox - -from django.core.urlresolvers import reverse -from django_openstack.nova import forms -from django_openstack.nova import shortcuts -from django_openstack.nova.tests.base import (BaseProjectViewTests, - TEST_PROJECT) - - -TEST_IMAGE_ID = 'ami_test' -TEST_INSTANCE_ID = 'i-abcdefg' -TEST_KEY = 'foo' - - -class ImageViewTests(BaseProjectViewTests): - def setUp(self): - self.ami = boto.ec2.image.Image() - self.ami.id = TEST_IMAGE_ID - setattr(self.ami, 'displayName', TEST_IMAGE_ID) - setattr(self.ami, 'description', TEST_IMAGE_ID) - super(ImageViewTests, self).setUp() - - def test_index(self): - self.mox.StubOutWithMock(self.project, 'get_images') - self.mox.StubOutWithMock(forms, 'get_key_pair_choices') - self.mox.StubOutWithMock(forms, 'get_instance_type_choices') - - self.project.get_images().AndReturn([]) - forms.get_key_pair_choices(self.project).AndReturn([]) - forms.get_instance_type_choices().AndReturn([]) - - self.mox.ReplayAll() - - res = self.client.get(reverse('nova_images', args=[TEST_PROJECT])) - self.assertEqual(res.status_code, 200) - self.assertTemplateUsed(res, 'django_openstack/nova/images/index.html') - self.assertEqual(len(res.context['image_lists']), 3) - - self.mox.VerifyAll() - - def test_launch_form(self): - self.mox.StubOutWithMock(self.project, 'get_image') - self.mox.StubOutWithMock(forms, 'get_key_pair_choices') - self.mox.StubOutWithMock(forms, 'get_instance_type_choices') - - self.project.get_image(TEST_IMAGE_ID).AndReturn(self.ami) - forms.get_key_pair_choices(self.project).AndReturn([]) - forms.get_instance_type_choices().AndReturn([]) - - self.mox.ReplayAll() - - args = [TEST_PROJECT, TEST_IMAGE_ID] - res = self.client.get(reverse('nova_images_launch', args=args)) - self.assertEqual(res.status_code, 200) - self.assertTemplateUsed(res, - 'django_openstack/nova/images/launch.html') - self.assertEqual(res.context['ami'].id, TEST_IMAGE_ID) - - self.mox.VerifyAll() - - def test_launch(self): - instance = boto.ec2.instance.Instance() - instance.id = TEST_INSTANCE_ID - instance.image_id = TEST_IMAGE_ID - reservation = boto.ec2.instance.Reservation() - reservation.instances = [instance] - - conn = self.mox.CreateMockAnything() - - self.mox.StubOutWithMock(forms, 'get_key_pair_choices') - self.mox.StubOutWithMock(forms, 'get_instance_type_choices') - self.mox.StubOutWithMock(self.project, 'get_openstack_connection') - - self.project.get_openstack_connection().AndReturn(conn) - - forms.get_key_pair_choices(self.project).AndReturn( - self.create_key_pair_choices([TEST_KEY])) - forms.get_instance_type_choices().AndReturn( - self.create_instance_type_choices()) - - params = {'addressing_type': 'private', - 'UserData': '', 'display_name': u'name', - 'MinCount': '1', 'key_name': TEST_KEY, - 'MaxCount': '1', 'InstanceType': 'm1.medium', - 'ImageId': TEST_IMAGE_ID} - conn.get_object('RunInstances', params, boto.ec2.instance.Reservation, - verb='POST').AndReturn(reservation) - - self.mox.ReplayAll() - - url = reverse('nova_images_launch', args=[TEST_PROJECT, TEST_IMAGE_ID]) - data = {'key_name': TEST_KEY, - 'count': '1', - 'size': 'm1.medium', - 'display_name': 'name', - 'user_data': ''} - res = self.client.post(url, data) - self.assertRedirectsNoFollow(res, reverse('nova_instances', - args=[TEST_PROJECT])) - self.mox.VerifyAll() - - def test_detail(self): - self.mox.StubOutWithMock(self.project, 'get_images') - self.mox.StubOutWithMock(self.project, 'get_image') - self.mox.StubOutWithMock(shortcuts, 'get_user_image_permissions') - self.mox.StubOutWithMock(forms, 'get_key_pair_choices') - self.mox.StubOutWithMock(forms, 'get_instance_type_choices') - - self.project.get_images().AndReturn([self.ami]) - self.project.get_image(TEST_IMAGE_ID).AndReturn(self.ami) - forms.get_key_pair_choices(self.project).AndReturn( - self.create_key_pair_choices([TEST_KEY])) - forms.get_instance_type_choices().AndReturn( - self.create_instance_type_choices()) - - self.mox.ReplayAll() - - res = self.client.get(reverse('nova_images_detail', - args=[TEST_PROJECT, TEST_IMAGE_ID])) - self.assertEqual(res.status_code, 200) - self.assertTemplateUsed(res, 'django_openstack/nova/images/index.html') - self.assertEqual(res.context['ami'].id, TEST_IMAGE_ID) - - self.mox.VerifyAll() - - def test_remove_form(self): - self.mox.StubOutWithMock(self.project, 'get_image') - self.mox.ReplayAll() - - res = self.client.get(reverse('nova_images_remove', - args=[TEST_PROJECT, TEST_IMAGE_ID])) - self.assertRedirectsNoFollow(res, reverse('nova_images', - args=[TEST_PROJECT])) - self.mox.VerifyAll() - - def test_remove(self): - self.mox.StubOutWithMock(self.project, 'deregister_image') - self.project.deregister_image(TEST_IMAGE_ID).AndReturn(True) - self.mox.ReplayAll() - - res = self.client.post(reverse('nova_images_remove', - args=[TEST_PROJECT, TEST_IMAGE_ID])) - self.assertRedirectsNoFollow(res, reverse('nova_images', - args=[TEST_PROJECT])) - - self.mox.VerifyAll() - - def test_make_public(self): - self.mox.StubOutWithMock(self.project, 'get_image') - self.mox.StubOutWithMock(self.project, 'modify_image_attribute') - - self.ami.is_public = False - self.project.get_image(TEST_IMAGE_ID).AndReturn(self.ami) - self.project.modify_image_attribute(TEST_IMAGE_ID, - attribute='launchPermission', - operation='add').AndReturn(True) - self.mox.ReplayAll() - - res = self.client.post(reverse('nova_images_privacy', - args=[TEST_PROJECT, TEST_IMAGE_ID])) - self.assertRedirectsNoFollow(res, reverse('nova_images_detail', - args=[TEST_PROJECT, TEST_IMAGE_ID])) - self.mox.VerifyAll() - - def test_make_private(self): - self.mox.StubOutWithMock(self.project, 'get_image') - self.mox.StubOutWithMock(self.project, 'modify_image_attribute') - - self.ami.is_public = True - self.project.get_image(TEST_IMAGE_ID).AndReturn(self.ami) - self.project.modify_image_attribute(TEST_IMAGE_ID, - attribute='launchPermission', - operation='remove').AndReturn(True) - self.mox.ReplayAll() - - args = [TEST_PROJECT, TEST_IMAGE_ID] - res = self.client.post(reverse('nova_images_privacy', args=args)) - self.assertRedirectsNoFollow(res, reverse('nova_images_detail', - args=args)) - self.mox.VerifyAll() - - def test_update_form(self): - self.mox.StubOutWithMock(self.project, 'get_image') - self.project.get_image(TEST_IMAGE_ID).AndReturn(self.ami) - self.mox.ReplayAll() - - args = [TEST_PROJECT, TEST_IMAGE_ID] - res = self.client.get(reverse('nova_images_update', args=args)) - self.assertEqual(res.status_code, 200) - self.assertTemplateUsed(res, 'django_openstack/nova/images/edit.html') - self.assertEqual(res.context['ami'].id, TEST_IMAGE_ID) - - self.mox.VerifyAll() - - def test_update(self): - self.mox.StubOutWithMock(self.project, 'get_image') - self.mox.StubOutWithMock(self.project, 'update_image') - - self.project.get_image(TEST_IMAGE_ID).AndReturn(self.ami) - self.project.update_image(TEST_IMAGE_ID, 'test', 'test') \ - .AndReturn(True) - - self.mox.ReplayAll() - - args = [TEST_PROJECT, TEST_IMAGE_ID] - data = {'nickname': 'test', - 'description': 'test'} - url = reverse('nova_images_update', args=args) - res = self.client.post(url, data) - expected_url = reverse('nova_images_detail', args=args) - self.assertRedirectsNoFollow(res, expected_url) - - self.mox.VerifyAll() diff --git a/django-openstack/django_openstack/tests/broken/instance_tests.py b/django-openstack/django_openstack/tests/broken/instance_tests.py deleted file mode 100644 index 7f407d42..00000000 --- a/django-openstack/django_openstack/tests/broken/instance_tests.py +++ /dev/null @@ -1,69 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# 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. - -""" -Unit tests for instance views. -""" - -import boto.ec2.instance -import mox - -from django.core.urlresolvers import reverse -from django_openstack.nova.tests.base import (BaseProjectViewTests, - TEST_PROJECT) - - -TEST_INSTANCE_ID = 'i-abcdefgh' - - -class InstanceViewTests(BaseProjectViewTests): - def test_index(self): - self.mox.StubOutWithMock(self.project, 'get_instances') - self.project.get_instances().AndReturn([]) - - self.mox.ReplayAll() - - res = self.client.get(reverse('nova_instances', args=[TEST_PROJECT])) - self.assertEqual(res.status_code, 200) - self.assertTemplateUsed(res, - 'django_openstack/nova/instances/index.html') - self.assertEqual(len(res.context['instances']), 0) - - self.mox.VerifyAll() - - def test_detail(self): - instance = boto.ec2.instance.Instance() - instance.id = TEST_INSTANCE_ID - instance.displayName = instance.id - instance.displayDescription = instance.id - - self.mox.StubOutWithMock(self.project, 'get_instance') - self.project.get_instance(instance.id).AndReturn(instance) - self.mox.StubOutWithMock(self.project, 'get_instances') - self.project.get_instances().AndReturn([instance]) - - self.mox.ReplayAll() - - res = self.client.get(reverse('nova_instances_detail', - args=[TEST_PROJECT, TEST_INSTANCE_ID])) - self.assertEqual(res.status_code, 200) - self.assertTemplateUsed(res, - 'django_openstack/nova/instances/index.html') - self.assertEqual(res.context['selected_instance'].id, instance.id) - - self.mox.VerifyAll() diff --git a/django-openstack/django_openstack/tests/broken/keypair_tests.py b/django-openstack/django_openstack/tests/broken/keypair_tests.py deleted file mode 100644 index 130e2feb..00000000 --- a/django-openstack/django_openstack/tests/broken/keypair_tests.py +++ /dev/null @@ -1,93 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# 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. - -""" -Unit tests for key pair views. -""" - -import boto.ec2.keypair -import mox - -from django.core.urlresolvers import reverse -from django_openstack.nova.tests.base import (BaseProjectViewTests, - TEST_PROJECT) - - -TEST_KEY = 'test_key' - - -class KeyPairViewTests(BaseProjectViewTests): - def test_index(self): - self.mox.StubOutWithMock(self.project, 'get_key_pairs') - self.project.get_key_pairs().AndReturn([]) - - self.mox.ReplayAll() - - response = self.client.get(reverse('nova_keypairs', - args=[TEST_PROJECT])) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, - 'django_openstack/nova/keypairs/index.html') - self.assertEqual(len(response.context['keypairs']), 0) - - self.mox.VerifyAll() - - def test_add_keypair(self): - key = boto.ec2.keypair.KeyPair() - key.name = TEST_KEY - - self.mox.StubOutWithMock(self.project, 'create_key_pair') - self.project.create_key_pair(key.name).AndReturn(key) - self.mox.StubOutWithMock(self.project, 'has_key_pair') - self.project.has_key_pair(key.name).AndReturn(False) - - self.mox.ReplayAll() - - url = reverse('nova_keypairs_add', args=[TEST_PROJECT]) - data = {'js': '0', 'name': key.name} - res = self.client.post(url, data) - self.assertEqual(res.status_code, 200) - self.assertEqual(res['Content-Type'], 'application/binary') - - self.mox.VerifyAll() - - def test_delete_keypair(self): - self.mox.StubOutWithMock(self.project, 'delete_key_pair') - self.project.delete_key_pair(TEST_KEY).AndReturn(None) - - self.mox.ReplayAll() - - data = {'key_name': TEST_KEY} - url = reverse('nova_keypairs_delete', args=[TEST_PROJECT]) - res = self.client.post(url, data) - self.assertRedirectsNoFollow(res, reverse('nova_keypairs', - args=[TEST_PROJECT])) - - self.mox.VerifyAll() - - def test_download_keypair(self): - material = 'abcdefgh' - session = self.client.session - session['key.%s' % TEST_KEY] = material - session.save() - - res = self.client.get(reverse('nova_keypairs_download', - args=['test', TEST_KEY])) - self.assertEqual(res.status_code, 200) - self.assertEqual(res['Content-Type'], 'application/binary') - self.assertContains(res, material) diff --git a/django-openstack/django_openstack/tests/broken/region_tests.py b/django-openstack/django_openstack/tests/broken/region_tests.py deleted file mode 100644 index bf49cc57..00000000 --- a/django-openstack/django_openstack/tests/broken/region_tests.py +++ /dev/null @@ -1,41 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# 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. - -""" -Unit tests for region views. -""" - -from django.core.urlresolvers import reverse -from django_openstack.nova.tests.base import BaseViewTests - - -TEST_REGION = 'one' - - -class RegionViewTests(BaseViewTests): - def test_change(self): - self.authenticateTestUser() - session = self.client.session - session['region'] = 'two' - session.save() - - data = {'redirect_url': '/', - 'region': TEST_REGION} - res = self.client.post(reverse('region_change'), data) - self.assertEqual(self.client.session['region'], TEST_REGION) - self.assertRedirectsNoFollow(res, '/') diff --git a/django-openstack/django_openstack/tests/broken/test_models.py b/django-openstack/django_openstack/tests/broken/test_models.py deleted file mode 100644 index 8fed2501..00000000 --- a/django-openstack/django_openstack/tests/broken/test_models.py +++ /dev/null @@ -1,187 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# 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. - -import datetime -import hashlib -import mox -import random - -from django import test -from django.conf import settings -from django.db.models.signals import post_save -from django_openstack import models as nova_models -from django_openstack import utils -from django_openstack.core import connection -from nova_adminclient import NovaAdminClient - - -TEST_USER = 'testUser' -TEST_PROJECT = 'testProject' -TEST_AUTH_TOKEN = hashlib.sha1('').hexdigest() -TEST_AUTH_DATE = utils.utcnow() -TEST_BAD_AUTH_TOKEN = 'badToken' - -HOUR = datetime.timedelta(seconds=3600) -AUTH_EXPIRATION_LENGTH = \ - datetime.timedelta(days=int(settings.CREDENTIAL_AUTHORIZATION_DAYS)) - - -class CredentialsAuthorizationTests(test.TestCase): - @classmethod - def setUpClass(cls): - # these post_save methods interact with external resources, shut them - # down to test credentials - post_save.disconnect(sender=nova_models.CredentialsAuthorization, - dispatch_uid='django_openstack.CredentialsAuthorization.post_save') - post_save.disconnect(sender=nova_models.CredentialsAuthorization, - dispatch_uid='django_openstack.User.post_save') - - def setUp(self): - test_cred = nova_models.CredentialsAuthorization() - test_cred.username = TEST_USER - test_cred.project = TEST_PROJECT - test_cred.auth_date = TEST_AUTH_DATE - test_cred.auth_token = TEST_AUTH_TOKEN - test_cred.save() - - badTestCred = nova_models.CredentialsAuthorization() - badTestCred.username = TEST_USER - badTestCred.project = TEST_PROJECT - badTestCred.auth_date = TEST_AUTH_DATE - badTestCred.auth_token = TEST_BAD_AUTH_TOKEN - badTestCred.save() - - self.mox = mox.Mox() - - def tearDown(self): - self.mox.UnsetStubs() - - def test_get_by_token(self): - TEST_MISSING_AUTH_TOKEN = hashlib.sha1('notAToken').hexdigest() - - # Token not a sha1, but exists in system - cred = nova_models.CredentialsAuthorization.get_by_token( - TEST_BAD_AUTH_TOKEN) - self.assertTrue(cred is None) - - # Token doesn't exist - cred = nova_models.CredentialsAuthorization.get_by_token( - TEST_MISSING_AUTH_TOKEN) - self.assertTrue(cred is None) - - # Good token - cred = nova_models.CredentialsAuthorization.get_by_token( - TEST_AUTH_TOKEN) - self.assertTrue(cred is not None) - - # Expire the token - cred.auth_date = utils.utcnow() - AUTH_EXPIRATION_LENGTH \ - - HOUR - cred.save() - - # Expired token - cred = nova_models.CredentialsAuthorization.get_by_token( - TEST_AUTH_TOKEN) - self.assertTrue(cred is None) - - def test_authorize(self): - TEST_USER2 = TEST_USER + '2' - TEST_AUTH_TOKEN_2 = hashlib.sha1('token2').hexdigest() - - cred_class = nova_models.CredentialsAuthorization - self.mox.StubOutWithMock(cred_class, 'create_auth_token') - cred_class.create_auth_token(TEST_USER2).AndReturn( - TEST_AUTH_TOKEN_2) - - self.mox.ReplayAll() - - cred = cred_class.authorize(TEST_USER2, TEST_PROJECT) - - self.mox.VerifyAll() - - self.assertTrue(cred is not None) - self.assertEqual(cred.username, TEST_USER2) - self.assertEqual(cred.project, TEST_PROJECT) - self.assertEqual(cred.auth_token, TEST_AUTH_TOKEN_2) - self.assertFalse(cred.auth_token_expired()) - - cred = cred_class.get_by_token(TEST_AUTH_TOKEN_2) - self.assertTrue(cred is not None) - - def test_create_auth_token(self): - rand_state = random.getstate() - expected_salt = hashlib.sha1(str(random.random())).hexdigest()[:5] - expected_token = hashlib.sha1(expected_salt + TEST_USER).hexdigest() - - random.setstate(rand_state) - auth_token = \ - nova_models.CredentialsAuthorization.create_auth_token(TEST_USER) - self.assertEqual(expected_token, auth_token) - - def test_auth_token_expired(self): - ''' - Test expired in past, expires in future, expires _right now_ - ''' - cred = \ - nova_models.CredentialsAuthorization.get_by_token(TEST_AUTH_TOKEN) - - cred.auth_date = utils.utcnow() - AUTH_EXPIRATION_LENGTH \ - - HOUR - self.assertTrue(cred.auth_token_expired()) - - cred.auth_date = utils.utcnow() - - self.assertFalse(cred.auth_token_expired()) - - # testing with time is tricky. Mock out "right now" test to avoid - # timing issues - time = utils.utcnow.override_time = utils.utcnow() - cred.auth_date = time - AUTH_EXPIRATION_LENGTH - - self.assertTrue(cred.auth_token_expired()) - - utils.utcnow.override_time = None - - def test_get_download_url(self): - cred = \ - nova_models.CredentialsAuthorization.get_by_token(TEST_AUTH_TOKEN) - - expected_url = settings.CREDENTIAL_DOWNLOAD_URL + TEST_AUTH_TOKEN - self.assertEqual(expected_url, cred.get_download_url()) - - def test_get_zip(self): - cred = \ - nova_models.CredentialsAuthorization.get_by_token(TEST_AUTH_TOKEN) - - admin_mock = self.mox.CreateMock(NovaAdminClient) - - self.mox.StubOutWithMock(connection, 'get_nova_admin_connection') - connection.get_nova_admin_connection().AndReturn(admin_mock) - - admin_mock.get_zip(TEST_USER, TEST_PROJECT) - - self.mox.ReplayAll() - - cred.get_zip() - - self.mox.VerifyAll() - - cred = \ - nova_models.CredentialsAuthorization.get_by_token(TEST_AUTH_TOKEN) - - self.assertTrue(cred is None) diff --git a/django-openstack/django_openstack/tests/broken/volume_tests.py b/django-openstack/django_openstack/tests/broken/volume_tests.py deleted file mode 100644 index b4ca10e3..00000000 --- a/django-openstack/django_openstack/tests/broken/volume_tests.py +++ /dev/null @@ -1,171 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# 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. - -""" -Unit tests for volume views. -""" - -import boto.ec2.volume -import mox - -from django.core.urlresolvers import reverse -from django_openstack.nova import forms -from django_openstack.nova.tests.base import (BaseProjectViewTests, - TEST_PROJECT) - - -TEST_VOLUME = 'vol-0000001' - - -class VolumeTests(BaseProjectViewTests): - def test_index(self): - instance_id = 'i-abcdefgh' - - volume = boto.ec2.volume.Volume() - volume.id = TEST_VOLUME - volume.displayName = TEST_VOLUME - volume.size = 1 - - self.mox.StubOutWithMock(self.project, 'get_volumes') - self.mox.StubOutWithMock(forms, 'get_available_volume_choices') - self.mox.StubOutWithMock(forms, 'get_instance_choices') - self.project.get_volumes().AndReturn([]) - forms.get_available_volume_choices(mox.IgnoreArg()).AndReturn( - self.create_available_volume_choices([volume])) - forms.get_instance_choices(mox.IgnoreArg()).AndReturn( - self.create_instance_choices([instance_id])) - - self.mox.ReplayAll() - - response = self.client.get(reverse('nova_volumes', - args=[TEST_PROJECT])) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, - 'django_openstack/nova/volumes/index.html') - self.assertEqual(len(response.context['volumes']), 0) - - self.mox.VerifyAll() - - def test_add_get(self): - self.mox.ReplayAll() - - res = self.client.get(reverse('nova_volumes_add', args=[TEST_PROJECT])) - self.assertRedirectsNoFollow(res, reverse('nova_volumes', - args=[TEST_PROJECT])) - self.mox.VerifyAll() - - def test_add_post(self): - vol = boto.ec2.volume.Volume() - vol.name = TEST_VOLUME - vol.displayName = TEST_VOLUME - vol.size = 1 - - self.mox.StubOutWithMock(self.project, 'create_volume') - self.project.create_volume(vol.size, vol.name, vol.name).AndReturn(vol) - - self.mox.ReplayAll() - - url = reverse('nova_volumes_add', args=[TEST_PROJECT]) - data = {'size': '1', - 'nickname': TEST_VOLUME, - 'description': TEST_VOLUME} - res = self.client.post(url, data) - self.assertRedirectsNoFollow(res, reverse('nova_volumes', - args=[TEST_PROJECT])) - self.mox.VerifyAll() - - def test_delete_get(self): - self.mox.ReplayAll() - - res = self.client.get(reverse('nova_volumes_delete', - args=[TEST_PROJECT, TEST_VOLUME])) - self.assertRedirectsNoFollow(res, reverse('nova_volumes', - args=[TEST_PROJECT])) - self.mox.VerifyAll() - - def test_delete_post(self): - self.mox.StubOutWithMock(self.project, 'delete_volume') - self.project.delete_volume(TEST_VOLUME).AndReturn(True) - - self.mox.ReplayAll() - - res = self.client.post(reverse('nova_volumes_delete', - args=[TEST_PROJECT, TEST_VOLUME])) - self.assertRedirectsNoFollow(res, reverse('nova_volumes', - args=[TEST_PROJECT])) - self.mox.VerifyAll() - - def test_attach_get(self): - self.mox.ReplayAll() - - res = self.client.get(reverse('nova_volumes_attach', - args=[TEST_PROJECT])) - self.assertRedirectsNoFollow(res, reverse('nova_volumes', - args=[TEST_PROJECT])) - self.mox.VerifyAll() - - def test_attach_post(self): - volume = boto.ec2.volume.Volume() - volume.id = TEST_VOLUME - volume.displayName = TEST_VOLUME - volume.size = 1 - - instance_id = 'i-abcdefgh' - device = '/dev/vdb' - - self.mox.StubOutWithMock(self.project, 'attach_volume') - self.mox.StubOutWithMock(forms, 'get_available_volume_choices') - self.mox.StubOutWithMock(forms, 'get_instance_choices') - self.project.attach_volume(TEST_VOLUME, instance_id, device) \ - .AndReturn(True) - forms.get_available_volume_choices(mox.IgnoreArg()).AndReturn( - self.create_available_volume_choices([volume])) - forms.get_instance_choices(mox.IgnoreArg()).AndReturn( - self.create_instance_choices([instance_id])) - - self.mox.ReplayAll() - - url = reverse('nova_volumes_attach', args=[TEST_PROJECT]) - data = {'volume': TEST_VOLUME, - 'instance': instance_id, - 'device': device} - res = self.client.post(url, data) - self.assertRedirectsNoFollow(res, reverse('nova_volumes', - args=[TEST_PROJECT])) - self.mox.VerifyAll() - - def test_detach_get(self): - self.mox.ReplayAll() - - res = self.client.get(reverse('nova_volumes_detach', - args=[TEST_PROJECT, TEST_VOLUME])) - self.assertRedirectsNoFollow(res, reverse('nova_volumes', - args=[TEST_PROJECT])) - self.mox.VerifyAll() - - def test_detach_post(self): - self.mox.StubOutWithMock(self.project, 'detach_volume') - self.project.detach_volume(TEST_VOLUME).AndReturn(True) - - self.mox.ReplayAll() - - res = self.client.post(reverse('nova_volumes_detach', - args=[TEST_PROJECT, TEST_VOLUME])) - self.assertRedirectsNoFollow(res, reverse('nova_volumes', - args=[TEST_PROJECT])) - self.mox.VerifyAll() diff --git a/django-openstack/django_openstack/tests/context_processor_tests.py b/django-openstack/django_openstack/tests/context_processor_tests.py deleted file mode 100644 index 33584586..00000000 --- a/django-openstack/django_openstack/tests/context_processor_tests.py +++ /dev/null @@ -1,23 +0,0 @@ -from django_openstack import context_processors, test - - -class ContextProcessorTests(test.TestCase): - def setUp(self): - super(ContextProcessorTests, self).setUp() - self._prev_catalog = self.request.user.service_catalog - - def tearDown(self): - super(ContextProcessorTests, self).tearDown() - self.request.user.service_catalog = self._prev_catalog - - def test_object_store(self): - # Returns the object store service data when it's in the catalog - object_store = context_processors.object_store(self.request) - self.assertNotEqual(None, object_store['object_store_configured']) - - # Returns None when the object store is not in the catalog - new_catalog = [service for service in self.request.user.service_catalog - if service['type'] != 'object-store'] - self.request.user.service_catalog = new_catalog - object_store = context_processors.object_store(self.request) - self.assertEqual(None, object_store['object_store_configured']) diff --git a/django-openstack/django_openstack/tests/dependency_tests.py b/django-openstack/django_openstack/tests/dependency_tests.py deleted file mode 100644 index 19df189f..00000000 --- a/django-openstack/django_openstack/tests/dependency_tests.py +++ /dev/null @@ -1,39 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Tests for dependency packages -Honestly, this can probably go away once tests that depend on these -packages become more ingrained in the code. -""" - -from django import test -from django.core import mail -from mailer import engine -from mailer import send_mail - - -class DjangoMailerPresenceTest(test.TestCase): - def test_mailsent(self): - send_mail('subject', 'message_body', 'from@test.com', ['to@test.com']) - engine.send_all() - - self.assertEqual(len(mail.outbox), 1) - self.assertEqual(mail.outbox[0].subject, 'subject') diff --git a/django-openstack/django_openstack/tests/templates/base-sidebar.html b/django-openstack/django_openstack/tests/templates/base-sidebar.html deleted file mode 100644 index e69de29b..00000000 diff --git a/django-openstack/django_openstack/tests/templatetag_tests.py b/django-openstack/django_openstack/tests/templatetag_tests.py deleted file mode 100644 index 7ca198e2..00000000 --- a/django-openstack/django_openstack/tests/templatetag_tests.py +++ /dev/null @@ -1,71 +0,0 @@ -import re - -from django import dispatch, http, template -from django.utils.text import normalize_newlines - -from django_openstack import signals, test - - -def single_line(text): - ''' Quick utility to make comparing template output easier. ''' - return re.sub(' +', - ' ', - normalize_newlines(text).replace('\n', '')).strip() - - -class TemplateTagTests(test.TestCase): - def setUp(self): - super(TemplateTagTests, self).setUp() - self._signal = self.mox.CreateMock(dispatch.Signal) - - def test_sidebar_modules(self): - ''' - Tests for the sidebar module registration mechanism. - - The standard "ping" signal return value looks like this: - - tuple(, { - 'title': 'Nixon', - 'links': [{'url':'/syspanel/nixon/google', - 'text':'Google', 'active_text': 'google'}], - 'type': 'syspanel', - }) - ''' - self.mox.StubOutWithMock(signals, 'dash_modules_detect') - signals_call = ( - (self._signal, { - 'title': 'Nixon', - 'links': [{'url':'/dash/nixon/google', - 'text':'Google', 'active_text': 'google'}], - 'type': 'dash', - }), - (self._signal, { - 'title': 'Nixon', - 'links': [{'url':'/syspanel/nixon/google', - 'text':'Google', 'active_text': 'google'}], - 'type': 'syspanel', - }), - ) - signals.dash_modules_detect().AndReturn(signals_call) - signals.dash_modules_detect().AndReturn(signals_call) - self.mox.ReplayAll() - - context = template.Context({'request': self.request}) - - # Dash module is rendered correctly, and only in dash sidebar - ttext = '{% load sidebar_modules %}{% dash_sidebar_modules request %}' - t = template.Template(ttext) - self.assertEqual(single_line(t.render(context)), - '

Nixon

') - - # Syspanel module is rendered correctly and only in syspanel sidebar - ttext = ('{% load sidebar_modules %}' - '{% syspanel_sidebar_modules request %}') - t = template.Template(ttext) - self.assertEqual(single_line(t.render(context)), - '

Nixon

') - - self.mox.VerifyAll() diff --git a/django-openstack/django_openstack/tests/testsettings.py b/django-openstack/django_openstack/tests/testsettings.py deleted file mode 100644 index 37657015..00000000 --- a/django-openstack/django_openstack/tests/testsettings.py +++ /dev/null @@ -1,84 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import os - -ROOT_PATH = os.path.dirname(os.path.abspath(__file__)) -DEBUG = True -TESTSERVER = 'http://testserver' -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': '/tmp/django-openstack.db', - }, - } -INSTALLED_APPS = ['django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django_openstack', - 'django_openstack.tests', - 'django_openstack.templatetags', - 'mailer', - ] - -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django_openstack.middleware.keystone.AuthenticationMiddleware', - ) - -ROOT_URLCONF = 'django_openstack.tests.testurls' -TEMPLATE_DIRS = ( - os.path.join(ROOT_PATH, 'tests', 'templates') -) -SITE_ID = 1 -SITE_BRANDING = 'OpenStack' -SITE_NAME = 'openstack' -ENABLE_VNC = True -NOVA_DEFAULT_ENDPOINT = None -NOVA_DEFAULT_REGION = 'test' -NOVA_ACCESS_KEY = 'test' -NOVA_SECRET_KEY = 'test' -QUANTUM_URL = '127.0.0.1' -QUANTUM_PORT = '9696' -QUANTUM_TENANT = '1234' -QUANTUM_CLIENT_VERSION = '0.1' - -CREDENTIAL_AUTHORIZATION_DAYS = 2 -CREDENTIAL_DOWNLOAD_URL = TESTSERVER + '/credentials/' - -TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' -NOSE_ARGS = ['--nocapture', - '--cover-package=django_openstack', - '--cover-inclusive', - ] - -# django-mailer uses a different config attribute -# even though it just wraps django.core.mail -MAILER_EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' -EMAIL_BACKEND = MAILER_EMAIL_BACKEND - -SWIFT_ACCOUNT = 'test' -SWIFT_USER = 'tester' -SWIFT_PASS = 'testing' -SWIFT_AUTHURL = 'http://swift/swiftapi/v1.0' diff --git a/django-openstack/django_openstack/tests/testurls.py b/django-openstack/django_openstack/tests/testurls.py deleted file mode 100644 index 4dc34d9f..00000000 --- a/django-openstack/django_openstack/tests/testurls.py +++ /dev/null @@ -1,40 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -URL patterns for testing django-openstack views. -""" - -from django.conf.urls.defaults import * - -from django_openstack import urls as django_openstack_urls - - -urlpatterns = patterns('', - url(r'^$', 'django_openstack.tests.views.fakeView', name='splash'), - url(r'^dash/$', 'django_openstack.dash.views.instances.usage', - name='dash_overview'), - url(r'^syspanel/$', 'django_openstack.syspanel.views.instances.usage', - name='syspanel_overview') -) - - -# NOTE(termie): just append them since we want the routes at the root -urlpatterns += django_openstack_urls.urlpatterns diff --git a/django-openstack/django_openstack/tests/view_tests/__init__.py b/django-openstack/django_openstack/tests/view_tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/django-openstack/django_openstack/tests/view_tests/auth_tests.py b/django-openstack/django_openstack/tests/view_tests/auth_tests.py deleted file mode 100644 index 1c637f27..00000000 --- a/django-openstack/django_openstack/tests/view_tests/auth_tests.py +++ /dev/null @@ -1,230 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from django import http -from django.contrib import messages -from django.core.urlresolvers import reverse -from django_openstack import api -from django_openstack.tests.view_tests import base -from openstackx.api import exceptions as api_exceptions -from mox import IsA - - -class AuthViewTests(base.BaseViewTests): - def setUp(self): - super(AuthViewTests, self).setUp() - self.setActiveUser() - self.PASSWORD = 'secret' - - def test_login_index(self): - res = self.client.get(reverse('auth_login')) - self.assertTemplateUsed(res, 'splash.html') - - def test_login_user_logged_in(self): - self.setActiveUser(self.TEST_TOKEN, self.TEST_USER, self.TEST_TENANT, - False, self.TEST_SERVICE_CATALOG) - - res = self.client.get(reverse('auth_login')) - self.assertRedirectsNoFollow(res, reverse('dash_overview')) - - def test_login_admin_logged_in(self): - self.setActiveUser(self.TEST_TOKEN, self.TEST_USER, self.TEST_TENANT, - True, self.TEST_SERVICE_CATALOG) - - res = self.client.get(reverse('auth_login')) - self.assertRedirectsNoFollow(res, reverse('syspanel_overview')) - - def test_login_no_tenants(self): - NEW_TENANT_ID = '6' - NEW_TENANT_NAME = 'FAKENAME' - TOKEN_ID = 1 - - form_data = {'method': 'Login', - 'password': self.PASSWORD, - 'username': self.TEST_USER} - - self.mox.StubOutWithMock(api, 'token_create') - - class FakeToken(object): - id = TOKEN_ID, - user = {'roles': [{'name': 'fake'}]}, - serviceCatalog = {} - aToken = api.Token(FakeToken()) - - api.token_create(IsA(http.HttpRequest), "", self.TEST_USER, - self.PASSWORD).AndReturn(aToken) - - aTenant = self.mox.CreateMock(api.Token) - aTenant.id = NEW_TENANT_ID - aTenant.name = NEW_TENANT_NAME - - self.mox.StubOutWithMock(api, 'tenant_list_for_token') - api.tenant_list_for_token(IsA(http.HttpRequest), aToken.id).\ - AndReturn([]) - - self.mox.StubOutWithMock(messages, 'error') - messages.error(IsA(http.HttpRequest), IsA(unicode)) - - self.mox.ReplayAll() - - res = self.client.post(reverse('auth_login'), form_data) - - self.assertTemplateUsed(res, 'splash.html') - - self.mox.VerifyAll() - - def test_login(self): - NEW_TENANT_ID = '6' - NEW_TENANT_NAME = 'FAKENAME' - TOKEN_ID = 1 - - form_data = {'method': 'Login', - 'password': self.PASSWORD, - 'username': self.TEST_USER} - - self.mox.StubOutWithMock(api, 'token_create') - - class FakeToken(object): - id = TOKEN_ID, - user = {"id": "1", - "roles": [{"id": "1", "name": "fake"}], "name": "user"} - serviceCatalog = {} - tenant = None - aToken = api.Token(FakeToken()) - bToken = aToken - - api.token_create(IsA(http.HttpRequest), "", self.TEST_USER, - self.PASSWORD).AndReturn(aToken) - - aTenant = self.mox.CreateMock(api.Token) - aTenant.id = NEW_TENANT_ID - aTenant.name = NEW_TENANT_NAME - bToken.tenant = {'id': aTenant.id, 'name': aTenant.name} - - self.mox.StubOutWithMock(api, 'tenant_list_for_token') - api.tenant_list_for_token(IsA(http.HttpRequest), aToken.id).\ - AndReturn([aTenant]) - - self.mox.StubOutWithMock(api, 'token_create_scoped') - api.token_create_scoped(IsA(http.HttpRequest), aTenant.id, - aToken.id).AndReturn(bToken) - - self.mox.ReplayAll() - - res = self.client.post(reverse('auth_login'), form_data) - - self.assertRedirectsNoFollow(res, reverse('dash_overview')) - - self.mox.VerifyAll() - - def test_login_invalid_credentials(self): - form_data = {'method': 'Login', - 'password': self.PASSWORD, - 'username': self.TEST_USER} - - self.mox.StubOutWithMock(api, 'token_create') - unauthorized = api_exceptions.Unauthorized('unauth', message='unauth') - api.token_create(IsA(http.HttpRequest), "", self.TEST_USER, - self.PASSWORD).AndRaise(unauthorized) - - self.mox.ReplayAll() - - res = self.client.post(reverse('auth_login'), form_data) - - self.assertTemplateUsed(res, 'splash.html') - - self.mox.VerifyAll() - - def test_login_exception(self): - form_data = {'method': 'Login', - 'password': self.PASSWORD, - 'username': self.TEST_USER} - - self.mox.StubOutWithMock(api, 'token_create') - api_exception = api_exceptions.ApiException('apiException', - message='apiException') - api.token_create(IsA(http.HttpRequest), "", self.TEST_USER, - self.PASSWORD).AndRaise(api_exception) - - self.mox.ReplayAll() - - res = self.client.post(reverse('auth_login'), form_data) - - self.assertTemplateUsed(res, 'splash.html') - - self.mox.VerifyAll() - - def test_switch_tenants_index(self): - res = self.client.get(reverse('auth_switch', args=[self.TEST_TENANT])) - - self.assertTemplateUsed(res, 'switch_tenants.html') - - def test_switch_tenants(self): - NEW_TENANT_ID = '6' - NEW_TENANT_NAME = 'FAKENAME' - TOKEN_ID = 1 - - self.setActiveUser(self.TEST_TOKEN, self.TEST_USER, self.TEST_TENANT, - False, self.TEST_SERVICE_CATALOG) - - form_data = {'method': 'LoginWithTenant', - 'password': self.PASSWORD, - 'tenant': NEW_TENANT_ID, - 'username': self.TEST_USER} - - self.mox.StubOutWithMock(api, 'token_create') - - aTenant = self.mox.CreateMock(api.Token) - aTenant.id = NEW_TENANT_ID - aTenant.name = NEW_TENANT_NAME - - aToken = self.mox.CreateMock(api.Token) - aToken.id = TOKEN_ID - aToken.user = {'name': self.TEST_USER, 'roles': [{'name': 'fake'}]} - aToken.serviceCatalog = {} - aToken.tenant = {'id': aTenant.id, 'name': aTenant.name} - - api.token_create(IsA(http.HttpRequest), NEW_TENANT_ID, self.TEST_USER, - self.PASSWORD).AndReturn(aToken) - - self.mox.StubOutWithMock(api, 'tenant_list_for_token') - api.tenant_list_for_token(IsA(http.HttpRequest), aToken.id).\ - AndReturn([aTenant]) - - self.mox.ReplayAll() - - res = self.client.post(reverse('auth_switch', args=[NEW_TENANT_ID]), - form_data) - - self.assertRedirectsNoFollow(res, reverse('dash_overview')) - self.assertEqual(self.client.session['tenant'], NEW_TENANT_NAME) - - self.mox.VerifyAll() - - def test_logout(self): - KEY = 'arbitraryKeyString' - VALUE = 'arbitraryKeyValue' - self.assertNotIn(KEY, self.client.session) - self.client.session[KEY] = VALUE - - res = self.client.get(reverse('auth_logout')) - - self.assertRedirectsNoFollow(res, reverse('splash')) - self.assertNotIn(KEY, self.client.session) diff --git a/django-openstack/django_openstack/tests/view_tests/base.py b/django-openstack/django_openstack/tests/view_tests/base.py deleted file mode 100644 index 8ec5f419..00000000 --- a/django-openstack/django_openstack/tests/view_tests/base.py +++ /dev/null @@ -1,77 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Base classes for view based unit tests. -""" -from django import http -from django import shortcuts -from django import test as django_test -from django import template as django_template -from django.conf import settings -from django_openstack import test - - -def fake_render_to_response(template_name, context, context_instance=None, - mimetype='text/html'): - """Replacement for render_to_response so that views can be tested - without having to stub out templates that belong in the frontend - implementation. - - Should be able to be tested using the django unit test assertions like a - normal render_to_response return value can be. - """ - class Template(object): - def __init__(self, name): - self.name = name - - if context_instance is None: - context_instance = django_template.Context(context) - else: - context_instance.update(context) - - resp = http.HttpResponse() - template = Template(template_name) - - resp.write('

' - 'This is a fake httpresponse for testing purposes only' - '

') - - # Allows django.test.client to populate fields on the response object - django_test.signals.template_rendered.send(template, template=template, - context=context_instance) - - return resp - - -class BaseViewTests(test.TestCase): - def setUp(self): - super(BaseViewTests, self).setUp() - self._real_render_to_response = shortcuts.render_to_response - shortcuts.render_to_response = fake_render_to_response - - def tearDown(self): - super(BaseViewTests, self).tearDown() - shortcuts.render_to_response = self._real_render_to_response - - def assertRedirectsNoFollow(self, response, expected_url): - self.assertEqual(response._headers['location'], - ('Location', settings.TESTSERVER + expected_url)) - self.assertEqual(response.status_code, 302) diff --git a/django-openstack/django_openstack/tests/view_tests/dash/__init__.py b/django-openstack/django_openstack/tests/view_tests/dash/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/django-openstack/django_openstack/tests/view_tests/dash/container_tests.py b/django-openstack/django_openstack/tests/view_tests/dash/container_tests.py deleted file mode 100644 index f9b03ea7..00000000 --- a/django-openstack/django_openstack/tests/view_tests/dash/container_tests.py +++ /dev/null @@ -1,121 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from cloudfiles.errors import ContainerNotEmpty -from django import http -from django.contrib import messages -from django.core.urlresolvers import reverse -from django_openstack import api -from django_openstack.tests.view_tests import base -from mox import IgnoreArg, IsA - - -class ContainerViewTests(base.BaseViewTests): - def setUp(self): - super(ContainerViewTests, self).setUp() - self.container = self.mox.CreateMock(api.Container) - self.container.name = 'containerName' - - def test_index(self): - self.mox.StubOutWithMock(api, 'swift_get_containers') - api.swift_get_containers( - IsA(http.HttpRequest), marker=None).AndReturn([self.container]) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_containers', args=['tenant'])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/containers/index.html') - self.assertIn('containers', res.context) - containers = res.context['containers'] - - self.assertEqual(len(containers), 1) - self.assertEqual(containers[0].name, 'containerName') - - self.mox.VerifyAll() - - def test_delete_container(self): - formData = {'container_name': 'containerName', - 'method': 'DeleteContainer'} - - self.mox.StubOutWithMock(api, 'swift_delete_container') - api.swift_delete_container(IsA(http.HttpRequest), - 'containerName') - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_containers', args=['tenant']), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_containers', - args=['tenant'])) - - self.mox.VerifyAll() - - def test_delete_container_nonempty(self): - formData = {'container_name': 'containerName', - 'method': 'DeleteContainer'} - - exception = ContainerNotEmpty('containerNotEmpty') - - self.mox.StubOutWithMock(api, 'swift_delete_container') - api.swift_delete_container( - IsA(http.HttpRequest), - 'containerName').AndRaise(exception) - - self.mox.StubOutWithMock(messages, 'error') - - messages.error(IgnoreArg(), IsA(unicode)) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_containers', args=['tenant']), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_containers', - args=['tenant'])) - - self.mox.VerifyAll() - - def test_create_container_get(self): - res = self.client.get(reverse('dash_containers_create', - args=['tenant'])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/containers/create.html') - - def test_create_container_post(self): - formData = {'name': 'containerName', - 'method': 'CreateContainer'} - - self.mox.StubOutWithMock(api, 'swift_create_container') - api.swift_create_container( - IsA(http.HttpRequest), 'CreateContainer') - - self.mox.StubOutWithMock(messages, 'success') - messages.success(IgnoreArg(), IsA(basestring)) - - res = self.client.post(reverse('dash_containers_create', - args=[self.request.user.tenant_id]), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_containers', - args=[self.request.user.tenant_id])) diff --git a/django-openstack/django_openstack/tests/view_tests/dash/floating_ip_tests.py b/django-openstack/django_openstack/tests/view_tests/dash/floating_ip_tests.py deleted file mode 100644 index 6a7c05f0..00000000 --- a/django-openstack/django_openstack/tests/view_tests/dash/floating_ip_tests.py +++ /dev/null @@ -1,221 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import datetime - -from django import http -from django.contrib import messages -from django.core.urlresolvers import reverse -from django.shortcuts import redirect -from django_openstack import api -from django_openstack import utils -from django_openstack.dash.views.floating_ips import FloatingIpAssociate -from django_openstack.tests.view_tests import base -from mox import IsA, IgnoreArg -from novaclient import exceptions as novaclient_exceptions - - -class FloatingIpViewTests(base.BaseViewTests): - - def setUp(self): - super(FloatingIpViewTests, self).setUp() - server = self.mox.CreateMock(api.Server) - server.id = 1 - server.name = 'serverName' - self.server = server - self.servers = (server, ) - - floating_ip = self.mox.CreateMock(api.FloatingIp) - floating_ip.id = 1 - floating_ip.fixed_ip = '10.0.0.4' - floating_ip.instance_id = 1 - floating_ip.ip = '58.58.58.58' - - self.floating_ip = floating_ip - self.floating_ips = [floating_ip, ] - - def test_index(self): - self.mox.StubOutWithMock(api, 'tenant_floating_ip_list') - api.tenant_floating_ip_list(IsA(http.HttpRequest)).\ - AndReturn(self.floating_ips) - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_floating_ips', - args=[self.TEST_TENANT])) - self.assertTemplateUsed(res, - 'django_openstack/dash/floating_ips/index.html') - self.assertItemsEqual(res.context['floating_ips'], self.floating_ips) - - self.mox.VerifyAll() - - def test_associate(self): - self.mox.StubOutWithMock(api, 'server_list') - api.server_list = self.mox.CreateMockAnything() - api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers) - - self.mox.StubOutWithMock(api, 'tenant_floating_ip_get') - api.tenant_floating_ip_get = self.mox.CreateMockAnything() - api.tenant_floating_ip_get(IsA(http.HttpRequest), str(1)).\ - AndReturn(self.floating_ip) - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_floating_ips_associate', - args=[self.TEST_TENANT, 1])) - self.assertTemplateUsed(res, - 'django_openstack/dash/floating_ips/associate.html') - self.mox.VerifyAll() - - def test_associate_post(self): - server = self.server - - self.mox.StubOutWithMock(api, 'server_list') - api.server_list = self.mox.CreateMockAnything() - api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers) - - self.mox.StubOutWithMock(api, 'tenant_floating_ip_list') - api.tenant_floating_ip_list(IsA(http.HttpRequest)).\ - AndReturn(self.floating_ips) - - self.mox.StubOutWithMock(api, 'server_add_floating_ip') - api.server_add_floating_ip = self.mox.CreateMockAnything() - api.server_add_floating_ip(IsA(http.HttpRequest), IsA(unicode), - IsA(unicode)).\ - AndReturn(None) - self.mox.StubOutWithMock(messages, 'info') - messages.info(IsA(http.HttpRequest), IsA(unicode)) - - self.mox.StubOutWithMock(api, 'tenant_floating_ip_get') - api.tenant_floating_ip_get = self.mox.CreateMockAnything() - api.tenant_floating_ip_get(IsA(http.HttpRequest), str(1)).\ - AndReturn(self.floating_ip) - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_floating_ips_associate', - args=[self.TEST_TENANT, 1]), - {'instance_id': 1, - 'floating_ip_id': self.floating_ip.id, - 'floating_ip': self.floating_ip.ip, - 'method': 'FloatingIpAssociate'}) - - self.assertRedirects(res, reverse('dash_floating_ips', - args=[self.TEST_TENANT])) - self.mox.VerifyAll() - - def test_associate_post_with_exception(self): - server = self.server - - self.mox.StubOutWithMock(api, 'server_list') - api.server_list = self.mox.CreateMockAnything() - api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers) - - self.mox.StubOutWithMock(api, 'tenant_floating_ip_list') - api.tenant_floating_ip_list(IsA(http.HttpRequest)).\ - AndReturn(self.floating_ips) - - self.mox.StubOutWithMock(api, 'server_add_floating_ip') - api.server_add_floating_ip = self.mox.CreateMockAnything() - exception = novaclient_exceptions.ClientException('ClientException', - message='clientException') - api.server_add_floating_ip(IsA(http.HttpRequest), IsA(unicode), - IsA(unicode)).\ - AndRaise(exception) - - self.mox.StubOutWithMock(messages, 'error') - messages.error(IsA(http.HttpRequest), IsA(basestring)) - - self.mox.StubOutWithMock(api, 'tenant_floating_ip_get') - api.tenant_floating_ip_get = self.mox.CreateMockAnything() - api.tenant_floating_ip_get(IsA(http.HttpRequest), IsA(unicode)).\ - AndReturn(self.floating_ip) - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_floating_ips_associate', - args=[self.TEST_TENANT, 1]), - {'instance_id': 1, - 'floating_ip_id': self.floating_ip.id, - 'floating_ip': self.floating_ip.ip, - 'method': 'FloatingIpAssociate'}) - self.assertRaises(novaclient_exceptions.ClientException) - - self.assertRedirects(res, reverse('dash_floating_ips', - args=[self.TEST_TENANT])) - - self.mox.VerifyAll() - - def test_disassociate(self): - res = self.client.get(reverse('dash_floating_ips_disassociate', - args=[self.TEST_TENANT, 1])) - self.assertTemplateUsed(res, - 'django_openstack/dash/floating_ips/associate.html') - self.mox.VerifyAll() - - def test_disassociate_post(self): - self.mox.StubOutWithMock(api, 'tenant_floating_ip_list') - api.tenant_floating_ip_list(IsA(http.HttpRequest)).\ - AndReturn(self.floating_ips) - - self.mox.StubOutWithMock(api, 'server_remove_floating_ip') - api.server_remove_floating_ip = self.mox.CreateMockAnything() - api.server_remove_floating_ip(IsA(http.HttpRequest), IsA(int), - IsA(int)).\ - AndReturn(None) - self.mox.StubOutWithMock(messages, 'info') - messages.info(IsA(http.HttpRequest), IsA(unicode)) - - self.mox.StubOutWithMock(api, 'tenant_floating_ip_get') - api.tenant_floating_ip_get = self.mox.CreateMockAnything() - api.tenant_floating_ip_get(IsA(http.HttpRequest), IsA(unicode)).\ - AndReturn(self.floating_ip) - self.mox.ReplayAll() - res = self.client.post(reverse('dash_floating_ips_disassociate', - args=[self.TEST_TENANT, 1]), - {'floating_ip_id': self.floating_ip.id, - 'method': 'FloatingIpDisassociate'}) - self.assertRedirects(res, reverse('dash_floating_ips', - args=[self.TEST_TENANT])) - self.mox.VerifyAll() - - def test_disassociate_post_with_exception(self): - self.mox.StubOutWithMock(api, 'tenant_floating_ip_list') - api.tenant_floating_ip_list(IsA(http.HttpRequest)).\ - AndReturn(self.floating_ips) - - self.mox.StubOutWithMock(api, 'server_remove_floating_ip') - exception = novaclient_exceptions.ClientException('ClientException', - message='clientException') - api.server_remove_floating_ip(IsA(http.HttpRequest), - IsA(int), - IsA(int)).AndRaise(exception) - self.mox.StubOutWithMock(messages, 'error') - messages.error(IsA(http.HttpRequest), IsA(basestring)) - - self.mox.StubOutWithMock(api, 'tenant_floating_ip_get') - api.tenant_floating_ip_get = self.mox.CreateMockAnything() - api.tenant_floating_ip_get(IsA(http.HttpRequest), IsA(unicode)).\ - AndReturn(self.floating_ip) - self.mox.ReplayAll() - res = self.client.post(reverse('dash_floating_ips_disassociate', - args=[self.TEST_TENANT, 1]), - {'floating_ip_id': self.floating_ip.id, - 'method': 'FloatingIpDisassociate'}) - self.assertRaises(novaclient_exceptions.ClientException) - self.assertRedirects(res, reverse('dash_floating_ips', - args=[self.TEST_TENANT])) - self.mox.VerifyAll() diff --git a/django-openstack/django_openstack/tests/view_tests/dash/images_tests.py b/django-openstack/django_openstack/tests/view_tests/dash/images_tests.py deleted file mode 100644 index 075e0e87..00000000 --- a/django-openstack/django_openstack/tests/view_tests/dash/images_tests.py +++ /dev/null @@ -1,374 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from django import http -from django.contrib import messages -from django.core.urlresolvers import reverse -from django_openstack import api -from django_openstack.tests.view_tests import base -from glance.common import exception as glance_exception -from openstackx.api import exceptions as api_exceptions -from mox import IgnoreArg, IsA - - -class FakeQuota: - ram = 100 - - -class ImageViewTests(base.BaseViewTests): - def setUp(self): - super(ImageViewTests, self).setUp() - image_dict = {'name': 'visibleImage', - 'container_format': 'novaImage'} - self.visibleImage = api.Image(image_dict) - - image_dict = {'name': 'invisibleImage', - 'container_format': 'aki'} - self.invisibleImage = api.Image(image_dict) - - self.images = (self.visibleImage, self.invisibleImage) - - flavor = self.mox.CreateMock(api.Flavor) - flavor.id = 1 - flavor.name = 'm1.massive' - flavor.vcpus = 1000 - flavor.disk = 1024 - flavor.ram = 10000 - self.flavors = (flavor,) - - keypair = self.mox.CreateMock(api.KeyPair) - keypair.name = 'keyName' - self.keypairs = (keypair,) - - security_group = self.mox.CreateMock(api.SecurityGroup) - security_group.name = 'default' - self.security_groups = (security_group,) - - def test_index(self): - self.mox.StubOutWithMock(api, 'image_list_detailed') - api.image_list_detailed(IsA(http.HttpRequest)).AndReturn(self.images) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_images', args=[self.TEST_TENANT])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/images/index.html') - - self.assertIn('images', res.context) - images = res.context['images'] - self.assertEqual(len(images), 1) - self.assertEqual(images[0].name, 'visibleImage') - - self.mox.VerifyAll() - - def test_index_no_images(self): - self.mox.StubOutWithMock(api, 'image_list_detailed') - api.image_list_detailed(IsA(http.HttpRequest)).AndReturn([]) - - self.mox.StubOutWithMock(messages, 'info') - messages.info(IsA(http.HttpRequest), IsA(basestring)) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_images', args=[self.TEST_TENANT])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/images/index.html') - - self.mox.VerifyAll() - - def test_index_client_conn_error(self): - self.mox.StubOutWithMock(api, 'image_list_detailed') - exception = glance_exception.ClientConnectionError('clientConnError') - api.image_list_detailed(IsA(http.HttpRequest)).AndRaise(exception) - - self.mox.StubOutWithMock(messages, 'error') - messages.error(IsA(http.HttpRequest), IsA(basestring)) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_images', args=[self.TEST_TENANT])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/images/index.html') - - self.mox.VerifyAll() - - def test_index_glance_error(self): - self.mox.StubOutWithMock(api, 'image_list_detailed') - exception = glance_exception.Error('glanceError') - api.image_list_detailed(IsA(http.HttpRequest)).AndRaise(exception) - - self.mox.StubOutWithMock(messages, 'error') - messages.error(IsA(http.HttpRequest), IsA(basestring)) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_images', args=[self.TEST_TENANT])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/images/index.html') - - self.mox.VerifyAll() - - def test_launch_get(self): - IMAGE_ID = '1' - - self.mox.StubOutWithMock(api, 'image_get') - api.image_get(IsA(http.HttpRequest), - IMAGE_ID).AndReturn(self.visibleImage) - - self.mox.StubOutWithMock(api, 'tenant_quota_get') - api.tenant_quota_get(IsA(http.HttpRequest), - self.TEST_TENANT).AndReturn(FakeQuota) - - self.mox.StubOutWithMock(api, 'flavor_list') - api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors) - - self.mox.StubOutWithMock(api, 'keypair_list') - api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs) - - self.mox.StubOutWithMock(api, 'security_group_list') - api.security_group_list(IsA(http.HttpRequest)).AndReturn( - self.security_groups) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_images_launch', - args=[self.TEST_TENANT, IMAGE_ID])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/images/launch.html') - - image = res.context['image'] - self.assertEqual(image.name, self.visibleImage.name) - - form = res.context['form'] - - form_flavorfield = form.fields['flavor'] - self.assertIn('m1.massive', form_flavorfield.choices[0][1]) - - form_keyfield = form.fields['key_name'] - self.assertEqual(form_keyfield.choices[0][0], - self.keypairs[0].name) - - self.mox.VerifyAll() - - def test_launch_post(self): - FLAVOR_ID = self.flavors[0].id - IMAGE_ID = '1' - KEY_NAME = self.keypairs[0].name - SERVER_NAME = 'serverName' - USER_DATA = 'userData' - - form_data = {'method': 'LaunchForm', - 'flavor': FLAVOR_ID, - 'image_id': IMAGE_ID, - 'key_name': KEY_NAME, - 'name': SERVER_NAME, - 'user_data': USER_DATA, - 'tenant_id': self.TEST_TENANT, - 'security_groups': 'default', - } - - self.mox.StubOutWithMock(api, 'image_get') - api.image_get(IsA(http.HttpRequest), - IMAGE_ID).AndReturn(self.visibleImage) - - self.mox.StubOutWithMock(api, 'tenant_quota_get') - api.tenant_quota_get(IsA(http.HttpRequest), - self.TEST_TENANT).AndReturn(FakeQuota) - - self.mox.StubOutWithMock(api, 'flavor_list') - api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors) - - self.mox.StubOutWithMock(api, 'keypair_list') - api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs) - - self.mox.StubOutWithMock(api, 'security_group_list') - api.security_group_list(IsA(http.HttpRequest)).AndReturn( - self.security_groups) - - # called again by the form - api.image_get(IsA(http.HttpRequest), - IMAGE_ID).AndReturn(self.visibleImage) - - self.mox.StubOutWithMock(api, 'flavor_get') - api.flavor_get(IsA(http.HttpRequest), - IsA(unicode)).AndReturn(self.flavors[0]) - - self.mox.StubOutWithMock(api, 'server_create') - - api.server_create(IsA(http.HttpRequest), SERVER_NAME, - self.visibleImage, self.flavors[0], - KEY_NAME, USER_DATA, [self.security_groups[0].name]) - - self.mox.StubOutWithMock(messages, 'success') - messages.success(IsA(http.HttpRequest), IsA(basestring)) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_images_launch', - args=[self.TEST_TENANT, IMAGE_ID]), - form_data) - - self.assertRedirectsNoFollow(res, reverse('dash_instances', - args=[self.TEST_TENANT])) - - self.mox.VerifyAll() - - def test_launch_flavorlist_error(self): - IMAGE_ID = '1' - - self.mox.StubOutWithMock(api, 'image_get') - api.image_get(IsA(http.HttpRequest), - IMAGE_ID).AndReturn(self.visibleImage) - - self.mox.StubOutWithMock(api, 'tenant_quota_get') - api.tenant_quota_get(IsA(http.HttpRequest), - self.TEST_TENANT).AndReturn(FakeQuota) - - exception = api_exceptions.ApiException('apiException') - self.mox.StubOutWithMock(api, 'flavor_list') - api.flavor_list(IsA(http.HttpRequest)).AndRaise(exception) - - self.mox.StubOutWithMock(api, 'keypair_list') - api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs) - - self.mox.StubOutWithMock(api, 'security_group_list') - api.security_group_list(IsA(http.HttpRequest)).AndReturn( - self.security_groups) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_images_launch', - args=[self.TEST_TENANT, IMAGE_ID])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/images/launch.html') - - form = res.context['form'] - - form_flavorfield = form.fields['flavor'] - self.assertIn('m1.tiny', form_flavorfield.choices[0][1]) - - self.mox.VerifyAll() - - def test_launch_keypairlist_error(self): - IMAGE_ID = '2' - - self.mox.StubOutWithMock(api, 'image_get') - api.image_get(IsA(http.HttpRequest), - IMAGE_ID).AndReturn(self.visibleImage) - - self.mox.StubOutWithMock(api, 'tenant_quota_get') - api.tenant_quota_get(IsA(http.HttpRequest), - self.TEST_TENANT).AndReturn(FakeQuota) - - self.mox.StubOutWithMock(api, 'flavor_list') - api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors) - - exception = api_exceptions.ApiException('apiException') - self.mox.StubOutWithMock(api, 'keypair_list') - api.keypair_list(IsA(http.HttpRequest)).AndRaise(exception) - - self.mox.StubOutWithMock(api, 'security_group_list') - api.security_group_list(IsA(http.HttpRequest)).AndReturn( - self.security_groups) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_images_launch', - args=[self.TEST_TENANT, IMAGE_ID])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/images/launch.html') - - form = res.context['form'] - - form_keyfield = form.fields['key_name'] - self.assertEqual(len(form_keyfield.choices), 0) - - self.mox.VerifyAll() - - def test_launch_form_apiexception(self): - FLAVOR_ID = self.flavors[0].id - IMAGE_ID = '1' - KEY_NAME = self.keypairs[0].name - SERVER_NAME = 'serverName' - USER_DATA = 'userData' - - form_data = {'method': 'LaunchForm', - 'flavor': FLAVOR_ID, - 'image_id': IMAGE_ID, - 'key_name': KEY_NAME, - 'name': SERVER_NAME, - 'tenant_id': self.TEST_TENANT, - 'user_data': USER_DATA, - 'security_groups': 'default', - } - - self.mox.StubOutWithMock(api, 'image_get') - api.image_get(IgnoreArg(), - IMAGE_ID).AndReturn(self.visibleImage) - - self.mox.StubOutWithMock(api, 'tenant_quota_get') - api.tenant_quota_get(IsA(http.HttpRequest), - self.TEST_TENANT).AndReturn(FakeQuota) - - self.mox.StubOutWithMock(api, 'flavor_list') - api.flavor_list(IgnoreArg()).AndReturn(self.flavors) - - self.mox.StubOutWithMock(api, 'keypair_list') - api.keypair_list(IgnoreArg()).AndReturn(self.keypairs) - - self.mox.StubOutWithMock(api, 'security_group_list') - api.security_group_list(IsA(http.HttpRequest)).AndReturn( - self.security_groups) - - # called again by the form - api.image_get(IgnoreArg(), - IMAGE_ID).AndReturn(self.visibleImage) - - self.mox.StubOutWithMock(api, 'flavor_get') - api.flavor_get(IgnoreArg(), - IsA(unicode)).AndReturn(self.flavors[0]) - - self.mox.StubOutWithMock(api, 'server_create') - - exception = api_exceptions.ApiException('apiException') - api.server_create(IsA(http.HttpRequest), SERVER_NAME, - self.visibleImage, self.flavors[0], - KEY_NAME, USER_DATA, - self.security_groups).AndRaise(exception) - - self.mox.StubOutWithMock(messages, 'error') - messages.error(IsA(http.HttpRequest), IsA(basestring)) - - self.mox.ReplayAll() - url = reverse('dash_images_launch', - args=[self.TEST_TENANT, IMAGE_ID]) - res = self.client.post(url, form_data) - - self.assertTemplateUsed(res, - 'django_openstack/dash/images/launch.html') - - self.mox.VerifyAll() diff --git a/django-openstack/django_openstack/tests/view_tests/dash/instance_tests.py b/django-openstack/django_openstack/tests/view_tests/dash/instance_tests.py deleted file mode 100644 index ed5812f7..00000000 --- a/django-openstack/django_openstack/tests/view_tests/dash/instance_tests.py +++ /dev/null @@ -1,470 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import datetime - -from django import http -from django.contrib import messages -from django.core.urlresolvers import reverse -from django_openstack import api -from django_openstack import utils -from django_openstack.tests.view_tests import base -from openstackx.api import exceptions as api_exceptions -from mox import IsA, IgnoreArg - - -class InstanceViewTests(base.BaseViewTests): - def setUp(self): - super(InstanceViewTests, self).setUp() - server = self.mox.CreateMock(api.Server) - server.id = 1 - server.name = 'serverName' - server.attrs = {'description': 'mydesc'} - self.servers = (server,) - - def test_index(self): - self.mox.StubOutWithMock(api, 'server_list') - api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_instances', - args=[self.TEST_TENANT])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/instances/index.html') - self.assertItemsEqual(res.context['instances'], self.servers) - - self.mox.VerifyAll() - - def test_index_server_list_exception(self): - self.mox.StubOutWithMock(api, 'server_list') - exception = api_exceptions.ApiException('apiException') - api.server_list(IsA(http.HttpRequest)).AndRaise(exception) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_instances', - args=[self.TEST_TENANT])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/instances/index.html') - self.assertEqual(len(res.context['instances']), 0) - - self.mox.VerifyAll() - - def test_terminate_instance(self): - formData = {'method': 'TerminateInstance', - 'instance': self.servers[0].id, - } - - self.mox.StubOutWithMock(api, 'server_get') - api.server_get(IsA(http.HttpRequest), - str(self.servers[0].id)).AndReturn(self.servers[0]) - self.mox.StubOutWithMock(api, 'server_delete') - api.server_delete(IsA(http.HttpRequest), - self.servers[0]) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_instances', - args=[self.TEST_TENANT]), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_instances', - args=[self.TEST_TENANT])) - - self.mox.VerifyAll() - - def test_terminate_instance_exception(self): - formData = {'method': 'TerminateInstance', - 'instance': self.servers[0].id, - } - - self.mox.StubOutWithMock(api, 'server_get') - api.server_get(IsA(http.HttpRequest), - str(self.servers[0].id)).AndReturn(self.servers[0]) - - exception = api_exceptions.ApiException('ApiException', - message='apiException') - self.mox.StubOutWithMock(api, 'server_delete') - api.server_delete(IsA(http.HttpRequest), - self.servers[0]).AndRaise(exception) - - self.mox.StubOutWithMock(messages, 'error') - messages.error(IsA(http.HttpRequest), IsA(unicode)) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_instances', - args=[self.TEST_TENANT]), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_instances', - args=[self.TEST_TENANT])) - - self.mox.VerifyAll() - - def test_reboot_instance(self): - formData = {'method': 'RebootInstance', - 'instance': self.servers[0].id, - } - - self.mox.StubOutWithMock(api, 'server_reboot') - api.server_reboot(IsA(http.HttpRequest), unicode(self.servers[0].id)) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_instances', - args=[self.TEST_TENANT]), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_instances', - args=[self.TEST_TENANT])) - - self.mox.VerifyAll() - - def test_reboot_instance_exception(self): - formData = {'method': 'RebootInstance', - 'instance': self.servers[0].id, - } - - self.mox.StubOutWithMock(api, 'server_reboot') - exception = api_exceptions.ApiException('ApiException', - message='apiException') - api.server_reboot(IsA(http.HttpRequest), - unicode(self.servers[0].id)).AndRaise(exception) - - self.mox.StubOutWithMock(messages, 'error') - messages.error(IsA(http.HttpRequest), IsA(basestring)) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_instances', - args=[self.TEST_TENANT]), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_instances', - args=[self.TEST_TENANT])) - - self.mox.VerifyAll() - - def override_times(self, time=datetime.datetime.now): - now = datetime.datetime.utcnow() - utils.time.override_time = \ - datetime.time(now.hour, now.minute, now.second) - utils.today.override_time = datetime.date(now.year, now.month, now.day) - utils.utcnow.override_time = now - - return now - - def reset_times(self): - utils.time.override_time = None - utils.today.override_time = None - utils.utcnow.override_time = None - - def test_instance_usage(self): - TEST_RETURN = 'testReturn' - - now = self.override_times() - - self.mox.StubOutWithMock(api, 'usage_get') - api.usage_get(IsA(http.HttpRequest), self.TEST_TENANT, - datetime.datetime(now.year, now.month, 1, - now.hour, now.minute, now.second), - now).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_usage', args=[self.TEST_TENANT])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/instances/usage.html') - - self.assertEqual(res.context['usage'], TEST_RETURN) - - self.mox.VerifyAll() - - self.reset_times() - - def test_instance_csv_usage(self): - TEST_RETURN = 'testReturn' - - now = self.override_times() - - self.mox.StubOutWithMock(api, 'usage_get') - api.usage_get(IsA(http.HttpRequest), self.TEST_TENANT, - datetime.datetime(now.year, now.month, 1, - now.hour, now.minute, now.second), - now).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_usage', args=[self.TEST_TENANT]) + - "?format=csv") - - self.assertTemplateUsed(res, - 'django_openstack/dash/instances/usage.csv') - - self.assertEqual(res.context['usage'], TEST_RETURN) - - self.mox.VerifyAll() - - self.reset_times() - - def test_instance_usage_exception(self): - now = self.override_times() - - exception = api_exceptions.ApiException('apiException', - message='apiException') - self.mox.StubOutWithMock(api, 'usage_get') - api.usage_get(IsA(http.HttpRequest), self.TEST_TENANT, - datetime.datetime(now.year, now.month, 1, - now.hour, now.minute, now.second), - now).AndRaise(exception) - - self.mox.StubOutWithMock(messages, 'error') - messages.error(IsA(http.HttpRequest), IsA(basestring)) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_usage', args=[self.TEST_TENANT])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/instances/usage.html') - - self.assertEqual(res.context['usage'], {}) - - self.mox.VerifyAll() - - self.reset_times() - - def test_instance_usage_default_tenant(self): - TEST_RETURN = 'testReturn' - - now = self.override_times() - - self.mox.StubOutWithMock(api, 'usage_get') - api.usage_get(IsA(http.HttpRequest), self.TEST_TENANT, - datetime.datetime(now.year, now.month, 1, - now.hour, now.minute, now.second), - now).AndReturn(TEST_RETURN) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_overview')) - - self.assertTemplateUsed(res, - 'django_openstack/dash/instances/usage.html') - - self.assertEqual(res.context['usage'], TEST_RETURN) - - self.mox.VerifyAll() - - self.reset_times() - - def test_instance_console(self): - CONSOLE_OUTPUT = 'output' - INSTANCE_ID = self.servers[0].id - - console_mock = self.mox.CreateMock(api.Console) - console_mock.output = CONSOLE_OUTPUT - - self.mox.StubOutWithMock(api, 'console_create') - api.console_create(IgnoreArg(), - unicode(INSTANCE_ID), - IgnoreArg()).AndReturn(console_mock) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_instances_console', - args=[self.TEST_TENANT, INSTANCE_ID])) - - self.assertIsInstance(res, http.HttpResponse) - self.assertContains(res, CONSOLE_OUTPUT) - - self.mox.VerifyAll() - - def test_instance_console_exception(self): - INSTANCE_ID = self.servers[0].id - - exception = api_exceptions.ApiException('apiException', - message='apiException') - - self.mox.StubOutWithMock(api, 'console_create') - api.console_create(IgnoreArg(), - unicode(INSTANCE_ID), - IgnoreArg()).AndRaise(exception) - - self.mox.StubOutWithMock(messages, 'error') - messages.error(IgnoreArg(), IsA(unicode)) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_instances_console', - args=[self.TEST_TENANT, INSTANCE_ID])) - - self.assertRedirectsNoFollow(res, reverse('dash_instances', - args=[self.TEST_TENANT])) - - self.mox.VerifyAll() - - def test_instance_vnc(self): - INSTANCE_ID = self.servers[0].id - CONSOLE_OUTPUT = '/vncserver' - - console_mock = self.mox.CreateMock(api.Console) - console_mock.output = CONSOLE_OUTPUT - - self.mox.StubOutWithMock(api, 'console_create') - self.mox.StubOutWithMock(api, 'server_get') - api.server_get(IsA(http.HttpRequest), - str(self.servers[0].id)).AndReturn(self.servers[0]) - api.console_create(IgnoreArg(), - unicode(INSTANCE_ID), - 'vnc').AndReturn(console_mock) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_instances_vnc', - args=[self.TEST_TENANT, INSTANCE_ID])) - - self.assertRedirectsNoFollow(res, - CONSOLE_OUTPUT + '&title=serverName(1)') - - self.mox.VerifyAll() - - def test_instance_vnc_exception(self): - INSTANCE_ID = self.servers[0].id - - exception = api_exceptions.ApiException('apiException', - message='apiException') - - self.mox.StubOutWithMock(api, 'console_create') - api.console_create(IsA(http.HttpRequest), - unicode(INSTANCE_ID), - 'vnc').AndRaise(exception) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_instances_vnc', - args=[self.TEST_TENANT, INSTANCE_ID])) - - self.assertRedirectsNoFollow(res, reverse('dash_instances', - args=[self.TEST_TENANT])) - - self.mox.VerifyAll() - - def test_instance_update_get(self): - INSTANCE_ID = self.servers[0].id - - self.mox.StubOutWithMock(api, 'server_get') - api.server_get(IsA(http.HttpRequest), - unicode(INSTANCE_ID)).AndReturn(self.servers[0]) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_instances_update', - args=[self.TEST_TENANT, INSTANCE_ID])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/instances/update.html') - - self.mox.VerifyAll() - - def test_instance_update_get_server_get_exception(self): - INSTANCE_ID = self.servers[0].id - - exception = api_exceptions.ApiException('apiException') - self.mox.StubOutWithMock(api, 'server_get') - api.server_get(IsA(http.HttpRequest), - unicode(INSTANCE_ID)).AndRaise(exception) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_instances_update', - args=[self.TEST_TENANT, INSTANCE_ID])) - - self.assertRedirectsNoFollow(res, reverse('dash_instances', - args=[self.TEST_TENANT])) - - self.mox.VerifyAll() - - def test_instance_update_post(self): - INSTANCE_ID = self.servers[0].id - NAME = 'myname' - DESC = 'mydesc' - formData = {'method': 'UpdateInstance', - 'instance': self.servers[0].id, - 'name': NAME, - 'tenant_id': self.TEST_TENANT, - 'description': DESC} - - self.mox.StubOutWithMock(api, 'server_get') - api.server_get(IsA(http.HttpRequest), - unicode(INSTANCE_ID)).AndReturn(self.servers[0]) - - self.mox.StubOutWithMock(api, 'server_update') - api.server_update(IsA(http.HttpRequest), - str(INSTANCE_ID), NAME, DESC) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_instances_update', - args=[self.TEST_TENANT, - INSTANCE_ID]), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_instances', - args=[self.TEST_TENANT])) - - self.mox.VerifyAll() - - def test_instance_update_post_api_exception(self): - INSTANCE_ID = self.servers[0].id - NAME = 'myname' - DESC = 'mydesc' - formData = {'method': 'UpdateInstance', - 'instance': INSTANCE_ID, - 'name': NAME, - 'tenant_id': self.TEST_TENANT, - 'description': DESC} - - self.mox.StubOutWithMock(api, 'server_get') - api.server_get(IsA(http.HttpRequest), - unicode(INSTANCE_ID)).AndReturn(self.servers[0]) - - exception = api_exceptions.ApiException('apiException') - self.mox.StubOutWithMock(api, 'server_update') - api.server_update(IsA(http.HttpRequest), - str(INSTANCE_ID), NAME, DESC).\ - AndRaise(exception) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_instances_update', - args=[self.TEST_TENANT, - INSTANCE_ID]), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_instances', - args=[self.TEST_TENANT])) - - self.mox.VerifyAll() diff --git a/django-openstack/django_openstack/tests/view_tests/dash/keypair_tests.py b/django-openstack/django_openstack/tests/view_tests/dash/keypair_tests.py deleted file mode 100644 index 45c090c9..00000000 --- a/django-openstack/django_openstack/tests/view_tests/dash/keypair_tests.py +++ /dev/null @@ -1,171 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from django import http -from django.contrib import messages -from django.core.urlresolvers import reverse -from django_openstack import api -from django_openstack.tests.view_tests import base -from mox import IsA - -from novaclient import exceptions as novaclient_exceptions - - -class KeyPairViewTests(base.BaseViewTests): - def setUp(self): - super(KeyPairViewTests, self).setUp() - keypair = self.mox.CreateMock(api.KeyPair) - keypair.name = 'keyName' - self.keypairs = (keypair,) - - def test_index(self): - self.mox.StubOutWithMock(api, 'keypair_list') - api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_keypairs', - args=[self.TEST_TENANT])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/keypairs/index.html') - self.assertItemsEqual(res.context['keypairs'], self.keypairs) - - self.mox.VerifyAll() - - def test_index_exception(self): - exception = novaclient_exceptions.ClientException('clientException', - message='clientException') - self.mox.StubOutWithMock(api, 'keypair_list') - api.keypair_list(IsA(http.HttpRequest)).AndRaise(exception) - - self.mox.StubOutWithMock(messages, 'error') - messages.error(IsA(http.HttpRequest), IsA(basestring)) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_keypairs', - args=[self.TEST_TENANT])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/keypairs/index.html') - self.assertEqual(len(res.context['keypairs']), 0) - - self.mox.VerifyAll() - - def test_delete_keypair(self): - KEYPAIR_ID = self.keypairs[0].name - formData = {'method': 'DeleteKeypair', - 'keypair_id': KEYPAIR_ID, - } - - self.mox.StubOutWithMock(api, 'keypair_delete') - api.keypair_delete(IsA(http.HttpRequest), unicode(KEYPAIR_ID)) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_keypairs', - args=[self.TEST_TENANT]), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_keypairs', - args=[self.TEST_TENANT])) - - self.mox.VerifyAll() - - def test_delete_keypair_exception(self): - KEYPAIR_ID = self.keypairs[0].name - formData = {'method': 'DeleteKeypair', - 'keypair_id': KEYPAIR_ID, - } - - exception = novaclient_exceptions.ClientException('clientException', - message='clientException') - self.mox.StubOutWithMock(api, 'keypair_delete') - api.keypair_delete(IsA(http.HttpRequest), - unicode(KEYPAIR_ID)).AndRaise(exception) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_keypairs', - args=[self.TEST_TENANT]), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_keypairs', - args=[self.TEST_TENANT])) - - self.mox.VerifyAll() - - def test_create_keypair_get(self): - res = self.client.get(reverse('dash_keypairs_create', - args=[self.TEST_TENANT])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/keypairs/create.html') - - def test_create_keypair_post(self): - KEYPAIR_NAME = 'newKeypair' - PRIVATE_KEY = 'privateKey' - - newKeyPair = self.mox.CreateMock(api.KeyPair) - newKeyPair.name = KEYPAIR_NAME - newKeyPair.private_key = PRIVATE_KEY - - formData = {'method': 'CreateKeypair', - 'name': KEYPAIR_NAME, - } - - self.mox.StubOutWithMock(api, 'keypair_create') - api.keypair_create(IsA(http.HttpRequest), - KEYPAIR_NAME).AndReturn(newKeyPair) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_keypairs_create', - args=[self.TEST_TENANT]), - formData) - - self.assertTrue(res.has_header('Content-Disposition')) - - self.mox.VerifyAll() - - def test_create_keypair_exception(self): - KEYPAIR_NAME = 'newKeypair' - - formData = {'method': 'CreateKeypair', - 'name': KEYPAIR_NAME, - } - - exception = novaclient_exceptions.ClientException('clientException', - message='clientException') - self.mox.StubOutWithMock(api, 'keypair_create') - api.keypair_create(IsA(http.HttpRequest), - KEYPAIR_NAME).AndRaise(exception) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_keypairs_create', - args=[self.TEST_TENANT]), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_keypairs_create', - args=[self.TEST_TENANT])) - - self.mox.VerifyAll() diff --git a/django-openstack/django_openstack/tests/view_tests/dash/network_tests.py b/django-openstack/django_openstack/tests/view_tests/dash/network_tests.py deleted file mode 100644 index ebd17046..00000000 --- a/django-openstack/django_openstack/tests/view_tests/dash/network_tests.py +++ /dev/null @@ -1,198 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from django import http -from django.contrib import messages -from django.core.urlresolvers import reverse -from django_openstack import api -from django_openstack.tests.view_tests import base -from mox import IgnoreArg, IsA -import quantum.client - - -class NetworkViewTests(base.BaseViewTests): - def setUp(self): - super(NetworkViewTests, self).setUp() - self.network = {} - self.network['networks'] = [] - self.network['networks'].append({'id': 'n1'}) - self.network_details = {'network': {'name': 'test_network'}} - self.ports = {} - self.ports['ports'] = [] - self.ports['ports'].append({'id': 'p1'}) - self.port_details = { - 'port': { - 'id': 'p1', - 'state': 'DOWN' - } - } - self.port_attachment = { - 'attachment': { - 'id': 'vif1' - } - } - self.vifs = [{'id': 'vif1'}] - - def test_network_index(self): - self.mox.StubOutWithMock(api, 'quantum_list_networks') - api.quantum_list_networks(IsA(http.HttpRequest)).\ - AndReturn(self.network) - - self.mox.StubOutWithMock(api, 'quantum_network_details') - api.quantum_network_details(IsA(http.HttpRequest), - 'n1').AndReturn(self.network_details) - - self.mox.StubOutWithMock(api, 'quantum_list_ports') - api.quantum_list_ports(IsA(http.HttpRequest), - 'n1').AndReturn(self.ports) - - self.mox.StubOutWithMock(api, 'quantum_port_attachment') - api.quantum_port_attachment(IsA(http.HttpRequest), - 'n1', 'p1').AndReturn(self.port_attachment) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_networks', args=['tenant'])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/networks/index.html') - self.assertIn('networks', res.context) - networks = res.context['networks'] - - self.assertEqual(len(networks), 1) - self.assertEqual(networks[0]['name'], 'test_network') - self.assertEqual(networks[0]['id'], 'n1') - self.assertEqual(networks[0]['total'], 1) - self.assertEqual(networks[0]['used'], 1) - self.assertEqual(networks[0]['available'], 0) - - self.mox.VerifyAll() - - def test_network_create(self): - self.mox.StubOutWithMock(api, "quantum_create_network") - api.quantum_create_network(IsA(http.HttpRequest), dict).AndReturn(True) - - self.mox.ReplayAll() - - formData = {'name': 'Test', - 'method': 'CreateNetwork'} - - res = self.client.post(reverse('dash_network_create', - args=[self.request.user.tenant_id]), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_networks', - args=[self.request.user.tenant_id])) - self.mox.VerifyAll() - - def test_network_delete(self): - self.mox.StubOutWithMock(api, "quantum_delete_network") - api.quantum_delete_network(IsA(http.HttpRequest), 'n1').AndReturn(True) - - self.mox.StubOutWithMock(api, 'quantum_list_networks') - api.quantum_list_networks(IsA(http.HttpRequest)).\ - AndReturn(self.network) - - self.mox.StubOutWithMock(api, 'quantum_network_details') - api.quantum_network_details(IsA(http.HttpRequest), - 'n1').AndReturn(self.network_details) - - self.mox.StubOutWithMock(api, 'quantum_list_ports') - api.quantum_list_ports(IsA(http.HttpRequest), - 'n1').AndReturn(self.ports) - - self.mox.StubOutWithMock(api, 'quantum_port_attachment') - api.quantum_port_attachment(IsA(http.HttpRequest), - 'n1', 'p1').AndReturn(self.port_attachment) - - self.mox.ReplayAll() - - formData = {'id': 'n1', - 'method': 'DeleteNetwork'} - - res = self.client.post(reverse('dash_networks', - args=[self.request.user.tenant_id]), - formData) - - def test_network_rename(self): - self.mox.StubOutWithMock(api, "quantum_update_network") - api.quantum_update_network(IsA(http.HttpRequest), - 'n1', dict).AndReturn(True) - - self.mox.StubOutWithMock(api, 'quantum_list_networks') - api.quantum_list_networks(IsA(http.HttpRequest)).\ - AndReturn(self.network) - - self.mox.StubOutWithMock(api, 'quantum_network_details') - api.quantum_network_details(IsA(http.HttpRequest), - 'n1').AndReturn(self.network_details) - - self.mox.StubOutWithMock(api, 'quantum_list_ports') - api.quantum_list_ports(IsA(http.HttpRequest), - 'n1').AndReturn(self.ports) - - self.mox.StubOutWithMock(api, 'quantum_port_attachment') - api.quantum_port_attachment(IsA(http.HttpRequest), - 'n1', 'p1').AndReturn(self.port_attachment) - - self.mox.ReplayAll() - - formData = {'new_name': 'Test1', - 'method': 'RenameNetwork'} - - res = self.client.post(reverse('dash_network_rename', - args=[self.request.user.tenant_id, "n1"]), - formData) - - def test_network_details(self): - self.mox.StubOutWithMock(api, 'quantum_network_details') - api.quantum_network_details(IsA(http.HttpRequest), - 'n1').AndReturn(self.network_details) - - self.mox.StubOutWithMock(api, 'quantum_list_ports') - api.quantum_list_ports(IsA(http.HttpRequest), - 'n1').AndReturn(self.ports) - - self.mox.StubOutWithMock(api, 'quantum_port_attachment') - api.quantum_port_attachment(IsA(http.HttpRequest), - 'n1', 'p1').AndReturn(self.port_attachment) - - self.mox.StubOutWithMock(api, 'quantum_port_details') - api.quantum_port_details(IsA(http.HttpRequest), - 'n1', 'p1').AndReturn(self.port_details) - - self.mox.StubOutWithMock(api, 'get_vif_ids') - api.get_vif_ids(IsA(http.HttpRequest)).AndReturn(self.vifs) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_networks_detail', - args=['tenant', 'n1'])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/networks/detail.html') - self.assertIn('network', res.context) - - network = res.context['network'] - - self.assertEqual(network['name'], 'test_network') - self.assertEqual(network['id'], 'n1') - - self.mox.VerifyAll() diff --git a/django-openstack/django_openstack/tests/view_tests/dash/object_tests.py b/django-openstack/django_openstack/tests/view_tests/dash/object_tests.py deleted file mode 100644 index 0314394e..00000000 --- a/django-openstack/django_openstack/tests/view_tests/dash/object_tests.py +++ /dev/null @@ -1,228 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import tempfile - -from django import http -from django.core.urlresolvers import reverse -from django_openstack import api -from django_openstack.tests.view_tests import base -from mox import IsA - - -class ObjectViewTests(base.BaseViewTests): - CONTAINER_NAME = 'containerName' - - def setUp(self): - super(ObjectViewTests, self).setUp() - swift_object = self.mox.CreateMock(api.SwiftObject) - self.swift_objects = [swift_object] - - def test_index(self): - self.mox.StubOutWithMock(api, 'swift_get_objects') - api.swift_get_objects( - IsA(http.HttpRequest), - self.CONTAINER_NAME, - marker=None).AndReturn(self.swift_objects) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_objects', - args=[self.TEST_TENANT, - self.CONTAINER_NAME])) - self.assertTemplateUsed(res, - 'django_openstack/dash/objects/index.html') - self.assertItemsEqual(res.context['objects'], self.swift_objects) - - self.mox.VerifyAll() - - def test_upload_index(self): - res = self.client.get(reverse('dash_objects_upload', - args=[self.TEST_TENANT, - self.CONTAINER_NAME])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/objects/upload.html') - - def test_upload(self): - OBJECT_DATA = 'objectData' - OBJECT_FILE = tempfile.TemporaryFile() - OBJECT_FILE.write(OBJECT_DATA) - OBJECT_FILE.flush() - OBJECT_FILE.seek(0) - OBJECT_NAME = 'objectName' - - formData = {'method': 'UploadObject', - 'container_name': self.CONTAINER_NAME, - 'name': OBJECT_NAME, - 'object_file': OBJECT_FILE} - - self.mox.StubOutWithMock(api, 'swift_upload_object') - api.swift_upload_object(IsA(http.HttpRequest), - unicode(self.CONTAINER_NAME), - unicode(OBJECT_NAME), - OBJECT_DATA) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_objects_upload', - args=[self.TEST_TENANT, - self.CONTAINER_NAME]), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_objects_upload', - args=[self.TEST_TENANT, - self.CONTAINER_NAME])) - - self.mox.VerifyAll() - - def test_delete(self): - OBJECT_NAME = 'objectName' - formData = {'method': 'DeleteObject', - 'container_name': self.CONTAINER_NAME, - 'object_name': OBJECT_NAME} - - self.mox.StubOutWithMock(api, 'swift_delete_object') - api.swift_delete_object( - IsA(http.HttpRequest), - self.CONTAINER_NAME, OBJECT_NAME) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_objects', - args=[self.TEST_TENANT, - self.CONTAINER_NAME]), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_objects', - args=[self.TEST_TENANT, - self.CONTAINER_NAME])) - - self.mox.VerifyAll() - - def test_download(self): - OBJECT_DATA = 'objectData' - OBJECT_NAME = 'objectName' - - self.mox.StubOutWithMock(api, 'swift_get_object_data') - api.swift_get_object_data(IsA(http.HttpRequest), - unicode(self.CONTAINER_NAME), - unicode(OBJECT_NAME)).AndReturn(OBJECT_DATA) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_objects_download', - args=[self.TEST_TENANT, - self.CONTAINER_NAME, - OBJECT_NAME])) - - self.assertEqual(res.content, OBJECT_DATA) - self.assertTrue(res.has_header('Content-Disposition')) - - self.mox.VerifyAll() - - def test_copy_index(self): - OBJECT_NAME = 'objectName' - - container = self.mox.CreateMock(api.Container) - container.name = self.CONTAINER_NAME - - self.mox.StubOutWithMock(api, 'swift_get_containers') - api.swift_get_containers( - IsA(http.HttpRequest)).AndReturn([container]) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_object_copy', - args=[self.TEST_TENANT, - self.CONTAINER_NAME, - OBJECT_NAME])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/objects/copy.html') - - self.mox.VerifyAll() - - def test_copy(self): - NEW_CONTAINER_NAME = self.CONTAINER_NAME - NEW_OBJECT_NAME = 'newObjectName' - ORIG_CONTAINER_NAME = 'origContainerName' - ORIG_OBJECT_NAME = 'origObjectName' - - formData = {'method': 'CopyObject', - 'new_container_name': NEW_CONTAINER_NAME, - 'new_object_name': NEW_OBJECT_NAME, - 'orig_container_name': ORIG_CONTAINER_NAME, - 'orig_object_name': ORIG_OBJECT_NAME} - - container = self.mox.CreateMock(api.Container) - container.name = self.CONTAINER_NAME - - self.mox.StubOutWithMock(api, 'swift_get_containers') - api.swift_get_containers( - IsA(http.HttpRequest)).AndReturn([container]) - - self.mox.StubOutWithMock(api, 'swift_copy_object') - api.swift_copy_object(IsA(http.HttpRequest), - ORIG_CONTAINER_NAME, - ORIG_OBJECT_NAME, - NEW_CONTAINER_NAME, - NEW_OBJECT_NAME) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_object_copy', - args=[self.TEST_TENANT, - ORIG_CONTAINER_NAME, - ORIG_OBJECT_NAME]), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_object_copy', - args=[self.TEST_TENANT, - ORIG_CONTAINER_NAME, - ORIG_OBJECT_NAME])) - - self.mox.VerifyAll() - - def test_filter(self): - PREFIX = 'prefix' - - formData = {'method': 'FilterObjects', - 'container_name': self.CONTAINER_NAME, - 'object_prefix': PREFIX, - } - - self.mox.StubOutWithMock(api, 'swift_get_objects') - api.swift_get_objects(IsA(http.HttpRequest), - unicode(self.CONTAINER_NAME), - prefix=unicode(PREFIX) - ).AndReturn(self.swift_objects) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_objects', - args=[self.TEST_TENANT, - self.CONTAINER_NAME]), - formData) - - self.assertTemplateUsed(res, - 'django_openstack/dash/objects/index.html') - - self.mox.VerifyAll() diff --git a/django-openstack/django_openstack/tests/view_tests/dash/port_tests.py b/django-openstack/django_openstack/tests/view_tests/dash/port_tests.py deleted file mode 100644 index 5d5d827d..00000000 --- a/django-openstack/django_openstack/tests/view_tests/dash/port_tests.py +++ /dev/null @@ -1,104 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from django import http -from django.contrib import messages -from django.core.urlresolvers import reverse -from django_openstack import api -from django_openstack.tests.view_tests import base -from mox import IgnoreArg, IsA -import quantum.client - - -class PortViewTests(base.BaseViewTests): - def setUp(self): - super(PortViewTests, self).setUp() - - def test_port_create(self): - self.mox.StubOutWithMock(api, "quantum_create_port") - api.quantum_create_port(IsA(http.HttpRequest), 'n1').AndReturn(True) - - formData = {'ports_num': 1, - 'network': 'n1', - 'method': 'CreatePort'} - - self.mox.StubOutWithMock(messages, 'success') - messages.success(IgnoreArg(), IsA(basestring)) - - res = self.client.post(reverse('dash_ports_create', - args=[self.request.user.tenant_id, "n1"]), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_networks_detail', - args=[self.request.user.tenant_id, - "n1"])) - - def test_port_delete(self): - self.mox.StubOutWithMock(api, "quantum_delete_port") - api.quantum_delete_port(IsA(http.HttpRequest), - 'n1', 'p1').AndReturn(True) - - formData = {'port': 'p1', - 'network': 'n1', - 'method': 'DeletePort'} - - self.mox.StubOutWithMock(messages, 'success') - messages.success(IgnoreArg(), IsA(basestring)) - - res = self.client.post(reverse('dash_networks_detail', - args=[self.request.user.tenant_id, "n1"]), - formData) - - def test_port_attach(self): - self.mox.StubOutWithMock(api, "quantum_attach_port") - api.quantum_attach_port(IsA(http.HttpRequest), - 'n1', 'p1', dict).AndReturn(True) - - formData = {'port': 'p1', - 'network': 'n1', - 'vif_id': 'v1', - 'method': 'AttachPort'} - - self.mox.StubOutWithMock(messages, 'success') - messages.success(IgnoreArg(), IsA(basestring)) - - res = self.client.post(reverse('dash_ports_attach', - args=[self.request.user.tenant_id, "n1", "p1"]), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_networks_detail', - args=[self.request.user.tenant_id, - "n1"])) - - def test_port_detach(self): - self.mox.StubOutWithMock(api, "quantum_detach_port") - api.quantum_detach_port(IsA(http.HttpRequest), - 'n1', 'p1').AndReturn(True) - - formData = {'port': 'p1', - 'network': 'n1', - 'method': 'DetachPort'} - - self.mox.StubOutWithMock(messages, 'success') - messages.success(IgnoreArg(), IsA(basestring)) - - res = self.client.post(reverse('dash_networks_detail', - args=[self.request.user.tenant_id, "n1"]), - formData) diff --git a/django-openstack/django_openstack/tests/view_tests/dash/security_groups_tests.py b/django-openstack/django_openstack/tests/view_tests/dash/security_groups_tests.py deleted file mode 100644 index 7d7bdee2..00000000 --- a/django-openstack/django_openstack/tests/view_tests/dash/security_groups_tests.py +++ /dev/null @@ -1,372 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from django import http -from django.contrib import messages -from django.core.urlresolvers import reverse -from django_openstack import api -from django_openstack.tests.view_tests import base -from glance.common import exception as glance_exception -from openstackx.api import exceptions as api_exceptions -from novaclient import exceptions as novaclient_exceptions -from mox import IgnoreArg, IsA - - -class SecurityGroupsViewTests(base.BaseViewTests): - def setUp(self): - super(SecurityGroupsViewTests, self).setUp() - - security_group = self.mox.CreateMock(api.SecurityGroup) - security_group.name = 'default' - self.security_groups = (security_group,) - - def test_index(self): - self.mox.StubOutWithMock(api, 'security_group_list') - api.security_group_list(IsA(http.HttpRequest)).\ - AndReturn(self.security_groups) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_security_groups', - args=[self.TEST_TENANT])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/security_groups/index.html') - self.assertItemsEqual(res.context['security_groups'], - self.security_groups) - - self.mox.VerifyAll() - - def test_index_exception(self): - exception = novaclient_exceptions.ClientException('ClientException', - message='ClientException') - self.mox.StubOutWithMock(api, 'security_group_list') - api.security_group_list(IsA(http.HttpRequest)).AndRaise(exception) - - self.mox.StubOutWithMock(messages, 'error') - messages.error(IsA(http.HttpRequest), IsA(basestring)) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_security_groups', - args=[self.TEST_TENANT])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/security_groups/index.html') - self.assertEqual(len(res.context['security_groups']), 0) - - self.mox.VerifyAll() - - def test_create_security_groups_get(self): - res = self.client.get(reverse('dash_security_groups_create', - args=[self.TEST_TENANT])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/security_groups/create.html') - - def test_create_security_groups_post(self): - SECGROUP_NAME = 'fakegroup' - SECGROUP_DESC = 'fakegroup_desc' - - new_group = self.mox.CreateMock(api.SecurityGroup) - new_group.name = SECGROUP_NAME - - formData = {'method': 'CreateGroup', - 'tenant_id': self.TEST_TENANT, - 'name': SECGROUP_NAME, - 'description': SECGROUP_DESC, - } - - self.mox.StubOutWithMock(api, 'security_group_create') - api.security_group_create(IsA(http.HttpRequest), - SECGROUP_NAME, SECGROUP_DESC).AndReturn(new_group) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_security_groups_create', - args=[self.TEST_TENANT]), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_security_groups', - args=[self.TEST_TENANT])) - - self.mox.VerifyAll() - - def test_create_security_groups_post_exception(self): - SECGROUP_NAME = 'fakegroup' - SECGROUP_DESC = 'fakegroup_desc' - - exception = novaclient_exceptions.ClientException('ClientException', - message='ClientException') - - formData = {'method': 'CreateGroup', - 'tenant_id': self.TEST_TENANT, - 'name': SECGROUP_NAME, - 'description': SECGROUP_DESC, - } - - self.mox.StubOutWithMock(api, 'security_group_create') - api.security_group_create(IsA(http.HttpRequest), - SECGROUP_NAME, SECGROUP_DESC).AndRaise(exception) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_security_groups_create', - args=[self.TEST_TENANT]), - formData) - - self.assertTemplateUsed(res, - 'django_openstack/dash/security_groups/create.html') - - self.mox.VerifyAll() - - def test_edit_rules_get(self): - SECGROUP_ID = '1' - - self.mox.StubOutWithMock(api, 'security_group_get') - api.security_group_get(IsA(http.HttpRequest), SECGROUP_ID).AndReturn( - self.security_groups[0]) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_security_groups_edit_rules', - args=[self.TEST_TENANT, SECGROUP_ID])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/security_groups/edit_rules.html') - self.assertItemsEqual(res.context['security_group'].name, - self.security_groups[0].name) - - self.mox.VerifyAll() - - def test_edit_rules_get_exception(self): - SECGROUP_ID = '1' - - exception = novaclient_exceptions.ClientException('ClientException', - message='ClientException') - - self.mox.StubOutWithMock(api, 'security_group_get') - api.security_group_get(IsA(http.HttpRequest), SECGROUP_ID).AndRaise( - exception) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_security_groups_edit_rules', - args=[self.TEST_TENANT, SECGROUP_ID])) - - self.assertRedirectsNoFollow(res, reverse('dash_security_groups', - args=[self.TEST_TENANT])) - - self.mox.VerifyAll() - - def test_edit_rules_add_rule(self): - SECGROUP_ID = '1' - RULE_ID = '1' - FROM_PORT = '-1' - TO_PORT = '-1' - IP_PROTOCOL = 'icmp' - CIDR = '0.0.0.0/0' - - new_rule = self.mox.CreateMock(api.SecurityGroup) - new_rule.from_port = FROM_PORT - new_rule.to_port = TO_PORT - new_rule.ip_protocol = IP_PROTOCOL - new_rule.cidr = CIDR - new_rule.security_group_id = SECGROUP_ID - new_rule.id = RULE_ID - - formData = {'method': 'AddRule', - 'tenant_id': self.TEST_TENANT, - 'security_group_id': SECGROUP_ID, - 'from_port': FROM_PORT, - 'to_port': TO_PORT, - 'ip_protocol': IP_PROTOCOL, - 'cidr': CIDR} - - self.mox.StubOutWithMock(api, 'security_group_rule_create') - api.security_group_rule_create(IsA(http.HttpRequest), - SECGROUP_ID, IP_PROTOCOL, FROM_PORT, TO_PORT, CIDR)\ - .AndReturn(new_rule) - - self.mox.StubOutWithMock(messages, 'info') - messages.info(IsA(http.HttpRequest), IsA(basestring)) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_security_groups_edit_rules', - args=[self.TEST_TENANT, SECGROUP_ID]), - formData) - - self.assertRedirectsNoFollow(res, - reverse('dash_security_groups_edit_rules', - args=[self.TEST_TENANT, SECGROUP_ID])) - - self.mox.VerifyAll() - - def test_edit_rules_add_rule_exception(self): - exception = novaclient_exceptions.ClientException('ClientException', - message='ClientException') - - SECGROUP_ID = '1' - RULE_ID = '1' - FROM_PORT = '-1' - TO_PORT = '-1' - IP_PROTOCOL = 'icmp' - CIDR = '0.0.0.0/0' - - formData = {'method': 'AddRule', - 'tenant_id': self.TEST_TENANT, - 'security_group_id': SECGROUP_ID, - 'from_port': FROM_PORT, - 'to_port': TO_PORT, - 'ip_protocol': IP_PROTOCOL, - 'cidr': CIDR} - - self.mox.StubOutWithMock(api, 'security_group_rule_create') - api.security_group_rule_create(IsA(http.HttpRequest), - SECGROUP_ID, IP_PROTOCOL, FROM_PORT, - TO_PORT, CIDR).AndRaise(exception) - - self.mox.StubOutWithMock(messages, 'error') - messages.error(IsA(http.HttpRequest), IsA(basestring)) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_security_groups_edit_rules', - args=[self.TEST_TENANT, SECGROUP_ID]), - formData) - - self.assertRedirectsNoFollow(res, - reverse('dash_security_groups_edit_rules', - args=[self.TEST_TENANT, SECGROUP_ID])) - - self.mox.VerifyAll() - - def test_edit_rules_delete_rule(self): - SECGROUP_ID = '1' - RULE_ID = '1' - - formData = {'method': 'DeleteRule', - 'tenant_id': self.TEST_TENANT, - 'security_group_rule_id': RULE_ID, - } - - self.mox.StubOutWithMock(api, 'security_group_rule_delete') - api.security_group_rule_delete(IsA(http.HttpRequest), RULE_ID) - - self.mox.StubOutWithMock(messages, 'info') - messages.info(IsA(http.HttpRequest), IsA(unicode)) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_security_groups_edit_rules', - args=[self.TEST_TENANT, SECGROUP_ID]), - formData) - - self.assertRedirectsNoFollow(res, - reverse('dash_security_groups_edit_rules', - args=[self.TEST_TENANT, SECGROUP_ID])) - - self.mox.VerifyAll() - - def test_edit_rules_delete_rule_exception(self): - exception = novaclient_exceptions.ClientException('ClientException', - message='ClientException') - - SECGROUP_ID = '1' - RULE_ID = '1' - - formData = {'method': 'DeleteRule', - 'tenant_id': self.TEST_TENANT, - 'security_group_rule_id': RULE_ID, - } - - self.mox.StubOutWithMock(api, 'security_group_rule_delete') - api.security_group_rule_delete(IsA(http.HttpRequest), RULE_ID).\ - AndRaise(exception) - - self.mox.StubOutWithMock(messages, 'error') - messages.error(IsA(http.HttpRequest), IsA(basestring)) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_security_groups_edit_rules', - args=[self.TEST_TENANT, SECGROUP_ID]), - formData) - - self.assertRedirectsNoFollow(res, - reverse('dash_security_groups_edit_rules', - args=[self.TEST_TENANT, SECGROUP_ID])) - - self.mox.VerifyAll() - - def test_delete_group(self): - SECGROUP_ID = '1' - - formData = {'method': 'DeleteGroup', - 'tenant_id': self.TEST_TENANT, - 'security_group_id': SECGROUP_ID, - } - - self.mox.StubOutWithMock(api, 'security_group_delete') - api.security_group_delete(IsA(http.HttpRequest), SECGROUP_ID) - - self.mox.StubOutWithMock(messages, 'info') - messages.info(IsA(http.HttpRequest), IsA(unicode)) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_security_groups', - args=[self.TEST_TENANT]), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_security_groups', - args=[self.TEST_TENANT])) - - self.mox.VerifyAll() - - def test_delete_group_exception(self): - exception = novaclient_exceptions.ClientException('ClientException', - message='ClientException') - - SECGROUP_ID = '1' - - formData = {'method': 'DeleteGroup', - 'tenant_id': self.TEST_TENANT, - 'security_group_id': SECGROUP_ID, - } - - self.mox.StubOutWithMock(api, 'security_group_delete') - api.security_group_delete(IsA(http.HttpRequest), SECGROUP_ID).\ - AndRaise(exception) - - self.mox.StubOutWithMock(messages, 'error') - messages.error(IsA(http.HttpRequest), IsA(basestring)) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_security_groups', - args=[self.TEST_TENANT]), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_security_groups', - args=[self.TEST_TENANT])) - - self.mox.VerifyAll() diff --git a/django-openstack/django_openstack/tests/view_tests/dash/snapshots_tests.py b/django-openstack/django_openstack/tests/view_tests/dash/snapshots_tests.py deleted file mode 100644 index 7317ef73..00000000 --- a/django-openstack/django_openstack/tests/view_tests/dash/snapshots_tests.py +++ /dev/null @@ -1,213 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from django import http -from django.contrib import messages -from django.core.urlresolvers import reverse -from django_openstack import api -from django_openstack.tests.view_tests import base -from glance.common import exception as glance_exception -from openstackx.api import exceptions as api_exceptions -from mox import IgnoreArg, IsA - - -class SnapshotsViewTests(base.BaseViewTests): - def setUp(self): - super(SnapshotsViewTests, self).setUp() - image_dict = {'name': 'snapshot', - 'container_format': 'novaImage'} - self.images = [image_dict] - - server = self.mox.CreateMock(api.Server) - server.id = 1 - server.status = 'ACTIVE' - server.name = 'sgoody' - self.good_server = server - - server = self.mox.CreateMock(api.Server) - server.id = 2 - server.status = 'BUILD' - server.name = 'baddy' - self.bad_server = server - - def test_index(self): - self.mox.StubOutWithMock(api, 'snapshot_list_detailed') - api.snapshot_list_detailed(IsA(http.HttpRequest)).\ - AndReturn(self.images) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_snapshots', - args=[self.TEST_TENANT])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/snapshots/index.html') - - self.assertIn('images', res.context) - images = res.context['images'] - self.assertEqual(len(images), 1) - - self.mox.VerifyAll() - - def test_index_client_conn_error(self): - self.mox.StubOutWithMock(api, 'snapshot_list_detailed') - exception = glance_exception.ClientConnectionError('clientConnError') - api.snapshot_list_detailed(IsA(http.HttpRequest)).AndRaise(exception) - - self.mox.StubOutWithMock(messages, 'error') - messages.error(IsA(http.HttpRequest), IsA(basestring)) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_snapshots', - args=[self.TEST_TENANT])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/snapshots/index.html') - - self.mox.VerifyAll() - - def test_index_glance_error(self): - self.mox.StubOutWithMock(api, 'snapshot_list_detailed') - exception = glance_exception.Error('glanceError') - api.snapshot_list_detailed(IsA(http.HttpRequest)).AndRaise(exception) - - self.mox.StubOutWithMock(messages, 'error') - messages.error(IsA(http.HttpRequest), IsA(basestring)) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_snapshots', - args=[self.TEST_TENANT])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/snapshots/index.html') - - self.mox.VerifyAll() - - def test_create_snapshot_get(self): - self.mox.StubOutWithMock(api, 'server_get') - api.server_get(IsA(http.HttpRequest), - str(self.good_server.id)).AndReturn(self.good_server) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_snapshots_create', - args=[self.TEST_TENANT, - self.good_server.id])) - - self.assertTemplateUsed(res, - 'django_openstack/dash/snapshots/create.html') - self.mox.VerifyAll() - - def test_create_snapshot_get_with_invalid_status(self): - self.mox.StubOutWithMock(api, 'server_get') - api.server_get(IsA(http.HttpRequest), - str(self.bad_server.id)).AndReturn(self.bad_server) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_snapshots_create', - args=[self.TEST_TENANT, - self.bad_server.id])) - - self.assertRedirectsNoFollow(res, reverse('dash_instances', - args=[self.TEST_TENANT])) - self.mox.VerifyAll() - - def test_create_get_server_exception(self): - self.mox.StubOutWithMock(api, 'server_get') - exception = api_exceptions.ApiException('apiException') - api.server_get(IsA(http.HttpRequest), - str(self.good_server.id)).AndRaise(exception) - - self.mox.ReplayAll() - - res = self.client.get(reverse('dash_snapshots_create', - args=[self.TEST_TENANT, - self.good_server.id])) - - self.assertRedirectsNoFollow(res, reverse('dash_instances', - args=[self.TEST_TENANT])) - - self.mox.VerifyAll() - - def test_create_snapshot_post(self): - SNAPSHOT_NAME = 'snappy' - - new_snapshot = self.mox.CreateMock(api.Image) - new_snapshot.name = SNAPSHOT_NAME - - formData = {'method': 'CreateSnapshot', - 'tenant_id': self.TEST_TENANT, - 'instance_id': self.good_server.id, - 'name': SNAPSHOT_NAME} - - self.mox.StubOutWithMock(api, 'server_get') - api.server_get(IsA(http.HttpRequest), - str(self.good_server.id)).AndReturn(self.good_server) - - self.mox.StubOutWithMock(api, 'snapshot_create') - api.snapshot_create(IsA(http.HttpRequest), - str(self.good_server.id), SNAPSHOT_NAME).\ - AndReturn(new_snapshot) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_snapshots_create', - args=[self.TEST_TENANT, - self.good_server.id]), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_snapshots', - args=[self.TEST_TENANT])) - - self.mox.VerifyAll() - - def test_create_snapshot_post_exception(self): - SNAPSHOT_NAME = 'snappy' - - new_snapshot = self.mox.CreateMock(api.Image) - new_snapshot.name = SNAPSHOT_NAME - - formData = {'method': 'CreateSnapshot', - 'tenant_id': self.TEST_TENANT, - 'instance_id': self.good_server.id, - 'name': SNAPSHOT_NAME} - - self.mox.StubOutWithMock(api, 'snapshot_create') - exception = api_exceptions.ApiException('apiException', - message='apiException') - api.snapshot_create(IsA(http.HttpRequest), - str(self.good_server.id), SNAPSHOT_NAME).\ - AndRaise(exception) - - self.mox.ReplayAll() - - res = self.client.post(reverse('dash_snapshots_create', - args=[self.TEST_TENANT, - self.good_server.id]), - formData) - - self.assertRedirectsNoFollow(res, reverse('dash_snapshots_create', - args=[self.TEST_TENANT, - self.good_server.id])) - - self.mox.VerifyAll() diff --git a/django-openstack/django_openstack/tests/view_tests/syspanel/__init__.py b/django-openstack/django_openstack/tests/view_tests/syspanel/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/django-openstack/django_openstack/tests/view_tests/syspanel/users_tests.py b/django-openstack/django_openstack/tests/view_tests/syspanel/users_tests.py deleted file mode 100644 index 9c08d12d..00000000 --- a/django-openstack/django_openstack/tests/view_tests/syspanel/users_tests.py +++ /dev/null @@ -1,107 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from django.core.urlresolvers import reverse -from django_openstack import api -from django_openstack.tests.view_tests import base -from mox import IgnoreArg -from openstackx.api import exceptions as api_exceptions - - -class UsersViewTests(base.BaseViewTests): - def setUp(self): - super(UsersViewTests, self).setUp() - - self.user = self.mox.CreateMock(api.User) - self.user.enabled = True - self.user.id = self.TEST_USER - self.user.tenantId = self.TEST_TENANT - - self.users = [self.user] - - def test_index(self): - self.mox.StubOutWithMock(api, 'user_list') - api.user_list(IgnoreArg()).AndReturn(self.users) - - self.mox.ReplayAll() - - res = self.client.get(reverse('syspanel_users')) - - self.assertTemplateUsed(res, - 'django_openstack/syspanel/users/index.html') - self.assertItemsEqual(res.context['users'], self.users) - - self.mox.VerifyAll() - - def test_enable_user(self): - OTHER_USER = 'otherUser' - formData = {'method': 'UserEnableDisableForm', - 'id': OTHER_USER, - 'enabled': 'enable'} - - self.mox.StubOutWithMock(api, 'user_update_enabled') - api.user_update_enabled(IgnoreArg(), OTHER_USER, True).AndReturn( - self.mox.CreateMock(api.User)) - - self.mox.ReplayAll() - - res = self.client.post(reverse('syspanel_users'), formData) - - self.assertRedirectsNoFollow(res, reverse('syspanel_users')) - - self.mox.VerifyAll() - - def test_disable_user(self): - OTHER_USER = 'otherUser' - formData = {'method': 'UserEnableDisableForm', - 'id': OTHER_USER, - 'enabled': 'disable'} - - self.mox.StubOutWithMock(api, 'user_update_enabled') - api.user_update_enabled(IgnoreArg(), OTHER_USER, False).AndReturn( - self.mox.CreateMock(api.User)) - - self.mox.ReplayAll() - - res = self.client.post(reverse('syspanel_users'), formData) - - self.assertRedirectsNoFollow(res, reverse('syspanel_users')) - - self.mox.VerifyAll() - - def test_enable_disable_user_exception(self): - OTHER_USER = 'otherUser' - formData = {'method': 'UserEnableDisableForm', - 'id': OTHER_USER, - 'enabled': 'enable'} - - self.mox.StubOutWithMock(api, 'user_update_enabled') - api_exception = api_exceptions.ApiException('apiException', - message='apiException') - api.user_update_enabled(IgnoreArg(), - OTHER_USER, True).AndRaise(api_exception) - - self.mox.ReplayAll() - - res = self.client.post(reverse('syspanel_users'), formData) - - self.assertRedirectsNoFollow(res, reverse('syspanel_users')) - - self.mox.VerifyAll() diff --git a/django-openstack/django_openstack/tests/views.py b/django-openstack/django_openstack/tests/views.py deleted file mode 100644 index a8a1684a..00000000 --- a/django-openstack/django_openstack/tests/views.py +++ /dev/null @@ -1,31 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from django import http - - -def fakeView(request): - resp = http.HttpResponse() - resp.write('

' - 'This is a fake httpresponse from a fake view for testing ' - ' purposes only' - '

') - - return resp diff --git a/django-openstack/django_openstack/urls.py b/django-openstack/django_openstack/urls.py deleted file mode 100644 index d3b1f505..00000000 --- a/django-openstack/django_openstack/urls.py +++ /dev/null @@ -1,38 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from django.conf.urls.defaults import patterns, url, include -from django.conf import settings -from django_openstack.signals import * -from django.views.generic import TemplateView - -urlpatterns = patterns('', - url(r'^auth/', include('django_openstack.auth.urls')), - url(r'^dash/', include('django_openstack.dash.urls')), - url(r'^syspanel/', include('django_openstack.syspanel.urls')), - url(r'^settings/$', TemplateView.as_view( - template_name='django_openstack/dash/settings.html'), - name='dashboard_settings') - -) - -# import urls from modules -for module_urls in dash_modules_urls.send(sender=dash_modules_urls): - urlpatterns += module_urls[1].urlpatterns diff --git a/django-openstack/django_openstack/utils.py b/django-openstack/django_openstack/utils.py deleted file mode 100644 index 573e7be4..00000000 --- a/django-openstack/django_openstack/utils.py +++ /dev/null @@ -1,48 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import datetime - - -def time(): - '''Overrideable version of datetime.datetime.today''' - if time.override_time: - return time.override_time - return datetime.time() - -time.override_time = None - - -def today(): - '''Overridable version of datetime.datetime.today''' - if today.override_time: - return today.override_time - return datetime.datetime.today() - -today.override_time = None - - -def utcnow(): - '''Overridable version of datetime.datetime.utcnow''' - if utcnow.override_time: - return utcnow.override_time - return datetime.datetime.utcnow() - -utcnow.override_time = None diff --git a/django-openstack/django_openstack/version.py b/django-openstack/django_openstack/version.py deleted file mode 100644 index 999ae39f..00000000 --- a/django-openstack/django_openstack/version.py +++ /dev/null @@ -1,35 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC -# -# 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. - -version_info = {'branch_nick': u'LOCALBRANCH', - 'revision_id': 'LOCALREVISION', - 'revno': 0} - - -HORIZON_VERSION = ['2012', '1'] -YEAR, COUNT = HORIZON_VERSION -FINAL = False # This becomes true at Release Candidate time - - -def canonical_version_string(): - return '.'.join([YEAR, COUNT]) - - -def version_string(): - if FINAL: - return canonical_version_string() - else: - return '%s-dev' % (canonical_version_string(),) diff --git a/django-openstack/setup.py b/django-openstack/setup.py deleted file mode 100755 index 2e3f46c0..00000000 --- a/django-openstack/setup.py +++ /dev/null @@ -1,52 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import os -from setuptools import setup, find_packages, findall -from django_openstack import version - -def read(fname): - return open(os.path.join(os.path.dirname(__file__), fname)).read() - -setup( - name = "django-openstack", - version = version.canonical_version_string(), - url = 'https://github.com/openstack/horizon/', - license = 'Apache 2.0', - description = "A Django interface for OpenStack.", - long_description = read('README'), - author = 'Devin Carlen', - author_email = 'devin.carlen@gmail.com', - packages = find_packages(), - package_data = {'django_openstack': - [s[len('django_openstack/'):] for s in - findall('django_openstack/templates')]}, - install_requires = ['setuptools', 'mox>=0.5.3', 'django_nose'], - classifiers = [ - 'Development Status :: 4 - Beta', - 'Framework :: Django', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Topic :: Internet :: WWW/HTTP', - ] -) - diff --git a/doc/Makefile b/doc/Makefile deleted file mode 100644 index 986ad3df..00000000 --- a/doc/Makefile +++ /dev/null @@ -1,153 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = build - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Horizon.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Horizon.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/Horizon" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Horizon" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." diff --git a/doc/generate_autodoc_index.py b/doc/generate_autodoc_index.py deleted file mode 100755 index 8abbeaf8..00000000 --- a/doc/generate_autodoc_index.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env python -"""Generates files for sphinx documentation using a simple Autodoc based -template. - -To use, just run as a script: - $ python doc/generate_autodoc_index.py -""" - -import os - - -base_dir = os.path.dirname(os.path.abspath(__file__)) -RSTDIR = os.path.join(base_dir, "source", "sourcecode") -SRCS = {'dashboard': os.path.join(base_dir, "..", "openstack-dashboard"), - 'django_openstack': os.path.join(base_dir, "..", "django-openstack")} - - -def find_autodoc_modules(module_name, sourcedir): - """returns a list of modules in the SOURCE directory""" - modlist = [] - os.chdir(os.path.join(sourcedir, module_name)) - print "SEARCHING %s" % sourcedir - for root, dirs, files in os.walk("."): - for filename in files: - if filename.endswith(".py"): - # root = ./dashboard/test/unit - # filename = base.py - # remove the pieces of the root - elements = root.split(os.path.sep) - # replace the leading "." with the module name - elements[0] = module_name - # and get the base module name - base, extension = os.path.splitext(filename) - if not (base == "__init__"): - elements.append(base) - result = ".".join(elements) - #print result - modlist.append(result) - return modlist - -if not(os.path.exists(RSTDIR)): - os.mkdir(RSTDIR) - -INDEXOUT = open("%s/autoindex.rst" % RSTDIR, "w") -INDEXOUT.write("Source Code Index\n") -INDEXOUT.write("=================\n") -INDEXOUT.write(".. toctree::\n") -INDEXOUT.write(" :maxdepth: 1\n") -INDEXOUT.write("\n") - -for modulename in SRCS: - for module in find_autodoc_modules(modulename, SRCS[modulename]): - generated_file = "%s/%s.rst" % (RSTDIR, module) - print "Generating %s" % generated_file - - INDEXOUT.write(" %s\n" % module) - FILEOUT = open(generated_file, "w") - FILEOUT.write("The :mod:`%s` Module\n" % module) - FILEOUT.write("==============================" - "==============================" - "==============================\n") - FILEOUT.write(".. automodule:: %s\n" % module) - FILEOUT.write(" :members:\n") - FILEOUT.write(" :undoc-members:\n") - FILEOUT.write(" :show-inheritance:\n") - FILEOUT.close() - -INDEXOUT.close() diff --git a/doc/source/conf.py b/doc/source/conf.py deleted file mode 100644 index 164c6560..00000000 --- a/doc/source/conf.py +++ /dev/null @@ -1,301 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Horizon documentation build configuration file, created by -# sphinx-quickstart on Thu Oct 27 11:38:59 2011. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys, os -from django_openstack import version as horizon_version - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ----------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.pngmath', - 'sphinx.ext.viewcode'] - -# Add any paths that contain templates here, relative to this directory. -if os.getenv('HUDSON_PUBLISH_DOCS'): - templates_path = ['_ga', '_templates'] -else: - templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'Horizon' -copyright = u'2011, OpenStack, LLC' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = horizon_version.canonical_version_string() -# The full version, including alpha/beta/rc tags. -release = horizon_version.canonical_version_string() - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = [] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - - -# -- Options for HTML output --------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'default' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'Horizondoc' - - -# -- Options for LaTeX output -------------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ('index', 'Horizon.tex', u'Horizon Documentation', - u'OpenStack, LLC', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output -------------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'horizon', u'Horizon Documentation', - [u'OpenStack'], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------------ - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'Horizon', u'Horizon Documentation', u'OpenStack', - 'Horizon', 'One line description of project.', 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - - -# -- Options for Epub output --------------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = u'Horizon' -epub_author = u'OpenStack' -epub_publisher = u'OpenStack' -epub_copyright = u'2011, OpenStack' - -# The language of the text. It defaults to the language option -# or en if the language is not set. -#epub_language = '' - -# The scheme of the identifier. Typical schemes are ISBN or URL. -#epub_scheme = '' - -# The unique identifier of the text. This can be a ISBN number -# or the project homepage. -#epub_identifier = '' - -# A unique identification for the text. -#epub_uid = '' - -# A tuple containing the cover image and cover page html template filenames. -#epub_cover = () - -# HTML files that should be inserted before the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_pre_files = [] - -# HTML files shat should be inserted after the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_post_files = [] - -# A list of files that should not be packed into the epub file. -#epub_exclude_files = [] - -# The depth of the table of contents in toc.ncx. -#epub_tocdepth = 3 - -# Allow duplicate toc entries. -#epub_tocdup = True - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'python': ('http://docs.python.org/', None), - 'nova': ('http://nova.openstack.org', None), - 'swift': ('http://swift.openstack.org', None), - 'keystone': ('http://keystone.openstack.org', None), - 'glance': ('http://glance.openstack.org', None)} diff --git a/doc/source/index.rst b/doc/source/index.rst deleted file mode 100644 index cb6e3278..00000000 --- a/doc/source/index.rst +++ /dev/null @@ -1,69 +0,0 @@ -.. - Copyright 2011 OpenStack, LLC - All Rights Reserved. - - 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. - -======================== -Horizon for Contributors -======================== - -Horizon is the canonical implementation of `Openstack's Dashboard -`_, which provides a web based user -interface to OpenStack services including Nova, Swift, Keystone, and Quantum. - -This document describes horizon for contributors of the project. - -Project Structure -================= - -This project is a bit different from other Openstack projects in that it has -two very distinct components underneath it: - -* django-openstack -* openstack-dashboard - -Django-openstack holds the generic libraries and components that can be -used in any Django project. In testing, this component is set up with -buildout (see run_tests.sh), and any dependencies that get added need to -be added to the django-openstack/buildout.cfg file. - -Openstack-dashboard is a reference django project that uses django-openstack -and is built with a virtualenv and tested through that environment. If -depdendencies are added that the reference django project needs, they -should be added to openstack-dashboard/tools/pip-requires. - -Contents: ---------- - -.. toctree:: - :maxdepth: 1 - - testing - -Developer Docs --------------- - -.. toctree:: - :maxdepth: 1 - - sourcecode/autoindex - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - diff --git a/doc/source/testing.rst b/doc/source/testing.rst deleted file mode 100644 index bab32a82..00000000 --- a/doc/source/testing.rst +++ /dev/null @@ -1,32 +0,0 @@ -.. - Copyright 2011 OpenStack, LLC - All Rights Reserved. - - 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. - -===================== -Testing the Dashboard -===================== - -Testing the dashbaord is a bit more complex due to having the two projects -in the same repository. - -The run_tests.sh script invokes tests and analysis on both of these -components in it's process, and is what Jenkins uses to verify the -stability of the project. - -To run the tests:: - - $ ./run_tests.sh - - diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..986ad3df --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,153 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Horizon.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Horizon.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Horizon" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Horizon" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/source/_static/.gitignore b/docs/source/_static/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 00000000..a1f7a3a8 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,391 @@ +# -*- coding: utf-8 -*- +# +# Horizon documentation build configuration file, created by +# sphinx-quickstart on Thu Oct 27 11:38:59 2011. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os +import horizon.version + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +HORIZON_DIR = os.path.abspath(os.path.join(BASE_DIR, "..", "..", "horizon")) +DASHBOARD_DIR = os.path.abspath(os.path.join(BASE_DIR, "..", "..", "openstack-dashboard")) + +sys.path.insert(0, HORIZON_DIR) +sys.path.insert(0, DASHBOARD_DIR) + +def write_autodoc_index(): + + def find_autodoc_modules(module_name, sourcedir): + """returns a list of modules in the SOURCE directory""" + modlist = [] + os.chdir(os.path.join(sourcedir, module_name)) + print "SEARCHING %s" % sourcedir + for root, dirs, files in os.walk("."): + for filename in files: + if filename.endswith(".py"): + # root = ./dashboard/test/unit + # filename = base.py + # remove the pieces of the root + elements = root.split(os.path.sep) + # replace the leading "." with the module name + elements[0] = module_name + # and get the base module name + base, extension = os.path.splitext(filename) + if not (base == "__init__"): + elements.append(base) + result = ".".join(elements) + #print result + modlist.append(result) + return modlist + + RSTDIR = os.path.abspath(os.path.join(BASE_DIR, "sourcecode")) + SRCS = {'horizon': HORIZON_DIR, + 'dashboard': DASHBOARD_DIR} + + if not(os.path.exists(RSTDIR)): + os.mkdir(RSTDIR) + + INDEXOUT = open(os.path.join(RSTDIR, "autoindex.rst"), "w") + INDEXOUT.write("=================\n") + INDEXOUT.write("Source Code Index\n") + INDEXOUT.write("=================\n") + + for modulename, path in SRCS.items(): + sys.stdout.write("Generating source documentation for %s\n" % modulename) + INDEXOUT.write("\n%s\n" % modulename.capitalize()) + INDEXOUT.write("%s\n" % ("=" * len(modulename),)) + INDEXOUT.write(".. toctree::\n") + INDEXOUT.write(" :maxdepth: 1\n") + INDEXOUT.write("\n") + if not(os.path.exists(os.path.join(RSTDIR, modulename))): + os.mkdir(os.path.join(RSTDIR, modulename)) + for module in find_autodoc_modules(modulename, path): + mod_path = os.path.join(path, *module.split(".")) + generated_file = os.path.join(RSTDIR, modulename, "%s.rst" % module) + + INDEXOUT.write(" %s/%s\n" % (modulename, module)) + + # Find the __init__.py module if this is a directory + if os.path.isdir(mod_path): + source_file = ".".join((os.path.join(mod_path, "__init__"), "py",)) + else: + source_file = ".".join((os.path.join(mod_path), "py")) + + # Only generate a new file if the source has changed or we don't + # have a doc file to begin with. + if not os.access(generated_file, os.F_OK) or \ + os.stat(generated_file).st_mtime < os.stat(source_file).st_mtime: + print "Module %s updated, generating new documentation." % module + FILEOUT = open(generated_file, "w") + header = "The :mod:`%s` Module" % module + FILEOUT.write("%s\n" % ("=" * len(header),)) + FILEOUT.write("%s\n" % header) + FILEOUT.write("%s\n" % ("=" * len(header),)) + FILEOUT.write(".. automodule:: %s\n" % module) + FILEOUT.write(" :members:\n") + FILEOUT.write(" :undoc-members:\n") + FILEOUT.write(" :show-inheritance:\n") + FILEOUT.write(" :noindex:\n") + FILEOUT.close() + + INDEXOUT.close() + +write_autodoc_index() + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.pngmath', + 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +if os.getenv('HUDSON_PUBLISH_DOCS'): + templates_path = ['_ga', '_templates'] +else: + templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Horizon' +copyright = u'2011, OpenStack, LLC' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = horizon.version.canonical_version_string() +# The full version, including alpha/beta/rc tags. +release = horizon.version.canonical_version_string() + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +primary_domain = 'py' +nitpicky = False + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Horizondoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'Horizon.tex', u'Horizon Documentation', + u'OpenStack, LLC', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'horizon', u'Horizon Documentation', + [u'OpenStack'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'Horizon', u'Horizon Documentation', u'OpenStack', + 'Horizon', 'One line description of project.', 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + + +# -- Options for Epub output --------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = u'Horizon' +epub_author = u'OpenStack' +epub_publisher = u'OpenStack' +epub_copyright = u'2011, OpenStack' + +# The language of the text. It defaults to the language option +# or en if the language is not set. +#epub_language = '' + +# The scheme of the identifier. Typical schemes are ISBN or URL. +#epub_scheme = '' + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +#epub_identifier = '' + +# A unique identification for the text. +#epub_uid = '' + +# A tuple containing the cover image and cover page html template filenames. +#epub_cover = () + +# HTML files that should be inserted before the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_pre_files = [] + +# HTML files shat should be inserted after the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_post_files = [] + +# A list of files that should not be packed into the epub file. +#epub_exclude_files = [] + +# The depth of the table of contents in toc.ncx. +#epub_tocdepth = 3 + +# Allow duplicate toc entries. +#epub_tocdup = True + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'python': ('http://docs.python.org/', None), + 'django': ('http://docs.djangoproject.com/en/dev/_objects/'), + 'nova': ('http://nova.openstack.org', None), + 'swift': ('http://swift.openstack.org', None), + 'keystone': ('http://keystone.openstack.org', None), + 'glance': ('http://glance.openstack.org', None)} diff --git a/docs/source/faq.rst b/docs/source/faq.rst new file mode 100644 index 00000000..26836ef6 --- /dev/null +++ b/docs/source/faq.rst @@ -0,0 +1,37 @@ +========================== +Frequently Asked Questions +========================== + +What is the relationship between ``Dashboards``, ``Panels``, and navigation? + + The navigational structure is strongly encouraged to flow from + ``Dashboard`` objects as top-level navigation items to ``Panel`` objects as + sub-navigation items as in the current implementation. Template tags + are provided to automatically generate this structure. + + That said, you are not required to use the provided tools and can write + templates and URLconfs by hand to create any desired structure. + +Does a panel have to be an app in ``INSTALLED_APPS``? + + A panel can live in any Python module. It can be a standalone which ties + into an existing dashboard, or it can be contained alongside others within + a larger dashboard "app". There is no strict enforcement here. Python + is "a language for consenting adults." A module containing a Panel does + not need to be added to ``INSTALLED_APPS``, but this is a common and + convenient way to load a standalone panel. + +Could I hook an external service into a panel using, for example, an iFrame? + + Panels are just entry-points to hook views into the larger dashboard + navigational structure and enforce common attributes like RBAC. The + view and corresponding templates can contain anything you would like, + including iFrames. + +What does this mean for visual design? + + The ability to add an arbitrary number of top-level navigational items + (``Dashboard`` objects) poses a new design challenge. Horizon's lead + designer has taken on the challenge of providing a reference design + for Horizon which supports this possibility. + diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst new file mode 100644 index 00000000..cca87400 --- /dev/null +++ b/docs/source/glossary.rst @@ -0,0 +1,19 @@ +======== +Glossary +======== + +Horizon + + The OpenStack dashboard project. Also the name of the top-level + Python object which handles registration for the app. + +Dashboard + + A Python class representing a top-level navigation item (e.g. "syspanel") + which provides a consistent API for Horizon-compatible applications. + +Panel + + A Python class representing a sub-navigation item (e.g. "instances") + which contains all the necessary logic (views, forms, tests, etc.) for + that interface. diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 00000000..c326d6de --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,101 @@ +.. + Copyright 2011 OpenStack, LLC + All Rights Reserved. + + 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. + +======================================== +Horizon: The OpenStack Dashboard Project +======================================== + +Introduction +============ + +Horizon is the canonical implementation of `Openstack's Dashboard +`_, which provides a web based user +interface to OpenStack services including Nova, Swift, Keystone, etc. + +For a more in-depth look at Horizon and it's architecture, see the +:doc:`Introduction to Horizon `. + +To learn what you need to know to get going, see the :doc:`quickstart`. + +Getting Started With Horizon +============================ + +How to use Horizon in your own projects. + +.. toctree:: + :maxdepth: 1 + + intro + quickstart + + +Developer Reference +=================== + +For those wishing to develop Horizon itself, or go in-depth with building +your own :class:`~horizon.Dashboard` or :class:`~horizon.Panel` classes, +the following documentation is provided. + +Topics +------ + +Brief guides to areas of interest and importance when developing Horizon. + +.. toctree:: + :maxdepth: 1 + + testing + +API Reference +------------- + +In-depth documentation for Horizon and it's APIs. + +.. toctree:: + :maxdepth: 1 + + ref/run_tests + ref/horizon + ref/users + ref/forms + ref/views + ref/middleware + ref/context_processors + ref/decorators + ref/exceptions + +Source Code Reference +--------------------- + +Auto-generated reference for the complete source code. + +.. toctree:: + :maxdepth: 1 + + sourcecode/autoindex + + +Information +=========== + +.. toctree:: + :maxdepth: 1 + + faq + glossary + +* :ref:`genindex` +* :ref:`modindex` diff --git a/docs/source/intro.rst b/docs/source/intro.rst new file mode 100644 index 00000000..83f7819f --- /dev/null +++ b/docs/source/intro.rst @@ -0,0 +1,124 @@ +=================== +Introducing Horizon +=================== + +.. contents:: Contents: + :local: + +Values +====== + + "Think simple" as my old master used to say - meaning reduce + the whole of its parts into the simplest terms, getting back + to first principles. + + -- Frank Lloyd Wright + +Horizon holds several key values at the core of it's design and architecture: + + * Core Support: Out-of-the-box support for all core OpenStack projects. + * Extensible: Anyone can add a new component as a "first-class citizen". + * Manageable: The core codebase should be simple and easy-to-navigate. + * Consistent: Visual and interaction paradigms are maintained throughout. + * Stable: A reliable API with an emphasis on backwards-compatibility. + * Usable: Providing an *awesome* interface that people *want* to use. + +The only way to attain and uphold those ideals is to make it *easy* for +developers to implement those values. + +History +======= + +Horizon started life as a single app to manage OpenStack's compute project. +As such, all it needed was a set of views, templates, and API calls. + +From there it grew to support multiple OpenStack projects and APIs gradually, +arranged rigidly into "dash" and "syspanel" groupings. + +During the "Diablo" release cycle an initial plugin system was added using +signals to hook in additional URL patterns and add links into the "dash" +and "syspanel" navigation. + +This incremental growth served the goal of "Core Support" phenomenally, but +left "Extensible" and "Manageable" behind. And while the other key values took +shape of their own accord, it was time to re-architect for an extensible, +modular future. + + +The Current Architecture & How It Meets Our Values +================================================== + +At it's core, **Horizon should be a registration pattern for +applications to hook into**. Here's what that means and how it's +implemented in terms of our values: + +Core Support +------------ + +Horizon ships with three central dashboards, a "User Dashboard", a +"System Dashboard", and a "Settings" dashboard. Between these three they +cover the core OpenStack applications and deliver on Core Support. + +The Horizon application also ships with a set of API abstractions +for the core OpenStack projects in order to provide a consistent, stable set +of reusable methods for developers. Using these abstractions, developers +working on Horizon don't need to be intimately familiar with the APIs of +each OpenStack project. + +Extensible +---------- + +A Horizon dashboard application is based around the :class:`~horizon.Dashboard` +class that provides a consistent API and set of capabilities for both +core OpenStack dashboard apps shipped with Horizon and equally for third-party +apps. The :class:`~horizon.Dashboard` class is treated as a top-level +navigation item. + +Should a developer wish to provide functionality within an existing dashboard +(e.g. adding a monitoring panel to the user dashboard) the simple registration +pattern makes it possible to write an app which hooks into other dashboards +just as easily as creating a new dashboard. All you have to do is import the +dashboard you wish to modify. + +Manageable +---------- + +Within the application, there is a simple method for registering a +:class:`~horizon.Panel` (sub-navigation items). Each panel contains the +necessary logic (views, forms, tests, etc.) for that interface. This granular +breakdown prevents files (such as ``api.py``) from becoming thousands of +lines long and makes code easy to find by correlating it directly to the +navigation. + +Consistent +---------- + +By providing the necessary core classes to build from, as well as a +solid set of reusable templates and additional tools (base form classes, +base widget classes, template tags, and perhaps even class-based views) +we can maintain consistency across applications. + +Stable +------ + +By architecting around these core classes and reusable components we +create an implicit contract that changes to these components will be +made in the most backwards-compatible ways whenever possible. + +Usable +------ + +Ultimately that's up to each and every developer that touches the code, +but if we get all the other goals out of the way then we are free to focus +on the best possible experience. + +.. seealso:: + + :doc:`Quickstart ` + A short guide to getting started with using Horizon. + + :doc:`Frequently Asked Questions ` + Common questions and answers. + + :doc:`Glossary ` + Common terms and their definitions. diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst new file mode 100644 index 00000000..2e010be9 --- /dev/null +++ b/docs/source/quickstart.rst @@ -0,0 +1,146 @@ +================== +Horizon Quickstart +================== + +Horizon's Structure +=================== + +This project is a bit different from other Openstack projects in that it is +composed of two distinct components: + + * ``horizon`` + * ``openstack-dashboard`` + +The ``horizon`` directory holds the generic libraries and components that can +be used in any Django project. In testing, this component is set up with +buildout (see :doc:`ref/run_tests`), and any dependencies that need to +be added to the ``horizon/buildout.cfg`` file. + +The ``openstack-dashboard`` directory contains a reference Django project that +uses ``horizon`` and is built with a virtualenv. If dependencies are added that +``openstack-dashboard`` requires they should be added to ``openstack- +dashboard/tools/pip-requires``. + +Project +======= + +INSTALLED_APPS +-------------- + +At the project level you add Horizon and any desired dashboards to your +``settings.INSTALLED_APPS``:: + + INSTALLED_APPS = ( + 'django', + ... + 'horizon', + 'horizon.dash', + 'horizon.syspanel', + ) + +URLs +---- + +Then you add a single line to your project's ``urls.py``:: + + url(r'', include(horizon.urls)), + +Those urls are automatically constructed based on the registered Horizon apps. +If a different URL structure is desired it can be constructed by hand. + +Templates +--------- + +Pre-built template tags generate navigation. In your ``nav.html`` +template you might have the following:: + + {% load horizon %} + + + +And in your ``sidebar.html`` you might have:: + + {% load horizon %} + + + +These template tags are aware of the current "active" dashboard and panel +via template context variables and will render accordingly. + +Application +=========== + +Structure +--------- + +An application would have the following structure (we'll use syspanel as +an example):: + + syspanel/ + |---__init__.py + |---dashboard.py <-----Registers the app with Horizon and sets dashboard properties + |---templates/ + |---templatetags/ + |---overview/ + |---services/ + |---images/ + |---__init__.py + |---panel.py <-----Registers the panel in the app and defines panel properties + |---urls.py + |---views.py + |---forms.py + |---tests.py + |---api.py <-------Optional additional API methods for non-core services + |---templates/ + ... + ... + +Dashboard Classes +----------------- + +Inside of ``dashboard.py`` you would have a class definition and the registration +process:: + + import horizon + + + class Syspanel(horizon.Dashboard): + name = "Syspanel" # Appears in navigation + slug = 'syspanel' # Appears in url + panels = ('overview', 'services', 'instances', 'flavors', 'images', + 'tenants', 'users', 'quotas',) + default_panel = 'overview' + roles = ('admin',) # Provides RBAC at the dashboard-level + ... + + + horizon.register(Syspanel) + +Panel Classes +------------- + +To connect a :class:`~horizon.Panel` with a :class:`~horizon.Dashboard` class +you register it in a ``panels.py`` file like so:: + + import horizon + + from horizon.dashboard.syspanel import dashboard + + + class Images(horizon.Panel): + name = "Images" + slug = 'images' + roles = ('admin', 'my_other_role',) # Fine-grained RBAC per-panel + + + # You could also register your panel with another application's dashboard + dashboard.Syspanel.register(Images) + +By default a :class:`~horizon.Panel` class looks for a ``urls.py`` file in the +same directory as ``panel.py`` to include in the rollup of url patterns from +panels to dashboards to Horizon, resulting in a wholly extensible, configurable +URL structure. diff --git a/docs/source/ref/context_processors.rst b/docs/source/ref/context_processors.rst new file mode 100644 index 00000000..b34c0109 --- /dev/null +++ b/docs/source/ref/context_processors.rst @@ -0,0 +1,6 @@ +========================== +Horizon Context Processors +========================== + +.. automodule:: horizon.context_processors + :members: diff --git a/docs/source/ref/decorators.rst b/docs/source/ref/decorators.rst new file mode 100644 index 00000000..777afbe5 --- /dev/null +++ b/docs/source/ref/decorators.rst @@ -0,0 +1,6 @@ +================== +Horizon Decorators +================== + +.. automodule:: horizon.decorators + :members: diff --git a/docs/source/ref/exceptions.rst b/docs/source/ref/exceptions.rst new file mode 100644 index 00000000..4151f18f --- /dev/null +++ b/docs/source/ref/exceptions.rst @@ -0,0 +1,6 @@ +================== +Horizon Exceptions +================== + +.. automodule:: horizon.exceptions + :members: diff --git a/docs/source/ref/forms.rst b/docs/source/ref/forms.rst new file mode 100644 index 00000000..9b30cb8e --- /dev/null +++ b/docs/source/ref/forms.rst @@ -0,0 +1,17 @@ +============= +Horizon Forms +============= + +Horizon ships with a number of form classes, some generic and some specific. + +Generic Forms +============= + +.. automodule:: horizon.forms + :members: + +Auth Forms +========== + +.. automodule:: horizon.views.auth_forms + :members: diff --git a/docs/source/ref/horizon.rst b/docs/source/ref/horizon.rst new file mode 100644 index 00000000..9206e950 --- /dev/null +++ b/docs/source/ref/horizon.rst @@ -0,0 +1,42 @@ +====================== +The ``horizon`` Module +====================== + +.. module:: horizon + +Horizon ships with a single point of contact for hooking into your project if +you aren't developing your own :class:`~horizon.Dashboard` or +:class:`~horizon.Panel`:: + + import horizon + +From there you can access all the key methods you need. + +Horizon +======= + +.. attribute:: urls + + The auto-generated URLconf for Horizon. Usage:: + + url(r'', include(horizon.urls)), + +.. autofunction:: register +.. autofunction:: unregister +.. autofunction:: get_absolute_url +.. autofunction:: get_user_home +.. autofunction:: get_dashboard +.. autofunction:: get_default_dashboard +.. autofunction:: get_dashboards + +Dashboard +========= + +.. autoclass:: Dashboard + :members: + +Panel +===== + +.. autoclass:: Panel + :members: diff --git a/docs/source/ref/middleware.rst b/docs/source/ref/middleware.rst new file mode 100644 index 00000000..fcca5ff0 --- /dev/null +++ b/docs/source/ref/middleware.rst @@ -0,0 +1,6 @@ +================== +Horizon Middleware +================== + +.. automodule:: horizon.middleware + :members: diff --git a/docs/source/ref/run_tests.rst b/docs/source/ref/run_tests.rst new file mode 100644 index 00000000..201198ed --- /dev/null +++ b/docs/source/ref/run_tests.rst @@ -0,0 +1,100 @@ +=========================== +The ``run_tests.sh`` Script +=========================== + +Horizon ships with a script called ``run_tests.sh`` at the root of the +repository. This script provides many crucial functions for the project, +and also makes several otherwise complex tasks trivial for you as a +developer. + +First Run +========= + +If you start with a clean copy of the Horizon repository, the first thing +you should do is to run ``./run_tests.sh`` from the root of the repository. +This will do three things for you: + + #. Set up a virtual environment for ``openstack-dashboard`` using + ``openstack-dashboard/tools/install_venv.py``. + #. Set up an environment for ``horizon`` using + ``horizon/bootstrap.py`` and ``horizon/bin/buildout``. + #. Run the tests for both ``horizon`` and ``openstack-dashboard`` using + their respective environments and verify that evreything is working. + +Setting up both environments the first time can take several minutes, but only +needs to be done once. If dependencies are added in the future, updating the +environments will be necessary but not necessarily as time consuming. + +I just want to run the tests! +============================= + +Running both sets of unit tests quickly and easily is the main goal of this +script. All you need to do is:: + + ./run_tests.sh + +Yep, that's it. Everything else the script can do is optional. + +Give me metrics! +================ + +You can generate various reports and metrics using command line arguments +to ``run_tests.sh``. + +Coverage +-------- + +To run coverage reports:: + + ./run_tests.sh --coverage + +The reports are saved to ``./reports/`` and ``./coverage.xml``. + +PEP8 +---- + +You can check for PEP8 violations as well:: + + ./run_tests.sh --pep8 + +The results are saved to ``./pep8.txt``. + +PyLint +------ + +For more detailed code analysis you can run:: + + ./run_tests.sh --pylint + +The output will be saved in ``./pylint.txt``. + +Running the development server +============================== + +As an added bonus, you can run Django's development server directly from +the root of the repository with ``run_tests.sh`` like so:: + + ./run_tests.sh --runserver + +This is effectively just an alias for:: + + ./openstack-dashboard/tools/with_venv.sh ./openstack-dashboard/dashboard/manage.py runserver + +Generating the documentation +============================ + +You can build Horizon's documentation automatically by running:: + + ./run_tests.sh --docs + +The output is stored in ``./docs/build/html/``. + +Starting clean +============== + +If you ever want to start clean with a new environment for Horizon, you can +run:: + + ./run_tests.sh --force + +That will blow away the existing environments and create new ones for you. diff --git a/docs/source/ref/users.rst b/docs/source/ref/users.rst new file mode 100644 index 00000000..857358d1 --- /dev/null +++ b/docs/source/ref/users.rst @@ -0,0 +1,6 @@ +================= +Horizon User APIs +================= + +.. automodule:: horizon.users + :members: diff --git a/docs/source/ref/views.rst b/docs/source/ref/views.rst new file mode 100644 index 00000000..970609ca --- /dev/null +++ b/docs/source/ref/views.rst @@ -0,0 +1,12 @@ +============= +Horizon Views +============= + +Horizon ships with a number of pre-built views which are used within +Horizon and can also be reused in your applications. + +Auth +==== + +.. automodule:: horizon.views.auth + :members: diff --git a/docs/source/testing.rst b/docs/source/testing.rst new file mode 100644 index 00000000..a968ae50 --- /dev/null +++ b/docs/source/testing.rst @@ -0,0 +1,62 @@ +.. + Copyright 2011 OpenStack, LLC + All Rights Reserved. + + 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. + +=============== +Testing Horizon +=============== + +How to run the tests +==================== + +Because Horizon is composed of both the ``horizon`` app and the +``openstack-dashboard`` reference project, there are in fact two sets of unit +tests. While they can be run individually without problem, there is an easier +way: + +Included at the root of the repository is the ``run_tests.sh`` script +which invokes both sets of tests, and optionally generates analyses on both +components in the process. This script is what what Jenkins uses to verify the +stability of the project, so you should make sure you run it and it passes +before you submit any pull requests/patches. + +To run the tests:: + + $ ./run_tests.sh + +.. seealso:: + + :doc:`ref/run_tests` + Full reference for the ``run_tests.sh`` script. + +How to write good tests +======================= + +Horizon uses Django's unit test machinery (which extends Python's ``unittest2`` +library) as the core of it's test suite. As such, all tests for the Python code +should be written as unit tests. No doctests please. + +A few pointers for writing good tests: + + * Write tests as you go--If you save them to the end you'll write less of + them and they'll often miss large chunks of code. + * Keep it as simple as possible--Make sure each test tests one thing + and tests it thoroughly. + * Think about all the possible inputs your code could have--It's usually + the edge cases that end up revealing bugs. + * Use ``coverage.py`` to find out what code is *not* being tested. + +In general new code without unit tests will not be accepted, and every bugfix +*must* include a regression test. diff --git a/horizon/LICENSE b/horizon/LICENSE new file mode 100644 index 00000000..68c771a0 --- /dev/null +++ b/horizon/LICENSE @@ -0,0 +1,176 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + diff --git a/horizon/Makefile b/horizon/Makefile new file mode 100644 index 00000000..d64cd72b --- /dev/null +++ b/horizon/Makefile @@ -0,0 +1,43 @@ +PYTHON=`which python` +DESTDIR=/ +BUILDIR=$(CURDIR)/debian/horizon +PROJECT=horizon + +all: + @echo "make buildout - Run through buildout" + @echo "make test - Run tests" + @echo "make source - Create source package" + @echo "make install - Install on local system" + @echo "make buildrpm - Generate a rpm package" + @echo "make builddeb - Generate a deb package" + @echo "make clean - Get rid of scratch and byte files" + +buildout: ./bin/buildout + ./bin/buildout + +./bin/buildout: + $(PYTHON) bootstrap.py + +source: + $(PYTHON) setup.py sdist $(COMPILE) + +install: + $(PYTHON) setup.py install --root $(DESTDIR) $(COMPILE) + +buildrpm: + $(PYTHON) setup.py bdist_rpm --post-install=rpm/postinstall --pre-uninstall=rpm/preuninstall + +builddeb: + # build the source package in the parent directory + # then rename it to project_version.orig.tar.gz + $(PYTHON) setup.py sdist $(COMPILE) --dist-dir=../ + rename -f 's/$(PROJECT)-(.*)\.tar\.gz/$(PROJECT)_$$1\.orig\.tar\.gz/' ../* + # build the package + #dpkg-buildpackage -i -I -rfakeroot + dpkg-buildpackage -b -rfakeroot -tc -uc -D + +clean: + $(PYTHON) setup.py clean + $(MAKE) -f $(CURDIR)/debian/rules clean + rm -rf build/ MANIFEST + find . -name '*.pyc' -delete diff --git a/horizon/README b/horizon/README new file mode 100644 index 00000000..1b6d6fd3 --- /dev/null +++ b/horizon/README @@ -0,0 +1,59 @@ +======================================== +Horizon: The OpenStack Dashboard Project +======================================== + +The Horizon project is a Django module that is used to provide web based +interactions with an OpenStack cloud. + +There is a reference implementation that uses this module located at: + + http://launchpad.net/horizon + +It is highly recommended that you make use of this reference implementation +so that changes you make can be visualized effectively and are consistent. +Using this reference implementation as a development environment will greatly +simplify development of the ``horizon`` module. + +Of course, if you are developing your own Django site using Horizon, then +you can disregard this advice. + + +Getting Started +=============== + +Horizon uses Buildout (http://www.buildout.org/) to manage local development. +To configure your local Buildout environment first install the following +system-level dependencies: + + * python-dev + * git + * bzr + +Then instantiate buildout with:: + + $ python bootstrap.py + $ bin/buildout + +This will install all the dependencies of Horizon and provide some useful +scripts in the ``bin/`` directory: + + bin/python provides a python shell for the current buildout. + bin/django provides django functions for the current buildout. + + +You should now be able to run unit tests as follows:: + + $ bin/django test + +or:: + + $ bin/test + +You can run unit tests with code coverage on Horizon by setting +``NOSE_WITH_COVERAGE``:: + + $ NOSE_WITH_COVERAGE=true bin/test + +Get even better coverage info by running coverage directly:: + + $ coverage run --branch --source horizon bin/django test horizon && coverage html diff --git a/horizon/bootstrap.py b/horizon/bootstrap.py new file mode 100644 index 00000000..5f2cb083 --- /dev/null +++ b/horizon/bootstrap.py @@ -0,0 +1,260 @@ +############################################################################## +# +# Copyright (c) 2006 Zope Foundation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Bootstrap a buildout-based project + +Simply run this script in a directory containing a buildout.cfg. +The script accepts buildout command-line options, so you can +use the -c option to specify an alternate configuration file. +""" + +import os, shutil, sys, tempfile, textwrap, urllib, urllib2, subprocess +from optparse import OptionParser + +if sys.platform == 'win32': + def quote(c): + if ' ' in c: + return '"%s"' % c # work around spawn lamosity on windows + else: + return c +else: + quote = str + +# See zc.buildout.easy_install._has_broken_dash_S for motivation and comments. +stdout, stderr = subprocess.Popen( + [sys.executable, '-Sc', + 'try:\n' + ' import ConfigParser\n' + 'except ImportError:\n' + ' print 1\n' + 'else:\n' + ' print 0\n'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() +has_broken_dash_S = bool(int(stdout.strip())) + +# In order to be more robust in the face of system Pythons, we want to +# run without site-packages loaded. This is somewhat tricky, in +# particular because Python 2.6's distutils imports site, so starting +# with the -S flag is not sufficient. However, we'll start with that: +if not has_broken_dash_S and 'site' in sys.modules: + # We will restart with python -S. + args = sys.argv[:] + args[0:0] = [sys.executable, '-S'] + args = map(quote, args) + os.execv(sys.executable, args) +# Now we are running with -S. We'll get the clean sys.path, import site +# because distutils will do it later, and then reset the path and clean +# out any namespace packages from site-packages that might have been +# loaded by .pth files. +clean_path = sys.path[:] +import site +sys.path[:] = clean_path +for k, v in sys.modules.items(): + if k in ('setuptools', 'pkg_resources') or ( + hasattr(v, '__path__') and + len(v.__path__)==1 and + not os.path.exists(os.path.join(v.__path__[0],'__init__.py'))): + # This is a namespace package. Remove it. + sys.modules.pop(k) + +is_jython = sys.platform.startswith('java') + +setuptools_source = 'http://peak.telecommunity.com/dist/ez_setup.py' +distribute_source = 'http://python-distribute.org/distribute_setup.py' + +# parsing arguments +def normalize_to_url(option, opt_str, value, parser): + if value: + if '://' not in value: # It doesn't smell like a URL. + value = 'file://%s' % ( + urllib.pathname2url( + os.path.abspath(os.path.expanduser(value))),) + if opt_str == '--download-base' and not value.endswith('/'): + # Download base needs a trailing slash to make the world happy. + value += '/' + else: + value = None + name = opt_str[2:].replace('-', '_') + setattr(parser.values, name, value) + +usage = '''\ +[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] + +Bootstraps a buildout-based project. + +Simply run this script in a directory containing a buildout.cfg, using the +Python that you want bin/buildout to use. + +Note that by using --setup-source and --download-base to point to +local resources, you can keep this script from going over the network. +''' + +parser = OptionParser(usage=usage) +parser.add_option("-v", "--version", dest="version", + help="use a specific zc.buildout version") +parser.add_option("-d", "--distribute", + action="store_true", dest="use_distribute", default=False, + help="Use Distribute rather than Setuptools.") +parser.add_option("--setup-source", action="callback", dest="setup_source", + callback=normalize_to_url, nargs=1, type="string", + help=("Specify a URL or file location for the setup file. " + "If you use Setuptools, this will default to " + + setuptools_source + "; if you use Distribute, this " + "will default to " + distribute_source +".")) +parser.add_option("--download-base", action="callback", dest="download_base", + callback=normalize_to_url, nargs=1, type="string", + help=("Specify a URL or directory for downloading " + "zc.buildout and either Setuptools or Distribute. " + "Defaults to PyPI.")) +parser.add_option("--eggs", + help=("Specify a directory for storing eggs. Defaults to " + "a temporary directory that is deleted when the " + "bootstrap script completes.")) +parser.add_option("-t", "--accept-buildout-test-releases", + dest='accept_buildout_test_releases', + action="store_true", default=False, + help=("Normally, if you do not specify a --version, the " + "bootstrap script and buildout gets the newest " + "*final* versions of zc.buildout and its recipes and " + "extensions for you. If you use this flag, " + "bootstrap and buildout will get the newest releases " + "even if they are alphas or betas.")) +parser.add_option("-c", None, action="store", dest="config_file", + help=("Specify the path to the buildout configuration " + "file to be used.")) + +options, args = parser.parse_args() + +# if -c was provided, we push it back into args for buildout's main function +if options.config_file is not None: + args += ['-c', options.config_file] + +if options.eggs: + eggs_dir = os.path.abspath(os.path.expanduser(options.eggs)) +else: + eggs_dir = tempfile.mkdtemp() + +if options.setup_source is None: + if options.use_distribute: + options.setup_source = distribute_source + else: + options.setup_source = setuptools_source + +if options.accept_buildout_test_releases: + args.append('buildout:accept-buildout-test-releases=true') +args.append('bootstrap') + +try: + import pkg_resources + import setuptools # A flag. Sometimes pkg_resources is installed alone. + if not hasattr(pkg_resources, '_distribute'): + raise ImportError +except ImportError: + ez_code = urllib2.urlopen( + options.setup_source).read().replace('\r\n', '\n') + ez = {} + exec ez_code in ez + setup_args = dict(to_dir=eggs_dir, download_delay=0) + if options.download_base: + setup_args['download_base'] = options.download_base + if options.use_distribute: + setup_args['no_fake'] = True + ez['use_setuptools'](**setup_args) + if 'pkg_resources' in sys.modules: + reload(sys.modules['pkg_resources']) + import pkg_resources + # This does not (always?) update the default working set. We will + # do it. + for path in sys.path: + if path not in pkg_resources.working_set.entries: + pkg_resources.working_set.add_entry(path) + +cmd = [quote(sys.executable), + '-c', + quote('from setuptools.command.easy_install import main; main()'), + '-mqNxd', + quote(eggs_dir)] + +if not has_broken_dash_S: + cmd.insert(1, '-S') + +find_links = options.download_base +if not find_links: + find_links = os.environ.get('bootstrap-testing-find-links') +if find_links: + cmd.extend(['-f', quote(find_links)]) + +if options.use_distribute: + setup_requirement = 'distribute' +else: + setup_requirement = 'setuptools' +ws = pkg_resources.working_set +setup_requirement_path = ws.find( + pkg_resources.Requirement.parse(setup_requirement)).location +env = dict( + os.environ, + PYTHONPATH=setup_requirement_path) + +requirement = 'zc.buildout' +version = options.version +if version is None and not options.accept_buildout_test_releases: + # Figure out the most recent final version of zc.buildout. + import setuptools.package_index + _final_parts = '*final-', '*final' + def _final_version(parsed_version): + for part in parsed_version: + if (part[:1] == '*') and (part not in _final_parts): + return False + return True + index = setuptools.package_index.PackageIndex( + search_path=[setup_requirement_path]) + if find_links: + index.add_find_links((find_links,)) + req = pkg_resources.Requirement.parse(requirement) + if index.obtain(req) is not None: + best = [] + bestv = None + for dist in index[req.project_name]: + distv = dist.parsed_version + if _final_version(distv): + if bestv is None or distv > bestv: + best = [dist] + bestv = distv + elif distv == bestv: + best.append(dist) + if best: + best.sort() + version = best[-1].version +if version: + requirement = '=='.join((requirement, version)) +cmd.append(requirement) + +if is_jython: + import subprocess + exitcode = subprocess.Popen(cmd, env=env).wait() +else: # Windows prefers this, apparently; otherwise we would prefer subprocess + exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env])) +if exitcode != 0: + sys.stdout.flush() + sys.stderr.flush() + print ("An error occurred when trying to install zc.buildout. " + "Look above this message for any errors that " + "were output by easy_install.") + sys.exit(exitcode) + +ws.add_entry(eggs_dir) +ws.require(requirement) +import zc.buildout.buildout +zc.buildout.buildout.main(args) +if not options.eggs: # clean up temporary egg directory + shutil.rmtree(eggs_dir) diff --git a/horizon/buildout.cfg b/horizon/buildout.cfg new file mode 100644 index 00000000..3910799f --- /dev/null +++ b/horizon/buildout.cfg @@ -0,0 +1,126 @@ +[buildout] +parts = + django + launchpad + openstack-compute + openstackx + python-novaclient + python-keystoneclient +develop = . +versions = versions + + +[versions] +django = 1.3 +# the following are for glance-dependencies +eventlet = 0.9.12 +greenlet = 0.3.1 +pep8 = 0.5.0 +sqlalchemy = 0.6.3 +sqlalchemy-migrate = 0.6 +webob = 1.0.8 + + +[dependencies] +# dependencies that are found locally ${buildout:directory}/module +# or can be fetched from pypi +recipe = zc.recipe.egg +eggs = + python-dateutil + django-mailer + httplib2 + python-cloudfiles + coverage + glance + quantum +interpreter = python + + +# glance doesn't have a client, and installing +# from bzr doesn't install deps +[glance-dependencies] +recipe = zc.recipe.egg +eggs = + PasteDeploy + anyjson + argparse + eventlet + greenlet + kombu + paste + pep8 + routes + sqlalchemy + sqlalchemy-migrate + webob + xattr +interpreter = python + + +[horizon] +recipe = zc.recipe.egg +eggs = horizon +interpreter = python + + +[django] +# defines settings for django +# any dependencies that cannot be satisifed via the dependencies +# recipe above will need to be added to the extra-paths here. +# IE, dependencies fetch from a git repo will not auto-populate +# like the zc.recipe.egg ones will +recipe = djangorecipe +project = horizon +projectegg = horizon +settings = tests +test = horizon +eggs = + ${dependencies:eggs} + ${horizon:eggs} + ${glance-dependencies:eggs} +extra-paths = + ${buildout:directory}/parts/openstack-compute + ${buildout:directory}/parts/openstackx + ${buildout:directory}/parts/python-novaclient + ${buildout:directory}/parts/python-keystoneclient + + +## Dependencies fetch from git +# git dependencies end up as a subdirectory of ${buildout:directory}/parts/ +[openstack-compute] +recipe = zerokspot.recipe.git +repository = git://github.com/jacobian/openstack.compute.git +as_egg = True + +[openstackx] +recipe = zerokspot.recipe.git +repository = git://github.com/cloudbuilders/openstackx.git +as_egg = True + +[python-novaclient] +recipe = zerokspot.recipe.git +repository = git://github.com/rackspace/python-novaclient.git +as_egg = True + +[python-keystoneclient] +recipe = zerokspot.recipe.git +repository = git://github.com/4P/python-keystoneclient.git +as_egg = True + +## Dependencies fetched from launchpad +# launchpad dependencies will appear as subfolders of +# ${buildout:directory}/launchpad/ +# multiple urls can be specified, format is +# branch_url subfolder_name +# don't forget to add directory to extra_paths in [django] +[launchpad] +recipe = bazaarrecipe +urls = + https://launchpad.net/~hudson-openstack/glance/trunk/ glance + + +## Dependencies fetch from other bzr locations +#[bzrdeps] +#recipe = bazaarrecipe +#urls = +# https://launchpad.net/~hudson-openstack/glance/trunk/ glance diff --git a/horizon/horizon/__init__.py b/horizon/horizon/__init__.py new file mode 100644 index 00000000..c9838c2f --- /dev/null +++ b/horizon/horizon/__init__.py @@ -0,0 +1,50 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" The Horizon OpenStack Dashboard interface. + +Contains the core Horizon classes--:class:`~horizon.Dashboard` and +:class:`horizon.Panel`--the dynamic URLconf for Horizon, and common interface +methods like :func:`~horizon.register` and :func:`~horizon.unregister`. + +""" +# Because this module is compiled by setup.py before Django may be installed +# in the environment we try importing Django and issue a warning but move on +# should that fail. +django = None +try: + import django +except ImportError: + import warnings + + def simple_warn(message, category, filename, lineno, file=None, line=None): + return '%s: %s' % (category.__name__, message) + + msg = ("Could not import Django. This is normal during installation.\n") + warnings.formatwarning = simple_warn + warnings.warn(msg, Warning) + +if django: + from horizon.base import Horizon, Dashboard, Panel, Workflow + + register = Horizon.register + unregister = Horizon.unregister + get_absolute_url = Horizon.get_absolute_url + get_user_home = Horizon.get_user_home + get_dashboard = Horizon.get_dashboard + get_default_dashboard = Horizon.get_default_dashboard + get_dashboards = Horizon.get_dashboards + urls = Horizon._lazy_urls diff --git a/horizon/horizon/api/__init__.py b/horizon/horizon/api/__init__.py new file mode 100644 index 00000000..4d0229ad --- /dev/null +++ b/horizon/horizon/api/__init__.py @@ -0,0 +1,39 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Methods and interface objects used to interact with external APIs. + +API method calls return objects that are in many cases objects with +attributes that are direct maps to the data returned from the API http call. +Unfortunately, these objects are also often constructed dynamically, making +it difficult to know what data is available from the API object. Because of +this, all API calls should wrap their returned object in one defined here, +using only explicitly defined atributes and/or methods. + +In other words, Horizon developers not working on horizon.api +shouldn't need to understand the finer details of APIs for +Keystone/Nova/Glance/Swift et. al. +""" +from horizon.api.glance import * +from horizon.api.keystone import * +from horizon.api.nova import * +from horizon.api.swift import * +from horizon.api.quantum import * diff --git a/horizon/horizon/api/base.py b/horizon/horizon/api/base.py new file mode 100644 index 00000000..093a214a --- /dev/null +++ b/horizon/horizon/api/base.py @@ -0,0 +1,118 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import functools +import logging + +from django.utils.decorators import available_attrs + +from horizon import exceptions + + +__all__ = ('APIResourceWrapper', 'APIDictWrapper', + 'get_service_from_catalog', 'url_for',) + + +LOG = logging.getLogger(__name__) + + +class APIResourceWrapper(object): + """ Simple wrapper for api objects + + Define _attrs on the child class and pass in the + api object as the only argument to the constructor + """ + _attrs = [] + + def __init__(self, apiresource): + self._apiresource = apiresource + + def __getattr__(self, attr): + if attr in self._attrs: + # __getattr__ won't find properties + return self._apiresource.__getattribute__(attr) + else: + LOG.debug('Attempted to access unknown attribute "%s" on' + ' APIResource object of type "%s" wrapping resource of' + ' type "%s"' % (attr, self.__class__, + self._apiresource.__class__)) + raise AttributeError(attr) + + +class APIDictWrapper(object): + """ Simple wrapper for api dictionaries + + Some api calls return dictionaries. This class provides identical + behavior as APIResourceWrapper, except that it will also behave as a + dictionary, in addition to attribute accesses. + + Attribute access is the preferred method of access, to be + consistent with api resource objects from openstackx + """ + def __init__(self, apidict): + self._apidict = apidict + + def __getattr__(self, attr): + if attr in self._attrs: + try: + return self._apidict[attr] + except KeyError, e: + raise AttributeError(e) + + else: + LOG.debug('Attempted to access unknown item "%s" on' + 'APIResource object of type "%s"' + % (attr, self.__class__)) + raise AttributeError(attr) + + def __getitem__(self, item): + try: + return self.__getattr__(item) + except AttributeError, e: + # caller is expecting a KeyError + raise KeyError(e) + + def get(self, item, default=None): + try: + return self.__getattr__(item) + except AttributeError: + return default + + +def get_service_from_catalog(catalog, service_type): + for service in catalog: + if service['type'] == service_type: + return service + return None + + +def url_for(request, service_type, admin=False): + catalog = request.user.service_catalog + service = get_service_from_catalog(catalog, service_type) + if service: + try: + if admin: + return service['endpoints'][0]['adminURL'] + else: + return service['endpoints'][0]['internalURL'] + except (IndexError, KeyError): + raise exceptions.ServiceCatalogException(service_type) + else: + raise exceptions.ServiceCatalogException(service_type) diff --git a/horizon/horizon/api/deprecated.py b/horizon/horizon/api/deprecated.py new file mode 100644 index 00000000..58f72ead --- /dev/null +++ b/horizon/horizon/api/deprecated.py @@ -0,0 +1,95 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import functools +import logging + +from django.utils.decorators import available_attrs +import openstack.compute +import openstackx.admin +import openstackx.api.exceptions as openstackx_exceptions +import openstackx.extras + +from horizon.api.base import * + + +LOG = logging.getLogger(__name__) + + +REBOOT_HARD = openstack.compute.servers.REBOOT_HARD + + +def check_openstackx(f): + """Decorator that adds extra info to api exceptions + + The OpenStack Dashboard currently depends on openstackx extensions + being present in Nova. Error messages depending for views depending + on these extensions do not lead to the conclusion that Nova is missing + extensions. + + This decorator should be dropped and removed after Keystone and + Horizon more gracefully handle extensions and openstackx extensions + aren't required by Horizon in Nova. + """ + @functools.wraps(f, assigned=available_attrs(f)) + def inner(*args, **kwargs): + try: + return f(*args, **kwargs) + except openstackx_exceptions.NotFound, e: + e.message = e.details or '' + e.message += ' This error may be caused by a misconfigured' \ + ' Nova url in keystone\'s service catalog, or ' \ + ' by missing openstackx extensions in Nova. ' \ + ' See the Horizon README.' + raise + return inner + + +def compute_api(request): + management_url = url_for(request, 'compute') + compute = openstack.compute.Compute( + auth_token=request.user.token, + management_url=management_url) + # this below hack is necessary to make the jacobian compute client work + # TODO(mgius): It looks like this is unused now? + compute.client.auth_token = request.user.token + compute.client.management_url = management_url + LOG.debug('compute_api connection created using token "%s"' + ' and url "%s"' % + (request.user.token, management_url)) + return compute + + +def admin_api(request): + management_url = url_for(request, 'compute', True) + LOG.debug('admin_api connection created using token "%s"' + ' and url "%s"' % + (request.user.token, management_url)) + return openstackx.admin.Admin(auth_token=request.user.token, + management_url=management_url) + + +def extras_api(request): + management_url = url_for(request, 'compute') + LOG.debug('extras_api connection created using token "%s"' + ' and url "%s"' % + (request.user.token, management_url)) + return openstackx.extras.Extras(auth_token=request.user.token, + management_url=management_url) diff --git a/horizon/horizon/api/glance.py b/horizon/horizon/api/glance.py new file mode 100644 index 00000000..24ac7fbd --- /dev/null +++ b/horizon/horizon/api/glance.py @@ -0,0 +1,89 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import + +import logging +import urlparse + +from glance import client as glance_client + +from horizon.api.base import * + + +LOG = logging.getLogger(__name__) + + +class Image(APIDictWrapper): + """Simple wrapper around glance image dictionary""" + _attrs = ['checksum', 'container_format', 'created_at', 'deleted', + 'deleted_at', 'disk_format', 'id', 'is_public', 'location', + 'name', 'properties', 'size', 'status', 'updated_at', 'owner'] + + def __getattr__(self, attrname): + if attrname == "properties": + return ImageProperties(super(Image, self).__getattr__(attrname)) + else: + return super(Image, self).__getattr__(attrname) + + +class ImageProperties(APIDictWrapper): + """Simple wrapper around glance image properties dictionary""" + _attrs = ['architecture', 'image_location', 'image_state', 'kernel_id', + 'project_id', 'ramdisk_id'] + + +def glance_api(request): + o = urlparse.urlparse(url_for(request, 'image')) + LOG.debug('glance_api connection created for host "%s:%d"' % + (o.hostname, o.port)) + return glance_client.Client(o.hostname, + o.port, + auth_tok=request.user.token) + + +def image_create(request, image_meta, image_file): + return Image(glance_api(request).add_image(image_meta, image_file)) + + +def image_delete(request, image_id): + return glance_api(request).delete_image(image_id) + + +def image_get(request, image_id): + return Image(glance_api(request).get_image(image_id)[0]) + + +def image_list_detailed(request): + return [Image(i) for i in glance_api(request).get_images_detailed()] + + +def image_update(request, image_id, image_meta=None): + image_meta = image_meta and image_meta or {} + return Image(glance_api(request).update_image(image_id, + image_meta=image_meta)) + + +def snapshot_list_detailed(request): + filters = {} + filters['property-image_type'] = 'snapshot' + filters['is_public'] = 'none' + return [Image(i) for i in glance_api(request) + .get_images_detailed(filters=filters)] diff --git a/horizon/horizon/api/keystone.py b/horizon/horizon/api/keystone.py new file mode 100644 index 00000000..cdc45516 --- /dev/null +++ b/horizon/horizon/api/keystone.py @@ -0,0 +1,256 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django.conf import settings + +from keystoneclient import exceptions as keystone_exceptions +from keystoneclient.v2_0 import client as keystone_client + +from horizon.api.base import * +from horizon.api.deprecated import admin_api +from horizon.api.deprecated import check_openstackx + + +LOG = logging.getLogger(__name__) + + +class Tenant(APIResourceWrapper): + """Simple wrapper around keystoneclient.tenants.Tenant""" + _attrs = ['id', 'description', 'enabled', 'name'] + + +class Token(APIResourceWrapper): + """Simple wrapper around keystoneclient.tokens.Tenant""" + _attrs = ['id', 'user', 'serviceCatalog', 'tenant'] + + +class User(APIResourceWrapper): + """Simple wrapper around keystoneclient.users.User""" + _attrs = ['email', 'enabled', 'id', 'tenantId', 'name'] + + +class Role(APIResourceWrapper): + """Wrapper around keystoneclient.roles.role""" + _attrs = ['id', 'name', 'description', 'service_id'] + + +class Services(APIResourceWrapper): + _attrs = ['disabled', 'host', 'id', 'last_update', 'stats', 'type', 'up', + 'zone'] + + +def keystoneclient(request, username=None, password=None, tenant_id=None, + token_id=None, endpoint=None): + """Returns a client connected to the Keystone backend. + + Several forms of authentication are supported: + + * Username + password -> Unscoped authentication + * Username + password + tenant id -> Scoped authentication + * Unscoped token -> Unscoped authentication + * Unscoped token + tenant id -> Scoped authentication + * Scoped token -> Scoped authentication + + Available services and data from the backend will vary depending on + whether the authentication was scoped or unscoped. + + Lazy authentication if an ``endpoint`` parameter is provided. + + The client is cached so that subsequent API calls during the same + request/response cycle don't have to be re-authenticated. + """ + # Take care of client connection caching/fetching a new client + user = request.user + if hasattr(request, '_keystone') and \ + request._keystone.auth_token == user.token: + conn = request._keystone + else: + conn = keystone_client.Client(username=username or user.username, + password=password, + project_id=tenant_id or user.tenant_id, + token=token_id or user.token, + auth_url=settings.OPENSTACK_KEYSTONE_URL, + endpoint=endpoint) + request._keystone = conn + + # Fetch the correct endpoint for the user type + catalog = getattr(conn, 'service_catalog', None) + if catalog and "serviceCatalog" in catalog.catalog.keys(): + if user.is_admin(): + endpoint = catalog.url_for(service_type='identity', + endpoint_type='adminURL') + else: + endpoint = catalog.url_for(service_type='identity', + endpoint_type='publicURL') + else: + endpoint = settings.OPENSTACK_KEYSTONE_URL + conn.management_url = endpoint + + return conn + + +def tenant_create(request, tenant_name, description, enabled): + return Tenant(keystoneclient(request).tenants.create(tenant_name, + description, + enabled)) + + +def tenant_get(request, tenant_id): + return Tenant(keystoneclient(request).tenants.get(tenant_id)) + + +def tenant_delete(request, tenant_id): + keystoneclient(request).tenants.delete(tenant_id) + + +def tenant_list(request): + return [Tenant(t) for t in keystoneclient(request).tenants.list()] + + +def tenant_update(request, tenant_id, tenant_name, description, enabled): + return Tenant(keystoneclient(request).tenants.update(tenant_id, + tenant_name, + description, + enabled)) + + +def tenant_delete(request, tenant_id): + keystoneclient(request).tenants.delete(tenant_id) + + +def tenant_list_for_token(request, token): + c = keystoneclient(request, token_id=token, + endpoint=settings.OPENSTACK_KEYSTONE_URL) + return [Tenant(t) for t in c.tenants.list()] + + +def token_create(request, tenant, username, password): + ''' + Creates a token using the username and password provided. If tenant + is provided it will retrieve a scoped token and the service catalog for + the given tenant. Otherwise it will return an unscoped token and without + a service catalog. + ''' + c = keystoneclient(request, + username=username, + password=password, + tenant_id=tenant, + endpoint=settings.OPENSTACK_KEYSTONE_URL) + token = c.tokens.authenticate(username=username, + password=password, + tenant=tenant) + return Token(token) + + +def token_create_scoped(request, tenant, token): + ''' + Creates a scoped token using the tenant id and unscoped token; retrieves + the service catalog for the given tenant. + ''' + if hasattr(request, '_keystone'): + del request._keystone + c = keystoneclient(request, tenant_id=tenant, token_id=token, + endpoint=settings.OPENSTACK_KEYSTONE_URL) + scoped_token = c.tokens.authenticate(tenant=tenant, token=token) + return Token(scoped_token) + + +def user_list(request, tenant_id=None): + return [User(u) for u in + keystoneclient(request).users.list(tenant_id=tenant_id)] + + +def user_create(request, user_id, email, password, tenant_id, enabled): + return User(keystoneclient(request).users.create( + user_id, password, email, tenant_id, enabled)) + + +def user_delete(request, user_id): + keystoneclient(request).users.delete(user_id) + + +def user_get(request, user_id): + return User(keystoneclient(request).users.get(user_id)) + + +def user_update_email(request, user_id, email): + return User(keystoneclient(request).users.update_email(user_id, email)) + + +def user_update_enabled(request, user_id, enabled): + return User(keystoneclient(request).users.update_enabled(user_id, enabled)) + + +def user_update_password(request, user_id, password): + return User(keystoneclient(request).users \ + .update_password(user_id, password)) + + +def user_update_tenant(request, user_id, tenant_id): + return User(keystoneclient(request).users \ + .update_tenant(user_id, tenant_id)) + + +def _get_role(request, name): + roles = keystoneclient(request).roles.list() + for role in roles: + if role.name.lower() == name.lower(): + return role + + raise Exception(_('Role does not exist: %s') % name) + + +def _get_roleref(request, user_id, tenant_id, role): + rolerefs = keystoneclient(request).roles.get_user_role_refs(user_id) + for roleref in rolerefs: + if roleref.roleId == role.id and roleref.tenantId == tenant_id: + return roleref + raise Exception(_('Role "%s" does not exist for that user on this tenant.') + % role.name) + + +def role_add_for_tenant_user(request, tenant_id, user_id, role): + role = _get_role(request, role) + return keystoneclient(request).roles.add_user_to_tenant(tenant_id, + user_id, + role.id) + + +def role_delete_for_tenant_user(request, tenant_id, user_id, role): + role = _get_role(request, role) + roleref = _get_roleref(request, user_id, tenant_id, role) + return keystoneclient(request).roles.remove_user_from_tenant(tenant_id, + user_id, + roleref.id) + + +def service_get(request, name): + return Services(admin_api(request).services.get(name)) + + +@check_openstackx +def service_list(request): + return [Services(s) for s in admin_api(request).services.list()] + + +def service_update(request, name, enabled): + return Services(admin_api(request).services.update(name, enabled)) diff --git a/horizon/horizon/api/nova.py b/horizon/horizon/api/nova.py new file mode 100644 index 00000000..1802d1a4 --- /dev/null +++ b/horizon/horizon/api/nova.py @@ -0,0 +1,357 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import + +import logging + +from django.contrib import messages +from novaclient.v1_1 import client as nova_client + +from horizon.api.base import * +from horizon.api.deprecated import admin_api +from horizon.api.deprecated import compute_api +from horizon.api.deprecated import check_openstackx +from horizon.api.deprecated import extras_api +from horizon.api.deprecated import REBOOT_HARD + +LOG = logging.getLogger(__name__) + + +class Console(APIResourceWrapper): + """Simple wrapper around openstackx.extras.consoles.Console""" + _attrs = ['id', 'output', 'type'] + + +class Flavor(APIResourceWrapper): + """Simple wrapper around openstackx.admin.flavors.Flavor""" + _attrs = ['disk', 'id', 'links', 'name', 'ram', 'vcpus'] + + +class FloatingIp(APIResourceWrapper): + """Simple wrapper for floating ips""" + _attrs = ['ip', 'fixed_ip', 'instance_id', 'id'] + + +class KeyPair(APIResourceWrapper): + """Simple wrapper around openstackx.extras.keypairs.Keypair""" + _attrs = ['fingerprint', 'name', 'private_key'] + + +class VirtualInterface(APIResourceWrapper): + _attrs = ['id', 'mac_address'] + + +class Volume(APIResourceWrapper): + """Nova Volume representation""" + _attrs = ['id', 'status', 'displayName', 'size', 'volumeType', 'createdAt', + 'attachments', 'displayDescription'] + + +class Server(APIResourceWrapper): + """Simple wrapper around openstackx.extras.server.Server + + Preserves the request info so image name can later be retrieved + """ + _attrs = ['addresses', 'attrs', 'hostId', 'id', 'image', 'links', + 'metadata', 'name', 'private_ip', 'public_ip', 'status', 'uuid', + 'image_name', 'VirtualInterfaces'] + + def __init__(self, apiresource, request): + super(Server, self).__init__(apiresource) + self.request = request + + def __getattr__(self, attr): + if attr == "attrs": + return ServerAttributes(super(Server, self).__getattr__(attr)) + else: + return super(Server, self).__getattr__(attr) + + @property + def image_name(self): + from glance.common import exception as glance_exceptions + from horizon.api import glance + try: + image = glance.image_get(self.request, self.image['id']) + return image.name + except glance_exceptions.NotFound: + return "(not found)" + + def reboot(self, hardness=REBOOT_HARD): + compute_api(self.request).servers.reboot(self.id, hardness) + + +class ServerAttributes(APIDictWrapper): + """Simple wrapper around openstackx.extras.server.Server attributes + + Preserves the request info so image name can later be retrieved + """ + _attrs = ['description', 'disk_gb', 'host', 'image_ref', 'kernel_id', + 'key_name', 'launched_at', 'mac_address', 'memory_mb', 'name', + 'os_type', 'tenant_id', 'ramdisk_id', 'scheduled_at', + 'terminated_at', 'user_data', 'user_id', 'vcpus', 'hostname', + 'security_groups'] + + +class Usage(APIResourceWrapper): + """Simple wrapper around openstackx.extras.usage.Usage""" + _attrs = ['begin', 'instances', 'stop', 'tenant_id', + 'total_active_disk_size', 'total_active_instances', + 'total_active_ram_size', 'total_active_vcpus', 'total_cpu_usage', + 'total_disk_usage', 'total_hours', 'total_ram_usage'] + + +class SecurityGroup(APIResourceWrapper): + """Simple wrapper around openstackx.extras.security_groups.SecurityGroup""" + _attrs = ['id', 'name', 'description', 'tenant_id', 'rules'] + + +class SecurityGroupRule(APIResourceWrapper): + """Simple wrapper around + openstackx.extras.security_groups.SecurityGroupRule""" + _attrs = ['id', 'parent_group_id', 'group_id', 'ip_protocol', + 'from_port', 'to_port', 'groups', 'ip_ranges'] + + +class SecurityGroupRule(APIResourceWrapper): + """Simple wrapper around openstackx.extras.users.User""" + _attrs = ['id', 'name', 'description', 'tenant_id', 'security_group_rules'] + + +def novaclient(request): + LOG.debug('novaclient connection created using token "%s" and url "%s"' % + (request.user.token, url_for(request, 'compute'))) + c = nova_client.Client(username=request.user.username, + api_key=request.user.token, + project_id=request.user.tenant_id, + auth_url=url_for(request, 'compute')) + c.client.auth_token = request.user.token + c.client.management_url = url_for(request, 'compute') + return c + + +def console_create(request, instance_id, kind='text'): + return Console(extras_api(request).consoles.create(instance_id, kind)) + + +def flavor_create(request, name, memory, vcpu, disk, flavor_id): + # TODO -- convert to novaclient when novaclient adds create support + return Flavor(admin_api(request).flavors.create( + name, int(memory), int(vcpu), int(disk), flavor_id)) + + +def flavor_delete(request, flavor_id, purge=False): + # TODO -- convert to novaclient when novaclient adds delete support + admin_api(request).flavors.delete(flavor_id, purge) + + +def flavor_get(request, flavor_id): + return Flavor(novaclient(request).flavors.get(flavor_id)) + + +def flavor_list(request): + return [Flavor(f) for f in novaclient(request).flavors.list()] + + +def tenant_floating_ip_list(request): + """ + Fetches a list of all floating ips. + """ + return [FloatingIp(ip) for ip in novaclient(request).floating_ips.list()] + + +def tenant_floating_ip_get(request, floating_ip_id): + """ + Fetches a floating ip. + """ + return novaclient(request).floating_ips.get(floating_ip_id) + + +def tenant_floating_ip_allocate(request): + """ + Allocates a floating ip to tenant. + """ + return novaclient(request).floating_ips.create() + + +def tenant_floating_ip_release(request, floating_ip_id): + """ + Releases floating ip from the pool of a tenant. + """ + return novaclient(request).floating_ips.delete(floating_ip_id) + + +def snapshot_create(request, instance_id, name): + return novaclient(request).servers.create_image(instance_id, name) + + +def keypair_create(request, name): + return KeyPair(novaclient(request).keypairs.create(name)) + + +def keypair_import(request, name, public_key): + return KeyPair(novaclient(request).keypairs.create(name, public_key)) + + +def keypair_delete(request, keypair_id): + novaclient(request).keypairs.delete(keypair_id) + + +def keypair_list(request): + return [KeyPair(key) for key in novaclient(request).keypairs.list()] + + +def server_create(request, name, image, flavor, + key_name, user_data, security_groups): + return Server(novaclient(request).servers.create( + name, image, flavor, userdata=user_data, + security_groups=security_groups, + key_name=key_name), request) + + +def server_delete(request, instance): + compute_api(request).servers.delete(instance) + + +def server_get(request, instance_id): + return Server(extras_api(request).servers.get(instance_id), request) + + +@check_openstackx +def server_list(request): + return [Server(s, request) for s in extras_api(request).servers.list()] + + +@check_openstackx +def admin_server_list(request): + return [Server(s, request) for s in admin_api(request).servers.list()] + + +def server_reboot(request, + instance_id, + hardness=REBOOT_HARD): + server = server_get(request, instance_id) + server.reboot(hardness) + + +def server_update(request, instance_id, name, description): + return extras_api(request).servers.update(instance_id, + name=name, + description=description) + + +def server_add_floating_ip(request, server, address): + """ + Associates floating IP to server's fixed IP. + """ + server = novaclient(request).servers.get(server) + fip = novaclient(request).floating_ips.get(address) + + return novaclient(request).servers.add_floating_ip(server, fip) + + +def server_remove_floating_ip(request, server, address): + """ + Removes relationship between floating and server's fixed ip. + """ + fip = novaclient(request).floating_ips.get(address) + server = novaclient(request).servers.get(fip.instance_id) + + return novaclient(request).servers.remove_floating_ip(server, fip) + + +def tenant_quota_get(request, tenant): + return novaclient(request).quotas.get(tenant) + + +@check_openstackx +def usage_get(request, tenant_id, start, end): + return Usage(extras_api(request).usage.get(tenant_id, start, end)) + + +@check_openstackx +def usage_list(request, start, end): + return [Usage(u) for u in extras_api(request).usage.list(start, end)] + + +def security_group_list(request): + return [SecurityGroup(g) for g in novaclient(request).\ + security_groups.list()] + + +def security_group_get(request, security_group_id): + return SecurityGroup(novaclient(request).\ + security_groups.get(security_group_id)) + + +def security_group_create(request, name, description): + return SecurityGroup(novaclient(request).\ + security_groups.create(name, description)) + + +def security_group_delete(request, security_group_id): + novaclient(request).security_groups.delete(security_group_id) + + +def security_group_rule_create(request, parent_group_id, ip_protocol=None, + from_port=None, to_port=None, cidr=None, + group_id=None): + return SecurityGroup(novaclient(request).\ + security_group_rules.create(parent_group_id, + ip_protocol, + from_port, + to_port, + cidr, + group_id)) + + +def security_group_rule_delete(request, security_group_rule_id): + novaclient(request).security_group_rules.delete(security_group_rule_id) + + +def volume_list(request): + return [Volume(vol) for vol in novaclient(request).volumes.list()] + + +def volume_get(request, volume_id): + return Volume(novaclient(request).volumes.get(volume_id)) + + +def volume_instance_list(request, instance_id): + return novaclient(request).volumes.get_server_volumes(instance_id) + + +def volume_create(request, size, name, description): + return Volume(novaclient(request).volumes.create( + size, name, description)) + + +def volume_delete(request, volume_id): + novaclient(request).volumes.delete(volume_id) + + +def volume_attach(request, volume_id, instance_id, device): + novaclient(request).volumes.create_server_volume( + instance_id, volume_id, device) + + +def volume_detach(request, instance_id, attachment_id): + novaclient(request).volumes.delete_server_volume( + instance_id, attachment_id) diff --git a/horizon/horizon/api/quantum.py b/horizon/horizon/api/quantum.py new file mode 100644 index 00000000..3d2675c3 --- /dev/null +++ b/horizon/horizon/api/quantum.py @@ -0,0 +1,133 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from __future__ import absolute_import + +import logging + +from django.conf import settings +from quantum import client as quantum_client + +from horizon.api.base import * +from horizon.api.deprecated import extras_api + + +LOG = logging.getLogger(__name__) + + +def quantum_api(request): + if hasattr(request, 'user'): + tenant = request.user.tenant_id + else: + tenant = settings.QUANTUM_TENANT + + return quantum_client.Client(settings.QUANTUM_URL, settings.QUANTUM_PORT, + False, tenant, 'json') + + +def quantum_list_networks(request): + return quantum_api(request).list_networks() + + +def quantum_network_details(request, network_id): + return quantum_api(request).show_network_details(network_id) + + +def quantum_list_ports(request, network_id): + return quantum_api(request).list_ports(network_id) + + +def quantum_port_details(request, network_id, port_id): + return quantum_api(request).show_port_details(network_id, port_id) + + +def quantum_create_network(request, data): + return quantum_api(request).create_network(data) + + +def quantum_delete_network(request, network_id): + return quantum_api(request).delete_network(network_id) + + +def quantum_update_network(request, network_id, data): + return quantum_api(request).update_network(network_id, data) + + +def quantum_create_port(request, network_id): + return quantum_api(request).create_port(network_id) + + +def quantum_delete_port(request, network_id, port_id): + return quantum_api(request).delete_port(network_id, port_id) + + +def quantum_attach_port(request, network_id, port_id, data): + return quantum_api(request).attach_resource(network_id, port_id, data) + + +def quantum_detach_port(request, network_id, port_id): + return quantum_api(request).detach_resource(network_id, port_id) + + +def quantum_set_port_state(request, network_id, port_id, data): + return quantum_api(request).set_port_state(network_id, port_id, data) + + +def quantum_port_attachment(request, network_id, port_id): + return quantum_api(request).show_port_attachment(network_id, port_id) + + +def get_vif_ids(request): + vifs = [] + attached_vifs = [] + # Get a list of all networks + networks_list = quantum_api(request).list_networks() + for network in networks_list['networks']: + ports = quantum_api(request).list_ports(network['id']) + # Get port attachments + for port in ports['ports']: + port_attachment = quantum_api(request).show_port_attachment( + network['id'], + port['id']) + if port_attachment['attachment']: + attached_vifs.append( + port_attachment['attachment']['id'].encode('ascii')) + # Get all instances + instances = server_list(request) + # Get virtual interface ids by instance + for instance in instances: + id = instance.id + instance_vifs = extras_api(request).virtual_interfaces.list(id) + for vif in instance_vifs: + # Check if this VIF is already connected to any port + if str(vif.id) in attached_vifs: + vifs.append({ + 'id': vif.id, + 'instance': instance.id, + 'instance_name': instance.name, + 'available': False}) + else: + vifs.append({ + 'id': vif.id, + 'instance': instance.id, + 'instance_name': instance.name, + 'available': True}) + return vifs diff --git a/horizon/horizon/api/swift.py b/horizon/horizon/api/swift.py new file mode 100644 index 00000000..55cfebb5 --- /dev/null +++ b/horizon/horizon/api/swift.py @@ -0,0 +1,132 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +import cloudfiles +from django.conf import settings + +from horizon.api.base import * + + +LOG = logging.getLogger(__name__) + + +class Container(APIResourceWrapper): + """Simple wrapper around cloudfiles.container.Container""" + _attrs = ['name'] + + +class SwiftObject(APIResourceWrapper): + _attrs = ['name'] + + +class SwiftAuthentication(object): + """Auth container to pass CloudFiles storage URL and token from + session. + """ + def __init__(self, storage_url, auth_token): + self.storage_url = storage_url + self.auth_token = auth_token + + def authenticate(self): + return (self.storage_url, '', self.auth_token) + + +def swift_api(request): + LOG.debug('object store connection created using token "%s"' + ' and url "%s"' % + (request.session['token'], url_for(request, 'object-store'))) + auth = SwiftAuthentication(url_for(request, 'object-store'), + request.session['token']) + return cloudfiles.get_connection(auth=auth) + + +def swift_container_exists(request, container_name): + try: + swift_api(request).get_container(container_name) + return True + except cloudfiles.errors.NoSuchContainer: + return False + + +def swift_object_exists(request, container_name, object_name): + container = swift_api(request).get_container(container_name) + + try: + container.get_object(object_name) + return True + except cloudfiles.errors.NoSuchObject: + return False + + +def swift_get_containers(request, marker=None): + return [Container(c) for c in swift_api(request).get_all_containers( + limit=getattr(settings, 'SWIFT_PAGINATE_LIMIT', 10000), + marker=marker)] + + +def swift_create_container(request, name): + if swift_container_exists(request, name): + raise Exception('Container with name %s already exists.' % (name)) + + return Container(swift_api(request).create_container(name)) + + +def swift_delete_container(request, name): + swift_api(request).delete_container(name) + + +def swift_get_objects(request, container_name, prefix=None, marker=None): + container = swift_api(request).get_container(container_name) + objects = container.get_objects(prefix=prefix, marker=marker, + limit=getattr(settings, 'SWIFT_PAGINATE_LIMIT', 10000)) + return [SwiftObject(o) for o in objects] + + +def swift_copy_object(request, orig_container_name, orig_object_name, + new_container_name, new_object_name): + + container = swift_api(request).get_container(orig_container_name) + + if swift_object_exists(request, + new_container_name, + new_object_name) == True: + raise Exception('Object with name %s already exists in container %s' + % (new_object_name, new_container_name)) + + orig_obj = container.get_object(orig_object_name) + return orig_obj.copy_to(new_container_name, new_object_name) + + +def swift_upload_object(request, container_name, object_name, object_data): + container = swift_api(request).get_container(container_name) + obj = container.create_object(object_name) + obj.write(object_data) + + +def swift_delete_object(request, container_name, object_name): + container = swift_api(request).get_container(container_name) + container.delete_object(object_name) + + +def swift_get_object_data(request, container_name, object_name): + container = swift_api(request).get_container(container_name) + return container.get_object(object_name).stream() diff --git a/horizon/horizon/base.py b/horizon/horizon/base.py new file mode 100644 index 00000000..10ebfd78 --- /dev/null +++ b/horizon/horizon/base.py @@ -0,0 +1,594 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Contains the core classes and functionality that makes Horizon what it is. +This module is considered internal, and should not be relied on directly. + +Public APIs are made available through the :mod:`horizon` module and +the classes contained therein. +""" + +import copy +import functools +import inspect + +from django.conf import settings +from django.conf.urls.defaults import patterns, url, include +from django.core.exceptions import ImproperlyConfigured +from django.core.urlresolvers import reverse, RegexURLPattern +from django.utils.functional import SimpleLazyObject +from django.utils.importlib import import_module +from django.utils.module_loading import module_has_submodule +from django.utils.translation import ugettext as _ + +from horizon.decorators import require_roles, _current_component + + +# Default configuration dictionary. Do not mutate directly. Use copy.copy(). +HORIZON_CONFIG = { + # Allow for ordering dashboards; list or tuple if provided. + 'dashboards': None, + # Name of a default dashboard; defaults to first alphabetically if None + 'default_dashboard': None, + 'user_home': None, +} + + +def _decorate_urlconf(urlpatterns, decorator, *args, **kwargs): + for pattern in urlpatterns: + if getattr(pattern, 'callback', None): + pattern._callback = decorator(pattern.callback, *args, **kwargs) + if getattr(pattern, 'url_patterns', []): + _decorate_urlconf(pattern.url_patterns, decorator, *args, **kwargs) + + +class NotRegistered(Exception): + pass + + +class HorizonComponent(object): + def __init__(self): + super(HorizonComponent, self).__init__() + if not self.slug: + raise ImproperlyConfigured('Every %s must have a slug.' + % self.__class__) + + def __unicode__(self): + return getattr(self, 'name', u"Unnamed %s" % self.__class__.__name__) + + def _get_default_urlpatterns(self): + package_string = '.'.join(self.__module__.split('.')[:-1]) + if getattr(self, 'urls', None): + try: + mod = import_module('.%s' % self.urls, package_string) + except ImportError: + mod = import_module(self.urls) + urlpatterns = mod.urlpatterns + else: + # Try importing a urls.py from the dashboard package + if module_has_submodule(import_module(package_string), 'urls'): + urls_mod = import_module('.urls', package_string) + urlpatterns = urls_mod.urlpatterns + else: + urlpatterns = patterns('') + return urlpatterns + + +class Registry(object): + def __init__(self): + self._registry = {} + if not getattr(self, '_registerable_class', None): + raise ImproperlyConfigured('Subclasses of Registry must set a ' + '"_registerable_class" property.') + + def _register(self, cls): + """Registers the given class. + + If the specified class is already registered then it is ignored. + """ + if not inspect.isclass(cls): + raise ValueError('Only classes may be registered.') + elif not issubclass(cls, self._registerable_class): + raise ValueError('Only %s classes or subclasses may be registered.' + % self._registerable_class) + + if cls not in self._registry: + cls._registered_with = self + self._registry[cls] = cls() + + return self._registry[cls] + + def _unregister(self, cls): + """Unregisters the given class. + + If the specified class isn't registered, ``NotRegistered`` will + be raised. + """ + if not issubclass(cls, self._registerable_class): + raise ValueError('Only %s classes or subclasses may be ' + 'unregistered.' % self._registerable_class) + + if cls not in self._registry.keys(): + raise NotRegistered('%s is not registered' % cls) + + del self._registry[cls] + + return True + + def _registered(self, cls): + if inspect.isclass(cls) and issubclass(cls, self._registerable_class): + cls = self._registry.get(cls, None) + if cls: + return cls + else: + # Allow for fetching by slugs as well. + for registered in self._registry.values(): + if registered.slug == cls: + return registered + raise NotRegistered('%s with slug "%s" is not registered' + % (self._registerable_class, cls)) + + +class Panel(HorizonComponent): + """ A base class for defining Horizon dashboard panels. + + All Horizon dashboard panels should extend from this class. It provides + the appropriate hooks for automatically constructing URLconfs, and + providing role-based access control. + + .. attribute:: name + + The name of the panel. This will be displayed in the + auto-generated navigation and various other places. + Default: ``''``. + + .. attribute:: slug + + A unique "short name" for the panel. The slug is used as + a component of the URL path for the panel. Default: ``''``. + + .. attribute: roles + + A list of role names, all of which a user must possess in order + to access any view associated with this panel. This attribute + is combined cumulatively with any roles required on the + ``Dashboard`` class with which it is registered. + + .. attribute:: urls + + Path to a URLconf of views for this panel using dotted Python + notation. If no value is specified, a file called ``urls.py`` + living in the same package as the ``panel.py`` file is used. + Default: ``None``. + + .. attribute:: nav + .. method:: nav(context) + + The ``nav`` attribute can be either boolean value or a callable + which accepts a ``RequestContext`` object as a single argument + to control whether or not this panel should appear in + automatically-generated navigation. Default: ``True``. + """ + name = '' + slug = '' + urls = None + nav = True + + def __repr__(self): + return "" % self.__unicode__() + + def get_absolute_url(self): + """ Returns the default URL for this panel. + + The default URL is defined as the URL pattern with ``name="index"`` in + the URLconf for this panel. + """ + return reverse('horizon:%s:%s:index' % (self._registered_with.slug, + self.slug,)) + + @property + def _decorated_urls(self): + urlpatterns = self._get_default_urlpatterns() + + # Apply access controls to all views in the patterns + roles = getattr(self, 'roles', []) + _decorate_urlconf(urlpatterns, require_roles, roles) + _decorate_urlconf(urlpatterns, _current_component, panel=self) + + # Return the three arguments to django.conf.urls.defaults.include + return urlpatterns, self.slug, self.slug + + +class Dashboard(Registry, HorizonComponent): + """ A base class for defining Horizon dashboards. + + All Horizon dashboards should extend from this base class. It provides the + appropriate hooks for automatic discovery of :class:`~horizon.Panel` + modules, automatically constructing URLconfs, and providing role-based + access control. + + .. attribute:: name + + The name of the dashboard. This will be displayed in the + auto-generated navigation and various other places. + Default: ``''``. + + .. attribute:: slug + + A unique "short name" for the dashboard. The slug is used as + a component of the URL path for the dashboard. Default: ``''``. + + .. attribute:: panels + + The ``panels`` attribute can be either a list containing the name + of each panel module which should be loaded as part of this + dashboard, or a dictionary of tuples which define groups of + as in the following example:: + + class Syspanel(horizon.Dashboard): + panels = {'System Panel': ('overview', 'instances', ...)} + + Automatically generated navigation will use the order of the + modules in this attribute. Default: ``[]``. + + Panel modules must be listed in ``panels`` in order to be + discovered by the automatic registration mechanism. + + .. attribute:: default_panel + + The name of the panel which should be treated as the default + panel for the dashboard, i.e. when you visit the root URL + for this dashboard, that's the panel that is displayed. + Default: ``None``. + + .. attribute: roles + + A list of role names, all of which a user must possess in order + to access any panel registered with this dashboard. This attribute + is combined cumulatively with any roles required on individual + :class:`~horizon.Panel` classes. + + .. attribute:: urls + + Optional path to a URLconf of additional views for this dashboard + which are not connected to specific panels. Default: ``None``. + + .. attribute:: nav + + Optional boolean to control whether or not this dashboard should + appear in automatically-generated navigation. Default: ``True``. + """ + _registerable_class = Panel + name = '' + slug = '' + urls = None + panels = [] + default_panel = None + nav = True + + def __repr__(self): + return "" % self.__unicode__() + + def get_panel(self, panel): + """ + Returns the specified :class:`~horizon.Panel` instance registered + with this dashboard. + """ + return self._registered(panel) + + def get_panels(self): + """ + Returns the :class:`~horizon.Panel` instances registered with this + dashboard in order. + """ + registered = copy.copy(self._registry) + if type(self.panels) is dict: + panels = {} + for heading, items in self.panels.iteritems(): + panels.setdefault(heading, []) + for item in items: + panel = self._registered(item) + panels[heading].append(panel) + registered.pop(panel.__class__) + if len(registered): + panels.setdefault(_("Other"), []).extend(registered.values()) + else: + panels = [] + for item in self.panels: + panel = self._registered(item) + panels.append(panel) + registered.pop(panel.__class__) + panels.extend(registered.values()) + return panels + + def get_absolute_url(self): + """ Returns the default URL for this dashboard. + + The default URL is defined as the URL pattern with ``name="index"`` + in the URLconf for the :class:`~horizon.Panel` specified by + :attr:`~horizon.Dashboard.default_panel`. + """ + return self._registered(self.default_panel).get_absolute_url() + + @property + def _decorated_urls(self): + urlpatterns = self._get_default_urlpatterns() + + self._autodiscover() + default_panel = None + + # Add in each panel's views except for the default view. + for panel in self._registry.values(): + if panel.slug == self.default_panel: + default_panel = panel + continue + urlpatterns += patterns('', + url(r'^%s/' % panel.slug, include(panel._decorated_urls))) + # Now the default view, which should come last + if not default_panel: + raise NotRegistered('The default panel "%s" is not registered.' + % self.default_panel) + urlpatterns += patterns('', + url(r'', include(default_panel._decorated_urls))) + + # Apply access controls to all views in the patterns + roles = getattr(self, 'roles', []) + _decorate_urlconf(urlpatterns, require_roles, roles) + _decorate_urlconf(urlpatterns, _current_component, dashboard=self) + + # Return the three arguments to django.conf.urls.defaults.include + return urlpatterns, self.slug, self.slug + + def _autodiscover(self): + """ Discovers panels to register from the current dashboard module. """ + package = '.'.join(self.__module__.split('.')[:-1]) + mod = import_module(package) + panels = [] + if type(self.panels) is dict: + [panels.extend(values) for values in self.panels.values()] + else: + panels = self.panels + for panel in panels: + try: + before_import_registry = copy.copy(self._registry) + import_module('.%s.panel' % panel, package) + except: + self._registry = before_import_registry + if module_has_submodule(mod, panel): + raise + + @classmethod + def register(cls, panel): + """ Registers a :class:`~horizon.Panel` with this dashboard. """ + from horizon import Horizon + return Horizon.register_panel(cls, panel) + + @classmethod + def unregister(cls, panel): + """ Unregisters a :class:`~horizon.Panel` from this dashboard. """ + from horizon import Horizon + return Horizon.unregister_panel(cls, panel) + + +class Workflow(object): + def __init__(*args, **kwargs): + raise NotImplementedError() + + +class LazyURLPattern(SimpleLazyObject): + def __iter__(self): + if self._wrapped is None: + self._setup() + return iter(self._wrapped) + + def __reversed__(self): + if self._wrapped is None: + self._setup() + return reversed(self._wrapped) + + +class Site(Registry, HorizonComponent): + """ The core OpenStack Dashboard class. """ + # Required for registry + _registerable_class = Dashboard + + name = "Horizon" + namespace = 'horizon' + slug = 'horizon' + urls = 'horizon.site_urls' + + def __repr__(self): + return u"" % self.__unicode__() + + @property + def _conf(self): + conf = copy.copy(HORIZON_CONFIG) + conf.update(getattr(settings, 'HORIZON_CONFIG', {})) + return conf + + @property + def dashboards(self): + return self._conf['dashboards'] + + @property + def default_dashboard(self): + return self._conf['default_dashboard'] + + def register(self, dashboard): + """ Registers a :class:`~horizon.Dashboard` with Horizon.""" + return self._register(dashboard) + + def unregister(self, dashboard): + """ Unregisters a :class:`~horizon.Dashboard` from Horizon. """ + return self._unregister(dashboard) + + def registered(self, dashboard): + return self._registered(dashboard) + + def register_panel(self, dashboard, panel): + dash_instance = self.registered(dashboard) + return dash_instance._register(panel) + + def unregister_panel(self, dashboard, panel): + dash_instance = self.registered(dashboard) + if not dash_instance: + raise NotRegistered("The dashboard %s is not registered." + % dashboard) + return dash_instance._unregister(panel) + + def get_dashboard(self, dashboard): + """ Returns the specified :class:`~horizon.Dashboard` instance. """ + return self._registered(dashboard) + + def get_dashboards(self): + """ Returns an ordered tuple of :class:`~horizon.Dashboard` modules. + + Orders dashboards according to the ``"dashboards"`` key in + ``settings.HORIZON_CONFIG`` or else returns all registered dashboards + in alphabetical order. + + Any remaining :class:`~horizon.Dashboard` classes registered with + Horizon but not listed in ``settings.HORIZON_CONFIG['dashboards']`` + will be appended to the end of the list alphabetically. + """ + if self.dashboards: + registered = copy.copy(self._registry) + dashboards = [] + for item in self.dashboards: + dashboard = self._registered(item) + dashboards.append(dashboard) + registered.pop(dashboard.__class__) + if len(registered): + extra = registered.values() + extra.sort() + dashboards.extend(extra) + return dashboards + else: + dashboards = self._registry.values() + dashboards.sort() + return dashboards + + def get_default_dashboard(self): + """ Returns the default :class:`~horizon.Dashboard` instance. + + If ``"default_dashboard"`` is specified in ``settings.HORIZON_CONFIG`` + then that dashboard will be returned. If not, the first dashboard + returned by :func:`~horizon.get_dashboards` will be returned. + """ + if self.default_dashboard: + return self._registered(self.default_dashboard) + elif len(self._registry): + return self.get_dashboards()[0] + else: + raise NotRegistered("No dashboard modules have been registered.") + + def get_user_home(self, user): + """ Returns the default URL for a particular user. + + This method can be used to customize where a user is sent when + they log in, etc. By default it returns the value of + :meth:`get_absolute_url`. + + An alternative function can be supplied to customize this behavior + by specifying a either a URL or a function which returns a URL via + the ``"user_home"`` key in ``settings.HORIZON_CONFIG``. Each of these + would be valid:: + + {"user_home": "/home",} # A URL + {"user_home": "my_module.get_user_home",} # Path to a function + {"user_home": lambda user: "/" + user.name,} # A function + + This can be useful if the default dashboard may not be accessible + to all users. + """ + user_home = self._conf['user_home'] + if user_home: + if callable(user_home): + return user_home(user) + elif isinstance(user_home, basestring): + # Assume we've got a URL if there's a slash in it + if user_home.find("/") != -1: + return user_home + else: + mod, func = user_home.rsplit(".", 1) + return getattr(import_module(mod), func)(user) + # If it's not callable and not a string, it's wrong. + raise ValueError('The user_home setting must be either a string ' + 'or a callable object (e.g. a function).') + else: + return self.get_absolute_url() + + def get_absolute_url(self): + """ Returns the default URL for Horizon's URLconf. + + The default URL is determined by calling + :meth:`~horizon.Dashboard.get_absolute_url` + on the :class:`~horizon.Dashboard` instance returned by + :meth:`~horizon.get_default_dashboard`. + """ + return self.get_default_dashboard().get_absolute_url() + + @property + def _lazy_urls(self): + """ Lazy loading for URL patterns. + + This method avoids problems associated with attempting to evaluate + the the URLconf before the settings module has been loaded. + """ + def url_patterns(): + return self._urls()[0] + + return LazyURLPattern(url_patterns), self.namespace, self.slug + + def _urls(self): + """ Constructs the URLconf for Horizon from registered Dashboards. """ + urlpatterns = self._get_default_urlpatterns() + + self._autodiscover() + + # Add in each dashboard's views. + for dash in self._registry.values(): + urlpatterns += patterns('', + url(r'^%s/' % dash.slug, include(dash._decorated_urls))) + + # Return the three arguments to django.conf.urls.defaults.include + return urlpatterns, self.namespace, self.slug + + def _autodiscover(self): + """ Discovers modules to register from ``settings.INSTALLED_APPS``. + + This makes sure that the appropriate modules get imported to register + themselves with Horizon. + """ + if not getattr(self, '_registerable_class', None): + raise ImproperlyConfigured('You must set a ' + '"_registerable_class" property ' + 'in order to use autodiscovery.') + # Discover both dashboards and panels, in that order + for mod_name in ('dashboard', 'panel'): + for app in settings.INSTALLED_APPS: + mod = import_module(app) + try: + before_import_registry = copy.copy(self._registry) + import_module('%s.%s' % (app, mod_name)) + except: + self._registry = before_import_registry + if module_has_submodule(mod, mod_name): + raise + +# The one true Horizon +Horizon = Site() diff --git a/horizon/horizon/context_processors.py b/horizon/horizon/context_processors.py new file mode 100644 index 00000000..9f0a9878 --- /dev/null +++ b/horizon/horizon/context_processors.py @@ -0,0 +1,69 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Context processors used by Horizon. +""" + +from django.conf import settings +from django.contrib import messages + +from horizon import api + + +def horizon(request): + """ The main Horizon context processor. Required for Horizon to function. + + Adds three variables to the request context: + + ``tenants`` + A list of the tenants the current uses is authorized to access. + + ``object_store_configured`` + Boolean. Will be ``True`` if there is a service of type + ``object-store`` in the user's ``ServiceCatalog``. + + ``network_configured`` + Boolean. Will be ``True`` if ``settings.QUANTUM_ENABLED`` is ``True``. + """ + context = {} + + # Auth/Keystone context + if request.user.is_authenticated(): + try: + tenants = api.tenant_list_for_token(request, request.user.token) + context['tenants'] = tenants + except Exception, e: + if hasattr(request.user, 'message_set'): + messages.error(request, _("Unable to retrieve tenant list from\ + keystone: %s") % e.message) + context['tenants'] = [] + + # Object Store/Swift context + catalog = getattr(request.user, 'service_catalog', []) + object_store = catalog and api.get_service_from_catalog(catalog, + 'object-store') + context['object_store_configured'] = object_store + + # Quantum context + # TODO(gabriel): Convert to service catalog check when Quantum starts + # supporting keystone integration. + context['network_configured'] = getattr(settings, 'QUANTUM_ENABLED', None) + + return context diff --git a/horizon/horizon/dashboards/__init__.py b/horizon/horizon/dashboards/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/nova/__init__.py b/horizon/horizon/dashboards/nova/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/nova/containers/__init__.py b/horizon/horizon/dashboards/nova/containers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/nova/containers/forms.py b/horizon/horizon/dashboards/nova/containers/forms.py new file mode 100644 index 00000000..576f2356 --- /dev/null +++ b/horizon/horizon/dashboards/nova/containers/forms.py @@ -0,0 +1,142 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from cloudfiles.errors import ContainerNotEmpty +from django import shortcuts +from django.contrib import messages + +from horizon import api +from horizon import forms + + +LOG = logging.getLogger(__name__) + + +class DeleteContainer(forms.SelfHandlingForm): + container_name = forms.CharField(widget=forms.HiddenInput()) + + def handle(self, request, data): + try: + api.swift_delete_container(request, data['container_name']) + except ContainerNotEmpty, e: + messages.error(request, + _('Unable to delete non-empty container: %s') % + data['container_name']) + LOG.exception('Unable to delete container "%s". Exception: "%s"' % + (data['container_name'], str(e))) + else: + messages.info(request, + _('Successfully deleted container: %s') % \ + data['container_name']) + return shortcuts.redirect(request.build_absolute_uri()) + + +class CreateContainer(forms.SelfHandlingForm): + name = forms.CharField(max_length="255", label=_("Container Name")) + + def handle(self, request, data): + api.swift_create_container(request, data['name']) + messages.success(request, _("Container was successfully created.")) + return shortcuts.redirect("horizon:nova:containers:index") + + +class FilterObjects(forms.SelfHandlingForm): + container_name = forms.CharField(widget=forms.HiddenInput()) + object_prefix = forms.CharField(required=False) + + def handle(self, request, data): + object_prefix = data['object_prefix'] or None + + objects = api.swift_get_objects(request, + data['container_name'], + prefix=object_prefix) + + if not objects: + messages.info(request, + _('There are no objects matching that prefix in %s') % + data['container_name']) + + return objects + + +class DeleteObject(forms.SelfHandlingForm): + object_name = forms.CharField(widget=forms.HiddenInput()) + container_name = forms.CharField(widget=forms.HiddenInput()) + + def handle(self, request, data): + api.swift_delete_object( + request, + data['container_name'], + data['object_name']) + messages.info(request, + _('Successfully deleted object: %s') % + data['object_name']) + return shortcuts.redirect(request.build_absolute_uri()) + + +class UploadObject(forms.SelfHandlingForm): + name = forms.CharField(max_length="255", label=_("Object Name")) + object_file = forms.FileField(label=_("File")) + container_name = forms.CharField(widget=forms.HiddenInput()) + + def handle(self, request, data): + api.swift_upload_object( + request, + data['container_name'], + data['name'], + self.files['object_file'].read()) + + messages.success(request, _("Object was successfully uploaded.")) + return shortcuts.redirect(request.build_absolute_uri()) + + +class CopyObject(forms.SelfHandlingForm): + new_container_name = forms.ChoiceField( + label=_("Container to store object in")) + + new_object_name = forms.CharField(max_length="255", + label=_("New object name")) + orig_container_name = forms.CharField(widget=forms.HiddenInput()) + orig_object_name = forms.CharField(widget=forms.HiddenInput()) + + def __init__(self, *args, **kwargs): + containers = kwargs.pop('containers') + + super(CopyObject, self).__init__(*args, **kwargs) + + self.fields['new_container_name'].choices = containers + + def handle(self, request, data): + orig_container_name = data['orig_container_name'] + orig_object_name = data['orig_object_name'] + new_container_name = data['new_container_name'] + new_object_name = data['new_object_name'] + + api.swift_copy_object(request, orig_container_name, + orig_object_name, new_container_name, + new_object_name) + + messages.success(request, + _('Object was successfully copied to %(container)s\%(obj)s') % + {"container": new_container_name, "obj": new_object_name}) + + return shortcuts.redirect(request.build_absolute_uri()) diff --git a/horizon/horizon/dashboards/nova/containers/panel.py b/horizon/horizon/dashboards/nova/containers/panel.py new file mode 100644 index 00000000..06c95289 --- /dev/null +++ b/horizon/horizon/dashboards/nova/containers/panel.py @@ -0,0 +1,35 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.utils.translation import ugettext as _ + +import horizon +from horizon.dashboards.nova import dashboard + + +class Containers(horizon.Panel): + name = _("Containers") + slug = 'containers' + + def nav(self, context): + return context['object_store_configured'] + + +dashboard.Nova.register(Containers) diff --git a/horizon/horizon/dashboards/nova/containers/tests.py b/horizon/horizon/dashboards/nova/containers/tests.py new file mode 100644 index 00000000..3be11297 --- /dev/null +++ b/horizon/horizon/dashboards/nova/containers/tests.py @@ -0,0 +1,307 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import tempfile + +from cloudfiles.errors import ContainerNotEmpty +from django import http +from django.contrib import messages +from django.core.urlresolvers import reverse +from mox import IgnoreArg, IsA + +from horizon import api +from horizon import test + + +CONTAINER_INDEX_URL = reverse('horizon:nova:containers:index') + + +class ContainerViewTests(test.BaseViewTests): + def setUp(self): + super(ContainerViewTests, self).setUp() + self.container = self.mox.CreateMock(api.Container) + self.container.name = 'containerName' + + def test_index(self): + self.mox.StubOutWithMock(api, 'swift_get_containers') + api.swift_get_containers( + IsA(http.HttpRequest), marker=None).AndReturn([self.container]) + + self.mox.ReplayAll() + + res = self.client.get(CONTAINER_INDEX_URL) + + self.assertTemplateUsed(res, 'nova/containers/index.html') + self.assertIn('containers', res.context) + containers = res.context['containers'] + + self.assertEqual(len(containers), 1) + self.assertEqual(containers[0].name, 'containerName') + + self.mox.VerifyAll() + + def test_delete_container(self): + formData = {'container_name': 'containerName', + 'method': 'DeleteContainer'} + + self.mox.StubOutWithMock(api, 'swift_delete_container') + api.swift_delete_container(IsA(http.HttpRequest), + 'containerName') + + self.mox.ReplayAll() + + res = self.client.post(CONTAINER_INDEX_URL, formData) + + self.assertRedirectsNoFollow(res, CONTAINER_INDEX_URL) + + self.mox.VerifyAll() + + def test_delete_container_nonempty(self): + formData = {'container_name': 'containerName', + 'method': 'DeleteContainer'} + + exception = ContainerNotEmpty('containerNotEmpty') + + self.mox.StubOutWithMock(api, 'swift_delete_container') + api.swift_delete_container( + IsA(http.HttpRequest), + 'containerName').AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + + messages.error(IgnoreArg(), IsA(unicode)) + + self.mox.ReplayAll() + + res = self.client.post(CONTAINER_INDEX_URL, formData) + + self.assertRedirectsNoFollow(res, CONTAINER_INDEX_URL) + + self.mox.VerifyAll() + + def test_create_container_get(self): + res = self.client.get(reverse('horizon:nova:containers:create')) + + self.assertTemplateUsed(res, 'nova/containers/create.html') + + def test_create_container_post(self): + formData = {'name': 'containerName', + 'method': 'CreateContainer'} + + self.mox.StubOutWithMock(api, 'swift_create_container') + api.swift_create_container( + IsA(http.HttpRequest), 'CreateContainer') + + self.mox.StubOutWithMock(messages, 'success') + messages.success(IgnoreArg(), IsA(basestring)) + + res = self.client.post(reverse('horizon:nova:containers:create'), + formData) + + self.assertRedirectsNoFollow(res, CONTAINER_INDEX_URL) + + +class ObjectViewTests(test.BaseViewTests): + CONTAINER_NAME = 'containerName' + + def setUp(self): + super(ObjectViewTests, self).setUp() + swift_object = self.mox.CreateMock(api.SwiftObject) + self.swift_objects = [swift_object] + + def test_index(self): + self.mox.StubOutWithMock(api, 'swift_get_objects') + api.swift_get_objects( + IsA(http.HttpRequest), + self.CONTAINER_NAME, + marker=None).AndReturn(self.swift_objects) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:containers:object_index', + args=[self.CONTAINER_NAME])) + self.assertTemplateUsed(res, 'nova/objects/index.html') + self.assertItemsEqual(res.context['objects'], self.swift_objects) + + self.mox.VerifyAll() + + def test_upload_index(self): + res = self.client.get(reverse('horizon:nova:containers:object_upload', + args=[self.CONTAINER_NAME])) + + self.assertTemplateUsed(res, 'nova/objects/upload.html') + + def test_upload(self): + OBJECT_DATA = 'objectData' + OBJECT_FILE = tempfile.TemporaryFile() + OBJECT_FILE.write(OBJECT_DATA) + OBJECT_FILE.flush() + OBJECT_FILE.seek(0) + OBJECT_NAME = 'objectName' + + formData = {'method': 'UploadObject', + 'container_name': self.CONTAINER_NAME, + 'name': OBJECT_NAME, + 'object_file': OBJECT_FILE} + + self.mox.StubOutWithMock(api, 'swift_upload_object') + api.swift_upload_object(IsA(http.HttpRequest), + unicode(self.CONTAINER_NAME), + unicode(OBJECT_NAME), + OBJECT_DATA) + + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:nova:containers:object_upload', + args=[self.CONTAINER_NAME]), + formData) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:containers:object_upload', + args=[self.CONTAINER_NAME])) + + self.mox.VerifyAll() + + def test_delete(self): + OBJECT_NAME = 'objectName' + formData = {'method': 'DeleteObject', + 'container_name': self.CONTAINER_NAME, + 'object_name': OBJECT_NAME} + + self.mox.StubOutWithMock(api, 'swift_delete_object') + api.swift_delete_object( + IsA(http.HttpRequest), + self.CONTAINER_NAME, OBJECT_NAME) + + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:nova:containers:object_index', + args=[self.CONTAINER_NAME]), + formData) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:containers:object_index', + args=[self.CONTAINER_NAME])) + + self.mox.VerifyAll() + + def test_download(self): + OBJECT_DATA = 'objectData' + OBJECT_NAME = 'objectName' + + self.mox.StubOutWithMock(api, 'swift_get_object_data') + api.swift_get_object_data(IsA(http.HttpRequest), + unicode(self.CONTAINER_NAME), + unicode(OBJECT_NAME)).AndReturn(OBJECT_DATA) + + self.mox.ReplayAll() + + res = self.client.get(reverse( + 'horizon:nova:containers:object_download', + args=[self.CONTAINER_NAME, OBJECT_NAME])) + + self.assertEqual(res.content, OBJECT_DATA) + self.assertTrue(res.has_header('Content-Disposition')) + + self.mox.VerifyAll() + + def test_copy_index(self): + OBJECT_NAME = 'objectName' + + container = self.mox.CreateMock(api.Container) + container.name = self.CONTAINER_NAME + + self.mox.StubOutWithMock(api, 'swift_get_containers') + api.swift_get_containers( + IsA(http.HttpRequest)).AndReturn([container]) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:containers:object_copy', + args=[self.CONTAINER_NAME, + OBJECT_NAME])) + + self.assertTemplateUsed(res, 'nova/objects/copy.html') + + self.mox.VerifyAll() + + def test_copy(self): + NEW_CONTAINER_NAME = self.CONTAINER_NAME + NEW_OBJECT_NAME = 'newObjectName' + ORIG_CONTAINER_NAME = 'origContainerName' + ORIG_OBJECT_NAME = 'origObjectName' + + formData = {'method': 'CopyObject', + 'new_container_name': NEW_CONTAINER_NAME, + 'new_object_name': NEW_OBJECT_NAME, + 'orig_container_name': ORIG_CONTAINER_NAME, + 'orig_object_name': ORIG_OBJECT_NAME} + + container = self.mox.CreateMock(api.Container) + container.name = self.CONTAINER_NAME + + self.mox.StubOutWithMock(api, 'swift_get_containers') + api.swift_get_containers( + IsA(http.HttpRequest)).AndReturn([container]) + + self.mox.StubOutWithMock(api, 'swift_copy_object') + api.swift_copy_object(IsA(http.HttpRequest), + ORIG_CONTAINER_NAME, + ORIG_OBJECT_NAME, + NEW_CONTAINER_NAME, + NEW_OBJECT_NAME) + + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:nova:containers:object_copy', + args=[ORIG_CONTAINER_NAME, + ORIG_OBJECT_NAME]), + formData) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:containers:object_copy', + args=[ORIG_CONTAINER_NAME, + ORIG_OBJECT_NAME])) + + self.mox.VerifyAll() + + def test_filter(self): + PREFIX = 'prefix' + + formData = {'method': 'FilterObjects', + 'container_name': self.CONTAINER_NAME, + 'object_prefix': PREFIX, + } + + self.mox.StubOutWithMock(api, 'swift_get_objects') + api.swift_get_objects(IsA(http.HttpRequest), + unicode(self.CONTAINER_NAME), + prefix=unicode(PREFIX))\ + .AndReturn(self.swift_objects) + + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:nova:containers:object_index', + args=[self.CONTAINER_NAME]), + formData) + + self.assertTemplateUsed(res, 'nova/objects/index.html') + + self.mox.VerifyAll() diff --git a/horizon/horizon/dashboards/nova/containers/urls.py b/horizon/horizon/dashboards/nova/containers/urls.py new file mode 100644 index 00000000..5ad9a367 --- /dev/null +++ b/horizon/horizon/dashboards/nova/containers/urls.py @@ -0,0 +1,36 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls.defaults import patterns, url + + +OBJECTS = r'^(?P[^/]+)/%s$' + + +# Swift containers and objects. +urlpatterns = patterns('horizon.dashboards.nova.containers.views', + url(r'^$', 'index', name='index'), + url(r'^create/$', 'create', name='create'), + url(OBJECTS % '', 'object_index', name='object_index'), + url(OBJECTS % 'upload', 'object_upload', name='object_upload'), + url(OBJECTS % '(?P[^/]+)/copy', + 'object_copy', name='object_copy'), + url(OBJECTS % '(?P[^/]+)/download', + 'object_download', name='object_download')) diff --git a/horizon/horizon/dashboards/nova/containers/views.py b/horizon/horizon/dashboards/nova/containers/views.py new file mode 100644 index 00000000..2f89709e --- /dev/null +++ b/horizon/horizon/dashboards/nova/containers/views.py @@ -0,0 +1,134 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Views for managing Swift containers. +""" +import logging + +from django import http +from django.contrib.auth.decorators import login_required +from django import shortcuts +from django.utils.translation import ugettext as _ + +from horizon import api +from horizon.dashboards.nova.containers.forms import (DeleteContainer, + CreateContainer, FilterObjects, DeleteObject, UploadObject, CopyObject) + + +LOG = logging.getLogger(__name__) + + +@login_required +def index(request): + marker = request.GET.get('marker', None) + + delete_form, handled = DeleteContainer.maybe_handle(request) + if handled: + return handled + + containers = api.swift_get_containers(request, marker=marker) + + return shortcuts.render(request, + 'nova/containers/index.html', + {'containers': containers, + 'delete_form': delete_form}) + + +@login_required +def create(request): + form, handled = CreateContainer.maybe_handle(request) + if handled: + return handled + + return shortcuts.render(request, + 'nova/containers/create.html', + {'create_form': form}) + + +@login_required +def object_index(request, container_name): + marker = request.GET.get('marker', None) + + delete_form, handled = DeleteObject.maybe_handle(request) + if handled: + return handled + + filter_form, objects = FilterObjects.maybe_handle(request) + + if objects is None: + filter_form.fields['container_name'].initial = container_name + objects = api.swift_get_objects(request, container_name, marker=marker) + + delete_form.fields['container_name'].initial = container_name + return shortcuts.render(request, + 'nova/objects/index.html', + {'container_name': container_name, + 'objects': objects, + 'delete_form': delete_form, + 'filter_form': filter_form}) + + +@login_required +def object_upload(request, container_name): + form, handled = UploadObject.maybe_handle(request) + if handled: + return handled + + form.fields['container_name'].initial = container_name + return shortcuts.render(request, + 'nova/objects/upload.html', + {'container_name': container_name, + 'upload_form': form}) + + +@login_required +def object_download(request, container_name, object_name): + object_data = api.swift_get_object_data( + request, container_name, object_name) + + response = http.HttpResponse() + response['Content-Disposition'] = 'attachment; filename=%s' % \ + object_name + for data in object_data: + response.write(data) + return response + + +@login_required +def object_copy(request, container_name, object_name): + containers = \ + [(c.name, c.name) for c in api.swift_get_containers( + request)] + form, handled = CopyObject.maybe_handle(request, + containers=containers) + + if handled: + return handled + + form.fields['new_container_name'].initial = container_name + form.fields['orig_container_name'].initial = container_name + form.fields['orig_object_name'].initial = object_name + + return shortcuts.render(request, + 'nova/objects/copy.html', + {'container_name': container_name, + 'object_name': object_name, + 'copy_form': form}) diff --git a/horizon/horizon/dashboards/nova/dashboard.py b/horizon/horizon/dashboards/nova/dashboard.py new file mode 100644 index 00000000..380b2710 --- /dev/null +++ b/horizon/horizon/dashboards/nova/dashboard.py @@ -0,0 +1,33 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.utils.translation import ugettext as _ + +import horizon + + +class Nova(horizon.Dashboard): + name = "User Dashboard" + slug = "nova" + panels = {_("Manage Compute"): ('overview', 'instances', 'images', + 'snapshots', 'keypairs', 'volumes', + 'floating_ips', 'security_groups',), + _("Network"): ('networks',), + _("Object Store"): ('containers',)} + default_panel = 'overview' + + +horizon.register(Nova) diff --git a/horizon/horizon/dashboards/nova/floating_ips/__init__.py b/horizon/horizon/dashboards/nova/floating_ips/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/nova/floating_ips/forms.py b/horizon/horizon/dashboards/nova/floating_ips/forms.py new file mode 100644 index 00000000..f7100e3b --- /dev/null +++ b/horizon/horizon/dashboards/nova/floating_ips/forms.py @@ -0,0 +1,123 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django import shortcuts +from django.utils.translation import ugettext as _ +from novaclient import exceptions as novaclient_exceptions + +from horizon import api +from horizon import forms + + +LOG = logging.getLogger(__name__) + + +class ReleaseFloatingIp(forms.SelfHandlingForm): + floating_ip_id = forms.CharField(widget=forms.HiddenInput()) + + def handle(self, request, data): + try: + LOG.info('Releasing Floating IP "%s"' % data['floating_ip_id']) + api.tenant_floating_ip_release(request, data['floating_ip_id']) + messages.info(request, _('Successfully released Floating IP: %s') + % data['floating_ip_id']) + except novaclient_exceptions.ClientException, e: + LOG.exception("ClientException in ReleaseFloatingIp") + messages.error(request, _('Error releasing Floating IP ' + 'from tenant: %s') % e.message) + return shortcuts.redirect(request.build_absolute_uri()) + + +class FloatingIpAssociate(forms.SelfHandlingForm): + floating_ip_id = forms.CharField(widget=forms.HiddenInput()) + floating_ip = forms.CharField(widget=forms.TextInput( + attrs={'readonly': 'readonly'})) + instance_id = forms.ChoiceField() + + def __init__(self, *args, **kwargs): + super(FloatingIpAssociate, self).__init__(*args, **kwargs) + instancelist = kwargs.get('initial', {}).get('instances', []) + self.fields['instance_id'] = forms.ChoiceField( + choices=instancelist, + label=_("Instance")) + + def handle(self, request, data): + try: + api.server_add_floating_ip(request, + data['instance_id'], + data['floating_ip_id']) + LOG.info('Associating Floating IP "%s" with Instance "%s"' + % (data['floating_ip'], data['instance_id'])) + messages.info(request, _('Successfully associated Floating IP: \ + %(ip)s with Instance: %(inst)s' + % {"ip": data['floating_ip'], + "inst": data['instance_id']})) + except novaclient_exceptions.ClientException, e: + LOG.exception("ClientException in FloatingIpAssociate") + messages.error(request, _('Error associating Floating IP: %s') + % e.message) + return shortcuts.redirect('horizon:nova:floating_ips:index') + + +class FloatingIpDisassociate(forms.SelfHandlingForm): + floating_ip_id = forms.CharField(widget=forms.HiddenInput()) + + def handle(self, request, data): + try: + fip = api.tenant_floating_ip_get(request, data['floating_ip_id']) + api.server_remove_floating_ip(request, fip.instance_id, fip.id) + + LOG.info('Disassociating Floating IP "%s"' + % data['floating_ip_id']) + + messages.info(request, + _('Successfully disassociated Floating IP: %s') + % data['floating_ip_id']) + except novaclient_exceptions.ClientException, e: + LOG.exception("ClientException in FloatingIpAssociate") + messages.error(request, _('Error disassociating Floating IP: %s') + % e.message) + return shortcuts.redirect('horizon:nova:floating_ips:index') + + +class FloatingIpAllocate(forms.SelfHandlingForm): + tenant_id = forms.CharField(widget=forms.HiddenInput()) + + def handle(self, request, data): + try: + fip = api.tenant_floating_ip_allocate(request) + LOG.info('Allocating Floating IP "%s" to tenant "%s"' + % (fip.ip, data['tenant_id'])) + + messages.success(request, + _('Successfully allocated Floating IP "%(ip)s"\ + to tenant "%(tenant)s"') + % {"ip": fip.ip, "tenant": data['tenant_id']}) + + except novaclient_exceptions.ClientException, e: + LOG.exception("ClientException in FloatingIpAllocate") + messages.error(request, _('Error allocating Floating IP "%(ip)s"\ + to tenant "%(tenant)s": %(msg)s') % + {"ip": fip.ip, "tenant": data['tenant_id'], "msg": e.message}) + return shortcuts.redirect('horizon:nova:floating_ips:index') diff --git a/horizon/horizon/dashboards/nova/floating_ips/panel.py b/horizon/horizon/dashboards/nova/floating_ips/panel.py new file mode 100644 index 00000000..cba90145 --- /dev/null +++ b/horizon/horizon/dashboards/nova/floating_ips/panel.py @@ -0,0 +1,30 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import horizon +from horizon.dashboards.nova import dashboard + + +class FloatingIPs(horizon.Panel): + name = "Floating IPs" + slug = 'floating_ips' + + +dashboard.Nova.register(FloatingIPs) diff --git a/horizon/horizon/dashboards/nova/floating_ips/tests.py b/horizon/horizon/dashboards/nova/floating_ips/tests.py new file mode 100644 index 00000000..3d6925a1 --- /dev/null +++ b/horizon/horizon/dashboards/nova/floating_ips/tests.py @@ -0,0 +1,216 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime + +from django import http +from django.contrib import messages +from django.core.urlresolvers import reverse +from django.shortcuts import redirect +from mox import IsA, IgnoreArg +from novaclient import exceptions as novaclient_exceptions + +from horizon import api +from horizon import test +from horizon.dashboards.nova.floating_ips.forms import FloatingIpAssociate + + +FLOATING_IPS_INDEX = reverse('horizon:nova:floating_ips:index') + + +class FloatingIpViewTests(test.BaseViewTests): + + def setUp(self): + super(FloatingIpViewTests, self).setUp() + server = self.mox.CreateMock(api.Server) + server.id = 1 + server.name = 'serverName' + self.server = server + self.servers = (server, ) + + floating_ip = self.mox.CreateMock(api.FloatingIp) + floating_ip.id = 1 + floating_ip.fixed_ip = '10.0.0.4' + floating_ip.instance_id = 1 + floating_ip.ip = '58.58.58.58' + + self.floating_ip = floating_ip + self.floating_ips = [floating_ip, ] + + def test_index(self): + self.mox.StubOutWithMock(api, 'tenant_floating_ip_list') + api.tenant_floating_ip_list(IsA(http.HttpRequest)).\ + AndReturn(self.floating_ips) + self.mox.ReplayAll() + + res = self.client.get(FLOATING_IPS_INDEX) + self.assertTemplateUsed(res, 'nova/floating_ips/index.html') + self.assertItemsEqual(res.context['floating_ips'], self.floating_ips) + + self.mox.VerifyAll() + + def test_associate(self): + self.mox.StubOutWithMock(api, 'server_list') + api.server_list = self.mox.CreateMockAnything() + api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers) + + self.mox.StubOutWithMock(api, 'tenant_floating_ip_get') + api.tenant_floating_ip_get = self.mox.CreateMockAnything() + api.tenant_floating_ip_get(IsA(http.HttpRequest), str(1)).\ + AndReturn(self.floating_ip) + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:floating_ips:associate', + args=[1])) + self.assertTemplateUsed(res, 'nova/floating_ips/associate.html') + self.mox.VerifyAll() + + def test_associate_post(self): + server = self.server + + self.mox.StubOutWithMock(api, 'server_list') + api.server_list = self.mox.CreateMockAnything() + api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers) + + self.mox.StubOutWithMock(api, 'tenant_floating_ip_list') + api.tenant_floating_ip_list(IsA(http.HttpRequest)).\ + AndReturn(self.floating_ips) + + self.mox.StubOutWithMock(api, 'server_add_floating_ip') + api.server_add_floating_ip = self.mox.CreateMockAnything() + api.server_add_floating_ip(IsA(http.HttpRequest), IsA(unicode), + IsA(unicode)).\ + AndReturn(None) + self.mox.StubOutWithMock(messages, 'info') + messages.info(IsA(http.HttpRequest), IsA(unicode)) + + self.mox.StubOutWithMock(api, 'tenant_floating_ip_get') + api.tenant_floating_ip_get = self.mox.CreateMockAnything() + api.tenant_floating_ip_get(IsA(http.HttpRequest), str(1)).\ + AndReturn(self.floating_ip) + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:nova:floating_ips:associate', + args=[1]), + {'instance_id': 1, + 'floating_ip_id': self.floating_ip.id, + 'floating_ip': self.floating_ip.ip, + 'method': 'FloatingIpAssociate'}) + + self.assertRedirects(res, FLOATING_IPS_INDEX) + self.mox.VerifyAll() + + def test_associate_post_with_exception(self): + server = self.server + + self.mox.StubOutWithMock(api, 'server_list') + api.server_list = self.mox.CreateMockAnything() + api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers) + + self.mox.StubOutWithMock(api, 'tenant_floating_ip_list') + api.tenant_floating_ip_list(IsA(http.HttpRequest)).\ + AndReturn(self.floating_ips) + + self.mox.StubOutWithMock(api, 'server_add_floating_ip') + api.server_add_floating_ip = self.mox.CreateMockAnything() + exception = novaclient_exceptions.ClientException('ClientException', + message='clientException') + api.server_add_floating_ip(IsA(http.HttpRequest), IsA(unicode), + IsA(unicode)).\ + AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(basestring)) + + self.mox.StubOutWithMock(api, 'tenant_floating_ip_get') + api.tenant_floating_ip_get = self.mox.CreateMockAnything() + api.tenant_floating_ip_get(IsA(http.HttpRequest), IsA(unicode)).\ + AndReturn(self.floating_ip) + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:nova:floating_ips:associate', + args=[1]), + {'instance_id': 1, + 'floating_ip_id': self.floating_ip.id, + 'floating_ip': self.floating_ip.ip, + 'method': 'FloatingIpAssociate'}) + self.assertRaises(novaclient_exceptions.ClientException) + + self.assertRedirects(res, FLOATING_IPS_INDEX) + + self.mox.VerifyAll() + + def test_disassociate(self): + res = self.client.get(reverse('horizon:nova:floating_ips:disassociate', + args=[1])) + self.assertTemplateUsed(res, 'nova/floating_ips/associate.html') + self.mox.VerifyAll() + + def test_disassociate_post(self): + self.mox.StubOutWithMock(api, 'tenant_floating_ip_list') + api.tenant_floating_ip_list(IsA(http.HttpRequest)).\ + AndReturn(self.floating_ips) + + self.mox.StubOutWithMock(api, 'server_remove_floating_ip') + api.server_remove_floating_ip = self.mox.CreateMockAnything() + api.server_remove_floating_ip(IsA(http.HttpRequest), IsA(int), + IsA(int)).\ + AndReturn(None) + self.mox.StubOutWithMock(messages, 'info') + messages.info(IsA(http.HttpRequest), IsA(unicode)) + + self.mox.StubOutWithMock(api, 'tenant_floating_ip_get') + api.tenant_floating_ip_get = self.mox.CreateMockAnything() + api.tenant_floating_ip_get(IsA(http.HttpRequest), IsA(unicode)).\ + AndReturn(self.floating_ip) + self.mox.ReplayAll() + res = self.client.post( + reverse('horizon:nova:floating_ips:disassociate', args=[1]), + {'floating_ip_id': self.floating_ip.id, + 'method': 'FloatingIpDisassociate'}) + self.assertRedirects(res, FLOATING_IPS_INDEX) + self.mox.VerifyAll() + + def test_disassociate_post_with_exception(self): + self.mox.StubOutWithMock(api, 'tenant_floating_ip_list') + api.tenant_floating_ip_list(IsA(http.HttpRequest)).\ + AndReturn(self.floating_ips) + + self.mox.StubOutWithMock(api, 'server_remove_floating_ip') + exception = novaclient_exceptions.ClientException('ClientException', + message='clientException') + api.server_remove_floating_ip(IsA(http.HttpRequest), + IsA(int), + IsA(int)).AndRaise(exception) + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(basestring)) + + self.mox.StubOutWithMock(api, 'tenant_floating_ip_get') + api.tenant_floating_ip_get = self.mox.CreateMockAnything() + api.tenant_floating_ip_get(IsA(http.HttpRequest), IsA(unicode)).\ + AndReturn(self.floating_ip) + self.mox.ReplayAll() + res = self.client.post( + reverse('horizon:nova:floating_ips:disassociate', args=[1]), + {'floating_ip_id': self.floating_ip.id, + 'method': 'FloatingIpDisassociate'}) + self.assertRaises(novaclient_exceptions.ClientException) + self.assertRedirects(res, FLOATING_IPS_INDEX) + self.mox.VerifyAll() diff --git a/horizon/horizon/dashboards/nova/floating_ips/urls.py b/horizon/horizon/dashboards/nova/floating_ips/urls.py new file mode 100644 index 00000000..64abd2d1 --- /dev/null +++ b/horizon/horizon/dashboards/nova/floating_ips/urls.py @@ -0,0 +1,28 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls.defaults import patterns, url + + +urlpatterns = patterns('horizon.dashboards.nova.floating_ips.views', + url(r'^$', 'index', name='index'), + url(r'^(?P[^/]+)/associate/$', 'associate', name='associate'), + url(r'^(?P[^/]+)/disassociate/$', 'disassociate', + name='disassociate')) diff --git a/horizon/horizon/dashboards/nova/floating_ips/views.py b/horizon/horizon/dashboards/nova/floating_ips/views.py new file mode 100644 index 00000000..8e795dec --- /dev/null +++ b/horizon/horizon/dashboards/nova/floating_ips/views.py @@ -0,0 +1,88 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Views for managing Nova floating IPs. +""" +import logging + +from django import template +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django import shortcuts +from django.utils.translation import ugettext as _ +from novaclient import exceptions as novaclient_exceptions + +from horizon import api +from horizon.dashboards.nova.floating_ips.forms import (ReleaseFloatingIp, + FloatingIpAssociate, FloatingIpDisassociate, FloatingIpAllocate) + + +LOG = logging.getLogger(__name__) + + +@login_required +def index(request): + for f in (ReleaseFloatingIp, FloatingIpDisassociate, FloatingIpAllocate): + _unused, handled = f.maybe_handle(request) + if handled: + return handled + try: + floating_ips = api.tenant_floating_ip_list(request) + except novaclient_exceptions.ClientException, e: + floating_ips = [] + LOG.exception("ClientException in floating ip index") + messages.error(request, + _('Error fetching floating ips: %s') % e.message) + allocate_form = FloatingIpAllocate(initial={ + 'tenant_id': request.user.tenant_id}) + return shortcuts.render(request, + 'nova/floating_ips/index.html', { + 'allocate_form': allocate_form, + 'disassociate_form': FloatingIpDisassociate(), + 'floating_ips': floating_ips, + 'release_form': ReleaseFloatingIp()}) + + +@login_required +def associate(request, ip_id): + instancelist = [(server.id, 'id: %s, name: %s' % + (server.id, server.name)) + for server in api.server_list(request)] + + form, handled = FloatingIpAssociate().maybe_handle(request, initial={ + 'floating_ip_id': ip_id, + 'floating_ip': api.tenant_floating_ip_get(request, ip_id).ip, + 'instances': instancelist}) + if handled: + return handled + + return shortcuts.render(request, + 'nova/floating_ips/associate.html', { + 'associate_form': form}) + + +@login_required +def disassociate(request, ip_id): + form, handled = FloatingIpDisassociate().maybe_handle(request) + if handled: + return handled + + return shortcuts.render(request, 'nova/floating_ips/associate.html', {}) diff --git a/horizon/horizon/dashboards/nova/images/__init__.py b/horizon/horizon/dashboards/nova/images/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/nova/images/forms.py b/horizon/horizon/dashboards/nova/images/forms.py new file mode 100644 index 00000000..806d0621 --- /dev/null +++ b/horizon/horizon/dashboards/nova/images/forms.py @@ -0,0 +1,200 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Views for managing Nova images. +""" + +import logging + +from django.contrib import messages +from django.shortcuts import redirect +from django.utils.text import normalize_newlines +from django.utils.translation import ugettext as _ +from glance.common import exception as glance_exception +from openstackx.api import exceptions as api_exceptions + +from horizon import api +from horizon import forms + +LOG = logging.getLogger(__name__) + + +class UpdateImageForm(forms.SelfHandlingForm): + image_id = forms.CharField(widget=forms.HiddenInput()) + name = forms.CharField(max_length="25", label=_("Name")) + kernel = forms.CharField(max_length="25", label=_("Kernel ID"), + required=False) + ramdisk = forms.CharField(max_length="25", label=_("Ramdisk ID"), + required=False) + architecture = forms.CharField(label=_("Architecture"), required=False) + container_format = forms.CharField(label=_("Container Format"), + required=False) + disk_format = forms.CharField(label=_("Disk Format")) + + def handle(self, request, data): + image_id = data['image_id'] + tenant_id = request.user.tenant_id + error_retrieving = _('Unable to retreive image info from glance: %s' + % image_id) + error_updating = _('Error updating image with id: %s' % image_id) + + try: + image = api.image_get(request, image_id) + except glance_exception.ClientConnectionError, e: + LOG.exception(_('Error connecting to glance')) + messages.error(request, error_retrieving) + except glance_exception.Error, e: + LOG.exception(error_retrieving) + messages.error(request, error_retrieving) + + if image.owner == request.user.username: + try: + meta = { + 'is_public': True, + 'disk_format': data['disk_format'], + 'container_format': data['container_format'], + 'name': data['name'], + } + # TODO add public flag to properties + meta['properties'] = {} + if data['kernel']: + meta['properties']['kernel_id'] = data['kernel'] + + if data['ramdisk']: + meta['properties']['ramdisk_id'] = data['ramdisk'] + + if data['architecture']: + meta['properties']['architecture'] = data['architecture'] + + api.image_update(request, image_id, meta) + messages.success(request, _('Image was successfully updated.')) + + except glance_exception.ClientConnectionError, e: + LOG.exception(_('Error connecting to glance')) + messages.error(request, error_retrieving) + except glance_exception.Error, e: + LOG.exception(error_updating) + messages.error(request, error_updating) + except: + LOG.exception(_('Unspecified Exception in image update')) + messages.error(request, error_updating) + return redirect('dash_images_update', tenant_id, image_id) + else: + messages.info(request, _('Unable to update image. You are not its \ + owner.')) + return redirect('dash_images_update', tenant_id, image_id) + + +class LaunchForm(forms.SelfHandlingForm): + name = forms.CharField(max_length=80, label=_("Server Name")) + image_id = forms.CharField(widget=forms.HiddenInput()) + tenant_id = forms.CharField(widget=forms.HiddenInput()) + user_data = forms.CharField(widget=forms.Textarea, + label=_("User Data"), + required=False) + + # make the dropdown populate when the form is loaded not when django is + # started + def __init__(self, *args, **kwargs): + super(LaunchForm, self).__init__(*args, **kwargs) + flavorlist = kwargs.get('initial', {}).get('flavorlist', []) + self.fields['flavor'] = forms.ChoiceField( + choices=flavorlist, + label=_("Flavor"), + help_text="Size of Image to launch") + + keynamelist = kwargs.get('initial', {}).get('keynamelist', []) + self.fields['key_name'] = forms.ChoiceField(choices=keynamelist, + label=_("Key Name"), + required=False, + help_text="Which keypair to use for authentication") + + securitygrouplist = kwargs.get('initial', {}).get( + 'securitygrouplist', []) + self.fields['security_groups'] = forms.MultipleChoiceField( + choices=securitygrouplist, + label=_("Security Groups"), + required=True, + initial=['default'], + widget=forms.SelectMultiple( + attrs={'class': 'chzn-select', + 'style': "min-width: 200px"}), + help_text="Launch instance in these Security Groups") + # setting self.fields.keyOrder seems to break validation, + # so ordering fields manually + field_list = ( + 'name', + 'user_data', + 'flavor', + 'key_name') + for field in field_list[::-1]: + self.fields.insert(0, field, self.fields.pop(field)) + + def handle(self, request, data): + image_id = data['image_id'] + tenant_id = data['tenant_id'] + try: + image = api.image_get(request, image_id) + flavor = api.flavor_get(request, data['flavor']) + api.server_create(request, + data['name'], + image, + flavor, + data.get('key_name'), + normalize_newlines(data.get('user_data')), + data.get('security_groups')) + + msg = _('Instance was successfully launched') + LOG.info(msg) + messages.success(request, msg) + return redirect('horizon:nova:instances:index') + + except api_exceptions.ApiException, e: + LOG.exception('ApiException while creating instances of image "%s"' + % image_id) + messages.error(request, + _('Unable to launch instance: %s') % e.message) + + +class DeleteImage(forms.SelfHandlingForm): + image_id = forms.CharField(required=True) + + def handle(self, request, data): + image_id = data['image_id'] + tenant_id = request.user.tenant_id + try: + image = api.image_get(request, image_id) + if image.owner == request.user.username: + api.image_delete(request, image_id) + else: + messages.info(request, _("Unable to delete image, you are not \ + its owner.")) + return redirect('dash_images_update', tenant_id, image_id) + except glance_exception.ClientConnectionError, e: + LOG.exception("Error connecting to glance") + messages.error(request, _("Error connecting to glance: %s") + % e.message) + except glance_exception.Error, e: + LOG.exception('Error deleting image with id "%s"' % image_id) + messages.error(request, + _("Error deleting image: %(image)s: %i(msg)s") + % {"image": image_id, "msg": e.message}) + return redirect(request.build_absolute_uri()) diff --git a/horizon/horizon/dashboards/nova/images/panel.py b/horizon/horizon/dashboards/nova/images/panel.py new file mode 100644 index 00000000..12f293c5 --- /dev/null +++ b/horizon/horizon/dashboards/nova/images/panel.py @@ -0,0 +1,30 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import horizon +from horizon.dashboards.nova import dashboard + + +class Images(horizon.Panel): + name = "Images" + slug = 'images' + + +dashboard.Nova.register(Images) diff --git a/horizon/horizon/dashboards/nova/images/tests.py b/horizon/horizon/dashboards/nova/images/tests.py new file mode 100644 index 00000000..24e2f5bc --- /dev/null +++ b/horizon/horizon/dashboards/nova/images/tests.py @@ -0,0 +1,370 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django import http +from django.contrib import messages +from django.core.urlresolvers import reverse +from glance.common import exception as glance_exception +from openstackx.api import exceptions as api_exceptions +from mox import IgnoreArg, IsA + +from horizon import api +from horizon import test + + +IMAGES_INDEX_URL = reverse('horizon:nova:images:index') + + +class FakeQuota: + ram = 100 + + +class ImageViewTests(test.BaseViewTests): + def setUp(self): + super(ImageViewTests, self).setUp() + image_dict = {'name': 'visibleImage', + 'container_format': 'novaImage'} + self.visibleImage = api.Image(image_dict) + + image_dict = {'name': 'invisibleImage', + 'container_format': 'aki'} + self.invisibleImage = api.Image(image_dict) + + self.images = (self.visibleImage, self.invisibleImage) + + flavor = self.mox.CreateMock(api.Flavor) + flavor.id = 1 + flavor.name = 'm1.massive' + flavor.vcpus = 1000 + flavor.disk = 1024 + flavor.ram = 10000 + self.flavors = (flavor,) + + keypair = self.mox.CreateMock(api.KeyPair) + keypair.name = 'keyName' + self.keypairs = (keypair,) + + security_group = self.mox.CreateMock(api.SecurityGroup) + security_group.name = 'default' + self.security_groups = (security_group,) + + def test_index(self): + self.mox.StubOutWithMock(api, 'image_list_detailed') + api.image_list_detailed(IsA(http.HttpRequest)).AndReturn(self.images) + + self.mox.ReplayAll() + + res = self.client.get(IMAGES_INDEX_URL) + + self.assertTemplateUsed(res, 'nova/images/index.html') + + self.assertIn('images', res.context) + images = res.context['images'] + self.assertEqual(len(images), 1) + self.assertEqual(images[0].name, 'visibleImage') + + self.mox.VerifyAll() + + def test_index_no_images(self): + self.mox.StubOutWithMock(api, 'image_list_detailed') + api.image_list_detailed(IsA(http.HttpRequest)).AndReturn([]) + + self.mox.StubOutWithMock(messages, 'info') + messages.info(IsA(http.HttpRequest), IsA(basestring)) + + self.mox.ReplayAll() + + res = self.client.get(IMAGES_INDEX_URL) + + self.assertTemplateUsed(res, 'nova/images/index.html') + + self.mox.VerifyAll() + + def test_index_client_conn_error(self): + self.mox.StubOutWithMock(api, 'image_list_detailed') + exception = glance_exception.ClientConnectionError('clientConnError') + api.image_list_detailed(IsA(http.HttpRequest)).AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(basestring)) + + self.mox.ReplayAll() + + res = self.client.get(IMAGES_INDEX_URL) + + self.assertTemplateUsed(res, + 'nova/images/index.html') + + self.mox.VerifyAll() + + def test_index_glance_error(self): + self.mox.StubOutWithMock(api, 'image_list_detailed') + exception = glance_exception.Error('glanceError') + api.image_list_detailed(IsA(http.HttpRequest)).AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(basestring)) + + self.mox.ReplayAll() + + res = self.client.get(IMAGES_INDEX_URL) + + self.assertTemplateUsed(res, 'nova/images/index.html') + + self.mox.VerifyAll() + + def test_launch_get(self): + IMAGE_ID = '1' + + self.mox.StubOutWithMock(api, 'image_get') + api.image_get(IsA(http.HttpRequest), + IMAGE_ID).AndReturn(self.visibleImage) + + self.mox.StubOutWithMock(api, 'tenant_quota_get') + api.tenant_quota_get(IsA(http.HttpRequest), + self.TEST_TENANT).AndReturn(FakeQuota) + + self.mox.StubOutWithMock(api, 'flavor_list') + api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors) + + self.mox.StubOutWithMock(api, 'keypair_list') + api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs) + + self.mox.StubOutWithMock(api, 'security_group_list') + api.security_group_list(IsA(http.HttpRequest)).AndReturn( + self.security_groups) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:images:launch', + args=[IMAGE_ID])) + + self.assertTemplateUsed(res, 'nova/images/launch.html') + + image = res.context['image'] + self.assertEqual(image.name, self.visibleImage.name) + + form = res.context['form'] + + form_flavorfield = form.fields['flavor'] + self.assertIn('m1.massive', form_flavorfield.choices[0][1]) + + form_keyfield = form.fields['key_name'] + self.assertEqual(form_keyfield.choices[0][0], + self.keypairs[0].name) + + self.mox.VerifyAll() + + def test_launch_post(self): + FLAVOR_ID = self.flavors[0].id + IMAGE_ID = '1' + KEY_NAME = self.keypairs[0].name + SERVER_NAME = 'serverName' + USER_DATA = 'userData' + + form_data = {'method': 'LaunchForm', + 'flavor': FLAVOR_ID, + 'image_id': IMAGE_ID, + 'key_name': KEY_NAME, + 'name': SERVER_NAME, + 'user_data': USER_DATA, + 'tenant_id': self.TEST_TENANT, + 'security_groups': 'default', + } + + self.mox.StubOutWithMock(api, 'image_get') + api.image_get(IsA(http.HttpRequest), + IMAGE_ID).AndReturn(self.visibleImage) + + self.mox.StubOutWithMock(api, 'tenant_quota_get') + api.tenant_quota_get(IsA(http.HttpRequest), + self.TEST_TENANT).AndReturn(FakeQuota) + + self.mox.StubOutWithMock(api, 'flavor_list') + api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors) + + self.mox.StubOutWithMock(api, 'keypair_list') + api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs) + + self.mox.StubOutWithMock(api, 'security_group_list') + api.security_group_list(IsA(http.HttpRequest)).AndReturn( + self.security_groups) + + # called again by the form + api.image_get(IsA(http.HttpRequest), + IMAGE_ID).AndReturn(self.visibleImage) + + self.mox.StubOutWithMock(api, 'flavor_get') + api.flavor_get(IsA(http.HttpRequest), + IsA(unicode)).AndReturn(self.flavors[0]) + + self.mox.StubOutWithMock(api, 'server_create') + + api.server_create(IsA(http.HttpRequest), SERVER_NAME, + self.visibleImage, self.flavors[0], + KEY_NAME, USER_DATA, [self.security_groups[0].name]) + + self.mox.StubOutWithMock(messages, 'success') + messages.success(IsA(http.HttpRequest), IsA(basestring)) + + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:nova:images:launch', + args=[IMAGE_ID]), + form_data) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:instances:index')) + + self.mox.VerifyAll() + + def test_launch_flavorlist_error(self): + IMAGE_ID = '1' + + self.mox.StubOutWithMock(api, 'image_get') + api.image_get(IsA(http.HttpRequest), + IMAGE_ID).AndReturn(self.visibleImage) + + self.mox.StubOutWithMock(api, 'tenant_quota_get') + api.tenant_quota_get(IsA(http.HttpRequest), + self.TEST_TENANT).AndReturn(FakeQuota) + + exception = api_exceptions.ApiException('apiException') + self.mox.StubOutWithMock(api, 'flavor_list') + api.flavor_list(IsA(http.HttpRequest)).AndRaise(exception) + + self.mox.StubOutWithMock(api, 'keypair_list') + api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs) + + self.mox.StubOutWithMock(api, 'security_group_list') + api.security_group_list(IsA(http.HttpRequest)).AndReturn( + self.security_groups) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:images:launch', + args=[IMAGE_ID])) + + self.assertTemplateUsed(res, 'nova/images/launch.html') + + form = res.context['form'] + + form_flavorfield = form.fields['flavor'] + self.assertIn('m1.tiny', form_flavorfield.choices[0][1]) + + self.mox.VerifyAll() + + def test_launch_keypairlist_error(self): + IMAGE_ID = '2' + + self.mox.StubOutWithMock(api, 'image_get') + api.image_get(IsA(http.HttpRequest), + IMAGE_ID).AndReturn(self.visibleImage) + + self.mox.StubOutWithMock(api, 'tenant_quota_get') + api.tenant_quota_get(IsA(http.HttpRequest), + self.TEST_TENANT).AndReturn(FakeQuota) + + self.mox.StubOutWithMock(api, 'flavor_list') + api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors) + + exception = api_exceptions.ApiException('apiException') + self.mox.StubOutWithMock(api, 'keypair_list') + api.keypair_list(IsA(http.HttpRequest)).AndRaise(exception) + + self.mox.StubOutWithMock(api, 'security_group_list') + api.security_group_list(IsA(http.HttpRequest)).AndReturn( + self.security_groups) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:images:launch', + args=[IMAGE_ID])) + + self.assertTemplateUsed(res, 'nova/images/launch.html') + + form = res.context['form'] + + form_keyfield = form.fields['key_name'] + self.assertEqual(len(form_keyfield.choices), 0) + + self.mox.VerifyAll() + + def test_launch_form_apiexception(self): + FLAVOR_ID = self.flavors[0].id + IMAGE_ID = '1' + KEY_NAME = self.keypairs[0].name + SERVER_NAME = 'serverName' + USER_DATA = 'userData' + + form_data = {'method': 'LaunchForm', + 'flavor': FLAVOR_ID, + 'image_id': IMAGE_ID, + 'key_name': KEY_NAME, + 'name': SERVER_NAME, + 'tenant_id': self.TEST_TENANT, + 'user_data': USER_DATA, + 'security_groups': 'default', + } + + self.mox.StubOutWithMock(api, 'image_get') + api.image_get(IgnoreArg(), + IMAGE_ID).AndReturn(self.visibleImage) + + self.mox.StubOutWithMock(api, 'tenant_quota_get') + api.tenant_quota_get(IsA(http.HttpRequest), + self.TEST_TENANT).AndReturn(FakeQuota) + + self.mox.StubOutWithMock(api, 'flavor_list') + api.flavor_list(IgnoreArg()).AndReturn(self.flavors) + + self.mox.StubOutWithMock(api, 'keypair_list') + api.keypair_list(IgnoreArg()).AndReturn(self.keypairs) + + self.mox.StubOutWithMock(api, 'security_group_list') + api.security_group_list(IsA(http.HttpRequest)).AndReturn( + self.security_groups) + + # called again by the form + api.image_get(IgnoreArg(), + IMAGE_ID).AndReturn(self.visibleImage) + + self.mox.StubOutWithMock(api, 'flavor_get') + api.flavor_get(IgnoreArg(), + IsA(unicode)).AndReturn(self.flavors[0]) + + self.mox.StubOutWithMock(api, 'server_create') + + exception = api_exceptions.ApiException('apiException') + api.server_create(IsA(http.HttpRequest), SERVER_NAME, + self.visibleImage, self.flavors[0], + KEY_NAME, USER_DATA, + self.security_groups).AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(basestring)) + + self.mox.ReplayAll() + url = reverse('horizon:nova:images:launch', args=[IMAGE_ID]) + res = self.client.post(url, form_data) + + self.assertTemplateUsed(res, 'nova/images/launch.html') + + self.mox.VerifyAll() diff --git a/horizon/horizon/dashboards/nova/images/urls.py b/horizon/horizon/dashboards/nova/images/urls.py new file mode 100644 index 00000000..3306293d --- /dev/null +++ b/horizon/horizon/dashboards/nova/images/urls.py @@ -0,0 +1,27 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls.defaults import * + + +urlpatterns = patterns('horizon.dashboards.nova.images.views', + url(r'^$', 'index', name='index'), + url(r'^(?P[^/]+)/launch/$', 'launch', name='launch'), + url(r'^(?P[^/]+)/update/$', 'update', name='update')) diff --git a/horizon/horizon/dashboards/nova/images/views.py b/horizon/horizon/dashboards/nova/images/views.py new file mode 100644 index 00000000..12e3ef3b --- /dev/null +++ b/horizon/horizon/dashboards/nova/images/views.py @@ -0,0 +1,163 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Views for managing Nova images. +""" + +import logging + +from django import shortcuts +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.utils.translation import ugettext as _ +from glance.common import exception as glance_exception +from novaclient import exceptions as novaclient_exceptions +from openstackx.api import exceptions as api_exceptions + +from horizon import api +from horizon.dashboards.nova.images.forms import (UpdateImageForm, + LaunchForm, DeleteImage) + + +LOG = logging.getLogger(__name__) + + +@login_required +def index(request): + for f in (DeleteImage, ): + unused, handled = f.maybe_handle(request) + if handled: + return handled + delete_form = DeleteImage() + + all_images = [] + try: + all_images = api.image_list_detailed(request) + if not all_images: + messages.info(request, _("There are currently no images.")) + except glance_exception.ClientConnectionError, e: + LOG.exception("Error connecting to glance") + messages.error(request, _("Error connecting to glance: %s") % str(e)) + except glance_exception.Error, e: + LOG.exception("Error retrieving image list") + messages.error(request, _("Error retrieving image list: %s") % str(e)) + except api_exceptions.ApiException, e: + msg = _("Unable to retreive image info from glance: %s") % str(e) + LOG.exception(msg) + messages.error(request, msg) + + images = [im for im in all_images + if im['container_format'] not in ['aki', 'ari']] + + return shortcuts.render(request, + 'nova/images/index.html', { + 'delete_form': delete_form, + 'images': images}) + + +@login_required +def launch(request, image_id): + + def flavorlist(): + try: + fl = api.flavor_list(request) + + # TODO add vcpu count to flavors + sel = [(f.id, '%s (%svcpu / %sGB Disk / %sMB Ram )' % + (f.name, f.vcpus, f.disk, f.ram)) for f in fl] + return sorted(sel) + except api_exceptions.ApiException: + LOG.exception('Unable to retrieve list of instance types') + return [(1, 'm1.tiny')] + + def keynamelist(): + try: + fl = api.keypair_list(request) + sel = [(f.name, f.name) for f in fl] + return sel + except api_exceptions.ApiException: + LOG.exception('Unable to retrieve list of keypairs') + return [] + + def securitygrouplist(): + try: + fl = api.security_group_list(request) + sel = [(f.name, f.name) for f in fl] + return sel + except novaclient_exceptions.ClientException, e: + LOG.exception('Unable to retrieve list of security groups') + return [] + + tenant_id = request.user.tenant_id + # TODO(mgius): Any reason why these can't be after the launchform logic? + # If The form is valid, we've just wasted these two api calls + image = api.image_get(request, image_id) + quotas = api.tenant_quota_get(request, request.user.tenant_id) + try: + quotas.ram = int(quotas.ram) + except Exception, e: + messages.error(request, + _('Error parsing quota for %(image)s: %(msg)s') % + {"image": image_id, "msg": e.message}) + return shortcuts.redirect('horizon:nova:instances:index') + + form, handled = LaunchForm.maybe_handle( + request, initial={'flavorlist': flavorlist(), + 'keynamelist': keynamelist(), + 'securitygrouplist': securitygrouplist(), + 'image_id': image_id, + 'tenant_id': tenant_id}) + if handled: + return handled + + return shortcuts.render(request, + 'nova/images/launch.html', { + 'image': image, + 'form': form, + 'quotas': quotas}) + + +@login_required +def update(request, image_id): + try: + image = api.image_get(request, image_id) + except glance_exception.ClientConnectionError, e: + LOG.exception("Error connecting to glance") + messages.error(request, _("Error connecting to glance: %s") + % e.message) + except glance_exception.Error, e: + LOG.exception('Error retrieving image with id "%s"' % image_id) + messages.error(request, + _("Error retrieving image %(image)s: %(msg)s") + % {"image": image_id, "msg": e.message}) + + form, handled = UpdateImageForm().maybe_handle(request, initial={ + 'image_id': image_id, + 'name': image.get('name', ''), + 'kernel': image['properties'].get('kernel_id', ''), + 'ramdisk': image['properties'].get('ramdisk_id', ''), + 'architecture': image['properties'].get('architecture', ''), + 'container_format': image.get('container_format', ''), + 'disk_format': image.get('disk_format', ''), }) + if handled: + return handled + + return shortcuts.render(request, 'nova/images/update.html', {'form': form}) diff --git a/horizon/horizon/dashboards/nova/instances/__init__.py b/horizon/horizon/dashboards/nova/instances/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/nova/instances/forms.py b/horizon/horizon/dashboards/nova/instances/forms.py new file mode 100644 index 00000000..1296008e --- /dev/null +++ b/horizon/horizon/dashboards/nova/instances/forms.py @@ -0,0 +1,101 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django import shortcuts +from django.contrib import messages +from django.utils.translation import ugettext as _ +import openstackx.api.exceptions as api_exceptions + +from horizon import api +from horizon import forms + + +LOG = logging.getLogger(__name__) + + +class TerminateInstance(forms.SelfHandlingForm): + instance = forms.CharField(required=True) + + def handle(self, request, data): + instance_id = data['instance'] + instance = api.server_get(request, instance_id) + + try: + api.server_delete(request, instance) + except api_exceptions.ApiException, e: + LOG.exception(_('ApiException while terminating instance "%s"') % + instance_id) + messages.error(request, + _('Unable to terminate %(inst)s: %(message)s') % + {"inst": instance_id, "message": e.message}) + else: + msg = _('Instance %s has been terminated.') % instance_id + LOG.info(msg) + messages.success(request, msg) + + return shortcuts.redirect(request.build_absolute_uri()) + + +class RebootInstance(forms.SelfHandlingForm): + instance = forms.CharField(required=True) + + def handle(self, request, data): + instance_id = data['instance'] + try: + server = api.server_reboot(request, instance_id) + messages.success(request, _("Instance rebooting")) + except api_exceptions.ApiException, e: + LOG.exception(_('ApiException while rebooting instance "%s"') % + instance_id) + messages.error(request, + _('Unable to reboot instance: %s') % e.message) + + else: + msg = _('Instance %s has been rebooted.') % instance_id + LOG.info(msg) + messages.success(request, msg) + + return shortcuts.redirect(request.build_absolute_uri()) + + +class UpdateInstance(forms.SelfHandlingForm): + tenant_id = forms.CharField(widget=forms.HiddenInput()) + instance = forms.CharField(widget=forms.TextInput( + attrs={'readonly': 'readonly'})) + name = forms.CharField(required=True) + description = forms.CharField(required=False) + + def handle(self, request, data): + tenant_id = data['tenant_id'] + description = data.get('description', '') + try: + api.server_update(request, + data['instance'], + data['name'], + description) + messages.success(request, _("Instance '%s' updated") % + data['name']) + except api_exceptions.ApiException, e: + messages.error(request, + _('Unable to update instance: %s') % e.message) + + return shortcuts.redirect('horizon:nova:instances:index') diff --git a/horizon/horizon/dashboards/nova/instances/panel.py b/horizon/horizon/dashboards/nova/instances/panel.py new file mode 100644 index 00000000..b4d3b0b8 --- /dev/null +++ b/horizon/horizon/dashboards/nova/instances/panel.py @@ -0,0 +1,30 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import horizon +from horizon.dashboards.nova import dashboard + + +class Instances(horizon.Panel): + name = "Instances" + slug = 'instances' + + +dashboard.Nova.register(Instances) diff --git a/horizon/horizon/dashboards/nova/instances/tests.py b/horizon/horizon/dashboards/nova/instances/tests.py new file mode 100644 index 00000000..e9ec1cec --- /dev/null +++ b/horizon/horizon/dashboards/nova/instances/tests.py @@ -0,0 +1,448 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime + +from django import http +from django.contrib import messages +from django.core.urlresolvers import reverse +from mox import IsA, IgnoreArg +from openstackx.api import exceptions as api_exceptions + +from horizon import api +from horizon import test + + +class InstanceViewTests(test.BaseViewTests): + def setUp(self): + super(InstanceViewTests, self).setUp() + server = self.mox.CreateMock(api.Server) + server.id = 1 + server.name = 'serverName' + server.attrs = {'description': 'mydesc'} + self.servers = (server,) + + def test_index(self): + self.mox.StubOutWithMock(api, 'server_list') + api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:instances:index')) + + self.assertTemplateUsed(res, + 'nova/instances/index.html') + self.assertItemsEqual(res.context['instances'], self.servers) + + self.mox.VerifyAll() + + def test_index_server_list_exception(self): + self.mox.StubOutWithMock(api, 'server_list') + exception = api_exceptions.ApiException('apiException') + api.server_list(IsA(http.HttpRequest)).AndRaise(exception) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:instances:index')) + + self.assertTemplateUsed(res, + 'nova/instances/index.html') + self.assertEqual(len(res.context['instances']), 0) + + self.mox.VerifyAll() + + def test_terminate_instance(self): + formData = {'method': 'TerminateInstance', + 'instance': self.servers[0].id, + } + + self.mox.StubOutWithMock(api, 'server_get') + api.server_get(IsA(http.HttpRequest), + str(self.servers[0].id)).AndReturn(self.servers[0]) + self.mox.StubOutWithMock(api, 'server_delete') + api.server_delete(IsA(http.HttpRequest), + self.servers[0]) + + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:nova:instances:index'), + formData) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:instances:index')) + + self.mox.VerifyAll() + + def test_terminate_instance_exception(self): + formData = {'method': 'TerminateInstance', + 'instance': self.servers[0].id, + } + + self.mox.StubOutWithMock(api, 'server_get') + api.server_get(IsA(http.HttpRequest), + str(self.servers[0].id)).AndReturn(self.servers[0]) + + exception = api_exceptions.ApiException('ApiException', + message='apiException') + self.mox.StubOutWithMock(api, 'server_delete') + api.server_delete(IsA(http.HttpRequest), + self.servers[0]).AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(unicode)) + + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:nova:instances:index'), + formData) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:instances:index')) + + self.mox.VerifyAll() + + def test_reboot_instance(self): + formData = {'method': 'RebootInstance', + 'instance': self.servers[0].id, + } + + self.mox.StubOutWithMock(api, 'server_reboot') + api.server_reboot(IsA(http.HttpRequest), unicode(self.servers[0].id)) + + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:nova:instances:index'), + formData) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:instances:index')) + + self.mox.VerifyAll() + + def test_reboot_instance_exception(self): + formData = {'method': 'RebootInstance', + 'instance': self.servers[0].id, + } + + self.mox.StubOutWithMock(api, 'server_reboot') + exception = api_exceptions.ApiException('ApiException', + message='apiException') + api.server_reboot(IsA(http.HttpRequest), + unicode(self.servers[0].id)).AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(basestring)) + + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:nova:instances:index'), + formData) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:instances:index')) + + self.mox.VerifyAll() + + def test_instance_usage(self): + TEST_RETURN = 'testReturn' + + now = self.override_times() + + self.mox.StubOutWithMock(api, 'usage_get') + api.usage_get(IsA(http.HttpRequest), self.TEST_TENANT, + datetime.datetime(now.year, now.month, 1, + now.hour, now.minute, now.second), + now).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:instances:usage')) + + self.assertTemplateUsed(res, + 'nova/instances/usage.html') + + self.assertEqual(res.context['usage'], TEST_RETURN) + + self.mox.VerifyAll() + + self.reset_times() + + def test_instance_csv_usage(self): + TEST_RETURN = 'testReturn' + + now = self.override_times() + + self.mox.StubOutWithMock(api, 'usage_get') + api.usage_get(IsA(http.HttpRequest), self.TEST_TENANT, + datetime.datetime(now.year, now.month, 1, + now.hour, now.minute, now.second), + now).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:instances:usage') + + "?format=csv") + + self.assertTemplateUsed(res, + 'nova/instances/usage.csv') + + self.assertEqual(res.context['usage'], TEST_RETURN) + + self.mox.VerifyAll() + + self.reset_times() + + def test_instance_usage_exception(self): + now = self.override_times() + + exception = api_exceptions.ApiException('apiException', + message='apiException') + self.mox.StubOutWithMock(api, 'usage_get') + api.usage_get(IsA(http.HttpRequest), self.TEST_TENANT, + datetime.datetime(now.year, now.month, 1, + now.hour, now.minute, now.second), + now).AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(basestring)) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:instances:usage')) + + self.assertTemplateUsed(res, + 'nova/instances/usage.html') + + self.assertEqual(res.context['usage'], {}) + + self.mox.VerifyAll() + + self.reset_times() + + def test_instance_usage_default_tenant(self): + TEST_RETURN = 'testReturn' + + now = self.override_times() + + self.mox.StubOutWithMock(api, 'usage_get') + api.usage_get(IsA(http.HttpRequest), self.TEST_TENANT, + datetime.datetime(now.year, now.month, 1, + now.hour, now.minute, now.second), + now).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:instances:usage')) + + self.assertTemplateUsed(res, + 'nova/instances/usage.html') + + self.assertEqual(res.context['usage'], TEST_RETURN) + + self.mox.VerifyAll() + + self.reset_times() + + def test_instance_console(self): + CONSOLE_OUTPUT = 'output' + INSTANCE_ID = self.servers[0].id + + console_mock = self.mox.CreateMock(api.Console) + console_mock.output = CONSOLE_OUTPUT + + self.mox.StubOutWithMock(api, 'console_create') + api.console_create(IgnoreArg(), + unicode(INSTANCE_ID), + IgnoreArg()).AndReturn(console_mock) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:instances:console', + args=[INSTANCE_ID])) + + self.assertIsInstance(res, http.HttpResponse) + self.assertContains(res, CONSOLE_OUTPUT) + + self.mox.VerifyAll() + + def test_instance_console_exception(self): + INSTANCE_ID = self.servers[0].id + + exception = api_exceptions.ApiException('apiException', + message='apiException') + + self.mox.StubOutWithMock(api, 'console_create') + api.console_create(IgnoreArg(), + unicode(INSTANCE_ID), + IgnoreArg()).AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IgnoreArg(), IsA(unicode)) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:instances:console', + args=[INSTANCE_ID])) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:instances:index')) + + self.mox.VerifyAll() + + def test_instance_vnc(self): + INSTANCE_ID = self.servers[0].id + CONSOLE_OUTPUT = '/vncserver' + + console_mock = self.mox.CreateMock(api.Console) + console_mock.output = CONSOLE_OUTPUT + + self.mox.StubOutWithMock(api, 'console_create') + self.mox.StubOutWithMock(api, 'server_get') + api.server_get(IsA(http.HttpRequest), + str(self.servers[0].id)).AndReturn(self.servers[0]) + api.console_create(IgnoreArg(), + unicode(INSTANCE_ID), + 'vnc').AndReturn(console_mock) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:instances:vnc', + args=[INSTANCE_ID])) + + self.assertRedirectsNoFollow(res, + CONSOLE_OUTPUT + '&title=serverName(1)') + + self.mox.VerifyAll() + + def test_instance_vnc_exception(self): + INSTANCE_ID = self.servers[0].id + + exception = api_exceptions.ApiException('apiException', + message='apiException') + + self.mox.StubOutWithMock(api, 'console_create') + api.console_create(IsA(http.HttpRequest), + unicode(INSTANCE_ID), + 'vnc').AndRaise(exception) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:instances:vnc', + args=[INSTANCE_ID])) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:instances:index')) + + self.mox.VerifyAll() + + def test_instance_update_get(self): + INSTANCE_ID = self.servers[0].id + + self.mox.StubOutWithMock(api, 'server_get') + api.server_get(IsA(http.HttpRequest), + unicode(INSTANCE_ID)).AndReturn(self.servers[0]) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:instances:update', + args=[INSTANCE_ID])) + + self.assertTemplateUsed(res, + 'nova/instances/update.html') + + self.mox.VerifyAll() + + def test_instance_update_get_server_get_exception(self): + INSTANCE_ID = self.servers[0].id + + exception = api_exceptions.ApiException('apiException') + self.mox.StubOutWithMock(api, 'server_get') + api.server_get(IsA(http.HttpRequest), + unicode(INSTANCE_ID)).AndRaise(exception) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:instances:update', + args=[INSTANCE_ID])) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:instances:index')) + + self.mox.VerifyAll() + + def test_instance_update_post(self): + INSTANCE_ID = self.servers[0].id + NAME = 'myname' + DESC = 'mydesc' + formData = {'method': 'UpdateInstance', + 'instance': self.servers[0].id, + 'name': NAME, + 'tenant_id': self.TEST_TENANT, + 'description': DESC} + + self.mox.StubOutWithMock(api, 'server_get') + api.server_get(IsA(http.HttpRequest), + unicode(INSTANCE_ID)).AndReturn(self.servers[0]) + + self.mox.StubOutWithMock(api, 'server_update') + api.server_update(IsA(http.HttpRequest), + str(INSTANCE_ID), NAME, DESC) + + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:nova:instances:update', + args=[INSTANCE_ID]), + formData) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:instances:index')) + + self.mox.VerifyAll() + + def test_instance_update_post_api_exception(self): + INSTANCE_ID = self.servers[0].id + NAME = 'myname' + DESC = 'mydesc' + formData = {'method': 'UpdateInstance', + 'instance': INSTANCE_ID, + 'name': NAME, + 'tenant_id': self.TEST_TENANT, + 'description': DESC} + + self.mox.StubOutWithMock(api, 'server_get') + api.server_get(IsA(http.HttpRequest), + unicode(INSTANCE_ID)).AndReturn(self.servers[0]) + + exception = api_exceptions.ApiException('apiException') + self.mox.StubOutWithMock(api, 'server_update') + api.server_update(IsA(http.HttpRequest), + str(INSTANCE_ID), NAME, DESC).\ + AndRaise(exception) + + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:nova:instances:update', + args=[INSTANCE_ID]), + formData) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:instances:index')) + + self.mox.VerifyAll() diff --git a/horizon/horizon/dashboards/nova/instances/urls.py b/horizon/horizon/dashboards/nova/instances/urls.py new file mode 100644 index 00000000..82809f24 --- /dev/null +++ b/horizon/horizon/dashboards/nova/instances/urls.py @@ -0,0 +1,33 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls.defaults import patterns, url + +INSTANCES = r'^(?P[^/]+)/%s$' + +urlpatterns = patterns('horizon.dashboards.nova.instances.views', + url(r'^$', 'index', name='index'), + url(r'^usage/$', 'usage', name='usage'), + url(r'^refresh$', 'refresh', name='refresh'), + url(INSTANCES % 'detail', 'detail', name='detail'), + url(INSTANCES % 'console', 'console', name='console'), + url(INSTANCES % 'vnc', 'vnc', name='vnc'), + url(INSTANCES % 'update', 'update', name='update'), +) diff --git a/horizon/horizon/dashboards/nova/instances/views.py b/horizon/horizon/dashboards/nova/instances/views.py new file mode 100644 index 00000000..75b975e2 --- /dev/null +++ b/horizon/horizon/dashboards/nova/instances/views.py @@ -0,0 +1,253 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Views for managing Nova instances. +""" +import datetime +import logging + +from django import http +from django import shortcuts +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.utils.translation import ugettext as _ +import openstackx.api.exceptions as api_exceptions + +from horizon import api +from horizon import forms +from horizon import test +from horizon.dashboards.nova.instances.forms import (TerminateInstance, + RebootInstance, UpdateInstance) + + +LOG = logging.getLogger(__name__) + + +@login_required +def index(request): + tenant_id = request.user.tenant_id + for f in (TerminateInstance, RebootInstance): + form, handled = f.maybe_handle(request) + if handled: + return handled + instances = [] + try: + instances = api.server_list(request) + except api_exceptions.ApiException as e: + LOG.exception(_('Exception in instance index')) + messages.error(request, _('Unable to get instance list: %s') + % e.message) + + # We don't have any way of showing errors for these, so don't bother + # trying to reuse the forms from above + terminate_form = TerminateInstance() + reboot_form = RebootInstance() + + return shortcuts.render(request, + 'nova/instances/index.html', { + 'instances': instances, + 'terminate_form': terminate_form, + 'reboot_form': reboot_form}) + + +@login_required +def refresh(request): + tenant_id = request.user.tenant_id + instances = [] + try: + instances = api.server_list(request) + except Exception as e: + messages.error(request, + _('Unable to get instance list: %s') % e.message) + + # We don't have any way of showing errors for these, so don't bother + # trying to reuse the forms from above + terminate_form = TerminateInstance() + reboot_form = RebootInstance() + + return shortcuts.render(request, + 'nova/instances/_list.html', { + 'instances': instances, + 'terminate_form': terminate_form, + 'reboot_form': reboot_form}) + + +@login_required +def usage(request, tenant_id=None): + tenant_id = tenant_id or request.user.tenant_id + today = test.today() + date_start = datetime.date(today.year, today.month, 1) + datetime_start = datetime.datetime.combine(date_start, test.time()) + datetime_end = test.utcnow() + + show_terminated = request.GET.get('show_terminated', False) + + usage = {} + if not tenant_id: + tenant_id = request.user.tenant_id + + try: + usage = api.usage_get(request, tenant_id, datetime_start, datetime_end) + except api_exceptions.ApiException, e: + LOG.exception(_('ApiException in instance usage')) + + messages.error(request, _('Unable to get usage info: %s') % e.message) + + ram_unit = "MB" + total_ram = 0 + if hasattr(usage, 'total_active_ram_size'): + total_ram = usage.total_active_ram_size + if total_ram > 999: + ram_unit = "GB" + total_ram /= float(1024) + + running_instances = [] + terminated_instances = [] + if hasattr(usage, 'instances'): + now = datetime.datetime.now() + for i in usage.instances: + # this is just a way to phrase uptime in a way that is compatible + # with the 'timesince' filter. Use of local time intentional + i['uptime_at'] = now - datetime.timedelta(seconds=i['uptime']) + if i['ended_at']: + terminated_instances.append(i) + else: + running_instances.append(i) + + instances = running_instances + if show_terminated: + instances += terminated_instances + + if request.GET.get('format', 'html') == 'csv': + template_name = 'nova/instances/usage.csv' + mimetype = "text/csv" + else: + template_name = 'nova/instances/usage.html' + mimetype = "text/html" + + return shortcuts.render(request, template_name, { + 'usage': usage, + 'ram_unit': ram_unit, + 'total_ram': total_ram, + 'csv_link': '?format=csv', + 'show_terminated': show_terminated, + 'datetime_start': datetime_start, + 'datetime_end': datetime_end, + 'instances': instances}, + content_type=mimetype) + + +@login_required +def console(request, instance_id): + tenant_id = request.user.tenant_id + try: + # TODO(jakedahn): clean this up once the api supports tailing. + length = request.GET.get('length', '') + console = api.console_create(request, instance_id, 'text') + response = http.HttpResponse(mimetype='text/plain') + if length: + response.write('\n'.join(console.output.split('\n') + [-int(length):])) + else: + response.write(console.output) + response.flush() + return response + except api_exceptions.ApiException, e: + LOG.exception(_('ApiException while fetching instance console')) + messages.error(request, + _('Unable to get log for instance %(inst)s: %(msg)s') % + {"inst": instance_id, "msg": e.message}) + return shortcuts.redirect('horizon:nova:instances:index') + + +@login_required +def vnc(request, instance_id): + tenant_id = request.user.tenant_id + try: + console = api.console_create(request, instance_id, 'vnc') + instance = api.server_get(request, instance_id) + return shortcuts.redirect(console.output + + ("&title=%s(%s)" % (instance.name, instance_id))) + except api_exceptions.ApiException, e: + LOG.exception(_('ApiException while fetching instance vnc connection')) + messages.error(request, + _('Unable to get vnc console for instance %(inst)s: %(message)s') % + {"inst": instance_id, "message": e.message}) + return shortcuts.redirect('horizon:nova:instances:index') + + +@login_required +def update(request, instance_id): + tenant_id = request.user.tenant_id + try: + instance = api.server_get(request, instance_id) + except api_exceptions.ApiException, e: + LOG.exception(_('ApiException while fetching instance info')) + messages.error(request, + _('Unable to get information for instance %(inst)s: %(message)s') % + {"inst": instance_id, "message": e.message}) + return shortcuts.redirect('horizon:nova:instances:index') + + form, handled = UpdateInstance.maybe_handle(request, initial={ + 'instance': instance_id, + 'tenant_id': tenant_id, + 'name': instance.name, + 'description': instance.attrs['description']}) + + if handled: + return handled + + return shortcuts.render(request, + 'nova/instances/update.html', { + 'instance': instance, + 'form': form}) + + +@login_required +def detail(request, instance_id): + tenant_id = request.user.tenant_id + try: + instance = api.server_get(request, instance_id) + volumes = api.volume_instance_list(request, instance_id) + try: + console = api.console_create(request, instance_id, 'vnc') + vnc_url = "%s&title=%s(%s)" % (console.output, + instance.name, + instance_id) + except api_exceptions.ApiException, e: + LOG.exception(_('ApiException while fetching instance vnc \ + connection')) + messages.error(request, + _('Unable to get vnc console for instance %(inst)s: %(msg)s') % + {"inst": instance_id, "msg": e.message}) + return shortcuts.redirect('horizon:nova:instances:index') + except api_exceptions.ApiException, e: + LOG.exception(_('ApiException while fetching instance info')) + messages.error(request, + _('Unable to get information for instance %(inst)s: %(msg)s') % + {"inst": instance_id, "msg": e.message}) + return shortcuts.redirect('horizon:nova:instances:index') + + return shortcuts.render(request, + 'nova/instances/detail.html', { + 'instance': instance, + 'vnc_url': vnc_url, + 'volumes': volumes}) diff --git a/horizon/horizon/dashboards/nova/keypairs/__init__.py b/horizon/horizon/dashboards/nova/keypairs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/nova/keypairs/forms.py b/horizon/horizon/dashboards/nova/keypairs/forms.py new file mode 100644 index 00000000..fddc1161 --- /dev/null +++ b/horizon/horizon/dashboards/nova/keypairs/forms.py @@ -0,0 +1,91 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django import http +from django import shortcuts +from django.contrib import messages +from django.core import validators +from django.utils.translation import ugettext as _ +from novaclient import exceptions as novaclient_exceptions + +from horizon import api +from horizon import forms + + +LOG = logging.getLogger(__name__) + + +class DeleteKeypair(forms.SelfHandlingForm): + keypair_id = forms.CharField(widget=forms.HiddenInput()) + + def handle(self, request, data): + try: + LOG.info('Deleting keypair "%s"' % data['keypair_id']) + api.keypair_delete(request, data['keypair_id']) + messages.info(request, _('Successfully deleted keypair: %s') + % data['keypair_id']) + except novaclient_exceptions.ClientException, e: + LOG.exception("ClientException in DeleteKeypair") + messages.error(request, + _('Error deleting keypair: %s') % e.message) + return shortcuts.redirect(request.build_absolute_uri()) + + +class CreateKeypair(forms.SelfHandlingForm): + + name = forms.CharField(max_length="20", label=_("Keypair Name"), + validators=[validators.RegexValidator('\w+')]) + + def handle(self, request, data): + try: + LOG.info('Creating keypair "%s"' % data['name']) + keypair = api.keypair_create(request, data['name']) + response = http.HttpResponse(mimetype='application/binary') + response['Content-Disposition'] = \ + 'attachment; filename=%s.pem' % keypair.name + response.write(keypair.private_key) + return response + except novaclient_exceptions.ClientException, e: + LOG.exception("ClientException in CreateKeyPair") + messages.error(request, + _('Error Creating Keypair: %s') % e.message) + return shortcuts.redirect(request.build_absolute_uri()) + + +class ImportKeypair(forms.SelfHandlingForm): + + name = forms.CharField(max_length="20", label=_("Keypair Name"), + validators=[validators.RegexValidator('\w+')]) + public_key = forms.CharField(label=_("Public Key"), widget=forms.Textarea) + + def handle(self, request, data): + try: + LOG.info('Importing keypair "%s"' % data['name']) + api.keypair_import(request, data['name'], data['public_key']) + messages.success(request, _('Successfully imported public key: %s') + % data['name']) + return shortcuts.redirect('horizon:nova:keypairs:index') + except novaclient_exceptions.ClientException, e: + LOG.exception("ClientException in ImportKeypair") + messages.error(request, + _('Error Importing Keypair: %s') % e.message) + return shortcuts.redirect(request.build_absolute_uri()) diff --git a/horizon/horizon/dashboards/nova/keypairs/panel.py b/horizon/horizon/dashboards/nova/keypairs/panel.py new file mode 100644 index 00000000..81887738 --- /dev/null +++ b/horizon/horizon/dashboards/nova/keypairs/panel.py @@ -0,0 +1,30 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import horizon +from horizon.dashboards.nova import dashboard + + +class Keypairs(horizon.Panel): + name = "Keypairs" + slug = 'keypairs' + + +dashboard.Nova.register(Keypairs) diff --git a/horizon/horizon/dashboards/nova/keypairs/tests.py b/horizon/horizon/dashboards/nova/keypairs/tests.py new file mode 100644 index 00000000..6f50f52d --- /dev/null +++ b/horizon/horizon/dashboards/nova/keypairs/tests.py @@ -0,0 +1,161 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django import http +from django.contrib import messages +from django.core.urlresolvers import reverse +from mox import IsA +from novaclient import exceptions as novaclient_exceptions + +from horizon import api +from horizon import test + + +class KeyPairViewTests(test.BaseViewTests): + def setUp(self): + super(KeyPairViewTests, self).setUp() + keypair = self.mox.CreateMock(api.KeyPair) + keypair.name = 'keyName' + self.keypairs = (keypair,) + + def test_index(self): + self.mox.StubOutWithMock(api, 'keypair_list') + api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:keypairs:index')) + + self.assertTemplateUsed(res, 'nova/keypairs/index.html') + self.assertItemsEqual(res.context['keypairs'], self.keypairs) + + self.mox.VerifyAll() + + def test_index_exception(self): + exception = novaclient_exceptions.ClientException('clientException', + message='clientException') + self.mox.StubOutWithMock(api, 'keypair_list') + api.keypair_list(IsA(http.HttpRequest)).AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(basestring)) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:keypairs:index')) + + self.assertTemplateUsed(res, 'nova/keypairs/index.html') + self.assertEqual(len(res.context['keypairs']), 0) + + self.mox.VerifyAll() + + def test_delete_keypair(self): + KEYPAIR_ID = self.keypairs[0].name + formData = {'method': 'DeleteKeypair', + 'keypair_id': KEYPAIR_ID, + } + + self.mox.StubOutWithMock(api, 'keypair_delete') + api.keypair_delete(IsA(http.HttpRequest), unicode(KEYPAIR_ID)) + + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:nova:keypairs:index'), + formData) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:keypairs:index')) + + self.mox.VerifyAll() + + def test_delete_keypair_exception(self): + KEYPAIR_ID = self.keypairs[0].name + formData = {'method': 'DeleteKeypair', + 'keypair_id': KEYPAIR_ID, + } + + exception = novaclient_exceptions.ClientException('clientException', + message='clientException') + self.mox.StubOutWithMock(api, 'keypair_delete') + api.keypair_delete(IsA(http.HttpRequest), + unicode(KEYPAIR_ID)).AndRaise(exception) + + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:nova:keypairs:index'), + formData) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:keypairs:index')) + + self.mox.VerifyAll() + + def test_create_keypair_get(self): + res = self.client.get(reverse('horizon:nova:keypairs:create')) + + self.assertTemplateUsed(res, 'nova/keypairs/create.html') + + def test_create_keypair_post(self): + KEYPAIR_NAME = 'newKeypair' + PRIVATE_KEY = 'privateKey' + + newKeyPair = self.mox.CreateMock(api.KeyPair) + newKeyPair.name = KEYPAIR_NAME + newKeyPair.private_key = PRIVATE_KEY + + formData = {'method': 'CreateKeypair', + 'name': KEYPAIR_NAME, + } + + self.mox.StubOutWithMock(api, 'keypair_create') + api.keypair_create(IsA(http.HttpRequest), + KEYPAIR_NAME).AndReturn(newKeyPair) + + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:nova:keypairs:create'), + formData) + + self.assertTrue(res.has_header('Content-Disposition')) + + self.mox.VerifyAll() + + def test_create_keypair_exception(self): + KEYPAIR_NAME = 'newKeypair' + + formData = {'method': 'CreateKeypair', + 'name': KEYPAIR_NAME, + } + + exception = novaclient_exceptions.ClientException('clientException', + message='clientException') + self.mox.StubOutWithMock(api, 'keypair_create') + api.keypair_create(IsA(http.HttpRequest), + KEYPAIR_NAME).AndRaise(exception) + + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:nova:keypairs:create'), + formData) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:keypairs:create')) + + self.mox.VerifyAll() diff --git a/horizon/horizon/dashboards/nova/keypairs/urls.py b/horizon/horizon/dashboards/nova/keypairs/urls.py new file mode 100644 index 00000000..8080debe --- /dev/null +++ b/horizon/horizon/dashboards/nova/keypairs/urls.py @@ -0,0 +1,28 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls.defaults import patterns, url + + +urlpatterns = patterns('horizon.dashboards.nova.keypairs.views', + url(r'^$', 'index', name='index'), + url(r'^create/$', 'create', name='create'), + url(r'^import/$', 'import_keypair', name='import'), +) diff --git a/horizon/horizon/dashboards/nova/keypairs/views.py b/horizon/horizon/dashboards/nova/keypairs/views.py new file mode 100644 index 00000000..b65b64a1 --- /dev/null +++ b/horizon/horizon/dashboards/nova/keypairs/views.py @@ -0,0 +1,80 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Views for managing Nova keypairs. +""" +import logging + +from django import http +from django import shortcuts +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.utils.translation import ugettext as _ +from novaclient import exceptions as novaclient_exceptions + +from horizon import api +from horizon.dashboards.nova.keypairs.forms import (CreateKeypair, + DeleteKeypair, ImportKeypair) + + +LOG = logging.getLogger(__name__) + + +@login_required +def index(request): + delete_form, handled = DeleteKeypair.maybe_handle(request) + + if handled: + return handled + + try: + keypairs = api.keypair_list(request) + except novaclient_exceptions.ClientException, e: + keypairs = [] + LOG.exception("ClientException in keypair index") + messages.error(request, _('Error fetching keypairs: %s') % e.message) + + return shortcuts.render(request, + 'nova/keypairs/index.html', { + 'keypairs': keypairs, + 'delete_form': delete_form}) + + +@login_required +def create(request): + form, handled = CreateKeypair.maybe_handle(request) + if handled: + return handled + + return shortcuts.render(request, + 'nova/keypairs/create.html', { + 'create_form': form}) + + +@login_required +def import_keypair(request): + form, handled = ImportKeypair.maybe_handle(request) + if handled: + return handled + + return shortcuts.render(request, + 'nova/keypairs/import.html', { + 'create_form': form}) diff --git a/horizon/horizon/dashboards/nova/models.py b/horizon/horizon/dashboards/nova/models.py new file mode 100644 index 00000000..300ba4b3 --- /dev/null +++ b/horizon/horizon/dashboards/nova/models.py @@ -0,0 +1,23 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Stub file to work around django bug: https://code.djangoproject.com/ticket/7198 +""" diff --git a/horizon/horizon/dashboards/nova/networks/__init__.py b/horizon/horizon/dashboards/nova/networks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/nova/networks/forms.py b/horizon/horizon/dashboards/nova/networks/forms.py new file mode 100644 index 00000000..2a23a094 --- /dev/null +++ b/horizon/horizon/dashboards/nova/networks/forms.py @@ -0,0 +1,208 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django import shortcuts +from django.contrib import messages +from django.utils.translation import ugettext as _ + +from horizon import api +from horizon import forms + + +LOG = logging.getLogger(__name__) + + +class CreateNetwork(forms.SelfHandlingForm): + name = forms.CharField(required=True, label=_("Network Name")) + + def handle(self, request, data): + network_name = data['name'] + + try: + LOG.info('Creating network %s ' % network_name) + send_data = {'network': {'name': '%s' % network_name}} + api.quantum_create_network(request, send_data) + except Exception, e: + messages.error(request, + _('Unable to create network %(network)s: %(msg)s') % + {"network": network_name, "msg": e.message}) + return shortcuts.redirect(request.build_absolute_uri()) + else: + msg = _('Network %s has been created.') % network_name + LOG.info(msg) + messages.success(request, msg) + return shortcuts.redirect('horizon:nova:networks:index') + + +class DeleteNetwork(forms.SelfHandlingForm): + network = forms.CharField(widget=forms.HiddenInput()) + + def handle(self, request, data): + try: + LOG.info('Deleting network %s ' % data['network']) + api.quantum_delete_network(request, data['network']) + except Exception, e: + messages.error(request, + _('Unable to delete network %(network)s: %(msg)s') % + {"network": data['network'], "msg": e.message}) + else: + msg = _('Network %s has been deleted.') % data['network'] + LOG.info(msg) + messages.success(request, msg) + + return shortcuts.redirect(request.build_absolute_uri()) + + +class RenameNetwork(forms.SelfHandlingForm): + network = forms.CharField(widget=forms.HiddenInput()) + new_name = forms.CharField(required=True) + + def handle(self, request, data): + try: + LOG.info('Renaming network %s to %s' % + (data['network'], data['new_name'])) + send_data = {'network': {'name': '%s' % data['new_name']}} + api.quantum_update_network(request, data['network'], send_data) + except Exception, e: + messages.error(request, + _('Unable to rename network %(network)s: %(msg)s') % + {"network": data['network'], "msg": e.message}) + else: + msg = _('Network %(net)s has been renamed to %(new_name)s.') % { + "net": data['network'], "new_name": data['new_name']} + LOG.info(msg) + messages.success(request, msg) + + return shortcuts.redirect(request.build_absolute_uri()) + + +class CreatePort(forms.SelfHandlingForm): + network = forms.CharField(widget=forms.HiddenInput()) + ports_num = forms.IntegerField(required=True, label=_("Number of Ports")) + + def handle(self, request, data): + try: + LOG.info('Creating %s ports on network %s' % + (data['ports_num'], data['network'])) + for i in range(0, data['ports_num']): + api.quantum_create_port(request, data['network']) + except Exception, e: + messages.error(request, + _('Unable to create ports on network %(network)s: %(msg)s') % + {"network": data['network'], "msg": e.message}) + else: + msg = _('%(num_ports)s ports created on network %(network)s.') % { + "num_ports": data['ports_num'], "network": data['network']} + LOG.info(msg) + messages.success(request, msg) + + return shortcuts.redirect(request.build_absolute_uri()) + + +class DeletePort(forms.SelfHandlingForm): + network = forms.CharField(widget=forms.HiddenInput()) + port = forms.CharField(widget=forms.HiddenInput()) + + def handle(self, request, data): + try: + LOG.info('Deleting %s ports on network %s' % + (data['port'], data['network'])) + api.quantum_delete_port(request, data['network'], data['port']) + except Exception, e: + messages.error(request, + _('Unable to delete port %(port)s: %(msg)s') % + {"port": data['port'], "msg": e.message}) + else: + msg = _('Port %(port)s deleted from network %(network)s.') % { + "port": data['port'], "network": data['network']} + LOG.info(msg) + messages.success(request, msg) + return shortcuts.redirect(request.build_absolute_uri()) + + +class AttachPort(forms.SelfHandlingForm): + network = forms.CharField(widget=forms.HiddenInput()) + port = forms.CharField(widget=forms.HiddenInput()) + vif_id = forms.CharField(widget=forms.Select(), + label=_("Select VIF to connect")) + + def handle(self, request, data): + try: + LOG.info('Attaching %s port to VIF %s' % + (data['port'], data['vif_id'])) + body = {'attachment': {'id': '%s' % data['vif_id']}} + api.quantum_attach_port(request, + data['network'], data['port'], body) + except Exception, e: + messages.error(request, + _('Unable to attach port %(port)s to VIF %(vif)s: %(msg)s') % + {"port": data['port'], + "vif": data['vif_id'], + "msg": e.message}) + else: + msg = _('Port %(port)s connected to VIF %(vif)s.') % \ + {"port": data['port'], "vif": data['vif_id']} + LOG.info(msg) + messages.success(request, msg) + return shortcuts.redirect(request.build_absolute_uri()) + + +class DetachPort(forms.SelfHandlingForm): + network = forms.CharField(widget=forms.HiddenInput()) + port = forms.CharField(widget=forms.HiddenInput()) + + def handle(self, request, data): + try: + LOG.info('Detaching port %s' % data['port']) + api.quantum_detach_port(request, data['network'], data['port']) + except Exception, e: + messages.error(request, + _('Unable to detach port %(port)s: %(message)s') % + {"port": data['port'], "message": e.message}) + else: + msg = _('Port %s detached.') % (data['port']) + LOG.info(msg) + messages.success(request, msg) + return shortcuts.redirect(request.build_absolute_uri()) + + +class TogglePort(forms.SelfHandlingForm): + network = forms.CharField(widget=forms.HiddenInput()) + port = forms.CharField(widget=forms.HiddenInput()) + state = forms.CharField(widget=forms.HiddenInput()) + + def handle(self, request, data): + try: + LOG.info('Toggling port state to %s' % data['state']) + body = {'port': {'state': '%s' % data['state']}} + api.quantum_set_port_state(request, + data['network'], data['port'], body) + except Exception, e: + messages.error(request, + _('Unable to set port state to %(state)s: %(message)s') % + {"state": data['state'], "message": e.message}) + else: + msg = _('Port %(port)s state set to %(state)s.') % { + "port": data['port'], "state": data['state']} + LOG.info(msg) + messages.success(request, msg) + return shortcuts.redirect(request.build_absolute_uri()) diff --git a/horizon/horizon/dashboards/nova/networks/panel.py b/horizon/horizon/dashboards/nova/networks/panel.py new file mode 100644 index 00000000..825c720d --- /dev/null +++ b/horizon/horizon/dashboards/nova/networks/panel.py @@ -0,0 +1,33 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import horizon +from horizon.dashboards.nova import dashboard + + +class Networks(horizon.Panel): + name = "Networks" + slug = 'networks' + + def nav(self, context): + return context.get('network_configured', False) + + +dashboard.Nova.register(Networks) diff --git a/horizon/horizon/dashboards/nova/networks/tests.py b/horizon/horizon/dashboards/nova/networks/tests.py new file mode 100644 index 00000000..aab9ba8e --- /dev/null +++ b/horizon/horizon/dashboards/nova/networks/tests.py @@ -0,0 +1,268 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django import http +from django.contrib import messages +from django.core.urlresolvers import reverse +from horizon import test +from mox import IgnoreArg, IsA +import quantum.client + +from horizon import api + + +class NetworkViewTests(test.BaseViewTests): + def setUp(self): + super(NetworkViewTests, self).setUp() + self.network = {} + self.network['networks'] = [] + self.network['networks'].append({'id': 'n1'}) + self.network_details = {'network': {'name': 'test_network'}} + self.ports = {} + self.ports['ports'] = [] + self.ports['ports'].append({'id': 'p1'}) + self.port_details = { + 'port': { + 'id': 'p1', + 'state': 'DOWN'}} + self.port_attachment = { + 'attachment': { + 'id': 'vif1'}} + self.vifs = [{'id': 'vif1'}] + + def test_network_index(self): + self.mox.StubOutWithMock(api, 'quantum_list_networks') + api.quantum_list_networks(IsA(http.HttpRequest)).\ + AndReturn(self.network) + + self.mox.StubOutWithMock(api, 'quantum_network_details') + api.quantum_network_details(IsA(http.HttpRequest), + 'n1').AndReturn(self.network_details) + + self.mox.StubOutWithMock(api, 'quantum_list_ports') + api.quantum_list_ports(IsA(http.HttpRequest), + 'n1').AndReturn(self.ports) + + self.mox.StubOutWithMock(api, 'quantum_port_attachment') + api.quantum_port_attachment(IsA(http.HttpRequest), + 'n1', 'p1').AndReturn(self.port_attachment) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:networks:index')) + + self.assertTemplateUsed(res, 'nova/networks/index.html') + self.assertIn('networks', res.context) + networks = res.context['networks'] + + self.assertEqual(len(networks), 1) + self.assertEqual(networks[0]['name'], 'test_network') + self.assertEqual(networks[0]['id'], 'n1') + self.assertEqual(networks[0]['total'], 1) + self.assertEqual(networks[0]['used'], 1) + self.assertEqual(networks[0]['available'], 0) + + self.mox.VerifyAll() + + def test_network_create(self): + self.mox.StubOutWithMock(api, "quantum_create_network") + api.quantum_create_network(IsA(http.HttpRequest), dict).AndReturn(True) + + self.mox.ReplayAll() + + formData = {'name': 'Test', + 'method': 'CreateNetwork'} + + res = self.client.post(reverse('horizon:nova:networks:create'), + formData) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:networks:index')) + self.mox.VerifyAll() + + def test_network_delete(self): + self.mox.StubOutWithMock(api, "quantum_delete_network") + api.quantum_delete_network(IsA(http.HttpRequest), 'n1').AndReturn(True) + + self.mox.StubOutWithMock(api, 'quantum_list_networks') + api.quantum_list_networks(IsA(http.HttpRequest)).\ + AndReturn(self.network) + + self.mox.StubOutWithMock(api, 'quantum_network_details') + api.quantum_network_details(IsA(http.HttpRequest), + 'n1').AndReturn(self.network_details) + + self.mox.StubOutWithMock(api, 'quantum_list_ports') + api.quantum_list_ports(IsA(http.HttpRequest), + 'n1').AndReturn(self.ports) + + self.mox.StubOutWithMock(api, 'quantum_port_attachment') + api.quantum_port_attachment(IsA(http.HttpRequest), + 'n1', 'p1').AndReturn(self.port_attachment) + + self.mox.ReplayAll() + + formData = {'id': 'n1', + 'method': 'DeleteNetwork'} + + res = self.client.post(reverse('horizon:nova:networks:index'), + formData) + + def test_network_rename(self): + self.mox.StubOutWithMock(api, "quantum_update_network") + api.quantum_update_network(IsA(http.HttpRequest), + 'n1', dict).AndReturn(True) + + self.mox.StubOutWithMock(api, 'quantum_list_networks') + api.quantum_list_networks(IsA(http.HttpRequest)).\ + AndReturn(self.network) + + self.mox.StubOutWithMock(api, 'quantum_network_details') + api.quantum_network_details(IsA(http.HttpRequest), + 'n1').AndReturn(self.network_details) + + self.mox.StubOutWithMock(api, 'quantum_list_ports') + api.quantum_list_ports(IsA(http.HttpRequest), + 'n1').AndReturn(self.ports) + + self.mox.StubOutWithMock(api, 'quantum_port_attachment') + api.quantum_port_attachment(IsA(http.HttpRequest), + 'n1', 'p1').AndReturn(self.port_attachment) + + self.mox.ReplayAll() + + formData = {'new_name': 'Test1', + 'method': 'RenameNetwork'} + + res = self.client.post(reverse('horizon:nova:networks:rename', + args=["n1"]), + formData) + + def test_network_details(self): + self.mox.StubOutWithMock(api, 'quantum_network_details') + api.quantum_network_details(IsA(http.HttpRequest), + 'n1').AndReturn(self.network_details) + + self.mox.StubOutWithMock(api, 'quantum_list_ports') + api.quantum_list_ports(IsA(http.HttpRequest), + 'n1').AndReturn(self.ports) + + self.mox.StubOutWithMock(api, 'quantum_port_attachment') + api.quantum_port_attachment(IsA(http.HttpRequest), + 'n1', 'p1').AndReturn(self.port_attachment) + + self.mox.StubOutWithMock(api, 'quantum_port_details') + api.quantum_port_details(IsA(http.HttpRequest), + 'n1', 'p1').AndReturn(self.port_details) + + self.mox.StubOutWithMock(api, 'get_vif_ids') + api.get_vif_ids(IsA(http.HttpRequest)).AndReturn(self.vifs) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:networks:detail', + args=['n1'])) + + self.assertTemplateUsed(res, 'nova/networks/detail.html') + self.assertIn('network', res.context) + + network = res.context['network'] + + self.assertEqual(network['name'], 'test_network') + self.assertEqual(network['id'], 'n1') + + self.mox.VerifyAll() + + +class PortViewTests(test.BaseViewTests): + def setUp(self): + super(PortViewTests, self).setUp() + + def test_port_create(self): + self.mox.StubOutWithMock(api, "quantum_create_port") + api.quantum_create_port(IsA(http.HttpRequest), 'n1').AndReturn(True) + + formData = {'ports_num': 1, + 'network': 'n1', + 'method': 'CreatePort'} + + self.mox.StubOutWithMock(messages, 'success') + messages.success(IgnoreArg(), IsA(basestring)) + + res = self.client.post(reverse('horizon:nova:networks:port_create', + args=["n1"]), + formData) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:networks:detail', + args=["n1"])) + + def test_port_delete(self): + self.mox.StubOutWithMock(api, "quantum_delete_port") + api.quantum_delete_port(IsA(http.HttpRequest), + 'n1', 'p1').AndReturn(True) + + formData = {'port': 'p1', + 'network': 'n1', + 'method': 'DeletePort'} + + self.mox.StubOutWithMock(messages, 'success') + messages.success(IgnoreArg(), IsA(basestring)) + + res = self.client.post(reverse('horizon:nova:networks:detail', + args=["n1"]), + formData) + + def test_port_attach(self): + self.mox.StubOutWithMock(api, "quantum_attach_port") + api.quantum_attach_port(IsA(http.HttpRequest), + 'n1', 'p1', dict).AndReturn(True) + + formData = {'port': 'p1', + 'network': 'n1', + 'vif_id': 'v1', + 'method': 'AttachPort'} + + self.mox.StubOutWithMock(messages, 'success') + messages.success(IgnoreArg(), IsA(basestring)) + + res = self.client.post(reverse('horizon:nova:networks:port_attach', + args=["n1", "p1"]), + formData) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:networks:detail', + args=["n1"])) + + def test_port_detach(self): + self.mox.StubOutWithMock(api, "quantum_detach_port") + api.quantum_detach_port(IsA(http.HttpRequest), + 'n1', 'p1').AndReturn(True) + + formData = {'port': 'p1', + 'network': 'n1', + 'method': 'DetachPort'} + + self.mox.StubOutWithMock(messages, 'success') + messages.success(IgnoreArg(), IsA(basestring)) + + res = self.client.post(reverse('horizon:nova:networks:detail', + args=["n1"]), + formData) diff --git a/horizon/horizon/dashboards/nova/networks/urls.py b/horizon/horizon/dashboards/nova/networks/urls.py new file mode 100644 index 00000000..fd0c5ae0 --- /dev/null +++ b/horizon/horizon/dashboards/nova/networks/urls.py @@ -0,0 +1,31 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls.defaults import patterns, url + +urlpatterns = patterns('horizon.dashboards.nova.networks.views', + url(r'^$', 'index', name='index'), + url(r'^create/$', 'create', name='create'), + url(r'^(?P[^/]+)/detail/$', 'detail', name='detail'), + url(r'^(?P[^/]+)/rename/$', 'rename', name='rename'), + url(r'^(?P[^/]+)/ports/create/$', 'port_create', + name='port_create'), + url(r'^(?P[^/]+)/ports/(?P[^/]+)/attach/$', + 'port_attach', name='port_attach')) diff --git a/horizon/horizon/dashboards/nova/networks/views.py b/horizon/horizon/dashboards/nova/networks/views.py new file mode 100644 index 00000000..69969f65 --- /dev/null +++ b/horizon/horizon/dashboards/nova/networks/views.py @@ -0,0 +1,231 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Views for managing Quantum networks. +""" + +import logging +import warnings + +from django import shortcuts +from django import template +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.utils.translation import ugettext as _ + +from horizon import api +from horizon.dashboards.nova.networks.forms import (CreateNetwork, + DeleteNetwork, RenameNetwork, AttachPort, CreatePort, DeletePort, + DetachPort, TogglePort) + + +LOG = logging.getLogger(__name__) + + +@login_required +def index(request): + tenant_id = request.user.tenant_id + delete_form, delete_handled = DeleteNetwork.maybe_handle(request) + + networks = [] + instances = [] + + try: + networks_list = api.quantum_list_networks(request) + details = [] + for network in networks_list['networks']: + net_stats = _calc_network_stats(request, network['id']) + # Get network details like name and id + details = api.quantum_network_details(request, network['id']) + networks.append({ + 'name': details['network']['name'], + 'id': network['id'], + 'total': net_stats['total'], + 'available': net_stats['available'], + 'used': net_stats['used'], + 'tenant': tenant_id}) + + except Exception, e: + LOG.exception("Unable to get network list.") + messages.error(request, + _('Unable to get network list: %s') % e.message) + + return shortcuts.render(request, + 'nova/networks/index.html', { + 'networks': networks, + 'delete_form': delete_form}) + + +@login_required +def create(request): + network_form, handled = CreateNetwork.maybe_handle(request) + if handled: + return shortcuts.redirect('horizon:nova:networks:index') + + return shortcuts.render(request, + 'nova/networks/create.html', + {'network_form': network_form}) + + +@login_required +def detail(request, network_id): + tenant_id = request.user.tenant_id + delete_port_form, delete_handled = DeletePort.maybe_handle(request) + detach_port_form, detach_handled = DetachPort.maybe_handle(request) + toggle_port_form, port_toggle_handled = TogglePort.maybe_handle(request) + + network = {} + + try: + network_details = api.quantum_network_details(request, network_id) + network['name'] = network_details['network']['name'] + network['id'] = network_id + network['ports'] = _get_port_states(request, network_id) + except Exception, e: + LOG.exception("Unable to get network details.") + messages.error(request, + _('Unable to get network details: %s') % e.message) + + return shortcuts.render(request, + 'nova/networks/detail.html', + {'network': network, + 'tenant': tenant_id, + 'delete_port_form': delete_port_form, + 'detach_port_form': detach_port_form, + 'toggle_port_form': toggle_port_form}) + + +@login_required +def rename(request, network_id): + rename_form, handled = RenameNetwork.maybe_handle(request) + network_details = api.quantum_network_details(request, network_id) + + if handled: + return shortcuts.redirect('horizon:nova:networks:index') + + return shortcuts.render(request, + 'nova/networks/rename.html', { + 'network': network_details, + 'rename_form': rename_form}) + + +def _get_port_states(request, network_id): + """ + Helper method to find port states for a network + """ + network_ports = [] + # Get all vifs for comparison with port attachments + vifs = api.get_vif_ids(request) + + # Get all ports on this network + ports = api.quantum_list_ports(request, network_id) + for port in ports['ports']: + port_details = api.quantum_port_details(request, + network_id, port['id']) + # Get port attachments + port_attachment = api.quantum_port_attachment(request, + network_id, port['id']) + # Find instance the attachment belongs to + connected_instance = None + if port_attachment['attachment']: + for vif in vifs: + if str(vif['id']) == str(port_attachment['attachment']['id']): + connected_instance = vif['instance_name'] + break + network_ports.append({ + 'id': port_details['port']['id'], + 'state': port_details['port']['state'], + 'attachment': port_attachment['attachment'], + 'instance': connected_instance}) + return network_ports + + +def _calc_network_stats(request, network_id): + """ + Helper method to calculate statistics for a network + """ + # Get all ports statistics for the network + total = 0 + available = 0 + used = 0 + ports = api.quantum_list_ports(request, network_id) + for port in ports['ports']: + total += 1 + # Get port attachment + port_attachment = api.quantum_port_attachment(request, + network_id, port['id']) + if port_attachment['attachment']: + used += 1 + else: + available += 1 + + return {'total': total, 'used': used, 'available': available} + + +@login_required +def port_create(request, network_id): + create_form, handled = CreatePort.maybe_handle(request) + + if handled: + return shortcuts.redirect('horizon:nova:networks:detail', + network_id=network_id) + + return shortcuts.render(request, + 'nova/ports/create.html', { + 'network_id': network_id, + 'create_form': create_form}) + + +@login_required +def port_attach(request, network_id, port_id): + attach_form, handled = AttachPort.maybe_handle(request) + + if handled: + return shortcuts.redirect('horizon:nova:networks:detail', + network_id=network_id) + + # Get all avaliable vifs + vifs = _get_available_vifs(request) + + return shortcuts.render(request, + 'nova/ports/attach.html', { + 'network': network_id, + 'port': port_id, + 'attach_form': attach_form, + 'vifs': vifs}) + + +def _get_available_vifs(request): + """ + Method to get a list of available virtual interfaces + """ + vif_choices = [] + vifs = api.get_vif_ids(request) + + for vif in vifs: + if vif['available']: + name = "Instance %s VIF %s" % \ + (str(vif['instance_name']), str(vif['id'])) + vif_choices.append({ + 'name': str(name), + 'id': str(vif['id'])}) + + return vif_choices diff --git a/horizon/horizon/dashboards/nova/overview/__init__.py b/horizon/horizon/dashboards/nova/overview/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/nova/overview/panel.py b/horizon/horizon/dashboards/nova/overview/panel.py new file mode 100644 index 00000000..fbd5b5d7 --- /dev/null +++ b/horizon/horizon/dashboards/nova/overview/panel.py @@ -0,0 +1,30 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import horizon +from horizon.dashboards.nova import dashboard + + +class Overview(horizon.Panel): + name = "Overview" + slug = 'overview' + + +dashboard.Nova.register(Overview) diff --git a/horizon/horizon/dashboards/nova/overview/urls.py b/horizon/horizon/dashboards/nova/overview/urls.py new file mode 100644 index 00000000..97e1d3da --- /dev/null +++ b/horizon/horizon/dashboards/nova/overview/urls.py @@ -0,0 +1,26 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from django.conf.urls.defaults import * + +urlpatterns = patterns('horizon.dashboards.nova', + url(r'^$', 'instances.views.usage', name='index'), +) diff --git a/horizon/horizon/dashboards/nova/security_groups/__init__.py b/horizon/horizon/dashboards/nova/security_groups/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/nova/security_groups/forms.py b/horizon/horizon/dashboards/nova/security_groups/forms.py new file mode 100644 index 00000000..1c27f0cb --- /dev/null +++ b/horizon/horizon/dashboards/nova/security_groups/forms.py @@ -0,0 +1,128 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django import shortcuts +from django.contrib import messages +from django.core import validators +from django.utils.translation import ugettext as _ +from novaclient import exceptions as novaclient_exceptions + +from horizon import api +from horizon import forms + + +LOG = logging.getLogger(__name__) + + +class CreateGroup(forms.SelfHandlingForm): + name = forms.CharField(validators=[validators.validate_slug]) + description = forms.CharField() + tenant_id = forms.CharField(widget=forms.HiddenInput()) + + def handle(self, request, data): + try: + LOG.info('Add security_group: "%s"' % data) + + security_group = api.security_group_create(request, + data['name'], + data['description']) + messages.info(request, _('Successfully created security_group: %s') + % data['name']) + return shortcuts.redirect('horizon:nova:security_groups:index') + except novaclient_exceptions.ClientException, e: + LOG.exception("ClientException in CreateGroup") + messages.error(request, _('Error creating security group: %s') % + e.message) + + +class DeleteGroup(forms.SelfHandlingForm): + tenant_id = forms.CharField(widget=forms.HiddenInput()) + security_group_id = forms.CharField(widget=forms.HiddenInput()) + + def handle(self, request, data): + try: + LOG.info('Delete security_group: "%s"' % data) + + security_group = api.security_group_delete(request, + data['security_group_id']) + messages.info(request, _('Successfully deleted security_group: %s') + % data['security_group_id']) + except novaclient_exceptions.ClientException, e: + LOG.exception("ClientException in DeleteGroup") + messages.error(request, _('Error deleting security group: %s') + % e.message) + return shortcuts.redirect('horizon:nova:security_groups:index') + + +class AddRule(forms.SelfHandlingForm): + ip_protocol = forms.ChoiceField(choices=[('tcp', 'tcp'), + ('udp', 'udp'), + ('icmp', 'icmp')]) + from_port = forms.CharField() + to_port = forms.CharField() + cidr = forms.CharField() + # TODO (anthony) source group support + # group_id = forms.CharField() + + security_group_id = forms.CharField(widget=forms.HiddenInput()) + tenant_id = forms.CharField(widget=forms.HiddenInput()) + + def handle(self, request, data): + tenant_id = data['tenant_id'] + try: + LOG.info('Add security_group_rule: "%s"' % data) + + rule = api.security_group_rule_create(request, + data['security_group_id'], + data['ip_protocol'], + data['from_port'], + data['to_port'], + data['cidr']) + messages.info(request, _('Successfully added rule: %s') \ + % rule.id) + except novaclient_exceptions.ClientException, e: + LOG.exception("ClientException in AddRule") + messages.error(request, _('Error adding rule security group: %s') + % e.message) + return shortcuts.redirect(request.build_absolute_uri()) + + +class DeleteRule(forms.SelfHandlingForm): + security_group_rule_id = forms.CharField(widget=forms.HiddenInput()) + tenant_id = forms.CharField(widget=forms.HiddenInput()) + + def handle(self, request, data): + security_group_rule_id = data['security_group_rule_id'] + tenant_id = data['tenant_id'] + try: + LOG.info('Delete security_group_rule: "%s"' % data) + + security_group = api.security_group_rule_delete( + request, + security_group_rule_id) + messages.info(request, _('Successfully deleted rule: %s') + % security_group_rule_id) + except novaclient_exceptions.ClientException, e: + LOG.exception("ClientException in DeleteRule") + messages.error(request, _('Error authorizing security group: %s') + % e.message) + return shortcuts.redirect(request.build_absolute_uri()) diff --git a/horizon/horizon/dashboards/nova/security_groups/panel.py b/horizon/horizon/dashboards/nova/security_groups/panel.py new file mode 100644 index 00000000..344238de --- /dev/null +++ b/horizon/horizon/dashboards/nova/security_groups/panel.py @@ -0,0 +1,30 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import horizon +from horizon.dashboards.nova import dashboard + + +class SecurityGroups(horizon.Panel): + name = "Security Groups" + slug = 'security_groups' + + +dashboard.Nova.register(SecurityGroups) diff --git a/horizon/horizon/dashboards/nova/security_groups/tests.py b/horizon/horizon/dashboards/nova/security_groups/tests.py new file mode 100644 index 00000000..de061f6f --- /dev/null +++ b/horizon/horizon/dashboards/nova/security_groups/tests.py @@ -0,0 +1,331 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django import http +from django.contrib import messages +from django.core.urlresolvers import reverse +from glance.common import exception as glance_exception +from openstackx.api import exceptions as api_exceptions +from novaclient import exceptions as novaclient_exceptions +from mox import IgnoreArg, IsA + +from horizon import api +from horizon import test + +SECGROUP_ID = '1' +SG_INDEX_URL = reverse('horizon:nova:security_groups:index') +SG_CREATE_URL = reverse('horizon:nova:security_groups:create') +SG_EDIT_RULE_URL = reverse('horizon:nova:security_groups:edit_rules', + args=[SECGROUP_ID]) + + +class SecurityGroupsViewTests(test.BaseViewTests): + def setUp(self): + super(SecurityGroupsViewTests, self).setUp() + + security_group = self.mox.CreateMock(api.SecurityGroup) + security_group.name = 'default' + self.security_groups = (security_group,) + + def test_index(self): + self.mox.StubOutWithMock(api, 'security_group_list') + api.security_group_list(IsA(http.HttpRequest)).\ + AndReturn(self.security_groups) + + self.mox.ReplayAll() + + res = self.client.get(SG_INDEX_URL) + + self.assertTemplateUsed(res, 'nova/security_groups/index.html') + self.assertItemsEqual(res.context['security_groups'], + self.security_groups) + + self.mox.VerifyAll() + + def test_index_exception(self): + exception = novaclient_exceptions.ClientException('ClientException', + message='ClientException') + self.mox.StubOutWithMock(api, 'security_group_list') + api.security_group_list(IsA(http.HttpRequest)).AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(basestring)) + + self.mox.ReplayAll() + + res = self.client.get(SG_INDEX_URL) + + self.assertTemplateUsed(res, 'nova/security_groups/index.html') + self.assertEqual(len(res.context['security_groups']), 0) + + self.mox.VerifyAll() + + def test_create_security_groups_get(self): + res = self.client.get(SG_CREATE_URL) + + self.assertTemplateUsed(res, 'nova/security_groups/create.html') + + def test_create_security_groups_post(self): + SECGROUP_NAME = 'fakegroup' + SECGROUP_DESC = 'fakegroup_desc' + + new_group = self.mox.CreateMock(api.SecurityGroup) + new_group.name = SECGROUP_NAME + + formData = {'method': 'CreateGroup', + 'tenant_id': self.TEST_TENANT, + 'name': SECGROUP_NAME, + 'description': SECGROUP_DESC, + } + + self.mox.StubOutWithMock(api, 'security_group_create') + api.security_group_create(IsA(http.HttpRequest), + SECGROUP_NAME, SECGROUP_DESC).AndReturn(new_group) + + self.mox.ReplayAll() + + res = self.client.post(SG_CREATE_URL, formData) + + self.assertRedirectsNoFollow(res, SG_INDEX_URL) + + self.mox.VerifyAll() + + def test_create_security_groups_post_exception(self): + SECGROUP_NAME = 'fakegroup' + SECGROUP_DESC = 'fakegroup_desc' + + exception = novaclient_exceptions.ClientException('ClientException', + message='ClientException') + + formData = {'method': 'CreateGroup', + 'tenant_id': self.TEST_TENANT, + 'name': SECGROUP_NAME, + 'description': SECGROUP_DESC, + } + + self.mox.StubOutWithMock(api, 'security_group_create') + api.security_group_create(IsA(http.HttpRequest), + SECGROUP_NAME, SECGROUP_DESC).AndRaise(exception) + + self.mox.ReplayAll() + + res = self.client.post(SG_CREATE_URL, formData) + + self.assertTemplateUsed(res, 'nova/security_groups/create.html') + + self.mox.VerifyAll() + + def test_edit_rules_get(self): + + self.mox.StubOutWithMock(api, 'security_group_get') + api.security_group_get(IsA(http.HttpRequest), SECGROUP_ID).AndReturn( + self.security_groups[0]) + + self.mox.ReplayAll() + + res = self.client.get(SG_EDIT_RULE_URL) + + self.assertTemplateUsed(res, 'nova/security_groups/edit_rules.html') + self.assertItemsEqual(res.context['security_group'].name, + self.security_groups[0].name) + + self.mox.VerifyAll() + + def test_edit_rules_get_exception(self): + exception = novaclient_exceptions.ClientException('ClientException', + message='ClientException') + + self.mox.StubOutWithMock(api, 'security_group_get') + api.security_group_get(IsA(http.HttpRequest), SECGROUP_ID).AndRaise( + exception) + + self.mox.ReplayAll() + + res = self.client.get(SG_EDIT_RULE_URL) + + self.assertRedirectsNoFollow(res, SG_INDEX_URL) + + self.mox.VerifyAll() + + def test_edit_rules_add_rule(self): + RULE_ID = '1' + FROM_PORT = '-1' + TO_PORT = '-1' + IP_PROTOCOL = 'icmp' + CIDR = '0.0.0.0/0' + + new_rule = self.mox.CreateMock(api.SecurityGroup) + new_rule.from_port = FROM_PORT + new_rule.to_port = TO_PORT + new_rule.ip_protocol = IP_PROTOCOL + new_rule.cidr = CIDR + new_rule.security_group_id = SECGROUP_ID + new_rule.id = RULE_ID + + formData = {'method': 'AddRule', + 'tenant_id': self.TEST_TENANT, + 'security_group_id': SECGROUP_ID, + 'from_port': FROM_PORT, + 'to_port': TO_PORT, + 'ip_protocol': IP_PROTOCOL, + 'cidr': CIDR} + + self.mox.StubOutWithMock(api, 'security_group_rule_create') + api.security_group_rule_create(IsA(http.HttpRequest), + SECGROUP_ID, IP_PROTOCOL, FROM_PORT, TO_PORT, CIDR)\ + .AndReturn(new_rule) + + self.mox.StubOutWithMock(messages, 'info') + messages.info(IsA(http.HttpRequest), IsA(basestring)) + + self.mox.ReplayAll() + + res = self.client.post(SG_EDIT_RULE_URL, formData) + + self.assertRedirectsNoFollow(res, SG_EDIT_RULE_URL) + + self.mox.VerifyAll() + + def test_edit_rules_add_rule_exception(self): + exception = novaclient_exceptions.ClientException('ClientException', + message='ClientException') + + RULE_ID = '1' + FROM_PORT = '-1' + TO_PORT = '-1' + IP_PROTOCOL = 'icmp' + CIDR = '0.0.0.0/0' + + formData = {'method': 'AddRule', + 'tenant_id': self.TEST_TENANT, + 'security_group_id': SECGROUP_ID, + 'from_port': FROM_PORT, + 'to_port': TO_PORT, + 'ip_protocol': IP_PROTOCOL, + 'cidr': CIDR} + + self.mox.StubOutWithMock(api, 'security_group_rule_create') + api.security_group_rule_create(IsA(http.HttpRequest), + SECGROUP_ID, IP_PROTOCOL, FROM_PORT, + TO_PORT, CIDR).AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(basestring)) + + self.mox.ReplayAll() + + res = self.client.post(SG_EDIT_RULE_URL, formData) + + self.assertRedirectsNoFollow(res, SG_EDIT_RULE_URL) + + self.mox.VerifyAll() + + def test_edit_rules_delete_rule(self): + RULE_ID = '1' + + formData = {'method': 'DeleteRule', + 'tenant_id': self.TEST_TENANT, + 'security_group_rule_id': RULE_ID, + } + + self.mox.StubOutWithMock(api, 'security_group_rule_delete') + api.security_group_rule_delete(IsA(http.HttpRequest), RULE_ID) + + self.mox.StubOutWithMock(messages, 'info') + messages.info(IsA(http.HttpRequest), IsA(unicode)) + + self.mox.ReplayAll() + + res = self.client.post(SG_EDIT_RULE_URL, formData) + + self.assertRedirectsNoFollow(res, SG_EDIT_RULE_URL) + + self.mox.VerifyAll() + + def test_edit_rules_delete_rule_exception(self): + exception = novaclient_exceptions.ClientException('ClientException', + message='ClientException') + + RULE_ID = '1' + + formData = {'method': 'DeleteRule', + 'tenant_id': self.TEST_TENANT, + 'security_group_rule_id': RULE_ID, + } + + self.mox.StubOutWithMock(api, 'security_group_rule_delete') + api.security_group_rule_delete(IsA(http.HttpRequest), RULE_ID).\ + AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(basestring)) + + self.mox.ReplayAll() + + res = self.client.post(SG_EDIT_RULE_URL, formData) + + self.assertRedirectsNoFollow(res, SG_EDIT_RULE_URL) + + self.mox.VerifyAll() + + def test_delete_group(self): + + formData = {'method': 'DeleteGroup', + 'tenant_id': self.TEST_TENANT, + 'security_group_id': SECGROUP_ID, + } + + self.mox.StubOutWithMock(api, 'security_group_delete') + api.security_group_delete(IsA(http.HttpRequest), SECGROUP_ID) + + self.mox.StubOutWithMock(messages, 'info') + messages.info(IsA(http.HttpRequest), IsA(unicode)) + + self.mox.ReplayAll() + + res = self.client.post(SG_INDEX_URL, formData) + + self.assertRedirectsNoFollow(res, SG_INDEX_URL) + + self.mox.VerifyAll() + + def test_delete_group_exception(self): + exception = novaclient_exceptions.ClientException('ClientException', + message='ClientException') + + formData = {'method': 'DeleteGroup', + 'tenant_id': self.TEST_TENANT, + 'security_group_id': SECGROUP_ID, + } + + self.mox.StubOutWithMock(api, 'security_group_delete') + api.security_group_delete(IsA(http.HttpRequest), SECGROUP_ID).\ + AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(basestring)) + + self.mox.ReplayAll() + + res = self.client.post(SG_INDEX_URL, formData) + + self.assertRedirectsNoFollow(res, SG_INDEX_URL) + + self.mox.VerifyAll() diff --git a/horizon/horizon/dashboards/nova/security_groups/urls.py b/horizon/horizon/dashboards/nova/security_groups/urls.py new file mode 100644 index 00000000..691a7461 --- /dev/null +++ b/horizon/horizon/dashboards/nova/security_groups/urls.py @@ -0,0 +1,28 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls.defaults import patterns, url + + +urlpatterns = patterns('horizon.dashboards.nova.security_groups.views', + url(r'^$', 'index', name='index'), + url(r'^create/$', 'create', name='create'), + url(r'^(?P[^/]+)/edit_rules/$', 'edit_rules', + name='edit_rules')) diff --git a/horizon/horizon/dashboards/nova/security_groups/views.py b/horizon/horizon/dashboards/nova/security_groups/views.py new file mode 100644 index 00000000..9d6530c4 --- /dev/null +++ b/horizon/horizon/dashboards/nova/security_groups/views.py @@ -0,0 +1,103 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Views for managing Nova instances. +""" +import logging + +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django import shortcuts +from django.utils.translation import ugettext as _ +from novaclient import exceptions as novaclient_exceptions + +from horizon import api +from horizon.dashboards.nova.security_groups.forms import (CreateGroup, + DeleteGroup, AddRule, DeleteRule) + + +LOG = logging.getLogger(__name__) + + +@login_required +def index(request): + tenant_id = request.user.tenant_id + delete_form, handled = DeleteGroup.maybe_handle(request, + initial={'tenant_id': tenant_id}) + + if handled: + return handled + + try: + security_groups = api.security_group_list(request) + except novaclient_exceptions.ClientException, e: + security_groups = [] + LOG.exception("ClientException in security_groups index") + messages.error(request, _('Error fetching security_groups: %s') + % e.message) + + return shortcuts.render(request, + 'nova/security_groups/index.html', { + 'security_groups': security_groups, + 'delete_form': delete_form}) + + +@login_required +def edit_rules(request, security_group_id): + tenant_id = request.user.tenant_id + add_form, handled = AddRule.maybe_handle(request, + initial={'tenant_id': tenant_id, + 'security_group_id': security_group_id}) + if handled: + return handled + + delete_form, handled = DeleteRule.maybe_handle(request, + initial={'tenant_id': tenant_id, + 'security_group_id': security_group_id}) + if handled: + return handled + + try: + security_group = api.security_group_get(request, security_group_id) + except novaclient_exceptions.ClientException, e: + LOG.exception("ClientException in security_groups rules edit") + messages.error(request, _('Error getting security_group: %s') + % e.message) + return shortcuts.redirect('horizon:nova:security_groups:index') + + return shortcuts.render(request, + 'nova/security_groups/edit_rules.html', { + 'security_group': security_group, + 'delete_form': delete_form, + 'form': add_form}) + + +@login_required +def create(request): + tenant_id = request.user.tenant_id + form, handled = CreateGroup.maybe_handle(request, + initial={'tenant_id': tenant_id}) + if handled: + return handled + + return shortcuts.render(request, + 'nova/security_groups/create.html', { + 'form': form}) diff --git a/horizon/horizon/dashboards/nova/snapshots/__init__.py b/horizon/horizon/dashboards/nova/snapshots/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/nova/snapshots/forms.py b/horizon/horizon/dashboards/nova/snapshots/forms.py new file mode 100644 index 00000000..3666c399 --- /dev/null +++ b/horizon/horizon/dashboards/nova/snapshots/forms.py @@ -0,0 +1,57 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django import shortcuts +from django.contrib import messages +from django.utils.translation import ugettext as _ +from openstackx.api import exceptions as api_exceptions + +from horizon import api +from horizon import forms + + +LOG = logging.getLogger(__name__) + + +class CreateSnapshot(forms.SelfHandlingForm): + tenant_id = forms.CharField(widget=forms.HiddenInput()) + instance_id = forms.CharField(widget=forms.TextInput( + attrs={'readonly': 'readonly'})) + name = forms.CharField(max_length="20", label=_("Snapshot Name")) + + def handle(self, request, data): + try: + LOG.info('Creating snapshot "%s"' % data['name']) + snapshot = api.snapshot_create(request, + data['instance_id'], + data['name']) + instance = api.server_get(request, data['instance_id']) + + messages.info(request, + _('Snapshot "%(name)s" created for instance "%(inst)s"') % + {"name": data['name'], "inst": instance.name}) + return shortcuts.redirect('horizon:nova:snapshots:index') + except api_exceptions.ApiException, e: + msg = _('Error Creating Snapshot: %s') % e.message + LOG.exception(msg) + messages.error(request, msg) + return shortcuts.redirect(request.build_absolute_uri()) diff --git a/horizon/horizon/dashboards/nova/snapshots/panel.py b/horizon/horizon/dashboards/nova/snapshots/panel.py new file mode 100644 index 00000000..5b17b193 --- /dev/null +++ b/horizon/horizon/dashboards/nova/snapshots/panel.py @@ -0,0 +1,30 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import horizon +from horizon.dashboards.nova import dashboard + + +class Snapshots(horizon.Panel): + name = "Snapshots" + slug = 'snapshots' + + +dashboard.Nova.register(Snapshots) diff --git a/horizon/horizon/dashboards/nova/snapshots/tests.py b/horizon/horizon/dashboards/nova/snapshots/tests.py new file mode 100644 index 00000000..c0c70f18 --- /dev/null +++ b/horizon/horizon/dashboards/nova/snapshots/tests.py @@ -0,0 +1,202 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django import http +from django.contrib import messages +from django.core.urlresolvers import reverse +from glance.common import exception as glance_exception +from openstackx.api import exceptions as api_exceptions +from mox import IgnoreArg, IsA + +from horizon import api +from horizon import test + + +class SnapshotsViewTests(test.BaseViewTests): + def setUp(self): + super(SnapshotsViewTests, self).setUp() + image_dict = {'name': 'snapshot', + 'container_format': 'novaImage'} + self.images = [image_dict] + + server = self.mox.CreateMock(api.Server) + server.id = 1 + server.status = 'ACTIVE' + server.name = 'sgoody' + self.good_server = server + + server = self.mox.CreateMock(api.Server) + server.id = 2 + server.status = 'BUILD' + server.name = 'baddy' + self.bad_server = server + + def test_index(self): + self.mox.StubOutWithMock(api, 'snapshot_list_detailed') + api.snapshot_list_detailed(IsA(http.HttpRequest)).\ + AndReturn(self.images) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:snapshots:index')) + + self.assertTemplateUsed(res, 'nova/snapshots/index.html') + + self.assertIn('images', res.context) + images = res.context['images'] + self.assertEqual(len(images), 1) + + self.mox.VerifyAll() + + def test_index_client_conn_error(self): + self.mox.StubOutWithMock(api, 'snapshot_list_detailed') + exception = glance_exception.ClientConnectionError('clientConnError') + api.snapshot_list_detailed(IsA(http.HttpRequest)).AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(basestring)) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:snapshots:index')) + + self.assertTemplateUsed(res, 'nova/snapshots/index.html') + + self.mox.VerifyAll() + + def test_index_glance_error(self): + self.mox.StubOutWithMock(api, 'snapshot_list_detailed') + exception = glance_exception.Error('glanceError') + api.snapshot_list_detailed(IsA(http.HttpRequest)).AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(basestring)) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:snapshots:index')) + + self.assertTemplateUsed(res, 'nova/snapshots/index.html') + + self.mox.VerifyAll() + + def test_create_snapshot_get(self): + self.mox.StubOutWithMock(api, 'server_get') + api.server_get(IsA(http.HttpRequest), + str(self.good_server.id)).AndReturn(self.good_server) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:snapshots:create', + args=[self.good_server.id])) + + self.assertTemplateUsed(res, 'nova/snapshots/create.html') + self.mox.VerifyAll() + + def test_create_snapshot_get_with_invalid_status(self): + self.mox.StubOutWithMock(api, 'server_get') + api.server_get(IsA(http.HttpRequest), + str(self.bad_server.id)).AndReturn(self.bad_server) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:snapshots:create', + args=[self.bad_server.id])) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:instances:index')) + self.mox.VerifyAll() + + def test_create_get_server_exception(self): + self.mox.StubOutWithMock(api, 'server_get') + exception = api_exceptions.ApiException('apiException') + api.server_get(IsA(http.HttpRequest), + str(self.good_server.id)).AndRaise(exception) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:snapshots:create', + args=[self.good_server.id])) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:instances:index')) + + self.mox.VerifyAll() + + def test_create_snapshot_post(self): + SNAPSHOT_NAME = 'snappy' + + new_snapshot = self.mox.CreateMock(api.Image) + new_snapshot.name = SNAPSHOT_NAME + + formData = {'method': 'CreateSnapshot', + 'tenant_id': self.TEST_TENANT, + 'instance_id': self.good_server.id, + 'name': SNAPSHOT_NAME} + + self.mox.StubOutWithMock(api, 'server_get') + api.server_get(IsA(http.HttpRequest), + str(self.good_server.id)).AndReturn(self.good_server) + + self.mox.StubOutWithMock(api, 'snapshot_create') + api.snapshot_create(IsA(http.HttpRequest), + str(self.good_server.id), SNAPSHOT_NAME).\ + AndReturn(new_snapshot) + + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:nova:snapshots:create', + args=[self.good_server.id]), + formData) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:snapshots:index')) + + self.mox.VerifyAll() + + def test_create_snapshot_post_exception(self): + SNAPSHOT_NAME = 'snappy' + + new_snapshot = self.mox.CreateMock(api.Image) + new_snapshot.name = SNAPSHOT_NAME + + formData = {'method': 'CreateSnapshot', + 'tenant_id': self.TEST_TENANT, + 'instance_id': self.good_server.id, + 'name': SNAPSHOT_NAME} + + self.mox.StubOutWithMock(api, 'snapshot_create') + exception = api_exceptions.ApiException('apiException', + message='apiException') + api.snapshot_create(IsA(http.HttpRequest), + str(self.good_server.id), SNAPSHOT_NAME).\ + AndRaise(exception) + + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:nova:snapshots:create', + args=[self.good_server.id]), + formData) + + self.assertRedirectsNoFollow(res, + reverse('horizon:nova:snapshots:create', + args=[self.good_server.id])) + + self.mox.VerifyAll() diff --git a/horizon/horizon/dashboards/nova/snapshots/urls.py b/horizon/horizon/dashboards/nova/snapshots/urls.py new file mode 100644 index 00000000..64b43af8 --- /dev/null +++ b/horizon/horizon/dashboards/nova/snapshots/urls.py @@ -0,0 +1,26 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls.defaults import patterns, url + + +urlpatterns = patterns('horizon.dashboards.nova.snapshots.views', + url(r'^$', 'index', name='index'), + url(r'^(?P[^/]+)/create', 'create', name='create')) diff --git a/horizon/horizon/dashboards/nova/snapshots/views.py b/horizon/horizon/dashboards/nova/snapshots/views.py new file mode 100644 index 00000000..5d99484b --- /dev/null +++ b/horizon/horizon/dashboards/nova/snapshots/views.py @@ -0,0 +1,92 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Views for managing Nova instance snapshots. +""" + +import logging +import re + +from django import http +from django import shortcuts +from django import template +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.utils.translation import ugettext as _ +from glance.common import exception as glance_exception +from openstackx.api import exceptions as api_exceptions + +from horizon import api +from horizon import forms +from horizon.dashboards.nova.snapshots.forms import CreateSnapshot + + +LOG = logging.getLogger(__name__) + + +@login_required +def index(request): + images = [] + + try: + images = api.snapshot_list_detailed(request) + except glance_exception.ClientConnectionError, e: + msg = _('Error connecting to glance: %s') % str(e) + LOG.exception(msg) + messages.error(request, msg) + except glance_exception.Error, e: + msg = _('Error retrieving image list: %s') % str(e) + LOG.exception(msg) + messages.error(request, msg) + + return shortcuts.render(request, + 'nova/snapshots/index.html', + {'images': images}) + + +@login_required +def create(request, instance_id): + tenant_id = request.user.tenant_id + form, handled = CreateSnapshot.maybe_handle(request, + initial={'tenant_id': tenant_id, + 'instance_id': instance_id}) + if handled: + return handled + + try: + instance = api.server_get(request, instance_id) + except api_exceptions.ApiException, e: + msg = _("Unable to retreive instance: %s") % e + LOG.exception(msg) + messages.error(request, msg) + return shortcuts.redirect('horizon:nova:instances:index') + + valid_states = ['ACTIVE'] + if instance.status not in valid_states: + messages.error(request, _("To snapshot, instance state must be\ + one of the following: %s") % + ', '.join(valid_states)) + return shortcuts.redirect('horizon:nova:instances:index') + + return shortcuts.render(request, + 'nova/snapshots/create.html', + {'instance': instance, + 'create_form': form}) diff --git a/horizon/horizon/dashboards/nova/templates/nova/base.html b/horizon/horizon/dashboards/nova/templates/nova/base.html new file mode 100644 index 00000000..2fe83bb6 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/base.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} + +{% block sidebar %} + {% include 'horizon/common/_sidebar.html' %} +{% endblock %} + +{% block main %} + {% block page_header %}{% endblock %} +
+ {% include "_messages.html" %} + {% block dash_main %}{% endblock %} +
+{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/containers/_delete.html b/horizon/horizon/dashboards/nova/templates/nova/containers/_delete.html new file mode 100644 index 00000000..97ee0609 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/containers/_delete.html @@ -0,0 +1,9 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/containers/_form.html b/horizon/horizon/dashboards/nova/templates/nova/containers/_form.html new file mode 100644 index 00000000..155a20d2 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/containers/_form.html @@ -0,0 +1,11 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/containers/_list.html b/horizon/horizon/dashboards/nova/templates/nova/containers/_list.html new file mode 100644 index 00000000..fe7828f5 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/containers/_list.html @@ -0,0 +1,29 @@ +{% load i18n swift_paging %} + + + + + + + + + + {% for container in containers %} + + + + + {% endfor %} + + + + + + +
{% trans "Name" %}{% trans "Actions" %}
{{ container.name }} + +
{% object_paging containers %}
diff --git a/horizon/horizon/dashboards/nova/templates/nova/containers/create.html b/horizon/horizon/dashboards/nova/templates/nova/containers/create.html new file mode 100644 index 00000000..4d4cde4e --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/containers/create.html @@ -0,0 +1,28 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="containers" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create Tenant") %} +{% endblock page_header %} + +{% block dash_main %} +
+
+ {% include 'nova/containers/_form.html' with form=create_form %} +
+ +
+

Description:

+

{% trans "A container is a storage compartment for your data and provides a way for you to organize your data. You can think of a container as a folder in Windows® or a directory in UNIX®. The primary difference between a container and these other file system concepts is that containers cannot be nested. You can, however, create an unlimited number of containers within your account. Data must be stored in a container so you must have at least one container defined in your account prior to uploading data."%}

+
+
 
+
+{% endblock %} + + diff --git a/horizon/horizon/dashboards/nova/templates/nova/containers/index.html b/horizon/horizon/dashboards/nova/templates/nova/containers/index.html new file mode 100644 index 00000000..156f4817 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/containers/index.html @@ -0,0 +1,19 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="containers" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% url horizon:nova:images:index as refresh_link %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Containers") refresh_link=refresh_link searchable="true" %} +{% endblock page_header %} + +{% block dash_main %} + {% include 'nova/containers/_list.html' %} + {% trans "Create New Container"%} >> +{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/floating_ips/_allocate.html b/horizon/horizon/dashboards/nova/templates/nova/floating_ips/_allocate.html new file mode 100644 index 00000000..f18b70b3 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/floating_ips/_allocate.html @@ -0,0 +1,8 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/floating_ips/_associate.html b/horizon/horizon/dashboards/nova/templates/nova/floating_ips/_associate.html new file mode 100644 index 00000000..4e8a3ead --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/floating_ips/_associate.html @@ -0,0 +1,17 @@ +{%load i18n%} +
+ {% csrf_token %} +
+ {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + {% for field in form.visible_fields %} + {{field.label_tag}} + {{field.errors}} + {{field}} + {% endfor %} + {% block submit %} + + {% endblock %} +
+
diff --git a/horizon/horizon/dashboards/nova/templates/nova/floating_ips/_disassociate.html b/horizon/horizon/dashboards/nova/templates/nova/floating_ips/_disassociate.html new file mode 100644 index 00000000..ca5fae02 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/floating_ips/_disassociate.html @@ -0,0 +1,9 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/floating_ips/_list.html b/horizon/horizon/dashboards/nova/templates/nova/floating_ips/_list.html new file mode 100644 index 00000000..92d11884 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/floating_ips/_list.html @@ -0,0 +1,34 @@ +{%load i18n%} + + + + + + + {% for ip in floating_ips %} + + + + + + + {% endfor %} +
IPInstanceActions
{{ ip.ip }} + {% if ip.fixed_ip %} +
    +
  • {%trans "Instance ID:"%} {{ip.instance_id}}
  • +
  • {%trans "Fixed IP:"%} {{ip.fixed_ip}}
  • +
+ {% else %} + None + {% endif %} +
+
    +
  • {% include "nova/floating_ips/_release.html" with form=release_form %}
  • + {% if ip.fixed_ip %} +
  • {% include "nova/floating_ips/_disassociate.html" with form=disassociate_form %}
  • + {% else %} +
  • {%trans "Associate to instance"%}
  • + {% endif %} +
+
diff --git a/horizon/horizon/dashboards/nova/templates/nova/floating_ips/_release.html b/horizon/horizon/dashboards/nova/templates/nova/floating_ips/_release.html new file mode 100644 index 00000000..ae13601d --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/floating_ips/_release.html @@ -0,0 +1,9 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/floating_ips/associate.html b/horizon/horizon/dashboards/nova/templates/nova/floating_ips/associate.html new file mode 100644 index 00000000..5d73f73e --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/floating_ips/associate.html @@ -0,0 +1,27 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="floatingips" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Associate Floating IP") %} +{% endblock page_header %} + +{% block dash_main %} +
+
+ {% include 'nova/floating_ips/_associate.html' with form=associate_form %} +
+ +
+

{% trans "Description:"%}

+

{% trans "Associate a floating ip with an instance."%}

+
+
 
+
+{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/floating_ips/index.html b/horizon/horizon/dashboards/nova/templates/nova/floating_ips/index.html new file mode 100644 index 00000000..013718a0 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/floating_ips/index.html @@ -0,0 +1,26 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="floatingips" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% url horizon:nova:floating_ips:index as refresh_link %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Floating IPs") refresh_link=refresh_link searchable="true" %} +{% endblock page_header %} + +{% block dash_main %} + {% if floating_ips %} + {% include 'nova/floating_ips/_list.html' %} + {% else %} +
+

{%trans "Info"%}

+

{%trans "There are currently no floating ips assigned to your tenant."%}

+
+ {% endif %} + {% include "nova/floating_ips/_allocate.html" with form=allocate_form %} +{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/images/_delete.html b/horizon/horizon/dashboards/nova/templates/nova/images/_delete.html new file mode 100644 index 00000000..cc8def96 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/images/_delete.html @@ -0,0 +1,9 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/images/_form.html b/horizon/horizon/dashboards/nova/templates/nova/images/_form.html new file mode 100644 index 00000000..92dbbade --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/images/_form.html @@ -0,0 +1,11 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/images/_launch.html b/horizon/horizon/dashboards/nova/templates/nova/images/_launch.html new file mode 100644 index 00000000..20f48c3d --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/images/_launch.html @@ -0,0 +1,6 @@ +{% extends 'nova/images/_launch_form.html' %} +{%load i18n%} + +{% block submit %} + +{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/images/_launch_form.html b/horizon/horizon/dashboards/nova/templates/nova/images/_launch_form.html new file mode 100644 index 00000000..8eab67f6 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/images/_launch_form.html @@ -0,0 +1,17 @@ +{%load i18n%} +
+ {% csrf_token %} +
+ {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + {% for field in form.visible_fields %} + {{field.label_tag}} + {{field.errors}} + {{field}} + {% endfor %} + {% block submit %} + + {% endblock %} +
+
diff --git a/horizon/horizon/dashboards/nova/templates/nova/images/_list.html b/horizon/horizon/dashboards/nova/templates/nova/images/_list.html new file mode 100644 index 00000000..b0f25a8d --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/images/_list.html @@ -0,0 +1,29 @@ +{%load i18n%} +{% load parse_date %} + + + + + + + + + {% for image in images %} + + + + + + + + + {% endfor %} +
{%trans "ID"%}{%trans "Name"%}{%trans "Created"%}{%trans "Updated"%}{%trans "Status"%}
{{image.id}}{{image.name}}{{image.created_at|parse_date}}{{image.updated_at|parse_date}}{{image.status|capfirst}} +
    + {% if image.owner == request.user.username %} +
  • {% include "nova/images/_delete.html" with form=delete_form %}
  • +
  • {%trans "Edit"%}
  • + {% endif %} +
  • {%trans "Launch"%}
  • +
+
diff --git a/horizon/horizon/dashboards/nova/templates/nova/images/index.html b/horizon/horizon/dashboards/nova/templates/nova/images/index.html new file mode 100644 index 00000000..78575e90 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/images/index.html @@ -0,0 +1,25 @@ +{% extends 'nova/base.html' %} +{%load i18n%} +{% block sidebar %} + {% with current_sidebar="images" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% url horizon:nova:images:index as refresh_link %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Images") refresh_link=refresh_link searchable="true" %} +{% endblock page_header %} + +{% block dash_main %} + {% if images %} + {% include 'nova/images/_list.html' %} + + {% else %} +
+

{% trans "Info"%}

+

{% trans "There are currently no images."%}

+
+ {% endif %} +{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/images/launch.html b/horizon/horizon/dashboards/nova/templates/nova/images/launch.html new file mode 100644 index 00000000..2e071200 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/images/launch.html @@ -0,0 +1,52 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="images" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Launch Instance") %} +{% endblock page_header %} + +{% block dash_main %} +
+
+ {% include 'nova/images/_launch.html' %} +
+
+

{% trans "Description:"%}

+

{% trans "Specify the details for launching an instance. Also please make note of the table below; all tenants have quotas which define the limit of resources you are allowed to provision."%}

+ + + + + + + + + + + + + + + + + + + + + + + + + +
{% trans "Quota Name"%}{% trans "Limit"%}
{% trans "RAM (MB)"%}{{quotas.ram}}MB
{% trans "Floating IPs"%}{{quotas.floating_ips}}
{% trans "Instances"%}{{quotas.instances}}
{% trans "Volumes"%}{{quotas.volumes}}
{% trans "Gigabytes"%}{{quotas.gigabytes}}GB
+
+
 
+
+{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/images/update.html b/horizon/horizon/dashboards/nova/templates/nova/images/update.html new file mode 100644 index 00000000..d20491aa --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/images/update.html @@ -0,0 +1,26 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="images" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Update Image") %} +{% endblock page_header %} + +{% block dash_main %} +
+
+ {% include 'nova/images/_form.html' %} +
+ +
+

{% trans "Description:"%}

+

{% trans "From here you can modify different properties of an image."%}

+
+
 
+
+{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/instances/_form.html b/horizon/horizon/dashboards/nova/templates/nova/instances/_form.html new file mode 100644 index 00000000..5e267eff --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/instances/_form.html @@ -0,0 +1,11 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/instances/_list.html b/horizon/horizon/dashboards/nova/templates/nova/instances/_list.html new file mode 100644 index 00000000..52e5180b --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/instances/_list.html @@ -0,0 +1,75 @@ + +{% load sizeformat %} +{%load i18n%} + + + + + + + + + + + + + {% for instance in instances %} + + + + + + + + + + + {% endfor %} + +
{% trans "ID"%}{% trans "Name"%}{% trans "Groups"%}{% trans "Image"%}{% trans "Size"%}{% trans "IPs"%}{% trans "State"%}{% trans "Actions"%}
{{instance.id}} + + {{instance.name}} + {% if instance.attrs.key_name %} +
+ ({{instance.attrs.key_name}}) + {% endif %} +
+
+
    + {% for group in instance.attrs.security_groups %} +
  • {{group}}
  • + {% endfor %} +
      +
{{instance.image_name}} +
    +
  • {{instance.attrs.memory_mb|mbformat}} Ram
  • +
  • {{instance.attrs.vcpus}} VCPU
  • +
  • {{instance.attrs.disk_gb}}GB Disk
  • +
+
+ {% for ip_group, addresses in instance.addresses.items %} + {% if instance.addresses.items|length > 1 %} +

{{ip_group}}

+
    + {% for address in addresses %} +
  • {{address.addr}}
  • + {% endfor %} +
+ {% else %} +
    + {% for address in addresses %} +
  • {{address.addr}}
  • + {% endfor %} +
+ {% endif %} + {% endfor %} +
{{instance.status|lower|capfirst}} + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/instances/detail.html b/horizon/horizon/dashboards/nova/templates/nova/instances/detail.html new file mode 100644 index 00000000..0e74a7ba --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/instances/detail.html @@ -0,0 +1,124 @@ +{% extends 'nova/base.html' %} +{% load i18n %} + +{% block sidebar %} + {% with current_sidebar="instances" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title="Instance Detail: "|add:instance.name %} +{% endblock page_header %} + +{% block dash_main %} + + +
+
+
    +
  • +
    +

    {% trans "Status" %}

    +
      +
    • {% trans "Status:" %} {{instance.status}}
    • +
    • {% trans "Instance Name:" %} {{instance.name}}
    • +
    • {% trans "Instance ID:" %} {{instance.id}}
    • +
    +
    +
  • + +
  • +
    +

    {% trans "Specs" %}

    +
      +
    • {% trans "RAM:" %} {{instance.attrs.memory_mb}}
    • +
    • {% trans "VCPUs:" %} {{instance.attrs.vcpus}} {% trans "VCPU" %}
    • +
    • {% trans "Disk:" %} {{instance.attrs.disk_gb}}{% trans "GB Disk" %}
    • +
    +
    +
  • + +
  • +
    +

    {% trans "Meta" %}

    +
      +
    • {% trans "Key name:" %} {{instance.attrs.key_name}}
    • +
    • {% trans "Security Group(s):" %} {% for group in instance.attrs.security_groups %}{{group}}, {% endfor %}
    • +
    • {% trans "Image Name:" %} {{instance.image_name}}
    • +
    +
    +
  • +
  • +
    +

    {% trans "Volumes" %}

    + +
    +
  • +
+
+ + + +
+ +
+ +
+{% endblock %} + +{% block footer_js %} + +{% endblock footer_js %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/instances/index.html b/horizon/horizon/dashboards/nova/templates/nova/instances/index.html new file mode 100644 index 00000000..9d49302f --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/instances/index.html @@ -0,0 +1,66 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="instances" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% url horizon:nova:instances:index as refresh_link %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Instances") refresh_link=refresh_link searchable="true" %} +{% endblock page_header %} + +{% block dash_main %} + {% if instances %} + {% include 'nova/instances/_list.html' %} + {% else %} +
+ {% url horizon:nova:images:index as dash_img_url %} +

{% trans "Info"%}

+

{% blocktrans %}There are currently no instances. You can launch an instance from the Images Page.{% endblocktrans %}

+
+ {% endif %} +{% endblock %} + +{% block footer_js %} + +{% endblock footer_js %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/instances/update.html b/horizon/horizon/dashboards/nova/templates/nova/instances/update.html new file mode 100644 index 00000000..85a62b64 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/instances/update.html @@ -0,0 +1,50 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="instances" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Update Instance") %} +{% endblock page_header %} + +{% block dash_main %} +
+
+ {% include 'nova/instances/_form.html' with form=form %} +

<< {% trans "Return to Instances List"%}

+
+ +
+

{% trans "Description:"%}

+

{% trans "Update the name and description of your instance"%}

+
+
+
+{% endblock %} + +{% block footer_js %} + +{% endblock footer_js %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/instances/usage.csv b/horizon/horizon/dashboards/nova/templates/nova/instances/usage.csv new file mode 100644 index 00000000..d618b637 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/instances/usage.csv @@ -0,0 +1,11 @@ +Usage Report For Period:,{{datetime_start|date:"b. d Y H:i"}},/,{{datetime_end|date:"b. d Y H:i"}} +Tenant ID:,{{usage.tenant_id}} +Total Active VCPUs:,{{usage.total_active_vcpus}} +CPU-HRs Used:,{{usage.total_cpu_usage}} +Total Active Ram (MB):,{{usage.total_active_ram_size}} +Total Disk Size:,{{usage.total_active_disk_size}} +Total Disk Usage:,{{usage.total_disk_usage}} + +ID,Name,UserId,VCPUs,RamMB,DiskGB,Flavor,Usage(Hours),Uptime(Seconds),State +{% for instance in usage.instances %}{{instance.id}},{{instance.name|addslashes}},{{instance.user_id|addslashes}},{{instance.vcpus|addslashes}},{{instance.ram_size|addslashes}},{{instance.disk_size|addslashes}},{{instance.flavor|addslashes}},{{instance.hours}},{{instance.uptime}},{{instance.state|capfirst|addslashes}} +{% endfor %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/instances/usage.html b/horizon/horizon/dashboards/nova/templates/nova/instances/usage.html new file mode 100644 index 00000000..abc97cbb --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/instances/usage.html @@ -0,0 +1,102 @@ +{% extends 'nova/base.html' %} +{% load parse_date %} +{% load sizeformat %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="overview" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Overview") %} +{% endblock page_header %} + +{% block dash_main %} +
+ + {% if usage.instances %} +
+

CPU

+
    +
  • {{usage.total_active_vcpus|default:0}}Cores Active
  • +
  • {{usage.total_cpu_usage|floatformat|default:0}}CPU-HR Used
  • +
+
+ +
+

RAM

+
    +
  • {{total_ram|default:0}}{{ram_unit}} Active
  • +
+
+ +
+

Disk

+
    +
  • {{usage.total_active_disk_size|default:0}}GB Active
  • +
  • {{usage.total_disk_usage|floatformat|default:0}}GB-HR Used
  • +
+
+
+ +
+ {% trans "Download CSV"%} » +

Server Usage Summary + + {% if show_terminated %} + ( {% trans "Hide Terminated"%} ) + {% else %} + ( {% trans "Show Terminated"%} ) + {% endif %} + +

+
+ + + + + + + + + + + + + + + {% for instance in instances %} + {% if instance.ended_at %} + + {% else %} + + {% endif %} + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "ID"%}{% trans "Name"%}{% trans "User"%}{% trans "VCPUs"%}{% trans "Ram Size"%}{% trans "Disk Size"%}{% trans "Flavor"%}{% trans "Uptime"%}{% trans "Status"%}
{{instance.id}}{{instance.name}}{{instance.user_id}}{{instance.vcpus}}{{instance.ram_size|mbformat}}{{instance.disk_size}}GB{{instance.flavor}}{{instance.uptime_at|timesince}}{{instance.state|lower|capfirst}}
{% trans "No active instances."%}
+ {% else %} +
+ {% url horizon:nova:images:index as dash_img_url%} +

{% trans "Info"%}

+

{% blocktrans %}There are currently no instances.

You can launch an instance from the Images Page.{% endblocktrans %}

+
+ {% endif %} + +{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/keypairs/_delete.html b/horizon/horizon/dashboards/nova/templates/nova/keypairs/_delete.html new file mode 100644 index 00000000..5e821bdc --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/keypairs/_delete.html @@ -0,0 +1,9 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/keypairs/_form.html b/horizon/horizon/dashboards/nova/templates/nova/keypairs/_form.html new file mode 100644 index 00000000..6e32d507 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/keypairs/_form.html @@ -0,0 +1,11 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/keypairs/_list.html b/horizon/horizon/dashboards/nova/templates/nova/keypairs/_list.html new file mode 100644 index 00000000..1b306949 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/keypairs/_list.html @@ -0,0 +1,19 @@ +{%load i18n%} + + + + + + + {% for keypair in keypairs %} + + + + + + {% endfor %} +
{% trans "Name"%}{% trans "Fingerprint"%}{% trans "Actions"%}
{{ keypair.name }}{{ keypair.fingerprint }} +
    +
  • {% include "nova/keypairs/_delete.html" with form=delete_form %}
  • +
+
diff --git a/horizon/horizon/dashboards/nova/templates/nova/keypairs/create.html b/horizon/horizon/dashboards/nova/templates/nova/keypairs/create.html new file mode 100644 index 00000000..ea64d36f --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/keypairs/create.html @@ -0,0 +1,43 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="keypairs" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block headerjs %} + +{% endblock %} + +{% block page_header %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Create Keypair") %} +{% endblock page_header %} + +{% block dash_main %} +
+
+

{% trans "Your private key is being downloaded."%}

+ {% include 'nova/keypairs/_form.html' with form=create_form %} +

<< {% trans "Return to keypairs list"%}

+
+ +
+

{% trans "Description"%}:

+

{% trans "Keypairs are ssh credentials which are injected into images when they are launched. Creating a new key pair registers the public key and downloads the private key (a .pem file)."%}

+

{% trans "Protect and use the key as you would any normal ssh private key."%}

+
+
 
+
+{% endblock %} + diff --git a/horizon/horizon/dashboards/nova/templates/nova/keypairs/import.html b/horizon/horizon/dashboards/nova/templates/nova/keypairs/import.html new file mode 100644 index 00000000..0df9e20b --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/keypairs/import.html @@ -0,0 +1,33 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="keypairs" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block headerjs %} +{% endblock %} + +{% block page_header %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Create Keypair") %} +{% endblock page_header %} + +{% block dash_main %} +
+
+ {% include 'nova/keypairs/_form.html' with form=create_form %} +

<< {% trans "Return to keypairs list"%}

+
+ +
+

{% trans "Description"%}:

+

{% trans "Keypairs are ssh credentials which are injected into images when they are launched. Creating a new key pair registers the public key and downloads the private key (a .pem file)."%}

+

{% trans "Protect and use the key as you would any normal ssh private key."%}

+
+
 
+
+{% endblock %} + diff --git a/horizon/horizon/dashboards/nova/templates/nova/keypairs/index.html b/horizon/horizon/dashboards/nova/templates/nova/keypairs/index.html new file mode 100644 index 00000000..8ad9fd70 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/keypairs/index.html @@ -0,0 +1,29 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="keypairs" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% url horizon:nova:keypairs:index as refresh_link %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Keypairs") refresh_link=refresh_link searchable="true" %} +{% endblock page_header %} + +{% block dash_main %} + {% if keypairs %} + {% include 'nova/keypairs/_list.html' %} + {% trans "Add New Keypair"%} + {% trans "Import Keypair"%} + {% else %} +
+

{% trans "Info"%}

+

{% trans "There are currently no keypairs."%}

+
+ {% trans "Add New Keypair"%} + {% trans "Import Keypair"%} + {% endif %} +{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/networks/_delete.html b/horizon/horizon/dashboards/nova/templates/nova/networks/_delete.html new file mode 100644 index 00000000..d009dbb2 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/networks/_delete.html @@ -0,0 +1,9 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/networks/_delete_port.html b/horizon/horizon/dashboards/nova/templates/nova/networks/_delete_port.html new file mode 100644 index 00000000..c9e69d09 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/networks/_delete_port.html @@ -0,0 +1,10 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/networks/_detach_port.html b/horizon/horizon/dashboards/nova/templates/nova/networks/_detach_port.html new file mode 100644 index 00000000..f1597e52 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/networks/_detach_port.html @@ -0,0 +1,10 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/networks/_detail.html b/horizon/horizon/dashboards/nova/templates/nova/networks/_detail.html new file mode 100644 index 00000000..b1bb51f3 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/networks/_detail.html @@ -0,0 +1,49 @@ +{%load i18n%} + + + + + + + + + + {% for port in network.ports %} + + + + + + + + {% endfor %} + +
{% trans "ID"%}{% trans "State"%}{% trans "Attachment"%}{% trans "Actions"%}{% trans "Extensions"%}
{{port.id}}{{port.state}} + {% if port.attachment %} + + + + + + + + + +
{% trans "Instance"%}{% trans "VIF Id"%}
{{port.instance}} {{port.attachment.id}}
+ {% else %} + -- + {% endif %} +
+
    + {% if port.attachment %} +
  • {% include "nova/networks/_detach_port.html" with form=detach_port_form %}
  • + {% else %} +
  • {% trans "Attach"%}
  • + {% endif %} +
  • {% include "nova/networks/_delete_port.html" with form=delete_port_form %}
  • +
  • {% include "nova/networks/_toggle_port.html" with form=toggle_port_form %}
  • +
+
+
    +
+
diff --git a/horizon/horizon/dashboards/nova/templates/nova/networks/_form.html b/horizon/horizon/dashboards/nova/templates/nova/networks/_form.html new file mode 100644 index 00000000..71e3e760 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/networks/_form.html @@ -0,0 +1,11 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/networks/_list.html b/horizon/horizon/dashboards/nova/templates/nova/networks/_list.html new file mode 100644 index 00000000..2e1ee13e --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/networks/_list.html @@ -0,0 +1,28 @@ +{%load i18n%} + + + + + + + + + + + {% for network in networks %} + + + + + + + + + {% endfor %} + +
{% trans "ID"%}{% trans "Name"%}{% trans "Ports"%}{% trans "Available"%}{% trans "Used"%}{% trans "Action"%}
{{network.id}}{{network.name}}{{network.total}}{{network.available}}{{network.used}} + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/networks/_rename.html b/horizon/horizon/dashboards/nova/templates/nova/networks/_rename.html new file mode 100644 index 00000000..7a915070 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/networks/_rename.html @@ -0,0 +1,18 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + + + + + +
+

+ +
+ +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/networks/_rename_form.html b/horizon/horizon/dashboards/nova/templates/nova/networks/_rename_form.html new file mode 100644 index 00000000..cc3368ed --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/networks/_rename_form.html @@ -0,0 +1,12 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/networks/_toggle_port.html b/horizon/horizon/dashboards/nova/templates/nova/networks/_toggle_port.html new file mode 100644 index 00000000..0be11240 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/networks/_toggle_port.html @@ -0,0 +1,16 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + + {% if port.state == 'DOWN' %} + + + {% else %} + + + {% endif %} +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/networks/create.html b/horizon/horizon/dashboards/nova/templates/nova/networks/create.html new file mode 100644 index 00000000..6ed8d51f --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/networks/create.html @@ -0,0 +1,29 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="networks" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Create Network") %} +{% endblock page_header %} + +{% block dash_main %} +
+
+ {% include 'nova/networks/_form.html' with form=network_form %} +

<< {% trans "Return to networks list"%}

+
+ +
+

{% trans "Description"%}:

+

{% trans "Networks provide layer 2 connectivity to your instances."%}

+
+
 
+
+{% endblock %} + diff --git a/horizon/horizon/dashboards/nova/templates/nova/networks/detail.html b/horizon/horizon/dashboards/nova/templates/nova/networks/detail.html new file mode 100644 index 00000000..09a3b011 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/networks/detail.html @@ -0,0 +1,31 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="networks" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% url horizon:nova:networks:detail network.id as refresh_link %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=network.name refresh_link=refresh_link searchable="true" %} +{% endblock page_header %} + +{% block breadcrumbs %} + Networks »  + {{network.name}} +{% endblock %} + +{% block dash_main %} + {% if network.ports %} + {% include 'nova/networks/_detail.html' %} + {% trans "Create Ports"%} + {% else %} +
+

{% trans "Info"%}

+

{% trans "There are currently no ports in this network."%} {% trans "Create Ports"%} >>

+
+ {% endif %} +{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/networks/index.html b/horizon/horizon/dashboards/nova/templates/nova/networks/index.html new file mode 100644 index 00000000..aac5d671 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/networks/index.html @@ -0,0 +1,28 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="networks" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% url horizon:nova:networks:index as refresh_link %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Networks") refresh_link=refresh_link searchable="true" %} +{% endblock page_header %} + +{% block dash_main %} + {% if networks %} + {% include 'nova/networks/_list.html' %} + {% url horizon:nova:networks:create as dash_net_url %} + {% trans "Create New Network"%} + {% else %} +
+

{% trans "Info"%}

+

{% trans "There are currently no networks."%} {% trans "Create A Network"%} >>

+
+ {% endif %} +
+{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/networks/rename.html b/horizon/horizon/dashboards/nova/templates/nova/networks/rename.html new file mode 100644 index 00000000..2ec732b3 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/networks/rename.html @@ -0,0 +1,37 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="networks" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Rename Network") %} +{% endblock page_header %} + +{% block headerjs %} + +{% endblock headerjs %} + +{% block dash_main %} +
+
+ {% include 'nova/networks/_rename_form.html' with form=rename_form %} +

<< {% trans "Return to networks list"%}

+
+ +
+

{% trans "Rename"%}:

+

{% trans "Enter a new name for your network."%}

+
+
 
+
+{% endblock %} + diff --git a/horizon/horizon/dashboards/nova/templates/nova/objects/_copy.html b/horizon/horizon/dashboards/nova/templates/nova/objects/_copy.html new file mode 100644 index 00000000..4544bce9 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/objects/_copy.html @@ -0,0 +1,11 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/objects/_delete.html b/horizon/horizon/dashboards/nova/templates/nova/objects/_delete.html new file mode 100644 index 00000000..4a0af7bf --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/objects/_delete.html @@ -0,0 +1,9 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/objects/_filter.html b/horizon/horizon/dashboards/nova/templates/nova/objects/_filter.html new file mode 100644 index 00000000..542439ed --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/objects/_filter.html @@ -0,0 +1,8 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{hidden}}{% endfor %} + {% for field in form.visible_fields %}{{field}}{% endfor %} + + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/objects/_form.html b/horizon/horizon/dashboards/nova/templates/nova/objects/_form.html new file mode 100644 index 00000000..0a6de49f --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/objects/_form.html @@ -0,0 +1,11 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/objects/_list.html b/horizon/horizon/dashboards/nova/templates/nova/objects/_list.html new file mode 100644 index 00000000..e38ae39c --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/objects/_list.html @@ -0,0 +1,29 @@ +{% load i18n swift_paging %} + + + + + + + + + + {% for object in objects %} + + + + + {% endfor %} + + + + + + +
{% trans "Name"%}{% trans "Actions"%}
{{ object.name }} + +
{% object_paging objects %}
diff --git a/horizon/horizon/dashboards/nova/templates/nova/objects/_paging.html b/horizon/horizon/dashboards/nova/templates/nova/objects/_paging.html new file mode 100644 index 00000000..99299bdd --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/objects/_paging.html @@ -0,0 +1 @@ +{% if marker %}More{% endif %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/objects/copy.html b/horizon/horizon/dashboards/nova/templates/nova/objects/copy.html new file mode 100644 index 00000000..fb23f270 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/objects/copy.html @@ -0,0 +1,33 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="containers" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Copy Object") %} +{% endblock page_header %} + +{% block dash_main %} +

Container: {{container_name}}

+ +
+
+

Copy Object: '{{object_name}}'

+ {% include 'nova/objects/_copy.html' with form=copy_form greeting="HI" %} +

<< {% trans "Return to objects list"%}

+
+ +
+

{% trans "Description"%}:

+

{% trans "You may make a new copy of an existing object to store in this or another container."%}

+
+
 
+
+ +{% endblock %} + + diff --git a/horizon/horizon/dashboards/nova/templates/nova/objects/index.html b/horizon/horizon/dashboards/nova/templates/nova/objects/index.html new file mode 100644 index 00000000..1ddb2aa8 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/objects/index.html @@ -0,0 +1,36 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="containers" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + +{% endblock %} + +{% block dash_main %} +

Container: {{ container_name }}

+ + {% if objects %} + {% include 'nova/objects/_list.html' %} + {% else %} +
+ {% url horizon:nova:containers:object_upload container_name as dash_obj_up_url %} +

Info

+

{% blocktrans %}There are currently no objects in the container {{container_name}}. You can upload a new object from the Object Upload Page >>{% endblocktrans %}

+
+ {% endif %} + {% trans "Upload New Object >>"%} + +{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/objects/upload.html b/horizon/horizon/dashboards/nova/templates/nova/objects/upload.html new file mode 100644 index 00000000..7dfa3eb7 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/objects/upload.html @@ -0,0 +1,31 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="containers" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Upload Objects") %} +{% endblock page_header %} + +{% block dash_main %} +

Container: {{ container_name }}

+ +
+
+ {% include 'nova/objects/_form.html' with form=upload_form %} +

<< {% trans "Return to objects list"%}

+
+ +
+

{% trans "Description"%}:

+

{% trans "An object is the basic storage entity and any optional metadata that represents the files you store in the OpenStack Object Storage system. When you upload data to OpenStack Object Storage, the data is stored as-is (no compression or encryption) and consists of a location (container), the object's name, and any metadata consisting of key/value pairs."%}

+
+
 
+
+ +{% endblock %} + diff --git a/horizon/horizon/dashboards/nova/templates/nova/ports/_attach.html b/horizon/horizon/dashboards/nova/templates/nova/ports/_attach.html new file mode 100644 index 00000000..5ff6f73a --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/ports/_attach.html @@ -0,0 +1,12 @@ +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{hidden}}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + + + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/ports/_create.html b/horizon/horizon/dashboards/nova/templates/nova/ports/_create.html new file mode 100644 index 00000000..c8f3fc3d --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/ports/_create.html @@ -0,0 +1,11 @@ +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/ports/attach.html b/horizon/horizon/dashboards/nova/templates/nova/ports/attach.html new file mode 100644 index 00000000..e0787111 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/ports/attach.html @@ -0,0 +1,48 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="networks" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Attach Port") %} +{% endblock page_header %} + +{% block headerjs %} + +{% endblock headerjs %} + +{% block dash_main %} +
+
+ {% include 'nova/ports/_attach.html' with form=attach_form %} +

<< {% trans "Return to network detail"%}

+
+ +
+ {% blocktrans %}

Select an interface from the list on the left to attach it to this port.

+

Only interfaces that are not connected to any existing port are shown

+

If you want to reconnect a connected interface, please detach it first

{% endblocktrans %} +
+
 
+
+{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/ports/create.html b/horizon/horizon/dashboards/nova/templates/nova/ports/create.html new file mode 100644 index 00000000..629c49a1 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/ports/create.html @@ -0,0 +1,29 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="networks" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Create Network") %} +{% endblock page_header %} + +{% block dash_main %} +
+
+ {% include 'nova/ports/_create.html' with form=create_form %} +

<< {% trans "Return to network detail"%}

+
+ +
+

{% trans "Description"%}:

+

{% trans "You can plug virtual interfaces from your instances to ports created in the network"%}

+
+
 
+
+{% endblock %} + diff --git a/horizon/horizon/dashboards/nova/templates/nova/security_groups/_delete.html b/horizon/horizon/dashboards/nova/templates/nova/security_groups/_delete.html new file mode 100644 index 00000000..2059d502 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/security_groups/_delete.html @@ -0,0 +1,9 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/security_groups/_delete_rule.html b/horizon/horizon/dashboards/nova/templates/nova/security_groups/_delete_rule.html new file mode 100644 index 00000000..cba1f9c4 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/security_groups/_delete_rule.html @@ -0,0 +1,9 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/security_groups/_form.html b/horizon/horizon/dashboards/nova/templates/nova/security_groups/_form.html new file mode 100644 index 00000000..0a8f7e40 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/security_groups/_form.html @@ -0,0 +1,13 @@ +{%load i18n%} +
+
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + +
+
diff --git a/horizon/horizon/dashboards/nova/templates/nova/security_groups/_list.html b/horizon/horizon/dashboards/nova/templates/nova/security_groups/_list.html new file mode 100644 index 00000000..7b324f13 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/security_groups/_list.html @@ -0,0 +1,22 @@ +{%load i18n%} + + + + + + + {% for security_group in security_groups %} + + + + + + {% endfor %} +
{% trans "Name"%}{% trans "Description"%}{% trans "Actions"%}
{{ security_group.name }}{{ security_group.description }} +
    +
  • {% trans "Edit Rules"%}
  • + {% if security_group.name != 'default' %} +
  • {% include "nova/security_groups/_delete.html" with form=delete_form %}
  • + {% endif %} +
+
diff --git a/horizon/horizon/dashboards/nova/templates/nova/security_groups/create.html b/horizon/horizon/dashboards/nova/templates/nova/security_groups/create.html new file mode 100644 index 00000000..9c0bc53c --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/security_groups/create.html @@ -0,0 +1,26 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="security_groups" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create Security Group") %} +{% endblock page_header %} + +{% block dash_main %} +
+
+ {% include 'nova/security_groups/_form.html' %} +
+ +
+

{% trans "Description"%}:

+

{% trans "From here you can create a new security group"%}

+
+
 
+
+{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/security_groups/edit_rules.html b/horizon/horizon/dashboards/nova/templates/nova/security_groups/edit_rules.html new file mode 100644 index 00000000..f2179103 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/security_groups/edit_rules.html @@ -0,0 +1,67 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="security_groups" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Edit Security Group Rules") %} +{% endblock page_header %} + +{% block dash_main %} +
+
+

{% trans "Rules for Security Group"%} '{{security_group.name}}'

+ + + + + + + + + {% for rule in security_group.rules %} + + + + + + + + {% empty %} + + + + {% endfor %} +
{% trans "IP Protocol"%}{% trans "From Port"%}{% trans "To Port"%}{% trans "CIDR"%}{% trans "Actions"%}
{{ rule.ip_protocol }}{{ rule.from_port }}{{ rule.to_port }}{{rule.ip_range.cidr}} +
    +
  • {% include "nova/security_groups/_delete_rule.html" with form=delete_form %}
  • +
+
+ {% trans "No rules for this security group"%} +
+
+
+
+

{% trans "Add a rule"%}

+
+
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + + +
+
+
+
+
+
+{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/security_groups/index.html b/horizon/horizon/dashboards/nova/templates/nova/security_groups/index.html new file mode 100644 index 00000000..b266ebb7 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/security_groups/index.html @@ -0,0 +1,28 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="security_groups" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% url horizon:nova:security_groups:index as refresh_link %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Security Groups") refresh_link=refresh_link searchable="true" %} +{% endblock page_header %} + +{% block dash_main %} + {% if security_groups %} + {% include 'nova/security_groups/_list.html' %} + {% url horizon:nova:security_groups:create as create_sec_url %} + {% trans "Create Security Group"%} + {% else %} +
+ {% url horizon:nova:security_groups:create as dash_sec_url %} +

{% trans "Info"%}

+

{% blocktrans %}There are currently no security groups. Create A Security Group >>{% endblocktrans %}

+
+ {% endif %} +{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/settings.html b/horizon/horizon/dashboards/nova/templates/nova/settings.html new file mode 100644 index 00000000..e7708774 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/settings.html @@ -0,0 +1,44 @@ +{% extends 'base.html' %} +{%load i18n%} + +{% block topbar %} + {% with current_topbar="settings" %} + {{block.super}} + {% endwith %} +{% endblock %} + + +{% block sidebar %} + {% include 'nova/_sidebar.html' %} +{% endblock %} + + +{% block main %} + {% block page_header %} + {% url horizon:nova:instances:index as refresh_link %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Dashboard Settings") %} + {% endblock page_header %} + {% include "_messages.html" %} +
+
+
+

{% trans "Dashboard User Interface Language"%}

+
{% csrf_token %} +

+ +
+
+
 
+
+
+{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/snapshots/_form.html b/horizon/horizon/dashboards/nova/templates/nova/snapshots/_form.html new file mode 100644 index 00000000..fea72672 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/snapshots/_form.html @@ -0,0 +1,14 @@ +{%load i18n%} +
+
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + +
+
+ diff --git a/horizon/horizon/dashboards/nova/templates/nova/snapshots/create.html b/horizon/horizon/dashboards/nova/templates/nova/snapshots/create.html new file mode 100644 index 00000000..f9fcabf1 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/snapshots/create.html @@ -0,0 +1,38 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="snapshots" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block headerjs %} + +{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create a Snapshot") %} +{% endblock page_header %} + +{% block dash_main %} +
+
+

{% trans "Choose a name for your snapshot."%}

+ {% include 'nova/snapshots/_form.html' with form=create_form %} +

<< {% trans "Return to snapshots list"%}

+
+ +
+

{% trans "Description"%}:

+

{% trans "Snapshots preserve the disk state of a running instance."%}

+
+
 
+
+{% endblock %} + + diff --git a/horizon/horizon/dashboards/nova/templates/nova/snapshots/index.html b/horizon/horizon/dashboards/nova/templates/nova/snapshots/index.html new file mode 100644 index 00000000..b4e84123 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/snapshots/index.html @@ -0,0 +1,27 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="snapshots" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% url horizon:nova:snapshots:index as refresh_link %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Snapshots") refresh_link=refresh_link searchable="true" %} +{% endblock page_header %} + +{% block dash_main %} + {% if images %} + {% include 'nova/images/_list.html' %} + {% else %} +
+ {% url horizon:nova:instances:index as inst_url %} +

{% trans "Info"%}

+

{% blocktrans %}There are currently no snapshots. You can create snapshots from running instances. View Running Instances >>{% endblocktrans %}

+
+ {% endif %} +{% endblock %} + diff --git a/horizon/horizon/dashboards/nova/templates/nova/volumes/_attach_form.html b/horizon/horizon/dashboards/nova/templates/nova/volumes/_attach_form.html new file mode 100644 index 00000000..6ad7d603 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/volumes/_attach_form.html @@ -0,0 +1,14 @@ +{% load i18n %} +
+
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + + +
+
diff --git a/horizon/horizon/dashboards/nova/templates/nova/volumes/_delete.html b/horizon/horizon/dashboards/nova/templates/nova/volumes/_delete.html new file mode 100644 index 00000000..77310986 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/volumes/_delete.html @@ -0,0 +1,10 @@ +{% load i18n %} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{ hidden }} + {% endfor %} + + + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/volumes/_detach_form.html b/horizon/horizon/dashboards/nova/templates/nova/volumes/_detach_form.html new file mode 100644 index 00000000..0f383a87 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/volumes/_detach_form.html @@ -0,0 +1,11 @@ +{% load i18n %} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{ hidden }} + {% endfor %} + + + + +
diff --git a/horizon/horizon/dashboards/nova/templates/nova/volumes/_form.html b/horizon/horizon/dashboards/nova/templates/nova/volumes/_form.html new file mode 100644 index 00000000..758fb185 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/volumes/_form.html @@ -0,0 +1,13 @@ +{% load i18n %} +
+
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + +
+
diff --git a/horizon/horizon/dashboards/nova/templates/nova/volumes/_list.html b/horizon/horizon/dashboards/nova/templates/nova/volumes/_list.html new file mode 100644 index 00000000..15935231 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/volumes/_list.html @@ -0,0 +1,58 @@ +{% load i18n %} +{% load parse_date %} + + + + + + + + + + + {% for volume in volumes %} + + + + + + + + + + {% endfor %} +
{% trans "ID" %}{% trans "Name" %}{% trans "Status" %}{% trans "Size" %}{% trans "Created" %}{% trans "Attached To" %}{% trans "Actions" %}
{{ volume.id }} + + {{ volume.displayName }} + + {{ volume.status|capfirst }}{{ volume.size }} {% trans "GB" %}{{ volume.createdAt|parse_date }} + {% for attachment in volume.attachments %} + {% if attachment %} + + + Instance {{ attachment.serverId }} + ({{ attachment.device }}) + + {% else %} + {% trans "Not Attached" %} + {% endif %} + {% endfor %} + +
    + {% if volume.status == "in-use" %} + {% for attachment in volume.attachments %} +
  • + {% include "nova/volumes/_detach_form.html" with form=detach_form %} +
  • + {% endfor %} + {% endif %} + {% if volume.status == "available" %} +
  • + {% trans "Attach" %} +
  • +
  • + {% include "nova/volumes/_delete.html" with form=delete_form %} +
  • + {% endif %} +
+
diff --git a/horizon/horizon/dashboards/nova/templates/nova/volumes/attach.html b/horizon/horizon/dashboards/nova/templates/nova/volumes/attach.html new file mode 100644 index 00000000..c762b52d --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/volumes/attach.html @@ -0,0 +1,27 @@ +{% extends 'nova/base.html' %} +{% load i18n %} + +{% block sidebar %} + {% with current_sidebar="volumes" %} + {{ block.super }} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Attach a Volume") %} +{% endblock page_header %} + +{% block dash_main %} +
+
+ {% include 'nova/volumes/_attach_form.html' with form=attach_form %} +

<< {% trans "Return to volumes list" %}

+
+ +
+

{% trans "Description" %}:

+

{% trans "Attach a volume to an instance." %}

+
+
 
+
+{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/volumes/create.html b/horizon/horizon/dashboards/nova/templates/nova/volumes/create.html new file mode 100644 index 00000000..4297d5d5 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/volumes/create.html @@ -0,0 +1,35 @@ +{% extends 'nova/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="volumes" %} + {{ block.super }} + {% endwith %} +{% endblock %} + +{% block headerjs %} + +{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create a Volume") %} +{% endblock page_header %} + +{% block dash_main %} +
+
+ {% include 'nova/volumes/_form.html' with form=create_form %} +

<< {% trans "Return to volumes list"%}

+
+ +
+

{% trans "Description" %}:

+

{% trans "Volumes are block devices that can be attached to instances." %}

+
+
 
+
+{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/volumes/detail.html b/horizon/horizon/dashboards/nova/templates/nova/volumes/detail.html new file mode 100644 index 00000000..55541636 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/volumes/detail.html @@ -0,0 +1,52 @@ +{% extends 'nova/base.html' %} +{% load i18n %} +{% load parse_date %} + +{% block sidebar %} + {% with current_sidebar="volumes" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title="Volume Detail: "|add:volume.displayName %} +{% endblock page_header %} + +{% block dash_main %} + + +
+
+
    +
  • +
    +

    {% trans "Details" %}

    +
      +
    • {% trans "ID:" %} {{ volume.id }}
    • +
    • {% trans "Name:" %} {{ volume.displayName }}
    • +
    • {% trans "Size:" %} {{ volume.size }} {% trans "GB" %}
    • +
    • {% trans "Description:" %} {{ volume.displayDescription }}
    • +
    • {% trans "Status:" %} {{ volume.status|capfirst }}
    • +
    • {% trans "Created:" %} {{ volume.createdAt|parse_date }}
    • +
    • + {% trans "Attached To:" %} + {% if instance %} + + {% trans "Instance" %} {{ instance.id }} + ({{ instance.name }}) + + {% trans "on" %} {{ attachment.device }} + {% else %} + {% trans "Not Attached" %} + {% endif %} +
    • +
    +
    +
  • +
+
+
+{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/volumes/index.html b/horizon/horizon/dashboards/nova/templates/nova/volumes/index.html new file mode 100644 index 00000000..7029e51f --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/volumes/index.html @@ -0,0 +1,26 @@ +{% extends 'nova/base.html' %} +{% load i18n %} + +{% block sidebar %} + {% with current_sidebar="volumes" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% url horizon:nova:volumes:index as refresh_link %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Volumes") refresh_link=refresh_link searchable="true" %} +{% endblock page_header %} + +{% block dash_main %} + {% if volumes %} + {% include 'nova/volumes/_list.html' %} + {% else %} +
+

{% trans "Info"%}

+

{% blocktrans %}There are currently no volumes.{% endblocktrans %}

+
+ {% endif %} + {% trans "Create New Volume" %} +{% endblock %} diff --git a/horizon/horizon/dashboards/nova/volumes/__init__.py b/horizon/horizon/dashboards/nova/volumes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/nova/volumes/forms.py b/horizon/horizon/dashboards/nova/volumes/forms.py new file mode 100644 index 00000000..b67d2d92 --- /dev/null +++ b/horizon/horizon/dashboards/nova/volumes/forms.py @@ -0,0 +1,106 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Nebula, Inc. +# All rights reserved. + +""" +Views for managing Nova volumes. +""" + +import logging + +from django import shortcuts +from django.contrib import messages +from django.utils.translation import ugettext as _ + +from horizon import api +from horizon import forms +from novaclient import exceptions as novaclient_exceptions + + +LOG = logging.getLogger(__name__) + + +class CreateForm(forms.SelfHandlingForm): + name = forms.CharField(max_length="255", label="Volume Name") + description = forms.CharField(widget=forms.Textarea, + label=_("Description"), required=False) + size = forms.IntegerField(min_value=1, label="Size (GB)") + + def handle(self, request, data): + try: + api.volume_create(request, data['size'], data['name'], + data['description']) + message = 'Creating volume "%s"' % data['name'] + LOG.info(message) + messages.info(request, message) + except novaclient_exceptions.ClientException, e: + LOG.exception("ClientException in CreateVolume") + messages.error(request, + _('Error Creating Volume: %s') % e.message) + return shortcuts.redirect(request.build_absolute_uri()) + + +class DeleteForm(forms.SelfHandlingForm): + volume_id = forms.CharField(widget=forms.HiddenInput()) + volume_name = forms.CharField(widget=forms.HiddenInput()) + + def handle(self, request, data): + try: + api.volume_delete(request, data['volume_id']) + message = 'Deleting volume "%s"' % data['volume_id'] + LOG.info(message) + messages.info(request, message) + except novaclient_exceptions.ClientException, e: + LOG.exception("ClientException in DeleteVolume") + messages.error(request, + _('Error deleting volume: %s') % e.message) + return shortcuts.redirect(request.build_absolute_uri()) + + +class AttachForm(forms.SelfHandlingForm): + volume_id = forms.CharField(widget=forms.HiddenInput()) + device = forms.CharField(label="Device Name", initial="/dev/vdb") + + def __init__(self, *args, **kwargs): + super(AttachForm, self).__init__(*args, **kwargs) + instance_list = kwargs.get('initial', {}).get('instance_list', []) + self.fields['instance'] = forms.ChoiceField( + choices=instance_list, + label="Attach to Instance", + help_text="Select an instance to attach to.") + + def handle(self, request, data): + try: + api.volume_attach(request, data['volume_id'], data['instance'], + data['device']) + message = (_('Attaching volume %s to instance %s at %s') % + (data['volume_id'], data['instance'], + data['device'])) + LOG.info(message) + messages.info(request, message) + except novaclient_exceptions.ClientException, e: + LOG.exception("ClientException in AttachVolume") + messages.error(request, + _('Error attaching volume: %s') % e.message) + return shortcuts.redirect(request.build_absolute_uri()) + + +class DetachForm(forms.SelfHandlingForm): + volume_id = forms.CharField(widget=forms.HiddenInput()) + instance_id = forms.CharField(widget=forms.HiddenInput()) + attachment_id = forms.CharField(widget=forms.HiddenInput()) + + def handle(self, request, data): + try: + api.volume_detach(request, data['instance_id'], + data['attachment_id']) + message = (_('Detaching volume %s from instance %s') % + (data['volume_id'], data['instance_id'])) + LOG.info(message) + messages.info(request, message) + except novaclient_exceptions.ClientException, e: + LOG.exception("ClientException in DetachVolume") + messages.error(request, + _('Error detaching volume: %s') % e.message) + return shortcuts.redirect(request.build_absolute_uri()) diff --git a/horizon/horizon/dashboards/nova/volumes/panel.py b/horizon/horizon/dashboards/nova/volumes/panel.py new file mode 100644 index 00000000..f5692e7b --- /dev/null +++ b/horizon/horizon/dashboards/nova/volumes/panel.py @@ -0,0 +1,26 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import horizon +from horizon.dashboards.nova import dashboard + + +class Volumes(horizon.Panel): + name = "Volumes" + slug = 'volumes' + + +dashboard.Nova.register(Volumes) diff --git a/horizon/horizon/dashboards/nova/volumes/tests.py b/horizon/horizon/dashboards/nova/volumes/tests.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/nova/volumes/urls.py b/horizon/horizon/dashboards/nova/volumes/urls.py new file mode 100644 index 00000000..1e5ec42d --- /dev/null +++ b/horizon/horizon/dashboards/nova/volumes/urls.py @@ -0,0 +1,25 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls.defaults import patterns, url + + +urlpatterns = patterns('horizon.dashboards.nova.volumes.views', + url(r'^$', 'index', name='index'), + url(r'^create/$', 'create', name='create'), + url(r'^(?P[^/]+)/attach/$', 'attach', name='attach'), + url(r'^(?P[^/]+)/detail/$', 'detail', name='detail'), +) diff --git a/horizon/horizon/dashboards/nova/volumes/views.py b/horizon/horizon/dashboards/nova/volumes/views.py new file mode 100644 index 00000000..72da94d8 --- /dev/null +++ b/horizon/horizon/dashboards/nova/volumes/views.py @@ -0,0 +1,110 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Views for managing Nova volumes. +""" + +import logging + +from django import shortcuts +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.utils.translation import ugettext as _ +from novaclient import exceptions as novaclient_exceptions + +from horizon import api +from horizon.dashboards.nova.volumes.forms import (CreateForm, + DeleteForm, AttachForm, DetachForm) + + +LOG = logging.getLogger(__name__) + + +@login_required +def index(request): + delete_form, handled = DeleteForm.maybe_handle(request) + detach_form, handled = DetachForm.maybe_handle(request) + + if handled: + return handled + + try: + volumes = api.volume_list(request) + except novaclient_exceptions.ClientException, e: + volumes = [] + LOG.exception("ClientException in volume index") + messages.error(request, _('Error fetching volumes: %s') % e.message) + + return shortcuts.render(request, + 'nova/volumes/index.html', { + 'volumes': volumes, + 'delete_form': delete_form, + 'detach_form': detach_form}) + + +@login_required +def detail(request, volume_id): + try: + volume = api.volume_get(request, volume_id) + attachment = volume.attachments[0] + if attachment: + instance = api.server_get( + request, volume.attachments[0]['serverId']) + else: + instance = None + except novaclient_exceptions.ClientException, e: + LOG.exception("ClientException in volume get") + messages.error(request, _('Error fetching volume: %s') % e.message) + return shortcuts.redirect('horizon:nova:volumes:index') + + return shortcuts.render(request, + 'nova/volumes/detail.html', { + 'volume': volume, + 'attachment': attachment, + 'instance': instance}) + + +@login_required +def create(request): + create_form, handled = CreateForm.maybe_handle(request) + + if handled: + return handled + + return shortcuts.render(request, + 'nova/volumes/create.html', { + 'create_form': create_form}) + + +@login_required +def attach(request, volume_id): + + def instances(): + insts = api.server_list(request) + return [(inst.id, '%s (Instance %s)' % (inst.name, inst.id)) + for inst in insts] + + attach_form, handled = AttachForm.maybe_handle( + request, initial={'instance_list': instances()}) + + if handled: + return handled + + return shortcuts.render(request, + 'nova/volumes/attach.html', { + 'attach_form': attach_form, + 'volume_id': volume_id}) diff --git a/horizon/horizon/dashboards/settings/__init__.py b/horizon/horizon/dashboards/settings/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/settings/dashboard.py b/horizon/horizon/dashboards/settings/dashboard.py new file mode 100644 index 00000000..9cc88c45 --- /dev/null +++ b/horizon/horizon/dashboards/settings/dashboard.py @@ -0,0 +1,30 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.utils.translation import ugettext as _ + +import horizon + + +class Settings(horizon.Dashboard): + name = "Settings" + slug = "settings" + panels = ('user',) + default_panel = 'user' + nav = False + + +horizon.register(Settings) diff --git a/horizon/horizon/dashboards/settings/models.py b/horizon/horizon/dashboards/settings/models.py new file mode 100644 index 00000000..300ba4b3 --- /dev/null +++ b/horizon/horizon/dashboards/settings/models.py @@ -0,0 +1,23 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Stub file to work around django bug: https://code.djangoproject.com/ticket/7198 +""" diff --git a/horizon/horizon/dashboards/settings/templates/settings/base.html b/horizon/horizon/dashboards/settings/templates/settings/base.html new file mode 100644 index 00000000..739b94c1 --- /dev/null +++ b/horizon/horizon/dashboards/settings/templates/settings/base.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} + +{% block sidebar %} + {% include 'horizon/common/_sidebar.html' %} +{% endblock %} + +{% block main %} + {% block page_header %}{% endblock %} +
+ {% include "_messages.html" %} + {% block settings_main %}{% endblock %} +
+{% endblock %} diff --git a/horizon/horizon/dashboards/settings/templates/settings/user/settings.html b/horizon/horizon/dashboards/settings/templates/settings/user/settings.html new file mode 100644 index 00000000..4e7fc531 --- /dev/null +++ b/horizon/horizon/dashboards/settings/templates/settings/user/settings.html @@ -0,0 +1,32 @@ +{% extends 'settings/base.html' %} +{% load i18n %} + +{% block page_header %} + {% url horizon:nova:instances:index as refresh_link %} + {% include "horizon/common/_page_header.html" with title=_("Dashboard Settings") %} +{% endblock page_header %} + +{% block settings_main %} + +
+
+

{% trans "Dashboard User Interface Language"%}

+
{% csrf_token %} +

+ +
+
+ +
 
+
 
+
+{% endblock %} diff --git a/horizon/horizon/dashboards/settings/user/__init__.py b/horizon/horizon/dashboards/settings/user/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/settings/user/panel.py b/horizon/horizon/dashboards/settings/user/panel.py new file mode 100644 index 00000000..5f11a897 --- /dev/null +++ b/horizon/horizon/dashboards/settings/user/panel.py @@ -0,0 +1,26 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import horizon +from horizon.dashboards.settings import dashboard + + +class UserPanel(horizon.Panel): + name = "User Settings" + slug = 'user' + + +dashboard.Settings.register(UserPanel) diff --git a/horizon/horizon/dashboards/settings/user/urls.py b/horizon/horizon/dashboards/settings/user/urls.py new file mode 100644 index 00000000..7bfddcf5 --- /dev/null +++ b/horizon/horizon/dashboards/settings/user/urls.py @@ -0,0 +1,28 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls.defaults import patterns, url +from django.views.generic import TemplateView + + +urlpatterns = patterns('', + url(r'^$', TemplateView.as_view( + template_name='settings/user/settings.html'), + name='index')) diff --git a/horizon/horizon/dashboards/syspanel/__init__.py b/horizon/horizon/dashboards/syspanel/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/syspanel/dashboard.py b/horizon/horizon/dashboards/syspanel/dashboard.py new file mode 100644 index 00000000..ce4e4e01 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/dashboard.py @@ -0,0 +1,32 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.utils.translation import ugettext as _ + +import horizon + + +class Syspanel(horizon.Dashboard): + name = "System Dashboard" # Appears in navigation + slug = "syspanel" + panels = {_("System Panel"): ('overview', 'instances', 'services', + 'flavors', 'images', 'tenants', 'users', + 'quotas',)} + default_panel = 'overview' + roles = ('admin',) + + +horizon.register(Syspanel) diff --git a/horizon/horizon/dashboards/syspanel/flavors/__init__.py b/horizon/horizon/dashboards/syspanel/flavors/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/syspanel/flavors/forms.py b/horizon/horizon/dashboards/syspanel/flavors/forms.py new file mode 100644 index 00000000..c79fcdbe --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/flavors/forms.py @@ -0,0 +1,69 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django import shortcuts +from django.contrib import messages +from django.utils.translation import ugettext as _ +from openstackx.api import exceptions as api_exceptions + +from horizon import api +from horizon import forms + + +LOG = logging.getLogger(__name__) + + +class CreateFlavor(forms.SelfHandlingForm): + flavorid = forms.CharField(max_length="10", label=_("Flavor ID")) + name = forms.CharField(max_length="25", label=_("Name")) + vcpus = forms.CharField(max_length="5", label=_("VCPUs")) + memory_mb = forms.CharField(max_length="5", label=_("Memory MB")) + disk_gb = forms.CharField(max_length="5", label=_("Disk GB")) + + def handle(self, request, data): + api.flavor_create(request, + data['name'], + int(data['memory_mb']), + int(data['vcpus']), + int(data['disk_gb']), + int(data['flavorid'])) + msg = _('%s was successfully added to flavors.') % data['name'] + LOG.info(msg) + messages.success(request, msg) + return shortcuts.redirect('horizon:syspanel:flavors:index') + + +class DeleteFlavor(forms.SelfHandlingForm): + flavorid = forms.CharField(required=True) + + def handle(self, request, data): + try: + flavor_id = data['flavorid'] + flavor = api.flavor_get(request, flavor_id) + LOG.info('Deleting flavor with id "%s"' % flavor_id) + api.flavor_delete(request, flavor_id, False) + messages.info(request, _('Successfully deleted flavor: %s') % + flavor.name) + except api_exceptions.ApiException, e: + messages.error(request, _('Unable to delete flavor: %s') % + e.message) + return shortcuts.redirect(request.build_absolute_uri()) diff --git a/horizon/horizon/dashboards/syspanel/flavors/panel.py b/horizon/horizon/dashboards/syspanel/flavors/panel.py new file mode 100644 index 00000000..7a765a27 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/flavors/panel.py @@ -0,0 +1,30 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import horizon +from horizon.dashboards.syspanel import dashboard + + +class Flavors(horizon.Panel): + name = "Flavors" + slug = 'flavors' + + +dashboard.Syspanel.register(Flavors) diff --git a/horizon/horizon/dashboards/syspanel/flavors/tests.py b/horizon/horizon/dashboards/syspanel/flavors/tests.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/syspanel/flavors/urls.py b/horizon/horizon/dashboards/syspanel/flavors/urls.py new file mode 100644 index 00000000..e38603d7 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/flavors/urls.py @@ -0,0 +1,26 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls.defaults import patterns, url + + +urlpatterns = patterns('horizon.dashboards.syspanel.flavors.views', + url(r'^$', 'index', name='index'), + url(r'^create/$', 'create', name='create')) diff --git a/horizon/horizon/dashboards/syspanel/flavors/views.py b/horizon/horizon/dashboards/syspanel/flavors/views.py new file mode 100644 index 00000000..7be1c1ed --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/flavors/views.py @@ -0,0 +1,77 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django import shortcuts +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.utils.translation import ugettext as _ +from openstackx.api import exceptions as api_exceptions + +from horizon import api +from horizon import forms +from horizon.dashboards.syspanel.flavors.forms import (CreateFlavor, + DeleteFlavor) +from horizon.dashboards.syspanel.instances import views as instance_views + + +LOG = logging.getLogger(__name__) + + +@login_required +def index(request): + for f in (DeleteFlavor,): + form, handled = f.maybe_handle(request) + if handled: + return handled + + delete_form = DeleteFlavor() + + flavors = [] + try: + flavors = api.flavor_list(request) + except api_exceptions.ApiException, e: + LOG.exception('ApiException while fetching usage info') + messages.error(request, _('Unable to get usage info: %s') % e.message) + + flavors.sort(key=lambda x: x.id, reverse=True) + return shortcuts.render(request, + 'syspanel/flavors/index.html', { + 'delete_form': delete_form, + 'flavors': flavors}) + + +@login_required +def create(request): + form, handled = CreateFlavor.maybe_handle(request) + if handled: + return handled + + global_summary = instance_views.GlobalSummary(request) + global_summary.service() + global_summary.avail() + global_summary.human_readable('disk_size') + global_summary.human_readable('ram_size') + + return shortcuts.render(request, + 'syspanel/flavors/create.html', { + 'global_summary': global_summary.summary, + 'form': form}) diff --git a/horizon/horizon/dashboards/syspanel/images/__init__.py b/horizon/horizon/dashboards/syspanel/images/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/syspanel/images/forms.py b/horizon/horizon/dashboards/syspanel/images/forms.py new file mode 100644 index 00000000..766bd474 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/images/forms.py @@ -0,0 +1,83 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django import shortcuts +from django.contrib import messages +from django.utils.translation import ugettext as _ + +from glance.common import exception as glance_exception + +from horizon import api +from horizon import forms + + +LOG = logging.getLogger(__name__) + + +class DeleteImage(forms.SelfHandlingForm): + image_id = forms.CharField(required=True) + + def handle(self, request, data): + image_id = data['image_id'] + try: + api.image_delete(request, image_id) + except glance_exception.ClientConnectionError, e: + LOG.exception("Error connecting to glance") + messages.error(request, + _("Error connecting to glance: %s") % e.message) + except glance_exception.Error, e: + LOG.exception('Error deleting image with id "%s"' % image_id) + messages.error(request, _("Error deleting image: %s") % e.message) + return shortcuts.redirect(request.build_absolute_uri()) + + +class ToggleImage(forms.SelfHandlingForm): + image_id = forms.CharField(required=True) + + def handle(self, request, data): + image_id = data['image_id'] + try: + api.image_update(request, image_id, + image_meta={'is_public': False}) + except glance_exception.ClientConnectionError, e: + LOG.exception("Error connecting to glance") + messages.error(request, + _("Error connecting to glance: %s") % e.message) + except glance_exception.Error, e: + LOG.exception('Error updating image with id "%s"' % image_id) + messages.error(request, _("Error updating image: %s") % e.message) + return shortcuts.redirect(request.build_absolute_uri()) + + +class UpdateImageForm(forms.Form): + name = forms.CharField(max_length="25", label=_("Name")) + kernel = forms.CharField(max_length="25", label=_("Kernel ID"), + required=False) + ramdisk = forms.CharField(max_length="25", label=_("Ramdisk ID"), + required=False) + architecture = forms.CharField(label=_("Architecture"), required=False) + #project_id = forms.CharField(label=_("Project ID")) + container_format = forms.CharField(label=_("Container Format"), + required=False) + disk_format = forms.CharField(label=_("Disk Format")) + #is_public = forms.BooleanField(label=_("Publicly Available"), + # required=False) diff --git a/horizon/horizon/dashboards/syspanel/images/panel.py b/horizon/horizon/dashboards/syspanel/images/panel.py new file mode 100644 index 00000000..77e7bbb0 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/images/panel.py @@ -0,0 +1,30 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import horizon +from horizon.dashboards.syspanel import dashboard + + +class Images(horizon.Panel): + name = "Images" + slug = 'images' + + +dashboard.Syspanel.register(Images) diff --git a/horizon/horizon/dashboards/syspanel/images/tests.py b/horizon/horizon/dashboards/syspanel/images/tests.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/syspanel/images/urls.py b/horizon/horizon/dashboards/syspanel/images/urls.py new file mode 100644 index 00000000..5df3696a --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/images/urls.py @@ -0,0 +1,26 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls.defaults import patterns, url + + +urlpatterns = patterns('horizon.dashboards.syspanel.images.views', + url(r'^images/$', 'index', name='index'), + url(r'^(?P[^/]+)/update/$', 'update', name='update')) diff --git a/horizon/horizon/dashboards/syspanel/images/views.py b/horizon/horizon/dashboards/syspanel/images/views.py new file mode 100644 index 00000000..4a998a5f --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/images/views.py @@ -0,0 +1,192 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django import shortcuts +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.utils.translation import ugettext as _ +from glance.common import exception as glance_exception + +from horizon import api +from horizon.dashboards.syspanel.images.forms import (DeleteImage, + ToggleImage, UpdateImageForm) + + +LOG = logging.getLogger(__name__) + + +@login_required +def index(request): + for f in (DeleteImage, ToggleImage): + form, handled = f.maybe_handle(request) + if handled: + return handled + + # We don't have any way of showing errors for these, so don't bother + # trying to reuse the forms from above + delete_form = DeleteImage() + toggle_form = ToggleImage() + + images = [] + try: + images = api.image_list_detailed(request) + if not images: + messages.info(request, _("There are currently no images.")) + except glance_exception.ClientConnectionError, e: + LOG.exception("Error connecting to glance") + messages.error(request, + _("Error connecting to glance: %s") % e.message) + except glance_exception.Error, e: + LOG.exception("Error retrieving image list") + messages.error(request, + _("Error retrieving image list: %s") % e.message) + + return shortcuts.render(request, + 'syspanel/images/index.html', { + 'delete_form': delete_form, + 'toggle_form': toggle_form, + 'images': images}) + + +@login_required +def update(request, image_id): + try: + image = api.image_get(request, image_id) + except glance_exception.ClientConnectionError, e: + LOG.exception("Error connecting to glance") + messages.error(request, + _("Error connecting to glance: %s") % e.message) + except glance_exception.Error, e: + LOG.exception('Error retrieving image with id "%s"' % image_id) + messages.error(request, + _("Error retrieving image %(image)s: %(msg)s") % + {"image": image_id, "msg": e.message}) + + if request.method == "POST": + form = UpdateImageForm(request.POST) + if form.is_valid(): + image_form = form.clean() + metadata = { + 'is_public': True, + 'disk_format': image_form['disk_format'], + 'container_format': image_form['container_format'], + 'name': image_form['name'], + } + try: + # TODO add public flag to properties + metadata['properties'] = {} + if image_form['kernel']: + metadata['properties']['kernel_id'] = image_form['kernel'] + if image_form['ramdisk']: + metadata['properties']['ramdisk_id'] = \ + image_form['ramdisk'] + if image_form['architecture']: + metadata['properties']['architecture'] = \ + image_form['architecture'] + api.image_update(request, image_id, metadata) + messages.success(request, _("Image was successfully updated.")) + except glance_exception.ClientConnectionError, e: + LOG.exception("Error connecting to glance") + messages.error(request, + _("Error connecting to glance: %s") % e.message) + except glance_exception.Error, e: + LOG.exception('Error updating image with id "%s"' % image_id) + messages.error(request, + _("Error updating image: %s") % e.message) + except: + LOG.exception('Unspecified Exception in image update') + messages.error(request, + _("Image could not be updated, please try again.")) + return shortcuts.redirect('syspanel_images_update', image_id) + else: + LOG.exception('Image "%s" failed to update' % image['name']) + messages.error(request, + _("Image could not be uploaded, please try agian.")) + form = UpdateImageForm(request.POST) + return shortcuts.render(request, + 'syspanel/images/update.html', { + 'image': image, + 'form': form}) + else: + form = UpdateImageForm(initial={ + 'name': image.get('name', ''), + 'kernel': image['properties'].get('kernel_id', ''), + 'ramdisk': image['properties'].get('ramdisk_id', ''), + 'is_public': image.get('is_public', ''), + 'location': image.get('location', ''), + 'state': image['properties'].get('image_state', ''), + 'architecture': image['properties'].get('architecture', ''), + 'project_id': image['properties'].get('project_id', ''), + 'container_format': image.get('container_format', ''), + 'disk_format': image.get('disk_format', ''), + }) + + return shortcuts.render(request, + 'syspanel/images/update.html', { + 'image': image, + 'form': form}) + + +@login_required +def upload(request): + if request.method == "POST": + form = UploadImageForm(request.POST) + if form.is_valid(): + image = form.clean() + metadata = {'is_public': image['is_public'], + 'disk_format': 'ami', + 'container_format': 'ami', + 'name': image['name']} + try: + messages.success(request, + _("Image was successfully uploaded.")) + except: + # TODO add better error management + messages.error(request, + _("Image could not be uploaded, please try again.")) + + try: + api.image_create(request, metadata, image['image_file']) + except glance_exception.ClientConnectionError, e: + LOG.exception('Error connecting to glance while trying to' + 'upload image') + messages.error(request, + _("Error connecting to glance: %s") % e.message) + except glance_exception.Error, e: + LOG.exception('Glance exception while uploading image') + messages.error(request, + _("Error adding image: %s") % e.message) + else: + LOG.exception('Image "%s" failed to upload' % image['name']) + messages.error(request, + _("Image could not be uploaded, please try agian.")) + form = UploadImageForm(request.POST) + return shortcuts.render(request, + 'django_nova_syspanel/images/upload.html', + {'form': form}) + + return shortcuts.redirect('syspanel_images') + else: + form = UploadImageForm() + return shortcuts.render(request, + 'django_nova_syspanel/images/upload.html', + {'form': form}) diff --git a/horizon/horizon/dashboards/syspanel/instances/__init__.py b/horizon/horizon/dashboards/syspanel/instances/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/syspanel/instances/panel.py b/horizon/horizon/dashboards/syspanel/instances/panel.py new file mode 100644 index 00000000..56b52590 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/instances/panel.py @@ -0,0 +1,31 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import horizon +from horizon.dashboards.syspanel import dashboard + + +class Instances(horizon.Panel): + name = "Instances" + slug = 'instances' + roles = ('admin',) + + +dashboard.Syspanel.register(Instances) diff --git a/horizon/horizon/dashboards/syspanel/instances/tests.py b/horizon/horizon/dashboards/syspanel/instances/tests.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/syspanel/instances/urls.py b/horizon/horizon/dashboards/syspanel/instances/urls.py new file mode 100644 index 00000000..369a3eb5 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/instances/urls.py @@ -0,0 +1,36 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls.defaults import * +from django.conf import settings + + +INSTANCES = r'^(?P[^/]+)/%s$' + + +urlpatterns = patterns('horizon.dashboards.syspanel.instances.views', + url(r'^usage/(?P[^/]+)$', 'tenant_usage', name='tenant_usage'), + url(r'^$', 'index', name='index'), + url(r'^refresh$', 'refresh', name='refresh'), + url(INSTANCES % 'detail', 'detail', name='detail'), + # NOTE(termie): currently just using the 'dash' versions + #url(INSTANCES % 'console', 'console', name='instances_console'), + #url(INSTANCES % 'vnc', 'vnc', name='syspanel_instances_vnc'), +) diff --git a/horizon/horizon/dashboards/syspanel/instances/views.py b/horizon/horizon/dashboards/syspanel/instances/views.py new file mode 100644 index 00000000..81634b6b --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/instances/views.py @@ -0,0 +1,337 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime +import logging + +from django import template +from django import http +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.shortcuts import render_to_response, redirect +from django.utils.translation import ugettext as _ + +from horizon import api +from horizon import forms +from horizon.dashboards.nova.instances import views as dash_instances +from openstackx.api import exceptions as api_exceptions + + +TerminateInstance = dash_instances.TerminateInstance +RebootInstance = dash_instances.RebootInstance + + +LOG = logging.getLogger(__name__) + + +class GlobalSummary(object): + node_resources = ['vcpus', 'disk_size', 'ram_size'] + unit_mem_size = {'disk_size': ['GiB', 'TiB'], 'ram_size': ['MiB', 'GiB']} + node_resource_info = ['', 'active_', 'avail_'] + + def __init__(self, request): + self.summary = {} + for rsrc in GlobalSummary.node_resources: + for info in GlobalSummary.node_resource_info: + self.summary['total_' + info + rsrc] = 0 + self.request = request + self.service_list = [] + self.usage_list = [] + + def service(self): + try: + self.service_list = api.service_list(self.request) + except api_exceptions.ApiException, e: + self.service_list = [] + LOG.exception('ApiException fetching service list ' + 'in instance usage') + messages.error(self.request, + _('Unable to get service info: %s') % e.message) + return + + for service in self.service_list: + if service.type == 'nova-compute': + self.summary['total_vcpus'] += min(service.stats['max_vcpus'], + service.stats.get('vcpus', 0)) + self.summary['total_disk_size'] += min( + service.stats['max_gigabytes'], + service.stats.get('local_gb', 0)) + self.summary['total_ram_size'] += min( + service.stats['max_ram'], + service.stats['memory_mb']) if 'max_ram' \ + in service.stats \ + else service.stats.get('memory_mb', 0) + + def usage(self, datetime_start, datetime_end): + try: + self.usage_list = api.usage_list(self.request, datetime_start, + datetime_end) + except api_exceptions.ApiException, e: + self.usage_list = [] + LOG.exception('ApiException fetching usage list in instance usage' + ' on date range "%s to %s"' % (datetime_start, + datetime_end)) + messages.error(self.request, + _('Unable to get usage info: %s') % e.message) + return + + for usage in self.usage_list: + # FIXME: api needs a simpler dict interface (with iteration) + # - anthony + # NOTE(mgius): Changed this on the api end. Not too much + # neater, but at least its not going into private member + # data of an external class anymore + # usage = usage._info + for k in usage._attrs: + v = usage.__getattr__(k) + if type(v) in [float, int]: + if not k in self.summary: + self.summary[k] = 0 + self.summary[k] += v + + def human_readable(self, rsrc): + if self.summary['total_' + rsrc] > 1023: + self.summary['unit_' + rsrc] = GlobalSummary.unit_mem_size[rsrc][1] + mult = 1024.0 + else: + self.summary['unit_' + rsrc] = GlobalSummary.unit_mem_size[rsrc][0] + mult = 1.0 + + for kind in GlobalSummary.node_resource_info: + self.summary['total_' + kind + rsrc + '_hr'] = \ + self.summary['total_' + kind + rsrc] / mult + + def avail(self): + for rsrc in GlobalSummary.node_resources: + self.summary['total_avail_' + rsrc] = \ + self.summary['total_' + rsrc] - \ + self.summary['total_active_' + rsrc] + + +def _next_month(date_start): + y = date_start.year + (date_start.month + 1) / 13 + m = ((date_start.month + 1) % 13) + if m == 0: + m = 1 + return datetime.date(y, m, 1) + + +def _current_month(): + today = datetime.date.today() + return datetime.date(today.year, today.month, 1) + + +def _get_start_and_end_date(request): + try: + date_start = datetime.date( + int(request.GET['date_year']), + int(request.GET['date_month']), + 1) + except: + today = datetime.date.today() + date_start = datetime.date(today.year, today.month, 1) + + date_end = _next_month(date_start) + datetime_start = datetime.datetime.combine(date_start, datetime.time()) + datetime_end = datetime.datetime.combine(date_end, datetime.time()) + + if date_end > datetime.date.today(): + datetime_end = datetime.datetime.utcnow() + return (date_start, date_end, datetime_start, datetime_end) + + +def _csv_usage_link(date_start): + return "?date_month=%s&date_year=%s&format=csv" % (date_start.month, + date_start.year) + + +def usage(request): + (date_start, date_end, datetime_start, datetime_end) = \ + _get_start_and_end_date(request) + + global_summary = GlobalSummary(request) + if date_start > _current_month(): + messages.error(request, _('No data for the selected period')) + date_end = date_start + datetime_end = datetime_start + else: + global_summary.service() + global_summary.usage(datetime_start, datetime_end) + + dateform = forms.DateForm() + dateform['date'].field.initial = date_start + + global_summary.avail() + global_summary.human_readable('disk_size') + global_summary.human_readable('ram_size') + + if request.GET.get('format', 'html') == 'csv': + template_name = 'syspanel/instances/usage.csv' + mimetype = "text/csv" + else: + template_name = 'syspanel/instances/usage.html' + mimetype = "text/html" + + return render_to_response( + template_name, { + 'dateform': dateform, + 'datetime_start': datetime_start, + 'datetime_end': datetime_end, + 'usage_list': global_summary.usage_list, + 'csv_link': _csv_usage_link(date_start), + 'global_summary': global_summary.summary, + 'external_links': getattr(settings, 'EXTERNAL_MONITORING', []), + }, context_instance=template.RequestContext(request), mimetype=mimetype) + + +def tenant_usage(request): + tenant_id = request.user.tenant + (date_start, date_end, datetime_start, datetime_end) = \ + _get_start_and_end_date(request) + if date_start > _current_month(): + messages.error(request, _('No data for the selected period')) + date_end = date_start + datetime_end = datetime_start + + dateform = forms.DateForm() + dateform['date'].field.initial = date_start + + usage = {} + try: + usage = api.usage_get(request, tenant_id, datetime_start, datetime_end) + except api_exceptions.ApiException, e: + LOG.exception('ApiException getting usage info for tenant "%s"' + ' on date range "%s to %s"' % (tenant_id, + datetime_start, + datetime_end)) + messages.error(request, _('Unable to get usage info: %s') % e.message) + + running_instances = [] + terminated_instances = [] + if hasattr(usage, 'instances'): + now = datetime.datetime.now() + for i in usage.instances: + # this is just a way to phrase uptime in a way that is compatible + # with the 'timesince' filter. Use of local time intentional + i['uptime_at'] = now - datetime.timedelta(seconds=i['uptime']) + if i['ended_at']: + terminated_instances.append(i) + else: + running_instances.append(i) + + if request.GET.get('format', 'html') == 'csv': + template_name = 'syspanel/instances/tenant_usage.csv' + mimetype = "text/csv" + else: + template_name = 'syspanel/instances/tenant_usage.html' + mimetype = "text/html" + + return render_to_response(template_name, { + 'dateform': dateform, + 'datetime_start': datetime_start, + 'datetime_end': datetime_end, + 'usage': usage, + 'csv_link': _csv_usage_link(date_start), + 'instances': running_instances + terminated_instances, + 'tenant_id': tenant_id, + }, context_instance=template.RequestContext(request), mimetype=mimetype) + + +def index(request): + for f in (TerminateInstance, RebootInstance): + form, handled = f.maybe_handle(request) + if handled: + return handled + + instances = [] + try: + instances = api.admin_server_list(request) + except Exception as e: + LOG.exception('Unspecified error in instance index') + messages.error(request, + _('Unable to get instance list: %s') % e.message) + + # We don't have any way of showing errors for these, so don't bother + # trying to reuse the forms from above + terminate_form = TerminateInstance() + reboot_form = RebootInstance() + + return render_to_response( + 'syspanel/instances/index.html', { + 'instances': instances, + 'terminate_form': terminate_form, + 'reboot_form': reboot_form, + }, context_instance=template.RequestContext(request)) + + +def refresh(request): + for f in (TerminateInstance, RebootInstance): + form, handled = f.maybe_handle(request) + if handled: + return handled + + instances = [] + try: + instances = api.admin_server_list(request) + except Exception as e: + messages.error(request, + _('Unable to get instance list: %s') % e.message) + + # We don't have any way of showing errors for these, so don't bother + # trying to reuse the forms from above + terminate_form = TerminateInstance() + reboot_form = RebootInstance() + + return render_to_response( + 'syspanel/instances/_list.html', { + 'instances': instances, + 'terminate_form': terminate_form, + 'reboot_form': reboot_form, + }, context_instance=template.RequestContext(request)) + + +def detail(request, instance_id): + try: + instance = api.server_get(request, instance_id) + try: + console = api.console_create(request, instance_id, 'vnc') + vnc_url = "%s&title=%s(%s)" % (console.output, + instance.name, + instance_id) + except api_exceptions.ApiException, e: + LOG.exception('ApiException while fetching instance vnc \ + connection') + messages.error(request, + _('Unable to get vnc console for instance %(inst)s: %(message)s') % + {"inst": instance_id, "message": e.message}) + return redirect('horizon:syspanel:instances:index', tenant_id) + except api_exceptions.ApiException, e: + LOG.exception('ApiException while fetching instance info') + messages.error(request, + _('Unable to get information for instance %(inst)s: %(message)s') % + {"inst": instance_id, "message": e.message}) + return redirect('horizon:syspanel:instances:index', tenant_id) + + return render_to_response( + 'syspanel/instances/detail.html', { + 'instance': instance, + 'vnc_url': vnc_url, + }, context_instance=template.RequestContext(request)) diff --git a/horizon/horizon/dashboards/syspanel/models.py b/horizon/horizon/dashboards/syspanel/models.py new file mode 100644 index 00000000..300ba4b3 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/models.py @@ -0,0 +1,23 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Stub file to work around django bug: https://code.djangoproject.com/ticket/7198 +""" diff --git a/horizon/horizon/dashboards/syspanel/overview/__init__.py b/horizon/horizon/dashboards/syspanel/overview/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/syspanel/overview/panel.py b/horizon/horizon/dashboards/syspanel/overview/panel.py new file mode 100644 index 00000000..5ca5d23d --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/overview/panel.py @@ -0,0 +1,31 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import horizon +from horizon.dashboards.syspanel import dashboard + + +class Overview(horizon.Panel): + name = "Overview" + slug = 'overview' + roles = ('admin',) + + +dashboard.Syspanel.register(Overview) diff --git a/horizon/horizon/dashboards/syspanel/overview/urls.py b/horizon/horizon/dashboards/syspanel/overview/urls.py new file mode 100644 index 00000000..614f58be --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/overview/urls.py @@ -0,0 +1,26 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from django.conf.urls.defaults import * + +urlpatterns = patterns('horizon.dashboards.syspanel', + url(r'^$', 'instances.views.usage', name='index'), +) diff --git a/horizon/horizon/dashboards/syspanel/quotas/__init__.py b/horizon/horizon/dashboards/syspanel/quotas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/syspanel/quotas/panel.py b/horizon/horizon/dashboards/syspanel/quotas/panel.py new file mode 100644 index 00000000..ede0efb4 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/quotas/panel.py @@ -0,0 +1,30 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import horizon +from horizon.dashboards.syspanel import dashboard + + +class Quotas(horizon.Panel): + name = "Quotas" + slug = 'quotas' + + +dashboard.Syspanel.register(Quotas) diff --git a/horizon/horizon/dashboards/syspanel/quotas/tests.py b/horizon/horizon/dashboards/syspanel/quotas/tests.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/syspanel/quotas/urls.py b/horizon/horizon/dashboards/syspanel/quotas/urls.py new file mode 100644 index 00000000..6f7436a8 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/quotas/urls.py @@ -0,0 +1,25 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls.defaults import patterns, url + + +urlpatterns = patterns('horizon.dashboards.syspanel.quotas.views', + url(r'^$', 'index', name='index')) diff --git a/horizon/horizon/dashboards/syspanel/quotas/views.py b/horizon/horizon/dashboards/syspanel/quotas/views.py new file mode 100644 index 00000000..73a1d4ee --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/quotas/views.py @@ -0,0 +1,36 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django import shortcuts +from django.contrib.auth.decorators import login_required +from openstackx.api import exceptions as api_exceptions + +from horizon import api + + +@login_required +def index(request): + quotas = api.admin_api(request).quota_sets.get(True)._info + quotas['ram'] = int(quotas['ram']) / 100 + quotas.pop('id') + + return shortcuts.render(request, + 'syspanel/quotas/index.html', { + 'quotas': quotas}) diff --git a/horizon/horizon/dashboards/syspanel/services/__init__.py b/horizon/horizon/dashboards/syspanel/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/syspanel/services/forms.py b/horizon/horizon/dashboards/syspanel/services/forms.py new file mode 100644 index 00000000..81f1f673 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/services/forms.py @@ -0,0 +1,58 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django import shortcuts +from django.contrib import messages +from django.utils.translation import ugettext as _ +from openstackx.api import exceptions as api_exceptions + +from horizon import api +from horizon import forms + + +LOG = logging.getLogger(__name__) + + +class ToggleService(forms.SelfHandlingForm): + service = forms.CharField(required=False) + name = forms.CharField(required=False) + + def handle(self, request, data): + try: + service = api.service_get(request, data['service']) + api.service_update(request, + data['service'], + not service.disabled) + if service.disabled: + messages.info(request, _("Service '%s' has been enabled") + % data['name']) + else: + messages.info(request, _("Service '%s' has been disabled") + % data['name']) + except api_exceptions.ApiException, e: + LOG.exception('ApiException while toggling service %s' % + data['service']) + messages.error(request, + _("Unable to update service '%(name)s': %(msg)s") + % {"name": data['name'], "msg": e.message}) + + return shortcuts.redirect(request.build_absolute_uri()) diff --git a/horizon/horizon/dashboards/syspanel/services/panel.py b/horizon/horizon/dashboards/syspanel/services/panel.py new file mode 100644 index 00000000..7a17f8de --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/services/panel.py @@ -0,0 +1,30 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import horizon +from horizon.dashboards.syspanel import dashboard + + +class Services(horizon.Panel): + name = "Services" + slug = 'services' + + +dashboard.Syspanel.register(Services) diff --git a/horizon/horizon/dashboards/syspanel/services/tests.py b/horizon/horizon/dashboards/syspanel/services/tests.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/syspanel/services/urls.py b/horizon/horizon/dashboards/syspanel/services/urls.py new file mode 100644 index 00000000..e7757de0 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/services/urls.py @@ -0,0 +1,25 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls.defaults import patterns, url + + +urlpatterns = patterns('horizon.dashboards.syspanel.services.views', + url(r'^$', 'index', name='index')) diff --git a/horizon/horizon/dashboards/syspanel/services/views.py b/horizon/horizon/dashboards/syspanel/services/views.py new file mode 100644 index 00000000..73d29ee0 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/services/views.py @@ -0,0 +1,81 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +import os +import subprocess +import urlparse + +from django import shortcuts +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.utils.translation import ugettext as _ +from openstackx.api import exceptions as api_exceptions + +from horizon import api +from horizon.dashboards.syspanel.services.forms import ToggleService + + +LOG = logging.getLogger(__name__) + + +@login_required +def index(request): + for f in (ToggleService,): + form, handled = f.maybe_handle(request) + if handled: + return handled + + services = [] + try: + services = api.service_list(request) + except api_exceptions.ApiException, e: + LOG.exception('ApiException fetching service list') + messages.error(request, + _('Unable to get service info: %s') % e.message) + + other_services = [] + + for service in request.session['serviceCatalog']: + url = service['endpoints'][0]['internalURL'] + try: + # TODO(mgius): This silences curl, but there's probably + # a better solution than using curl to begin with + subprocess.check_call(['curl', '-m', '1', url], + stdout=open(os.devnull, 'w'), + stderr=open(os.devnull, 'w')) + up = True + except: + up = False + hostname = urlparse.urlparse(url).hostname + row = {'type': service['type'], 'internalURL': url, 'host': hostname, + 'region': service['endpoints'][0]['region'], 'up': up} + other_services.append(row) + + services = sorted(services, key=lambda svc: (svc.type + + svc.host)) + other_services = sorted(other_services, key=lambda svc: (svc['type'] + + svc['host'])) + + return shortcuts.render(request, + 'syspanel/services/index.html', { + 'services': services, + 'service_toggle_enabled_form': ToggleService, + 'other_services': other_services}) diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/base.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/base.html new file mode 100644 index 00000000..1add065c --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/base.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} + +{% block sidebar %} + {% include 'horizon/common/_sidebar.html' %} +{% endblock %} + +{% block main %} + {% block page_header %}{% endblock %} +
+ {% include "_messages.html" %} + {% block syspanel_main %}{% endblock %} +
+{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/_create.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/_create.html new file mode 100644 index 00000000..a5bc2dd3 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/_create.html @@ -0,0 +1,6 @@ +{% extends 'syspanel/flavors/_form.html' %} +{%load i18n%} + +{% block submit %} + +{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/_delete.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/_delete.html new file mode 100644 index 00000000..9d00e04e --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/_delete.html @@ -0,0 +1,9 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + +
diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/_form.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/_form.html new file mode 100644 index 00000000..2ec0f4cd --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/_form.html @@ -0,0 +1,17 @@ +{%load i18n%} +
+ {% csrf_token %} +
+ {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + {% for field in form.visible_fields %} + {{field.label_tag}} + {{field.errors}} + {{field}} + {% endfor %} + {% block submit %} + + {% endblock %} +
+
diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/_list.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/_list.html new file mode 100644 index 00000000..e2fe3df2 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/_list.html @@ -0,0 +1,25 @@ +{%load i18n%} + + + + + + + + + + {% for flavor in flavors %} + + + + + + + + + {% endfor %} +
{% trans "Id"%}{% trans "Name"%}{% trans "VCPUs"%}{% trans "Memory"%}{% trans "Disk"%}{% trans "Actions"%}
{{flavor.id}}{{flavor.name}}{{flavor.vcpus}}{{flavor.ram}}MB{{flavor.disk}}GB +
    +
  • {% include "syspanel/flavors/_delete.html" with form=delete_form %}
  • +
+
diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/create.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/create.html new file mode 100644 index 00000000..c80ae350 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/create.html @@ -0,0 +1,41 @@ +{% extends 'syspanel/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="flavors" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create Flavor") %} +{% endblock page_header %} + +{% block syspanel_main %} +
+
+
    +
  • +

    {{global_summary.total_vcpus}} Cores

    +

    {{global_summary.total_avail_vcpus}} Avail

    +
  • +
  • +

    {{global_summary.total_ram_size_hr|floatformat}} {{global_summary.unit_ram_size}} Ram

    +

    {{global_summary.total_avail_ram_size_hr|floatformat}} {{global_summary.unit_ram_size}} Avail

    +
  • +
  • +

    {{global_summary.total_disk_size_hr|floatformat}} {{global_summary.unit_disk_size}} Disk

    +

    {{global_summary.total_avail_disk_size_hr|floatformat}} {{global_summary.unit_disk_size}} Avail

    +
  • +
+
+
+ {% include "syspanel/flavors/_create.html" %} +
+
+

{% trans "Description"%}:

+

{% trans "From here you can define the sizing of a new flavor."%}

+
+
 
+
+{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/index.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/index.html new file mode 100644 index 00000000..d1eabacf --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/flavors/index.html @@ -0,0 +1,19 @@ +{% extends 'syspanel/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="flavors" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% url horizon:syspanel:flavors:index as refresh_link %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Flavors") refresh_link=refresh_link searchable="true" %} +{% endblock page_header %} + +{% block syspanel_main %} + {% include "syspanel/flavors/_list.html" %} + {% trans "Create New Flavor"%} +{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/images/_delete.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/images/_delete.html new file mode 100644 index 00000000..3ec7a2e5 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/images/_delete.html @@ -0,0 +1,9 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + +
diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/images/_form.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/images/_form.html new file mode 100644 index 00000000..c2c5e7dd --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/images/_form.html @@ -0,0 +1,11 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + +
diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/images/_list.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/images/_list.html new file mode 100644 index 00000000..c3ada501 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/images/_list.html @@ -0,0 +1,47 @@ +{% load parse_date %} +{%load i18n%} + + + + + + + + + + + + {% for image in images %} + + + + + + + + + + + + + + {% endfor %} +
{% trans "ID"%}{% trans "Name"%}{% trans "Size"%}{% trans "Public"%}{% trans "Created"%}{% trans "Updated"%}{% trans "Status"%}
{{image.id}}{{image.name}}{{image.size|filesizeformat}}{{image.is_public}}{{image.created_at|parse_date}}{{image.updated_at|parse_date}}{{image.status|capfirst}} +
    +
  • {% include "syspanel/images/_delete.html" with form=delete_form %}
  • + {#
  • {% include "syspanel/images/_toggle.html" with form=toggle_form %}
  • #} + +
  • {% trans "Edit"%}
  • +
+
+
    +
  • {% trans "Location"%}: {{image.properties.image_location}}
  • +
  • {% trans "State"%}: {{image.properties.image_state}}
  • +
  • {% trans "Kernel ID"%}: {{image.properties.kernel_id}}
  • +
  • {% trans "Ramdisk ID"%}: {{image.properties.ramdisk_id}}
  • +
  • {% trans "Architecture"%}: {{image.properties.architecture}}
  • +
  • {% trans "Project ID"%}: {{image.properties.project_id}}
  • +
  • {% trans "Container Format"%}: {{image.container_format}}
  • +
  • {% trans "Disk Format"%}: {{image.disk_format}}
  • +
+
diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/images/_toggle.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/images/_toggle.html new file mode 100644 index 00000000..9aee6370 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/images/_toggle.html @@ -0,0 +1,9 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + +
diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/images/index.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/images/index.html new file mode 100644 index 00000000..a0e29433 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/images/index.html @@ -0,0 +1,18 @@ +{% extends 'syspanel/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="images" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% url horizon:syspanel:images:index as refresh_link %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Images") refresh_link=refresh_link searchable="true" %} +{% endblock page_header %} + +{% block syspanel_main %} + {% include "syspanel/images/_list.html" %} +{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/images/update.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/images/update.html new file mode 100644 index 00000000..a9ff84ee --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/images/update.html @@ -0,0 +1,26 @@ +{% extends 'syspanel/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="images" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Update Image") %} +{% endblock page_header %} + +{% block syspanel_main %} +
+
+ {% include 'syspanel/images/_form.html' %} +
+ +
+

{% trans "Description"%}:

+

{% trans "From here you can modify different properties of an image."%}

+
+
 
+
+{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/instances/_list.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/instances/_list.html new file mode 100644 index 00000000..71a13041 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/instances/_list.html @@ -0,0 +1,54 @@ +{% load parse_date %} +{% load i18n %} + + + + + + + + + + + + + {% for instance in instances %} + + + + + + + + + + + + + + {% endfor %} +
{% trans "Name"%}{% trans "Tenant"%}{% trans "User"%}{% trans "Host"%}{% trans "Created"%}{% trans "Image"%}{% trans "IPs"%}{% trans "State"%}{% trans "Actions"%}
{{instance.name}} (id: {{instance.id}}){{instance.attrs.tenant_id}}{{instance.attrs.user_id}}{{instance.attrs.host}}{{instance.attrs.launched_at|parse_date}}{{instance.image_name}} + {% for ip_group, addresses in instance.addresses.items %} + {% if instance.addresses.items|length > 1 %} +

{{ip_group}}

+
    + {% for address in addresses %} +
  • {{address.addr}}
  • + {% endfor %} +
+ {% else %} +
    + {% for address in addresses %} +
  • {{address.addr}}
  • + {% endfor %} +
+ {% endif %} + {% endfor %} +
{{instance.status|lower|capfirst}} + +
diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/instances/detail.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/instances/detail.html new file mode 100644 index 00000000..def6e8ab --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/instances/detail.html @@ -0,0 +1,104 @@ +{% extends 'syspanel/base.html' %} + +{% block sidebar %} + {% with current_sidebar="instances" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title="Instance Detail: "|add:instance.name %} +{% endblock page_header %} + +{% block syspanel_main %} + + +
+
+
    +
  • +
    +

    Status

    +
      +
    • Status: {{instance.status}}
    • +
    • Instance Name: {{instance.name}}
    • +
    • Instance ID: {{instance.id}}
    • +
    +
    +
  • + +
  • +
    +

    Specs

    +
      +
    • RAM: {{instance.attrs.memory_mb}} MB
    • +
    • VCPUs: {{instance.attrs.vcpus}} VCPU
    • +
    • Disk: {{instance.attrs.disk_gb}}GB Disk
    • +
    +
    +
  • + +
  • +
    +

    Meta

    +
      +
    • Key name: {{instance.attrs.key_name}}
    • +
    • Security Group(s): {% for group in instance.attrs.security_groups %}{{group}}, {% endfor %}
    • +
    • Image Name: {{instance.image_name}}
    • +
    +
    +
  • +
+
+ + + +
+ +
+ +
+{% endblock %} + +{% block footer_js %} + +{% endblock footer_js %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/instances/index.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/instances/index.html new file mode 100644 index 00000000..3f956cc9 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/instances/index.html @@ -0,0 +1,66 @@ +{% extends 'syspanel/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="instances" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% url horizon:syspanel:instances:index as refresh_link %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Instances") refresh_link=refresh_link searchable="true" %} +{% endblock page_header %} + +{% block syspanel_main %} + {% if instances %} + {% include 'syspanel/instances/_list.html' %} + {% else %} +
+ {% url horizon:nova:images:index as dash_image_url%} +

{% trans "Info"%}

+

{% blocktrans %}There are currently no instances. You can launch an instance from the Images Page.{% endblocktrans %}

+
+ {% endif %} +{% endblock %} + +{% block footer_js %} + +{% endblock footer_js %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/instances/tenant_usage.csv b/horizon/horizon/dashboards/syspanel/templates/syspanel/instances/tenant_usage.csv new file mode 100644 index 00000000..d618b637 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/instances/tenant_usage.csv @@ -0,0 +1,11 @@ +Usage Report For Period:,{{datetime_start|date:"b. d Y H:i"}},/,{{datetime_end|date:"b. d Y H:i"}} +Tenant ID:,{{usage.tenant_id}} +Total Active VCPUs:,{{usage.total_active_vcpus}} +CPU-HRs Used:,{{usage.total_cpu_usage}} +Total Active Ram (MB):,{{usage.total_active_ram_size}} +Total Disk Size:,{{usage.total_active_disk_size}} +Total Disk Usage:,{{usage.total_disk_usage}} + +ID,Name,UserId,VCPUs,RamMB,DiskGB,Flavor,Usage(Hours),Uptime(Seconds),State +{% for instance in usage.instances %}{{instance.id}},{{instance.name|addslashes}},{{instance.user_id|addslashes}},{{instance.vcpus|addslashes}},{{instance.ram_size|addslashes}},{{instance.disk_size|addslashes}},{{instance.flavor|addslashes}},{{instance.hours}},{{instance.uptime}},{{instance.state|capfirst|addslashes}} +{% endfor %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/instances/tenant_usage.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/instances/tenant_usage.html new file mode 100644 index 00000000..15a5cc73 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/instances/tenant_usage.html @@ -0,0 +1,98 @@ +{% extends 'syspanel/base.html' %} +{% load parse_date %} +{% load sizeformat %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="tenants" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("System Panel Overview") %} +{% endblock page_header %} + +{% block syspanel_main %} +
+ +

Select a month to query its usage:

+
+ {{dateform.date}} + +
+
+ +
+
+

CPU

+
    +
  • {{usage.total_active_vcpus}}Cores Active
  • +
  • {{usage.total_cpu_usage|floatformat}}CPU-HR Used
  • +
+
+ +
+

RAM

+
    +
  • {{usage.total_active_ram_size}}MB Active
  • +
+
+ +
+

Disk

+
    +
  • {{usage.total_active_disk_size}}GB Active
  • +
  • {{usage.total_disk_usage|floatformat}}GB-HR Used
  • +
+
+
+

+ {% trans "Active Instances"%}: {{usage.total_active_instances}} + {% trans "This month's VCPU-Hours"%}: {{usage.total_cpu_usage|floatformat}} + {% trans "This month's GB-Hours"%}: {{usage.total_disk_usage|floatformat}} +

+ + + {% if usage.instances %} +
+ {% trans "Download CSV"%} » +

{% trans "Tenant Usage"%}: {{tenant_id}}

+
+ + + + + + + + + + + + + + + {% for instance in instances %} + {% if instance.ended_at %} + + {% else %} + + {% endif %} + + + + + + + + + + + {% endfor %} + +
{% trans "ID"%}{% trans "Name"%}{% trans "User"%}{% trans "VCPUs"%}{% trans "Ram Size"%}{% trans "Disk Size"%}{% trans "Flavor"%}{% trans "Uptime"%}{% trans "Status"%}
{{instance.id}}{{instance.name}}{{instance.user_id}}{{instance.vcpus}}{{instance.ram_size|mbformat}}{{instance.disk_size}}GB{{instance.flavor}}{{instance.uptime_at|timesince}}{{instance.state|capfirst}}
+ {% endif %} + +{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/instances/usage.csv b/horizon/horizon/dashboards/syspanel/templates/syspanel/instances/usage.csv new file mode 100644 index 00000000..79acd2c7 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/instances/usage.csv @@ -0,0 +1,8 @@ +Usage Report For Period:,{{datetime_start|date:"b. d Y H:i"}},/,{{datetime_end|date:"b. d Y H:i"}} +Active Instances:,{{global_summary.total_active_instances|default:'-'}} +This month's VCPU-Hours:,{{global_summary.total_cpu_usage|floatformat|default:'-'}} +This month's GB-Hours:,{{global_summary.total_disk_usage|floatformat|default:'-'}} + +Name,UserId,VCPUs,RamMB,DiskGB,Flavor,Usage(Hours),Uptime(Seconds),State +{% for usage in usage_list %}{% for instance in usage.instances %}{{instance.name|addslashes}},{{instance.user_id|addslashes}},{{instance.vcpus|addslashes}},{{instance.ram_size|addslashes}},{{instance.disk_size|addslashes}},{{instance.flavor|addslashes}},{{instance.hours}},{{instance.uptime}},{{instance.state|capfirst|addslashes}}{% endfor %} +{% endfor %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/instances/usage.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/instances/usage.html new file mode 100644 index 00000000..de58d8a4 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/instances/usage.html @@ -0,0 +1,99 @@ +{% extends 'syspanel/base.html' %} +{% load sizeformat %} +{%load i18n%} + +{# default landing page for a admin user #} +{# nav bar on top, sidebar, overview info in main #} + +{% block sidebar %} + {% with current_sidebar="overview" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("System Panel Overview") %} +{% endblock page_header %} + +{% block syspanel_main %} +
+ {% if external_links %} +
+

{% trans "Monitoring"%}:

+ +
+ {% endif %} + +
+ +

{% trans "Select a month to query its usage"%}:

+
+ {{ dateform.date }} + +
+
+ +
    +
  • Status: Good
  • +
  • +

    {{global_summary.total_vcpus}} Cores

    +

    {{global_summary.total_active_vcpus}} Used

    +

    {{global_summary.total_avail_vcpus}} Avail

    +
  • +
  • +

    {{global_summary.total_ram_size_hr|floatformat}} {{global_summary.unit_ram_size}} Ram

    +

    {{global_summary.total_active_ram_size_hr|floatformat}} {{global_summary.unit_ram_size}} Used

    +

    {{global_summary.total_avail_ram_size_hr|floatformat}} {{global_summary.unit_ram_size}} Avail

    +
  • +
  • +

    {{global_summary.total_disk_size_hr|floatformat}} {{global_summary.unit_disk_size}} Disk

    +

    {{global_summary.total_active_disk_size_hr|floatformat}} {{global_summary.unit_disk_size}} Used

    +

    {{global_summary.total_avail_disk_size_hr|floatformat}} {{global_summary.unit_disk_size}} Avail

    +
  • +
+ +

+ {% trans "Active Instances"%}: {{global_summary.total_active_instances|default:'-'}} + {% trans "This month's VCPU-Hours"%}: {{global_summary.total_cpu_usage|floatformat|default:'-'}} + {% trans "This month's GB-Hours"%}: {{global_summary.total_disk_usage|floatformat|default:'-'}} +

+
+ + {% if usage_list %} +
+
+ {% trans "Download CSV"%} » +

{% trans "Server Usage Summary"%}

+
+ + + + + + + + + + + + {% for usage in usage_list %} + + + + + + + + + + {% endfor %} + + {% endif %} + + +{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/quotas/index.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/quotas/index.html new file mode 100644 index 00000000..ccc90c88 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/quotas/index.html @@ -0,0 +1,29 @@ +{% extends 'syspanel/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="quotas" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% url horizon:syspanel:quotas:index as refresh_link %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Default Quotas") refresh_link=refresh_link searchable="true" %} +{% endblock page_header %} + +{% block syspanel_main %} +
{% trans "Tenant"%}{% trans "Instances"%}{% trans "VCPUs"%}{% trans "Disk"%}{% trans "RAM"%}{% trans "VCPU CPU-Hours"%}{% trans "Disk GB-Hours"%}
{{usage.tenant_id}}{{usage.total_active_instances}}{{usage.total_active_vcpus}}{{usage.total_active_disk_size|diskgbformat}}{{usage.total_active_ram_size|mbformat}}{{usage.total_cpu_usage|floatformat}}{{usage.total_disk_usage|floatformat}}
+ + + + + {% for name,value in quotas.items %} + + + + + {% endfor %} +
{% trans "Quota Name"%}{% trans "Limit"%}
{{name}}{{value}}
+{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/services/_list.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/services/_list.html new file mode 100644 index 00000000..9bda1f4c --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/services/_list.html @@ -0,0 +1,65 @@ +{%load i18n%} +{% load sizeformat %} + + + + + + + + + + {% for service in services %} + + + {% if service.type == 'nova-compute' %} + + {% else %} + + {% endif %} + + + + + {% endfor %} + {% for service in other_services %} + + + + + + + + {% endfor %} +
{% trans "Service"%}{% trans "System Stats"%}{% trans "Enabled"%}{% trans "Up"%}{% trans "Actions"%}
+ {{service.type}}
+ ( {{service.host}} ) +
+
    +
  • + {% trans "Hypervisor"%}: {{service.stats.hypervisor_type}}( {{service.stats.cpu_info.features|join:', '}}) +
  • +
  • + {% trans "Allocable Cores"%}: + {{service.stats.max_vcpus}} + ({{service.stats.vcpus_used}} Used, {{service.stats.vcpus}} Physical/Virtual) +
  • +
  • + {% trans "Allocable Storage"%}: + {{service.stats.max_gigabytes|diskgbformat}} + ({{service.stats.local_gb_used|diskgbformat}} Used, {{service.stats.local_gb|diskgbformat}} Physical) +
  • +
  • + {% trans "System Ram"%}: + {{service.stats.memory_mb|mbformat}} + ({{service.stats.memory_mb_used|mbformat}} Used) +
  • +
+
- {{service.disabled|yesno:"Disabled,Enabled"}}{{service.up}} +
    +
  • {% include "syspanel/services/_toggle.html" with form=service_toggle_enabled_form %}
  • +
+
+ {{service.type}}
+ ( {{service.host}} ) +
- {{service.disabled|yesno:"Disabled,Enabled"}}{{service.up}}
diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/services/_toggle.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/services/_toggle.html new file mode 100644 index 00000000..43c5bfb3 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/services/_toggle.html @@ -0,0 +1,22 @@ +{%load i18n%} +{% if service.disabled %} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + + +
+{% else %} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + + +
+{% endif %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/services/index.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/services/index.html new file mode 100644 index 00000000..b5145c71 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/services/index.html @@ -0,0 +1,19 @@ +{% extends 'syspanel/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="services" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% url horizon:syspanel:services:index as refresh_link %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Services") refresh_link=refresh_link searchable="true" %} +{% endblock page_header %} + +{% block syspanel_main %} + {% include "syspanel/services/_list.html" %} +{% endblock %} + diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_add_user.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_add_user.html new file mode 100644 index 00000000..a326e825 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_add_user.html @@ -0,0 +1,10 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + + +
diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_create_form.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_create_form.html new file mode 100644 index 00000000..6bdae710 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_create_form.html @@ -0,0 +1,6 @@ +{% extends "syspanel/tenants/_form.html" %} +{%load i18n%} + +{% block submit %} + +{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_delete.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_delete.html new file mode 100644 index 00000000..e933f95b --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_delete.html @@ -0,0 +1,9 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + +
diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_form.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_form.html new file mode 100644 index 00000000..69276c0f --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_form.html @@ -0,0 +1,11 @@ +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + {% block submit %} + {% endblock %} +
diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_list.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_list.html new file mode 100644 index 00000000..ac6410c4 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_list.html @@ -0,0 +1,26 @@ +{%load i18n%} + + + + + + + + + {% for tenant in tenants %} + + + + + + + + {% endfor %} +
{% trans "Id"%}{% trans "Name"%}{% trans "Description"%}{% trans "Enabled"%}{% trans "Options"%}
{{tenant.id}}{{tenant.name}}{{tenant.description}}{{tenant.enabled}} + +
diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_quotas_form.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_quotas_form.html new file mode 100644 index 00000000..a2168618 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_quotas_form.html @@ -0,0 +1,12 @@ +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + {% block submit %} + {% endblock %} +
+ diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_remove_user.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_remove_user.html new file mode 100644 index 00000000..828c48c7 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_remove_user.html @@ -0,0 +1,10 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + + +
diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_update_form.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_update_form.html new file mode 100644 index 00000000..5748ec0f --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_update_form.html @@ -0,0 +1,6 @@ +{% extends "syspanel/tenants/_form.html" %} +{%load i18n%} + +{% block submit %} + +{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_update_quotas_form.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_update_quotas_form.html new file mode 100644 index 00000000..5875f6ad --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_update_quotas_form.html @@ -0,0 +1,6 @@ +{% extends "syspanel/tenants/_quotas_form.html" %} +{%load i18n%} + +{% block submit %} + +{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/create.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/create.html new file mode 100644 index 00000000..0a621d69 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/create.html @@ -0,0 +1,26 @@ +{% extends 'syspanel/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="tenants" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create Tenant") %} +{% endblock page_header %} + +{% block syspanel_main %} +
+
+ {% include 'syspanel/tenants/_create_form.html' %} +
+ +
+

{% trans "Description"%}:

+

{% trans "From here you can create a new tenant (aka project) to organize users."%}

+
+
 
+
+{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/index.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/index.html new file mode 100644 index 00000000..251759ec --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/index.html @@ -0,0 +1,25 @@ +{% extends 'syspanel/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="tenants" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% url horizon:syspanel:tenants:index as refresh_link %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Tenants") refresh_link=refresh_link searchable="true" %} +{% endblock page_header %} + +{% block syspanel_main %} + {% include "syspanel/tenants/_list.html" %} + {% trans "Create New Tenant"%} +{% endblock %} + + + + + + diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/quotas.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/quotas.html new file mode 100644 index 00000000..75e762cc --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/quotas.html @@ -0,0 +1,30 @@ +{% extends 'syspanel/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="tenants" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Update Tenant Quotas") %} +{% endblock page_header %} + +{% block syspanel_main %} +
+
+ {% include 'syspanel/tenants/_update_quotas_form.html' with form=form %} +
+ +
+

{% trans "Description"%}:

+

{% trans "From here you can edit quotas (max limits) for the tenant "%}{{ tenant_id }}.

+
+
 
+
+{% endblock %} + + + + diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/update.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/update.html new file mode 100644 index 00000000..6602bac0 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/update.html @@ -0,0 +1,29 @@ +{% extends 'syspanel/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="tenants" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Update Tenant") %} +{% endblock page_header %} + +{% block syspanel_main %} +
+
+ {% include 'syspanel/tenants/_update_form.html' %} +
+ +
+

{% trans "Description"%}:

+

{% trans "From here you can edit a tenant."%}

+
+
 
+
+{% endblock %} + + + diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/users.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/users.html new file mode 100644 index 00000000..6ca867e0 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/users.html @@ -0,0 +1,72 @@ +{% extends 'syspanel/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="tenants" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + +{% endblock %} + +{% block syspanel_main %} +
+ + {% if users %} + + + + + + + + + {% for user in users %} + + + + + + + {% endfor %} + +
{% trans "ID"%}{% trans "Name"%}{% trans "Email"%}{% trans "Actions"%}
{{user.id}}{{user.name}}{{user.email}} +
    +
  • {% include "syspanel/tenants/_remove_user.html" with form=remove_user_form %}
  • +
+
+ {% else %} +
+

{% trans "Info"%}

+

T{% trans "here are currently no users for this tenant"%}

+
+ {% endif %} + {% if new_users %} +

{% trans "Add new users"%}

+ + + + + + + + {% for user in new_users %} + + + + + + {% endfor %} + +
{% trans "ID"%}{% trans "Name"%}{% trans "Actions"%}
{{user.id}}{{user.name}} +
    +
  • {% include "syspanel/tenants/_add_user.html" with form=add_user_form %}
  • +
+
+ {% endif %} + +{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/users/_create_form.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/users/_create_form.html new file mode 100644 index 00000000..c55fd77d --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/users/_create_form.html @@ -0,0 +1,7 @@ +{% extends "syspanel/users/_form.html" %} +{%load i18n%} + +{% block submit %} + +{% endblock %} + diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/users/_delete.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/users/_delete.html new file mode 100644 index 00000000..408f27e2 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/users/_delete.html @@ -0,0 +1,9 @@ +{%load i18n%} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + +
diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/users/_enable_disable.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/users/_enable_disable.html new file mode 100644 index 00000000..69f3ac5f --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/users/_enable_disable.html @@ -0,0 +1,9 @@ +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + + +
diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/users/_form.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/users/_form.html new file mode 100644 index 00000000..3caecdb6 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/users/_form.html @@ -0,0 +1,12 @@ +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + {% block submit %} + {% endblock %} +
+ diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/users/_toggle_enabled.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/users/_toggle_enabled.html new file mode 100644 index 00000000..7436648e --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/users/_toggle_enabled.html @@ -0,0 +1,20 @@ +{%load i18n%} +{% if user.enabled %} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + +
+{% else %} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + +
+{% endif %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/users/_update_form.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/users/_update_form.html new file mode 100644 index 00000000..02b77f75 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/users/_update_form.html @@ -0,0 +1,7 @@ +{% extends "syspanel/users/_form.html" %} +{%load i18n%} + +{% block submit %} + +{% endblock %} + diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/users/create.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/users/create.html new file mode 100644 index 00000000..f22120d3 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/users/create.html @@ -0,0 +1,27 @@ +{% extends 'syspanel/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="users" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Create User") %} +{% endblock page_header %} + +{% block syspanel_main %} +
+
+ {% include 'syspanel/users/_create_form.html' %} +
+ +
+

{% trans "Description"%}:

+

{% trans "From here you can create a new user and assign them to a tenant (aka project)."%}

+
+
 
+
+{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/users/index.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/users/index.html new file mode 100644 index 00000000..0890295f --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/users/index.html @@ -0,0 +1,45 @@ +{% extends 'syspanel/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="users" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% url horizon:syspanel:users:index as refresh_link %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Users") refresh_link=refresh_link searchable="true" %} +{% endblock page_header %} + +{% block syspanel_main %} + + + + + + + + + + {% for user in users %} + + + + + + + + {% endfor %} +
{% trans "ID"%}{% trans "Name"%}{% trans "Email"%}{% trans "Default Tenant"%}{% trans "Options"%}
{{user.id}}{% if not user.enabled %} (disabled){% endif %}{{user.name}}{{user.email}}{{user.tenantId}} +
    +
  • {% include "syspanel/users/_enable_disable.html" with form=user_enable_disable_form %}
  • +
  • {% include "syspanel/users/_delete.html" with form=user_delete_form %}
  • +
  • {% trans "Edit"%}
  • +
+
+ {% trans "Create New User"%} +
+ +{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/users/update.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/users/update.html new file mode 100644 index 00000000..6d9839fb --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/users/update.html @@ -0,0 +1,27 @@ +{% extends 'syspanel/base.html' %} +{%load i18n%} + +{% block sidebar %} + {% with current_sidebar="users" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {# to make searchable false, just remove it from the include statement #} + {% include "horizon/common/_page_header.html" with title=_("Update User") %} +{% endblock page_header %} + +{% block syspanel_main %} +
+
+ {% include 'syspanel/users/_update_form.html' %} +
+ +
+

{% trans "Description"%}:

+

{% trans "From here you can edit users by changing their usernames, emails, passwords, and tenants."%}

+
+
 
+
+{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/tenants/__init__.py b/horizon/horizon/dashboards/syspanel/tenants/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/syspanel/tenants/forms.py b/horizon/horizon/dashboards/syspanel/tenants/forms.py new file mode 100644 index 00000000..e84b63da --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/tenants/forms.py @@ -0,0 +1,183 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django import shortcuts +from django.conf import settings +from django.contrib import messages +from django.utils.translation import ugettext as _ +from openstackx.api import exceptions as api_exceptions + +from horizon import api +from horizon import forms + + +LOG = logging.getLogger(__name__) + + +class AddUser(forms.SelfHandlingForm): + user = forms.CharField() + tenant = forms.CharField() + + def handle(self, request, data): + try: + api.role_add_for_tenant_user( + request, + data['tenant'], + data['user'], + settings.OPENSTACK_KEYSTONE_DEFAULT_ROLE) + messages.success(request, + _('%(user)s was successfully added to %(tenant)s.') + % {"user": data['user'], "tenant": data['tenant']}) + except api_exceptions.ApiException, e: + messages.error(request, _('Unable to create user association: %s') + % (e.message)) + return shortcuts.redirect('horizon:syspanel:tenants:users', + tenant_id=data['tenant']) + + +class RemoveUser(forms.SelfHandlingForm): + user = forms.CharField() + tenant = forms.CharField() + + def handle(self, request, data): + try: + api.role_delete_for_tenant_user( + request, + data['tenant'], + data['user'], + settings.OPENSTACK_KEYSTONE_DEFAULT_ROLE) + messages.success(request, + _('%(user)s was successfully removed from %(tenant)s.') + % {"user": data['user'], "tenant": data['tenant']}) + except api_exceptions.ApiException, e: + messages.error(request, _('Unable to create tenant: %s') % + (e.message)) + return shortcuts.redirect('horizon:syspanel:tenants:users', + tenant_id=data['tenant']) + + +class CreateTenant(forms.SelfHandlingForm): + name = forms.CharField(label=_("Name")) + description = forms.CharField(widget=forms.widgets.Textarea(), + label=_("Description")) + enabled = forms.BooleanField(label=_("Enabled"), required=False, + initial=True) + + def handle(self, request, data): + try: + LOG.info('Creating tenant with name "%s"' % data['name']) + api.tenant_create(request, + data['name'], + data['description'], + data['enabled']) + messages.success(request, + _('%s was successfully created.') + % data['name']) + except api_exceptions.ApiException, e: + LOG.exception('ApiException while creating tenant\n' + 'Id: "%s", Description: "%s", Enabled "%s"' % + (data['name'], data['description'], data['enabled'])) + messages.error(request, _('Unable to create tenant: %s') % + (e.message)) + return shortcuts.redirect('horizon:syspanel:tenants:index') + + +class UpdateTenant(forms.SelfHandlingForm): + id = forms.CharField(label=_("ID"), + widget=forms.TextInput(attrs={'readonly': 'readonly'})) + name = forms.CharField(label=_("Name"), + widget=forms.TextInput(attrs={'readonly': 'readonly'})) + description = forms.CharField(widget=forms.widgets.Textarea(), + label=_("Description")) + enabled = forms.BooleanField(required=False, label=_("Enabled")) + + def handle(self, request, data): + try: + LOG.info('Updating tenant with id "%s"' % data['id']) + api.tenant_update(request, + data['id'], + data['name'], + data['description'], + data['enabled']) + messages.success(request, + _('%s was successfully updated.') + % data['name']) + except api_exceptions.ApiException, e: + LOG.exception('ApiException while updating tenant\n' + 'Id: "%s", Name: "%s", Description: "%s", Enabled "%s"' % + (data['id'], data['name'], + data['description'], data['enabled'])) + messages.error(request, + _('Unable to update tenant: %s') % e.message) + return shortcuts.redirect('horizon:syspanel:tenants:index') + + +class UpdateQuotas(forms.SelfHandlingForm): + tenant_id = forms.CharField(label=_("ID (name)"), + widget=forms.TextInput(attrs={'readonly': 'readonly'})) + metadata_items = forms.CharField(label=_("Metadata Items")) + injected_files = forms.CharField(label=_("Injected Files")) + injected_file_content_bytes = forms.CharField(label=_("Injected File " + "Content Bytes")) + cores = forms.CharField(label=_("VCPUs")) + instances = forms.CharField(label=_("Instances")) + volumes = forms.CharField(label=_("Volumes")) + gigabytes = forms.CharField(label=_("Gigabytes")) + ram = forms.CharField(label=_("RAM (in MB)")) + floating_ips = forms.CharField(label=_("Floating IPs")) + + def handle(self, request, data): + try: + api.admin_api(request).quota_sets.update(data['tenant_id'], + metadata_items=data['metadata_items'], + injected_file_content_bytes=data['injected_file_content_bytes'], + volumes=data['volumes'], + gigabytes=data['gigabytes'], + ram=int(data['ram']), + floating_ips=data['floating_ips'], + instances=data['instances'], + injected_files=data['injected_files'], + cores=data['cores'], + ) + messages.success(request, + _('Quotas for %s were successfully updated.') + % data['tenant_id']) + except api_exceptions.ApiException, e: + messages.error(request, + _('Unable to update quotas: %s') % e.message) + return shortcuts.redirect('horizon:syspanel:tenants:index') + + +class DeleteTenant(forms.SelfHandlingForm): + tenant_id = forms.CharField(required=True) + + def handle(self, request, data): + tenant_id = data['tenant_id'] + try: + api.tenant_delete(request, tenant_id) + messages.info(request, _('Successfully deleted tenant %(tenant)s.') + % {"tenant": tenant_id}) + except Exception, e: + LOG.exception("Error deleting tenant") + messages.error(request, + _("Error deleting tenant: %s") % e.message) + return shortcuts.redirect(request.build_absolute_uri()) diff --git a/horizon/horizon/dashboards/syspanel/tenants/panel.py b/horizon/horizon/dashboards/syspanel/tenants/panel.py new file mode 100644 index 00000000..692a93ee --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/tenants/panel.py @@ -0,0 +1,30 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import horizon +from horizon.dashboards.syspanel import dashboard + + +class Tenants(horizon.Panel): + name = "Tenants" + slug = 'tenants' + + +dashboard.Syspanel.register(Tenants) diff --git a/horizon/horizon/dashboards/syspanel/tenants/tests.py b/horizon/horizon/dashboards/syspanel/tenants/tests.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/syspanel/tenants/urls.py b/horizon/horizon/dashboards/syspanel/tenants/urls.py new file mode 100644 index 00000000..1e43b874 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/tenants/urls.py @@ -0,0 +1,29 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls.defaults import patterns, url + + +urlpatterns = patterns('horizon.dashboards.syspanel.tenants.views', + url(r'^$', 'index', name='index'), + url(r'^create$', 'create', name='create'), + url(r'^(?P[^/]+)/update/$', 'update', name='update'), + url(r'^(?P[^/]+)/users/$', 'users', name='users'), + url(r'^(?P[^/]+)/quotas/$', 'quotas', name='quotas')) diff --git a/horizon/horizon/dashboards/syspanel/tenants/views.py b/horizon/horizon/dashboards/syspanel/tenants/views.py new file mode 100644 index 00000000..d6cc2735 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/tenants/views.py @@ -0,0 +1,143 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django import shortcuts +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.utils.translation import ugettext as _ +from openstackx.api import exceptions as api_exceptions + +from horizon import api +from horizon.dashboards.syspanel.tenants.forms import (AddUser, RemoveUser, + CreateTenant, UpdateTenant, UpdateQuotas, DeleteTenant) + + +LOG = logging.getLogger(__name__) + + +@login_required +def index(request): + form, handled = DeleteTenant.maybe_handle(request) + if handled: + return handled + + tenant_delete_form = DeleteTenant() + + tenants = [] + try: + tenants = api.tenant_list(request) + except api_exceptions.ApiException, e: + LOG.exception('ApiException while getting tenant list') + messages.error(request, _('Unable to get tenant info: %s') % e.message) + tenants.sort(key=lambda x: x.id, reverse=True) + return shortcuts.render(request, + 'syspanel/tenants/index.html', { + 'tenants': tenants, + 'tenant_delete_form': tenant_delete_form}) + + +@login_required +def create(request): + form, handled = CreateTenant.maybe_handle(request) + if handled: + return handled + + return shortcuts.render(request, + 'syspanel/tenants/create.html', { + 'form': form}) + + +@login_required +def update(request, tenant_id): + form, handled = UpdateTenant.maybe_handle(request) + if handled: + return handled + + if request.method == 'GET': + try: + tenant = api.tenant_get(request, tenant_id) + form = UpdateTenant(initial={'id': tenant.id, + 'name': tenant.name, + 'description': tenant.description, + 'enabled': tenant.enabled}) + except api_exceptions.ApiException, e: + LOG.exception('Error fetching tenant with id "%s"' % tenant_id) + messages.error(request, + _('Unable to update tenant: %s') % e.message) + return shortcuts.redirect('horizon:syspanel:tenants:index') + + return shortcuts.render(request, + 'syspanel/tenants/update.html', { + 'form': form}) + + +@login_required +def users(request, tenant_id): + for f in (AddUser, RemoveUser,): + form, handled = f.maybe_handle(request) + if handled: + return handled + + add_user_form = AddUser() + remove_user_form = RemoveUser() + + users = api.user_list(request, tenant_id) + all_users = api.user_list(request) + user_ids = [u.id for u in users] + new_users = [u for u in all_users if not u.id in user_ids] + return shortcuts.render(request, + 'syspanel/tenants/users.html', { + 'add_user_form': add_user_form, + 'remove_user_form': remove_user_form, + 'tenant_id': tenant_id, + 'users': users, + 'new_users': new_users}) + + +@login_required +def quotas(request, tenant_id): + for f in (UpdateQuotas,): + form, handled = f.maybe_handle(request) + if handled: + return handled + + quotas = api.admin_api(request).quota_sets.get(tenant_id) + quota_set = { + 'tenant_id': quotas.id, + 'metadata_items': quotas.metadata_items, + 'injected_file_content_bytes': quotas.injected_file_content_bytes, + 'volumes': quotas.volumes, + 'gigabytes': quotas.gigabytes, + 'ram': int(quotas.ram), + 'floating_ips': quotas.floating_ips, + 'instances': quotas.instances, + 'injected_files': quotas.injected_files, + 'cores': quotas.cores, + } + form = UpdateQuotas(initial=quota_set) + + return shortcuts.render(request, + 'syspanel/tenants/quotas.html', { + 'form': form, + 'tenant_id': tenant_id, + 'quotas': quotas}) diff --git a/horizon/horizon/dashboards/syspanel/users/__init__.py b/horizon/horizon/dashboards/syspanel/users/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/dashboards/syspanel/users/forms.py b/horizon/horizon/dashboards/syspanel/users/forms.py new file mode 100644 index 00000000..4033c37e --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/users/forms.py @@ -0,0 +1,102 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django import shortcuts +from django.contrib import messages +from django.utils.translation import ugettext as _ +from openstackx.api import exceptions as api_exceptions + +from horizon import api +from horizon import forms + + +LOG = logging.getLogger(__name__) + + +class UserForm(forms.Form): + def __init__(self, *args, **kwargs): + tenant_list = kwargs.pop('tenant_list', None) + super(UserForm, self).__init__(*args, **kwargs) + self.fields['tenant_id'].choices = [[tenant.id, tenant.name] + for tenant in tenant_list] + + name = forms.CharField(label=_("Name")) + email = forms.CharField(label=_("Email")) + password = forms.CharField(label=_("Password"), + widget=forms.PasswordInput(render_value=False), + required=False) + tenant_id = forms.ChoiceField(label=_("Primary Tenant")) + + +class UserUpdateForm(forms.Form): + def __init__(self, *args, **kwargs): + tenant_list = kwargs.pop('tenant_list', None) + super(UserUpdateForm, self).__init__(*args, **kwargs) + self.fields['tenant_id'].choices = [[tenant.id, tenant.name] + for tenant in tenant_list] + + id = forms.CharField(label=_("ID"), + widget=forms.TextInput(attrs={'readonly': 'readonly'})) + # FIXME: keystone doesn't return the username from a get API call. + #name = forms.CharField(label=_("Name")) + email = forms.CharField(label=_("Email")) + password = forms.CharField(label=_("Password"), + widget=forms.PasswordInput(render_value=False), + required=False) + tenant_id = forms.ChoiceField(label=_("Primary Tenant")) + + +class UserDeleteForm(forms.SelfHandlingForm): + user = forms.CharField(required=True) + + def handle(self, request, data): + user_id = data['user'] + LOG.info('Deleting user with id "%s"' % user_id) + api.user_delete(request, user_id) + messages.info(request, _('%(user)s was successfully deleted.') + % {"user": user_id}) + return shortcuts.redirect(request.build_absolute_uri()) + + +class UserEnableDisableForm(forms.SelfHandlingForm): + id = forms.CharField(label=_("ID (username)"), widget=forms.HiddenInput()) + enabled = forms.ChoiceField(label=_("enabled"), widget=forms.HiddenInput(), + choices=[[c, c] + for c in ("disable", "enable")]) + + def handle(self, request, data): + user_id = data['id'] + enabled = data['enabled'] == "enable" + + try: + api.user_update_enabled(request, user_id, enabled) + messages.info(request, + _("User %(user)s %(state)s") % + {"user": user_id, + "state": "enabled" if enabled else "disabled"}) + except api_exceptions.ApiException: + messages.error(request, + _("Unable to %(state)s user %(user)s") % + {"state": "enable" if enabled else "disable", + "user": user_id}) + + return shortcuts.redirect(request.build_absolute_uri()) diff --git a/horizon/horizon/dashboards/syspanel/users/panel.py b/horizon/horizon/dashboards/syspanel/users/panel.py new file mode 100644 index 00000000..7a7380e3 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/users/panel.py @@ -0,0 +1,30 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import horizon +from horizon.dashboards.syspanel import dashboard + + +class Users(horizon.Panel): + name = "Users" + slug = 'users' + + +dashboard.Syspanel.register(Users) diff --git a/horizon/horizon/dashboards/syspanel/users/tests.py b/horizon/horizon/dashboards/syspanel/users/tests.py new file mode 100644 index 00000000..7b5273e0 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/users/tests.py @@ -0,0 +1,111 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.core.urlresolvers import reverse +from mox import IgnoreArg +from openstackx.api import exceptions as api_exceptions + +from horizon import api +from horizon import test + + +USERS_INDEX_URL = reverse('horizon:syspanel:users:index') + + +class UsersViewTests(test.BaseAdminViewTests): + def setUp(self): + super(UsersViewTests, self).setUp() + + self.user = self.mox.CreateMock(api.User) + self.user.enabled = True + self.user.id = self.TEST_USER + self.user.roles = self.TEST_ROLES + self.user.tenantId = self.TEST_TENANT + + self.users = [self.user] + + def test_index(self): + self.mox.StubOutWithMock(api, 'user_list') + api.user_list(IgnoreArg()).AndReturn(self.users) + + self.mox.ReplayAll() + + res = self.client.get(USERS_INDEX_URL) + + self.assertTemplateUsed(res, 'syspanel/users/index.html') + self.assertItemsEqual(res.context['users'], self.users) + + self.mox.VerifyAll() + + def test_enable_user(self): + OTHER_USER = 'otherUser' + formData = {'method': 'UserEnableDisableForm', + 'id': OTHER_USER, + 'enabled': 'enable'} + + self.mox.StubOutWithMock(api, 'user_update_enabled') + api.user_update_enabled(IgnoreArg(), OTHER_USER, True).AndReturn( + self.mox.CreateMock(api.User)) + + self.mox.ReplayAll() + + res = self.client.post(USERS_INDEX_URL, formData) + + self.assertRedirectsNoFollow(res, USERS_INDEX_URL) + + self.mox.VerifyAll() + + def test_disable_user(self): + OTHER_USER = 'otherUser' + formData = {'method': 'UserEnableDisableForm', + 'id': OTHER_USER, + 'enabled': 'disable'} + + self.mox.StubOutWithMock(api, 'user_update_enabled') + api.user_update_enabled(IgnoreArg(), OTHER_USER, False).AndReturn( + self.mox.CreateMock(api.User)) + + self.mox.ReplayAll() + + res = self.client.post(USERS_INDEX_URL, formData) + + self.assertRedirectsNoFollow(res, USERS_INDEX_URL) + + self.mox.VerifyAll() + + def test_enable_disable_user_exception(self): + OTHER_USER = 'otherUser' + formData = {'method': 'UserEnableDisableForm', + 'id': OTHER_USER, + 'enabled': 'enable'} + + self.mox.StubOutWithMock(api, 'user_update_enabled') + api_exception = api_exceptions.ApiException('apiException', + message='apiException') + api.user_update_enabled(IgnoreArg(), + OTHER_USER, True).AndRaise(api_exception) + + self.mox.ReplayAll() + + res = self.client.post(USERS_INDEX_URL, formData) + + self.assertRedirectsNoFollow(res, USERS_INDEX_URL) + + self.mox.VerifyAll() diff --git a/horizon/horizon/dashboards/syspanel/users/urls.py b/horizon/horizon/dashboards/syspanel/users/urls.py new file mode 100644 index 00000000..2be685f9 --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/users/urls.py @@ -0,0 +1,26 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls.defaults import patterns, url + +urlpatterns = patterns('horizon.dashboards.syspanel.users.views', + url(r'^users/$', 'index', name='index'), + url(r'^(?P[^/]+)/update/$', 'update', name='update'), + url(r'^users/create/$', 'create', name='create')) diff --git a/horizon/horizon/dashboards/syspanel/users/views.py b/horizon/horizon/dashboards/syspanel/users/views.py new file mode 100644 index 00000000..9a1dfdad --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/users/views.py @@ -0,0 +1,163 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django import shortcuts +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.utils.translation import ugettext as _ +from openstackx.api import exceptions as api_exceptions + +from horizon import api +from horizon.dashboards.syspanel.users.forms import (UserForm, UserUpdateForm, + UserDeleteForm, UserEnableDisableForm) + + +LOG = logging.getLogger(__name__) + + +@login_required +def index(request): + for f in (UserDeleteForm, UserEnableDisableForm): + form, handled = f.maybe_handle(request) + if handled: + return handled + + users = [] + try: + users = api.user_list(request) + except api_exceptions.ApiException, e: + messages.error(request, _('Unable to list users: %s') % + e.message) + + user_delete_form = UserDeleteForm() + toggle_form = UserEnableDisableForm() + + return shortcuts.render(request, + 'syspanel/users/index.html', { + 'users': users, + 'user_delete_form': user_delete_form, + 'user_enable_disable_form': toggle_form}) + + +@login_required +def update(request, user_id): + if request.method == "POST": + tenants = api.tenant_list(request) + form = UserUpdateForm(request.POST, tenant_list=tenants) + if form.is_valid(): + user = form.clean() + updated = [] + if user['email']: + updated.append('email') + api.user_update_email(request, user['id'], user['email']) + if user['password']: + updated.append('password') + api.user_update_password(request, user['id'], user['password']) + if user['tenant_id']: + updated.append('tenant') + api.user_update_tenant(request, user['id'], user['tenant_id']) + messages.success(request, + _('Updated %(attrib)s for %(user)s.') % + {"attrib": ', '.join(updated), "user": user_id}) + return shortcuts.redirect('horizon:syspanel:users:index') + else: + # TODO add better error management + messages.error(request, + _('Unable to update user, please try again.')) + + return shortcuts.render(request, + 'syspanel/users/update.html', { + 'form': form, + 'user_id': user_id}) + + else: + user = api.user_get(request, user_id) + tenants = api.tenant_list(request) + form = UserUpdateForm(tenant_list=tenants, + initial={'id': user_id, + 'tenant_id': getattr(user, + 'tenantId', + None), + 'email': getattr(user, 'email', '')}) + return shortcuts.render(request, + 'syspanel/users/update.html', { + 'form': form, + 'user_id': user_id}) + + +@login_required +def create(request): + try: + tenants = api.tenant_list(request) + except api_exceptions.ApiException, e: + messages.error(request, _('Unable to retrieve tenant list: %s') % + e.message) + return shortcuts.redirect('horizon:syspanel:users:index') + + if request.method == "POST": + form = UserForm(request.POST, tenant_list=tenants) + if form.is_valid(): + user = form.clean() + # TODO Make this a real request + try: + LOG.info('Creating user with name "%s"' % user['name']) + new_user = api.user_create(request, + user['name'], + user['email'], + user['password'], + user['tenant_id'], + True) + messages.success(request, + _('User "%s" was successfully created.') + % user['name']) + try: + api.role_add_for_tenant_user( + request, user['tenant_id'], new_user.id, + settings.OPENSTACK_KEYSTONE_DEFAULT_ROLE) + except Exception, e: + LOG.exception('Exception while assigning \ + role to new user: %s' % new_user.id) + messages.error(request, + _('Error assigning role to user: %s') + % e.message) + + return shortcuts.redirect('horizon:syspanel:users:index') + + except Exception, e: + LOG.exception('Exception while creating user\n' + 'name: "%s", email: "%s", tenant_id: "%s"' % + (user['name'], user['email'], user['tenant_id'])) + messages.error(request, + _('Error creating user: %s') + % e.message) + return shortcuts.redirect('horizon:syspanel:users:index') + else: + return shortcuts.render(request, + 'syspanel/users/create.html', { + 'form': form}) + + else: + form = UserForm(tenant_list=tenants) + return shortcuts.render(request, + 'syspanel/users/create.html', { + 'form': form}) diff --git a/horizon/horizon/decorators.py b/horizon/horizon/decorators.py new file mode 100644 index 00000000..c5dc7bf2 --- /dev/null +++ b/horizon/horizon/decorators.py @@ -0,0 +1,86 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 CRS4 +# +# 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. + +""" +General-purpose decorators for use with Horizon. +""" +import functools +import logging + +from django import http +from django.utils.decorators import available_attrs + +from horizon.exceptions import NotAuthorized + + +def _current_component(view_func, dashboard=None, panel=None): + """ Sets the currently-active dashboard and/or panel on the request. """ + @functools.wraps(view_func, assigned=available_attrs(view_func)) + def dec(request, *args, **kwargs): + if dashboard: + request.horizon['dashboard'] = dashboard + if panel: + request.horizon['panel'] = panel + return view_func(request, *args, **kwargs) + return dec + + +def require_roles(view_func, required): + """ Enforces role-based access controls. + + :param list required: A tuple of role names, all of which the request user + must possess in order access the decorated view. + + Example usage:: + + from horizon.decorators import require_roles + + + @require_roles(['admin', 'member']) + def my_view(request): + ... + + Raises a :exc:`~horizon.exceptions.NotAuthorized` exception if the + requirements are not met. + """ + # We only need to check each role once for a view, so we'll use a set + current_roles = getattr(view_func, '_required_roles', set([])) + view_func._required_roles = current_roles | set(required) + + @functools.wraps(view_func, assigned=available_attrs(view_func)) + def dec(request, *args, **kwargs): + if request.user.is_authenticated(): + roles = set([role['name'].lower() for role in request.user.roles]) + # set operator <= tests that all members of set 1 are in set 2 + if view_func._required_roles <= set(roles): + return view_func(request, *args, **kwargs) + raise NotAuthorized("You are not authorized to access %s" + % request.path) + + # If we don't have any roles, just return the original view. + if required: + return dec + else: + return view_func + + +def enforce_admin_access(view_func): + """ Marks a view as requiring the ``"admin"`` role for access. """ + return require_roles(view_func, ('admin',)) diff --git a/horizon/horizon/exceptions.py b/horizon/horizon/exceptions.py new file mode 100644 index 00000000..a2c621f2 --- /dev/null +++ b/horizon/horizon/exceptions.py @@ -0,0 +1,40 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" Exceptions raised by the Horizon code. """ + + +class NotAuthorized(Exception): + """ + Raised whenever a user attempts to access a resource which they do not + have role-based access to (such as when failing the + :func:`~horizon.decorators.require_roles` decorator). + + The included :class:`~horizon.middleware.HorizonMiddleware` catches + ``NotAuthorized`` and handles it gracefully by displaying an error + message and redirecting the user to a login page. + """ + pass + + +class ServiceCatalogException(Exception): + """ + Raised when a requested service is not available in the ``ServiceCatalog`` + returned by Keystone. + """ + def __init__(self, service_name): + message = 'Invalid service catalog service: %s' % service_name + super(ServiceCatalogException, self).__init__(message) diff --git a/horizon/horizon/forms.py b/horizon/horizon/forms.py new file mode 100644 index 00000000..dda6eafd --- /dev/null +++ b/horizon/horizon/forms.py @@ -0,0 +1,220 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime +import logging +import re + +from django import utils +from django.conf import settings +from django.contrib import messages +from django.forms import * +from django.forms import widgets +from django.utils import dates +from django.utils import safestring +from django.utils import formats + +from horizon import exceptions + + +LOG = logging.getLogger(__name__) +RE_DATE = re.compile(r'(\d{4})-(\d\d?)-(\d\d?)$') + + +class SelectDateWidget(widgets.Widget): + """ + A Widget that splits date input into three + {% endblock %} + + diff --git a/horizon/horizon/templates/horizon/auth/_switch.html b/horizon/horizon/templates/horizon/auth/_switch.html new file mode 100644 index 00000000..7be5450a --- /dev/null +++ b/horizon/horizon/templates/horizon/auth/_switch.html @@ -0,0 +1,17 @@ +{% load i18n %} +
+ {% csrf_token %} +
+ {% for hidden in form.hidden_fields %} + {{ hidden }} + {% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + {% block submit %} + + {% endblock %} +
+
diff --git a/horizon/horizon/templates/horizon/common/_page_header.html b/horizon/horizon/templates/horizon/common/_page_header.html new file mode 100644 index 00000000..28199218 --- /dev/null +++ b/horizon/horizon/templates/horizon/common/_page_header.html @@ -0,0 +1,21 @@ +{% load i18n %} +{% block page_header %} + +{% endblock %} diff --git a/horizon/horizon/templates/horizon/common/_sidebar.html b/horizon/horizon/templates/horizon/common/_sidebar.html new file mode 100644 index 00000000..a9b06ada --- /dev/null +++ b/horizon/horizon/templates/horizon/common/_sidebar.html @@ -0,0 +1,5 @@ +{% load horizon i18n %} + + diff --git a/horizon/horizon/templates/horizon/common/_sidebar_module.html b/horizon/horizon/templates/horizon/common/_sidebar_module.html new file mode 100644 index 00000000..2b9739ce --- /dev/null +++ b/horizon/horizon/templates/horizon/common/_sidebar_module.html @@ -0,0 +1,10 @@ +{% for module in modules %} +

{{ module.title }}

+ + +{% endfor %} + diff --git a/horizon/horizon/templates/horizon/common/instances/_reboot.html b/horizon/horizon/templates/horizon/common/instances/_reboot.html new file mode 100644 index 00000000..2fa3e537 --- /dev/null +++ b/horizon/horizon/templates/horizon/common/instances/_reboot.html @@ -0,0 +1,9 @@ +{% load i18n %} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{ hidden }} + {% endfor %} + + +
diff --git a/horizon/horizon/templates/horizon/common/instances/_terminate.html b/horizon/horizon/templates/horizon/common/instances/_terminate.html new file mode 100644 index 00000000..054c64ea --- /dev/null +++ b/horizon/horizon/templates/horizon/common/instances/_terminate.html @@ -0,0 +1,9 @@ +{% load i18n %} +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{ hidden }} + {% endfor %} + + +
diff --git a/horizon/horizon/templatetags/__init__.py b/horizon/horizon/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/templatetags/branding.py b/horizon/horizon/templatetags/branding.py new file mode 100644 index 00000000..d62c79bd --- /dev/null +++ b/horizon/horizon/templatetags/branding.py @@ -0,0 +1,62 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Template tags for customizing Horizon. +""" + +from django import template +from django.conf import settings + + +register = template.Library() + + +class SiteBrandingNode(template.Node): + def render(self, context): + return settings.SITE_BRANDING + + +@register.tag +def site_branding(parser, token): + return SiteBrandingNode() + + +@register.tag +def site_title(parser, token): + return settings.SITE_BRANDING + + +# TODO(jeffjapan): This is just an assignment tag version of the above, replace +# when the dashboard is upgraded to a django version that +# supports the @assignment_tag decorator syntax instead. +class SaveBrandingNode(template.Node): + def __init__(self, var_name): + self.var_name = var_name + + def render(self, context): + context[self.var_name] = settings.SITE_BRANDING + return "" + + +@register.tag +def save_site_branding(parser, token): + tagname = token.contents.split() + return SaveBrandingNode(tagname[-1]) diff --git a/horizon/horizon/templatetags/horizon.py b/horizon/horizon/templatetags/horizon.py new file mode 100644 index 00000000..9df91a80 --- /dev/null +++ b/horizon/horizon/templatetags/horizon.py @@ -0,0 +1,79 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import + +import copy + +from django import template + +from horizon.base import Horizon + + +register = template.Library() + + +@register.filter +def can_haz(user, component): + """ Checks if the given user has the necessary roles for the component. """ + if hasattr(user, 'roles'): + user_roles = set([role['name'].lower() for role in user.roles]) + else: + user_roles = set([]) + if set(getattr(component, 'roles', [])) <= user_roles: + return True + return False + + +@register.inclusion_tag('horizon/_nav_list.html', takes_context=True) +def horizon_main_nav(context): + """ Generates top-level dashboard navigation entries. """ + if 'request' not in context: + return {} + dashboards = [] + for dash in Horizon.get_dashboards(): + if callable(dash.nav) and dash.nav(context): + dashboards.append(dash) + elif dash.nav: + dashboards.append(dash) + return {'components': dashboards, + 'user': context['request'].user, + 'current': context['request'].horizon['dashboard'].slug} + + +@register.inclusion_tag('horizon/_subnav_list.html', takes_context=True) +def horizon_dashboard_nav(context): + """ Generates sub-navigation entries for the current dashboard. """ + if 'request' not in context: + return {} + dashboard = context['request'].horizon['dashboard'] + if type(dashboard.panels) is dict: + panels = copy.copy(dashboard.get_panels()) + else: + panels = {dashboard.name: dashboard.get_panels()} + + for heading, items in panels.iteritems(): + temp_panels = [] + for panel in items: + if callable(panel.nav) and panel.nav(context): + temp_panels.append(panel) + elif not callable(panel.nav) and panel.nav: + temp_panels.append(panel) + panels[heading] = temp_panels + non_empty_panels = dict([(k, v) for k, v in panels.items() if len(v) > 0]) + return {'components': non_empty_panels, + 'user': context['request'].user, + 'current': context['request'].horizon['panel'].slug} diff --git a/horizon/horizon/templatetags/parse_date.py b/horizon/horizon/templatetags/parse_date.py new file mode 100644 index 00000000..6adff07b --- /dev/null +++ b/horizon/horizon/templatetags/parse_date.py @@ -0,0 +1,73 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Template tags for parsing date strings. +""" + +import datetime +from django import template +from dateutil import tz + + +register = template.Library() + + +def _parse_datetime(dtstr): + fmts = ["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%d %H:%M:%S.%f", + "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"] + for fmt in fmts: + try: + return datetime.datetime.strptime(dtstr, fmt) + except: + pass + + +class ParseDateNode(template.Node): + def render(self, context): + """Turn an iso formatted time back into a datetime.""" + if context == None: + return "None" + date_obj = _parse_datetime(context) + return date_obj.strftime("%m/%d/%y at %H:%M:%S") + + +@register.filter(name='parse_date') +def parse_date(value): + return ParseDateNode().render(value) + + +@register.filter(name='parse_datetime') +def parse_datetime(value): + return _parse_datetime(value) + + +@register.filter(name='parse_local_datetime') +def parse_local_datetime(value): + dt = _parse_datetime(value) + local_tz = tz.tzlocal() + utc = tz.gettz('UTC') + local_dt = dt.replace(tzinfo=utc) + return local_dt.astimezone(local_tz) + + +@register.filter(name='pretty_date') +def pretty_date(value): + return value.strftime("%d/%m/%y at %H:%M:%S") diff --git a/horizon/horizon/templatetags/sizeformat.py b/horizon/horizon/templatetags/sizeformat.py new file mode 100644 index 00000000..6e7e9ecc --- /dev/null +++ b/horizon/horizon/templatetags/sizeformat.py @@ -0,0 +1,76 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Template tags for displaying sizes +""" + +import datetime +from django import template +from django.utils import translation +from django.utils import formats + + +register = template.Library() + + +def int_format(value): + return int(value) + + +def float_format(value): + return formats.number_format(round(value, 1), 0) + + +def filesizeformat(bytes, filesize_number_format): + try: + bytes = float(bytes) + except (TypeError, ValueError, UnicodeDecodeError): + return translation.ungettext("%(size)d byte", + "%(size)d bytes", 0) % {'size': 0} + + if bytes < 1024: + return translation.ungettext("%(size)d", + "%(size)d", bytes) % {'size': bytes} + if bytes < 1024 * 1024: + return translation.ugettext("%s KB") % \ + filesize_number_format(bytes / 1024) + if bytes < 1024 * 1024 * 1024: + return translation.ugettext("%s MB") % \ + filesize_number_format(bytes / (1024 * 1024)) + if bytes < 1024 * 1024 * 1024 * 1024: + return translation.ugettext("%s GB") % \ + filesize_number_format(bytes / (1024 * 1024 * 1024)) + if bytes < 1024 * 1024 * 1024 * 1024 * 1024: + return translation.ugettext("%s TB") % \ + filesize_number_format(bytes / (1024 * 1024 * 1024 * 1024)) + return translation.ugettext("%s PB") % \ + filesize_number_format(bytes / (1024 * 1024 * 1024 * 1024 * 1024)) + + +@register.filter(name='mbformat') +def mbformat(mb): + return filesizeformat(mb * 1024 * 1024, int_format).replace(' ', '') + + +@register.filter(name='diskgbformat') +def diskgbformat(gb): + return filesizeformat(gb * 1024 * 1024 * 1024, + float_format).replace(' ', '') diff --git a/horizon/horizon/templatetags/swift_paging.py b/horizon/horizon/templatetags/swift_paging.py new file mode 100644 index 00000000..5c79c4c1 --- /dev/null +++ b/horizon/horizon/templatetags/swift_paging.py @@ -0,0 +1,35 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django import template +from django.conf import settings +from django.utils import http + +register = template.Library() + + +@register.inclusion_tag('nova/objects/_paging.html') +def object_paging(objects): + marker = None + if objects and not \ + len(objects) < getattr(settings, 'SWIFT_PAGINATE_LIMIT', 10000): + last_object = objects[-1] + marker = http.urlquote_plus(last_object.name) + return {'marker': marker} diff --git a/horizon/horizon/templatetags/truncate_filter.py b/horizon/horizon/templatetags/truncate_filter.py new file mode 100644 index 00000000..c8b4e4a3 --- /dev/null +++ b/horizon/horizon/templatetags/truncate_filter.py @@ -0,0 +1,35 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Template tags for truncating strings. +""" + +from django import template + +register = template.Library() + + +@register.filter("truncate") +def truncate(value, size): + if len(value) > size and size > 3: + return value[0:(size - 3)] + '...' + else: + return value[0:size] diff --git a/horizon/horizon/test.py b/horizon/horizon/test.py new file mode 100644 index 00000000..97f512af --- /dev/null +++ b/horizon/horizon/test.py @@ -0,0 +1,212 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime + +from django import http +from django import shortcuts +from django import test as django_test +from django import template as django_template +from django.conf import settings +import mox + +from horizon import context_processors +from horizon import middleware +from horizon import users + + +def time(): + '''Overrideable version of datetime.datetime.today''' + if time.override_time: + return time.override_time + return datetime.time() + +time.override_time = None + + +def today(): + '''Overridable version of datetime.datetime.today''' + if today.override_time: + return today.override_time + return datetime.datetime.today() + +today.override_time = None + + +def utcnow(): + '''Overridable version of datetime.datetime.utcnow''' + if utcnow.override_time: + return utcnow.override_time + return datetime.datetime.utcnow() + +utcnow.override_time = None + + +class TestCase(django_test.TestCase): + TEST_STAFF_USER = 'staffUser' + TEST_TENANT = '1' + TEST_TENANT_NAME = 'aTenant' + TEST_TOKEN = 'aToken' + TEST_USER = 'test' + TEST_ROLES = [{'name': 'admin', 'id': '1'}] + + TEST_SERVICE_CATALOG = [ + {"endpoints": [{ + "adminURL": "http://cdn.admin-nets.local:8774/v1.0", + "region": "RegionOne", + "internalURL": "http://127.0.0.1:8774/v1.0", + "publicURL": "http://cdn.admin-nets.local:8774/v1.0/"}], + "type": "nova_compat", + "name": "nova_compat"}, + {"endpoints": [{ + "adminURL": "http://nova/novapi/admin", + "region": "RegionOne", + "internalURL": "http://nova/novapi/internal", + "publicURL": "http://nova/novapi/public"}], + "type": "compute", + "name": "nova"}, + {"endpoints": [{ + "adminURL": "http://glance/glanceapi/admin", + "region": "RegionOne", + "internalURL": "http://glance/glanceapi/internal", + "publicURL": "http://glance/glanceapi/public"}], + "type": "image", + "name": "glance"}, + {"endpoints": [{ + "adminURL": "http://cdn.admin-nets.local:35357/v2.0", + "region": "RegionOne", + "internalURL": "http://127.0.0.1:5000/v2.0", + "publicURL": "http://cdn.admin-nets.local:5000/v2.0"}], + "type": "identity", + "name": "identity"}, + {"endpoints": [{ + "adminURL": "http://swift/swiftapi/admin", + "region": "RegionOne", + "internalURL": "http://swift/swiftapi/internal", + "publicURL": "http://swift/swiftapi/public"}], + "type": "object-store", + "name": "swift"}] + + def setUp(self): + self.mox = mox.Mox() + + context_dict = {'tenants': [], + 'object_store_configured': False, + 'network_configured': False} + + self._real_horizon_context_processor = context_processors.horizon + context_processors.horizon = lambda request: context_dict + + self._real_get_user_from_request = users.get_user_from_request + self.setActiveUser(token=self.TEST_TOKEN, + username=self.TEST_USER, + tenant_id=self.TEST_TENANT, + service_catalog=self.TEST_SERVICE_CATALOG) + self.request = http.HttpRequest() + middleware.HorizonMiddleware().process_request(self.request) + + def tearDown(self): + self.mox.UnsetStubs() + context_processors.horizon = self._real_horizon_context_processor + users.get_user_from_request = self._real_get_user_from_request + + def setActiveUser(self, token=None, username=None, tenant_id=None, + service_catalog=None, tenant_name=None, roles=None): + users.get_user_from_request = lambda x: \ + users.User(token=token, + user=username, + tenant_id=tenant_id, + service_catalog=service_catalog) + + def override_times(self): + now = datetime.datetime.utcnow() + time.override_time = \ + datetime.time(now.hour, now.minute, now.second) + today.override_time = datetime.date(now.year, now.month, now.day) + utcnow.override_time = now + + return now + + def reset_times(self): + time.override_time = None + today.override_time = None + utcnow.override_time = None + + +def fake_render_to_response(template_name, context, context_instance=None, + mimetype='text/html'): + """Replacement for render_to_response so that views can be tested + without having to stub out templates that belong in the frontend + implementation. + + Should be able to be tested using the django unit test assertions like a + normal render_to_response return value can be. + """ + class Template(object): + def __init__(self, name): + self.name = name + + if context_instance is None: + context_instance = django_template.Context(context) + else: + context_instance.update(context) + + resp = http.HttpResponse() + template = Template(template_name) + + resp.write('

' + 'This is a fake httpresponse for testing purposes only' + '

') + + # Allows django.test.client to populate fields on the response object + django_test.signals.template_rendered.send(template, template=template, + context=context_instance) + + return resp + + +class BaseViewTests(TestCase): + """ + Base class for view based unit tests. + """ + def setUp(self): + super(BaseViewTests, self).setUp() + self._real_render_to_response = shortcuts.render_to_response + shortcuts.render_to_response = fake_render_to_response + + def tearDown(self): + super(BaseViewTests, self).tearDown() + shortcuts.render_to_response = self._real_render_to_response + + def assertRedirectsNoFollow(self, response, expected_url): + self.assertEqual(response._headers['location'], + ('Location', settings.TESTSERVER + expected_url)) + self.assertEqual(response.status_code, 302) + + +class BaseAdminViewTests(BaseViewTests): + def setActiveUser(self, token=None, username=None, tenant_id=None, + service_catalog=None, tenant_name=None, roles=None): + users.get_user_from_request = lambda x: \ + users.User(token=self.TEST_TOKEN, + user=self.TEST_USER, + tenant_id=self.TEST_TENANT, + service_catalog=self.TEST_SERVICE_CATALOG, + roles=self.TEST_ROLES) diff --git a/horizon/horizon/tests/__init__.py b/horizon/horizon/tests/__init__.py new file mode 100644 index 00000000..31e69bd4 --- /dev/null +++ b/horizon/horizon/tests/__init__.py @@ -0,0 +1,21 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from horizon.tests.testsettings import * diff --git a/horizon/horizon/tests/api_tests/__init__.py b/horizon/horizon/tests/api_tests/__init__.py new file mode 100644 index 00000000..b3fc6e5b --- /dev/null +++ b/horizon/horizon/tests/api_tests/__init__.py @@ -0,0 +1,29 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from horizon.tests.api_tests.base import (APIResourceWrapperTests, + APIDictWrapperTests, ApiHelperTests) +from horizon.tests.api_tests.glance import GlanceApiTests, ImageWrapperTests +from horizon.tests.api_tests.keystone import (KeystoneAdminApiTests, + TokenApiTests, RoleAPITests, TenantAPITests, UserAPITests) +from horizon.tests.api_tests.nova import (ServerWrapperTests, + NovaAdminApiTests, ComputeApiTests, ExtrasApiTests, VolumeTests, + APIExtensionTests) +from horizon.tests.api_tests.swift import SwiftApiTests diff --git a/horizon/horizon/tests/api_tests/base.py b/horizon/horizon/tests/api_tests/base.py new file mode 100644 index 00000000..71daae4f --- /dev/null +++ b/horizon/horizon/tests/api_tests/base.py @@ -0,0 +1,112 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import + +from django import http +from django.conf import settings +from mox import IsA + +from horizon import exceptions +from horizon.tests.api_tests.utils import * + + +# Wrapper classes that only define _attrs don't need extra testing. +class APIResourceWrapperTests(test.TestCase): + def test_get_attribute(self): + resource = APIResource.get_instance() + self.assertEqual(resource.foo, 'foo') + + def test_get_invalid_attribute(self): + resource = APIResource.get_instance() + self.assertNotIn('missing', resource._attrs, + msg="Test assumption broken. Find new missing attribute") + with self.assertRaises(AttributeError): + resource.missing + + def test_get_inner_missing_attribute(self): + resource = APIResource.get_instance() + with self.assertRaises(AttributeError): + resource.baz + + +class APIDictWrapperTests(test.TestCase): + # APIDict allows for both attribute access and dictionary style [element] + # style access. Test both + def test_get_item(self): + resource = APIDict.get_instance() + self.assertEqual(resource.foo, 'foo') + self.assertEqual(resource['foo'], 'foo') + + def test_get_invalid_item(self): + resource = APIDict.get_instance() + self.assertNotIn('missing', resource._attrs, + msg="Test assumption broken. Find new missing attribute") + with self.assertRaises(AttributeError): + resource.missing + with self.assertRaises(KeyError): + resource['missing'] + + def test_get_inner_missing_attribute(self): + resource = APIDict.get_instance() + with self.assertRaises(AttributeError): + resource.baz + with self.assertRaises(KeyError): + resource['baz'] + + def test_get_with_default(self): + resource = APIDict.get_instance() + + self.assertEqual(resource.get('foo'), 'foo') + + self.assertIsNone(resource.get('baz')) + + self.assertEqual('retValue', resource.get('baz', 'retValue')) + + +class ApiHelperTests(test.TestCase): + """ Tests for functions that don't use one of the api objects """ + + def test_url_for(self): + GLANCE_URL = 'http://glance/glanceapi/' + NOVA_URL = 'http://nova/novapi/' + + url = api.url_for(self.request, 'image') + self.assertEqual(url, GLANCE_URL + 'internal') + + url = api.url_for(self.request, 'image', admin=False) + self.assertEqual(url, GLANCE_URL + 'internal') + + url = api.url_for(self.request, 'image', admin=True) + self.assertEqual(url, GLANCE_URL + 'admin') + + url = api.url_for(self.request, 'compute') + self.assertEqual(url, NOVA_URL + 'internal') + + url = api.url_for(self.request, 'compute', admin=False) + self.assertEqual(url, NOVA_URL + 'internal') + + url = api.url_for(self.request, 'compute', admin=True) + self.assertEqual(url, NOVA_URL + 'admin') + + self.assertNotIn('notAnApi', self.request.user.service_catalog, + 'Select a new nonexistent service catalog key') + with self.assertRaises(exceptions.ServiceCatalogException): + url = api.url_for(self.request, 'notAnApi') diff --git a/horizon/horizon/tests/api_tests/glance.py b/horizon/horizon/tests/api_tests/glance.py new file mode 100644 index 00000000..dea27d12 --- /dev/null +++ b/horizon/horizon/tests/api_tests/glance.py @@ -0,0 +1,174 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import + +from django import http +from glance import client as glance_client +from mox import IsA + +from horizon.tests.api_tests.utils import * + + +class GlanceApiTests(APITestCase): + def stub_glance_api(self, count=1): + self.mox.StubOutWithMock(api.glance, 'glance_api') + glance_api = self.mox.CreateMock(glance_client.Client) + glance_api.token = TEST_TOKEN + for i in range(count): + api.glance.glance_api(IsA(http.HttpRequest)).AndReturn(glance_api) + return glance_api + + def test_get_glance_api(self): + self.mox.StubOutClassWithMocks(glance_client, 'Client') + client_instance = glance_client.Client(TEST_HOSTNAME, TEST_PORT, + auth_tok=TEST_TOKEN) + # Normally ``auth_tok`` is set in ``Client.__init__``, but mox doesn't + # duplicate that behavior so we set it manually. + client_instance.auth_tok = TEST_TOKEN + + self.mox.StubOutWithMock(api.glance, 'url_for') + api.glance.url_for(IsA(http.HttpRequest), 'image').AndReturn(TEST_URL) + + self.mox.ReplayAll() + + ret_val = api.glance.glance_api(self.request) + self.assertIsNotNone(ret_val) + self.assertEqual(ret_val.auth_tok, TEST_TOKEN) + + self.mox.VerifyAll() + + def test_image_create(self): + IMAGE_FILE = 'someData' + IMAGE_META = {'metadata': 'foo'} + + glance_api = self.stub_glance_api() + glance_api.add_image(IMAGE_META, IMAGE_FILE).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.image_create(self.request, IMAGE_META, IMAGE_FILE) + + self.assertIsInstance(ret_val, api.Image) + self.assertEqual(ret_val._apidict, TEST_RETURN) + + self.mox.VerifyAll() + + def test_image_delete(self): + IMAGE_ID = '1' + + glance_api = self.stub_glance_api() + glance_api.delete_image(IMAGE_ID).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.image_delete(self.request, IMAGE_ID) + + self.assertEqual(ret_val, TEST_RETURN) + + self.mox.VerifyAll() + + def test_image_get(self): + IMAGE_ID = '1' + + glance_api = self.stub_glance_api() + glance_api.get_image(IMAGE_ID).AndReturn([TEST_RETURN]) + + self.mox.ReplayAll() + + ret_val = api.image_get(self.request, IMAGE_ID) + + self.assertIsInstance(ret_val, api.Image) + self.assertEqual(ret_val._apidict, TEST_RETURN) + + def test_image_list_detailed(self): + images = (TEST_RETURN, TEST_RETURN + '2') + glance_api = self.stub_glance_api() + glance_api.get_images_detailed().AndReturn(images) + + self.mox.ReplayAll() + + ret_val = api.image_list_detailed(self.request) + + self.assertEqual(len(ret_val), len(images)) + for image in ret_val: + self.assertIsInstance(image, api.Image) + self.assertIn(image._apidict, images) + + self.mox.VerifyAll() + + def test_image_update(self): + IMAGE_ID = '1' + IMAGE_META = {'metadata': 'foobar'} + + glance_api = self.stub_glance_api(count=2) + glance_api.update_image(IMAGE_ID, image_meta={}).AndReturn(TEST_RETURN) + glance_api.update_image(IMAGE_ID, + image_meta=IMAGE_META).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.image_update(self.request, IMAGE_ID) + + self.assertIsInstance(ret_val, api.Image) + self.assertEqual(ret_val._apidict, TEST_RETURN) + + ret_val = api.image_update(self.request, + IMAGE_ID, + image_meta=IMAGE_META) + + self.assertIsInstance(ret_val, api.Image) + self.assertEqual(ret_val._apidict, TEST_RETURN) + + self.mox.VerifyAll() + + +# Wrapper classes that have other attributes or methods need testing +class ImageWrapperTests(test.TestCase): + dict_with_properties = { + 'properties': + {'image_state': 'running'}, + 'size': 100, + } + dict_without_properties = { + 'size': 100, + } + + def test_get_properties(self): + image = api.Image(self.dict_with_properties) + image_props = image.properties + self.assertIsInstance(image_props, api.ImageProperties) + self.assertEqual(image_props.image_state, 'running') + + def test_get_other(self): + image = api.Image(self.dict_with_properties) + self.assertEqual(image.size, 100) + + def test_get_properties_missing(self): + image = api.Image(self.dict_without_properties) + with self.assertRaises(AttributeError): + image.properties + + def test_get_other_missing(self): + image = api.Image(self.dict_without_properties) + with self.assertRaises(AttributeError): + self.assertNotIn('missing', image._attrs, + msg="Test assumption broken. Find new missing attribute") + image.missing diff --git a/horizon/horizon/tests/api_tests/keystone.py b/horizon/horizon/tests/api_tests/keystone.py new file mode 100644 index 00000000..3af463f3 --- /dev/null +++ b/horizon/horizon/tests/api_tests/keystone.py @@ -0,0 +1,366 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import + +from django import http +from django.conf import settings +from mox import IsA +from openstackx import admin as OSAdmin + +from horizon.tests.api_tests.utils import * + + +class Token(object): + """ More or less fakes what the api is looking for """ + def __init__(self, id, username, tenant_id, + tenant_name, serviceCatalog=None): + self.id = id + self.user = {'name': username} + self.tenant = {'id': tenant_id, 'name': tenant_name} + self.serviceCatalog = serviceCatalog + + def __eq__(self, other): + return self.id == other.id and \ + self.user['name'] == other.user['name'] and \ + self.tenant_id == other.tenant_id and \ + self.serviceCatalog == other.serviceCatalog + + def __ne__(self, other): + return not self == other + + +class KeystoneAdminApiTests(APITestCase): + def stub_admin_api(self, count=1): + self.mox.StubOutWithMock(api.keystone, 'admin_api') + admin_api = self.mox.CreateMock(OSAdmin.Admin) + for i in range(count): + api.keystone.admin_api(IsA(http.HttpRequest)) \ + .AndReturn(admin_api) + return admin_api + + def test_service_get(self): + NAME = 'serviceName' + + admin_api = self.stub_admin_api() + admin_api.services = self.mox.CreateMockAnything() + admin_api.services.get(NAME).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.service_get(self.request, NAME) + + self.assertIsInstance(ret_val, api.Services) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_service_list(self): + services = (TEST_RETURN, TEST_RETURN + '2') + + admin_api = self.stub_admin_api() + admin_api.services = self.mox.CreateMockAnything() + admin_api.services.list().AndReturn(services) + + self.mox.ReplayAll() + + ret_val = api.service_list(self.request) + + for service in ret_val: + self.assertIsInstance(service, api.Services) + self.assertIn(service._apiresource, services) + + self.mox.VerifyAll() + + def test_service_update(self): + ENABLED = True + NAME = 'serviceName' + + admin_api = self.stub_admin_api() + admin_api.services = self.mox.CreateMockAnything() + admin_api.services.update(NAME, ENABLED).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.service_update(self.request, NAME, ENABLED) + + self.assertIsInstance(ret_val, api.Services) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + +class TokenApiTests(APITestCase): + def setUp(self): + super(TokenApiTests, self).setUp() + self._prev_OPENSTACK_KEYSTONE_URL = getattr(settings, + 'OPENSTACK_KEYSTONE_URL', + None) + settings.OPENSTACK_KEYSTONE_URL = TEST_URL + + def tearDown(self): + super(TokenApiTests, self).tearDown() + settings.OPENSTACK_KEYSTONE_URL = self._prev_OPENSTACK_KEYSTONE_URL + + def test_token_create(self): + catalog = { + 'access': { + 'token': { + 'id': TEST_TOKEN_ID, + }, + 'user': { + 'roles': [], + } + } + } + test_token = Token(TEST_TOKEN_ID, TEST_USERNAME, + TEST_TENANT_ID, TEST_TENANT_NAME) + + keystoneclient = self.stub_keystoneclient() + + keystoneclient.tokens = self.mox.CreateMockAnything() + keystoneclient.tokens.authenticate(username=TEST_USERNAME, + password=TEST_PASSWORD, + tenant=TEST_TENANT_ID)\ + .AndReturn(test_token) + + self.mox.ReplayAll() + + ret_val = api.token_create(self.request, TEST_TENANT_ID, + TEST_USERNAME, TEST_PASSWORD) + + self.assertEqual(test_token.tenant['id'], ret_val.tenant['id']) + + self.mox.VerifyAll() + + +class RoleAPITests(APITestCase): + def test_role_add_for_tenant_user(self): + keystoneclient = self.stub_keystoneclient() + + role = api.Role(APIResource.get_instance()) + role.id = TEST_RETURN + role.name = TEST_RETURN + + keystoneclient.roles = self.mox.CreateMockAnything() + keystoneclient.roles.add_user_to_tenant(TEST_TENANT_ID, + TEST_USERNAME, + TEST_RETURN).AndReturn(role) + api.keystone._get_role = self.mox.CreateMockAnything() + api.keystone._get_role(IsA(http.HttpRequest), IsA(str)).AndReturn(role) + + self.mox.ReplayAll() + ret_val = api.role_add_for_tenant_user(self.request, + TEST_TENANT_ID, + TEST_USERNAME, + TEST_RETURN) + self.assertEqual(ret_val, role) + + self.mox.VerifyAll() + + +class TenantAPITests(APITestCase): + def test_tenant_create(self): + DESCRIPTION = 'aDescription' + ENABLED = True + + keystoneclient = self.stub_keystoneclient() + + keystoneclient.tenants = self.mox.CreateMockAnything() + keystoneclient.tenants.create(TEST_TENANT_ID, DESCRIPTION, + ENABLED).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.tenant_create(self.request, TEST_TENANT_ID, + DESCRIPTION, ENABLED) + + self.assertIsInstance(ret_val, api.Tenant) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_tenant_get(self): + keystoneclient = self.stub_keystoneclient() + + keystoneclient.tenants = self.mox.CreateMockAnything() + keystoneclient.tenants.get(TEST_TENANT_ID).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.tenant_get(self.request, TEST_TENANT_ID) + + self.assertIsInstance(ret_val, api.Tenant) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_tenant_list(self): + tenants = (TEST_RETURN, TEST_RETURN + '2') + + keystoneclient = self.stub_keystoneclient() + + keystoneclient.tenants = self.mox.CreateMockAnything() + keystoneclient.tenants.list().AndReturn(tenants) + + self.mox.ReplayAll() + + ret_val = api.tenant_list(self.request) + + self.assertEqual(len(ret_val), len(tenants)) + for tenant in ret_val: + self.assertIsInstance(tenant, api.Tenant) + self.assertIn(tenant._apiresource, tenants) + + self.mox.VerifyAll() + + def test_tenant_update(self): + DESCRIPTION = 'aDescription' + ENABLED = True + + keystoneclient = self.stub_keystoneclient() + + keystoneclient.tenants = self.mox.CreateMockAnything() + keystoneclient.tenants.update(TEST_TENANT_ID, TEST_TENANT_NAME, + DESCRIPTION, ENABLED).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.tenant_update(self.request, TEST_TENANT_ID, + TEST_TENANT_NAME, DESCRIPTION, ENABLED) + + self.assertIsInstance(ret_val, api.Tenant) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + +class UserAPITests(APITestCase): + def test_user_create(self): + keystoneclient = self.stub_keystoneclient() + + keystoneclient.users = self.mox.CreateMockAnything() + keystoneclient.users.create(TEST_USERNAME, TEST_PASSWORD, TEST_EMAIL, + TEST_TENANT_ID, True).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.user_create(self.request, TEST_USERNAME, TEST_EMAIL, + TEST_PASSWORD, TEST_TENANT_ID, True) + + self.assertIsInstance(ret_val, api.User) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_user_delete(self): + keystoneclient = self.stub_keystoneclient() + + keystoneclient.users = self.mox.CreateMockAnything() + keystoneclient.users.delete(TEST_USERNAME).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.user_delete(self.request, TEST_USERNAME) + + self.assertIsNone(ret_val) + + self.mox.VerifyAll() + + def test_user_get(self): + keystoneclient = self.stub_keystoneclient() + + keystoneclient.users = self.mox.CreateMockAnything() + keystoneclient.users.get(TEST_USERNAME).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.user_get(self.request, TEST_USERNAME) + + self.assertIsInstance(ret_val, api.User) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_user_list(self): + users = (TEST_USERNAME, TEST_USERNAME + '2') + + keystoneclient = self.stub_keystoneclient() + keystoneclient.users = self.mox.CreateMockAnything() + keystoneclient.users.list(tenant_id=None).AndReturn(users) + + self.mox.ReplayAll() + + ret_val = api.user_list(self.request) + + self.assertEqual(len(ret_val), len(users)) + for user in ret_val: + self.assertIsInstance(user, api.User) + self.assertIn(user._apiresource, users) + + self.mox.VerifyAll() + + def test_user_update_email(self): + keystoneclient = self.stub_keystoneclient() + keystoneclient.users = self.mox.CreateMockAnything() + keystoneclient.users.update_email(TEST_USERNAME, + TEST_EMAIL).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.user_update_email(self.request, TEST_USERNAME, + TEST_EMAIL) + + self.assertIsInstance(ret_val, api.User) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_user_update_password(self): + keystoneclient = self.stub_keystoneclient() + keystoneclient.users = self.mox.CreateMockAnything() + keystoneclient.users.update_password(TEST_USERNAME, + TEST_PASSWORD).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.user_update_password(self.request, TEST_USERNAME, + TEST_PASSWORD) + + self.assertIsInstance(ret_val, api.User) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_user_update_tenant(self): + keystoneclient = self.stub_keystoneclient() + keystoneclient.users = self.mox.CreateMockAnything() + keystoneclient.users.update_tenant(TEST_USERNAME, + TEST_TENANT_ID).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.user_update_tenant(self.request, TEST_USERNAME, + TEST_TENANT_ID) + + self.assertIsInstance(ret_val, api.User) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() diff --git a/horizon/horizon/tests/api_tests/nova.py b/horizon/horizon/tests/api_tests/nova.py new file mode 100644 index 00000000..92dac4ae --- /dev/null +++ b/horizon/horizon/tests/api_tests/nova.py @@ -0,0 +1,697 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import + +from django import http +from django.conf import settings +from mox import IsA +from openstack import compute as OSCompute +from openstackx import admin as OSAdmin +from openstackx import auth as OSAuth +from openstackx import extras as OSExtras + + +from horizon.tests.api_tests.utils import * + + +class Server(object): + """ More or less fakes what the api is looking for """ + def __init__(self, id, image, attrs=None): + self.id = id + + self.image = image + if attrs is not None: + self.attrs = attrs + + def __eq__(self, other): + if self.id != other.id or \ + self.image['id'] != other.image['id']: + return False + + for k in self.attrs: + if other.attrs.__getattr__(k) != v: + return False + + return True + + def __ne__(self, other): + return not self == other + + +class ServerWrapperTests(test.TestCase): + HOST = 'hostname' + ID = '1' + IMAGE_NAME = 'imageName' + IMAGE_OBJ = {'id': '3', 'links': [{'href': '3', u'rel': u'bookmark'}]} + + def setUp(self): + super(ServerWrapperTests, self).setUp() + + # these are all objects "fetched" from the api + self.inner_attrs = {'host': self.HOST} + + self.inner_server = Server(self.ID, self.IMAGE_OBJ, self.inner_attrs) + self.inner_server_no_attrs = Server(self.ID, self.IMAGE_OBJ) + + #self.request = self.mox.CreateMock(http.HttpRequest) + + def test_get_attrs(self): + server = api.Server(self.inner_server, self.request) + attrs = server.attrs + # for every attribute in the "inner" object passed to the api wrapper, + # see if it can be accessed through the api.ServerAttribute instance + for k in self.inner_attrs: + self.assertEqual(attrs.__getattr__(k), self.inner_attrs[k]) + + def test_get_other(self): + server = api.Server(self.inner_server, self.request) + self.assertEqual(server.id, self.ID) + + def test_get_attrs_missing(self): + server = api.Server(self.inner_server_no_attrs, self.request) + with self.assertRaises(AttributeError): + server.attrs + + def test_get_other_missing(self): + server = api.Server(self.inner_server, self.request) + with self.assertRaises(AttributeError): + self.assertNotIn('missing', server._attrs, + msg="Test assumption broken. Find new missing attribute") + server.missing + + def test_image_name(self): + image = api.Image({'name': self.IMAGE_NAME}) + self.mox.StubOutWithMock(api.glance, 'image_get') + api.glance.image_get(IsA(http.HttpRequest), + self.IMAGE_OBJ['id']).AndReturn(image) + + server = api.Server(self.inner_server, self.request) + + self.mox.ReplayAll() + + image_name = server.image_name + + self.assertEqual(image_name, self.IMAGE_NAME) + + self.mox.VerifyAll() + + +class NovaAdminApiTests(APITestCase): + def stub_admin_api(self, count=1): + self.mox.StubOutWithMock(api.nova, 'admin_api') + admin_api = self.mox.CreateMock(OSAdmin.Admin) + for i in range(count): + api.nova.admin_api(IsA(http.HttpRequest)) \ + .AndReturn(admin_api) + return admin_api + + def test_get_admin_api(self): + self.mox.StubOutClassWithMocks(OSAdmin, 'Admin') + OSAdmin.Admin(auth_token=TEST_TOKEN, management_url=TEST_URL) + + self.mox.StubOutWithMock(api.deprecated, 'url_for') + api.deprecated.url_for(IsA(http.HttpRequest), + 'compute', True).AndReturn(TEST_URL) + + self.mox.ReplayAll() + + self.assertIsNotNone(api.nova.admin_api(self.request)) + + self.mox.VerifyAll() + + def test_flavor_create(self): + FLAVOR_DISK = 1000 + FLAVOR_ID = 6 + FLAVOR_MEMORY = 1024 + FLAVOR_NAME = 'newFlavor' + FLAVOR_VCPU = 2 + + admin_api = self.stub_admin_api() + + admin_api.flavors = self.mox.CreateMockAnything() + admin_api.flavors.create(FLAVOR_NAME, FLAVOR_MEMORY, FLAVOR_VCPU, + FLAVOR_DISK, FLAVOR_ID).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.flavor_create(self.request, FLAVOR_NAME, + str(FLAVOR_MEMORY), str(FLAVOR_VCPU), + str(FLAVOR_DISK), FLAVOR_ID) + + self.assertIsInstance(ret_val, api.Flavor) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_flavor_delete(self): + FLAVOR_ID = 6 + + admin_api = self.stub_admin_api(count=2) + + admin_api.flavors = self.mox.CreateMockAnything() + admin_api.flavors.delete(FLAVOR_ID, False).AndReturn(TEST_RETURN) + admin_api.flavors.delete(FLAVOR_ID, True).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.flavor_delete(self.request, FLAVOR_ID) + self.assertIsNone(ret_val) + + ret_val = api.flavor_delete(self.request, FLAVOR_ID, purge=True) + self.assertIsNone(ret_val) + + +class ComputeApiTests(APITestCase): + def stub_compute_api(self, count=1): + self.mox.StubOutWithMock(api.nova, 'compute_api') + compute_api = self.mox.CreateMock(OSCompute.Compute) + for i in range(count): + api.nova.compute_api(IsA(http.HttpRequest)) \ + .AndReturn(compute_api) + return compute_api + + def test_get_compute_api(self): + class ComputeClient(object): + __slots__ = ['auth_token', 'management_url'] + + self.mox.StubOutClassWithMocks(OSCompute, 'Compute') + compute_api = OSCompute.Compute(auth_token=TEST_TOKEN, + management_url=TEST_URL) + + compute_api.client = ComputeClient() + + self.mox.StubOutWithMock(api.deprecated, 'url_for') + api.deprecated.url_for(IsA(http.HttpRequest), + 'compute').AndReturn(TEST_URL) + + self.mox.ReplayAll() + + compute_api = api.nova.compute_api(self.request) + + self.assertIsNotNone(compute_api) + self.assertEqual(compute_api.client.auth_token, TEST_TOKEN) + self.assertEqual(compute_api.client.management_url, TEST_URL) + + self.mox.VerifyAll() + + def test_flavor_get(self): + FLAVOR_ID = 6 + + novaclient = self.stub_novaclient() + + novaclient.flavors = self.mox.CreateMockAnything() + novaclient.flavors.get(FLAVOR_ID).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.flavor_get(self.request, FLAVOR_ID) + self.assertIsInstance(ret_val, api.Flavor) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_server_delete(self): + INSTANCE = 'anInstance' + + compute_api = self.stub_compute_api() + + compute_api.servers = self.mox.CreateMockAnything() + compute_api.servers.delete(INSTANCE).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.server_delete(self.request, INSTANCE) + + self.assertIsNone(ret_val) + + self.mox.VerifyAll() + + def test_server_reboot(self): + INSTANCE_ID = '2' + HARDNESS = 'diamond' + + self.mox.StubOutWithMock(api.nova, 'server_get') + + server = self.mox.CreateMock(OSCompute.Server) + server.reboot(OSCompute.servers.REBOOT_HARD).AndReturn(TEST_RETURN) + api.nova.server_get(IsA(http.HttpRequest), + INSTANCE_ID).AndReturn(server) + + server = self.mox.CreateMock(OSCompute.Server) + server.reboot(HARDNESS).AndReturn(TEST_RETURN) + api.nova.server_get(IsA(http.HttpRequest), + INSTANCE_ID).AndReturn(server) + + self.mox.ReplayAll() + + ret_val = api.server_reboot(self.request, INSTANCE_ID) + self.assertIsNone(ret_val) + + ret_val = api.server_reboot(self.request, INSTANCE_ID, + hardness=HARDNESS) + self.assertIsNone(ret_val) + + self.mox.VerifyAll() + + def test_server_create(self): + NAME = 'server' + IMAGE = 'anImage' + FLAVOR = 'cherry' + USER_DATA = {'nuts': 'berries'} + KEY = 'user' + SECGROUP = self.mox.CreateMock(api.SecurityGroup) + + server = self.mox.CreateMock(OSCompute.Server) + novaclient = self.stub_novaclient() + novaclient.servers = self.mox.CreateMockAnything() + novaclient.servers.create(NAME, IMAGE, FLAVOR, userdata=USER_DATA, + security_groups=[SECGROUP], key_name=KEY)\ + .AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.server_create(self.request, NAME, IMAGE, FLAVOR, + KEY, USER_DATA, [SECGROUP]) + + self.assertIsInstance(ret_val, api.Server) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + +class ExtrasApiTests(APITestCase): + + def stub_extras_api(self, count=1): + self.mox.StubOutWithMock(api.nova, 'extras_api') + extras_api = self.mox.CreateMock(OSExtras.Extras) + for i in range(count): + api.nova.extras_api(IsA(http.HttpRequest)) \ + .AndReturn(extras_api) + return extras_api + + def test_get_extras_api(self): + self.mox.StubOutClassWithMocks(OSExtras, 'Extras') + OSExtras.Extras(auth_token=TEST_TOKEN, management_url=TEST_URL) + + self.mox.StubOutWithMock(api.deprecated, 'url_for') + api.deprecated.url_for(IsA(http.HttpRequest), + 'compute').AndReturn(TEST_URL) + + self.mox.ReplayAll() + + self.assertIsNotNone(api.nova.extras_api(self.request)) + + self.mox.VerifyAll() + + def test_console_create(self): + extras_api = self.stub_extras_api(count=2) + extras_api.consoles = self.mox.CreateMockAnything() + extras_api.consoles.create( + TEST_INSTANCE_ID, TEST_CONSOLE_KIND).AndReturn(TEST_RETURN) + extras_api.consoles.create( + TEST_INSTANCE_ID, 'text').AndReturn(TEST_RETURN + '2') + + self.mox.ReplayAll() + + ret_val = api.console_create(self.request, + TEST_INSTANCE_ID, + TEST_CONSOLE_KIND) + self.assertIsInstance(ret_val, api.Console) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + ret_val = api.console_create(self.request, TEST_INSTANCE_ID) + self.assertIsInstance(ret_val, api.Console) + self.assertEqual(ret_val._apiresource, TEST_RETURN + '2') + + self.mox.VerifyAll() + + def test_flavor_list(self): + flavors = (TEST_RETURN, TEST_RETURN + '2') + novaclient = self.stub_novaclient() + novaclient.flavors = self.mox.CreateMockAnything() + novaclient.flavors.list().AndReturn(flavors) + + self.mox.ReplayAll() + + ret_val = api.flavor_list(self.request) + + self.assertEqual(len(ret_val), len(flavors)) + for flavor in ret_val: + self.assertIsInstance(flavor, api.Flavor) + self.assertIn(flavor._apiresource, flavors) + + self.mox.VerifyAll() + + def test_server_list(self): + servers = (TEST_RETURN, TEST_RETURN + '2') + + extras_api = self.stub_extras_api() + + extras_api.servers = self.mox.CreateMockAnything() + extras_api.servers.list().AndReturn(servers) + + self.mox.ReplayAll() + + ret_val = api.server_list(self.request) + + self.assertEqual(len(ret_val), len(servers)) + for server in ret_val: + self.assertIsInstance(server, api.Server) + self.assertIn(server._apiresource, servers) + + self.mox.VerifyAll() + + def test_usage_get(self): + extras_api = self.stub_extras_api() + + extras_api.usage = self.mox.CreateMockAnything() + extras_api.usage.get(TEST_TENANT_ID, 'start', + 'end').AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.usage_get(self.request, TEST_TENANT_ID, 'start', 'end') + + self.assertIsInstance(ret_val, api.Usage) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_usage_list(self): + usages = (TEST_RETURN, TEST_RETURN + '2') + + extras_api = self.stub_extras_api() + + extras_api.usage = self.mox.CreateMockAnything() + extras_api.usage.list('start', 'end').AndReturn(usages) + + self.mox.ReplayAll() + + ret_val = api.usage_list(self.request, 'start', 'end') + + self.assertEqual(len(ret_val), len(usages)) + for usage in ret_val: + self.assertIsInstance(usage, api.Usage) + self.assertIn(usage._apiresource, usages) + + self.mox.VerifyAll() + + def test_server_get(self): + INSTANCE_ID = '2' + + extras_api = self.stub_extras_api() + extras_api.servers = self.mox.CreateMockAnything() + extras_api.servers.get(INSTANCE_ID).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.server_get(self.request, INSTANCE_ID) + + self.assertIsInstance(ret_val, api.Server) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + +class APIExtensionTests(APITestCase): + + def setUp(self): + super(APIExtensionTests, self).setUp() + keypair = api.KeyPair(APIResource.get_instance()) + keypair.id = 1 + keypair.name = TEST_RETURN + + self.keypair = keypair + self.keypairs = [keypair, ] + + floating_ip = api.FloatingIp(APIResource.get_instance()) + floating_ip.id = 1 + floating_ip.fixed_ip = '10.0.0.4' + floating_ip.instance_id = 1 + floating_ip.ip = '58.58.58.58' + + self.floating_ip = floating_ip + self.floating_ips = [floating_ip, ] + + server = api.Server(APIResource.get_instance(), self.request) + server.id = 1 + + self.server = server + self.servers = [server, ] + + def test_server_snapshot_create(self): + novaclient = self.stub_novaclient() + + novaclient.servers = self.mox.CreateMockAnything() + novaclient.servers.create_image(IsA(int), IsA(str)).\ + AndReturn(self.server) + self.mox.ReplayAll() + + server = api.snapshot_create(self.request, 1, 'test-snapshot') + + self.assertIsInstance(server, api.Server) + self.mox.VerifyAll() + + def test_tenant_floating_ip_list(self): + novaclient = self.stub_novaclient() + + novaclient.floating_ips = self.mox.CreateMockAnything() + novaclient.floating_ips.list().AndReturn(self.floating_ips) + self.mox.ReplayAll() + + floating_ips = api.tenant_floating_ip_list(self.request) + + self.assertEqual(len(floating_ips), len(self.floating_ips)) + self.assertIsInstance(floating_ips[0], api.FloatingIp) + self.mox.VerifyAll() + + def test_tenant_floating_ip_get(self): + novaclient = self.stub_novaclient() + + novaclient.floating_ips = self.mox.CreateMockAnything() + novaclient.floating_ips.get(IsA(int)).AndReturn(self.floating_ip) + self.mox.ReplayAll() + + floating_ip = api.tenant_floating_ip_get(self.request, 1) + + self.assertIsInstance(floating_ip, api.FloatingIp) + self.mox.VerifyAll() + + def test_tenant_floating_ip_allocate(self): + novaclient = self.stub_novaclient() + + novaclient.floating_ips = self.mox.CreateMockAnything() + novaclient.floating_ips.create().AndReturn(self.floating_ip) + self.mox.ReplayAll() + + floating_ip = api.tenant_floating_ip_allocate(self.request) + + self.assertIsInstance(floating_ip, api.FloatingIp) + self.mox.VerifyAll() + + def test_tenant_floating_ip_release(self): + novaclient = self.stub_novaclient() + + novaclient.floating_ips = self.mox.CreateMockAnything() + novaclient.floating_ips.delete(1).AndReturn(self.floating_ip) + self.mox.ReplayAll() + + floating_ip = api.tenant_floating_ip_release(self.request, 1) + + self.assertIsInstance(floating_ip, api.FloatingIp) + self.mox.VerifyAll() + + def test_server_remove_floating_ip(self): + novaclient = self.stub_novaclient() + + novaclient.servers = self.mox.CreateMockAnything() + novaclient.floating_ips = self.mox.CreateMockAnything() + + novaclient.servers.get(IsA(int)).AndReturn(self.server) + novaclient.floating_ips.get(IsA(int)).AndReturn(self.floating_ip) + novaclient.servers.remove_floating_ip(IsA(self.server.__class__), + IsA(self.floating_ip.__class__)) \ + .AndReturn(self.server) + self.mox.ReplayAll() + + server = api.server_remove_floating_ip(self.request, 1, 1) + + self.assertIsInstance(server, api.Server) + self.mox.VerifyAll() + + def test_server_add_floating_ip(self): + novaclient = self.stub_novaclient() + + novaclient.floating_ips = self.mox.CreateMockAnything() + novaclient.servers = self.mox.CreateMockAnything() + + novaclient.servers.get(IsA(int)).AndReturn(self.server) + novaclient.floating_ips.get(IsA(int)).AndReturn(self.floating_ip) + novaclient.servers.add_floating_ip(IsA(self.server.__class__), + IsA(self.floating_ip.__class__)) \ + .AndReturn(self.server) + self.mox.ReplayAll() + + server = api.server_add_floating_ip(self.request, 1, 1) + + self.assertIsInstance(server, api.Server) + self.mox.VerifyAll() + + def test_keypair_create(self): + novaclient = self.stub_novaclient() + + novaclient.keypairs = self.mox.CreateMockAnything() + novaclient.keypairs.create(IsA(str)).AndReturn(self.keypair) + self.mox.ReplayAll() + + ret_val = api.keypair_create(self.request, TEST_RETURN) + self.assertIsInstance(ret_val, api.KeyPair) + self.assertEqual(ret_val.name, self.keypair.name) + + self.mox.VerifyAll() + + def test_keypair_import(self): + novaclient = self.stub_novaclient() + + novaclient.keypairs = self.mox.CreateMockAnything() + novaclient.keypairs.create(IsA(str), IsA(str)).AndReturn(self.keypair) + self.mox.ReplayAll() + + ret_val = api.keypair_import(self.request, TEST_RETURN, TEST_RETURN) + self.assertIsInstance(ret_val, api.KeyPair) + self.assertEqual(ret_val.name, self.keypair.name) + + self.mox.VerifyAll() + + def test_keypair_delete(self): + novaclient = self.stub_novaclient() + + novaclient.keypairs = self.mox.CreateMockAnything() + novaclient.keypairs.delete(IsA(int)) + + self.mox.ReplayAll() + + ret_val = api.keypair_delete(self.request, self.keypair.id) + self.assertIsNone(ret_val) + + self.mox.VerifyAll() + + def test_keypair_list(self): + novaclient = self.stub_novaclient() + + novaclient.keypairs = self.mox.CreateMockAnything() + novaclient.keypairs.list().AndReturn(self.keypairs) + + self.mox.ReplayAll() + + ret_val = api.keypair_list(self.request) + + self.assertEqual(len(ret_val), len(self.keypairs)) + for keypair in ret_val: + self.assertIsInstance(keypair, api.KeyPair) + + self.mox.VerifyAll() + + +class VolumeTests(APITestCase): + def setUp(self): + super(VolumeTests, self).setUp() + volume = api.Volume(APIResource.get_instance()) + volume.id = 1 + volume.displayName = "displayName" + volume.attachments = [{"device": "/dev/vdb", + "serverId": 1, + "id": 1, + "volumeId": 1}] + self.volume = volume + self.volumes = [volume, ] + + self.novaclient = self.stub_novaclient() + self.novaclient.volumes = self.mox.CreateMockAnything() + + def test_volume_list(self): + self.novaclient.volumes.list().AndReturn(self.volumes) + self.mox.ReplayAll() + + volumes = api.volume_list(self.request) + + self.assertIsInstance(volumes[0], api.Volume) + self.mox.VerifyAll() + + def test_volume_get(self): + self.novaclient.volumes.get(IsA(int)).AndReturn(self.volume) + self.mox.ReplayAll() + + volume = api.volume_get(self.request, 1) + + self.assertIsInstance(volume, api.Volume) + self.mox.VerifyAll() + + def test_volume_instance_list(self): + self.novaclient.volumes.get_server_volumes(IsA(int)).AndReturn( + self.volume.attachments) + self.mox.ReplayAll() + + attachments = api.volume_instance_list(self.request, 1) + + self.assertEqual(attachments, self.volume.attachments) + self.mox.VerifyAll() + + def test_volume_create(self): + self.novaclient.volumes.create(IsA(int), IsA(str), IsA(str)).AndReturn( + self.volume) + self.mox.ReplayAll() + + new_volume = api.volume_create(self.request, + 10, + "new volume", + "new description") + + self.assertIsInstance(new_volume, api.Volume) + self.mox.VerifyAll() + + def test_volume_delete(self): + self.novaclient.volumes.delete(IsA(int)) + self.mox.ReplayAll() + + ret_val = api.volume_delete(self.request, 1) + + self.assertIsNone(ret_val) + self.mox.VerifyAll() + + def test_volume_attach(self): + self.novaclient.volumes.create_server_volume( + IsA(int), IsA(int), IsA(str)) + self.mox.ReplayAll() + + ret_val = api.volume_attach(self.request, 1, 1, "/dev/vdb") + + self.assertIsNone(ret_val) + self.mox.VerifyAll() + + def test_volume_detach(self): + self.novaclient.volumes.delete_server_volume(IsA(int), IsA(int)) + self.mox.ReplayAll() + + ret_val = api.volume_detach(self.request, 1, 1) + + self.assertIsNone(ret_val) + self.mox.VerifyAll() diff --git a/horizon/horizon/tests/api_tests/swift.py b/horizon/horizon/tests/api_tests/swift.py new file mode 100644 index 00000000..77c667e0 --- /dev/null +++ b/horizon/horizon/tests/api_tests/swift.py @@ -0,0 +1,260 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import + +import cloudfiles +from django import http +from mox import IsA + +from horizon.tests.api_tests.utils import * + + +class SwiftApiTests(APITestCase): + def setUp(self): + super(SwiftApiTests, self).setUp() + self.request = http.HttpRequest() + self.request.session = dict() + self.request.session['token'] = TEST_TOKEN + + def stub_swift_api(self, count=1): + self.mox.StubOutWithMock(api.swift, 'swift_api') + swift_api = self.mox.CreateMock(cloudfiles.connection.Connection) + for i in range(count): + api.swift.swift_api(IsA(http.HttpRequest)).AndReturn(swift_api) + return swift_api + + def test_swift_get_containers(self): + containers = (TEST_RETURN, TEST_RETURN + '2') + + swift_api = self.stub_swift_api() + + swift_api.get_all_containers(limit=10000, + marker=None).AndReturn(containers) + + self.mox.ReplayAll() + + ret_val = api.swift_get_containers(self.request) + + self.assertEqual(len(ret_val), len(containers)) + for container in ret_val: + self.assertIsInstance(container, api.Container) + self.assertIn(container._apiresource, containers) + + self.mox.VerifyAll() + + def test_swift_create_container(self): + NAME = 'containerName' + + swift_api = self.stub_swift_api() + self.mox.StubOutWithMock(api.swift, 'swift_container_exists') + + api.swift.swift_container_exists(self.request, + NAME).AndReturn(False) + swift_api.create_container(NAME).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.swift_create_container(self.request, NAME) + + self.assertIsInstance(ret_val, api.Container) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_swift_delete_container(self): + NAME = 'containerName' + + swift_api = self.stub_swift_api() + + swift_api.delete_container(NAME).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.swift_delete_container(self.request, NAME) + + self.assertIsNone(ret_val) + + self.mox.VerifyAll() + + def test_swift_get_objects(self): + NAME = 'containerName' + + swift_objects = (TEST_RETURN, TEST_RETURN + '2') + container = self.mox.CreateMock(cloudfiles.container.Container) + container.get_objects(limit=10000, + marker=None, + prefix=None).AndReturn(swift_objects) + + swift_api = self.stub_swift_api() + + swift_api.get_container(NAME).AndReturn(container) + + self.mox.ReplayAll() + + ret_val = api.swift_get_objects(self.request, NAME) + + self.assertEqual(len(ret_val), len(swift_objects)) + for swift_object in ret_val: + self.assertIsInstance(swift_object, api.SwiftObject) + self.assertIn(swift_object._apiresource, swift_objects) + + self.mox.VerifyAll() + + def test_swift_get_objects_with_prefix(self): + NAME = 'containerName' + PREFIX = 'prefacedWith' + + swift_objects = (TEST_RETURN, TEST_RETURN + '2') + container = self.mox.CreateMock(cloudfiles.container.Container) + container.get_objects(limit=10000, + marker=None, + prefix=PREFIX).AndReturn(swift_objects) + + swift_api = self.stub_swift_api() + + swift_api.get_container(NAME).AndReturn(container) + + self.mox.ReplayAll() + + ret_val = api.swift_get_objects(self.request, + NAME, + prefix=PREFIX) + + self.assertEqual(len(ret_val), len(swift_objects)) + for swift_object in ret_val: + self.assertIsInstance(swift_object, api.SwiftObject) + self.assertIn(swift_object._apiresource, swift_objects) + + self.mox.VerifyAll() + + def test_swift_upload_object(self): + CONTAINER_NAME = 'containerName' + OBJECT_NAME = 'objectName' + OBJECT_DATA = 'someData' + + swift_api = self.stub_swift_api() + container = self.mox.CreateMock(cloudfiles.container.Container) + swift_object = self.mox.CreateMock(cloudfiles.storage_object.Object) + + swift_api.get_container(CONTAINER_NAME).AndReturn(container) + container.create_object(OBJECT_NAME).AndReturn(swift_object) + swift_object.write(OBJECT_DATA).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.swift_upload_object(self.request, + CONTAINER_NAME, + OBJECT_NAME, + OBJECT_DATA) + + self.assertIsNone(ret_val) + + self.mox.VerifyAll() + + def test_swift_delete_object(self): + CONTAINER_NAME = 'containerName' + OBJECT_NAME = 'objectName' + + swift_api = self.stub_swift_api() + container = self.mox.CreateMock(cloudfiles.container.Container) + + swift_api.get_container(CONTAINER_NAME).AndReturn(container) + container.delete_object(OBJECT_NAME).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.swift_delete_object(self.request, + CONTAINER_NAME, + OBJECT_NAME) + + self.assertIsNone(ret_val) + + self.mox.VerifyAll() + + def test_swift_get_object_data(self): + CONTAINER_NAME = 'containerName' + OBJECT_NAME = 'objectName' + OBJECT_DATA = 'objectData' + + swift_api = self.stub_swift_api() + container = self.mox.CreateMock(cloudfiles.container.Container) + swift_object = self.mox.CreateMock(cloudfiles.storage_object.Object) + + swift_api.get_container(CONTAINER_NAME).AndReturn(container) + container.get_object(OBJECT_NAME).AndReturn(swift_object) + swift_object.stream().AndReturn(OBJECT_DATA) + + self.mox.ReplayAll() + + ret_val = api.swift_get_object_data(self.request, + CONTAINER_NAME, + OBJECT_NAME) + + self.assertEqual(ret_val, OBJECT_DATA) + + self.mox.VerifyAll() + + def test_swift_object_exists(self): + CONTAINER_NAME = 'containerName' + OBJECT_NAME = 'objectName' + + swift_api = self.stub_swift_api() + container = self.mox.CreateMock(cloudfiles.container.Container) + swift_object = self.mox.CreateMock(cloudfiles.Object) + + swift_api.get_container(CONTAINER_NAME).AndReturn(container) + container.get_object(OBJECT_NAME).AndReturn(swift_object) + + self.mox.ReplayAll() + + ret_val = api.swift_object_exists(self.request, + CONTAINER_NAME, + OBJECT_NAME) + self.assertTrue(ret_val) + + self.mox.VerifyAll() + + def test_swift_copy_object(self): + CONTAINER_NAME = 'containerName' + OBJECT_NAME = 'objectName' + + swift_api = self.stub_swift_api() + container = self.mox.CreateMock(cloudfiles.container.Container) + self.mox.StubOutWithMock(api.swift, 'swift_object_exists') + + swift_object = self.mox.CreateMock(cloudfiles.Object) + + swift_api.get_container(CONTAINER_NAME).AndReturn(container) + api.swift.swift_object_exists(self.request, + CONTAINER_NAME, + OBJECT_NAME).AndReturn(False) + + container.get_object(OBJECT_NAME).AndReturn(swift_object) + swift_object.copy_to(CONTAINER_NAME, OBJECT_NAME) + + self.mox.ReplayAll() + + ret_val = api.swift_copy_object(self.request, CONTAINER_NAME, + OBJECT_NAME, CONTAINER_NAME, + OBJECT_NAME) + + self.assertIsNone(ret_val) + self.mox.VerifyAll() diff --git a/horizon/horizon/tests/api_tests/utils.py b/horizon/horizon/tests/api_tests/utils.py new file mode 100644 index 00000000..37dcd2b3 --- /dev/null +++ b/horizon/horizon/tests/api_tests/utils.py @@ -0,0 +1,99 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient.v2_0 import client as keystone_client +from novaclient.v1_1 import client as nova_client + +from horizon import api +from horizon import test + + +TEST_CONSOLE_KIND = 'vnc' +TEST_EMAIL = 'test@test.com' +TEST_HOSTNAME = 'hostname' +TEST_INSTANCE_ID = '2' +TEST_PASSWORD = '12345' +TEST_PORT = 8000 +TEST_RETURN = 'retValue' +TEST_TENANT_DESCRIPTION = 'tenantDescription' +TEST_TENANT_ID = '1234' +TEST_TENANT_NAME = 'foo' +TEST_TOKEN = 'aToken' +TEST_TOKEN_ID = 'userId' +TEST_URL = 'http://%s:%s/something/v1.0' % (TEST_HOSTNAME, TEST_PORT) +TEST_USERNAME = 'testUser' + + +class APIResource(api.APIResourceWrapper): + """ Simple APIResource for testing """ + _attrs = ['foo', 'bar', 'baz'] + + @staticmethod + def get_instance(innerObject=None): + if innerObject is None: + + class InnerAPIResource(object): + pass + + innerObject = InnerAPIResource() + innerObject.foo = 'foo' + innerObject.bar = 'bar' + return APIResource(innerObject) + + +class APIDict(api.APIDictWrapper): + """ Simple APIDict for testing """ + _attrs = ['foo', 'bar', 'baz'] + + @staticmethod + def get_instance(innerDict=None): + if innerDict is None: + innerDict = {'foo': 'foo', + 'bar': 'bar'} + return APIDict(innerDict) + + +class APITestCase(test.TestCase): + def setUp(self): + def fake_keystoneclient(request, username=None, password=None, + tenant_id=None, token_id=None, endpoint=None): + return self.stub_keystoneclient() + super(APITestCase, self).setUp() + self._original_keystoneclient = api.keystone.keystoneclient + self._original_novaclient = api.nova.novaclient + api.keystone.keystoneclient = fake_keystoneclient + api.nova.novaclient = lambda request: self.stub_novaclient() + + def stub_novaclient(self): + if not hasattr(self, "novaclient"): + self.mox.StubOutWithMock(nova_client, 'Client') + self.novaclient = self.mox.CreateMock(nova_client.Client) + return self.novaclient + + def stub_keystoneclient(self): + if not hasattr(self, "keystoneclient"): + self.mox.StubOutWithMock(keystone_client, 'Client') + self.keystoneclient = self.mox.CreateMock(keystone_client.Client) + return self.keystoneclient + + def tearDown(self): + super(APITestCase, self).tearDown() + api.nova.novaclient = self._original_novaclient + api.keystone.keystoneclient = self._original_keystoneclient diff --git a/horizon/horizon/tests/auth_tests.py b/horizon/horizon/tests/auth_tests.py new file mode 100644 index 00000000..50333f12 --- /dev/null +++ b/horizon/horizon/tests/auth_tests.py @@ -0,0 +1,229 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django import http +from django.contrib import messages +from django.core.urlresolvers import reverse +from openstackx.api import exceptions as api_exceptions +from mox import IsA + +from horizon import api +from horizon import test + + +SYSPANEL_INDEX_URL = reverse('horizon:syspanel:overview:index') +DASH_INDEX_URL = reverse('horizon:nova:overview:index') + + +class AuthViewTests(test.BaseViewTests): + def setUp(self): + super(AuthViewTests, self).setUp() + self.setActiveUser() + self.PASSWORD = 'secret' + + def test_login_index(self): + res = self.client.get(reverse('horizon:auth_login')) + self.assertTemplateUsed(res, 'splash.html') + + def test_login_user_logged_in(self): + self.setActiveUser(self.TEST_TOKEN, self.TEST_USER, self.TEST_TENANT, + False, self.TEST_SERVICE_CATALOG) + + res = self.client.get(reverse('horizon:auth_login')) + self.assertRedirectsNoFollow(res, DASH_INDEX_URL) + + def test_login_no_tenants(self): + NEW_TENANT_ID = '6' + NEW_TENANT_NAME = 'FAKENAME' + TOKEN_ID = 1 + + form_data = {'method': 'Login', + 'password': self.PASSWORD, + 'username': self.TEST_USER} + + self.mox.StubOutWithMock(api, 'token_create') + + class FakeToken(object): + id = TOKEN_ID, + user = {'roles': [{'name': 'fake'}]}, + serviceCatalog = {} + aToken = api.Token(FakeToken()) + + api.token_create(IsA(http.HttpRequest), "", self.TEST_USER, + self.PASSWORD).AndReturn(aToken) + + aTenant = self.mox.CreateMock(api.Token) + aTenant.id = NEW_TENANT_ID + aTenant.name = NEW_TENANT_NAME + + self.mox.StubOutWithMock(api, 'tenant_list_for_token') + api.tenant_list_for_token(IsA(http.HttpRequest), aToken.id).\ + AndReturn([]) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(unicode)) + + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:auth_login'), form_data) + + self.assertTemplateUsed(res, 'splash.html') + + self.mox.VerifyAll() + + def test_login(self): + NEW_TENANT_ID = '6' + NEW_TENANT_NAME = 'FAKENAME' + TOKEN_ID = 1 + + form_data = {'method': 'Login', + 'password': self.PASSWORD, + 'username': self.TEST_USER} + + self.mox.StubOutWithMock(api, 'token_create') + + class FakeToken(object): + id = TOKEN_ID, + user = {"id": "1", + "roles": [{"id": "1", "name": "fake"}], "name": "user"} + serviceCatalog = {} + tenant = None + aToken = api.Token(FakeToken()) + bToken = aToken + + api.token_create(IsA(http.HttpRequest), "", self.TEST_USER, + self.PASSWORD).AndReturn(aToken) + + aTenant = self.mox.CreateMock(api.Token) + aTenant.id = NEW_TENANT_ID + aTenant.name = NEW_TENANT_NAME + bToken.tenant = {'id': aTenant.id, 'name': aTenant.name} + + self.mox.StubOutWithMock(api, 'tenant_list_for_token') + api.tenant_list_for_token(IsA(http.HttpRequest), aToken.id).\ + AndReturn([aTenant]) + + self.mox.StubOutWithMock(api, 'token_create_scoped') + api.token_create_scoped(IsA(http.HttpRequest), aTenant.id, + aToken.id).AndReturn(bToken) + + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:auth_login'), form_data) + + self.assertRedirectsNoFollow(res, DASH_INDEX_URL) + + self.mox.VerifyAll() + + def test_login_invalid_credentials(self): + form_data = {'method': 'Login', + 'password': self.PASSWORD, + 'username': self.TEST_USER} + + self.mox.StubOutWithMock(api, 'token_create') + unauthorized = api_exceptions.Unauthorized('unauth', message='unauth') + api.token_create(IsA(http.HttpRequest), "", self.TEST_USER, + self.PASSWORD).AndRaise(unauthorized) + + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:auth_login'), form_data) + + self.assertTemplateUsed(res, 'splash.html') + + self.mox.VerifyAll() + + def test_login_exception(self): + form_data = {'method': 'Login', + 'password': self.PASSWORD, + 'username': self.TEST_USER} + + self.mox.StubOutWithMock(api, 'token_create') + api_exception = api_exceptions.ApiException('apiException', + message='apiException') + api.token_create(IsA(http.HttpRequest), "", self.TEST_USER, + self.PASSWORD).AndRaise(api_exception) + + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:auth_login'), form_data) + + self.assertTemplateUsed(res, 'splash.html') + + self.mox.VerifyAll() + + def test_switch_tenants_index(self): + res = self.client.get(reverse('horizon:auth_switch', + args=[self.TEST_TENANT])) + + self.assertTemplateUsed(res, 'switch_tenants.html') + + def test_switch_tenants(self): + NEW_TENANT_ID = '6' + NEW_TENANT_NAME = 'FAKENAME' + TOKEN_ID = 1 + + self.setActiveUser(self.TEST_TOKEN, self.TEST_USER, self.TEST_TENANT, + False, self.TEST_SERVICE_CATALOG) + + form_data = {'method': 'LoginWithTenant', + 'password': self.PASSWORD, + 'tenant': NEW_TENANT_ID, + 'username': self.TEST_USER} + + self.mox.StubOutWithMock(api, 'token_create') + + aTenant = self.mox.CreateMock(api.Token) + aTenant.id = NEW_TENANT_ID + aTenant.name = NEW_TENANT_NAME + + aToken = self.mox.CreateMock(api.Token) + aToken.id = TOKEN_ID + aToken.user = {'name': self.TEST_USER, 'roles': [{'name': 'fake'}]} + aToken.serviceCatalog = {} + aToken.tenant = {'id': aTenant.id, 'name': aTenant.name} + + api.token_create(IsA(http.HttpRequest), NEW_TENANT_ID, self.TEST_USER, + self.PASSWORD).AndReturn(aToken) + + self.mox.StubOutWithMock(api, 'tenant_list_for_token') + api.tenant_list_for_token(IsA(http.HttpRequest), aToken.id).\ + AndReturn([aTenant]) + + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:auth_switch', + args=[NEW_TENANT_ID]), form_data) + + self.assertRedirectsNoFollow(res, DASH_INDEX_URL) + self.assertEqual(self.client.session['tenant'], NEW_TENANT_NAME) + + self.mox.VerifyAll() + + def test_logout(self): + KEY = 'arbitraryKeyString' + VALUE = 'arbitraryKeyValue' + self.assertNotIn(KEY, self.client.session) + self.client.session[KEY] = VALUE + + res = self.client.get(reverse('horizon:auth_logout')) + + self.assertRedirectsNoFollow(res, reverse('splash')) + self.assertNotIn(KEY, self.client.session) diff --git a/horizon/horizon/tests/base_tests.py b/horizon/horizon/tests/base_tests.py new file mode 100644 index 00000000..fbef3149 --- /dev/null +++ b/horizon/horizon/tests/base_tests.py @@ -0,0 +1,133 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +import horizon +from horizon import base +from horizon import exceptions +from horizon import test +from horizon.base import Horizon +from horizon.users import User + + +class MyDash(horizon.Dashboard): + name = "My Dashboard" + slug = "mydash" + + +class MyPanel(horizon.Panel): + name = "My Panel" + slug = "myslug" + + +class HorizonTests(test.TestCase): + def setUp(self): + super(HorizonTests, self).setUp() + self._orig_horizon = copy.deepcopy(base.Horizon) + + def tearDown(self): + super(HorizonTests, self).tearDown() + base.Horizon = self._orig_horizon + + def test_registry(self): + """ Verify registration and autodiscovery work correctly. + + Please note that this implicitly tests that autodiscovery works + by virtue of the fact that the dashboards listed in + ``settings.INSTALLED_APPS`` are loaded from the start. + """ + + # Registration + self.assertEqual(len(Horizon._registry), 3) + horizon.register(MyDash) + self.assertEqual(len(Horizon._registry), 4) + with self.assertRaises(ValueError): + horizon.register(MyPanel) + with self.assertRaises(ValueError): + horizon.register("MyPanel") + + # Retrieval + my_dash_instance_by_name = horizon.get_dashboard("mydash") + self.assertTrue(isinstance(my_dash_instance_by_name, MyDash)) + my_dash_instance_by_class = horizon.get_dashboard(MyDash) + self.assertEqual(my_dash_instance_by_name, my_dash_instance_by_class) + with self.assertRaises(base.NotRegistered): + horizon.get_dashboard("fake") + self.assertQuerysetEqual(horizon.get_dashboards(), + ['', + '', + '', + '']) + + # Removal + self.assertEqual(len(Horizon._registry), 4) + horizon.unregister(MyDash) + self.assertEqual(len(Horizon._registry), 3) + with self.assertRaises(base.NotRegistered): + horizon.get_dashboard(MyDash) + + def test_site(self): + self.assertEqual(unicode(Horizon), "Horizon") + self.assertEqual(repr(Horizon), "") + dash = Horizon.get_dashboard('nova') + self.assertEqual(Horizon.get_default_dashboard(), dash) + user = User() + self.assertEqual(Horizon.get_user_home(user), dash.get_absolute_url()) + + def test_dashboard(self): + syspanel = horizon.get_dashboard("syspanel") + self.assertEqual(syspanel._registered_with, Horizon) + self.assertQuerysetEqual(syspanel.get_panels()['System Panel'], + ['', + '', + '', + '', + '', + '', + '', + '']) + self.assertEqual(syspanel.get_absolute_url(), "/syspanel/") + # Test registering a module with a dashboard that defines panels + # as a dictionary. + syspanel.register(MyPanel) + self.assertQuerysetEqual(syspanel.get_panels()['Other'], + ['']) + + # Test registering a module with a dashboard that defines panels + # as a tuple. + settings_dash = horizon.get_dashboard("settings") + settings_dash.register(MyPanel) + self.assertQuerysetEqual(settings_dash.get_panels(), + ['', + '']) + + def test_panels(self): + syspanel = horizon.get_dashboard("syspanel") + instances = syspanel.get_panel("instances") + self.assertEqual(instances._registered_with, syspanel) + self.assertEqual(instances.get_absolute_url(), "/syspanel/instances/") + + def test_lazy_urls(self): + urlpatterns = horizon.urls[0] + self.assertTrue(isinstance(urlpatterns, base.LazyURLPattern)) + # The following two methods simply should not raise any exceptions + iter(urlpatterns) + reversed(urlpatterns) diff --git a/horizon/horizon/tests/context_processor_tests.py b/horizon/horizon/tests/context_processor_tests.py new file mode 100644 index 00000000..b4e507d1 --- /dev/null +++ b/horizon/horizon/tests/context_processor_tests.py @@ -0,0 +1,45 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from horizon import context_processors +from horizon import test + + +class ContextProcessorTests(test.TestCase): + def setUp(self): + super(ContextProcessorTests, self).setUp() + self._prev_catalog = self.request.user.service_catalog + context_processors.horizon = self._real_horizon_context_processor + + def tearDown(self): + super(ContextProcessorTests, self).tearDown() + self.request.user.service_catalog = self._prev_catalog + + def test_object_store(self): + # Returns the object store service data when it's in the catalog + context = context_processors.horizon(self.request) + self.assertNotEqual(None, context['object_store_configured']) + + # Returns None when the object store is not in the catalog + new_catalog = [service for service in self.request.user.service_catalog + if service['type'] != 'object-store'] + self.request.user.service_catalog = new_catalog + context = context_processors.horizon(self.request) + self.assertEqual(None, context['object_store_configured']) diff --git a/horizon/horizon/tests/templates/base-sidebar.html b/horizon/horizon/tests/templates/base-sidebar.html new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/tests/templates/base.html b/horizon/horizon/tests/templates/base.html new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/tests/templates/splash.html b/horizon/horizon/tests/templates/splash.html new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/tests/templates/switch_tenants.html b/horizon/horizon/tests/templates/switch_tenants.html new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/tests/templatetag_tests.py b/horizon/horizon/tests/templatetag_tests.py new file mode 100644 index 00000000..d05c7d89 --- /dev/null +++ b/horizon/horizon/tests/templatetag_tests.py @@ -0,0 +1,38 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import re + +from django import dispatch, http, template +from django.utils.text import normalize_newlines + +from horizon import test + + +def single_line(text): + ''' Quick utility to make comparing template output easier. ''' + return re.sub(' +', + ' ', + normalize_newlines(text).replace('\n', '')).strip() + + +class TemplateTagTests(test.TestCase): + def setUp(self): + super(TemplateTagTests, self).setUp() diff --git a/horizon/horizon/tests/testsettings.py b/horizon/horizon/tests/testsettings.py new file mode 100644 index 00000000..145f2165 --- /dev/null +++ b/horizon/horizon/tests/testsettings.py @@ -0,0 +1,104 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os + +ROOT_PATH = os.path.dirname(os.path.abspath(__file__)) +DEBUG = True +TESTSERVER = 'http://testserver' +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': '/tmp/horizon.db'}} + +INSTALLED_APPS = ( + 'django.contrib.sessions', + 'django.contrib.messages', + 'horizon', + 'horizon.tests', + 'horizon.dashboards.nova', + 'horizon.dashboards.syspanel', + 'horizon.dashboards.settings', + 'mailer') + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.doc.XViewMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'horizon.middleware.HorizonMiddleware') + +TEMPLATE_CONTEXT_PROCESSORS = ( + 'django.core.context_processors.debug', + 'django.core.context_processors.i18n', + 'django.core.context_processors.request', + 'django.core.context_processors.media', + 'django.core.context_processors.static', + 'django.contrib.messages.context_processors.messages', + 'horizon.context_processors.horizon') + +ROOT_URLCONF = 'horizon.tests.testurls' +TEMPLATE_DIRS = (os.path.join(ROOT_PATH, 'tests', 'templates')) +SITE_ID = 1 +SITE_BRANDING = 'OpenStack' +SITE_NAME = 'openstack' +ENABLE_VNC = True +NOVA_DEFAULT_ENDPOINT = None +NOVA_DEFAULT_REGION = 'test' +NOVA_ACCESS_KEY = 'test' +NOVA_SECRET_KEY = 'test' + +QUANTUM_URL = '127.0.0.1' +QUANTUM_PORT = '9696' +QUANTUM_TENANT = '1234' +QUANTUM_CLIENT_VERSION = '0.1' +QUANTUM_ENABLED = True + +CREDENTIAL_AUTHORIZATION_DAYS = 2 +CREDENTIAL_DOWNLOAD_URL = TESTSERVER + '/credentials/' + +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' +NOSE_ARGS = ['--nocapture', + '--cover-package=horizon', + '--cover-inclusive'] + +# django-mailer uses a different config attribute +# even though it just wraps django.core.mail +MAILER_EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' +EMAIL_BACKEND = MAILER_EMAIL_BACKEND +SESSION_ENGINE = 'django.contrib.sessions.backends.cache' + +HORIZON_CONFIG = { + 'dashboards': ('nova', 'syspanel', 'settings',), + 'default_dashboard': 'nova', +} + +SWIFT_ACCOUNT = 'test' +SWIFT_USER = 'tester' +SWIFT_PASS = 'testing' +SWIFT_AUTHURL = 'http://swift/swiftapi/v1.0' + +OPENSTACK_ADDRESS = "localhost" +OPENSTACK_ADMIN_TOKEN = "openstack" +OPENSTACK_KEYSTONE_URL = "http://%s:5000/v2.0" % OPENSTACK_ADDRESS +OPENSTACK_KEYSTONE_ADMIN_URL = "http://%s:35357/v2.0" % OPENSTACK_ADDRESS +OPENSTACK_KEYSTONE_DEFAULT_ROLE_ID = "2" diff --git a/horizon/horizon/tests/testurls.py b/horizon/horizon/tests/testurls.py new file mode 100644 index 00000000..21fa7e36 --- /dev/null +++ b/horizon/horizon/tests/testurls.py @@ -0,0 +1,33 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +URL patterns for testing Horizon views. +""" + +from django.conf.urls.defaults import * + +import horizon + + +urlpatterns = patterns('', + url(r'^$', 'horizon.tests.views.fakeView', name='splash'), + url(r'', include(horizon.urls)), +) diff --git a/horizon/horizon/tests/views.py b/horizon/horizon/tests/views.py new file mode 100644 index 00000000..a8a1684a --- /dev/null +++ b/horizon/horizon/tests/views.py @@ -0,0 +1,31 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django import http + + +def fakeView(request): + resp = http.HttpResponse() + resp.write('

' + 'This is a fake httpresponse from a fake view for testing ' + ' purposes only' + '

') + + return resp diff --git a/horizon/horizon/users.py b/horizon/horizon/users.py new file mode 100644 index 00000000..a3b880c1 --- /dev/null +++ b/horizon/horizon/users.py @@ -0,0 +1,123 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Classes and methods related to user handling in Horizon. +""" + + +def get_user_from_request(request): + """ Checks the current session and returns a :class:`~horizon.users.User`. + + If the session contains user data the User will be treated as + authenticated and the :class:`~horizon.users.User` will have all + its attributes set. + + If not, the :class:`~horizon.users.User` will have no attributes set. + + If the session contains invalid data, + :exc:`~horizon.exceptions.NotAuthorized` will be raised. + """ + if 'user' not in request.session: + return User() + try: + return User(token=request.session['token'], + user=request.session['user'], + tenant_id=request.session['tenant_id'], + tenant_name=request.session['tenant'], + service_catalog=request.session['serviceCatalog'], + roles=request.session['roles']) + except KeyError: + # If any of those keys are missing from the session it is + # overwhelmingly likely that we're dealing with an outdated session. + request.session.clear() + raise exceptions.NotAuthorized(_("Your session has expired. " + "Please log in again.")) + + +class LazyUser(object): + def __get__(self, request, obj_type=None): + if not hasattr(request, '_cached_user'): + request._cached_user = get_user_from_request(request) + return request._cached_user + + +class User(object): + """ The main user class which Horizon expects. + + .. attribute:: token + + The id of the Keystone token associated with the current user/tenant. + + .. attribute:: username + + The name of the current user. + + .. attribute:: tenant_id + + The id of the Keystone tenant for the current user/token. + + .. attribute:: tenant_name + + The name of the Keystone tenant for the current user/token. + + .. attribute:: service_catalog + + The ``ServiceCatalog`` data returned by Keystone. + + .. attribute:: roles + + A list of dictionaries containing role names and ids as returned + by Keystone. + + .. attribute:: admin + + Boolean value indicating whether or not this user has admin + privileges. Internally mapped to :meth:`horizon.users.User.is_admin`. + """ + def __init__(self, token=None, user=None, tenant_id=None, + service_catalog=None, tenant_name=None, roles=None): + self.token = token + self.username = user + self.tenant_id = tenant_id + self.tenant_name = tenant_name + self.service_catalog = service_catalog + self.roles = roles or [] + + def is_authenticated(self): + """ + Evaluates whether this :class:`.User` instance has been authenticated. + Returns ``True`` or ``False``. + """ + # TODO: deal with token expiration + return self.token + + @property + def admin(self): + return self.is_admin() + + def is_admin(self): + """ + Evaluates whether this user has admin privileges. Returns + ``True`` or ``False``. + """ + for role in self.roles: + if role['name'].lower() == 'admin': + return True + return False diff --git a/horizon/horizon/version.py b/horizon/horizon/version.py new file mode 100644 index 00000000..999ae39f --- /dev/null +++ b/horizon/horizon/version.py @@ -0,0 +1,35 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC +# +# 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. + +version_info = {'branch_nick': u'LOCALBRANCH', + 'revision_id': 'LOCALREVISION', + 'revno': 0} + + +HORIZON_VERSION = ['2012', '1'] +YEAR, COUNT = HORIZON_VERSION +FINAL = False # This becomes true at Release Candidate time + + +def canonical_version_string(): + return '.'.join([YEAR, COUNT]) + + +def version_string(): + if FINAL: + return canonical_version_string() + else: + return '%s-dev' % (canonical_version_string(),) diff --git a/horizon/horizon/views/__init__.py b/horizon/horizon/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon/horizon/views/auth.py b/horizon/horizon/views/auth.py new file mode 100644 index 00000000..9322b049 --- /dev/null +++ b/horizon/horizon/views/auth.py @@ -0,0 +1,174 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django.conf import settings +from django import template +from django import shortcuts +from django.contrib import messages +from django.utils.translation import ugettext as _ +from openstackx.api import exceptions as api_exceptions + +from horizon import api +from horizon import exceptions +from horizon import forms + + +LOG = logging.getLogger(__name__) + + +def _is_admin(token): + for role in token.user['roles']: + if role['name'].lower() == 'admin': + return True + return False + + +def _set_session_data(request, token): + request.session['admin'] = _is_admin(token) + request.session['serviceCatalog'] = token.serviceCatalog + request.session['tenant'] = token.tenant['name'] + request.session['tenant_id'] = token.tenant['id'] + request.session['token'] = token.id + request.session['user'] = token.user['name'] + request.session['roles'] = token.user['roles'] + + +class Login(forms.SelfHandlingForm): + username = forms.CharField(max_length="20", label=_("User Name")) + password = forms.CharField(max_length="20", label=_("Password"), + widget=forms.PasswordInput(render_value=False)) + + def handle(self, request, data): + try: + if data.get('tenant'): + token = api.token_create(request, + data.get('tenant'), + data['username'], + data['password']) + + tenants = api.tenant_list_for_token(request, token.id) + tenant = None + for t in tenants: + if t.id == data.get('tenant'): + tenant = t + else: + token = api.token_create(request, + '', + data['username'], + data['password']) + + # Unscoped token + request.session['unscoped_token'] = token.id + request.user.username = data['username'] + + # Get the tenant list, and log in using first tenant + # FIXME (anthony): add tenant chooser here? + tenants = api.tenant_list_for_token(request, token.id) + + # Abort if there are no valid tenants for this user + if not tenants: + messages.error(request, + _('No tenants present for user: %(user)s') % + {"user": data['username']}) + return + + # Create a token. + # NOTE(gabriel): Keystone can return tenants that you're + # authorized to administer but not to log into as a user, so in + # the case of an Unauthorized error we should iterate through + # the tenants until one succeeds or we've failed them all. + while tenants: + tenant = tenants.pop() + try: + token = api.token_create_scoped(request, + tenant.id, + token.id) + break + except exceptions.Unauthorized as e: + token = None + if token is None: + raise exceptions.Unauthorized( + _("You are not authorized for any available tenants.")) + + LOG.info('Login form for user "%s". Service Catalog data:\n%s' % + (data['username'], token.serviceCatalog)) + _set_session_data(request, token) + + return shortcuts.redirect('horizon:nova:overview:index') + + except api_exceptions.Unauthorized as e: + msg = _('Error authenticating: %s') % e.message + LOG.exception(msg) + messages.error(request, msg) + except api_exceptions.ApiException as e: + messages.error(request, + _('Error authenticating with keystone: %s') % + e.message) + + +class LoginWithTenant(Login): + username = forms.CharField(max_length="20", + widget=forms.TextInput(attrs={'readonly': 'readonly'})) + tenant = forms.CharField(widget=forms.HiddenInput()) + + +def login(request): + if request.user and request.user.is_authenticated(): + if request.user.is_admin(): + return shortcuts.redirect('horizon:syspanel:overview:index') + else: + return shortcuts.redirect('horizon:nova:overview:index') + + form, handled = Login.maybe_handle(request) + if handled: + return handled + + return shortcuts.render(request, 'splash.html', {'form': form}) + + +def switch_tenants(request, tenant_id): + form, handled = LoginWithTenant.maybe_handle( + request, initial={'tenant': tenant_id, + 'username': request.user.username}) + if handled: + return handled + + unscoped_token = request.session.get('unscoped_token', None) + if unscoped_token: + try: + token = api.token_create_scoped(request, + tenant_id, + unscoped_token) + _set_session_data(request, token) + return shortcuts.redirect('horizon:nova:overview:index') + except exceptions.Unauthorized as e: + messages.error(_("You are not authorized for that tenant.")) + + return shortcuts.render(request, + 'switch_tenants.html', { + 'to_tenant': tenant_id, + 'form': form}) + + +def logout(request): + request.session.clear() + return shortcuts.redirect('splash') diff --git a/horizon/horizon/views/auth_forms.py b/horizon/horizon/views/auth_forms.py new file mode 100644 index 00000000..7664b694 --- /dev/null +++ b/horizon/horizon/views/auth_forms.py @@ -0,0 +1,141 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Forms used for Horizon's auth mechanisms. +""" + +import logging + +from django import shortcuts +from django.contrib import messages +from django.utils.translation import ugettext as _ +from openstackx.api import exceptions as api_exceptions + +from horizon import api +from horizon import base +from horizon import exceptions +from horizon import forms +from horizon import users + + +LOG = logging.getLogger(__name__) + + +def _set_session_data(request, token): + request.session['serviceCatalog'] = token.serviceCatalog + request.session['tenant'] = token.tenant['name'] + request.session['tenant_id'] = token.tenant['id'] + request.session['token'] = token.id + request.session['user'] = token.user['name'] + request.session['roles'] = token.user['roles'] + + +class Login(forms.SelfHandlingForm): + """ Form used for logging in a user. + + Handles authentication with Keystone, choosing a tenant, and fetching + a scoped token token for that tenant. Redirects to the URL returned + by :meth:`horizon.get_user_home` if successful. + + Subclass of :class:`~horizon.forms.SelfHandlingForm`. + """ + username = forms.CharField(max_length="20", label=_("User Name")) + password = forms.CharField(max_length="20", label=_("Password"), + widget=forms.PasswordInput(render_value=False)) + + def handle(self, request, data): + try: + if data.get('tenant', None): + token = api.token_create(request, + data.get('tenant'), + data['username'], + data['password']) + + tenants = api.tenant_list_for_token(request, token.id) + tenant = None + for t in tenants: + if t.id == data.get('tenant'): + tenant = t + _set_session_data(request, token) + user = users.get_user_from_request(request) + return shortcuts.redirect(base.Horizon.get_user_home(user)) + + elif data.get('username', None): + token = api.token_create(request, + '', + data['username'], + data['password']) + + # Unscoped token + request.session['unscoped_token'] = token.id + request.user.username = data['username'] + + # Get the tenant list, and log in using first tenant + # FIXME (anthony): add tenant chooser here? + tenants = api.tenant_list_for_token(request, token.id) + + # Abort if there are no valid tenants for this user + if not tenants: + messages.error(request, + _('No tenants present for user: %(user)s') % + {"user": data['username']}) + return + + # Create a token. + # NOTE(gabriel): Keystone can return tenants that you're + # authorized to administer but not to log into as a user, so in + # the case of an Unauthorized error we should iterate through + # the tenants until one succeeds or we've failed them all. + while tenants: + tenant = tenants.pop() + try: + token = api.token_create_scoped(request, + tenant.id, + token.id) + break + except api_exceptions.Unauthorized as e: + token = None + if token is None: + raise exceptions.NotAuthorized( + _("You are not authorized for any available tenants.")) + + _set_session_data(request, token) + user = users.get_user_from_request(request) + return shortcuts.redirect(base.Horizon.get_user_home(user)) + + except api_exceptions.Unauthorized as e: + msg = _('Error authenticating: %s') % e.message + LOG.exception(msg) + messages.error(request, msg) + except api_exceptions.ApiException as e: + messages.error(request, + _('Error authenticating with keystone: %s') % + e.message) + + +class LoginWithTenant(Login): + """ + Exactly like :class:`.Login` but includes the tenant id as a field + so that the process of choosing a default tenant is bypassed. + """ + username = forms.CharField(max_length="20", + widget=forms.TextInput(attrs={'readonly': 'readonly'})) + tenant = forms.CharField(widget=forms.HiddenInput()) diff --git a/horizon/setup.py b/horizon/setup.py new file mode 100644 index 00000000..66644ec5 --- /dev/null +++ b/horizon/setup.py @@ -0,0 +1,51 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +from setuptools import setup, find_packages, findall +from horizon import version + +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() + +setup( + name = "horizon", + version = version.canonical_version_string(), + url = 'https://github.com/openstack/horizon/', + license = 'Apache 2.0', + description = "A Django interface for OpenStack.", + long_description = read('README'), + author = 'Devin Carlen', + author_email = 'devin.carlen@gmail.com', + packages = find_packages(), + package_data = {'horizon': + [s[len('horizon/'):] for s in + findall('horizon/templates')]}, + install_requires = ['setuptools', 'mox>=0.5.3', 'django_nose'], + classifiers = [ + 'Development Status :: 4 - Beta', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Internet :: WWW/HTTP', + ] +) diff --git a/openstack-dashboard/README b/openstack-dashboard/README index 3c9053a2..f902e97d 100644 --- a/openstack-dashboard/README +++ b/openstack-dashboard/README @@ -1,53 +1,34 @@ +=================== OpenStack Dashboard -------------------- +=================== The OpenStack Dashboard is a reference implementation of a Django site that -uses the Django-Nova project to provide web based interactions with the -OpenStack Nova cloud controller. +uses the Horizon project to provide web based interactions with the various +OpenStack projects. Getting Started ---------------- +=============== -For local development, first create a virtualenv for local development. +For local development, first create a virtualenv for the project. A tool is included to create one for you: $ python tools/install_venv.py - Now that the virtualenv is created, you need to configure your local -environment. To do this, create a local_settings.py file in the local/ -directory. There is a local_settings.py.example file there that may be used -as a template. - -Finally, issue the django syncdb command: - - $ tools/with_venv.sh dashboard/manage.py syncdb - -If after you have specified the admin user the script appears to hang, it -probably means the installation of Nova being referred to in local_settings.py -is unavailable. - +environment. To do this, create a ``local_settings.py`` file in the ``local/`` +directory. There is a ``local_settings.py.example`` file there that may be +used as a template. If all is well you should now able to run the server locally: $ tools/with_venv.sh dashboard/manage.py runserver -Adding openstackx Extensions to Nova ------------------------------------- - -If you are seeing large numbers of 404 exceptions on operations such as listing -servers, you are probably not running the openstackx extensions that the -dashboard depends on. You will need to download the openstackx code from - -> https://github.com/cloudbuilders/openstackx - -and add the following option to your nova instantiation: - -> --osapi_extensions_path=/path/to/openstackx/extensions +Settings Up OpenStack +===================== -The rackspace cloudbuilders nova.sh script automates this process and creates a -full nova installation compatible with the dashboard. You can acquire this -script from the repository at +The recommended tool for installing and configuring the core OpenStack +components is `Devstack`_. Refer to their documentation for getting +Nova, Keystone, Glance, etc. up and running. -https://github.com/cloudbuilders/deploy.sh +.. _Devstack: http://devstack.org/ diff --git a/openstack-dashboard/dashboard/manage.py b/openstack-dashboard/dashboard/manage.py index 95d00493..73053f39 100755 --- a/openstack-dashboard/dashboard/manage.py +++ b/openstack-dashboard/dashboard/manage.py @@ -20,6 +20,8 @@ # under the License. from django.core.management import execute_manager + + try: import settings # Assumed to be in the same directory. except ImportError: @@ -31,5 +33,6 @@ except ImportError: "somehow.)\n" % __file__) sys.exit(1) + if __name__ == "__main__": execute_manager(settings) diff --git a/openstack-dashboard/dashboard/middleware.py b/openstack-dashboard/dashboard/middleware.py index d0678953..6a8772f7 100644 --- a/openstack-dashboard/dashboard/middleware.py +++ b/openstack-dashboard/dashboard/middleware.py @@ -24,6 +24,7 @@ from django import shortcuts from django.contrib import messages from openstackx.api import exceptions as api_exceptions + LOG = logging.getLogger('openstack_dashboard') diff --git a/openstack-dashboard/dashboard/settings.py b/openstack-dashboard/dashboard/settings.py index 5c43e549..1b5100be 100644 --- a/openstack-dashboard/dashboard/settings.py +++ b/openstack-dashboard/dashboard/settings.py @@ -52,10 +52,10 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', - 'django_openstack.middleware.keystone.AuthenticationMiddleware', + 'dashboard.middleware.DashboardLogUnhandledExceptionsMiddleware', + 'horizon.middleware.HorizonMiddleware', 'django.middleware.doc.XViewMiddleware', 'django.middleware.locale.LocaleMiddleware', - 'dashboard.middleware.DashboardLogUnhandledExceptionsMiddleware', ) TEMPLATE_CONTEXT_PROCESSORS = ( @@ -65,9 +65,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.media', 'django.core.context_processors.static', 'django.contrib.messages.context_processors.messages', - 'django_openstack.context_processors.object_store', - 'django_openstack.context_processors.tenants', - 'django_openstack.context_processors.quantum', + 'horizon.context_processors.horizon', ) TEMPLATE_LOADERS = ( @@ -85,12 +83,13 @@ STATICFILES_DIRS = ( INSTALLED_APPS = ( 'dashboard', - 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'django_openstack', - 'django_openstack.templatetags', + 'horizon', + 'horizon.dashboards.nova', + 'horizon.dashboards.syspanel', + 'horizon.dashboards.settings', 'mailer', ) @@ -98,6 +97,7 @@ TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',) MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' +SESSION_ENGINE = 'django.contrib.sessions.backends.cache' SESSION_EXPIRE_AT_BROWSER_CLOSE = True TIME_ZONE = None gettext_noop = lambda s: s @@ -119,6 +119,9 @@ ACCOUNT_ACTIVATION_DAYS = 7 TOTAL_CLOUD_RAM_GB = 10 +OPENSTACK_KEYSTONE_DEFAULT_ROLE = 'Member' +LIVE_SERVER_PORT = 8000 + try: from local.local_settings import * except Exception, e: @@ -136,4 +139,3 @@ if DEBUG: except ImportError: logging.info('Running in debug mode without debug_toolbar.') -OPENSTACK_KEYSTONE_DEFAULT_ROLE = 'Member' diff --git a/openstack-dashboard/dashboard/static/dashboard/images/favicon.ico b/openstack-dashboard/dashboard/static/dashboard/images/favicon.ico new file mode 100644 index 00000000..f3b9bf9c Binary files /dev/null and b/openstack-dashboard/dashboard/static/dashboard/images/favicon.ico differ diff --git a/openstack-dashboard/dashboard/templates/_login.html b/openstack-dashboard/dashboard/templates/_login.html index c2a1ead9..db15f938 100644 --- a/openstack-dashboard/dashboard/templates/_login.html +++ b/openstack-dashboard/dashboard/templates/_login.html @@ -1,4 +1,4 @@ -{% extends 'django_openstack/auth/_login.html' %} +{% extends 'horizon/auth/_login.html' %} {% load i18n %} {% block submit %} diff --git a/openstack-dashboard/dashboard/templates/_switch.html b/openstack-dashboard/dashboard/templates/_switch.html index 9b6004ae..69945832 100644 --- a/openstack-dashboard/dashboard/templates/_switch.html +++ b/openstack-dashboard/dashboard/templates/_switch.html @@ -1,4 +1,4 @@ -{% extends 'django_openstack/auth/_switch.html' %} +{% extends 'horizon/auth/_switch.html' %} {%load i18n%} {% block submit %} diff --git a/openstack-dashboard/dashboard/templates/_topbar.html b/openstack-dashboard/dashboard/templates/_topbar.html index ea1729e9..b75d38a5 100644 --- a/openstack-dashboard/dashboard/templates/_topbar.html +++ b/openstack-dashboard/dashboard/templates/_topbar.html @@ -1,21 +1,14 @@ -{%load i18n%} +{% load horizon i18n %} + diff --git a/openstack-dashboard/dashboard/templates/base.html b/openstack-dashboard/dashboard/templates/base.html index 97f92dde..ab136e19 100644 --- a/openstack-dashboard/dashboard/templates/base.html +++ b/openstack-dashboard/dashboard/templates/base.html @@ -13,6 +13,7 @@ + {% block headerjs %}{% endblock %} {% block headercss %}{% endblock %} diff --git a/openstack-dashboard/dashboard/templates/switch_tenants.html b/openstack-dashboard/dashboard/templates/switch_tenants.html index e19a9739..f0066431 100644 --- a/openstack-dashboard/dashboard/templates/switch_tenants.html +++ b/openstack-dashboard/dashboard/templates/switch_tenants.html @@ -1,4 +1,4 @@ -{%load i18n%} +{% load i18n %} @@ -9,7 +9,7 @@
-

{% trans "Log-in to tenant"%}: {{to_tenant}}

+

{% trans "Log-in to tenant"%}: {{ to_tenant }}


{% include "_messages.html" %} {% include '_login.html' %} diff --git a/openstack-dashboard/dashboard/tests.py b/openstack-dashboard/dashboard/tests.py deleted file mode 100644 index a7231056..00000000 --- a/openstack-dashboard/dashboard/tests.py +++ /dev/null @@ -1,38 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Nebula, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -''' Test for django mailer. - -This test is pretty much worthless, and should be removed once real testing of -views that send emails is implemented -''' - -from django import test -from django.core import mail -from mailer import engine -from mailer import send_mail - - -class DjangoMailerPresenceTest(test.TestCase): - def test_mailsent(self): - send_mail('subject', 'message_body', 'from@test.com', ['to@test.com']) - engine.send_all() - self.assertEqual(len(mail.outbox), 1) - self.assertEqual(mail.outbox[0].subject, 'subject') diff --git a/openstack-dashboard/dashboard/urls.py b/openstack-dashboard/dashboard/urls.py index 1f2c6bd9..ed60acc5 100644 --- a/openstack-dashboard/dashboard/urls.py +++ b/openstack-dashboard/dashboard/urls.py @@ -26,19 +26,13 @@ from django.conf.urls.defaults import * from django.conf.urls.static import static from django.conf import settings from django.contrib.staticfiles.urls import staticfiles_urlpatterns -from django.views import generic as generic_views -import django.views.i18n -from django_openstack import urls as django_openstack_urls +import horizon + urlpatterns = patterns('', url(r'^$', 'dashboard.views.splash', name='splash'), - url(r'^dash/$', 'django_openstack.dash.views.instances.usage', - name='dash_overview'), - url(r'^syspanel/$', 'django_openstack.syspanel.views.instances.usage', - name='syspanel_overview'), - url(r'^i18n/', include('django.conf.urls.i18n')), -) + url(r'', include(horizon.urls))) # Development static app and project media serving using the staticfiles app. urlpatterns += staticfiles_urlpatterns() @@ -47,6 +41,3 @@ urlpatterns += staticfiles_urlpatterns() # development. Only active if DEBUG==True and the URL prefix is a local # path. Production media should NOT be served by Django. urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) - -# NOTE(termie): just append them since we want the routes at the root -urlpatterns += django_openstack_urls.urlpatterns diff --git a/openstack-dashboard/dashboard/views.py b/openstack-dashboard/dashboard/views.py index 0d7fab07..fb06c62d 100644 --- a/openstack-dashboard/dashboard/views.py +++ b/openstack-dashboard/dashboard/views.py @@ -25,22 +25,20 @@ from django import template from django import shortcuts from django.views.decorators import vary -from django_openstack import api -from django_openstack.auth import views as auth_views +import horizon +from horizon.views import auth as auth_views + + +def user_home(user): + if user.admin: + return horizon.get_dashboard('syspanel').get_absolute_url() + return horizon.get_dashboard('dash').get_absolute_url() @vary.vary_on_cookie def splash(request): - if request.user: - if request.user.is_admin(): - return shortcuts.redirect('syspanel_overview') - else: - return shortcuts.redirect('dash_overview') - form, handled = auth_views.Login.maybe_handle(request) if handled: return handled - return shortcuts.render_to_response('splash.html', { - 'form': form, - }, context_instance=template.RequestContext(request)) + return shortcuts.render(request, 'splash.html', {'form': form}) diff --git a/openstack-dashboard/local/local_settings.py.example b/openstack-dashboard/local/local_settings.py.example index b6063244..ceebde77 100644 --- a/openstack-dashboard/local/local_settings.py.example +++ b/openstack-dashboard/local/local_settings.py.example @@ -13,8 +13,7 @@ DATABASES = { }, } -CACHE_BACKEND = 'dummy://' - +CACHE_BACKEND = 'locmem://' # Send email to the console by default EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' @@ -30,61 +29,73 @@ MAILER_EMAIL_BACKEND = EMAIL_BACKEND # EMAIL_HOST_USER = 'djangomail' # EMAIL_HOST_PASSWORD = 'top-secret!' +HORIZON_CONFIG = { + 'dashboards': ('nova', 'syspanel', 'settings',), + 'default_dashboard': 'nova', + 'user_home': 'dashboard.views.user_home', +} -OPENSTACK_KEYSTONE_URL = "http://localhost:5000/v2.0" +OPENSTACK_HOST = "127.0.0.1" +OPENSTACK_KEYSTONE_URL = "http://%s:5000/v2.0" % OPENSTACK_HOST # FIXME: this is only needed until keystone fixes its GET /tenants call # so that it doesn't return everything for admins -OPENSTACK_KEYSTONE_ADMIN_URL = "http://localhost:35357/v2.0" +OPENSTACK_KEYSTONE_ADMIN_URL = "http://%s:35357/v2.0" % OPENSTACK_HOST OPENSTACK_KEYSTONE_DEFAULT_ROLE = "Member" SWIFT_PAGINATE_LIMIT = 100 # Configure quantum connection details for networking QUANTUM_ENABLED = True -QUANTUM_URL = '127.0.0.1' +QUANTUM_URL = '%s' % OPENSTACK_HOST QUANTUM_PORT = '9696' QUANTUM_TENANT = '1234' QUANTUM_CLIENT_VERSION='0.1' -# If you have external monitoring links -EXTERNAL_MONITORING = [ - ['Nagios','http://foo.com'], - ['Ganglia','http://bar.com'], -] +# If you have external monitoring links, eg: +# EXTERNAL_MONITORING = [ +# ['Nagios','http://foo.com'], +# ['Ganglia','http://bar.com'], +# ] -# If you do not have external monitoring links -# EXTERNAL_MONITORING = [] - -# Uncomment the following segment to silence most logging -# django.db and boto DEBUG logging is extremely verbose. -#LOGGING = { -# 'version': 1, -# # set to True will disable all logging except that specified, unless -# # nothing is specified except that django.db.backends will still log, -# # even when set to True, so disable explicitly -# 'disable_existing_loggers': False, -# 'handlers': { -# 'null': { -# 'level': 'DEBUG', -# 'class': 'django.utils.log.NullHandler', -# }, -# 'console': { -# 'level': 'DEBUG', -# 'class': 'logging.StreamHandler', -# }, -# }, -# 'loggers': { -# # Comment or Uncomment these to turn on/off logging output -# 'django.db.backends': { -# 'handlers': ['null'], -# 'propagate': False, -# }, -# 'django_openstack': { -# 'handlers': ['null'], -# 'propagate': False, -# }, -# } -#} +LOGGING = { + 'version': 1, + # When set to True this will disable all logging except + # for loggers specified in this configuration dictionary. Note that + # if nothing is specified here and disable_existing_loggers is True, + # django.db.backends will still log unless it is disabled explicitly. + 'disable_existing_loggers': False, + 'handlers': { + 'null': { + 'level': 'DEBUG', + 'class': 'django.utils.log.NullHandler', + }, + 'console': { + # Set the level to "DEBUG" for verbose output logging. + 'level': 'INFO', + 'class': 'logging.StreamHandler', + }, + }, + 'loggers': { + # Logging from django.db.backends is VERY verbose, send to null + # by default. + 'django.db.backends': { + 'handlers': ['null'], + 'propagate': False, + }, + 'horizon': { + 'handlers': ['console'], + 'propagate': False, + }, + 'novaclient': { + 'handlers': ['console'], + 'propagate': False, + }, + 'keystoneclient': { + 'handlers': ['console'], + 'propagate': False, + }, + } +} # How much ram on each compute host? COMPUTE_HOST_RAM_GB = 16 diff --git a/openstack-dashboard/tools/install_venv.py b/openstack-dashboard/tools/install_venv.py index 37c35c33..50195979 100644 --- a/openstack-dashboard/tools/install_venv.py +++ b/openstack-dashboard/tools/install_venv.py @@ -107,9 +107,10 @@ def create_virtualenv(venv=VENV): def install_dependencies(venv=VENV): - print 'Installing dependencies with pip (this can take a while)...' - run_command([WITH_VENV, 'pip', 'install', '-E', venv, '-r', PIP_REQUIRES], - redirect_output=False) + print "Quietly installing dependencies..." + print "(This may take several minutes, don't panic)" + run_command([WITH_VENV, 'pip', 'install', '-q', '-E', + venv, '-r', PIP_REQUIRES], redirect_output=False) # Tell the virtual env how to "import dashboard" py = 'python%d.%d' % (sys.version_info[0], sys.version_info[1]) @@ -119,8 +120,8 @@ def install_dependencies(venv=VENV): def install_django_openstack(): - print 'Installing django_openstack in development mode...' - path = os.path.join(ROOT, '..', 'django-openstack') + print 'Installing horizon module in development mode...' + path = os.path.join(ROOT, '..', 'horizon') run_command([WITH_VENV, 'python', 'setup.py', 'develop'], cwd=path) diff --git a/openstack-dashboard/tools/pip-requires b/openstack-dashboard/tools/pip-requires index 1ac5a74c..17c3481c 100644 --- a/openstack-dashboard/tools/pip-requires +++ b/openstack-dashboard/tools/pip-requires @@ -1,27 +1,27 @@ -nose==1.0.0 +coverage Django==1.3 -django-nose==0.1.2 django-mailer +django-nose==0.1.2 django-registration==0.7 +eventlet +glance kombu +nose==1.0.0 +paste +PasteDeploy python-cloudfiles python-dateutil routes -webob sqlalchemy -paste -PasteDeploy sqlalchemy-migrate -eventlet -xattr pep8 pylint -coverage -glance sphinx +webob +xattr --e git+https://github.com/openstack/quantum.git#egg=quantum --e git+https://github.com/jacobian/openstack.compute.git#egg=openstack -e git+https://github.com/cloudbuilders/openstackx.git#egg=openstackx +-e git+https://github.com/jacobian/openstack.compute.git#egg=openstack +-e git+https://github.com/openstack/quantum.git#egg=quantum -e git+https://github.com/rackspace/python-novaclient.git#egg=python-novaclient -e git+https://github.com/4P/python-keystoneclient.git#egg=python-keystoneclient diff --git a/openstack-dashboard/tools/with_venv.sh b/openstack-dashboard/tools/with_venv.sh index 91299647..51efe29f 100755 --- a/openstack-dashboard/tools/with_venv.sh +++ b/openstack-dashboard/tools/with_venv.sh @@ -2,4 +2,3 @@ TOOLS=`dirname $0` VENV=$TOOLS/../.dashboard-venv source $VENV/bin/activate && $@ - diff --git a/run_tests.sh b/run_tests.sh index 94950878..89e021d8 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -2,17 +2,21 @@ function usage { echo "Usage: $0 [OPTION]..." - echo "Run Openstack Dashboard's test suite(s)" + echo "Run Horizon's test suite(s)" echo "" echo " -V, --virtual-env Always use virtualenv. Install automatically" echo " if not present" echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local" echo " environment" + echo " -c, --coverage Generate reports using Coverage" echo " -f, --force Force a clean re-build of the virtual" echo " environment. Useful when dependencies have" echo " been added." echo " -p, --pep8 Just run pep8" echo " -y, --pylint Just run pylint" + echo " --runserver Run the Django development server for" + echo " openstack-dashboard in the virtual" + echo " environment." echo " --docs Just build the documentation" echo " -h, --help Print this usage message" echo "" @@ -31,14 +35,21 @@ function process_option { -p|--pep8) let just_pep8=1;; -y|--pylint) let just_pylint=1;; -f|--force) let force=1;; + -c|--coverage) let with_coverage=1;; --docs) let just_docs=1;; + --runserver) let runserver=1;; *) testargs="$testargs $1" esac } +function run_server { + echo "Starting Django development server..." + ${django_wrapper} python openstack-dashboard/dashboard/manage.py runserver +} + function run_pylint { echo "Running pylint ..." - PYLINT_INCLUDE="openstack-dashboard/dashboard django-openstack/django_openstack" + PYLINT_INCLUDE="openstack-dashboard/dashboard horizon/horizon" ${django_wrapper} pylint --rcfile=.pylintrc -f parseable $PYLINT_INCLUDE > pylint.txt CODE=$? grep Global -A2 pylint.txt @@ -54,7 +65,7 @@ function run_pep8 { echo "Running pep8 ..." PEP8_EXCLUDE=vcsversion.py PEP8_OPTIONS="--exclude=$PEP8_EXCLUDE --repeat" - PEP8_INCLUDE="openstack-dashboard/dashboard django-openstack/django_openstack" + PEP8_INCLUDE="openstack-dashboard/dashboard horizon/horizon" echo "${django_wrapper} pep8 $PEP8_OPTIONS $PEP8_INCLUDE > pep8.txt" #${django_wrapper} pep8 $PEP8_OPTIONS $PEP8_INCLUDE > pep8.txt #perl string strips out the [ and ] characters @@ -63,15 +74,12 @@ function run_pep8 { function run_sphinx { echo "Building sphinx..." - echo "${django_wrapper} export DJANGO_SETTINGS_MODULE=local.local_settings" - ${django_wrapper} export DJANGO_SETTINGS_MODULE=local.local_settings - echo "${django_wrapper} python doc/generate_autodoc_index.py" - ${django_wrapper} python doc/generate_autodoc_index.py - echo "${django_wrapper} sphinx-build -b html doc/source build/sphinx/html" - ${django_wrapper} sphinx-build -b html doc/source build/sphinx/html + echo "export DJANGO_SETTINGS_MODULE=dashboard.settings" + export DJANGO_SETTINGS_MODULE=dashboard.settings + echo "${django_wrapper} sphinx-build -b html docs/source docs/build/html" + ${django_wrapper} sphinx-build -b html docs/source docs/build/html } - # DEFAULTS FOR RUN_TESTS.SH # venv=openstack-dashboard/.dashboard-venv @@ -80,12 +88,14 @@ dashboard_with_venv=tools/with_venv.sh always_venv=0 never_venv=0 force=0 +with_coverage=0 testargs="" django_wrapper="" dashboard_wrapper="" just_pep8=0 just_pylint=0 just_docs=0 +runserver=0 # PROCESS ARGUMENTS, OVERRIDE DEFAULTS for arg in "$@"; do @@ -108,6 +118,10 @@ then cd openstack-dashboard python tools/install_venv.py cd .. + cd horizon + python bootstrap.py + bin/buildout + cd .. django_wrapper="${django_with_venv}" dashboard_wrapper="${dashboard_with_venv}" else @@ -118,6 +132,10 @@ then cd openstack-dashboard python tools/install_venv.py cd .. + cd horizon + python bootstrap.py + bin/buildout + cd .. django_wrapper="${django_with_venv}" dashboard_wrapper="${dashboard_with_venv}" fi @@ -126,29 +144,33 @@ then fi function run_tests { - echo "Running django-openstack (core django) tests" + echo "Running Horizon application tests" ${django_wrapper} coverage erase - cd django-openstack - python bootstrap.py - bin/buildout - cd .. - ${django_wrapper} coverage run django-openstack/bin/test - # get results of the django-openstack tests + ${django_wrapper} coverage run horizon/bin/test + # get results of the Horizon tests OPENSTACK_RESULT=$? - echo "Running openstack-dashboard (django website) tests" + echo "Running openstack-dashboard (Django project) tests" cd openstack-dashboard + if [ -f local/local_settings.py ]; then + cp local/local_settings.py local/local_settings.py.bak + fi cp local/local_settings.py.example local/local_settings.py ${dashboard_wrapper} coverage run dashboard/manage.py test + if [ -f local/local_settings.py.bak ]; then + cp local/local_settings.py.bak local/local_settings.py + rm local/local_settings.py.bak + fi # get results of the openstack-dashboard tests DASHBOARD_RESULT=$? cd .. - - echo "Generating coverage reports" - ${django_wrapper} coverage combine - ${django_wrapper} coverage xml --omit='/usr*,setup.py,*egg*' - ${django_wrapper} coverage html --omit='/usr*,setup.py,*egg*' -d reports - exit $(($OPENSTACK_RESULT || $DASHBOARD_RESULT)) + if [ $with_coverage -eq 1 ]; then + echo "Generating coverage reports" + ${django_wrapper} coverage combine + ${django_wrapper} coverage xml -i --omit='/usr*,setup.py,*egg*' + ${django_wrapper} coverage html -i --omit='/usr*,setup.py,*egg*' -d reports + exit $(($OPENSTACK_RESULT || $DASHBOARD_RESULT)) + fi } if [ $just_docs -eq 1 ]; then @@ -166,4 +188,9 @@ if [ $just_pylint -eq 1 ]; then exit $? fi +if [ $runserver -eq 1 ]; then + run_server + exit $? +fi + run_tests || exit -- cgit v1.2.1