Merge lp:~rvb/maas/cluster-connected into lp:~maas-committers/maas/trunk

Proposed by Raphaël Badin
Status: Merged
Approved by: Raphaël Badin
Approved revision: no longer in the source branch.
Merged at revision: 2958
Proposed branch: lp:~rvb/maas/cluster-connected
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 220 lines (+110/-8)
7 files modified
src/maasserver/models/nodegroup.py (+10/-0)
src/maasserver/models/tests/test_nodegroup.py (+19/-0)
src/maasserver/templates/maasserver/cluster_listing.html (+1/-1)
src/maasserver/templates/maasserver/cluster_listing_head.html (+1/-0)
src/maasserver/templates/maasserver/cluster_listing_row.html (+17/-2)
src/maasserver/views/clusters.py (+3/-3)
src/maasserver/views/tests/test_clusters.py (+59/-2)
To merge this branch: bzr merge lp:~rvb/maas/cluster-connected
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+234281@code.launchpad.net

Commit message

Display if clusters are connected on the cluster listing page.

Description of the change

Here is what it looks like:
connected: http://people.canonical.com/~rvb/connected_cluster.png
disconnected: http://people.canonical.com/~rvb/disconnected_cluster.png

The plan is to integrate this with a proper notification system when it's implemented but this needs some serious UI work. This branch is a first step: backend work + basic display in the UI.

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

I guess there are no HTML entity codes for the special characters you wanted... shame.

The naming trick in test_listing_displays_connection_status is a bit hairy. I'd explain that right up front so that the reader understands how the code fits together.

Also in that test, instead of “self.name[-1] == '0'” I'd say “self.name.endswith('0')”.

And finally, test_warning_is_displayed_if_a_cluster_is_not_connected could use the HasLength matcher.

review: Approve
Revision history for this message
Raphaël Badin (rvb) wrote :

Thanks for the review!

> I guess there are no HTML entity codes for the special characters you
> wanted... shame.

Well, yes: ✓ and ❌. I'm even using them here. Am I missing something?

>
> The naming trick in test_listing_displays_connection_status is a bit hairy.
> I'd explain that right up front so that the reader understands how the code
> fits together.

Done. I thought about alternative solutions like using the UUID of the nodegroup but it's not trivial mainly because the UUIDs are not really random.

> Also in that test, instead of “self.name[-1] == '0'” I'd say
> “self.name.endswith('0')”.

Done.

> And finally, test_warning_is_displayed_if_a_cluster_is_not_connected could use
> the HasLength matcher.

Done.

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (77.3 KiB)

The attempt to merge lp:~rvb/maas/cluster-connected into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com trusty-security InRelease
Hit http://security.ubuntu.com trusty-security Release.gpg
Hit http://security.ubuntu.com trusty-security Release
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Hit http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Hit http://nova.clouds.archive.ubuntu.com trusty-updates Release
Hit http://security.ubuntu.com trusty-security/main Sources
Hit http://security.ubuntu.com trusty-security/universe Sources
Hit http://security.ubuntu.com trusty-security/main amd64 Packages
Hit http://security.ubuntu.com trusty-security/universe amd64 Packages
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 authbind bind9 bind9utils build-essential bzr-builddeb curl daemontools debhelper dh-apport distro-info dnsutils firefox freeipmi-tools ipython isc-dhcp-common libjs-raphael libjs-yui3-full libjs-yui3-min libpq-dev make pep8 postgresql pyflakes python-amqplib python-bzrlib python-celery python-convoy python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-extras python-fixtures python-flake8 python-formencode python-hivex python-httplib2 python-jinja2 python-jsonschema python-lockfile python-lxml python-mimeparse python-mock python-netaddr python-netifaces python-nose python-oauth python-oops python-oops-amqp python-oops-datedir-repo python-oops-twisted python-oops-wsgi python-openssl python-paramiko python-pexpect python-pip python-pocket-lint python-psycopg2 python-pyinotify python-seamicroclient python-simplejson python-simplestreams python-sphinx python-su...

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

I was thinking of ✓ and ✗ for the entity codes. Those do actually seem to be in the HTML5 spec: http://dev.w3.org/html5/html-author/charref

Revision history for this message
Raphaël Badin (rvb) wrote :

> I was thinking of ✓ and ✗ for the entity codes. Those do actually
> seem to be in the HTML5 spec: http://dev.w3.org/html5/html-author/charref

Right, done.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/models/nodegroup.py'
2--- src/maasserver/models/nodegroup.py 2014-09-11 10:08:50 +0000
3+++ src/maasserver/models/nodegroup.py 2014-09-11 12:52:50 +0000
4@@ -229,6 +229,16 @@
5 # Persist the dhcp_key without triggering the signals.
6 NodeGroup.objects.filter(id=self.id).update(dhcp_key=dhcp_key)
7
8+ def is_connected(self):
9+ """Is this cluster connected to his provisioning server?"""
10+ try:
11+ # Use a timeout of zero not to block.
12+ getClientFor(self.uuid, timeout=0)
13+ except NoConnectionsAvailable:
14+ return False
15+ else:
16+ return True
17+
18 @property
19 def work_queue(self):
20 """The name of the queue for tasks specific to this nodegroup."""
21
22=== modified file 'src/maasserver/models/tests/test_nodegroup.py'
23--- src/maasserver/models/tests/test_nodegroup.py 2014-09-11 10:08:50 +0000
24+++ src/maasserver/models/tests/test_nodegroup.py 2014-09-11 12:52:50 +0000
25@@ -44,6 +44,7 @@
26 ANY,
27 call,
28 Mock,
29+ sentinel,
30 )
31 from provisioningserver.dhcp.omshell import generate_omapi_key
32 from provisioningserver.rpc.cluster import (
33@@ -420,6 +421,24 @@
34 protocol.ImportBootImages,
35 MockCalledOnceWith(ANY, sources=[get_simplestream_endpoint()]))
36
37+ def test_is_connected_returns_true_if_connection(self):
38+ mock_getClientFor = self.patch(nodegroup_module, 'getClientFor')
39+ mock_getClientFor.return_value = sentinel
40+ nodegroup = factory.make_NodeGroup()
41+ connected = nodegroup.is_connected()
42+ self.assertThat(
43+ mock_getClientFor, MockCalledOnceWith(nodegroup.uuid, timeout=0))
44+ self.assertTrue(connected)
45+
46+ def test_is_connected_returns_true_if_no_connection(self):
47+ mock_getClientFor = self.patch(nodegroup_module, 'getClientFor')
48+ mock_getClientFor.side_effect = NoConnectionsAvailable
49+ nodegroup = factory.make_NodeGroup()
50+ connected = nodegroup.is_connected()
51+ self.assertThat(
52+ mock_getClientFor, MockCalledOnceWith(nodegroup.uuid, timeout=0))
53+ self.assertFalse(connected)
54+
55 def test_add_virsh_end_to_end(self):
56 nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ACCEPTED)
57
58
59=== modified file 'src/maasserver/templates/maasserver/cluster_listing.html'
60--- src/maasserver/templates/maasserver/cluster_listing.html 2014-09-04 10:32:51 +0000
61+++ src/maasserver/templates/maasserver/cluster_listing.html 2014-09-11 12:52:50 +0000
62@@ -14,7 +14,7 @@
63 <tbody>
64 {% for cluster in cluster_list %}
65 {% cycle 'even' 'odd' as cycle silent %}
66- {% include "maasserver/cluster_listing_row.html" with cycle=cycle warning_no_images=warn_no_images %}
67+ {% include "maasserver/cluster_listing_row.html" with cycle=cycle display_warnings=display_warnings %}
68 {% endfor %}
69 </tbody>
70 </table>
71
72=== modified file 'src/maasserver/templates/maasserver/cluster_listing_head.html'
73--- src/maasserver/templates/maasserver/cluster_listing_head.html 2014-04-02 13:53:19 +0000
74+++ src/maasserver/templates/maasserver/cluster_listing_head.html 2014-09-11 12:52:50 +0000
75@@ -1,6 +1,7 @@
76 <thead>
77 <tr>
78 <th>Name</th>
79+ <th><span title="Is the region connected to this cluster?">Connected</span></th>
80 <th>Managed interfaces</th>
81 <th>Boot images</th>
82 <th>Nodes</th>
83
84=== modified file 'src/maasserver/templates/maasserver/cluster_listing_row.html'
85--- src/maasserver/templates/maasserver/cluster_listing_row.html 2014-04-02 13:53:19 +0000
86+++ src/maasserver/templates/maasserver/cluster_listing_row.html 2014-09-11 12:52:50 +0000
87@@ -1,13 +1,28 @@
88-<tr class="cluster {{ cycle }} {% if warning_no_images and cluster.bootimage_set.count == 0 %}warning{% endif %}"
89+ <tr class="cluster {{ cycle }}
90+ {% if display_warnings %}
91+ {% if cluster.bootimage_set.count == 0 or not cluster.is_connected %}
92+ warning
93+ {% endif %}
94+ {% endif %}"
95 id="{{ cluster.uuid }}">
96 <td>
97 <a href="{% url 'cluster-edit' cluster.uuid %}">{{ cluster.cluster_name }}</a>
98 </td>
99+ <td id="{{ cluster.uuid }}_connection">
100+ {% if cluster.is_connected %}
101+ &check;
102+ {% else %}
103+ &cross;
104+ {% if display_warnings%}
105+ <img src="{{ STATIC_URL }}img/warning.png" title="Warning: this cluster isn't connected."/>
106+ {% endif %}
107+ {% endif %}
108+ </td>
109 <td>
110 {{ cluster.get_managed_interfaces|length }}
111 </td>
112 <td>
113- {% if warning_no_images and cluster.bootimage_set.count == 0 %}
114+ {% if display_warnings and cluster.bootimage_set.count == 0 %}
115 0 <img src="{{ STATIC_URL }}img/warning.png" title="Warning: this cluster has no boot images."/>
116 {% else %}
117 {% if cluster.bootimage_set.count == 0 %}
118
119=== modified file 'src/maasserver/views/clusters.py'
120--- src/maasserver/views/clusters.py 2014-08-26 18:01:08 +0000
121+++ src/maasserver/views/clusters.py 2014-09-11 12:52:50 +0000
122@@ -109,9 +109,9 @@
123 context['current_count'] = NodeGroup.objects.filter(
124 status=self.status).count()
125 context['title'] = self.make_cluster_listing_title()
126- # Display warnings for clusters that have no images, but only for the
127- # display of 'accepted' clusters.
128- context['warn_no_images'] = self.status == NODEGROUP_STATUS.ACCEPTED
129+ # Display warnings (no images, cluster not connected) for clusters,
130+ # but only for the display of 'accepted' clusters.
131+ context['display_warnings'] = self.status == NODEGROUP_STATUS.ACCEPTED
132 context['status'] = self.status
133 context['statuses'] = NODEGROUP_STATUS
134 context['status_name'] = NODEGROUP_STATUS_CHOICES[self.status][1]
135
136=== modified file 'src/maasserver/views/tests/test_clusters.py'
137--- src/maasserver/views/tests/test_clusters.py 2014-09-05 16:38:32 +0000
138+++ src/maasserver/views/tests/test_clusters.py 2014-09-11 12:52:50 +0000
139@@ -15,6 +15,7 @@
140 __all__ = []
141
142 import httplib
143+import random
144
145 from django.core.urlresolvers import reverse
146 from lxml.html import fromstring
147@@ -143,6 +144,41 @@
148 HasLength(1),
149 "Couldn't find pagination tag.")
150
151+ def test_listing_displays_connection_status(self):
152+ # We're using a little trick here: we create nodegroups
153+ # whose name end with either '0' or '1' and we then
154+ # patch nodegroup.is_connected so that it returns
155+ # True or False based of the name of the nodegroup.
156+ self.client_log_in(as_admin=True)
157+ nodegroup_connections = {}
158+ connection_representation = {
159+ '&check;': True,
160+ '&cross;': False,
161+ }
162+
163+ def mock_is_connected(self):
164+ # Return a boolean based on the last character in the
165+ # nodegroup's name.
166+ return True if self.name.endswith('0') else False
167+ self.patch(NodeGroup, 'is_connected', mock_is_connected)
168+
169+ for _ in range(3):
170+ # Create a name with a random value of '0' or '1' at the end.
171+ name = "nodegroup-%s-%s" % (
172+ factory.make_name(), random.choice(['0', '1']))
173+ nodegroup = factory.make_NodeGroup(
174+ status=self.status, name=name)
175+ nodegroup_connections[nodegroup.uuid] = nodegroup.is_connected()
176+ response = self.client.get(self.get_url())
177+ document = fromstring(response.content)
178+ connection_displayed = {}
179+ for uuid in nodegroup_connections:
180+ connection_row = document.xpath(
181+ "//td[@id='%s_connection']" % uuid)[0]
182+ connection_displayed[uuid] = connection_representation[
183+ connection_row.text.strip()]
184+ self.assertEqual(nodegroup_connections, connection_displayed)
185+
186
187 class ClusterListingAccess(MAASServerTestCase):
188
189@@ -226,8 +262,29 @@
190 warning_elems = (
191 nodegroup_row.xpath(
192 "//img[@title='Warning: this cluster has no boot images.']"))
193- self.assertEqual(
194- 1, len(warning_elems), "No warning about missing boot images.")
195+ self.assertThat(
196+ warning_elems, HasLength(1),
197+ "No warning about missing boot images.")
198+
199+ def test_warning_is_displayed_if_a_cluster_is_not_connected(self):
200+ self.client_log_in(as_admin=True)
201+ nodegroup = factory.make_NodeGroup(
202+ status=NODEGROUP_STATUS.ACCEPTED)
203+ # Create a boot image so that the warning about the missing boot images
204+ # isn't displayed.
205+ factory.make_BootImage(nodegroup=nodegroup)
206+
207+ self.patch(NodeGroup, 'is_connected', lambda _: False)
208+ response = self.client.get(reverse('cluster-list'))
209+ document = fromstring(response.content)
210+ nodegroup_row = document.xpath("//tr[@id='%s']" % nodegroup.uuid)[0]
211+ self.assertIn('warning', nodegroup_row.get('class'))
212+ warning_elems = (
213+ nodegroup_row.xpath(
214+ """//img[@title="Warning: this cluster isn't connected."]"""))
215+ self.assertThat(
216+ warning_elems, HasLength(1),
217+ "No warning about disconnected cluster.")
218
219
220 class ClusterDeleteTest(MAASServerTestCase):