diff --git a/.gitignore b/.gitignore index 3fedf1d0ec..d353785ecb 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ pylint.txt reports horizon.egg-info openstack_dashboard/local/local_settings.py +openstack_dashboard/test/.secret_key_store doc/build/ doc/source/sourcecode /static/ diff --git a/MANIFEST.in b/MANIFEST.in index eefe078cd6..ce88137c14 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,7 @@ recursive-include bin *.js recursive-include doc *.py *.rst *.css *.js *.html *.conf *.jpg *.gif *.png *.css_t recursive-include horizon *.html *.css *.js *.csv *.template *.tmpl *.mo *.po -recursive-include openstack_dashboard *.html *.js *.css *.less *.csv *.template *.mo *.po *.example *.eot *.svg *.ttf *.woff *.png *.gif *.ico *.wsgi +recursive-include openstack_dashboard *.html *.js *.less *.mo *.po *.example *.eot *.svg *.ttf *.woff *.png *.ico *.wsgi recursive-include tools *.py *.sh include AUTHORS diff --git a/horizon/dashboards/nova/access_and_security/floating_ips/tables.py b/horizon/dashboards/nova/access_and_security/floating_ips/tables.py index 4b72c4d18d..1ad9f3b114 100644 --- a/horizon/dashboards/nova/access_and_security/floating_ips/tables.py +++ b/horizon/dashboards/nova/access_and_security/floating_ips/tables.py @@ -104,7 +104,7 @@ def get_instance_info(instance): def get_instance_link(datum): - view = "horizon:nova:instances_and_volumes:instances:detail" + view = "horizon:nova:instances:detail" if datum.instance_id: return urlresolvers.reverse(view, args=(datum.instance_id,)) else: diff --git a/horizon/dashboards/nova/access_and_security/floating_ips/tests.py b/horizon/dashboards/nova/access_and_security/floating_ips/tests.py index 8bd686bf6e..0fc0c96875 100644 --- a/horizon/dashboards/nova/access_and_security/floating_ips/tests.py +++ b/horizon/dashboards/nova/access_and_security/floating_ips/tests.py @@ -91,7 +91,7 @@ def test_associate_post_with_redirect(self): form_data = {'instance_id': server.id, 'ip_id': floating_ip.id} url = reverse('%s:associate' % NAMESPACE) - next = reverse("horizon:nova:instances_and_volumes:index") + next = reverse("horizon:nova:instances:index") res = self.client.post("%s?next=%s" % (url, next), form_data) self.assertRedirectsNoFollow(res, next) diff --git a/horizon/dashboards/nova/containers/tables.py b/horizon/dashboards/nova/containers/tables.py index 3b357b6cc0..6a1950622c 100644 --- a/horizon/dashboards/nova/containers/tables.py +++ b/horizon/dashboards/nova/containers/tables.py @@ -152,15 +152,14 @@ def sanitize_name(name): return name.split("/")[-1] -def get_size(obj): - return filesizeformat(obj.size) - - class ObjectsTable(tables.DataTable): name = tables.Column("name", verbose_name=_("Object Name"), filters=(sanitize_name,)) - size = tables.Column(get_size, verbose_name=_('Size')) + size = tables.Column("size", + verbose_name=_('Size'), + filters=(filesizeformat,), + summation="sum") def get_object_id(self, obj): return obj.name diff --git a/horizon/dashboards/nova/dashboard.py b/horizon/dashboards/nova/dashboard.py index c87bbceb61..e950873c39 100644 --- a/horizon/dashboards/nova/dashboard.py +++ b/horizon/dashboards/nova/dashboard.py @@ -23,7 +23,8 @@ class BasePanels(horizon.PanelGroup): slug = "compute" name = _("Manage Compute") panels = ('overview', - 'instances_and_volumes', + 'instances', + 'volumes', 'images_and_snapshots', 'access_and_security') diff --git a/horizon/dashboards/nova/images_and_snapshots/images/tables.py b/horizon/dashboards/nova/images_and_snapshots/images/tables.py index b92197f203..9ec80394ff 100644 --- a/horizon/dashboards/nova/images_and_snapshots/images/tables.py +++ b/horizon/dashboards/nova/images_and_snapshots/images/tables.py @@ -31,7 +31,7 @@ class LaunchImage(tables.LinkAction): name = "launch_image" verbose_name = _("Launch") - url = "horizon:nova:instances_and_volumes:instances:launch" + url = "horizon:nova:instances:launch" classes = ("btn-launch", "ajax-modal") def get_link_url(self, datum): diff --git a/horizon/dashboards/nova/images_and_snapshots/snapshots/forms.py b/horizon/dashboards/nova/images_and_snapshots/snapshots/forms.py index d59c8271fc..9add576d35 100644 --- a/horizon/dashboards/nova/images_and_snapshots/snapshots/forms.py +++ b/horizon/dashboards/nova/images_and_snapshots/snapshots/forms.py @@ -51,7 +51,7 @@ def handle(self, request, data): return shortcuts.redirect('horizon:nova:images_and_snapshots:' 'index') except: - redirect = reverse("horizon:nova:instances_and_volumes:index") + redirect = reverse("horizon:nova:instances:index") exceptions.handle(request, _('Unable to create snapshot.'), redirect=redirect) diff --git a/horizon/dashboards/nova/images_and_snapshots/snapshots/tables.py b/horizon/dashboards/nova/images_and_snapshots/snapshots/tables.py index 328fd3845c..62d32c4bde 100644 --- a/horizon/dashboards/nova/images_and_snapshots/snapshots/tables.py +++ b/horizon/dashboards/nova/images_and_snapshots/snapshots/tables.py @@ -30,7 +30,7 @@ class LaunchSnapshot(tables.LinkAction): name = "launch_snapshot" verbose_name = _("Launch") - url = "horizon:nova:instances_and_volumes:instances:launch" + url = "horizon:nova:instances:launch" classes = ("btn-launch", "ajax-modal") def get_link_url(self, datum): diff --git a/horizon/dashboards/nova/images_and_snapshots/snapshots/tests.py b/horizon/dashboards/nova/images_and_snapshots/snapshots/tests.py index d406f21002..740bb16f9c 100644 --- a/horizon/dashboards/nova/images_and_snapshots/snapshots/tests.py +++ b/horizon/dashboards/nova/images_and_snapshots/snapshots/tests.py @@ -51,7 +51,7 @@ def test_create_snapshot_get_with_invalid_status(self): url = reverse('horizon:nova:images_and_snapshots:snapshots:create', args=[server.id]) res = self.client.get(url) - redirect = reverse("horizon:nova:instances_and_volumes:index") + redirect = reverse("horizon:nova:instances:index") self.assertRedirectsNoFollow(res, redirect) def test_create_get_server_exception(self): @@ -64,7 +64,7 @@ def test_create_get_server_exception(self): url = reverse('horizon:nova:images_and_snapshots:snapshots:create', args=[server.id]) res = self.client.get(url) - redirect = reverse("horizon:nova:instances_and_volumes:index") + redirect = reverse("horizon:nova:instances:index") self.assertRedirectsNoFollow(res, redirect) def test_create_snapshot_post(self): @@ -107,5 +107,5 @@ def test_create_snapshot_post_exception(self): url = reverse('horizon:nova:images_and_snapshots:snapshots:create', args=[server.id]) res = self.client.post(url, formData) - redirect = reverse("horizon:nova:instances_and_volumes:index") + redirect = reverse("horizon:nova:instances:index") self.assertRedirectsNoFollow(res, redirect) diff --git a/horizon/dashboards/nova/images_and_snapshots/snapshots/views.py b/horizon/dashboards/nova/images_and_snapshots/snapshots/views.py index a45c4313e4..26860cb0eb 100644 --- a/horizon/dashboards/nova/images_and_snapshots/snapshots/views.py +++ b/horizon/dashboards/nova/images_and_snapshots/snapshots/views.py @@ -41,7 +41,7 @@ class CreateView(forms.ModalFormView): template_name = 'nova/images_and_snapshots/snapshots/create.html' def get_initial(self): - redirect = reverse('horizon:nova:instances_and_volumes:index') + redirect = reverse('horizon:nova:instances:index') instance_id = self.kwargs["instance_id"] try: self.instance = api.server_get(self.request, instance_id) diff --git a/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/tables.py b/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/tables.py index 01f30b77bc..f7f5be68fb 100644 --- a/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/tables.py +++ b/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/tables.py @@ -20,7 +20,7 @@ from horizon import api from horizon import tables -from ...instances_and_volumes.volumes import tables as volume_tables +from ...volumes import tables as volume_tables LOG = logging.getLogger(__name__) diff --git a/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/tests.py b/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/tests.py index 26beb2c544..fafecd9edd 100644 --- a/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/tests.py +++ b/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/tests.py @@ -32,12 +32,10 @@ class VolumeSnapshotsViewTests(test.TestCase): def test_create_snapshot_get(self): volume = self.volumes.first() - res = self.client.get(reverse('horizon:nova:instances_and_volumes:' - 'volumes:create_snapshot', + res = self.client.get(reverse('horizon:nova:volumes:create_snapshot', args=[volume.id])) - self.assertTemplateUsed(res, 'nova/instances_and_volumes/' - 'volumes/create_snapshot.html') + self.assertTemplateUsed(res, 'nova/volumes/create_snapshot.html') def test_create_snapshot_post(self): volume = self.volumes.first() @@ -56,7 +54,6 @@ def test_create_snapshot_post(self): 'volume_id': volume.id, 'name': snapshot.display_name, 'description': snapshot.display_description} - url = reverse('horizon:nova:instances_and_volumes:volumes:' - 'create_snapshot', args=[volume.id]) + url = reverse('horizon:nova:volumes:create_snapshot', args=[volume.id]) res = self.client.post(url, formData) self.assertRedirectsNoFollow(res, INDEX_URL) diff --git a/horizon/dashboards/nova/instances_and_volumes/__init__.py b/horizon/dashboards/nova/instances/__init__.py similarity index 100% rename from horizon/dashboards/nova/instances_and_volumes/__init__.py rename to horizon/dashboards/nova/instances/__init__.py diff --git a/horizon/dashboards/nova/instances_and_volumes/instances/forms.py b/horizon/dashboards/nova/instances/forms.py similarity index 93% rename from horizon/dashboards/nova/instances_and_volumes/instances/forms.py rename to horizon/dashboards/nova/instances/forms.py index 8e56f8b469..85cab29612 100644 --- a/horizon/dashboards/nova/instances_and_volumes/instances/forms.py +++ b/horizon/dashboards/nova/instances/forms.py @@ -45,5 +45,4 @@ def handle(self, request, data): except: exceptions.handle(request, _('Unable to update instance.')) - return shortcuts.redirect( - 'horizon:nova:instances_and_volumes:index') + return shortcuts.redirect('horizon:nova:instances:index') diff --git a/horizon/dashboards/nova/instances_and_volumes/panel.py b/horizon/dashboards/nova/instances/panel.py similarity index 83% rename from horizon/dashboards/nova/instances_and_volumes/panel.py rename to horizon/dashboards/nova/instances/panel.py index b12c225231..1a4ab8d670 100644 --- a/horizon/dashboards/nova/instances_and_volumes/panel.py +++ b/horizon/dashboards/nova/instances/panel.py @@ -20,9 +20,9 @@ from horizon.dashboards.nova import dashboard -class InstancesAndVolumes(horizon.Panel): - name = _("Instances & Volumes") - slug = 'instances_and_volumes' +class Instances(horizon.Panel): + name = _("Instances") + slug = 'instances' -dashboard.Nova.register(InstancesAndVolumes) +dashboard.Nova.register(Instances) diff --git a/horizon/dashboards/nova/instances_and_volumes/instances/tables.py b/horizon/dashboards/nova/instances/tables.py similarity index 94% rename from horizon/dashboards/nova/instances_and_volumes/instances/tables.py rename to horizon/dashboards/nova/instances/tables.py index cb51b72f3b..116ce1540e 100644 --- a/horizon/dashboards/nova/instances_and_volumes/instances/tables.py +++ b/horizon/dashboards/nova/instances/tables.py @@ -146,14 +146,14 @@ def action(self, request, obj_id): class LaunchLink(tables.LinkAction): name = "launch" verbose_name = _("Launch Instance") - url = "horizon:nova:instances_and_volumes:instances:launch" + url = "horizon:nova:instances:launch" classes = ("btn-launch", "ajax-modal") class EditInstance(tables.LinkAction): name = "edit" verbose_name = _("Edit Instance") - url = "horizon:nova:instances_and_volumes:instances:update" + url = "horizon:nova:instances:update" classes = ("ajax-modal", "btn-edit") @@ -170,7 +170,7 @@ def allowed(self, request, instance=None): class ConsoleLink(tables.LinkAction): name = "console" verbose_name = _("VNC Console") - url = "horizon:nova:instances_and_volumes:instances:detail" + url = "horizon:nova:instances:detail" classes = ("btn-console",) def allowed(self, request, instance=None): @@ -185,7 +185,7 @@ def get_link_url(self, datum): class LogLink(tables.LinkAction): name = "log" verbose_name = _("View Log") - url = "horizon:nova:instances_and_volumes:instances:detail" + url = "horizon:nova:instances:detail" classes = ("btn-log",) def allowed(self, request, instance=None): @@ -205,7 +205,7 @@ class AssociateIP(tables.LinkAction): def get_link_url(self, datum): base_url = urlresolvers.reverse(self.url) - next = urlresolvers.reverse("horizon:nova:instances_and_volumes:index") + next = urlresolvers.reverse("horizon:nova:instances:index") params = {"instance_id": self.table.get_object_id(datum), IPAssociationWorkflow.redirect_param_name: next} params = urlencode(params) @@ -222,14 +222,15 @@ def get_data(self, request, instance_id): def get_ips(instance): - template_name = 'nova/instances_and_volumes/instances/_instance_ips.html' + template_name = 'nova/instances/_instance_ips.html' context = {"instance": instance} return template.loader.render_to_string(template_name, context) def get_size(instance): if hasattr(instance, "full_flavor"): - size_string = _("%(name)s | %(RAM)s RAM | %(VCPU)s VCPU | %(disk)s Disk") + size_string = _("%(name)s | %(RAM)s RAM | %(VCPU)s VCPU " + "| %(disk)s Disk") vals = {'name': instance.full_flavor.name, 'RAM': sizeformat.mbformat(instance.full_flavor.ram), 'VCPU': instance.full_flavor.vcpus, @@ -237,6 +238,7 @@ def get_size(instance): return size_string % vals return _("Not available") + def get_keyname(instance): if hasattr(instance, "key_name"): keyname = instance.key_name @@ -263,8 +265,7 @@ class InstancesTable(tables.DataTable): ("image_snapshot", "Snapshotting"), ) name = tables.Column("name", - link=("horizon:nova:instances_and_volumes:" - "instances:detail"), + link=("horizon:nova:instances:detail"), verbose_name=_("Instance Name")) ip = tables.Column(get_ips, verbose_name=_("IP Address")) size = tables.Column(get_size, verbose_name=_("Size")) diff --git a/horizon/dashboards/nova/instances_and_volumes/instances/tabs.py b/horizon/dashboards/nova/instances/tabs.py similarity index 91% rename from horizon/dashboards/nova/instances_and_volumes/instances/tabs.py rename to horizon/dashboards/nova/instances/tabs.py index 94df3c5bf7..9e27c7d23c 100644 --- a/horizon/dashboards/nova/instances_and_volumes/instances/tabs.py +++ b/horizon/dashboards/nova/instances/tabs.py @@ -24,7 +24,7 @@ class OverviewTab(tabs.Tab): name = _("Overview") slug = "overview" - template_name = ("nova/instances_and_volumes/instances/" + template_name = ("nova/instances/" "_detail_overview.html") def get_context_data(self, request): @@ -34,7 +34,7 @@ def get_context_data(self, request): class LogTab(tabs.Tab): name = _("Log") slug = "log" - template_name = "nova/instances_and_volumes/instances/_detail_log.html" + template_name = "nova/instances/_detail_log.html" preload = False def get_context_data(self, request): @@ -53,7 +53,7 @@ def get_context_data(self, request): class VNCTab(tabs.Tab): name = _("VNC") slug = "vnc" - template_name = "nova/instances_and_volumes/instances/_detail_vnc.html" + template_name = "nova/instances/_detail_vnc.html" preload = False def get_context_data(self, request): diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_detail_log.html b/horizon/dashboards/nova/instances/templates/instances/_detail_log.html similarity index 68% rename from horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_detail_log.html rename to horizon/dashboards/nova/instances/templates/instances/_detail_log.html index 4eba3a0a91..8e60f63dca 100644 --- a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_detail_log.html +++ b/horizon/dashboards/nova/instances/templates/instances/_detail_log.html @@ -3,11 +3,11 @@

Instance Console Log

- {% url horizon:nova:instances_and_volumes:instances:console instance.id as console_url %} + {% url horizon:nova:instances:console instance.id as console_url %} {% trans "View Full Log" %}

-
+ diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_detail_overview.html b/horizon/dashboards/nova/instances/templates/instances/_detail_overview.html similarity index 94% rename from horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_detail_overview.html rename to horizon/dashboards/nova/instances/templates/instances/_detail_overview.html index 8a9cc561ca..cebc82862f 100644 --- a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_detail_overview.html +++ b/horizon/dashboards/nova/instances/templates/instances/_detail_overview.html @@ -87,7 +87,7 @@

{% trans "Volumes Attached" %}

{% for volume in instance.volumes %}
{% trans "Attached To" %}
- {{ volume.name }} {% trans "on" %} {{ volume.device }} + {{ volume.name }} {% trans "on" %} {{ volume.device }}
{% empty %}
{% trans "Volume" %}
diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_detail_vnc.html b/horizon/dashboards/nova/instances/templates/instances/_detail_vnc.html similarity index 77% rename from horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_detail_vnc.html rename to horizon/dashboards/nova/instances/templates/instances/_detail_vnc.html index 5768b41ebb..143b5e88cc 100644 --- a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_detail_vnc.html +++ b/horizon/dashboards/nova/instances/templates/instances/_detail_vnc.html @@ -6,5 +6,5 @@

{% trans "Instance VNC Console" %}

{% else %}

{% blocktrans %}VNC console is currently unavailabe. Please try again later.{% endblocktrans %} -{% trans "Reload" %}

-{% endif %} +{% trans "Reload" %}

+{% endif %} \ No newline at end of file diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_instance_ips.html b/horizon/dashboards/nova/instances/templates/instances/_instance_ips.html similarity index 100% rename from horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_instance_ips.html rename to horizon/dashboards/nova/instances/templates/instances/_instance_ips.html diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_launch_customize_help.html b/horizon/dashboards/nova/instances/templates/instances/_launch_customize_help.html similarity index 100% rename from horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_launch_customize_help.html rename to horizon/dashboards/nova/instances/templates/instances/_launch_customize_help.html diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_launch_details_help.html b/horizon/dashboards/nova/instances/templates/instances/_launch_details_help.html similarity index 100% rename from horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_launch_details_help.html rename to horizon/dashboards/nova/instances/templates/instances/_launch_details_help.html diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_launch_volumes_help.html b/horizon/dashboards/nova/instances/templates/instances/_launch_volumes_help.html similarity index 100% rename from horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_launch_volumes_help.html rename to horizon/dashboards/nova/instances/templates/instances/_launch_volumes_help.html diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_update.html b/horizon/dashboards/nova/instances/templates/instances/_update.html similarity index 72% rename from horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_update.html rename to horizon/dashboards/nova/instances/templates/instances/_update.html index 1a27ef3122..5bd8006d1b 100644 --- a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_update.html +++ b/horizon/dashboards/nova/instances/templates/instances/_update.html @@ -2,7 +2,7 @@ {% load i18n %} {% block form_id %}update_instance_form{% endblock %} -{% block form_action %}{% url horizon:nova:instances_and_volumes:instances:update instance.id %}{% endblock %} +{% block form_action %}{% url horizon:nova:instances:update instance.id %}{% endblock %} {% block modal-header %}{% trans "Edit Instance" %}{% endblock %} @@ -20,5 +20,5 @@

{% trans "Description:" %}

{% block modal-footer %} - {% trans "Cancel" %} + {% trans "Cancel" %} {% endblock %} diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/detail.html b/horizon/dashboards/nova/instances/templates/instances/detail.html similarity index 100% rename from horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/detail.html rename to horizon/dashboards/nova/instances/templates/instances/detail.html diff --git a/horizon/dashboards/nova/instances/templates/instances/index.html b/horizon/dashboards/nova/instances/templates/instances/index.html new file mode 100644 index 0000000000..71a9939c96 --- /dev/null +++ b/horizon/dashboards/nova/instances/templates/instances/index.html @@ -0,0 +1,11 @@ +{% extends 'nova/base.html' %} +{% load i18n %} +{% block title %}{% trans "Instances" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Instances") %} +{% endblock page_header %} + +{% block dash_main %} + {{ table.render }} +{% endblock %} diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/launch.html b/horizon/dashboards/nova/instances/templates/instances/launch.html similarity index 100% rename from horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/launch.html rename to horizon/dashboards/nova/instances/templates/instances/launch.html diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/update.html b/horizon/dashboards/nova/instances/templates/instances/update.html similarity index 80% rename from horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/update.html rename to horizon/dashboards/nova/instances/templates/instances/update.html index f87493ac7d..a0efa9d2ae 100644 --- a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/update.html +++ b/horizon/dashboards/nova/instances/templates/instances/update.html @@ -7,5 +7,5 @@ {% endblock page_header %} {% block dash_main %} - {% include 'nova/instances_and_volumes/instances/_update.html' %} + {% include 'nova/instances/_update.html' %} {% endblock %} diff --git a/horizon/dashboards/nova/instances_and_volumes/instances/tests.py b/horizon/dashboards/nova/instances/tests.py similarity index 87% rename from horizon/dashboards/nova/instances_and_volumes/instances/tests.py rename to horizon/dashboards/nova/instances/tests.py index f0897dd453..bc050ed105 100644 --- a/horizon/dashboards/nova/instances_and_volumes/instances/tests.py +++ b/horizon/dashboards/nova/instances/tests.py @@ -21,6 +21,7 @@ from django import http from django.core.urlresolvers import reverse from django.utils.http import urlencode +from django.utils.datastructures import SortedDict from mox import IsA, IgnoreArg from copy import deepcopy @@ -31,18 +32,90 @@ from .workflows import LaunchInstance -INDEX_URL = reverse('horizon:nova:instances_and_volumes:index') +INDEX_URL = reverse('horizon:nova:instances:index') -class InstanceViewTests(test.TestCase): +class InstanceTests(test.TestCase): + @test.create_stubs({api: ('flavor_list', 'server_list',)}) + def test_index(self): + api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list()) + api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) + + self.mox.ReplayAll() + + res = self.client.get( + reverse('horizon:nova:instances:index')) + + self.assertTemplateUsed(res, + 'nova/instances/index.html') + instances = res.context['instances_table'].data + + self.assertItemsEqual(instances, self.servers.list()) + + @test.create_stubs({api: ('server_list',)}) + def test_index_server_list_exception(self): + api.server_list(IsA(http.HttpRequest)).AndRaise(self.exceptions.nova) + + 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_table'].data), 0) + self.assertMessageCount(res, error=1) + + @test.create_stubs({api: ('flavor_list', 'server_list', 'flavor_get',)}) + def test_index_flavor_list_exception(self): + servers = self.servers.list() + flavors = self.flavors.list() + full_flavors = SortedDict([(f.id, f) for f in flavors]) + + api.server_list(IsA(http.HttpRequest)).AndReturn(servers) + api.flavor_list(IsA(http.HttpRequest)).AndRaise(self.exceptions.nova) + for server in servers: + api.flavor_get(IsA(http.HttpRequest), server.flavor["id"]). \ + AndReturn(full_flavors[server.flavor["id"]]) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:instances:index')) + + self.assertTemplateUsed(res, 'nova/instances/index.html') + instances = res.context['instances_table'].data + + self.assertItemsEqual(instances, self.servers.list()) + + @test.create_stubs({api: ('flavor_list', 'server_list', 'flavor_get',)}) + def test_index_flavor_get_exception(self): + servers = self.servers.list() + flavors = self.flavors.list() + max_id = max([int(flavor.id) for flavor in flavors]) + for server in servers: + max_id += 1 + server.flavor["id"] = max_id + + api.server_list(IsA(http.HttpRequest)).AndReturn(servers) + api.flavor_list(IsA(http.HttpRequest)).AndReturn(flavors) + for server in servers: + api.flavor_get(IsA(http.HttpRequest), server.flavor["id"]). \ + AndRaise(self.exceptions.nova) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:instances:index')) + + instances = res.context['instances_table'].data + + self.assertTemplateUsed(res, 'nova/instances/index.html') + self.assertMessageCount(res, error=len(servers)) + self.assertItemsEqual(instances, self.servers.list()) + @test.create_stubs({api: ('server_list', 'flavor_list', - 'server_delete', - 'volume_list',)}) + 'server_delete',)}) def test_terminate_instance(self): server = self.servers.first() - api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list()) api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) api.flavor_list(IgnoreArg()).AndReturn(self.flavors.list()) api.server_delete(IsA(http.HttpRequest), server.id) @@ -56,12 +129,10 @@ def test_terminate_instance(self): @test.create_stubs({api: ('server_list', 'flavor_list', - 'server_delete', - 'volume_list',)}) + 'server_delete',)}) def test_terminate_instance_exception(self): server = self.servers.first() - api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list()) api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) api.flavor_list(IgnoreArg()).AndReturn(self.flavors.list()) api.server_delete(IsA(http.HttpRequest), server.id) \ @@ -76,13 +147,11 @@ def test_terminate_instance_exception(self): @test.create_stubs({api: ('server_pause', 'server_list', - 'volume_list', 'flavor_list',)}) def test_pause_instance(self): server = self.servers.first() api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list()) - api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list()) api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) api.server_pause(IsA(http.HttpRequest), server.id) @@ -95,13 +164,11 @@ def test_pause_instance(self): @test.create_stubs({api: ('server_pause', 'server_list', - 'volume_list', 'flavor_list',)}) def test_pause_instance_exception(self): server = self.servers.first() api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list()) - api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list()) api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) api.server_pause(IsA(http.HttpRequest), server.id) \ .AndRaise(self.exceptions.nova) @@ -113,8 +180,7 @@ def test_pause_instance_exception(self): self.assertRedirectsNoFollow(res, INDEX_URL) - @test.create_stubs({api: ('volume_list', - 'server_unpause', + @test.create_stubs({api: ('server_unpause', 'server_list', 'flavor_list',)}) def test_unpause_instance(self): @@ -122,7 +188,6 @@ def test_unpause_instance(self): server.status = "PAUSED" api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list()) - api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list()) api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) api.server_unpause(IsA(http.HttpRequest), server.id) @@ -133,8 +198,7 @@ def test_unpause_instance(self): self.assertRedirectsNoFollow(res, INDEX_URL) - @test.create_stubs({api: ('volume_list', - 'server_unpause', + @test.create_stubs({api: ('server_unpause', 'server_list', 'flavor_list',)}) def test_unpause_instance_exception(self): @@ -142,7 +206,6 @@ def test_unpause_instance_exception(self): server.status = "PAUSED" api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list()) - api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list()) api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) api.server_unpause(IsA(http.HttpRequest), server.id) \ .AndRaise(self.exceptions.nova) @@ -154,15 +217,13 @@ def test_unpause_instance_exception(self): self.assertRedirectsNoFollow(res, INDEX_URL) - @test.create_stubs({api: ('volume_list', - 'server_reboot', + @test.create_stubs({api: ('server_reboot', 'server_list', 'flavor_list',)}) def test_reboot_instance(self): server = self.servers.first() api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list()) - api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list()) api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) api.server_reboot(IsA(http.HttpRequest), server.id) @@ -173,15 +234,13 @@ def test_reboot_instance(self): self.assertRedirectsNoFollow(res, INDEX_URL) - @test.create_stubs({api: ('volume_list', - 'server_reboot', + @test.create_stubs({api: ('server_reboot', 'server_list', 'flavor_list',)}) def test_reboot_instance_exception(self): server = self.servers.first() api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list()) - api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list()) api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) api.server_reboot(IsA(http.HttpRequest), server.id) \ .AndRaise(self.exceptions.nova) @@ -193,15 +252,13 @@ def test_reboot_instance_exception(self): self.assertRedirectsNoFollow(res, INDEX_URL) - @test.create_stubs({api: ('volume_list', - 'server_suspend', + @test.create_stubs({api: ('server_suspend', 'server_list', 'flavor_list',)}) def test_suspend_instance(self): server = self.servers.first() api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list()) - api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list()) api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) api.server_suspend(IsA(http.HttpRequest), unicode(server.id)) @@ -212,15 +269,13 @@ def test_suspend_instance(self): self.assertRedirectsNoFollow(res, INDEX_URL) - @test.create_stubs({api: ('volume_list', - 'server_suspend', + @test.create_stubs({api: ('server_suspend', 'server_list', 'flavor_list',)}) def test_suspend_instance_exception(self): server = self.servers.first() api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list()) - api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list()) api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) api.server_suspend(IsA(http.HttpRequest), unicode(server.id)).AndRaise(self.exceptions.nova) @@ -232,8 +287,7 @@ def test_suspend_instance_exception(self): self.assertRedirectsNoFollow(res, INDEX_URL) - @test.create_stubs({api: ('volume_list', - 'server_resume', + @test.create_stubs({api: ('server_resume', 'server_list', 'flavor_list',)}) def test_resume_instance(self): @@ -241,7 +295,6 @@ def test_resume_instance(self): server.status = "SUSPENDED" api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list()) - api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list()) api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) api.server_resume(IsA(http.HttpRequest), unicode(server.id)) @@ -252,8 +305,7 @@ def test_resume_instance(self): self.assertRedirectsNoFollow(res, INDEX_URL) - @test.create_stubs({api: ('volume_list', - 'server_resume', + @test.create_stubs({api: ('server_resume', 'server_list', 'flavor_list',)}) def test_resume_instance_exception(self): @@ -261,7 +313,6 @@ def test_resume_instance_exception(self): server.status = "SUSPENDED" api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list()) - api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list()) api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) api.server_resume(IsA(http.HttpRequest), unicode(server.id)).AndRaise(self.exceptions.nova) @@ -291,7 +342,7 @@ def test_instance_details_volumes(self): self.mox.ReplayAll() - url = reverse('horizon:nova:instances_and_volumes:instances:detail', + url = reverse('horizon:nova:instances:detail', args=[server.id]) res = self.client.get(url) @@ -315,7 +366,7 @@ def test_instance_details_volume_sorting(self): self.mox.ReplayAll() - url = reverse('horizon:nova:instances_and_volumes:instances:detail', + url = reverse('horizon:nova:instances:detail', args=[server.id]) res = self.client.get(url) @@ -342,7 +393,7 @@ def test_instance_details_metadata(self): self.mox.ReplayAll() - url = reverse('horizon:nova:instances_and_volumes:instances:detail', + url = reverse('horizon:nova:instances:detail', args=[server.id]) tg = InstanceDetailTabs(self.request, instance=server) qs = "?%s=%s" % (tg.param_name, tg.get_tab("overview").get_id()) @@ -368,7 +419,7 @@ def test_instance_log(self): self.mox.ReplayAll() - url = reverse('horizon:nova:instances_and_volumes:instances:console', + url = reverse('horizon:nova:instances:console', args=[server.id]) tg = InstanceDetailTabs(self.request, instance=server) qs = "?%s=%s" % (tg.param_name, tg.get_tab("log").get_id()) @@ -388,7 +439,7 @@ def test_instance_log_exception(self): self.mox.ReplayAll() - url = reverse('horizon:nova:instances_and_volumes:instances:console', + url = reverse('horizon:nova:instances:console', args=[server.id]) tg = InstanceDetailTabs(self.request, instance=server) qs = "?%s=%s" % (tg.param_name, tg.get_tab("log").get_id()) @@ -409,7 +460,7 @@ def test_instance_vnc(self): api.server_vnc_console(IgnoreArg(), server.id).AndReturn(console_mock) self.mox.ReplayAll() - url = reverse('horizon:nova:instances_and_volumes:instances:vnc', + url = reverse('horizon:nova:instances:vnc', args=[server.id]) res = self.client.get(url) redirect = CONSOLE_OUTPUT + '&title=%s(1)' % server.name @@ -424,7 +475,7 @@ def test_instance_vnc_exception(self): self.mox.ReplayAll() - url = reverse('horizon:nova:instances_and_volumes:instances:vnc', + url = reverse('horizon:nova:instances:vnc', args=[server.id]) res = self.client.get(url) @@ -437,8 +488,7 @@ def test_instance_vnc_exception(self): 'volume_snapshot_list', 'server_list', 'flavor_list', - 'server_delete', - 'volume_list',)}) + 'server_delete',)}) def test_create_instance_snapshot(self): server = self.servers.first() snapshot_server = deepcopy(server) @@ -455,7 +505,6 @@ def test_create_instance_snapshot(self): api.image_list_detailed(IsA(http.HttpRequest), marker=None).AndReturn([[], False]) api.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) - api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list()) api.server_list(IsA(http.HttpRequest)).AndReturn([snapshot_server]) api.flavor_list(IgnoreArg()).AndReturn(self.flavors.list()) @@ -483,12 +532,12 @@ def test_instance_update_get(self): self.mox.ReplayAll() - url = reverse('horizon:nova:instances_and_volumes:instances:update', + url = reverse('horizon:nova:instances:update', args=[server.id]) res = self.client.get(url) self.assertTemplateUsed(res, - 'nova/instances_and_volumes/instances/update.html') + 'nova/instances/update.html') @test.create_stubs({api: ('server_get',)}) def test_instance_update_get_server_get_exception(self): @@ -499,7 +548,7 @@ def test_instance_update_get_server_get_exception(self): self.mox.ReplayAll() - url = reverse('horizon:nova:instances_and_volumes:instances:update', + url = reverse('horizon:nova:instances:update', args=[server.id]) res = self.client.get(url) @@ -518,7 +567,7 @@ def test_instance_update_post(self): 'instance': server.id, 'name': server.name, 'tenant_id': self.tenant.id} - url = reverse('horizon:nova:instances_and_volumes:instances:update', + url = reverse('horizon:nova:instances:update', args=[server.id]) res = self.client.post(url, formData) @@ -538,7 +587,7 @@ def test_instance_update_post_api_exception(self): 'instance': server.id, 'name': server.name, 'tenant_id': self.tenant.id} - url = reverse('horizon:nova:instances_and_volumes:instances:update', + url = reverse('horizon:nova:instances:update', args=[server.id]) res = self.client.post(url, formData) @@ -578,14 +627,14 @@ def test_launch_get(self): self.mox.ReplayAll() - url = reverse('horizon:nova:instances_and_volumes:instances:launch') + url = reverse('horizon:nova:instances:launch') params = urlencode({"source_type": "image_id", "source_id": image.id}) res = self.client.get("%s?%s" % (url, params)) workflow = res.context['workflow'] self.assertTemplateUsed(res, - 'nova/instances_and_volumes/instances/launch.html') + 'nova/instances/launch.html') self.assertEqual(res.context['workflow'].name, LaunchInstance.name) step = workflow.get_step("setinstancedetailsaction") self.assertEqual(step.action.initial['image_id'], image.id) @@ -655,12 +704,12 @@ def test_launch_post(self): 'volume_id': volume_choice, 'device_name': device_name, 'count': 1} - url = reverse('horizon:nova:instances_and_volumes:instances:launch') + url = reverse('horizon:nova:instances:launch') res = self.client.post(url, form_data) self.assertNoFormErrors(res) self.assertRedirectsNoFollow(res, - reverse('horizon:nova:instances_and_volumes:index')) + reverse('horizon:nova:instances:index')) @test.create_stubs({api.glance: ('image_list_detailed',), api.nova: ('tenant_quota_usages', @@ -693,11 +742,11 @@ def test_launch_flavorlist_error(self): self.mox.ReplayAll() - url = reverse('horizon:nova:instances_and_volumes:instances:launch') + url = reverse('horizon:nova:instances:launch') res = self.client.get(url) self.assertTemplateUsed(res, - 'nova/instances_and_volumes/instances/launch.html') + 'nova/instances/launch.html') @test.create_stubs({api.glance: ('image_list_detailed',), api.nova: ('flavor_list', @@ -751,7 +800,7 @@ def test_launch_form_keystone_exception(self): 'groups': sec_group.name, 'volume_type': '', 'count': 1} - url = reverse('horizon:nova:instances_and_volumes:instances:launch') + url = reverse('horizon:nova:instances:launch') res = self.client.post(url, form_data) self.assertRedirectsNoFollow(res, INDEX_URL) @@ -810,7 +859,7 @@ def test_launch_form_instance_count_error(self): 'volume_id': volume_choice, 'device_name': device_name, 'count': 0} - url = reverse('horizon:nova:instances_and_volumes:instances:launch') + url = reverse('horizon:nova:instances:launch') res = self.client.post(url, form_data) self.assertContains(res, "greater than or equal to 1") diff --git a/horizon/dashboards/nova/instances_and_volumes/instances/urls.py b/horizon/dashboards/nova/instances/urls.py similarity index 81% rename from horizon/dashboards/nova/instances_and_volumes/instances/urls.py rename to horizon/dashboards/nova/instances/urls.py index 65e7c4c3ef..f040098389 100644 --- a/horizon/dashboards/nova/instances_and_volumes/instances/urls.py +++ b/horizon/dashboards/nova/instances/urls.py @@ -20,16 +20,16 @@ from django.conf.urls.defaults import patterns, url -from .views import UpdateView, DetailView, LaunchInstanceView +from .views import IndexView, UpdateView, DetailView, LaunchInstanceView INSTANCES = r'^(?P[^/]+)/%s$' -urlpatterns = patterns( - 'horizon.dashboards.nova.instances_and_volumes.instances.views', +urlpatterns = patterns('horizon.dashboards.nova.instances.views', + url(r'^$', IndexView.as_view(), name='index'), url(r'^launch$', LaunchInstanceView.as_view(), name='launch'), - url(INSTANCES % 'detail', DetailView.as_view(), name='detail'), + url(r'^(?P[^/]+)/$', DetailView.as_view(), name='detail'), url(INSTANCES % 'update', UpdateView.as_view(), name='update'), url(INSTANCES % 'console', 'console', name='console'), url(INSTANCES % 'vnc', 'vnc', name='vnc') diff --git a/horizon/dashboards/nova/instances_and_volumes/instances/views.py b/horizon/dashboards/nova/instances/views.py similarity index 70% rename from horizon/dashboards/nova/instances_and_volumes/instances/views.py rename to horizon/dashboards/nova/instances/views.py index 6dfa08f225..c5a83098a5 100644 --- a/horizon/dashboards/nova/instances_and_volumes/instances/views.py +++ b/horizon/dashboards/nova/instances/views.py @@ -26,24 +26,66 @@ from django import http from django import shortcuts from django.core.urlresolvers import reverse +from django.utils.datastructures import SortedDict from django.utils.translation import ugettext_lazy as _ from horizon import api from horizon import exceptions from horizon import forms from horizon import tabs +from horizon import tables from horizon import workflows from .forms import UpdateInstance from .tabs import InstanceDetailTabs +from .tables import InstancesTable from .workflows import LaunchInstance LOG = logging.getLogger(__name__) +class IndexView(tables.DataTableView): + table_class = InstancesTable + template_name = 'nova/instances/index.html' + + def get_data(self): + # Gather our instances + try: + instances = api.server_list(self.request) + except: + instances = [] + exceptions.handle(self.request, + _('Unable to retrieve instances.')) + # Gather our flavors and correlate our instances to them + if instances: + try: + flavors = api.flavor_list(self.request) + except: + flavors = [] + exceptions.handle(self.request, ignore=True) + + full_flavors = SortedDict([(str(flavor.id), flavor) + for flavor in flavors]) + # Loop through instances to get flavor info. + for instance in instances: + try: + flavor_id = instance.flavor["id"] + if flavor_id in full_flavors: + instance.full_flavor = full_flavors[flavor_id] + else: + # If the flavor_id is not in full_flavors list, + # get it via nova api. + instance.full_flavor = api.flavor_get(self.request, + flavor_id) + except: + msg = _('Unable to retrieve instance size information.') + exceptions.handle(self.request, msg) + return instances + + class LaunchInstanceView(workflows.WorkflowView): workflow_class = LaunchInstance - template_name = "nova/instances_and_volumes/instances/launch.html" + template_name = "nova/instances/launch.html" def get_initial(self): initial = super(LaunchInstanceView, self).get_initial() @@ -75,14 +117,14 @@ def vnc(request, instance_id): return shortcuts.redirect(console.url + ("&title=%s(%s)" % (instance.name, instance_id))) except: - redirect = reverse("horizon:nova:instances_and_volumes:index") + redirect = reverse("horizon:nova:instances:index") msg = _('Unable to get VNC console for instance "%s".') % instance_id exceptions.handle(request, msg, redirect=redirect) class UpdateView(forms.ModalFormView): form_class = UpdateInstance - template_name = 'nova/instances_and_volumes/instances/update.html' + template_name = 'nova/instances/update.html' context_object_name = 'instance' def get_object(self, *args, **kwargs): @@ -91,7 +133,7 @@ def get_object(self, *args, **kwargs): try: self.object = api.server_get(self.request, instance_id) except: - redirect = reverse("horizon:nova:instances_and_volumes:index") + redirect = reverse("horizon:nova:instances:index") msg = _('Unable to retrieve instance details.') exceptions.handle(self.request, msg, redirect=redirect) return self.object @@ -104,7 +146,7 @@ def get_initial(self): class DetailView(tabs.TabView): tab_group_class = InstanceDetailTabs - template_name = 'nova/instances_and_volumes/instances/detail.html' + template_name = 'nova/instances/detail.html' def get_context_data(self, **kwargs): context = super(DetailView, self).get_context_data(**kwargs) @@ -125,7 +167,7 @@ def get_data(self): instance.security_groups = api.server_security_groups( self.request, instance_id) except: - redirect = reverse('horizon:nova:instances_and_volumes:index') + redirect = reverse('horizon:nova:instances:index') exceptions.handle(self.request, _('Unable to retrieve details for ' 'instance "%s".') % instance_id, diff --git a/horizon/dashboards/nova/instances_and_volumes/instances/workflows.py b/horizon/dashboards/nova/instances/workflows.py similarity index 98% rename from horizon/dashboards/nova/instances_and_volumes/instances/workflows.py rename to horizon/dashboards/nova/instances/workflows.py index bf0e1170da..7f3fd8d8c0 100644 --- a/horizon/dashboards/nova/instances_and_volumes/instances/workflows.py +++ b/horizon/dashboards/nova/instances/workflows.py @@ -82,7 +82,8 @@ class VolumeOptionsAction(workflows.Action): class Meta: name = _("Volume Options") - help_text_template = ("nova/instances_and_volumes/instances/" + services = ('volume',) + help_text_template = ("nova/instances/" "_launch_volumes_help.html") def clean(self): @@ -176,7 +177,7 @@ class SetInstanceDetailsAction(workflows.Action): class Meta: name = _("Details") - help_text_template = ("nova/instances_and_volumes/instances/" + help_text_template = ("nova/instances/" "_launch_details_help.html") def clean(self): @@ -378,7 +379,7 @@ class CustomizeAction(workflows.Action): class Meta: name = _("Post-Creation") - help_text_template = ("nova/instances_and_volumes/instances/" + help_text_template = ("nova/instances/" "_launch_customize_help.html") @@ -393,7 +394,7 @@ class LaunchInstance(workflows.Workflow): finalize_button_name = _("Launch") success_message = _('Launched %s named "%s".') failure_message = _('Unable to launch %s named "%s".') - success_url = "horizon:nova:instances_and_volumes:index" + success_url = "horizon:nova:instances:index" default_steps = (SelectProjectUser, SetInstanceDetails, SetAccessControls, diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/index.html b/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/index.html deleted file mode 100644 index 133c467376..0000000000 --- a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/index.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends 'nova/base.html' %} -{% load i18n %} -{% block title %}Instances & Volumes{% endblock %} - -{% block page_header %} - {% include "horizon/common/_page_header.html" with title=_("Instances & Volumes") %} -{% endblock page_header %} - -{% block dash_main %} -
- {{ instances_table.render }} -
- -
- {{ volumes_table.render }} -
-{% endblock %} diff --git a/horizon/dashboards/nova/instances_and_volumes/tests.py b/horizon/dashboards/nova/instances_and_volumes/tests.py deleted file mode 100644 index 637551ec8b..0000000000 --- a/horizon/dashboards/nova/instances_and_volumes/tests.py +++ /dev/null @@ -1,153 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2012 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.core.urlresolvers import reverse -from django.utils.datastructures import SortedDict -from mox import IsA - -from horizon import api -from horizon import test - - -class InstancesAndVolumesViewTest(test.TestCase): - @test.create_stubs({api: ('flavor_list', 'server_list', 'volume_list',)}) - def test_index(self): - api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list()) - api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) - api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list()) - - self.mox.ReplayAll() - - res = self.client.get( - reverse('horizon:nova:instances_and_volumes:index')) - - self.assertTemplateUsed(res, - 'nova/instances_and_volumes/index.html') - instances = res.context['instances_table'].data - volumes = res.context['volumes_table'].data - - self.assertItemsEqual(instances, self.servers.list()) - self.assertItemsEqual(volumes, self.volumes.list()) - - @test.create_stubs({api: ('flavor_list', 'server_list', 'volume_list',)}) - def test_attached_volume(self): - api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list()) - api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) - api.volume_list(IsA(http.HttpRequest)) \ - .AndReturn(self.volumes.list()[1:3]) - - self.mox.ReplayAll() - - res = self.client.get( - reverse('horizon:nova:instances_and_volumes:index')) - - self.assertTemplateUsed(res, - 'nova/instances_and_volumes/index.html') - instances = res.context['instances_table'].data - resp_volumes = res.context['volumes_table'].data - - self.assertItemsEqual(instances, self.servers.list()) - self.assertItemsEqual(resp_volumes, self.volumes.list()[1:3]) - - self.assertContains(res, ">My Volume<", 1, 200) - self.assertContains(res, ">30GB<", 1, 200) - self.assertContains(res, ">3b189ac8-9166-ac7f-90c9-16c8bf9e01ac<", - 1, - 200) - self.assertContains(res, ">10GB<", 1, 200) - self.assertContains(res, ">In-Use<", 2, 200) - self.assertContains(res, "on /dev/hda", 1, 200) - self.assertContains(res, "on /dev/hdk", 1, 200) - - @test.create_stubs({api: ('server_list', 'volume_list',)}) - def test_index_server_list_exception(self): - api.server_list(IsA(http.HttpRequest)).AndRaise(self.exceptions.nova) - api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list()) - api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) - - self.mox.ReplayAll() - - res = self.client.get( - reverse('horizon:nova:instances_and_volumes:index')) - - self.assertTemplateUsed(res, - 'nova/instances_and_volumes/index.html') - self.assertEqual(len(res.context['instances_table'].data), 0) - - @test.create_stubs({api: ('flavor_list', 'server_list', - 'flavor_get', 'volume_list',)}) - def test_index_flavor_list_exception(self): - servers = self.servers.list() - flavors = self.flavors.list() - volumes = self.volumes.list() - full_flavors = SortedDict([(f.id, f) for f in flavors]) - - api.server_list(IsA(http.HttpRequest)).AndReturn(servers) - api.flavor_list(IsA(http.HttpRequest)).AndRaise(self.exceptions.nova) - for server in servers: - api.flavor_get(IsA(http.HttpRequest), server.flavor["id"]). \ - AndReturn(full_flavors[server.flavor["id"]]) - api.volume_list(IsA(http.HttpRequest)).AndReturn(volumes) - - self.mox.ReplayAll() - - res = self.client.get( - reverse('horizon:nova:instances_and_volumes:index')) - - self.assertTemplateUsed(res, - 'nova/instances_and_volumes/index.html') - instances = res.context['instances_table'].data - volumes = res.context['volumes_table'].data - - self.assertItemsEqual(instances, self.servers.list()) - self.assertItemsEqual(volumes, self.volumes.list()) - - @test.create_stubs({api: ('flavor_list', 'server_list', - 'flavor_get', 'volume_list',)}) - def test_index_flavor_get_exception(self): - servers = self.servers.list() - flavors = self.flavors.list() - volumes = self.volumes.list() - max_id = max([int(flavor.id) for flavor in flavors]) - for server in servers: - max_id += 1 - server.flavor["id"] = max_id - - api.server_list(IsA(http.HttpRequest)).AndReturn(servers) - api.flavor_list(IsA(http.HttpRequest)).AndReturn(flavors) - for server in servers: - api.flavor_get(IsA(http.HttpRequest), server.flavor["id"]). \ - AndRaise(self.exceptions.nova) - api.volume_list(IsA(http.HttpRequest)).AndReturn(volumes) - - self.mox.ReplayAll() - - res = self.client.get( - reverse('horizon:nova:instances_and_volumes:index')) - - instances = res.context['instances_table'].data - volumes = res.context['volumes_table'].data - - self.assertTemplateUsed(res, - 'nova/instances_and_volumes/index.html') - self.assertMessageCount(res, error=len(servers)) - self.assertItemsEqual(instances, self.servers.list()) - self.assertItemsEqual(volumes, self.volumes.list()) diff --git a/horizon/dashboards/nova/instances_and_volumes/views.py b/horizon/dashboards/nova/instances_and_volumes/views.py deleted file mode 100644 index edafbc24cd..0000000000 --- a/horizon/dashboards/nova/instances_and_volumes/views.py +++ /dev/null @@ -1,107 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2012 Nebula, Inc. -# Copyright 2012 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. - -""" -Views for Instances and Volumes. -""" -import logging - -from django.utils.translation import ugettext_lazy as _ -from django.utils.datastructures import SortedDict - -from horizon import api -from horizon import exceptions -from horizon import tables -from .instances.tables import InstancesTable -from .volumes.tables import VolumesTable - - -LOG = logging.getLogger(__name__) - - -class IndexView(tables.MultiTableView): - table_classes = (InstancesTable, VolumesTable) - template_name = 'nova/instances_and_volumes/index.html' - - def get_instances_data(self): - if not hasattr(self, "_instances"): - # Gather our instances - try: - instances = self._get_instances() - except: - instances = [] - exceptions.handle(self.request, - _('Unable to retrieve instances.')) - # Gather our flavors and correlate our instances to them - if instances: - try: - flavors = api.flavor_list(self.request) - except: - # If fails to retrieve flavor list, creates an empty list. - flavors = [] - - full_flavors = SortedDict([(str(flavor.id), flavor) - for flavor in flavors]) - # Loop through instances to get flavor info. - for instance in instances: - try: - flavor_id = instance.flavor["id"] - if flavor_id in full_flavors: - instance.full_flavor = full_flavors[flavor_id] - else: - # If the flavor_id is not in full_flavors list, - # gets it via nova api. - instance.full_flavor = api.flavor_get( - self.request, flavor_id) - except: - msg = _('Unable to retrieve instance \ - size information.') - exceptions.handle(self.request, msg) - self._instances = instances - return self._instances - - def get_volumes_data(self): - # Gather our volumes - try: - volumes = api.volume_list(self.request) - instances = SortedDict([(inst.id, inst) for inst in - self._get_instances()]) - for volume in volumes: - # It is possible to create a volume with no name through the - # EC2 API, use the ID in those cases. - if not volume.display_name: - volume.display_name = volume.id - - description = getattr(volume, 'display_description', '') - - for att in volume.attachments: - server_id = att.get('server_id', None) - att['instance'] = instances.get(server_id, None) - except: - volumes = [] - exceptions.handle(self.request, - _('Unable to retrieve volume list.')) - return volumes - - def _get_instances(self): - if not hasattr(self, "_instances_list"): - self._instances_list = api.server_list(self.request) - return self._instances_list diff --git a/horizon/dashboards/nova/instances_and_volumes/instances/__init__.py b/horizon/dashboards/nova/volumes/__init__.py similarity index 100% rename from horizon/dashboards/nova/instances_and_volumes/instances/__init__.py rename to horizon/dashboards/nova/volumes/__init__.py diff --git a/horizon/dashboards/nova/instances_and_volumes/volumes/forms.py b/horizon/dashboards/nova/volumes/forms.py similarity index 97% rename from horizon/dashboards/nova/instances_and_volumes/volumes/forms.py rename to horizon/dashboards/nova/volumes/forms.py index b35c181702..7708af8547 100644 --- a/horizon/dashboards/nova/instances_and_volumes/volumes/forms.py +++ b/horizon/dashboards/nova/volumes/forms.py @@ -56,10 +56,9 @@ def handle(self, request, data): return self.api_error(e.messages[0]) except: exceptions.handle(request, ignore=True) - return self.api_error(_("Unable to create volume.")) - return shortcuts.redirect("horizon:nova:instances_and_volumes:index") + return shortcuts.redirect("horizon:nova:volumes:index") class AttachForm(forms.SelfHandlingForm): @@ -116,8 +115,7 @@ def handle(self, request, data): except: exceptions.handle(request, _('Unable to attach volume.')) - return shortcuts.redirect( - "horizon:nova:instances_and_volumes:index") + return shortcuts.redirect("horizon:nova:volumes:index") class CreateSnapshotForm(forms.SelfHandlingForm): diff --git a/horizon/dashboards/nova/instances_and_volumes/urls.py b/horizon/dashboards/nova/volumes/panel.py similarity index 53% rename from horizon/dashboards/nova/instances_and_volumes/urls.py rename to horizon/dashboards/nova/volumes/panel.py index 7437f3e951..869c01ecaf 100644 --- a/horizon/dashboards/nova/instances_and_volumes/urls.py +++ b/horizon/dashboards/nova/volumes/panel.py @@ -1,9 +1,5 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2012 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# # Copyright 2012 Nebula, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -18,15 +14,16 @@ # License for the specific language governing permissions and limitations # under the License. -from django.conf.urls.defaults import * +from django.utils.translation import ugettext_lazy as _ + +import horizon +from horizon.dashboards.nova import dashboard + -from .instances import urls as instance_urls -from .views import IndexView -from .volumes import urls as volume_urls +class Volumes(horizon.Panel): + name = _("Volumes") + slug = 'volumes' + services = ('volume',) -urlpatterns = patterns('horizon.dashboards.nova.instances_and_volumes', - url(r'^$', IndexView.as_view(), name='index'), - url(r'^instances/', include(instance_urls, namespace='instances')), - url(r'^volumes/', include(volume_urls, namespace='volumes')), -) +dashboard.Nova.register(Volumes) diff --git a/horizon/dashboards/nova/instances_and_volumes/volumes/tables.py b/horizon/dashboards/nova/volumes/tables.py similarity index 91% rename from horizon/dashboards/nova/instances_and_volumes/volumes/tables.py rename to horizon/dashboards/nova/volumes/tables.py index 0632db1495..5d6e082d94 100644 --- a/horizon/dashboards/nova/instances_and_volumes/volumes/tables.py +++ b/horizon/dashboards/nova/volumes/tables.py @@ -29,7 +29,6 @@ LOG = logging.getLogger(__name__) -URL_PREFIX = "horizon:nova:instances_and_volumes" DELETABLE_STATES = ("available", "error") @@ -49,14 +48,14 @@ def allowed(self, request, volume=None): class CreateVolume(tables.LinkAction): name = "create" verbose_name = _("Create Volume") - url = "%s:volumes:create" % URL_PREFIX + url = "horizon:nova:volumes:create" classes = ("ajax-modal", "btn-create") class EditAttachments(tables.LinkAction): name = "attachments" verbose_name = _("Edit Attachments") - url = "%s:volumes:attach" % URL_PREFIX + url = "horizon:nova:volumes:attach" classes = ("ajax-modal", "btn-edit") def allowed(self, request, volume=None): @@ -66,7 +65,7 @@ def allowed(self, request, volume=None): class CreateSnapshot(tables.LinkAction): name = "snapshots" verbose_name = _("Create Snapshot") - url = "%s:volumes:create_snapshot" % URL_PREFIX + url = "horizon:nova:volumes:create_snapshot" classes = ("ajax-modal", "btn-camera") def allowed(self, request, volume=None): @@ -87,7 +86,7 @@ def get_size(volume): def get_attachment_name(request, attachment): server_id = attachment.get("server_id", None) - if "instance" in attachment: + if "instance" in attachment and attachment['instance']: name = attachment["instance"].name else: try: @@ -98,7 +97,7 @@ def get_attachment_name(request, attachment): exceptions.handle(request, _("Unable to retrieve " "attachment information.")) try: - url = reverse("%s:instances:detail" % URL_PREFIX, args=(server_id,)) + url = reverse("horizon:nova:instances:detail", args=(server_id,)) instance = '%s' % (url, name) except NoReverseMatch: instance = name @@ -116,7 +115,7 @@ def get_raw_data(self, volume): attachments = [] # Filter out "empty" attachments which the client returns... for attachment in [att for att in volume.attachments if att]: - # When a volume is first attached it may return the server_id + # When a volume is attached it may return the server_id # without the server name... instance = get_attachment_name(request, attachment) vals = {"instance": instance, @@ -132,8 +131,9 @@ class VolumesTableBase(tables.DataTable): ("creating", None), ("error", False), ) - name = tables.Column("display_name", verbose_name=_("Name"), - link="%s:volumes:detail" % URL_PREFIX) + name = tables.Column("display_name", + verbose_name=_("Name"), + link="horizon:nova:volumes:detail") description = tables.Column("display_description", verbose_name=_("Description"), truncate=40) @@ -151,7 +151,7 @@ def get_object_display(self, obj): class VolumesTable(VolumesTableBase): name = tables.Column("display_name", verbose_name=_("Name"), - link="%s:volumes:detail" % URL_PREFIX) + link="horizon:nova:volumes:detail") attachments = AttachmentColumn("attachments", verbose_name=_("Attached To")) @@ -177,7 +177,7 @@ def action(self, request, obj_id): api.volume_detach(request, attachment.get('server_id', None), obj_id) def get_success_url(self, request): - return reverse('%s:index' % URL_PREFIX) + return reverse('horizon:nova:volumes:index') class AttachedInstanceColumn(tables.Column): diff --git a/horizon/dashboards/nova/instances_and_volumes/volumes/tabs.py b/horizon/dashboards/nova/volumes/tabs.py similarity index 92% rename from horizon/dashboards/nova/instances_and_volumes/volumes/tabs.py rename to horizon/dashboards/nova/volumes/tabs.py index d3836604e9..eaf533b2d7 100644 --- a/horizon/dashboards/nova/instances_and_volumes/volumes/tabs.py +++ b/horizon/dashboards/nova/volumes/tabs.py @@ -25,7 +25,7 @@ class OverviewTab(tabs.Tab): name = _("Overview") slug = "overview" - template_name = ("nova/instances_and_volumes/volumes/" + template_name = ("nova/volumes/" "_detail_overview.html") def get_context_data(self, request): @@ -36,7 +36,7 @@ def get_context_data(self, request): att['instance'] = api.nova.server_get(request, att['server_id']) except: - redirect = reverse('horizon:nova:instances_and_volumes:index') + redirect = reverse('horizon:nova:volumes:index') exceptions.handle(self.request, _('Unable to retrieve volume details.'), redirect=redirect) diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/_attach.html b/horizon/dashboards/nova/volumes/templates/volumes/_attach.html similarity index 73% rename from horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/_attach.html rename to horizon/dashboards/nova/volumes/templates/volumes/_attach.html index 4b21e05cab..fa1898af6d 100644 --- a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/_attach.html +++ b/horizon/dashboards/nova/volumes/templates/volumes/_attach.html @@ -2,7 +2,7 @@ {% load i18n %} {% block form_id %}attach_volume_form{% endblock %} -{% block form_action %}{% url horizon:nova:instances_and_volumes:volumes:attach volume.id %}{% endblock %} +{% block form_action %}{% url horizon:nova:volumes:attach volume.id %}{% endblock %} {% block form_class %}{{ block.super }} horizontal split_half{% endblock %} {% block modal_id %}attach_volume_modal{% endblock %} @@ -17,5 +17,5 @@

{% trans "Attach To Instance" %}

{% block modal-footer %} - {% trans "Cancel" %} + {% trans "Cancel" %} {% endblock %} diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/_create.html b/horizon/dashboards/nova/volumes/templates/volumes/_create.html similarity index 89% rename from horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/_create.html rename to horizon/dashboards/nova/volumes/templates/volumes/_create.html index 47f1adef28..4c8525785f 100644 --- a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/_create.html +++ b/horizon/dashboards/nova/volumes/templates/volumes/_create.html @@ -2,7 +2,7 @@ {% load i18n horizon humanize %} {% block form_id %}{% endblock %} -{% block form_action %}{% url horizon:nova:instances_and_volumes:volumes:create %}{% endblock %} +{% block form_action %}{% url horizon:nova:volumes:create %}{% endblock %} {% block modal_id %}create_volume_modal{% endblock %} {% block modal-header %}{% trans "Create Volume" %}{% endblock %} @@ -53,5 +53,5 @@

{% trans "Volume Quotas" %}

{% block modal-footer %} - {% trans "Cancel" %} + {% trans "Cancel" %} {% endblock %} diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/_create_snapshot.html b/horizon/dashboards/nova/volumes/templates/volumes/_create_snapshot.html similarity index 73% rename from horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/_create_snapshot.html rename to horizon/dashboards/nova/volumes/templates/volumes/_create_snapshot.html index b4db08ea11..2f28fc1037 100644 --- a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/_create_snapshot.html +++ b/horizon/dashboards/nova/volumes/templates/volumes/_create_snapshot.html @@ -2,7 +2,7 @@ {% load i18n %} {% block form_id %}{% endblock %} -{% block form_action %}{% url horizon:nova:instances_and_volumes:volumes:create_snapshot volume_id %}{% endblock %} +{% block form_action %}{% url horizon:nova:volumes:create_snapshot volume_id %}{% endblock %} {% block modal_id %}create_volume_snapshot_modal{% endblock %} {% block modal-header %}{% trans "Create Volume Snapshot" %}{% endblock %} @@ -21,5 +21,5 @@

