Browse Source

Merge "Host aggregates panel."

Jenkins 5 years ago
parent
commit
3a153c86af
23 changed files with 1036 additions and 123 deletions
  1. 29
    2
      openstack_dashboard/api/nova.py
  2. 0
    0
      openstack_dashboard/dashboards/admin/aggregates/__init__.py
  3. 21
    0
      openstack_dashboard/dashboards/admin/aggregates/constants.py
  4. 48
    0
      openstack_dashboard/dashboards/admin/aggregates/forms.py
  5. 25
    0
      openstack_dashboard/dashboards/admin/aggregates/panel.py
  6. 127
    0
      openstack_dashboard/dashboards/admin/aggregates/tables.py
  7. 29
    0
      openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_manage_hosts.html
  8. 26
    0
      openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_update.html
  9. 11
    0
      openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/create.html
  10. 17
    0
      openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/index.html
  11. 11
    0
      openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/manage_hosts.html
  12. 12
    0
      openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/update.html
  13. 256
    0
      openstack_dashboard/dashboards/admin/aggregates/tests.py
  14. 29
    0
      openstack_dashboard/dashboards/admin/aggregates/urls.py
  15. 108
    0
      openstack_dashboard/dashboards/admin/aggregates/views.py
  16. 238
    0
      openstack_dashboard/dashboards/admin/aggregates/workflows.py
  17. 3
    2
      openstack_dashboard/dashboards/admin/dashboard.py
  18. 17
    0
      openstack_dashboard/dashboards/admin/info/constants.py
  19. 0
    59
      openstack_dashboard/dashboards/admin/info/tables.py
  20. 4
    41
      openstack_dashboard/dashboards/admin/info/tabs.py
  21. 1
    15
      openstack_dashboard/dashboards/admin/info/tests.py
  22. 2
    3
      openstack_dashboard/dashboards/admin/info/views.py
  23. 22
    1
      openstack_dashboard/test/test_data/nova_data.py

+ 29
- 2
openstack_dashboard/api/nova.py View File

@@ -702,15 +702,42 @@ def service_list(request):
702 702
     return novaclient(request).services.list()
703 703
 
704 704
 
705
-def aggregate_list(request):
705
+def aggregate_details_list(request):
706 706
     result = []
707 707
     c = novaclient(request)
708 708
     for aggregate in c.aggregates.list():
709 709
         result.append(c.aggregates.get_details(aggregate.id))
710
-
711 710
     return result
712 711
 
713 712
 
713
+def aggregate_create(request, name, availability_zone=None):
714
+    return novaclient(request).aggregates.create(name, availability_zone)
715
+
716
+
717
+def aggregate_delete(request, aggregate_id):
718
+    return novaclient(request).aggregates.delete(aggregate_id)
719
+
720
+
721
+def aggregate_get(request, aggregate_id):
722
+    return novaclient(request).aggregates.get(aggregate_id)
723
+
724
+
725
+def aggregate_update(request, aggregate_id, values):
726
+    return novaclient(request).aggregates.update(aggregate_id, values)
727
+
728
+
729
+def host_list(request):
730
+    return novaclient(request).hosts.list()
731
+
732
+
733
+def add_host_to_aggregate(request, aggregate_id, host):
734
+    return novaclient(request).aggregates.add_host(aggregate_id, host)
735
+
736
+
737
+def remove_host_from_aggregate(request, aggregate_id, host):
738
+    return novaclient(request).aggregates.remove_host(aggregate_id, host)
739
+
740
+
714 741
 @memoized
715 742
 def list_extensions(request):
716 743
     return nova_list_extensions.ListExtManager(novaclient(request)).show_all()

+ 0
- 0
openstack_dashboard/dashboards/admin/aggregates/__init__.py View File


+ 21
- 0
openstack_dashboard/dashboards/admin/aggregates/constants.py View File

@@ -0,0 +1,21 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    a copy of the License at
4
+#
5
+#         http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#    Unless required by applicable law or agreed to in writing, software
8
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+AGGREGATES_TEMPLATE_NAME = 'admin/aggregates/index.html'
14
+AGGREGATES_INDEX_URL = 'horizon:admin:aggregates:index'
15
+AGGREGATES_INDEX_VIEW_TEMPLATE = 'admin/aggregates/index.html'
16
+AGGREGATES_CREATE_URL = 'horizon:admin:aggregates:create'
17
+AGGREGATES_CREATE_VIEW_TEMPLATE = 'admin/aggregates/create.html'
18
+AGGREGATES_MANAGE_HOSTS_URL = 'horizon:admin:aggregates:manage_hosts'
19
+AGGREGATES_MANAGE_HOSTS_TEMPLATE = 'admin/aggregates/manage_hosts.html'
20
+AGGREGATES_UPDATE_URL = 'horizon:admin:aggregates:update'
21
+AGGREGATES_UPDATE_VIEW_TEMPLATE = 'admin/aggregates/update.html'

+ 48
- 0
openstack_dashboard/dashboards/admin/aggregates/forms.py View File

@@ -0,0 +1,48 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    a copy of the License at
4
+#
5
+#         http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#    Unless required by applicable law or agreed to in writing, software
8
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+from django.utils.translation import ugettext_lazy as _
14
+
15
+from horizon import exceptions
16
+from horizon import forms
17
+from horizon import messages
18
+
19
+from openstack_dashboard import api
20
+from openstack_dashboard.dashboards.admin.aggregates import constants
21
+
22
+INDEX_URL = constants.AGGREGATES_INDEX_URL
23
+
24
+
25
+class UpdateAggregateForm(forms.SelfHandlingForm):
26
+    name = forms.CharField(max_length="255", label=_("Name"))
27
+    availability_zone = forms.CharField(label=_("Availability zones"),
28
+                                        required=False)
29
+
30
+    def __init__(self, request, *args, **kwargs):
31
+        super(UpdateAggregateForm, self).__init__(request, *args, **kwargs)
32
+
33
+    def handle(self, request, data):
34
+        id = self.initial['id']
35
+        name = data['name']
36
+        availability_zone = data['availability_zone']
37
+        aggregate = {'name': name}
38
+        if availability_zone:
39
+            aggregate['availability_zone'] = availability_zone
40
+        try:
41
+            api.nova.aggregate_update(request, id, aggregate)
42
+            message = _('Successfully updated aggregate: "%s."') \
43
+                      % data['name']
44
+            messages.success(request, message)
45
+        except Exception:
46
+            exceptions.handle(request,
47
+                              _('Unable to update the aggregate.'))
48
+        return True

+ 25
- 0
openstack_dashboard/dashboards/admin/aggregates/panel.py View File

@@ -0,0 +1,25 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    a copy of the License at
4
+#
5
+#         http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#    Unless required by applicable law or agreed to in writing, software
8
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+from django.utils.translation import ugettext_lazy as _
14
+
15
+import horizon
16
+
17
+from openstack_dashboard.dashboards.admin import dashboard
18
+
19
+
20
+class Aggregates(horizon.Panel):
21
+    name = _("Host Aggregates")
22
+    slug = 'aggregates'
23
+
24
+
25
+dashboard.Admin.register(Aggregates)

+ 127
- 0
openstack_dashboard/dashboards/admin/aggregates/tables.py View File

@@ -0,0 +1,127 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    a copy of the License at
4
+#
5
+#         http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#    Unless required by applicable law or agreed to in writing, software
8
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+from django.template import defaultfilters as filters
14
+from django.utils.translation import ugettext_lazy as _
15
+
16
+from horizon import tables
17
+
18
+from openstack_dashboard import api
19
+from openstack_dashboard.dashboards.admin.aggregates import constants
20
+
21
+
22
+class DeleteAggregateAction(tables.DeleteAction):
23
+    data_type_singular = _("Host Aggregate")
24
+    data_type_plural = _("Host Aggregates")
25
+
26
+    def delete(self, request, obj_id):
27
+        api.nova.aggregate_delete(request, obj_id)
28
+
29
+
30
+class CreateAggregateAction(tables.LinkAction):
31
+    name = "create"
32
+    verbose_name = _("Create Host Aggregate")
33
+    url = constants.AGGREGATES_CREATE_URL
34
+    classes = ("ajax-modal", "btn-create")
35
+
36
+
37
+class ManageHostsAction(tables.LinkAction):
38
+    name = "manage"
39
+    verbose_name = _("Manage Hosts")
40
+    url = constants.AGGREGATES_MANAGE_HOSTS_URL
41
+    classes = ("ajax-modal", "btn-create")
42
+
43
+
44
+class UpdateAggregateAction(tables.LinkAction):
45
+    name = "update"
46
+    verbose_name = _("Edit Host Aggregate")
47
+    url = constants.AGGREGATES_UPDATE_URL
48
+    classes = ("ajax-modal", "btn-edit")
49
+
50
+
51
+class AggregateFilterAction(tables.FilterAction):
52
+    def filter(self, table, aggregates, filter_string):
53
+        q = filter_string.lower()
54
+
55
+        def comp(aggregate):
56
+            return q in aggregate.name.lower()
57
+
58
+        return filter(comp, aggregates)
59
+
60
+
61
+class AvailabilityZoneFilterAction(tables.FilterAction):
62
+    def filter(self, table, availability_zones, filter_string):
63
+        q = filter_string.lower()
64
+
65
+        def comp(availabilityZone):
66
+            return q in availabilityZone.name.lower()
67
+
68
+        return filter(comp, availability_zones)
69
+
70
+
71
+def get_aggregate_hosts(aggregate):
72
+    return [host for host in aggregate.hosts]
73
+
74
+
75
+def get_available(zone):
76
+    return zone.zoneState['available']
77
+
78
+
79
+def get_zone_hosts(zone):
80
+    hosts = zone.hosts
81
+    host_details = []
82
+    for name, services in hosts.items():
83
+        up = all([s['active'] and s['available'] for k, s in services.items()])
84
+        up = _("Services Up") if up else _("Services Down")
85
+        host_details.append("%(host)s (%(up)s)" % {'host': name, 'up': up})
86
+    return host_details
87
+
88
+
89
+class HostAggregatesTable(tables.DataTable):
90
+    name = tables.Column('name', verbose_name=_('Name'))
91
+    availability_zone = tables.Column('availability_zone',
92
+                                      verbose_name=_('Availability Zone'))
93
+    hosts = tables.Column(get_aggregate_hosts,
94
+                          verbose_name=_("Hosts"),
95
+                          wrap_list=True,
96
+                          filters=(filters.unordered_list,))
97
+
98
+    class Meta:
99
+        name = "host_aggregates"
100
+        verbose_name = _("Host Aggregates")
101
+        table_actions = (AggregateFilterAction,
102
+                         CreateAggregateAction,
103
+                         DeleteAggregateAction)
104
+        row_actions = (UpdateAggregateAction,
105
+                       ManageHostsAction,
106
+                       DeleteAggregateAction)
107
+
108
+
109
+class AvailabilityZonesTable(tables.DataTable):
110
+    name = tables.Column('zoneName',
111
+                         verbose_name=_('Availability Zone Name'))
112
+    hosts = tables.Column(get_zone_hosts,
113
+                          verbose_name=_('Hosts'),
114
+                          wrap_list=True,
115
+                          filters=(filters.unordered_list,))
116
+    available = tables.Column(get_available,
117
+                              verbose_name=_('Available'),
118
+                              status=True,
119
+                              filters=(filters.yesno, filters.capfirst))
120
+
121
+    def get_object_id(self, zone):
122
+        return zone.zoneName
123
+
124
+    class Meta:
125
+        name = "availability_zones"
126
+        verbose_name = _("Availability Zones")
127
+        table_actions = (AggregateFilterAction,)

+ 29
- 0
openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_manage_hosts.html View File

@@ -0,0 +1,29 @@
1
+{% extends "horizon/common/_modal_form.html" %}
2
+{% load i18n %}
3
+{% load url from future %}
4
+
5
+{% block form_id %}{% endblock %}
6
+{% block form_action %}{% url 'horizon:admin:aggregates:manage_hosts' id%}{% endblock %}
7
+
8
+{% block modal_id %}add_aggregate_modal{% endblock %}
9
+{% block modal-header %}{% trans "Manage Hosts" %}{% endblock %}
10
+
11
+{% block modal-body %}
12
+<div class="left">
13
+  <fieldset>
14
+  {% include "horizon/common/_form_fields.html" %}
15
+  </fieldset>
16
+</div>
17
+<div class="right">
18
+  <h3>{% trans "Description" %}:</h3>
19
+  <p>{% blocktrans %}
20
+    Here you can add/remove hosts to the selected aggregate host.
21
+    Note that while a host can be a member of multiple aggregates, it can belong to one availability zone at most.
22
+    {% endblocktrans %}</p>
23
+</div>
24
+{% endblock %}
25
+
26
+{% block modal-footer %}
27
+  <input class="btn btn-primary pull-right" type="submit" value="{% trans "Add Host" %}" />
28
+  <a href="{% url 'horizon:admin:aggregates:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
29
+{% endblock %}

+ 26
- 0
openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_update.html View File

@@ -0,0 +1,26 @@
1
+{% extends "horizon/common/_modal_form.html" %}
2
+{% load i18n %}
3
+{% load url from future %}
4
+
5
+{% block form_id %}edit_aggregate_form{% endblock %}
6
+{% block form_action %}{% url 'horizon:admin:aggregates:update' id %}{% endblock %}
7
+
8
+{% block modal_id %}edit_aggregate_modal{% endblock %}
9
+{% block modal-header %}{% trans "Edit Host Aggregate" %}{% endblock %}
10
+
11
+{% block modal-body %}
12
+<div class="left">
13
+  <fieldset>
14
+  {% include "horizon/common/_form_fields.html" %}
15
+  </fieldset>
16
+</div>
17
+<div class="right">
18
+  <h3>{% trans "Description" %}:</h3>
19
+  <p>{% trans "From here you can edit the aggregate name and availability zone" %}</p>
20
+</div>
21
+{% endblock %}
22
+
23
+{% block modal-footer %}
24
+  <input class="btn btn-primary pull-right" type="submit" value="{% trans "Save" %}" />
25
+  <a href="{% url 'horizon:admin:aggregates:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
26
+{% endblock %}

