Merge lp:~rvb/maas/cluster-own-ui4-1.5 into lp:maas/1.5

Proposed by Raphaël Badin on 2014-04-03
Status: Merged
Approved by: Raphaël Badin on 2014-04-03
Approved revision: 2220
Merged at revision: 2220
Proposed branch: lp:~rvb/maas/cluster-own-ui4-1.5
Merge into: lp:maas/1.5
Diff against target: 959 lines (+412/-205)
18 files modified
docs/man/maas-import-pxe-files.8.rst (+1/-1)
man/maas-import-pxe-files.8 (+2/-2)
src/maasserver/api.py (+1/-2)
src/maasserver/templates/maasserver/base.html (+5/-0)
src/maasserver/templates/maasserver/cluster_listing.html (+54/-58)
src/maasserver/templates/maasserver/cluster_listing_head.html (+10/-0)
src/maasserver/templates/maasserver/cluster_listing_row.html (+16/-3)
src/maasserver/templates/maasserver/no-bootimages-warning.html (+1/-1)
src/maasserver/templates/maasserver/nodegroup_confirm_delete.html (+1/-1)
src/maasserver/templates/maasserver/nodegroup_edit.html (+1/-1)
src/maasserver/templates/maasserver/settings.html (+0/-5)
src/maasserver/tests/test_api.py (+3/-1)
src/maasserver/urls.py (+22/-9)
src/maasserver/views/clusters.py (+108/-3)
src/maasserver/views/settings.py (+1/-38)
src/maasserver/views/tests/test_boot_image_list.py (+1/-1)
src/maasserver/views/tests/test_clusters.py (+185/-6)
src/maasserver/views/tests/test_settings.py (+0/-73)
To merge this branch: bzr merge lp:~rvb/maas/cluster-own-ui4-1.5
Reviewer Review Type Date Requested Status
Raphaël Badin (community) Approve on 2014-04-03
Review via email: mp+214006@code.launchpad.net

Commit message

Backport revision 2223: This branch makes the cluster listings deal gracefully with a large number of clusters. Additionally, it makes the cluster listing pages more accessible.

In details:
- Move the cluster listings to their own url (instead of displaying them on the 'settings' page).
- Split the cluster listings into 3 separate listings (one page for each possible cluster status).
- Add a link to the cluster listing page next to 'Nodes' at the top of the page.
- Add pagination to all the listings.

To post a comment you must log in.
Raphaël Badin (rvb) wrote :