{% trans "Description" %}:

{% block modal-footer %} - {% trans "Cancel" %} + {% trans "Cancel" %} {% endblock %} diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/_detail_overview.html b/horizon/dashboards/nova/volumes/templates/volumes/_detail_overview.html similarity index 92% rename from horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/_detail_overview.html rename to horizon/dashboards/nova/volumes/templates/volumes/_detail_overview.html index 4f242682ec..1844ddcee2 100644 --- a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/_detail_overview.html +++ b/horizon/dashboards/nova/volumes/templates/volumes/_detail_overview.html @@ -37,7 +37,7 @@

{% trans "Attachments" %}

{% for attachment in volume.attachments %}
{% trans "Attached To" %}
- {% url horizon:nova:instances_and_volumes:instances:detail attachment.server_id as instance_url%} + {% url horizon:nova:instances:detail attachment.server_id as instance_url%} {{ attachment.instance.name }} {% trans "on" %} {{ attachment.device }}
diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/attach.html b/horizon/dashboards/nova/volumes/templates/volumes/attach.html similarity index 80% rename from horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/attach.html rename to horizon/dashboards/nova/volumes/templates/volumes/attach.html index 3417f1ee09..e46d86212e 100644 --- a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/attach.html +++ b/horizon/dashboards/nova/volumes/templates/volumes/attach.html @@ -7,5 +7,5 @@ {% endblock page_header %} {% block dash_main %} - {% include 'nova/instances_and_volumes/volumes/_attach.html' %} + {% include 'nova/volumes/_attach.html' %} {% endblock %} diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/create.html b/horizon/dashboards/nova/volumes/templates/volumes/create.html similarity index 79% rename from horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/create.html rename to horizon/dashboards/nova/volumes/templates/volumes/create.html index ac8e233aef..0c7a6b33ba 100644 --- a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/create.html +++ b/horizon/dashboards/nova/volumes/templates/volumes/create.html @@ -7,5 +7,5 @@ {% endblock page_header %} {% block dash_main %} - {% include 'nova/instances_and_volumes/volumes/_create.html' %} + {% include 'nova/volumes/_create.html' %} {% endblock %} diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/create_snapshot.html b/horizon/dashboards/nova/volumes/templates/volumes/create_snapshot.html similarity index 79% rename from horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/create_snapshot.html rename to horizon/dashboards/nova/volumes/templates/volumes/create_snapshot.html index 0c870ee032..d73294789a 100644 --- a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/create_snapshot.html +++ b/horizon/dashboards/nova/volumes/templates/volumes/create_snapshot.html @@ -7,5 +7,5 @@ {% endblock page_header %} {% block dash_main %} - {% include 'nova/instances_and_volumes/volumes/_create_snapshot.html' %} + {% include 'nova/volumes/_create_snapshot.html' %} {% endblock %} diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/detail.html b/horizon/dashboards/nova/volumes/templates/volumes/detail.html similarity index 100% rename from horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/detail.html rename to horizon/dashboards/nova/volumes/templates/volumes/detail.html diff --git a/horizon/dashboards/nova/volumes/templates/volumes/index.html b/horizon/dashboards/nova/volumes/templates/volumes/index.html new file mode 100644 index 0000000000..5b8efdc72f --- /dev/null +++ b/horizon/dashboards/nova/volumes/templates/volumes/index.html @@ -0,0 +1,11 @@ +{% extends 'nova/base.html' %} +{% load i18n %} +{% block title %}{% trans "Volumes" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Volumes") %} +{% endblock page_header %} + +{% block dash_main %} + {{ table.render }} +{% endblock %} diff --git a/horizon/dashboards/nova/instances_and_volumes/volumes/tests.py b/horizon/dashboards/nova/volumes/tests.py similarity index 89% rename from horizon/dashboards/nova/instances_and_volumes/volumes/tests.py rename to horizon/dashboards/nova/volumes/tests.py index bfdef759e5..868cfa1bd8 100644 --- a/horizon/dashboards/nova/instances_and_volumes/volumes/tests.py +++ b/horizon/dashboards/nova/volumes/tests.py @@ -43,10 +43,10 @@ def test_create_volume(self): self.mox.ReplayAll() - url = reverse('horizon:nova:instances_and_volumes:volumes:create') + url = reverse('horizon:nova:volumes:create') res = self.client.post(url, formData) - redirect_url = reverse('horizon:nova:instances_and_volumes:index') + redirect_url = reverse('horizon:nova:volumes:index') self.assertRedirectsNoFollow(res, redirect_url) @test.create_stubs({api: ('tenant_quota_usages',)}) @@ -62,7 +62,7 @@ def test_create_volume_gb_used_over_alloted_quota(self): self.mox.ReplayAll() - url = reverse('horizon:nova:instances_and_volumes:volumes:create') + url = reverse('horizon:nova:volumes:create') res = self.client.post(url, formData) expected_error = [u'A volume of 5000GB cannot be created as you only' @@ -83,7 +83,7 @@ def test_create_volume_number_over_alloted_quota(self): self.mox.ReplayAll() - url = reverse('horizon:nova:instances_and_volumes:volumes:create') + url = reverse('horizon:nova:volumes:create') res = self.client.post(url, formData) expected_error = [u'You are already using all of your available' @@ -101,7 +101,7 @@ def test_edit_attachments(self): self.mox.ReplayAll() - url = reverse('horizon:nova:instances_and_volumes:volumes:attach', + url = reverse('horizon:nova:volumes:attach', args=[volume.id]) res = self.client.get(url) # Asserting length of 2 accounts for the one instance option, @@ -123,7 +123,7 @@ def test_edit_attachments_attached_volume(self): self.mox.ReplayAll() - url = reverse('horizon:nova:instances_and_volumes:volumes:attach', + url = reverse('horizon:nova:volumes:attach', args=[volume.id]) res = self.client.get(url) @@ -147,7 +147,7 @@ def test_detail_view(self): self.mox.ReplayAll() - url = reverse('horizon:nova:instances_and_volumes:volumes:detail', + url = reverse('horizon:nova:volumes:detail', args=[volume.id]) res = self.client.get(url) @@ -158,7 +158,10 @@ def test_detail_view(self): 200) self.assertContains(res, "
Available
", 1, 200) self.assertContains(res, "
40 GB
", 1, 200) - self.assertContains(res, "server_1", 1, 200) + self.assertContains(res, + ("%s" + % server.name), + 1, + 200) self.assertNoMessages() diff --git a/horizon/dashboards/nova/instances_and_volumes/volumes/urls.py b/horizon/dashboards/nova/volumes/urls.py similarity index 82% rename from horizon/dashboards/nova/instances_and_volumes/volumes/urls.py rename to horizon/dashboards/nova/volumes/urls.py index 90f07357c4..c9b22af8c1 100644 --- a/horizon/dashboards/nova/instances_and_volumes/volumes/urls.py +++ b/horizon/dashboards/nova/volumes/urls.py @@ -16,12 +16,12 @@ from django.conf.urls.defaults import patterns, url -from .views import (CreateView, EditAttachmentsView, DetailView, +from .views import (IndexView, CreateView, EditAttachmentsView, DetailView, CreateSnapshotView) -urlpatterns = patterns( - 'horizon.dashboards.nova.instances_and_volumes.volumes.views', +urlpatterns = patterns('horizon.dashboards.nova.volumes.views', + url(r'^$', IndexView.as_view(), name='index'), url(r'^create/$', CreateView.as_view(), name='create'), url(r'^(?P[^/]+)/attach/$', EditAttachmentsView.as_view(), @@ -29,7 +29,7 @@ url(r'^(?P[^/]+)/create_snapshot/$', CreateSnapshotView.as_view(), name='create_snapshot'), - url(r'^(?P[^/]+)/detail/$', + url(r'^(?P[^/]+)/$', DetailView.as_view(), name='detail'), ) diff --git a/horizon/dashboards/nova/instances_and_volumes/volumes/views.py b/horizon/dashboards/nova/volumes/views.py similarity index 70% rename from horizon/dashboards/nova/instances_and_volumes/volumes/views.py rename to horizon/dashboards/nova/volumes/views.py index 63eb6de28f..dbd9299947 100644 --- a/horizon/dashboards/nova/instances_and_volumes/volumes/views.py +++ b/horizon/dashboards/nova/volumes/views.py @@ -21,6 +21,7 @@ import logging from django.utils.translation import ugettext_lazy as _ +from django.utils.datastructures import SortedDict from horizon import api from horizon import exceptions @@ -28,21 +29,54 @@ from horizon import tables from horizon import tabs from .forms import CreateForm, AttachForm, CreateSnapshotForm -from .tables import AttachmentsTable +from .tables import AttachmentsTable, VolumesTable from .tabs import VolumeDetailTabs LOG = logging.getLogger(__name__) +class IndexView(tables.DataTableView): + table_class = VolumesTable + template_name = 'nova/volumes/index.html' + + def get_data(self): + # Gather our volumes + try: + volumes = api.volume_list(self.request) + except: + volumes = [] + exceptions.handle(self.request, + _('Unable to retrieve volume list.')) + try: + instance_list = api.server_list(self.request) + except: + instance_list = [] + exceptions.handle(self.request, + _("Unable to retrieve volume/instance " + "attachment information")) + + instances = SortedDict([(inst.id, inst) for inst in instance_list]) + for volume in volumes: + # It is possible to create a volume with no name through the + # EC2 API, use the ID in those cases. + if not volume.display_name: + volume.display_name = volume.id + + for att in volume.attachments: + server_id = att.get('server_id', None) + att['instance'] = instances.get(server_id, None) + return volumes + + class DetailView(tabs.TabView): tab_group_class = VolumeDetailTabs - template_name = 'nova/instances_and_volumes/volumes/detail.html' + template_name = 'nova/volumes/detail.html' class CreateView(forms.ModalFormView): form_class = CreateForm - template_name = 'nova/instances_and_volumes/volumes/create.html' + template_name = 'nova/volumes/create.html' def get_context_data(self, **kwargs): context = super(CreateView, self).get_context_data(**kwargs) @@ -56,7 +90,7 @@ def get_context_data(self, **kwargs): class CreateSnapshotView(forms.ModalFormView): form_class = CreateSnapshotForm - template_name = 'nova/instances_and_volumes/volumes/create_snapshot.html' + template_name = 'nova/volumes/create_snapshot.html' def get_context_data(self, **kwargs): return {'volume_id': kwargs['volume_id']} @@ -67,7 +101,7 @@ def get_initial(self): class EditAttachmentsView(tables.DataTableView): table_class = AttachmentsTable - template_name = 'nova/instances_and_volumes/volumes/attach.html' + template_name = 'nova/volumes/attach.html' def get_object(self): if not hasattr(self, "_object"): @@ -113,7 +147,7 @@ def get(self, request, *args, **kwargs): context['form'] = self.form if request.is_ajax(): context['hide'] = True - self.template_name = ('nova/instances_and_volumes/volumes' + self.template_name = ('nova/volumes' '/_attach.html') return self.render_to_response(context) diff --git a/horizon/dashboards/settings/project/panel.py b/horizon/dashboards/settings/project/panel.py index 6e9c040e2e..ff01498c37 100644 --- a/horizon/dashboards/settings/project/panel.py +++ b/horizon/dashboards/settings/project/panel.py @@ -21,7 +21,7 @@ class TenantPanel(horizon.Panel): - name = _("OpenStack Credentials") + name = _("OpenStack API") slug = 'project' diff --git a/horizon/dashboards/settings/project/tables.py b/horizon/dashboards/settings/project/tables.py new file mode 100644 index 0000000000..48abd8d51a --- /dev/null +++ b/horizon/dashboards/settings/project/tables.py @@ -0,0 +1,33 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 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 _ + +from horizon import tables + + +def get_endpoint(service): + return service.endpoints[0]['publicURL'] + + +class EndpointsTable(tables.DataTable): + api_name = tables.Column('name', verbose_name=_("Service Name")) + api_endpoint = tables.Column(get_endpoint, + verbose_name=_("Service Endpoint")) + + class Meta: + name = "endpoints" + verbose_name = _("API Endpoints") diff --git a/horizon/dashboards/settings/project/templates/project/_openrc.html b/horizon/dashboards/settings/project/templates/project/_openrc.html index 524db70119..9e1078dc52 100644 --- a/horizon/dashboards/settings/project/templates/project/_openrc.html +++ b/horizon/dashboards/settings/project/templates/project/_openrc.html @@ -5,9 +5,16 @@ {% block form_action %}{% url horizon:settings:project:index %}{% endblock %} {% block modal_id %}language_settings_modal{% endblock %} -{% block modal-header %}{% trans "Download OpenStack RC File" %}{% endblock %} +{% block modal-header %}{% trans "OpenStack API" %}{% endblock %} {% block modal-body %} +
+ {{ endpoints.render }} +
+
+

{% trans "Download OpenStack RC File" %}

+
+
{% include "horizon/common/_form_fields.html" %} @@ -23,4 +30,3 @@

{% trans "Description:" %}

{% if hide %}{% trans "Cancel" %}{% endif %} {% endblock %} - diff --git a/horizon/dashboards/settings/project/templates/project/settings.html b/horizon/dashboards/settings/project/templates/project/settings.html index a8e5b740b2..20c9b1a4ee 100644 --- a/horizon/dashboards/settings/project/templates/project/settings.html +++ b/horizon/dashboards/settings/project/templates/project/settings.html @@ -1,9 +1,9 @@ {% extends 'settings/base.html' %} {% load i18n %} -{% block title %}OpenRC Download{% endblock %} +{% block title %}{% trans "OpenStack API" %}{% endblock %} {% block page_header %} - {% include "horizon/common/_page_header.html" with title=_("Download OpenStack RC File") %} + {% include "horizon/common/_page_header.html" with title=_("OpenStack API") %} {% endblock page_header %} {% block settings_main %} diff --git a/horizon/dashboards/settings/project/urls.py b/horizon/dashboards/settings/project/urls.py index 93405bb979..d418e883d0 100644 --- a/horizon/dashboards/settings/project/urls.py +++ b/horizon/dashboards/settings/project/urls.py @@ -16,6 +16,8 @@ from django.conf.urls.defaults import patterns, url +from .views import OpenRCView -urlpatterns = patterns('horizon.dashboards.settings.project.views', - url(r'^$', 'index', name='index')) + +urlpatterns = patterns('', + url(r'^$', OpenRCView.as_view(), name='index')) diff --git a/horizon/dashboards/settings/project/views.py b/horizon/dashboards/settings/project/views.py index 704932569a..82070c94c9 100644 --- a/horizon/dashboards/settings/project/views.py +++ b/horizon/dashboards/settings/project/views.py @@ -14,16 +14,28 @@ # License for the specific language governing permissions and limitations # under the License. -from django import shortcuts +from horizon.api import keystone +from horizon.forms import ModalFormView + from .forms import DownloadOpenRCForm +from .tables import EndpointsTable + +class OpenRCView(ModalFormView): + form_class = DownloadOpenRCForm + template_name = 'settings/project/settings.html' -def index(request): - form, handled = DownloadOpenRCForm.maybe_handle(request, - initial={'tenant': request.user.tenant_id}) - if handled: - return handled + def get_data(self): + services = [] + for i, service in enumerate(self.request.user.service_catalog): + service['id'] = i + services.append(keystone.Service(service)) + return services - context = {'form': form} + def get_context_data(self, **kwargs): + context = super(OpenRCView, self).get_context_data(**kwargs) + context["endpoints"] = EndpointsTable(self.request, self.get_data()) + return context - return shortcuts.render(request, 'settings/project/settings.html', context) + def get_initial(self): + return {'tenant': self.request.user.tenant_id} diff --git a/horizon/dashboards/settings/user/forms.py b/horizon/dashboards/settings/user/forms.py new file mode 100644 index 0000000000..e94c311820 --- /dev/null +++ b/horizon/dashboards/settings/user/forms.py @@ -0,0 +1,59 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 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 pytz + +from django import shortcuts +from django.conf import settings +from django.utils import translation + +from horizon import forms +from horizon import messages + + +class UserSettingsForm(forms.SelfHandlingForm): + language = forms.ChoiceField() + timezone = forms.ChoiceField() + + def __init__(self, *args, **kwargs): + super(UserSettingsForm, self).__init__(*args, **kwargs) + + # Languages + languages = [(k, "%s (%s)" + % (translation.get_language_info(k)['name_local'], k)) + for k, v in settings.LANGUAGES] + self.fields['language'].choices = languages + + # Timezones + timezones = [(tz, tz) for tz in pytz.common_timezones] + self.fields['timezone'].choices = timezones + + def handle(self, request, data): + response = shortcuts.redirect(request.build_absolute_uri()) + # Language + lang_code = data['language'] + if lang_code and translation.check_for_language(lang_code): + if hasattr(request, 'session'): + request.session['django_language'] = lang_code + else: + response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang_code) + + # Timezone + request.session['django_timezone'] = pytz.timezone(data['timezone']) + + messages.success(request, translation.ugettext("Settings saved.")) + + return response diff --git a/horizon/dashboards/settings/user/templates/user/_language.html b/horizon/dashboards/settings/user/templates/user/_language.html deleted file mode 100644 index 9feaaec568..0000000000 --- a/horizon/dashboards/settings/user/templates/user/_language.html +++ /dev/null @@ -1,39 +0,0 @@ -{% extends "horizon/common/_modal_form.html" %} -{% load i18n %} - -{% block form_id %}language_settings_form{% endblock %} -{% block form_action %}{% url horizon:set_language %}{% endblock %} - -{% block modal_id %}language_settings_modal{% endblock %} -{% block modal-header %}{% trans "Select Language" %}{% endblock %} - -{% block modal-body %} -
-

{% trans "Dashboard User Interface Language" %}

-
- -
-

- -
-
-
-

{% trans "Description:" %}

-

{% trans "From here you can modify different settings for your dashboard." %}

-
-{% endblock %} - -{% block modal-footer %} - - {% if hide %}{% trans "Cancel" %}{% endif %} -{% endblock %} - diff --git a/horizon/dashboards/settings/user/templates/user/_settings.html b/horizon/dashboards/settings/user/templates/user/_settings.html new file mode 100644 index 0000000000..1e2eebef2c --- /dev/null +++ b/horizon/dashboards/settings/user/templates/user/_settings.html @@ -0,0 +1,26 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_id %}user_settings_modal{% endblock %} +{% block form_action %}{% url horizon:settings:user:index %}{% endblock %} + +{% block modal_id %}user_settings_modal{% endblock %} +{% block modal-header %}{% trans "User Settings" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description:" %}

+

{% trans "From here you can modify dashboard settings for your user." %}

+
+{% endblock %} + +{% block modal-footer %} + + {% if hide %}{% trans "Cancel" %}{% endif %} +{% endblock %} + diff --git a/horizon/dashboards/settings/user/templates/user/settings.html b/horizon/dashboards/settings/user/templates/user/settings.html index 8fbc8c3a63..d01b232b37 100644 --- a/horizon/dashboards/settings/user/templates/user/settings.html +++ b/horizon/dashboards/settings/user/templates/user/settings.html @@ -1,11 +1,11 @@ {% extends 'settings/base.html' %} {% load i18n %} -{% block title %}User Settings{% endblock %} +{% block title %}{% trans "User Settings" %}{% endblock %} {% block page_header %} - {% include "horizon/common/_page_header.html" with title=_("Dashboard Settings") %} + {% include "horizon/common/_page_header.html" with title=_("User Settings") %} {% endblock page_header %} {% block settings_main %} - {% include "settings/user/_language.html" %} + {% include "settings/user/_settings.html" %} {% endblock %} diff --git a/horizon/dashboards/settings/user/urls.py b/horizon/dashboards/settings/user/urls.py index 4befb6785d..ba5de426ff 100644 --- a/horizon/dashboards/settings/user/urls.py +++ b/horizon/dashboards/settings/user/urls.py @@ -16,6 +16,8 @@ from django.conf.urls.defaults import patterns, url +from .views import UserSettingsView -urlpatterns = patterns('horizon.dashboards.settings.user.views', - url(r'^$', 'index', name='index')) + +urlpatterns = patterns('', + url(r'^$', UserSettingsView.as_view(), name='index')) diff --git a/horizon/dashboards/settings/user/views.py b/horizon/dashboards/settings/user/views.py index 9eb410859c..4b4bbf25e2 100644 --- a/horizon/dashboards/settings/user/views.py +++ b/horizon/dashboards/settings/user/views.py @@ -14,8 +14,15 @@ # License for the specific language governing permissions and limitations # under the License. -from django import shortcuts +from horizon import forms +from .forms import UserSettingsForm -def index(request): - return shortcuts.render(request, 'settings/user/settings.html', {}) + +class UserSettingsView(forms.ModalFormView): + form_class = UserSettingsForm + template_name = 'settings/user/settings.html' + + def get_initial(self): + return {'language': self.request.LANGUAGE_CODE, + 'timezone': self.request.session.get('django_timezone', 'UTC')} diff --git a/horizon/dashboards/syspanel/dashboard.py b/horizon/dashboards/syspanel/dashboard.py index a0373e9ea7..ee40d68dd8 100644 --- a/horizon/dashboards/syspanel/dashboard.py +++ b/horizon/dashboards/syspanel/dashboard.py @@ -22,8 +22,8 @@ class SystemPanels(horizon.PanelGroup): slug = "syspanel" name = _("System Panel") - panels = ('overview', 'instances', 'services', 'flavors', 'images', - 'projects', 'users', 'quotas',) + panels = ('overview', 'instances', 'volumes', 'services', 'flavors', + 'images', 'projects', 'users', 'quotas',) class Syspanel(horizon.Dashboard): diff --git a/horizon/dashboards/syspanel/instances/tables.py b/horizon/dashboards/syspanel/instances/tables.py index 50631d4e7b..8933258d64 100644 --- a/horizon/dashboards/syspanel/instances/tables.py +++ b/horizon/dashboards/syspanel/instances/tables.py @@ -22,8 +22,8 @@ from horizon import api from horizon import tables -from horizon.dashboards.nova.instances_and_volumes.instances.tables import ( - TerminateInstance, EditInstance, ConsoleLink, LogLink, SnapshotLink, +from horizon.dashboards.nova.instances.tables import (TerminateInstance, + EditInstance, ConsoleLink, LogLink, SnapshotLink, TogglePause, ToggleSuspend, RebootInstance, get_size, UpdateRow, LaunchLink, get_ips, get_power_state) from horizon.utils.filters import replace_underscores @@ -69,8 +69,7 @@ class SyspanelInstancesTable(tables.DataTable): verbose_name=_("Host"), classes=('nowrap-col',)) name = tables.Column("name", - link=("horizon:nova:instances_and_volumes:" - "instances:detail"), + link=("horizon:nova:instances:detail"), verbose_name=_("Instance Name")) ip = tables.Column(get_ips, verbose_name=_("IP Address")) size = tables.Column(get_size, diff --git a/horizon/dashboards/syspanel/instances/views.py b/horizon/dashboards/syspanel/instances/views.py index 4246beab73..c7385a2e9e 100644 --- a/horizon/dashboards/syspanel/instances/views.py +++ b/horizon/dashboards/syspanel/instances/views.py @@ -28,8 +28,8 @@ from horizon import exceptions from horizon import tables from horizon.dashboards.syspanel.instances.tables import SyspanelInstancesTable -from horizon.dashboards.nova.instances_and_volumes.instances.views import ( - console, DetailView, vnc, LaunchInstanceView) +from horizon.dashboards.nova.instances.views import (console, DetailView, + vnc, LaunchInstanceView) from .workflows import AdminLaunchInstance diff --git a/horizon/dashboards/syspanel/instances/workflows.py b/horizon/dashboards/syspanel/instances/workflows.py index 989577b04f..01e87a87be 100644 --- a/horizon/dashboards/syspanel/instances/workflows.py +++ b/horizon/dashboards/syspanel/instances/workflows.py @@ -14,8 +14,7 @@ # License for the specific language governing permissions and limitations # under the License. -from horizon.dashboards.nova.instances_and_volumes.instances.workflows import ( - LaunchInstance) +from horizon.dashboards.nova.instances.workflows import LaunchInstance class AdminLaunchInstance(LaunchInstance): diff --git a/horizon/dashboards/nova/instances_and_volumes/volumes/__init__.py b/horizon/dashboards/syspanel/volumes/__init__.py similarity index 100% rename from horizon/dashboards/nova/instances_and_volumes/volumes/__init__.py rename to horizon/dashboards/syspanel/volumes/__init__.py diff --git a/horizon/dashboards/syspanel/volumes/panel.py b/horizon/dashboards/syspanel/volumes/panel.py new file mode 100644 index 0000000000..7117a16e4b --- /dev/null +++ b/horizon/dashboards/syspanel/volumes/panel.py @@ -0,0 +1,14 @@ +from django.utils.translation import ugettext_lazy as _ + +import horizon + +from horizon.dashboards.syspanel import dashboard + + +class Volumes(horizon.Panel): + name = _("Volumes") + slug = "volumes" + services = ('volume',) + + +dashboard.Syspanel.register(Volumes) diff --git a/horizon/dashboards/syspanel/volumes/tables.py b/horizon/dashboards/syspanel/volumes/tables.py new file mode 100644 index 0000000000..91f11909f2 --- /dev/null +++ b/horizon/dashboards/syspanel/volumes/tables.py @@ -0,0 +1,19 @@ +from django.utils.translation import ugettext_lazy as _ + +from horizon import tables +from horizon.dashboards.nova.volumes.tables import (UpdateRow, + VolumesTable as _VolumesTable, DeleteVolume) + + +class VolumesTable(_VolumesTable): + name = tables.Column("display_name", + verbose_name=_("Name"), + link="horizon:syspanel:volumes:detail") + + class Meta: + name = "volumes" + verbose_name = _("Volumes") + status_columns = ["status"] + row_class = UpdateRow + table_actions = (DeleteVolume,) + row_actions = (DeleteVolume,) diff --git a/horizon/dashboards/syspanel/volumes/templates/volumes/detail.html b/horizon/dashboards/syspanel/volumes/templates/volumes/detail.html new file mode 100644 index 0000000000..38ec1d3290 --- /dev/null +++ b/horizon/dashboards/syspanel/volumes/templates/volumes/detail.html @@ -0,0 +1,15 @@ +{% extends 'syspanel/base.html' %} +{% load i18n %} +{% block title %}{% trans "Volume Details" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Volume Detail") %} +{% endblock page_header %} + +{% block syspanel_main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} diff --git a/horizon/dashboards/syspanel/volumes/templates/volumes/index.html b/horizon/dashboards/syspanel/volumes/templates/volumes/index.html new file mode 100644 index 0000000000..b85e8cc906 --- /dev/null +++ b/horizon/dashboards/syspanel/volumes/templates/volumes/index.html @@ -0,0 +1,13 @@ +{% extends 'syspanel/base.html' %} +{% load i18n %} +{% block title %}{% trans "Volumes" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Volumes") %} +{% endblock page_header %} + +{% block syspanel_main %} + {{ table.render }} +{% endblock %} + + diff --git a/horizon/dashboards/syspanel/volumes/tests.py b/horizon/dashboards/syspanel/volumes/tests.py new file mode 100644 index 0000000000..9234ae287a --- /dev/null +++ b/horizon/dashboards/syspanel/volumes/tests.py @@ -0,0 +1,38 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 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.core.urlresolvers import reverse +from mox import IsA + +from horizon import api +from horizon import test + + +class VolumeTests(test.BaseAdminViewTests): + @test.create_stubs({api: ('server_list', 'volume_list',)}) + def test_index(self): + api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list()) + api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:syspanel:volumes:index')) + + self.assertTemplateUsed(res, 'syspanel/volumes/index.html') + volumes = res.context['volumes_table'].data + + self.assertItemsEqual(volumes, self.volumes.list()) diff --git a/horizon/dashboards/syspanel/volumes/urls.py b/horizon/dashboards/syspanel/volumes/urls.py new file mode 100644 index 0000000000..f42c753f09 --- /dev/null +++ b/horizon/dashboards/syspanel/volumes/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls.defaults import patterns, url + +from .views import IndexView, DetailView + +urlpatterns = patterns('', + url(r'^$', IndexView.as_view(), name='index'), + url(r'^(?P[^/]+)/$', DetailView.as_view(), name='detail'), +) diff --git a/horizon/dashboards/syspanel/volumes/views.py b/horizon/dashboards/syspanel/volumes/views.py new file mode 100644 index 0000000000..4acd5582cc --- /dev/null +++ b/horizon/dashboards/syspanel/volumes/views.py @@ -0,0 +1,32 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 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. + +""" +Admin views for managing Nova volumes. +""" + +from horizon.dashboards.nova.volumes.views import (IndexView as _IndexView, + DetailView as _DetailView) +from .tables import VolumesTable + + +class IndexView(_IndexView): + table_class = VolumesTable + template_name = "syspanel/volumes/index.html" + + +class DetailView(_DetailView): + template_name = "syspanel/volumes/detail.html" diff --git a/horizon/facebook/__init__.py b/horizon/facebook/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/horizon/facebook/admin.py b/horizon/facebook/admin.py new file mode 100644 index 0000000000..24f0471942 --- /dev/null +++ b/horizon/facebook/admin.py @@ -0,0 +1,16 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from django.contrib.auth.models import User + +from models import FacebookProfile + +# We want to display our facebook profile, not the default user +admin.site.unregister(User) + +class FacebookProfileInline(admin.StackedInline): + model = FacebookProfile + +class FacebookProfileAdmin(UserAdmin): + inlines = [FacebookProfileInline] + +admin.site.register(User, FacebookProfileAdmin) diff --git a/horizon/facebook/backend.py b/horizon/facebook/backend.py new file mode 100644 index 0000000000..2784ebcba7 --- /dev/null +++ b/horizon/facebook/backend.py @@ -0,0 +1,81 @@ +import cgi, urllib, json + +from django.conf import settings +from django.contrib.auth.models import User, AnonymousUser +from django.db import IntegrityError + +from models import FacebookProfile + +class FacebookBackend: + def authenticate(self, token=None, request=None): + """ Reads in a Facebook code and asks Facebook if it's valid and what user it points to. """ + args = { + 'client_id': settings.FACEBOOK_APP_ID, + 'client_secret': settings.FACEBOOK_APP_SECRET, + 'redirect_uri': request.build_absolute_uri('/facebook/authentication_callback'), + 'code': token, + } + + # Get a legit access token + target = urllib.urlopen('https://graph.facebook.com/oauth/access_token?' + urllib.urlencode(args)).read() + response = cgi.parse_qs(target) + access_token = response['access_token'][-1] + + # Read the user's profile information + fb_profile = urllib.urlopen('https://graph.facebook.com/me?access_token=%s' % access_token) + fb_profile = json.load(fb_profile) + + try: + # Try and find existing user + fb_user = FacebookProfile.objects.get(facebook_id=fb_profile['id']) + user = fb_user.user + + # Update access_token + fb_user.access_token = access_token + fb_user.save() + + except FacebookProfile.DoesNotExist: + # No existing user + + # Not all users have usernames + username = fb_profile.get('username', fb_profile['email'].split('@')[0]) + + if getattr(settings, 'FACEBOOK_FORCE_SIGNUP', False): + # No existing user, use anonymous + user = AnonymousUser() + user.username = username + user.first_name = fb_profile['first_name'] + user.last_name = fb_profile['last_name'] + fb_user = FacebookProfile( + facebook_id=fb_profile['id'], + access_token=access_token + ) + user.facebookprofile = fb_user + + else: + # No existing user, create one + + try: + user = User.objects.create_user(username, fb_profile['email']) + except IntegrityError: + # Username already exists, make it unique + user = User.objects.create_user(username + fb_profile['id'], fb_profile['email']) + user.first_name = fb_profile['first_name'] + user.last_name = fb_profile['last_name'] + user.save() + + # Create the FacebookProfile + fb_user = FacebookProfile(user=user, facebook_id=fb_profile['id'], access_token=access_token) + fb_user.save() + + return user + + def get_user(self, user_id): + """ Just returns the user of a given ID. """ + try: + return User.objects.get(pk=user_id) + except User.DoesNotExist: + return None + + supports_object_permissions = False + supports_anonymous_user = True diff --git a/horizon/facebook/models.py b/horizon/facebook/models.py new file mode 100644 index 0000000000..27a592645f --- /dev/null +++ b/horizon/facebook/models.py @@ -0,0 +1,14 @@ +import json, urllib + +from django.db import models +from django.contrib.auth.models import User + + +class FacebookProfile(models.Model): + user = models.OneToOneField(User) + facebook_id = models.BigIntegerField() + access_token = models.CharField(max_length=150) + + def get_facebook_profile(self): + fb_profile = urllib.urlopen('https://graph.facebook.com/me?access_token=%s' % self.access_token) + return json.load(fb_profile) diff --git a/horizon/facebook/views.py b/horizon/facebook/views.py new file mode 100644 index 0000000000..a5398ebdc8 --- /dev/null +++ b/horizon/facebook/views.py @@ -0,0 +1,40 @@ +import urllib + +from django.http import HttpResponseRedirect +from django.conf import settings +from django.contrib.auth import login as auth_login +from django.contrib.auth import authenticate +from django.core.urlresolvers import reverse + +def login(request): + """ First step of process, redirects user to facebook, which redirects to authentication_callback. """ + + args = { + 'client_id': settings.FACEBOOK_APP_ID, + 'scope': settings.FACEBOOK_SCOPE, + 'redirect_uri': request.build_absolute_uri('/facebook/authentication_callback'), + } + return HttpResponseRedirect('https://www.facebook.com/dialog/oauth?' + urllib.urlencode(args)) + +def authentication_callback(request): + """ Second step of the login process. + It reads in a code from Facebook, then redirects back to the home page. """ + code = request.GET.get('code') + user = authenticate(token=code, request=request) + + if user.is_anonymous(): + #we have to set this user up + url = reverse('facebook_setup') + url += "?code=%s" % code + + resp = HttpResponseRedirect(url) + + else: + auth_login(request, user) + + #figure out where to go after setup + url = getattr(settings, "LOGIN_REDIRECT_URL", "/") + + resp = HttpResponseRedirect(url) + + return resp diff --git a/horizon/middleware.py b/horizon/middleware.py index e755cee645..a5ba11dd27 100644 --- a/horizon/middleware.py +++ b/horizon/middleware.py @@ -25,10 +25,10 @@ from django import http from django import shortcuts - from django.core.urlresolvers import reverse from django.contrib import messages from django.contrib.auth import REDIRECT_FIELD_NAME +from django.utils import timezone from django.utils.encoding import iri_to_uri from horizon import exceptions @@ -50,6 +50,11 @@ def process_request(self, request): Adds a :class:`~horizon.users.User` object to ``request.user``. """ + # Activate timezone handling + tz = request.session.get('django_timezone') + if tz: + timezone.activate(tz) + # A quick and dirty way to log users out def user_logout(request): if hasattr(request, '_cached_user'): diff --git a/horizon/site_urls.py b/horizon/site_urls.py index 0986272bf8..0f45cdd927 100644 --- a/horizon/site_urls.py +++ b/horizon/site_urls.py @@ -39,6 +39,11 @@ name="set_language"), url(r'^i18n/', include('django.conf.urls.i18n'))) +urlpatterns += patterns('horizon', + url(r'^facebook/login$', 'facebook.views.login'), + url(r'^facebook/authentication_callback$', 'facebook.views.authentication_callback'), +) + if settings.DEBUG: urlpatterns += patterns('', url(r'^qunit/$', diff --git a/horizon/tables/base.py b/horizon/tables/base.py index 7110f7e08f..f1d4e3815a 100644 --- a/horizon/tables/base.py +++ b/horizon/tables/base.py @@ -320,6 +320,7 @@ def get_summation(self): return None summation_function = self.summation_methods[self.summation] data = [self.get_raw_data(datum) for datum in self.table.data] + data = filter(lambda datum: datum is not None, data) summation = summation_function(data) for filter_func in self.filters: summation = filter_func(summation) diff --git a/horizon/templates/horizon/auth/_login.html b/horizon/templates/horizon/auth/_login.html index 6e09c8f612..ce7bf6537c 100644 --- a/horizon/templates/horizon/auth/_login.html +++ b/horizon/templates/horizon/auth/_login.html @@ -15,4 +15,5 @@ {% block modal-footer %} + Login with Facebook {% endblock %} diff --git a/horizon/tests/utils_tests.py b/horizon/tests/utils_tests.py index c7588dbc05..7fb3018219 100644 --- a/horizon/tests/utils_tests.py +++ b/horizon/tests/utils_tests.py @@ -15,9 +15,12 @@ # under the License. +import os + from horizon import test from django.core.exceptions import ValidationError from horizon.utils import fields +from horizon.utils import secret_key class ValidatorsTests(test.TestCase): @@ -169,3 +172,24 @@ def test_validate_IPs(self): "169.144.11.107/8") self.assertIsNone(iprange.validate("fe80::204:61ff:254.157.241.86/36")) self.assertIsNone(iprange.validate("169.144.11.107/18")) + + +class SecretKeyTests(test.TestCase): + def test_generate_secret_key(self): + key = secret_key.generate_key(32) + self.assertEqual(len(key), 32) + self.assertNotEqual(key, secret_key.generate_key(32)) + + def test_generate_or_read_key_from_file(self): + key_file = ".test_secret_key_store" + key = secret_key.generate_or_read_from_file(key_file) + + # Consecutive reads should come from the already existing file: + self.assertEqual(key, secret_key.generate_or_read_from_file(key_file)) + + # Key file only be read/writable by user: + self.assertEqual(oct(os.stat(key_file).st_mode & 0777), "0600") + os.chmod(key_file, 0777) + self.assertRaises(secret_key.FilePermissionError, + secret_key.generate_or_read_from_file, key_file) + os.remove(key_file) diff --git a/horizon/usage/tables.py b/horizon/usage/tables.py index 523445aa35..c05bdb66a0 100644 --- a/horizon/usage/tables.py +++ b/horizon/usage/tables.py @@ -44,7 +44,7 @@ class Meta: def get_instance_link(datum): - view = "horizon:nova:instances_and_volumes:instances:detail" + view = "horizon:nova:instances:detail" if datum.get('instance_id', False): return urlresolvers.reverse(view, args=(datum.get('instance_id'),)) else: diff --git a/horizon/utils/secret_key.py b/horizon/utils/secret_key.py new file mode 100644 index 0000000000..6eba25ebfd --- /dev/null +++ b/horizon/utils/secret_key.py @@ -0,0 +1,68 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 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 with_statement # Python 2.5 compliance + +import lockfile +import random +import string +import tempfile +import os + + +class FilePermissionError(Exception): + """The key file permissions are insecure.""" + pass + + +def generate_key(key_length=64): + """Secret key generator. + + The quality of randomness depends on operating system support, + see http://docs.python.org/library/random.html#random.SystemRandom. + """ + if hasattr(random, 'SystemRandom'): + choice = random.SystemRandom().choice + else: + choice = random.choice + return ''.join(map(lambda x: choice(string.digits + string.letters), + range(key_length))) + + +def generate_or_read_from_file(key_file='.secret_key', key_length=64): + """Multiprocess-safe secret key file generator. + + Useful to replace the default (and thus unsafe) SECRET_KEY in settings.py + upon first start. Save to use, i.e. when multiple Python interpreters + serve the dashboard Django application (e.g. in a mod_wsgi + daemonized + environment). Also checks if file permissions are set correctly and + throws an exception if not. + """ + lock = lockfile.FileLock(key_file) + with lock: + if not os.path.exists(key_file): + key = generate_key(key_length) + old_umask = os.umask(0177) # Use '0600' file permissions + with open(key_file, 'w') as f: + f.write(key) + os.umask(old_umask) + else: + if oct(os.stat(key_file).st_mode & 0777) != '0600': + raise FilePermissionError("Insecure key file permissions!") + with open(key_file, 'r') as f: + key = f.readline() + return key diff --git a/horizon/workflows/base.py b/horizon/workflows/base.py index 594609649f..ba96b00d94 100644 --- a/horizon/workflows/base.py +++ b/horizon/workflows/base.py @@ -64,6 +64,7 @@ def __new__(mcs, name, bases, attrs): attrs['name'] = getattr(opts, "name", name) attrs['slug'] = getattr(opts, "slug", slugify(name)) attrs['roles'] = getattr(opts, "roles", ()) + attrs['services'] = getattr(opts, "services", ()) attrs['progress_message'] = getattr(opts, "progress_message", _("Processing...")) @@ -112,6 +113,11 @@ class within them: A list of role names which this action requires in order to be completed. Defaults to an empty list (``[]``). + .. attribute:: services + + A list of service types which this action requires in order to be + completed. Defaults to an empty list (``[]``). + .. attribute:: help_text A string of simple help text to be displayed alongside the Action's @@ -257,6 +263,10 @@ class Step(object): .. attribute:: roles Inherited from the ``Action`` class. + + .. attribute:: services + + Inherited from the ``Action`` class. """ action_class = None depends_on = () @@ -284,6 +294,7 @@ def __init__(self, workflow): self.slug = self.action_class.slug self.name = self.action_class.name self.roles = self.action_class.roles + self.services = self.action_class.services self.has_errors = False self._handlers = {} diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example index 48cfc425a8..9dddfcb93c 100644 --- a/openstack_dashboard/local/local_settings.py.example +++ b/openstack_dashboard/local/local_settings.py.example @@ -12,9 +12,6 @@ TEMPLATE_DEBUG = DEBUG # https://docs.djangoproject.com/en/1.4/ref/settings/#secure-proxy-ssl-header # SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') -# Note: You should change this value -SECRET_KEY = 'elj1IWiLoWHgcyYxFVLj7cM5rGOOxWl0' - # Specify a regular expression to validate user passwords. # HORIZON_CONFIG = { # "password_validator": { @@ -25,6 +22,18 @@ SECRET_KEY = 'elj1IWiLoWHgcyYxFVLj7cM5rGOOxWl0' LOCAL_PATH = os.path.dirname(os.path.abspath(__file__)) +# Set custom secret key: +# You can either set it to a specific value or you can let horizion generate a +# default secret key that is unique on this machine, e.i. regardless of the +# amount of Python WSGI workers (if used behind Apache+mod_wsgi): However, there +# may be situations where you would want to set this explicitly, e.g. when +# multiple dashboard instances are distributed on different machines (usually +# behind a load-balancer). Either you have to make sure that a session gets all +# requests routed to the same dashboard instance or you set the same SECRET_KEY +# for all of them. +# from horizon.utils import secret_key +# SECRET_KEY = secret_key.generate_or_read_from_file(os.path.join(LOCAL_PATH, '.secret_key_store')) + # We recommend you use memcached for development; otherwise after every reload # of the django development server, you will have to login again. To use # memcached set CACHE_BACKED to something like 'memcached://127.0.0.1:11211/' diff --git a/openstack_dashboard/settings.py b/openstack_dashboard/settings.py index b5f536c5c5..9f354a3b59 100644 --- a/openstack_dashboard/settings.py +++ b/openstack_dashboard/settings.py @@ -108,6 +108,8 @@ INSTALLED_APPS = ( 'openstack_dashboard', + 'django.contrib.auth', + 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', @@ -117,9 +119,22 @@ 'horizon.dashboards.nova', 'horizon.dashboards.syspanel', 'horizon.dashboards.settings', + 'horizon.facebook', +) + + +# Facebook settings are set via environment variables +FACEBOOK_APP_ID = '448675451823188' #os.environ['FACEBOOK_APP_ID'] +FACEBOOK_APP_SECRET = '50da6b7c719efb8a22c70df4fb03315d' #os.environ['FACEBOOK_APP_SECRET'] +FACEBOOK_SCOPE = 'email' + +AUTH_PROFILE_MODULE = 'horizon.facebook.FacebookProfile' + +AUTHENTICATION_BACKENDS = ( + 'horizon.facebook.backend.FacebookBackend', + 'django.contrib.auth.backends.ModelBackend', ) -AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',) MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies' diff --git a/openstack_dashboard/static/dashboard/less/horizon.less b/openstack_dashboard/static/dashboard/less/horizon.less index e28be1f3e3..25ed902d33 100644 --- a/openstack_dashboard/static/dashboard/less/horizon.less +++ b/openstack_dashboard/static/dashboard/less/horizon.less @@ -404,6 +404,11 @@ a.current_item:hover h3, a.current_item:hover h4 { .table th.headerSortUp { background-image: url(/static/dashboard/img/up_arrow.png); } +.table tr.summation td:first-child, +.table tr.summation td:last-child { + border-radius: 0; + border-bottom: 0 none; +} th { background: #f1f1f1; diff --git a/openstack_dashboard/test/settings.py b/openstack_dashboard/test/settings.py index f903ca2219..d3f376d0c3 100644 --- a/openstack_dashboard/test/settings.py +++ b/openstack_dashboard/test/settings.py @@ -1,10 +1,13 @@ import os from horizon.tests.testsettings import * +from horizon.utils.secret_key import generate_or_read_from_file TEST_DIR = os.path.dirname(os.path.abspath(__file__)) ROOT_PATH = os.path.abspath(os.path.join(TEST_DIR, "..")) +SECRET_KEY = generate_or_read_from_file(os.path.join(TEST_DIR, + '.secret_key_store')) ROOT_URLCONF = 'openstack_dashboard.urls' TEMPLATE_DIRS = (os.path.join(ROOT_PATH, 'templates'),) STATICFILES_DIRS = (os.path.join(ROOT_PATH, 'static'),) diff --git a/run_tests.sh b/run_tests.sh index 4f56087673..6c597771df 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -6,7 +6,7 @@ set -o errexit # Increment me any time the environment should be rebuilt. # This includes dependncy changes, directory renames, etc. # Simple integer secuence: 1, 2, 3... -environment_version=22 +environment_version=23 #--------------------------------------------------------# function usage { diff --git a/setup.py b/setup.py index 28e053ecb0..8a1d82423a 100755 --- a/setup.py +++ b/setup.py @@ -20,23 +20,67 @@ # under the License. import os -import re -import setuptools + +from distutils.core import setup +from distutils.command.install import INSTALL_SCHEMES + from horizon import version +from horizon.openstack.common import setup as os_common_setup -from horizon.openstack.common import setup -requires = setup.parse_requirements() -depend_links = setup.parse_dependency_links() -tests_require = setup.parse_requirements(['tools/test-requires']) +requires = os_common_setup.parse_requirements() +depend_links = os_common_setup.parse_dependency_links() +tests_require = os_common_setup.parse_requirements(['tools/test-requires']) ROOT = os.path.dirname(__file__) +target_dirs = ['horizon', 'openstack_dashboard', 'bin'] + def read(fname): return open(os.path.join(ROOT, fname)).read() -setuptools.setup(name="horizon", +def split(path, result=None): + """ + Split a path into components in a platform-neutral way. + """ + if result is None: + result = [] + head, tail = os.path.split(path) + if head == '': + return [tail] + result + if head == path: + return result + return split(head, [tail] + result) + + +# Tell distutils not to put the data_files in platform-specific installation +# locations. See here for an explanation: +# https://groups.google.com/forum/#!topic/comp.lang.python/Nex7L-026uw +for scheme in INSTALL_SCHEMES.values(): + scheme['data'] = scheme['purelib'] + +# Compile the list of packages available, because distutils doesn't have +# an easy way to do this. +packages, data_files = [], [] +root_dir = os.path.dirname(__file__) +if root_dir != '': + os.chdir(root_dir) + +for target_dir in target_dirs: + for dirpath, dirnames, filenames in os.walk(target_dir): + # Ignore dirnames that start with '.' + for i, dirname in enumerate(dirnames): + if dirname.startswith('.'): + del dirnames[i] + if '__init__.py' in filenames: + packages.append('.'.join(split(dirpath))) + elif filenames: + data_files.append([dirpath, [os.path.join(dirpath, f) + for f in filenames]]) + + +setup(name="horizon", version=version.canonical_version_string(), url='https://github.com/openstack/horizon/', license='Apache 2.0', @@ -44,14 +88,15 @@ def read(fname): long_description=read('README.rst'), author='OpenStack', author_email='horizon@lists.launchpad.net', - packages=setuptools.find_packages(), - cmdclass=setup.get_cmdclass(), + packages=packages, + data_files=data_files, + cmdclass=os_common_setup.get_cmdclass(), include_package_data=True, install_requires=requires, tests_require=tests_require, dependency_links=depend_links, zip_safe=False, - classifiers=['Development Status :: 4 - Beta', + classifiers=['Development Status :: 5 - Production/Stable', 'Framework :: Django', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', diff --git a/tools/pip-requires b/tools/pip-requires index 6be44ee220..3ffd0988b7 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -2,10 +2,10 @@ Django>=1.4 django_compressor python-cloudfiles +python-glanceclient +python-keystoneclient +python-novaclient pytz -# Horizon Non-pip Requirements -# FIXME(gabriel): set these back to master once the versioninfo bug is resolved. -https://github.com/openstack/python-novaclient/zipball/3dd0d3be63b4bf35aede852d096deff9be5b63e4#egg=python-novaclient -https://github.com/openstack/python-keystoneclient/zipball/44a1ee32e29825257cac5c0a61fc3be51b79eb65#egg=python-keystoneclient -https://github.com/openstack/python-glanceclient/zipball/d6e0a03a937841ee509c37003762fd92c9b762ef#egg=python-glanceclient +# Horizon Utility Requirements +lockfile # for SECURE_KEY generation