+ 11
- 0
openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/create.html View File

@@ -0,0 +1,11 @@
1
+{% extends 'base.html' %}
2
+{% load i18n %}
3
+{% block title %}{% trans "Create Host Aggregate" %}{% endblock %}
4
+
5
+{% block page_header %}
6
+  {% include "horizon/common/_page_header.html" with title=_("Create Host Aggregate") %}
7
+{% endblock page_header %}
8
+
9
+{% block main %}
10
+    {% include 'horizon/common/_workflow.html' %}
11
+{% endblock %}

+ 17
- 0
openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/index.html View File

@@ -0,0 +1,17 @@
1
+{% extends 'base.html' %}
2
+{% load i18n %}
3
+{% block title %}{% trans "Host Aggregates" %}{% endblock %}
4
+
5
+{% block page_header %}
6
+  {% include "horizon/common/_page_header.html" with title=_("Host Aggregates") %}
7
+{% endblock page_header %}
8
+
9
+{% block main %}
10
+  <div id="host-aggregates">
11
+      {{ host_aggregates_table.render }}
12
+  </div>
13
+
14
+  <div id="availability-zones">
15
+      {{ availability_zones_table.render }}
16
+  </div>
17
+{% endblock %}

+ 11
- 0
openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/manage_hosts.html View File

@@ -0,0 +1,11 @@
1
+{% extends 'base.html' %}
2
+{% load i18n %}
3
+{% block title %}{% trans "Manage Hosts Aggregate" %}{% endblock %}
4
+
5
+{% block page_header %}
6
+  {% include "horizon/common/_page_header.html" with title=_("Manage Hosts Aggregate") %}
7
+{% endblock page_header %}
8
+
9
+{% block main %}
10
+    {% include 'admin/aggregates/_manage_hosts.html' %}
11
+{% endblock %}

+ 12
- 0
openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/update.html View File

@@ -0,0 +1,12 @@
1
+{% extends 'base.html' %}
2
+{% load i18n %}
3
+{% block title %}{% trans "Edit Host Aggregate" %}{% endblock %}
4
+
5
+{% block page_header %}
6
+    {% include "horizon/common/_page_header.html" with title=_("Edit Host Aggregate") %}
7
+{% endblock page_header %}
8
+
9
+
10
+{% block main %}
11
+    {% include 'admin/aggregates/_update.html' %}
12
+{% endblock %}

+ 256
- 0
openstack_dashboard/dashboards/admin/aggregates/tests.py View File

@@ -0,0 +1,256 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    a copy of the License at
4
+#
5
+#         http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#    Unless required by applicable law or agreed to in writing, software
8
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+
14
+from django.core.urlresolvers import reverse
15
+from django import http
16
+from mox import IsA  # noqa
17
+
18
+from openstack_dashboard import api
19
+from openstack_dashboard.dashboards.admin.aggregates import constants
20
+from openstack_dashboard.dashboards.admin.aggregates import workflows
21
+from openstack_dashboard.test import helpers as test
22
+
23
+
24
+class BaseAggregateWorkflowTests(test.BaseAdminViewTests):
25
+
26
+    def _get_create_workflow_data(self, aggregate, hosts=None):
27
+        aggregate_info = {"name": aggregate.name,
28
+                          "availability_zone": aggregate.availability_zone}
29
+
30
+        if hosts:
31
+            compute_hosts = []
32
+            for host in hosts:
33
+                if host.service == 'compute':
34
+                    compute_hosts.append(host)
35
+
36
+            host_field_name = 'add_host_to_aggregate_role_member'
37
+            aggregate_info[host_field_name] = \
38
+                [h.host_name for h in compute_hosts]
39
+
40
+        return aggregate_info
41
+
42
+    def _get_manage_workflow_data(self, aggregate, hosts=None, ):
43
+        aggregate_info = {"id": aggregate.id}
44
+
45
+        if hosts:
46
+            compute_hosts = []
47
+            for host in hosts:
48
+                if host.service == 'compute':
49
+                    compute_hosts.append(host)
50
+
51
+            host_field_name = 'add_host_to_aggregate_role_member'
52
+            aggregate_info[host_field_name] = \
53
+                [h.host_name for h in compute_hosts]
54
+
55
+        return aggregate_info
56
+
57
+
58
+class CreateAggregateWorkflowTests(BaseAggregateWorkflowTests):
59
+
60
+    @test.create_stubs({api.nova: ('host_list', ), })
61
+    def test_workflow_get(self):
62
+
63
+        api.nova.host_list(IsA(http.HttpRequest)).AndReturn(self.hosts.list())
64
+        self.mox.ReplayAll()
65
+
66
+        url = reverse(constants.AGGREGATES_CREATE_URL)
67
+        res = self.client.get(url)
68
+        workflow = res.context['workflow']
69
+
70
+        self.assertTemplateUsed(res, constants.AGGREGATES_CREATE_VIEW_TEMPLATE)
71
+        self.assertEqual(workflow.name, workflows.CreateAggregateWorkflow.name)
72
+        self.assertQuerysetEqual(workflow.steps,
73
+                        ['<SetAggregateInfoStep: set_aggregate_info>',
74
+                        '<AddHostsToAggregateStep: add_host_to_aggregate>'])
75
+
76
+    @test.create_stubs({api.nova: ('host_list', 'aggregate_details_list',
77
+                                   'aggregate_create'), })
78
+    def test_create_aggregate(self):
79
+
80
+        aggregate = self.aggregates.first()
81
+
82
+        api.nova.host_list(IsA(http.HttpRequest)).AndReturn(self.hosts.list())
83
+        api.nova.aggregate_details_list(IsA(http.HttpRequest)).AndReturn([])
84
+
85
+        workflow_data = self._get_create_workflow_data(aggregate)
86
+        api.nova.aggregate_create(IsA(http.HttpRequest),
87
+                                  name=workflow_data['name'],
88
+                                  availability_zone=
89
+                                  workflow_data['availability_zone'])\
90
+            .AndReturn(aggregate)
91
+
92
+        self.mox.ReplayAll()
93
+
94
+        url = reverse(constants.AGGREGATES_CREATE_URL)
95
+        res = self.client.post(url, workflow_data)
96
+
97
+        self.assertNoFormErrors(res)
98
+        self.assertRedirectsNoFollow(res,
99
+                                     reverse(constants.AGGREGATES_INDEX_URL))
100
+
101
+    @test.create_stubs({api.nova: ('host_list',
102
+                                   'aggregate_details_list',
103
+                                   'aggregate_create',
104
+                                   'add_host_to_aggregate'), })
105
+    def test_create_aggregate_with_hosts(self):
106
+
107
+        aggregate = self.aggregates.first()
108
+        hosts = self.hosts.list()
109
+
110
+        api.nova.host_list(IsA(http.HttpRequest)).AndReturn(self.hosts.list())
111
+        api.nova.aggregate_details_list(IsA(http.HttpRequest)).AndReturn([])
112
+
113
+        workflow_data = self._get_create_workflow_data(aggregate, hosts)
114
+        api.nova.aggregate_create(IsA(http.HttpRequest),
115
+                                  name=workflow_data['name'],
116
+                                  availability_zone=
117
+                                  workflow_data['availability_zone'])\
118
+            .AndReturn(aggregate)
119
+
120
+        compute_hosts = []
121
+        for host in hosts:
122
+            if host.service == 'compute':
123
+                compute_hosts.append(host)
124
+
125
+        for host in compute_hosts:
126
+            api.nova.add_host_to_aggregate(IsA(http.HttpRequest),
127
+                                           aggregate.id, host.host_name)
128
+
129
+        self.mox.ReplayAll()
130
+
131
+        url = reverse(constants.AGGREGATES_CREATE_URL)
132
+        res = self.client.post(url, workflow_data)
133
+
134
+        self.assertNoFormErrors(res)
135
+        self.assertRedirectsNoFollow(res,
136
+                                     reverse(constants.AGGREGATES_INDEX_URL))
137
+
138
+    @test.create_stubs({api.nova: ('host_list', 'aggregate_details_list', ), })
139
+    def test_host_list_nova_compute(self):
140
+
141
+        hosts = self.hosts.list()
142
+        compute_hosts = []
143
+
144
+        for host in hosts:
145
+            if host.service == 'compute':
146
+                compute_hosts.append(host)
147
+
148
+        api.nova.host_list(IsA(http.HttpRequest)).AndReturn(self.hosts.list())
149
+
150
+        self.mox.ReplayAll()
151
+
152
+        url = reverse(constants.AGGREGATES_CREATE_URL)
153
+        res = self.client.get(url)
154
+        workflow = res.context['workflow']
155
+        step = workflow.get_step("add_host_to_aggregate")
156
+        field_name = step.get_member_field_name('member')
157
+        self.assertEqual(len(step.action.fields[field_name].choices),
158
+                         len(compute_hosts))
159
+
160
+
161
+class AggregatesViewTests(test.BaseAdminViewTests):
162
+
163
+    @test.create_stubs({api.nova: ('aggregate_details_list',
164
+                                   'availability_zone_list',), })
165
+    def test_index(self):
166
+        api.nova.aggregate_details_list(IsA(http.HttpRequest)) \
167
+                .AndReturn(self.aggregates.list())
168
+        api.nova.availability_zone_list(IsA(http.HttpRequest), detailed=True) \
169
+                .AndReturn(self.availability_zones.list())
170
+        self.mox.ReplayAll()
171
+
172
+        res = self.client.get(reverse(constants.AGGREGATES_INDEX_URL))
173
+        self.assertTemplateUsed(res, constants.AGGREGATES_INDEX_VIEW_TEMPLATE)
174
+        self.assertItemsEqual(res.context['host_aggregates_table'].data,
175
+                              self.aggregates.list())
176
+        self.assertItemsEqual(res.context['availability_zones_table'].data,
177
+                              self.availability_zones.list())
178
+
179
+    @test.create_stubs({api.nova: ('aggregate_update', 'aggregate_get',), })
180
+    def _test_generic_update_aggregate(self, form_data, aggregate,
181
+                                       error_count=0,
182
+                                       expected_error_message=None):
183
+        api.nova.aggregate_get(IsA(http.HttpRequest), str(aggregate.id))\
184
+                .AndReturn(aggregate)
185
+        if not expected_error_message:
186
+            az = form_data['availability_zone']
187
+            aggregate_data = {'name': form_data['name'],
188
+                              'availability_zone': az}
189
+            api.nova.aggregate_update(IsA(http.HttpRequest), str(aggregate.id),
190
+                                      aggregate_data)
191
+        self.mox.ReplayAll()
192
+
193
+        res = self.client.post(reverse(constants.AGGREGATES_UPDATE_URL,
194
+                               args=[aggregate.id]),
195
+                               form_data)
196
+
197
+        if not expected_error_message:
198
+            self.assertNoFormErrors(res)
199
+            self.assertRedirectsNoFollow(res,
200
+                    reverse(constants.AGGREGATES_INDEX_URL))
201
+        else:
202
+            self.assertFormErrors(res, error_count, expected_error_message)
203
+
204
+    def test_update_aggregate(self):
205
+        aggregate = self.aggregates.first()
206
+        form_data = {'id': aggregate.id,
207
+                     'name': 'my_new_name',
208
+                     'availability_zone': 'my_new_zone'}
209
+
210
+        self._test_generic_update_aggregate(form_data, aggregate)
211
+
212
+    def test_update_aggregate_fails_missing_fields(self):
213
+        aggregate = self.aggregates.first()
214
+        form_data = {'id': aggregate.id}
215
+
216
+        self._test_generic_update_aggregate(form_data, aggregate, 1,
217
+                                            u'This field is required')
218
+
219
+
220
+class ManageHostsTests(test.BaseAdminViewTests):
221
+
222
+    def test_manage_hosts(self):
223
+        aggregate = self.aggregates.first()
224
+        res = self.client.get(reverse(constants.AGGREGATES_MANAGE_HOSTS_URL,
225
+                                      args=[aggregate.id]))
226
+        self.assertEqual(res.status_code, 200)
227
+        self.assertTemplateUsed(res,
228
+                                constants.AGGREGATES_MANAGE_HOSTS_TEMPLATE)
229
+
230
+    @test.create_stubs({api.nova: ('aggregate_get', 'add_host_to_aggregate',
231
+                                   'host_list')})
232
+    def test_manage_hosts_update_empty_aggregate(self):
233
+        aggregate = self.aggregates.first()
234
+        aggregate.hosts = []
235
+        host = self.hosts.get(service="compute")
236
+
237
+        form_data = {'manageaggregatehostsaction_role_member':
238
+                     [host.host_name]}
239
+
240
+        api.nova.aggregate_get(IsA(http.HttpRequest), str(aggregate.id)) \
241
+                .AndReturn(aggregate)
242
+        api.nova.host_list(IsA(http.HttpRequest)) \
243
+                .AndReturn(self.hosts.list())
244
+        api.nova.aggregate_get(IsA(http.HttpRequest), str(aggregate.id)) \
245
+                .AndReturn(aggregate)
246
+        api.nova.add_host_to_aggregate(IsA(http.HttpRequest),
247
+                                       str(aggregate.id), host.host_name)
248
+        self.mox.ReplayAll()
249
+
250
+        res = self.client.post(reverse(constants.AGGREGATES_MANAGE_HOSTS_URL,
251
+                                       args=[aggregate.id]),
252
+                               form_data)
253
+
254
+        self.assertNoFormErrors(res)
255
+        self.assertRedirectsNoFollow(res,
256
+                                     reverse(constants.AGGREGATES_INDEX_URL))

+ 29
- 0
openstack_dashboard/dashboards/admin/aggregates/urls.py View File

@@ -0,0 +1,29 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    a copy of the License at
4
+#
5
+#         http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#    Unless required by applicable law or agreed to in writing, software
8
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+from django.conf.urls import patterns  # noqa
14
+from django.conf.urls import url  # noqa
15
+
16
+from openstack_dashboard.dashboards.admin.aggregates \
17
+    import views
18
+
19
+
20
+urlpatterns = patterns('openstack_dashboard.dashboards.admin.aggregates.views',
21
+    url(r'^$',
22
+        views.IndexView.as_view(), name='index'),
23
+    url(r'^create/$',
24
+        views.CreateView.as_view(), name='create'),
25
+    url(r'^(?P<id>[^/]+)/update/$',
26
+        views.UpdateView.as_view(), name='update'),
27
+    url(r'^(?P<id>[^/]+)/manage_hosts/$',
28
+        views.ManageHostsView.as_view(), name='manage_hosts'),
29
+)

+ 108
- 0
openstack_dashboard/dashboards/admin/aggregates/views.py View File

@@ -0,0 +1,108 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    a copy of the License at
4
+#
5
+#         http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#    Unless required by applicable law or agreed to in writing, software
8
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+from django.core.urlresolvers import reverse_lazy
14
+from django.utils.translation import ugettext_lazy as _
15
+
16
+from horizon import exceptions
17
+from horizon import forms
18
+from horizon import tables
19
+from horizon import workflows
20
+
21
+from openstack_dashboard import api
22
+from openstack_dashboard.dashboards.admin.aggregates \
23
+    import constants
24
+from openstack_dashboard.dashboards.admin.aggregates \
25
+    import forms as aggregate_forms
26
+from openstack_dashboard.dashboards.admin.aggregates \
27
+    import tables as project_tables
28
+from openstack_dashboard.dashboards.admin.aggregates \
29
+    import workflows as aggregate_workflows
30
+
31
+
32
+INDEX_URL = constants.AGGREGATES_INDEX_URL
33
+
34
+
35
+class IndexView(tables.MultiTableView):
36
+    table_classes = (project_tables.HostAggregatesTable,
37
+                     project_tables.AvailabilityZonesTable)
38
+    template_name = constants.AGGREGATES_TEMPLATE_NAME
39
+
40
+    def get_host_aggregates_data(self):
41
+        request = self.request
42
+        aggregates = []
43
+        try:
44
+            aggregates = api.nova.aggregate_details_list(self.request)
45
+        except Exception:
46
+            exceptions.handle(request,
47
+                              _('Unable to retrieve host aggregates list.'))
48
+        aggregates.sort(key=lambda aggregate: aggregate.name.lower())
49
+        return aggregates
50
+
51
+    def get_availability_zones_data(self):
52
+        request = self.request
53
+        availability_zones = []
54
+        try:
55
+            availability_zones = \
56
+                api.nova.availability_zone_list(self.request, detailed=True)
57
+        except Exception:
58
+            exceptions.handle(request,
59
+                              _('Unable to retrieve availability zone list.'))
60
+        availability_zones.sort(key=lambda az: az.zoneName.lower())
61
+        return availability_zones
62
+
63
+
64
+class CreateView(workflows.WorkflowView):
65
+    workflow_class = aggregate_workflows.CreateAggregateWorkflow
66
+    template_name = constants.AGGREGATES_CREATE_VIEW_TEMPLATE
67
+
68
+
69
+class UpdateView(forms.ModalFormView):
70
+    template_name = constants.AGGREGATES_UPDATE_VIEW_TEMPLATE
71
+    form_class = aggregate_forms.UpdateAggregateForm
72
+    success_url = reverse_lazy(constants.AGGREGATES_INDEX_URL)
73
+
74
+    def get_initial(self):
75
+        aggregate = self.get_object()
76
+        return {'id': self.kwargs["id"],
77
+                'name': aggregate.name,
78
+                'availability_zone': aggregate.availability_zone}
79
+
80
+    def get_context_data(self, **kwargs):
81
+        context = super(UpdateView, self).get_context_data(**kwargs)
82
+        context['id'] = self.kwargs['id']
83
+        return context
84
+
85
+    def get_object(self):
86
+        if not hasattr(self, "_object"):
87
+            aggregate_id = self.kwargs['id']
88
+            try:
89
+                self._object = \
90
+                    api.nova.aggregate_get(self.request, aggregate_id)
91
+            except Exception:
92
+                msg = _('Unable to retrieve the aggregate to be updated')
93
+                exceptions.handle(self.request, msg)
94
+        return self._object
95
+
96
+
97
+class ManageHostsView(workflows.WorkflowView):
98
+    template_name = constants.AGGREGATES_MANAGE_HOSTS_TEMPLATE
99
+    workflow_class = aggregate_workflows.ManageAggregateHostsWorkflow
100
+    success_url = reverse_lazy(constants.AGGREGATES_INDEX_URL)
101
+
102
+    def get_initial(self):
103
+        return {'id': self.kwargs["id"]}
104
+
105
+    def get_context_data(self, **kwargs):
106
+        context = super(ManageHostsView, self).get_context_data(**kwargs)
107
+        context['id'] = self.kwargs['id']
108
+        return context

+ 238
- 0
openstack_dashboard/dashboards/admin/aggregates/workflows.py View File

@@ -0,0 +1,238 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    a copy of the License at
4
+#
5
+#         http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#    Unless required by applicable law or agreed to in writing, software
8
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+from django.utils.translation import ugettext_lazy as _
14
+
15
+from horizon import exceptions
16
+from horizon import forms
17
+from horizon import workflows
18
+
19
+from openstack_dashboard import api
20
+from openstack_dashboard.dashboards.admin.aggregates import constants
21
+
22
+
23
+class SetAggregateInfoAction(workflows.Action):
24
+    name = forms.CharField(label=_("Name"),
25
+                           max_length=255)
26
+
27
+    availability_zone = forms.CharField(label=_("Availability Zone"),
28
+                                        max_length=255,
29
+                                        required=False)
30
+
31
+    class Meta:
32
+        name = _("Host Aggregate Info")
33
+        help_text = _("From here you can create a new "
34
+                      "host aggregate to organize instances.")
35
+        slug = "set_aggregate_info"
36
+
37
+    def clean(self):
38
+        cleaned_data = super(SetAggregateInfoAction, self).clean()
39
+        name = cleaned_data.get('name')
40
+
41
+        try:
42
+            aggregates = api.nova.aggregate_details_list(self.request)
43
+        except Exception:
44
+            msg = _('Unable to get host aggregate list')
45
+            exceptions.check_message(["Connection", "refused"], msg)
46
+            raise
47
+        if aggregates is not None:
48
+            for aggregate in aggregates:
49
+                if aggregate.name.lower() == name.lower():
50
+                    raise forms.ValidationError(
51
+                        _('The name "%s" is already used by '
52
+                          'another host aggregate.')
53
+                        % name
54
+                    )
55
+        return cleaned_data
56
+
57
+
58
+class SetAggregateInfoStep(workflows.Step):
59
+    action_class = SetAggregateInfoAction
60
+    contributes = ("availability_zone",
61
+                   "name")
62
+
63
+
64
+class AddHostsToAggregateAction(workflows.MembershipAction):
65
+    def __init__(self, request, *args, **kwargs):
66
+        super(AddHostsToAggregateAction, self).__init__(request,
67
+                                                        *args,
68
+                                                        **kwargs)
69
+        err_msg = _('Unable to get the available hosts')
70
+
71
+        default_role_field_name = self.get_default_role_field_name()
72
+        self.fields[default_role_field_name] = forms.CharField(required=False)
73
+        self.fields[default_role_field_name].initial = 'member'
74
+
75
+        field_name = self.get_member_field_name('member')
76
+        self.fields[field_name] = forms.MultipleChoiceField(required=False)
77
+
78
+        hosts = []
79
+        try:
80
+            hosts = api.nova.host_list(request)
81
+        except Exception:
82
+            exceptions.handle(request, err_msg)
83
+
84
+        host_names = []
85
+        for host in hosts:
86
+            if host.host_name not in host_names and host.service == u'compute':
87
+                host_names.append(host.host_name)
88
+        host_names.sort()
89
+
90
+        self.fields[field_name].choices = \
91
+            [(host_name, host_name) for host_name in host_names]
92
+
93
+    class Meta:
94
+        name = _("Hosts within aggregate")
95
+        slug = "add_host_to_aggregate"
96
+
97
+
98
+class ManageAggregateHostsAction(workflows.MembershipAction):
99
+    def __init__(self, request, *args, **kwargs):
100
+        super(ManageAggregateHostsAction, self).__init__(request,
101
+                                                         *args,
102
+                                                         **kwargs)
103
+        err_msg = _('Unable to get the available hosts')
104
+
105
+        default_role_field_name = self.get_default_role_field_name()
106
+        self.fields[default_role_field_name] = forms.CharField(required=False)
107
+        self.fields[default_role_field_name].initial = 'member'
108
+
109
+        field_name = self.get_member_field_name('member')
110
+        self.fields[field_name] = forms.MultipleChoiceField(required=False)
111
+
112
+        aggregate_id = self.initial['id']
113
+        aggregate = api.nova.aggregate_get(request, aggregate_id)
114
+        aggregate_hosts = aggregate.hosts
115
+
116
+        hosts = []
117
+        try:
118
+            hosts = api.nova.host_list(request)
119
+        except Exception:
120
+            exceptions.handle(request, err_msg)
121
+
122
+        host_names = []
123
+        for host in hosts:
124
+            if host.host_name not in host_names and host.service == u'compute':
125
+                host_names.append(host.host_name)
126
+        host_names.sort()
127
+
128
+        self.fields[field_name].choices = \
129
+            [(host_name, host_name) for host_name in host_names]
130
+
131
+        self.fields[field_name].initial = aggregate_hosts
132
+
133
+    class Meta:
134
+        name = _("Hosts within aggregate")
135
+
136
+
137
+class AddHostsToAggregateStep(workflows.UpdateMembersStep):
138
+    action_class = AddHostsToAggregateAction
139
+    help_text = _("You can add hosts to this aggregate. One host can be added "
140
+                  "to one or more aggregate. You can also add the hosts later "
141
+                  "by editing the aggregate.")
142
+    available_list_title = _("All available hosts")
143
+    members_list_title = _("Selected hosts")
144
+    no_available_text = _("No hosts found.")
145
+    no_members_text = _("No host selected.")
146
+    show_roles = False
147
+    contributes = ("hosts_aggregate",)
148
+
149
+    def contribute(self, data, context):
150
+        if data:
151
+            member_field_name = self.get_member_field_name('member')
152
+            context['hosts_aggregate'] = data.get(member_field_name, [])
153
+        return context
154
+
155
+
156
+class ManageAggregateHostsStep(workflows.UpdateMembersStep):
157
+    action_class = ManageAggregateHostsAction
158
+    help_text = _("You can add hosts to this aggregate, as well as remove "
159
+                  "hosts from it.")
160
+    available_list_title = _("All Available Hosts")
161
+    members_list_title = _("Selected Hosts")
162
+    no_available_text = _("No Hosts found.")
163
+    no_members_text = _("No Host selected.")
164
+    show_roles = False
165
+    depends_on = ("id",)
166
+    contributes = ("hosts_aggregate",)
167
+
168
+    def contribute(self, data, context):
169
+        if data:
170
+            member_field_name = self.get_member_field_name('member')
171
+            context['hosts_aggregate'] = data.get(member_field_name, [])
172
+        return context
173
+
174
+
175
+class CreateAggregateWorkflow(workflows.Workflow):
176
+    slug = "create_aggregate"
177
+    name = _("Create Host Aggregate")
178
+    finalize_button_name = _("Create Host Aggregate")
179
+    success_message = _('Created new host aggregate "%s".')
180
+    failure_message = _('Unable to create host aggregate "%s".')
181
+    success_url = constants.AGGREGATES_INDEX_URL
182
+    default_steps = (SetAggregateInfoStep, AddHostsToAggregateStep)
183
+
184
+    def format_status_message(self, message):
185
+        return message % self.context['name']
186
+
187
+    def handle(self, request, context):
188
+        try:
189
+            self.object = \
190
+                api.nova.aggregate_create(
191
+                    request,
192
+                    name=context['name'],
193
+                    availability_zone=context['availability_zone'])
194
+        except Exception:
195
+            exceptions.handle(request, _('Unable to create host aggregate.'))
196
+            return False
197
+
198
+        hosts = context['hosts_aggregate']
199
+        for host in hosts:
200
+            try:
201
+                api.nova.add_host_to_aggregate(request, self.object.id, host)
202
+            except Exception:
203
+                exceptions.handle(
204
+                    request, _('Error adding Hosts to the aggregate.'))
205
+                return False
206
+
207
+        return True
208
+
209
+
210
+class ManageAggregateHostsWorkflow(workflows.Workflow):
211
+    slug = "manage_hosts_aggregate"
212
+    name = _("Add/Remove Hosts to Aggregate")
213
+    finalize_button_name = _("Save")
214
+    success_message = _('The Aggregate was updated.')
215
+    failure_message = _('Unable to update the aggregate.')
216
+    success_url = constants.AGGREGATES_INDEX_URL
217
+    default_steps = (ManageAggregateHostsStep, )
218
+
219
+    def format_status_message(self, message):
220
+        return message
221
+
222
+    def handle(self, request, context):
223
+        hosts_aggregate = context['hosts_aggregate']
224
+        aggregate_id = context['id']
225
+        aggregate = api.nova.aggregate_get(request, aggregate_id)
226
+        aggregate_hosts = aggregate.hosts
227
+        for host in aggregate_hosts:
228
+            api.nova.remove_host_from_aggregate(request, aggregate_id, host)
229
+
230
+        for host in hosts_aggregate:
231
+            try:
232
+                api.nova.add_host_to_aggregate(request, aggregate_id, host)
233
+            except Exception:
234
+                exceptions.handle(
235
+                    request, _('Error updating the aggregate.'))
236
+                return False
237
+
238
+        return True

+ 3
- 2
openstack_dashboard/dashboards/admin/dashboard.py View File

@@ -22,8 +22,9 @@ import horizon
22 22
 class SystemPanels(horizon.PanelGroup):
23 23
     slug = "admin"
24 24
     name = _("System Panel")
25
-    panels = ('overview', 'metering', 'hypervisors', 'instances', 'volumes',
26
-              'flavors', 'images', 'networks', 'routers', 'defaults', 'info')
25
+    panels = ('overview', 'metering', 'hypervisors', 'aggregates',
26
+              'instances', 'volumes', 'flavors', 'images',
27
+              'networks', 'routers', 'defaults', 'info')
27 28
 
28 29
 
29 30
 class IdentityPanels(horizon.PanelGroup):

+ 17
- 0
openstack_dashboard/dashboards/admin/info/constants.py View File

@@ -0,0 +1,17 @@
1
+# Copyright 2014 Intel Corporation
2
+# All Rights Reserved.
3
+#
4
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5
+#    not use this file except in compliance with the License. You may obtain
6
+#    a copy of the License at
7
+#
8
+#         http://www.apache.org/licenses/LICENSE-2.0
9
+#
10
+#    Unless required by applicable law or agreed to in writing, software
11
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+#    License for the specific language governing permissions and limitations
14
+#    under the License.
15
+
16
+INFO_TEMPLATE_NAME = 'admin/info/index.html'
17
+INFO_DETAIL_TEMPLATE_NAME = 'horizon/common/_detail_table.html'

+ 0
- 59
openstack_dashboard/dashboards/admin/info/tables.py View File

@@ -66,37 +66,6 @@ def get_available(zone):
66 66
     return zone.zoneState['available']
67 67
 
68 68
 
69
-def get_zone_hosts(zone):
70
-    hosts = zone.hosts
71
-    host_details = []
72
-    for name, services in hosts.items():
73
-        up = all([s['active'] and s['available'] for k, s in services.items()])
74
-        up = _("Services Up") if up else _("Services Down")
75
-        host_details.append("%(host)s (%(up)s)" % {'host': name, 'up': up})
76
-    return host_details
77
-
78
-
79
-class ZonesTable(tables.DataTable):
80
-    name = tables.Column('zoneName', verbose_name=_('Name'))
81
-    hosts = tables.Column(get_zone_hosts,
82
-                          verbose_name=_('Hosts'),
83
-                          wrap_list=True,
84
-                          filters=(filters.unordered_list,))
85
-    available = tables.Column(get_available,
86
-                              verbose_name=_('Available'),
87
-                              status=True,
88
-                              filters=(filters.yesno, filters.capfirst))
89
-
90
-    def get_object_id(self, zone):
91
-        return zone.zoneName
92
-
93
-    class Meta:
94
-        name = "zones"
95
-        verbose_name = _("Availability Zones")
96
-        multi_select = False
97
-        status_columns = ["available"]
98
-
99
-
100 69
 class NovaServiceFilterAction(tables.FilterAction):
101 70
     def filter(self, table, services, filter_string):
102 71
         q = filter_string.lower()
@@ -130,34 +99,6 @@ class NovaServicesTable(tables.DataTable):
130 99
         multi_select = False
131 100
 
132 101
 
133
-def get_aggregate_hosts(aggregate):
134
-    return [host for host in aggregate.hosts]
135
-
136
-
137
-def get_metadata(aggregate):
138
-    return [' = '.join([key, val]) for key, val
139
-            in aggregate.metadata.iteritems()]
140
-
141
-
142
-class AggregatesTable(tables.DataTable):
143
-    name = tables.Column("name",
144
-                         verbose_name=_("Name"))
145
-    availability_zone = tables.Column("availability_zone",
146
-                                      verbose_name=_("Availability Zone"))
147
-    hosts = tables.Column(get_aggregate_hosts,
148
-                          verbose_name=_("Hosts"),
149
-                          wrap_list=True,
150
-                          filters=(filters.unordered_list,))
151
-    metadata = tables.Column(get_metadata,
152
-                             verbose_name=_("Metadata"),
153
-                             wrap_list=True,
154
-                             filters=(filters.unordered_list,))
155
-
156
-    class Meta:
157
-        name = "aggregates"
158
-        verbose_name = _("Host Aggregates")
159
-
160
-
161 102
 class NetworkAgentsFilterAction(tables.FilterAction):
162 103
     def filter(self, table, agents, filter_string):
163 104
         q = filter_string.lower()

+ 4
- 41
openstack_dashboard/dashboards/admin/info/tabs.py View File

@@ -1,5 +1,3 @@
1
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
-
3 1
 # Copyright 2012 Nebula, Inc.
4 2
 #
5 3
 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -24,6 +22,7 @@ from openstack_dashboard.api import keystone
24 22
 from openstack_dashboard.api import neutron
25 23
 from openstack_dashboard.api import nova
26 24
 
25
+from openstack_dashboard.dashboards.admin.info import constants
27 26
 from openstack_dashboard.dashboards.admin.info import tables
28 27
 
29 28
 
@@ -31,7 +30,7 @@ class ServicesTab(tabs.TableTab):
31 30
     table_classes = (tables.ServicesTable,)
32 31
     name = _("Services")
33 32
     slug = "services"
34
-    template_name = ("horizon/common/_detail_table.html")
33
+    template_name = constants.INFO_DETAIL_TEMPLATE_NAME
35 34
 
36 35
     def get_services_data(self):
37 36
         request = self.tab_group.request
@@ -43,50 +42,16 @@ class ServicesTab(tabs.TableTab):
43 42
         return services
44 43
 
45 44
 
46
-class ZonesTab(tabs.TableTab):
47
-    table_classes = (tables.ZonesTable,)
48
-    name = _("Availability Zones")
49
-    slug = "zones"
50
-    template_name = ("horizon/common/_detail_table.html")
51
-
52
-    def get_zones_data(self):
53
-        request = self.tab_group.request
54
-        zones = []
55
-        try:
56
-            zones = nova.availability_zone_list(request, detailed=True)
57
-        except Exception:
58
-            msg = _('Unable to retrieve availability zone data.')
59
-            exceptions.handle(request, msg)
60
-        return zones
61
-
62
-
63
-class HostAggregatesTab(tabs.TableTab):
64
-    table_classes = (tables.AggregatesTable,)
65
-    name = _("Host Aggregates")
66
-    slug = "aggregates"
67
-    template_name = ("horizon/common/_detail_table.html")
68
-
69
-    def get_aggregates_data(self):
70
-        aggregates = []
71
-        try:
72
-            aggregates = nova.aggregate_list(self.tab_group.request)
73
-        except Exception:
74
-            exceptions.handle(self.request,
75
-                _('Unable to retrieve host aggregates list.'))
76
-        return aggregates
77
-
78
-
79 45
 class NovaServicesTab(tabs.TableTab):