Simple backport: self-approving.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'docs/man/maas-import-pxe-files.8.rst'
2--- docs/man/maas-import-pxe-files.8.rst 2014-04-01 07:59:47 +0000
3+++ docs/man/maas-import-pxe-files.8.rst 2014-04-03 11:47:02 +0000
4@@ -20,7 +20,7 @@
5 An easier way to run the script is to trigger it from the MAAS web user
6 interface. To do that, log in to your MAAS as an administrator using a
7 web browser, click the cogwheel icon in the top right of the page to go
8-to the Settings page, and click "Import boot images." This will start
9+to the Clusters page, and click "Import boot images." This will start
10 imports on all cluster controllers simultaneously. The same thing can
11 also be done through the region-controller API, or through the
12 command-line interface.
13
14=== modified file 'man/maas-import-pxe-files.8'
15--- man/maas-import-pxe-files.8 2014-04-01 07:59:47 +0000
16+++ man/maas-import-pxe-files.8 2014-04-03 11:47:02 +0000
17@@ -1,6 +1,6 @@
18 .\" Man page generated from reStructuredText.
19 .
20-.TH "MAAS-IMPORT-PXE-FILES" "8" "April 01, 2014" "1.5" "MAAS"
21+.TH "MAAS-IMPORT-PXE-FILES" "8" "April 03, 2014" "1.5" "MAAS"
22 .SH NAME
23 maas-import-pxe-files \- MAAS helper script
24 .
25@@ -46,7 +46,7 @@
26 An easier way to run the script is to trigger it from the MAAS web user
27 interface. To do that, log in to your MAAS as an administrator using a
28 web browser, click the cogwheel icon in the top right of the page to go
29-to the Settings page, and click "Import boot images." This will start
30+to the Clusters page, and click "Import boot images." This will start
31 imports on all cluster controllers simultaneously. The same thing can
32 also be done through the region\-controller API, or through the
33 command\-line interface.
34
35=== modified file 'src/maasserver/api.py'
36--- src/maasserver/api.py 2014-03-27 17:24:02 +0000
37+++ src/maasserver/api.py 2014-04-03 11:47:02 +0000
38@@ -2492,8 +2492,7 @@
39 nodegroups_without_images = nodegroups_without_images.filter(
40 status=NODEGROUP_STATUS.ACCEPTED)
41 if nodegroups_without_images.exists():
42- accepted_clusters_url = (
43- "%s#accepted-clusters" % absolute_reverse("settings"))
44+ accepted_clusters_url = absolute_reverse("cluster-list")
45 warning = dedent("""\
46 Some cluster controllers are missing boot images. Either the
47 import task has not been initiated (for each cluster, the task
48
49=== modified file 'src/maasserver/templates/maasserver/base.html'
50--- src/maasserver/templates/maasserver/base.html 2014-02-25 09:57:24 +0000
51+++ src/maasserver/templates/maasserver/base.html 2014-04-03 11:47:02 +0000
52@@ -72,6 +72,11 @@
53 <li class="{% block nav-active-node-list %}{% endblock %}">
54 <a href="{% url 'node-list' %}">Nodes</a>
55 </li>
56+ {% if user.is_superuser %}
57+ <li class="{% block nav-active-cluster-list %}{% endblock %}">
58+ <a href="{% url 'cluster-list' %}">Clusters</a>
59+ </li>
60+ {% endif %}
61 <li class="{% block nav-active-zone-list %}{% endblock %}">
62 <a href="{% url 'zone-list' %}">Zones</a>
63 </li>
64
65=== renamed file 'src/maasserver/templates/maasserver/settings_cluster_listing.html' => 'src/maasserver/templates/maasserver/cluster_listing.html'
66--- src/maasserver/templates/maasserver/settings_cluster_listing.html 2012-11-07 10:39:19 +0000
67+++ src/maasserver/templates/maasserver/cluster_listing.html 2014-04-03 11:47:02 +0000
68@@ -1,58 +1,54 @@
69-<h2 id="accepted-clusters">Cluster controllers</h2>
70-<table class="list" id="accepted-clusters-list">
71- <tbody>
72- {% for cluster in accepted_clusters %}
73- {% cycle 'even' 'odd' as cycle silent %}
74- {% include "maasserver/settings_cluster_listing_row.html" with cycle=cycle %}
75- {% endfor %}
76- </tbody>
77-</table>
78-<div>
79- <form method="POST"
80- action="{% url 'settings' %}">
81- {% csrf_token %}
82- <input type="hidden" name="import_all_boot_images" value="1" />
83- <input type="submit" class="button right" value="Import boot images" />
84- </form>
85-</div>
86-<div class="clear"></div>
87-{% if pending_clusters %}
88-<h3 id="pending-clusters">Pending clusters</h3>
89-<table class="list" id="pending-clusters-list">
90- <tbody>
91- {% for cluster in pending_clusters %}
92- {% cycle 'even' 'odd' as cycle silent %}
93- {% include "maasserver/settings_cluster_listing_row.html" with cycle=cycle %}
94- {% endfor %}
95- </tbody>
96-</table>
97-<div>
98- <form id="reject_all_pending_nodegroups"
99- method="POST"
100- action="{% url 'settings' %}">
101- {% csrf_token %}
102- <input type="hidden" name="mass_accept_submit" value="1" />
103- <input type="submit" class="button right" value="Accept all" />
104- </form>
105- <form id="accept_all_pending_nodegroups"
106- method="POST"
107- action="{% url 'settings' %}">
108- {% csrf_token %}
109- <input type="hidden" name="mass_reject_submit" value="1" />
110- <input type="submit" class="button right space-right-small"
111- value="Reject all" />
112- </form>
113-</div>
114-<div class="clear"></div>
115-{% endif %}
116-{% if rejected_clusters %}
117-<h3 id="rejected-clusters">Rejected clusters</h3>
118-<table class="list" id="rejected-clusters-list">
119- <tbody>
120- {% for cluster in rejected_clusters %}
121- {% cycle 'even' 'odd' as cycle silent %}
122- {% include "maasserver/settings_cluster_listing_row.html" with cycle=cycle %}
123- {% endfor %}
124- </tbody>
125-</table>
126-{% endif %}
127+{% extends "maasserver/base.html" %}
128+
129+{% block nav-active-cluster-list %}active{% endblock %}
130+{% block title %}{{ status_name }} clusters{% endblock %}
131+{% block page-title %}{{ current_count }}{% if input_query %} matching{% endif %} {{ status_name|lower }} cluster{{ current_count|pluralize }} in {% include "maasserver/site_title.html" %}{% endblock %}
132+{% block site-switcher %}{% endblock %}
133+{% block header-search %}{% endblock %}
134+
135+{% block content %}
136+<div id="clusters" style="position: relative;">
137+
138+<h2 id="clusters">{{ title }}</h2>
139+<table class="list" id="clusters-list">
140+ {% include "maasserver/cluster_listing_head.html" %}
141+ <tbody>
142+ {% for cluster in cluster_list %}
143+ {% cycle 'even' 'odd' as cycle silent %}
144+ {% include "maasserver/cluster_listing_row.html" with cycle=cycle warning_no_images=warn_no_images %}
145+ {% endfor %}
146+ </tbody>
147+</table>
148+{% include "maasserver/pagination.html" %}
149+<div>
150+ {% if status == statuses.ACCEPTED %}
151+ <form method="POST"
152+ action="{% url 'cluster-list' %}">
153+ {% csrf_token %}
154+ <input type="hidden" name="import_all_boot_images" value="1" />
155+ <input type="submit" class="button right" value="Import boot images" />
156+ </form>
157+ {% endif %}
158+ {% if status == statuses.PENDING %}
159+ <form id="reject_all_pending_nodegroups"
160+ method="POST"
161+ action="{% url 'cluster-list' %}">
162+ {% csrf_token %}
163+ <input type="hidden" name="mass_accept_submit" value="1" />
164+ <input type="submit" class="button right" value="Accept all" />
165+ </form>
166+ <form id="accept_all_pending_nodegroups"
167+ method="POST"
168+ action="{% url 'cluster-list' %}">
169+ {% csrf_token %}
170+ <input type="hidden" name="mass_reject_submit" value="1" />
171+ <input type="submit" class="button right space-right-small"
172+ value="Reject all" />
173+ </form>
174+ {% endif %}
175+
176+</div>
177+<div class="clear"></div>
178+
179+
180+{% endblock %}
181\ No newline at end of file
182
183=== added file 'src/maasserver/templates/maasserver/cluster_listing_head.html'
184--- src/maasserver/templates/maasserver/cluster_listing_head.html 1970-01-01 00:00:00 +0000
185+++ src/maasserver/templates/maasserver/cluster_listing_head.html 2014-04-03 11:47:02 +0000
186@@ -0,0 +1,10 @@
187+<thead>
188+ <tr>
189+ <th>Name</th>
190+ <th>Managed interfaces</th>
191+ <th>Boot images</th>
192+ <th>Nodes</th>
193+ {% comment %} Action buttons {% endcomment %}
194+ <th></th>
195+ </tr>
196+</thead>
197\ No newline at end of file
198
199=== renamed file 'src/maasserver/templates/maasserver/settings_cluster_listing_row.html' => 'src/maasserver/templates/maasserver/cluster_listing_row.html'
200--- src/maasserver/templates/maasserver/settings_cluster_listing_row.html 2012-12-06 04:22:51 +0000
201+++ src/maasserver/templates/maasserver/cluster_listing_row.html 2014-04-03 11:47:02 +0000
202@@ -1,11 +1,24 @@
203-<tr class="cluster {{ cycle }}"
204+<tr class="cluster {{ cycle }} {% if warning_no_images and cluster.bootimage_set.count == 0 %}warning{% endif %}"
205 id="{{ cluster.uuid }}">
206 <td>
207- {{ cluster.cluster_name }}
208+ <a href="{% url 'cluster-edit' cluster.uuid %}">{{ cluster.cluster_name }}</a>
209+ </td>
210+ <td>
211+ {{ cluster.get_managed_interfaces|length }}
212+ </td>
213+ <td>
214+ {% if warning_no_images and cluster.bootimage_set.count == 0 %}
215+ 0 <img src="{{ STATIC_URL }}img/warning.png" title="Warning: this cluster has no boot images."/>
216+ {% else %}
217 {% if cluster.bootimage_set.count == 0 %}
218- <span class="warning">(Warning: this cluster has no boot images.)</span>
219+ 0
220+ {% else %}
221+ <a title="View boot images"
222+ href="{% url 'cluster-bootimages-list' cluster.uuid %}">{{ cluster.bootimage_set.count }}</a>
223 {% endif %}
224+ {% endif %}
225 </td>
226+ <td>{{ cluster.node_set.count }}</td>
227 <td class="icon-controls">
228 <a href="{% url 'cluster-edit' cluster.uuid %}"
229 class="icon"
230
231=== modified file 'src/maasserver/templates/maasserver/no-bootimages-warning.html'
232--- src/maasserver/templates/maasserver/no-bootimages-warning.html 2014-03-27 10:51:28 +0000
233+++ src/maasserver/templates/maasserver/no-bootimages-warning.html 2014-04-03 11:47:02 +0000
234@@ -5,5 +5,5 @@
235 <span class="console">/etc/maas/bootresources.yaml</span> on the cluster's
236 machine) suits the configuration of the machines this cluster will have to
237 handle and then run the import script by clicking the "Import boot images"
238- button on the <a href="{% url 'settings' %}#clusters">cluster's listing page</a>.
239+ button on the <a href="{% url 'cluster-list' %}">cluster listing page</a>.
240 </p>
241
242=== modified file 'src/maasserver/templates/maasserver/nodegroup_confirm_delete.html'
243--- src/maasserver/templates/maasserver/nodegroup_confirm_delete.html 2012-10-08 12:50:32 +0000
244+++ src/maasserver/templates/maasserver/nodegroup_confirm_delete.html 2014-04-03 11:47:02 +0000
245@@ -17,7 +17,7 @@
246 <input type="hidden" name="post" value="yes" />
247 <input type="submit" value="Delete cluster controller"
248 class="right" />
249- <a href="{% url 'settings' %}">Cancel</a>
250+ <a href="{% url 'cluster-list' %}">Cancel</a>
251 </form>
252 </p>
253 </div>
254
255=== modified file 'src/maasserver/templates/maasserver/nodegroup_edit.html'
256--- src/maasserver/templates/maasserver/nodegroup_edit.html 2014-03-25 10:35:14 +0000
257+++ src/maasserver/templates/maasserver/nodegroup_edit.html 2014-04-03 11:47:02 +0000
258@@ -15,7 +15,7 @@
259 </ul>
260 <input type="submit" value="Save cluster controller"
261 class="button right" />
262- <a class="link-button" href="{% url 'settings' %}">Cancel</a>
263+ <a class="link-button" href="{% url 'cluster-list' %}">Cancel</a>
264 <h2>Interfaces</h2>
265 {% with nb_interfaces=cluster.nodegroupinterface_set.count %}
266 This cluster controller has {{ nb_interfaces }}
267
268=== modified file 'src/maasserver/templates/maasserver/settings.html'
269--- src/maasserver/templates/maasserver/settings.html 2012-11-30 17:21:28 +0000
270+++ src/maasserver/templates/maasserver/settings.html 2014-04-03 11:47:02 +0000
271@@ -70,11 +70,6 @@
272 <div class="clear"></div>
273 </div>
274 <div class="divider"></div>
275- <div id="clusters" class="block size11 first">
276- {% include "maasserver/settings_cluster_listing.html" %}
277- <div class="clear"></div>
278- </div>
279- <div class="divider"></div>
280 <div id="commissioning_scripts" class="block size11 first">
281 {% include "maasserver/settings_commissioning_scripts.html" %}
282 <div class="clear"></div>
283
284=== modified file 'src/maasserver/tests/test_api.py'
285--- src/maasserver/tests/test_api.py 2014-03-19 13:54:20 +0000
286+++ src/maasserver/tests/test_api.py 2014-04-03 11:47:02 +0000
287@@ -53,6 +53,7 @@
288 from maasserver.testing.oauthclient import OAuthAuthenticatedClient
289 from maasserver.testing.testcase import MAASServerTestCase
290 from maasserver.tests.test_forms import make_interface_settings
291+from maasserver.utils import absolute_reverse
292 from maasserver.utils.orm import get_one
293 from maastesting.djangotestcase import TransactionTestCase
294 from maastesting.matchers import MockCalledOnceWith
295@@ -754,7 +755,8 @@
296 [args[0][0] for args in recorder.call_args_list])
297 # The persistent error message links to the clusters listing.
298 self.assertIn(
299- "/settings/#accepted-clusters", recorder.call_args_list[0][0][1])
300+ absolute_reverse("cluster-list"),
301+ recorder.call_args_list[0][0][1])
302
303 def test_warns_if_any_nodegroup_has_no_images(self):
304 factory.make_node_group(status=NODEGROUP_STATUS.ACCEPTED)
305
306=== modified file 'src/maasserver/urls.py'
307--- src/maasserver/urls.py 2014-03-25 10:35:14 +0000
308+++ src/maasserver/urls.py 2014-04-03 11:47:02 +0000
309@@ -21,12 +21,22 @@
310 url,
311 )
312 from django.contrib.auth.decorators import user_passes_test
313+from maasserver.enum import NODEGROUP_STATUS
314 from maasserver.models import Node
315 from maasserver.views import TextTemplateView
316 from maasserver.views.account import (
317 login,
318 logout,
319 )
320+from maasserver.views.clusters import (
321+ BootImagesListView,
322+ ClusterDelete,
323+ ClusterEdit,
324+ ClusterInterfaceCreate,
325+ ClusterInterfaceDelete,
326+ ClusterInterfaceEdit,
327+ ClusterListView,
328+ )
329 from maasserver.views.networks import (
330 NetworkAdd,
331 NetworkDelete,
332@@ -60,14 +70,6 @@
333 AccountsView,
334 settings,
335 )
336-from maasserver.views.settings_clusters import (
337- BootImagesListView,
338- ClusterDelete,
339- ClusterEdit,
340- ClusterInterfaceCreate,
341- ClusterInterfaceDelete,
342- ClusterInterfaceEdit,
343- )
344 from maasserver.views.settings_commissioning_scripts import (
345 CommissioningScriptCreate,
346 CommissioningScriptDelete,
347@@ -116,7 +118,6 @@
348 r'^account/prefs/sshkey/delete/(?P<keyid>\d*)/$',
349 SSHKeyDeleteView.as_view(), name='prefs-delete-sshkey'),
350 )
351-
352 # Logout view.
353 urlpatterns += patterns(
354 'maasserver.views',
355@@ -159,6 +160,18 @@
356 urlpatterns += patterns(
357 'maasserver.views',
358 adminurl(
359+ r'^clusters/$',
360+ ClusterListView.as_view(status=NODEGROUP_STATUS.ACCEPTED),
361+ name='cluster-list'),
362+ adminurl(
363+ r'^clusters/pending/$',
364+ ClusterListView.as_view(status=NODEGROUP_STATUS.PENDING),
365+ name='cluster-list-pending'),
366+ adminurl(
367+ r'^clusters/rejected/$',
368+ ClusterListView.as_view(status=NODEGROUP_STATUS.REJECTED),
369+ name='cluster-list-rejected'),
370+ adminurl(
371 r'^clusters/(?P<uuid>[\w\-]+)/edit/$', ClusterEdit.as_view(),
372 name='cluster-edit'),
373 adminurl(
374
375=== renamed file 'src/maasserver/views/settings_clusters.py' => 'src/maasserver/views/clusters.py'
376--- src/maasserver/views/settings_clusters.py 2014-03-25 10:35:14 +0000
377+++ src/maasserver/views/clusters.py 2014-04-03 11:47:02 +0000
378@@ -1,7 +1,7 @@
379 # Copyright 2012 Canonical Ltd. This software is licensed under the
380 # GNU Affero General Public License version 3 (see the file LICENSE).
381
382-"""Cluster Settings views."""
383+"""Cluster views."""
384
385 from __future__ import (
386 absolute_import,
387@@ -19,17 +19,29 @@
388 "ClusterInterfaceCreate",
389 "ClusterInterfaceDelete",
390 "ClusterInterfaceEdit",
391+ "ClusterListView",
392 ]
393
394+from collections import OrderedDict
395+
396 from django.contrib import messages
397 from django.core.urlresolvers import reverse
398 from django.http import HttpResponseRedirect
399 from django.shortcuts import get_object_or_404
400+from django.utils.safestring import mark_safe
401 from django.views.generic import (
402 CreateView,
403 DeleteView,
404 UpdateView,
405 )
406+from django.views.generic.edit import (
407+ FormMixin,
408+ ProcessFormView,
409+ )
410+from maasserver.enum import (
411+ NODEGROUP_STATUS,
412+ NODEGROUP_STATUS_CHOICES,
413+ )
414 from maasserver.forms import (
415 NodeGroupEdit,
416 NodeGroupInterfaceForm,
417@@ -41,6 +53,99 @@
418 from maasserver.views import PaginatedListView
419
420
421+class ClusterListView(PaginatedListView, FormMixin, ProcessFormView):
422+ template_name = 'maasserver/cluster_listing.html'
423+ context_object_name = "cluster_list"
424+ status = None
425+
426+ def get_queryset(self):
427+ return NodeGroup.objects.filter(
428+ status=self.status).order_by('cluster_name')
429+
430+ # A record of the urls used to reach the clusters of different
431+ # statuses.
432+ status_links = OrderedDict((
433+ (NODEGROUP_STATUS.ACCEPTED, 'cluster-list'),
434+ (NODEGROUP_STATUS.PENDING, 'cluster-list-pending'),
435+ (NODEGROUP_STATUS.REJECTED, 'cluster-list-rejected'),
436+ ))
437+
438+ def make_title_entry(self, status, link_name):
439+ """Generate an entry as used by make_cluster_listing_title().
440+
441+ This is a utility method only used by make_cluster_listing_title.
442+ It is a separate method for clarity and to help testing."""
443+ link = reverse(link_name)
444+ status_name = NODEGROUP_STATUS_CHOICES[status][1]
445+ nb_clusters = NodeGroup.objects.filter(
446+ status=status).count()
447+ entry = "%d %s cluster%s" % (
448+ nb_clusters,
449+ status_name.lower(),
450+ 's' if nb_clusters != 1 else '')
451+ if nb_clusters != 0 and status != self.status:
452+ entry = '<a href="%s">%s</a>' % (link, entry)
453+ return entry
454+
455+ def make_cluster_listing_title(self):
456+ """Generate this view's title with "tabs" for each cluster status.
457+
458+ Generate a title for this view with the number of clusters for each
459+ possible status. The title includes the links to the other listings
460+ (i.e. if this is the listing for the accepted clusters, include links
461+ to the listings of the pending/rejected clusters). The title will be
462+ of the form: "3 accepted clusters / 1 pending cluster / 2 rejected
463+ clusters". (This is simpler to do in the view rather than write this
464+ using the template language.)
465+ """
466+ return mark_safe(
467+ ' / '.join(
468+ self.make_title_entry(status, link_name)
469+ for status, link_name in self.status_links.items()))
470+
471+ def get_context_data(self, **kwargs):
472+ context = super(ClusterListView, self).get_context_data(**kwargs)
473+ context['current_count'] = NodeGroup.objects.filter(
474+ status=self.status).count()
475+ context['title'] = self.make_cluster_listing_title()
476+ # Display warnings for clusters that have no images, but only for the
477+ # display of 'accepted' clusters.
478+ context['warn_no_images'] = self.status == NODEGROUP_STATUS.ACCEPTED
479+ context['status'] = self.status
480+ context['statuses'] = NODEGROUP_STATUS
481+ context['status_name'] = NODEGROUP_STATUS_CHOICES[self.status][1]
482+ return context
483+
484+ def post(self, request, *args, **kwargs):
485+ """Handle a POST request."""
486+ if 'mass_accept_submit' in request.POST:
487+ # Process accept clusters en masse.
488+ number = NodeGroup.objects.accept_all_pending()
489+ messages.info(request, "Accepted %d cluster(s)." % number)
490+ return HttpResponseRedirect(reverse('cluster-list'))
491+
492+ elif 'mass_reject_submit' in request.POST:
493+ # Process reject clusters en masse.
494+ number = NodeGroup.objects.reject_all_pending()
495+ messages.info(request, "Rejected %d cluster(s)." % number)
496+ return HttpResponseRedirect(reverse('cluster-list'))
497+
498+ elif 'import_all_boot_images' in request.POST:
499+ # Import PXE files for all the accepted clusters.
500+ NodeGroup.objects.import_boot_images_accepted_clusters()
501+ message = (
502+ "Import of boot images started on all cluster controllers. "
503+ "Importing the boot images can take a long time depending on "
504+ "the available bandwidth.")
505+ messages.info(request, message)
506+ return HttpResponseRedirect(reverse('cluster-list'))
507+
508+ else:
509+ # Unknown action: redirect to the cluster listing page (this
510+ # shouldn't happen).
511+ return HttpResponseRedirect(reverse('cluster-list'))
512+
513+
514 class ClusterEdit(UpdateView):
515 model = NodeGroup
516 template_name = 'maasserver/nodegroup_edit.html'
517@@ -54,7 +159,7 @@
518 return context
519
520 def get_success_url(self):
521- return reverse('settings')
522+ return reverse('cluster-list')
523
524 def get_object(self):
525 uuid = self.kwargs.get('uuid', None)
526@@ -75,7 +180,7 @@
527 return get_object_or_404(NodeGroup, uuid=uuid)
528
529 def get_next_url(self):
530- return reverse('settings')
531+ return reverse('cluster-list')
532
533 def delete(self, request, *args, **kwargs):
534 cluster = self.get_object()
535
536=== modified file 'src/maasserver/views/settings.py'
537--- src/maasserver/views/settings.py 2013-10-07 09:12:40 +0000
538+++ src/maasserver/views/settings.py 2014-04-03 11:47:02 +0000
539@@ -38,7 +38,6 @@
540 from django.views.generic.base import TemplateView
541 from django.views.generic.detail import SingleObjectTemplateResponseMixin
542 from django.views.generic.edit import ModelFormMixin
543-from maasserver.enum import NODEGROUP_STATUS
544 from maasserver.exceptions import CannotDeleteUserException
545 from maasserver.forms import (
546 CommissioningForm,
547@@ -48,10 +47,7 @@
548 NewUserCreationForm,
549 UbuntuForm,
550 )
551-from maasserver.models import (
552- NodeGroup,
553- UserProfile,
554- )
555+from maasserver.models import UserProfile
556 from maasserver.views import process_form
557 from metadataserver.models import CommissioningScript
558
559@@ -186,36 +182,6 @@
560 if response is not None:
561 return response
562
563- # Process accept clusters en masse.
564- if 'mass_accept_submit' in request.POST:
565- number = NodeGroup.objects.accept_all_pending()
566- messages.info(request, "Accepted %d cluster(s)." % number)
567- return HttpResponseRedirect(reverse('settings'))
568-
569- # Process reject clusters en masse.
570- if 'mass_reject_submit' in request.POST:
571- number = NodeGroup.objects.reject_all_pending()
572- messages.info(request, "Rejected %d cluster(s)." % number)
573- return HttpResponseRedirect(reverse('settings'))
574-
575- # Import PXE files for all the accepted clusters.
576- if 'import_all_boot_images' in request.POST:
577- NodeGroup.objects.import_boot_images_accepted_clusters()
578- message = (
579- "Import of boot images started on all cluster controllers. "
580- "Importing the boot images can take a long time depending on "
581- "the available bandwidth.")
582- messages.info(request, message)
583- return HttpResponseRedirect(reverse('settings'))
584-
585- # Cluster listings.
586- accepted_clusters = NodeGroup.objects.filter(
587- status=NODEGROUP_STATUS.ACCEPTED).order_by('cluster_name')
588- pending_clusters = NodeGroup.objects.filter(
589- status=NODEGROUP_STATUS.PENDING).order_by('cluster_name')
590- rejected_clusters = NodeGroup.objects.filter(
591- status=NODEGROUP_STATUS.REJECTED).order_by('cluster_name')
592-
593 # Commissioning scripts.
594 commissioning_scripts = CommissioningScript.objects.all()
595
596@@ -224,9 +190,6 @@
597 {
598 'user_list': user_list,
599 'commissioning_scripts': commissioning_scripts,
600- 'accepted_clusters': accepted_clusters,
601- 'pending_clusters': pending_clusters,
602- 'rejected_clusters': rejected_clusters,
603 'maas_and_network_form': maas_and_network_form,
604 'commissioning_form': commissioning_form,
605 'ubuntu_form': ubuntu_form,
606
607=== modified file 'src/maasserver/views/tests/test_boot_image_list.py'
608--- src/maasserver/views/tests/test_boot_image_list.py 2014-03-27 07:39:38 +0000
609+++ src/maasserver/views/tests/test_boot_image_list.py 2014-04-03 11:47:02 +0000
610@@ -21,7 +21,7 @@
611 from lxml.html import fromstring
612 from maasserver.testing.factory import factory
613 from maasserver.testing.testcase import MAASServerTestCase
614-from maasserver.views.settings_clusters import BootImagesListView
615+from maasserver.views.clusters import BootImagesListView
616 from testtools.matchers import ContainsAll
617
618
619
620=== renamed file 'src/maasserver/views/tests/test_settings_clusters.py' => 'src/maasserver/views/tests/test_clusters.py'
621--- src/maasserver/views/tests/test_settings_clusters.py 2014-03-25 12:53:32 +0000
622+++ src/maasserver/views/tests/test_clusters.py 2014-04-03 11:47:02 +0000
623@@ -20,10 +20,12 @@
624 from lxml.html import fromstring
625 from maasserver.enum import (
626 NODEGROUP_STATUS,
627+ NODEGROUP_STATUS_CHOICES,
628 NODEGROUPINTERFACE_MANAGEMENT,
629 )
630 from maasserver.models import (
631 NodeGroup,
632+ nodegroup as nodegroup_module,
633 NodeGroupInterface,
634 )
635 from maasserver.testing import (
636@@ -33,25 +35,42 @@
637 )
638 from maasserver.testing.factory import factory
639 from maasserver.testing.testcase import MAASServerTestCase
640+from maasserver.utils import map_enum
641+from maasserver.views.clusters import ClusterListView
642+from mock import (
643+ ANY,
644+ call,
645+ )
646 from testtools.matchers import (
647 AllMatch,
648 Contains,
649 ContainsAll,
650 Equals,
651+ HasLength,
652 MatchesStructure,
653 )
654
655
656 class ClusterListingTest(MAASServerTestCase):
657
658- def test_settings_contains_links_to_edit_and_delete_clusters(self):
659+ scenarios = [
660+ ('accepted-clusters', {'status': NODEGROUP_STATUS.ACCEPTED}),
661+ ('pending-clusters', {'status': NODEGROUP_STATUS.PENDING}),
662+ ('rejected-clusters', {'status': NODEGROUP_STATUS.REJECTED}),
663+ ]
664+
665+ def get_url(self):
666+ """Return the listing url used in this scenario."""
667+ return reverse(ClusterListView.status_links[
668+ self.status])
669+
670+ def test_cluster_listing_contains_links_to_manipulate_clusters(self):
671 self.client_log_in(as_admin=True)
672 nodegroups = {
673- factory.make_node_group(status=NODEGROUP_STATUS.ACCEPTED),
674- factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
675- factory.make_node_group(status=NODEGROUP_STATUS.REJECTED),
676+ factory.make_node_group(status=self.status)
677+ for _ in range(3)
678 }
679- links = get_content_links(self.client.get(reverse('settings')))
680+ links = get_content_links(self.client.get(self.get_url()))
681 nodegroup_edit_links = [
682 reverse('cluster-edit', args=[nodegroup.uuid])
683 for nodegroup in nodegroups]
684@@ -62,6 +81,166 @@
685 links,
686 ContainsAll(nodegroup_edit_links + nodegroup_delete_links))
687
688+ def make_listing_view(self, status):
689+ view = ClusterListView()
690+ view.status = status
691+ return view
692+
693+ def test_make_title_entry_returns_link_for_other_status(self):
694+ # If the entry's status is different from the view's status,
695+ # the returned entry is a link.
696+ other_status = factory.getRandomChoice(
697+ NODEGROUP_STATUS_CHOICES, but_not=[self.status])
698+ factory.make_node_group(status=other_status)
699+ link_name = ClusterListView.status_links[other_status]
700+ view = self.make_listing_view(self.status)
701+ entry = view.make_title_entry(other_status, link_name)
702+ status_name = NODEGROUP_STATUS_CHOICES[other_status][1]
703+ self.assertEqual(
704+ '<a href="%s">1 %s cluster</a>' % (
705+ reverse(link_name), status_name.lower()),
706+ entry)
707+
708+ def test_make_title_entry_returns_title_if_no_cluster(self):
709+ # If no cluster correspond to the entry's status, the returned
710+ # entry is not a link: it's a simple mention '0 <status> clusters'.
711+ other_status = factory.getRandomChoice(
712+ NODEGROUP_STATUS_CHOICES, but_not=[self.status])
713+ link_name = ClusterListView.status_links[other_status]
714+ view = self.make_listing_view(self.status)
715+ entry = view.make_title_entry(other_status, link_name)
716+ status_name = NODEGROUP_STATUS_CHOICES[other_status][1]
717+ self.assertEqual(
718+ '0 %s clusters' % status_name.lower(), entry)
719+
720+ def test_title_displays_number_of_clusters(self):
721+ for _ in range(3):
722+ factory.make_node_group(status=self.status)
723+ view = self.make_listing_view(self.status)
724+ status_name = NODEGROUP_STATUS_CHOICES[self.status][1]
725+ title = view.make_cluster_listing_title()
726+ self.assertIn("3 %s clusters" % status_name.lower(), title)
727+
728+ def test_title_contains_links_to_other_listings(self):
729+ view = self.make_listing_view(self.status)
730+ other_statuses = []
731+ # Compute a list with the statuses of the clusters not being
732+ # displayed by the 'view'. Create clusters with these statuses.
733+ for status in map_enum(NODEGROUP_STATUS).values():
734+ if status != self.status:
735+ other_statuses.append(status)
736+ factory.make_node_group(status=status)
737+ for status in other_statuses:
738+ link_name = ClusterListView.status_links[status]
739+ title = view.make_cluster_listing_title()
740+ self.assertIn(reverse(link_name), title)
741+
742+ def test_listing_is_paginated(self):
743+ self.patch(ClusterListView, "paginate_by", 2)
744+ self.client_log_in(as_admin=True)
745+ for _ in range(3):
746+ factory.make_node_group(status=self.status)
747+ response = self.client.get(self.get_url())
748+ self.assertEqual(httplib.OK, response.status_code)
749+ doc = fromstring(response.content)
750+ self.assertThat(
751+ doc.cssselect('div.pagination'),
752+ HasLength(1),
753+ "Couldn't find pagination tag.")
754+
755+
756+class ClusterListingAccess(MAASServerTestCase):
757+
758+ def test_admin_sees_cluster_tab(self):
759+ self.client_log_in(as_admin=True)
760+ links = get_content_links(
761+ self.client.get(reverse('index')), element='#main-nav')
762+ self.assertIn(reverse('cluster-list'), links)
763+
764+ def test_non_admin_doesnt_see_cluster_tab(self):
765+ self.client_log_in(as_admin=False)
766+ links = get_content_links(
767+ self.client.get(reverse('index')), element='#main-nav')
768+ self.assertNotIn(reverse('cluster-list'), links)
769+
770+
771+class ClusterPendingListingTest(MAASServerTestCase):
772+
773+ def test_pending_listing_contains_form_to_accept_all_nodegroups(self):
774+ self.client_log_in(as_admin=True)
775+ factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
776+ response = self.client.get(reverse('cluster-list-pending'))
777+ doc = fromstring(response.content)
778+ forms = doc.cssselect('form#accept_all_pending_nodegroups')
779+ self.assertEqual(1, len(forms))
780+
781+ def test_pending_listing_contains_form_to_reject_all_nodegroups(self):
782+ self.client_log_in(as_admin=True)
783+ factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
784+ response = self.client.get(reverse('cluster-list-pending'))
785+ doc = fromstring(response.content)
786+ forms = doc.cssselect('form#reject_all_pending_nodegroups')
787+ self.assertEqual(1, len(forms))
788+
789+ def test_pending_listing_accepts_all_pending_nodegroups_POST(self):
790+ self.client_log_in(as_admin=True)
791+ nodegroups = {
792+ factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
793+ factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
794+ }
795+ response = self.client.post(
796+ reverse('cluster-list-pending'), {'mass_accept_submit': 1})
797+ self.assertEqual(httplib.FOUND, response.status_code)
798+ self.assertEqual(
799+ [reload_object(nodegroup).status for nodegroup in nodegroups],
800+ [NODEGROUP_STATUS.ACCEPTED] * 2)
801+
802+ def test_pending_listing_rejects_all_pending_nodegroups_POST(self):
803+ self.client_log_in(as_admin=True)
804+ nodegroups = {
805+ factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
806+ factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
807+ }
808+ response = self.client.post(
809+ reverse('cluster-list-pending'), {'mass_reject_submit': 1})
810+ self.assertEqual(httplib.FOUND, response.status_code)
811+ self.assertEqual(
812+ [reload_object(nodegroup).status for nodegroup in nodegroups],
813+ [NODEGROUP_STATUS.REJECTED] * 2)
814+
815+
816+class ClusterAcceptedListingTest(MAASServerTestCase):
817+
818+ def test_accepted_listing_import_boot_images_calls_tasks(self):
819+ self.client_log_in(as_admin=True)
820+ recorder = self.patch(nodegroup_module, 'import_boot_images')
821+ accepted_nodegroups = [
822+ factory.make_node_group(status=NODEGROUP_STATUS.ACCEPTED),
823+ factory.make_node_group(status=NODEGROUP_STATUS.ACCEPTED),
824+ ]
825+ response = self.client.post(
826+ reverse('cluster-list'), {'import_all_boot_images': 1})
827+ self.assertEqual(httplib.FOUND, response.status_code)
828+ calls = [
829+ call(queue=nodegroup.work_queue, kwargs=ANY)
830+ for nodegroup in accepted_nodegroups
831+ ]
832+ self.assertItemsEqual(calls, recorder.apply_async.call_args_list)
833+
834+ def test_a_warning_is_displayed_if_the_cluster_has_no_boot_images(self):
835+ self.client_log_in(as_admin=True)
836+ nodegroup = factory.make_node_group(
837+ status=NODEGROUP_STATUS.ACCEPTED)
838+ response = self.client.get(reverse('cluster-list'))
839+ document = fromstring(response.content)
840+ nodegroup_row = document.xpath("//tr[@id='%s']" % nodegroup.uuid)[0]
841+ self.assertIn('warning', nodegroup_row.get('class'))
842+ warning_elems = (
843+ nodegroup_row.xpath(
844+ "//img[@title='Warning: this cluster has no boot images.']"))
845+ self.assertEqual(
846+ 1, len(warning_elems), "No warning about missing boot images.")
847+
848
849 class ClusterDeleteTest(MAASServerTestCase):
850
851@@ -71,7 +250,7 @@
852 delete_link = reverse('cluster-delete', args=[nodegroup.uuid])
853 response = self.client.post(delete_link, {'post': 'yes'})
854 self.assertEqual(
855- (httplib.FOUND, reverse('settings')),
856+ (httplib.FOUND, reverse('cluster-list')),
857 (response.status_code, extract_redirect(response)))
858 self.assertFalse(
859 NodeGroup.objects.filter(uuid=nodegroup.uuid).exists())
860
861=== modified file 'src/maasserver/views/tests/test_settings.py'
862--- src/maasserver/views/tests/test_settings.py 2014-04-01 06:50:01 +0000
863+++ src/maasserver/views/tests/test_settings.py 2014-04-03 11:47:02 +0000
864@@ -23,11 +23,9 @@
865 from maasserver.enum import (
866 COMMISSIONING_DISTRO_SERIES_CHOICES,
867 DISTRO_SERIES,
868- NODEGROUP_STATUS,
869 )
870 from maasserver.models import (
871 Config,
872- nodegroup as nodegroup_module,
873 UserProfile,
874 )
875 from maasserver.testing import (
876@@ -37,10 +35,6 @@
877 )
878 from maasserver.testing.factory import factory
879 from maasserver.testing.testcase import MAASServerTestCase
880-from mock import (
881- ANY,
882- call,
883- )
884
885
886 class SettingsTest(MAASServerTestCase):
887@@ -185,73 +179,6 @@
888 new_kernel_opts,
889 Config.objects.get_config('kernel_opts'))
890
891- def test_settings_contains_form_to_accept_all_nodegroups(self):
892- self.client_log_in(as_admin=True)
893- factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
894- response = self.client.get(reverse('settings'))
895- doc = fromstring(response.content)
896- forms = doc.cssselect('form#accept_all_pending_nodegroups')
897- self.assertEqual(1, len(forms))
898-
899- def test_settings_contains_form_to_reject_all_nodegroups(self):
900- self.client_log_in(as_admin=True)
901- factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
902- response = self.client.get(reverse('settings'))
903- doc = fromstring(response.content)
904- forms = doc.cssselect('form#reject_all_pending_nodegroups')
905- self.assertEqual(1, len(forms))
906-
907- def test_settings_accepts_all_pending_nodegroups_POST(self):
908- self.client_log_in(as_admin=True)
909- nodegroups = {
910- factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
911- factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
912- }
913- response = self.client.post(
914- reverse('settings'), {'mass_accept_submit': 1})
915- self.assertEqual(httplib.FOUND, response.status_code)
916- self.assertEqual(
917- [reload_object(nodegroup).status for nodegroup in nodegroups],
918- [NODEGROUP_STATUS.ACCEPTED] * 2)
919-
920- def test_settings_rejects_all_pending_nodegroups_POST(self):
921- self.client_log_in(as_admin=True)
922- nodegroups = {
923- factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
924- factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
925- }
926- response = self.client.post(
927- reverse('settings'), {'mass_reject_submit': 1})
928- self.assertEqual(httplib.FOUND, response.status_code)
929- self.assertEqual(
930- [reload_object(nodegroup).status for nodegroup in nodegroups],
931- [NODEGROUP_STATUS.REJECTED] * 2)
932-
933- def test_settings_import_boot_images_calls_tasks(self):
934- self.client_log_in(as_admin=True)
935- recorder = self.patch(nodegroup_module, 'import_boot_images')
936- accepted_nodegroups = [
937- factory.make_node_group(status=NODEGROUP_STATUS.ACCEPTED),
938- factory.make_node_group(status=NODEGROUP_STATUS.ACCEPTED),
939- ]
940- response = self.client.post(
941- reverse('settings'), {'import_all_boot_images': 1})
942- self.assertEqual(httplib.FOUND, response.status_code)
943- calls = [
944- call(queue=nodegroup.work_queue, kwargs=ANY)
945- for nodegroup in accepted_nodegroups
946- ]
947- self.assertItemsEqual(calls, recorder.apply_async.call_args_list)
948-
949- def test_cluster_no_boot_images_message_displayed_if_no_boot_images(self):
950- self.client_log_in(as_admin=True)
951- nodegroup = factory.make_node_group(
952- status=NODEGROUP_STATUS.ACCEPTED)
953- response = self.client.get(reverse('settings'))
954- document = fromstring(response.content)
955- nodegroup_row = document.xpath("//tr[@id='%s']" % nodegroup.uuid)[0]
956- self.assertIn('no boot images', nodegroup_row.text_content())
957-
958
959 class NonAdminSettingsTest(MAASServerTestCase):
960

Subscribers

People subscribed via source and target branches

to all changes: