1
# Copyright 2012-2014 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
"""Test maasserver clusters views."""
6
from __future__ import (
19
from django.core.urlresolvers import reverse
20
from lxml.html import fromstring
21
from maasserver.enum import (
23
NODEGROUP_STATUS_CHOICES,
24
NODEGROUPINTERFACE_MANAGEMENT,
26
from maasserver.models import (
28
nodegroup as nodegroup_module,
31
from maasserver.testing import (
36
from maasserver.testing.factory import factory
37
from maasserver.testing.testcase import MAASServerTestCase
38
from maasserver.utils import map_enum
39
from maasserver.views.clusters import ClusterListView
44
from testtools.matchers import (
54
class ClusterListingTest(MAASServerTestCase):
57
('accepted-clusters', {'status': NODEGROUP_STATUS.ACCEPTED}),
58
('pending-clusters', {'status': NODEGROUP_STATUS.PENDING}),
59
('rejected-clusters', {'status': NODEGROUP_STATUS.REJECTED}),
63
"""Return the listing url used in this scenario."""
64
return reverse(ClusterListView.status_links[
67
def test_cluster_listing_contains_links_to_manipulate_clusters(self):
68
self.client_log_in(as_admin=True)
70
factory.make_node_group(status=self.status)
73
links = get_content_links(self.client.get(self.get_url()))
74
nodegroup_edit_links = [
75
reverse('cluster-edit', args=[nodegroup.uuid])
76
for nodegroup in nodegroups]
77
nodegroup_delete_links = [
78
reverse('cluster-delete', args=[nodegroup.uuid])
79
for nodegroup in nodegroups]
82
ContainsAll(nodegroup_edit_links + nodegroup_delete_links))
84
def make_listing_view(self, status):
85
view = ClusterListView()
89
def test_make_title_entry_returns_link_for_other_status(self):
90
# If the entry's status is different from the view's status,
91
# the returned entry is a link.
92
other_status = factory.getRandomChoice(
93
NODEGROUP_STATUS_CHOICES, but_not=[self.status])
94
factory.make_node_group(status=other_status)
95
link_name = ClusterListView.status_links[other_status]
96
view = self.make_listing_view(self.status)
97
entry = view.make_title_entry(other_status, link_name)
98
status_name = NODEGROUP_STATUS_CHOICES[other_status][1]
100
'<a href="%s">1 %s cluster</a>' % (
101
reverse(link_name), status_name.lower()),
104
def test_make_title_entry_returns_title_if_no_cluster(self):
105
# If no cluster correspond to the entry's status, the returned
106
# entry is not a link: it's a simple mention '0 <status> clusters'.
107
other_status = factory.getRandomChoice(
108
NODEGROUP_STATUS_CHOICES, but_not=[self.status])
109
link_name = ClusterListView.status_links[other_status]
110
view = self.make_listing_view(self.status)
111
entry = view.make_title_entry(other_status, link_name)
112
status_name = NODEGROUP_STATUS_CHOICES[other_status][1]
114
'0 %s clusters' % status_name.lower(), entry)
116
def test_title_displays_number_of_clusters(self):
118
factory.make_node_group(status=self.status)
119
view = self.make_listing_view(self.status)
120
status_name = NODEGROUP_STATUS_CHOICES[self.status][1]
121
title = view.make_cluster_listing_title()
122
self.assertIn("3 %s clusters" % status_name.lower(), title)
124
def test_title_contains_links_to_other_listings(self):
125
view = self.make_listing_view(self.status)
127
# Compute a list with the statuses of the clusters not being
128
# displayed by the 'view'. Create clusters with these statuses.
129
for status in map_enum(NODEGROUP_STATUS).values():
130
if status != self.status:
131
other_statuses.append(status)
132
factory.make_node_group(status=status)
133
for status in other_statuses:
134
link_name = ClusterListView.status_links[status]
135
title = view.make_cluster_listing_title()
136
self.assertIn(reverse(link_name), title)
138
def test_listing_is_paginated(self):
139
self.patch(ClusterListView, "paginate_by", 2)
140
self.client_log_in(as_admin=True)
142
factory.make_node_group(status=self.status)
143
response = self.client.get(self.get_url())
144
self.assertEqual(httplib.OK, response.status_code)
145
doc = fromstring(response.content)
147
doc.cssselect('div.pagination'),
149
"Couldn't find pagination tag.")
152
class ClusterListingAccess(MAASServerTestCase):
154
def test_admin_sees_cluster_tab(self):
155
self.client_log_in(as_admin=True)
156
links = get_content_links(
157
self.client.get(reverse('index')), element='#main-nav')
158
self.assertIn(reverse('cluster-list'), links)
160
def test_non_admin_doesnt_see_cluster_tab(self):
161
self.client_log_in(as_admin=False)
162
links = get_content_links(
163
self.client.get(reverse('index')), element='#main-nav')
164
self.assertNotIn(reverse('cluster-list'), links)
167
class ClusterPendingListingTest(MAASServerTestCase):
169
def test_pending_listing_contains_form_to_accept_all_nodegroups(self):
170
self.client_log_in(as_admin=True)
171
factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
172
response = self.client.get(reverse('cluster-list-pending'))
173
doc = fromstring(response.content)
174
forms = doc.cssselect('form#accept_all_pending_nodegroups')
175
self.assertEqual(1, len(forms))
177
def test_pending_listing_contains_form_to_reject_all_nodegroups(self):
178
self.client_log_in(as_admin=True)
179
factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
180
response = self.client.get(reverse('cluster-list-pending'))
181
doc = fromstring(response.content)
182
forms = doc.cssselect('form#reject_all_pending_nodegroups')
183
self.assertEqual(1, len(forms))
185
def test_pending_listing_accepts_all_pending_nodegroups_POST(self):
186
self.client_log_in(as_admin=True)
188
factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
189
factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
191
response = self.client.post(
192
reverse('cluster-list-pending'), {'mass_accept_submit': 1})
193
self.assertEqual(httplib.FOUND, response.status_code)
195
[reload_object(nodegroup).status for nodegroup in nodegroups],
196
[NODEGROUP_STATUS.ACCEPTED] * 2)
198
def test_pending_listing_rejects_all_pending_nodegroups_POST(self):
199
self.client_log_in(as_admin=True)
201
factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
202
factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
204
response = self.client.post(
205
reverse('cluster-list-pending'), {'mass_reject_submit': 1})
206
self.assertEqual(httplib.FOUND, response.status_code)
208
[reload_object(nodegroup).status for nodegroup in nodegroups],
209
[NODEGROUP_STATUS.REJECTED] * 2)
212
class ClusterAcceptedListingTest(MAASServerTestCase):
214
def test_accepted_listing_import_boot_images_calls_tasks(self):
215
self.client_log_in(as_admin=True)
216
recorder = self.patch(nodegroup_module, 'import_boot_images')
217
accepted_nodegroups = [
218
factory.make_node_group(status=NODEGROUP_STATUS.ACCEPTED),
219
factory.make_node_group(status=NODEGROUP_STATUS.ACCEPTED),
221
response = self.client.post(
222
reverse('cluster-list'), {'import_all_boot_images': 1})
223
self.assertEqual(httplib.FOUND, response.status_code)
225
call(queue=nodegroup.work_queue, kwargs=ANY)
226
for nodegroup in accepted_nodegroups
228
self.assertItemsEqual(calls, recorder.apply_async.call_args_list)
230
def test_a_warning_is_displayed_if_the_cluster_has_no_boot_images(self):
231
self.client_log_in(as_admin=True)
232
nodegroup = factory.make_node_group(
233
status=NODEGROUP_STATUS.ACCEPTED)
234
response = self.client.get(reverse('cluster-list'))
235
document = fromstring(response.content)
236
nodegroup_row = document.xpath("//tr[@id='%s']" % nodegroup.uuid)[0]
237
self.assertIn('warning', nodegroup_row.get('class'))
240
"//img[@title='Warning: this cluster has no boot images.']"))
242
1, len(warning_elems), "No warning about missing boot images.")
245
class ClusterDeleteTest(MAASServerTestCase):
247
def test_can_delete_cluster(self):
248
self.client_log_in(as_admin=True)
249
nodegroup = factory.make_node_group()
250
delete_link = reverse('cluster-delete', args=[nodegroup.uuid])
251
response = self.client.post(delete_link, {'post': 'yes'})
253
(httplib.FOUND, reverse('cluster-list')),
254
(response.status_code, extract_redirect(response)))
256
NodeGroup.objects.filter(uuid=nodegroup.uuid).exists())
259
class ClusterEditTest(MAASServerTestCase):
261
def test_cluster_page_contains_links_to_edit_and_delete_interfaces(self):
262
self.client_log_in(as_admin=True)
263
nodegroup = factory.make_node_group()
266
interface = factory.make_node_group_interface(
268
management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
269
interfaces.add(interface)
270
links = get_content_links(
271
self.client.get(reverse('cluster-edit', args=[nodegroup.uuid])))
272
interface_edit_links = [
274
'cluster-interface-edit',
275
args=[nodegroup.uuid, interface.interface])
276
for interface in interfaces]
277
interface_delete_links = [
279
'cluster-interface-delete',
280
args=[nodegroup.uuid, interface.interface])
281
for interface in interfaces]
284
ContainsAll(interface_edit_links + interface_delete_links))
286
def test_can_edit_cluster(self):
287
self.client_log_in(as_admin=True)
288
nodegroup = factory.make_node_group()
289
edit_link = reverse('cluster-edit', args=[nodegroup.uuid])
291
'cluster_name': factory.make_name('cluster_name'),
292
'name': factory.make_name('name'),
293
'status': factory.getRandomEnum(NODEGROUP_STATUS),
295
response = self.client.post(edit_link, data)
296
self.assertEqual(httplib.FOUND, response.status_code, response.content)
298
reload_object(nodegroup),
299
MatchesStructure.byEquality(**data))
301
def test_contains_link_to_add_interface(self):
302
self.client_log_in(as_admin=True)
303
nodegroup = factory.make_node_group()
304
links = get_content_links(
305
self.client.get(reverse('cluster-edit', args=[nodegroup.uuid])))
307
reverse('cluster-interface-create', args=[nodegroup.uuid]), links)
309
def test_contains_link_to_boot_image_list(self):
310
self.client_log_in(as_admin=True)
311
nodegroup = factory.make_node_group()
312
[factory.make_boot_image(nodegroup=nodegroup) for _ in range(3)]
313
response = self.client.get(
314
reverse('cluster-edit', args=[nodegroup.uuid]))
316
httplib.OK, response.status_code, response.content)
317
links = get_content_links(response)
319
reverse('cluster-bootimages-list', args=[nodegroup.uuid]), links)
321
def test_displays_warning_if_boot_image_list_is_empty(self):
322
# Create boot images in another nodegroup.
323
[factory.make_boot_image() for _ in range(3)]
324
self.client_log_in(as_admin=True)
325
nodegroup = factory.make_node_group()
326
response = self.client.get(
327
reverse('cluster-edit', args=[nodegroup.uuid]))
328
self.assertEqual(httplib.OK, response.status_code)
329
doc = fromstring(response.content)
331
1, len(doc.cssselect('#no_boot_images_warning')),
332
"Warning about missing images not present")
333
links = get_content_links(response)
335
reverse('cluster-bootimages-list', args=[nodegroup.uuid]), links)
338
class ClusterInterfaceDeleteTest(MAASServerTestCase):
340
def test_can_delete_cluster_interface(self):
341
self.client_log_in(as_admin=True)
342
nodegroup = factory.make_node_group()
343
interface = factory.make_node_group_interface(
345
management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
346
delete_link = reverse(
347
'cluster-interface-delete',
348
args=[nodegroup.uuid, interface.interface])
349
response = self.client.post(delete_link, {'post': 'yes'})
351
(httplib.FOUND, reverse('cluster-edit', args=[nodegroup.uuid])),
352
(response.status_code, extract_redirect(response)))
354
NodeGroupInterface.objects.filter(id=interface.id).exists())
356
def test_interface_delete_supports_interface_alias(self):
357
self.client_log_in(as_admin=True)
358
nodegroup = factory.make_node_group(
359
management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
360
interface = factory.make_node_group_interface(
361
nodegroup=nodegroup, interface="eth0:0")
362
delete_link = reverse(
363
'cluster-interface-delete',
364
args=[nodegroup.uuid, interface.interface])
365
# The real test is that reverse() does not blow up when the
366
# interface's name contains an alias.
367
self.assertIsInstance(delete_link, (bytes, unicode))
370
class ClusterInterfaceEditTest(MAASServerTestCase):
372
def test_can_edit_cluster_interface(self):
373
self.client_log_in(as_admin=True)
374
nodegroup = factory.make_node_group(
375
management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
376
interface = factory.make_node_group_interface(
379
'cluster-interface-edit',
380
args=[nodegroup.uuid, interface.interface])
381
data = factory.get_interface_fields()
382
response = self.client.post(edit_link, data)
384
(httplib.FOUND, reverse('cluster-edit', args=[nodegroup.uuid])),
385
(response.status_code, extract_redirect(response)))
387
reload_object(interface),
388
MatchesStructure.byEquality(**data))
390
def test_interface_edit_supports_interface_alias(self):
391
self.client_log_in(as_admin=True)
392
nodegroup = factory.make_node_group(
393
management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
394
interface = factory.make_node_group_interface(
395
nodegroup=nodegroup, interface="eth0:0")
397
'cluster-interface-edit',
398
args=[nodegroup.uuid, interface.interface])
399
# The real test is that reverse() does not blow up when the
400
# interface's name contains an alias.
401
self.assertIsInstance(edit_link, (bytes, unicode))
404
class ClusterInterfaceCreateTest(MAASServerTestCase):
406
def test_can_create_cluster_interface(self):
407
self.client_log_in(as_admin=True)
408
nodegroup = factory.make_node_group(
409
management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
410
create_link = reverse(
411
'cluster-interface-create', args=[nodegroup.uuid])
412
data = factory.get_interface_fields()
413
response = self.client.post(create_link, data)
415
(httplib.FOUND, reverse('cluster-edit', args=[nodegroup.uuid])),
416
(response.status_code, extract_redirect(response)))
417
interface = NodeGroupInterface.objects.get(
418
nodegroup__uuid=nodegroup.uuid, interface=data['interface'])
420
reload_object(interface),
421
MatchesStructure.byEquality(**data))
424
# XXX: rvb 2012-10-08 bug=1063881: apache transforms '//' into '/' in
425
# the urls it passes around and this happens when an interface has an empty
427
class ClusterInterfaceDoubleSlashBugTest(MAASServerTestCase):
429
def test_edit_delete_empty_cluster_interface_when_slash_removed(self):
430
self.client_log_in(as_admin=True)
431
nodegroup = factory.make_node_group()
432
interface = factory.make_node_group_interface(
433
nodegroup=nodegroup, interface='',
434
management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
436
'cluster-interface-edit',
437
args=[nodegroup.uuid, interface.interface])
438
delete_link = reverse(
439
'cluster-interface-delete',
440
args=[nodegroup.uuid, interface.interface])
441
links = [edit_link, delete_link]
442
# Just make sure that the urls contains '//'. If this is not
443
# true anymore, because we've refactored the urls, this test can
444
# problably be removed.
445
self.assertThat(links, AllMatch(Contains('//')))
446
# Simulate what apache (when used as a frontend) does to the
448
new_links = [link.replace('//', '/') for link in links]
449
response_statuses = [
450
self.client.get(link).status_code for link in new_links]
451
self.assertThat(response_statuses, AllMatch(Equals(httplib.OK)))