80 46
     table_classes = (tables.NovaServicesTable,)
81 47
     name = _("Compute Services")
82 48
     slug = "nova_services"
83
-    template_name = ("horizon/common/_detail_table.html")
49
+    template_name = constants.INFO_DETAIL_TEMPLATE_NAME
84 50
 
85 51
     def get_nova_services_data(self):
86 52
         try:
87 53
             services = nova.service_list(self.tab_group.request)
88 54
         except Exception:
89
-            services = []
90 55
             msg = _('Unable to get nova services list.')
91 56
             exceptions.check_message(["Connection", "refused"], msg)
92 57
             raise
@@ -98,7 +63,7 @@ class NetworkAgentsTab(tabs.TableTab):
98 63
     table_classes = (tables.NetworkAgentsTable,)
99 64
     name = _("Network Agents")
100 65
     slug = "network_agents"
101
-    template_name = ("horizon/common/_detail_table.html")
66
+    template_name = constants.INFO_DETAIL_TEMPLATE_NAME
102 67
 
103 68
     def allowed(self, request):
104 69
         return base.is_service_enabled(request, 'network')
@@ -107,7 +72,6 @@ class NetworkAgentsTab(tabs.TableTab):
107 72
         try:
108 73
             agents = neutron.agent_list(self.tab_group.request)
109 74
         except Exception:
110
-            agents = []
111 75
             msg = _('Unable to get network agents list.')
112 76
             exceptions.check_message(["Connection", "refused"], msg)
113 77
             raise
@@ -118,6 +82,5 @@ class NetworkAgentsTab(tabs.TableTab):
118 82
 class SystemInfoTabs(tabs.TabGroup):
119 83
     slug = "system_info"
120 84
     tabs = (ServicesTab, NovaServicesTab,
121
-            ZonesTab, HostAggregatesTab,
122 85
             NetworkAgentsTab)
123 86
     sticky = True

+ 1
- 15
openstack_dashboard/dashboards/admin/info/tests.py View File

@@ -26,17 +26,11 @@ INDEX_URL = reverse('horizon:admin:info:index')
26 26
 
27 27
 class SystemInfoViewTests(test.BaseAdminViewTests):
28 28
 
29
-    @test.create_stubs({api.nova: ('service_list',
30
-                                   'availability_zone_list',
31
-                                   'aggregate_list'),
29
+    @test.create_stubs({api.nova: ('service_list',),
32 30
                         api.neutron: ('agent_list',)})
33 31
     def test_index(self):
34 32
         services = self.services.list()
35 33
         api.nova.service_list(IsA(http.HttpRequest)).AndReturn(services)
36
-        api.nova.availability_zone_list(IsA(http.HttpRequest), detailed=True) \
37
-            .AndReturn(self.availability_zones.list())
38
-        api.nova.aggregate_list(IsA(http.HttpRequest)) \
39
-            .AndReturn(self.aggregates.list())
40 34
         agents = self.agents.list()
41 35
         api.neutron.agent_list(IsA(http.HttpRequest)).AndReturn(agents)
42 36
 
@@ -59,14 +53,6 @@ class SystemInfoViewTests(test.BaseAdminViewTests):
59 53
                                   '<Service: orchestration>',
60 54
                                   '<Service: database>'])
61 55
 
62
-        zones_tab = res.context['tab_group'].get_tab('zones')
63
-        self.assertQuerysetEqual(zones_tab._tables['zones'].data,
64
-                                 ['<AvailabilityZone: nova>'])
65
-
66
-        aggregates_tab = res.context['tab_group'].get_tab('aggregates')
67
-        self.assertQuerysetEqual(aggregates_tab._tables['aggregates'].data,
68
-                                 ['<Aggregate: 1>', '<Aggregate: 2>'])
69
-
70 56
         network_agents_tab = res.context['tab_group'].get_tab('network_agents')
71 57
         self.assertQuerysetEqual(
72 58
             network_agents_tab._tables['network_agents'].data,

+ 2
- 3
openstack_dashboard/dashboards/admin/info/views.py View File

@@ -1,5 +1,3 @@
1
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
-
3 1
 # Copyright 2012 United States Government as represented by the
4 2
 # Administrator of the National Aeronautics and Space Administration.
5 3
 # All Rights Reserved.
@@ -20,9 +18,10 @@
20 18
 
21 19
 from horizon import tabs
22 20
 
21
+from openstack_dashboard.dashboards.admin.info import constants
23 22
 from openstack_dashboard.dashboards.admin.info import tabs as project_tabs
24 23
 
25 24
 
26 25
 class IndexView(tabs.TabbedTableView):
27 26
     tab_group_class = project_tabs.SystemInfoTabs
28
-    template_name = 'admin/info/index.html'
27
+    template_name = constants.INFO_TEMPLATE_NAME

+ 22
- 1
openstack_dashboard/test/test_data/nova_data.py View File

@@ -21,6 +21,7 @@ from novaclient.v1_1 import certs
21 21
 from novaclient.v1_1 import flavor_access
22 22
 from novaclient.v1_1 import flavors
23 23
 from novaclient.v1_1 import floating_ips
24
+from novaclient.v1_1 import hosts
24 25
 from novaclient.v1_1 import hypervisors
25 26
 from novaclient.v1_1 import keypairs
26 27
 from novaclient.v1_1 import quotas
@@ -170,6 +171,7 @@ def data(TEST):
170 171
     TEST.hypervisors = utils.TestDataContainer()
171 172
     TEST.services = utils.TestDataContainer()
172 173
     TEST.aggregates = utils.TestDataContainer()
174
+    TEST.hosts = utils.TestDataContainer()
173 175
 
174 176
     # Data return by novaclient.
175 177
     # It is used if API layer does data conversion.
@@ -620,7 +622,7 @@ def data(TEST):
620 622
     aggregate_1 = aggregates.Aggregate(aggregates.AggregateManager(None),
621 623
         {
622 624
             "name": "foo",
623
-            "availability_zone": None,
625
+            "availability_zone": "testing",
624 626
             "deleted": 0,
625 627
             "created_at": "2013-07-04T13:34:38.000000",
626 628
             "updated_at": None,
@@ -653,3 +655,22 @@ def data(TEST):
653 655
 
654 656
     TEST.aggregates.add(aggregate_1)
655 657
     TEST.aggregates.add(aggregate_2)
658
+
659
+    host1 = hosts.Host(hosts.HostManager(None),
660
+        {
661
+            "host_name": "devstack001",
662
+            "service": "compute",
663
+            "zone": "testing"
664
+        }
665
+    )
666
+
667
+    host2 = hosts.Host(hosts.HostManager(None),
668
+        {
669
+            "host_name": "devstack002",
670
+            "service": "nova-conductor",
671
+            "zone": "testing"
672
+        }
673
+    )
674
+
675
+    TEST.hosts.add(host1)
676
+    TEST.hosts.add(host2)

Loading…
Cancel
Save