~smoser/ubuntu/trusty/maas/lp-1172566

« back to all changes in this revision

Viewing changes to src/maasserver/views/tests/test_clusters.py

  • Committer: Package Import Robot
  • Author(s): Andres Rodriguez
  • Date: 2014-04-03 13:45:02 UTC
  • mto: This revision was merged to the branch mainline in revision 58.
  • Revision ID: package-import@ubuntu.com-20140403134502-8a6wvuqwyuekufh0
Tags: upstream-1.5+bzr2227
ImportĀ upstreamĀ versionĀ 1.5+bzr2227

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2012-2014 Canonical Ltd.  This software is licensed under the
 
2
# GNU Affero General Public License version 3 (see the file LICENSE).
 
3
 
 
4
"""Test maasserver clusters views."""
 
5
 
 
6
from __future__ import (
 
7
    absolute_import,
 
8
    print_function,
 
9
    unicode_literals,
 
10
    )
 
11
 
 
12
str = None
 
13
 
 
14
__metaclass__ = type
 
15
__all__ = []
 
16
 
 
17
import httplib
 
18
 
 
19
from django.core.urlresolvers import reverse
 
20
from lxml.html import fromstring
 
21
from maasserver.enum import (
 
22
    NODEGROUP_STATUS,
 
23
    NODEGROUP_STATUS_CHOICES,
 
24
    NODEGROUPINTERFACE_MANAGEMENT,
 
25
    )
 
26
from maasserver.models import (
 
27
    NodeGroup,
 
28
    nodegroup as nodegroup_module,
 
29
    NodeGroupInterface,
 
30
    )
 
31
from maasserver.testing import (
 
32
    extract_redirect,
 
33
    get_content_links,
 
34
    reload_object,
 
35
    )
 
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
 
40
from mock import (
 
41
    ANY,
 
42
    call,
 
43
    )
 
44
from testtools.matchers import (
 
45
    AllMatch,
 
46
    Contains,
 
47
    ContainsAll,
 
48
    Equals,
 
49
    HasLength,
 
50
    MatchesStructure,
 
51
    )
 
52
 
 
53
 
 
54
class ClusterListingTest(MAASServerTestCase):
 
55
 
 
56
    scenarios = [
 
57
        ('accepted-clusters', {'status': NODEGROUP_STATUS.ACCEPTED}),
 
58
        ('pending-clusters', {'status': NODEGROUP_STATUS.PENDING}),
 
59
        ('rejected-clusters', {'status': NODEGROUP_STATUS.REJECTED}),
 
60
    ]
 
61
 
 
62
    def get_url(self):
 
63
        """Return the listing url used in this scenario."""
 
64
        return reverse(ClusterListView.status_links[
 
65
            self.status])
 
66
 
 
67
    def test_cluster_listing_contains_links_to_manipulate_clusters(self):
 
68
        self.client_log_in(as_admin=True)
 
69
        nodegroups = {
 
70
            factory.make_node_group(status=self.status)
 
71
            for _ in range(3)
 
72
            }
 
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]
 
80
        self.assertThat(
 
81
            links,
 
82
            ContainsAll(nodegroup_edit_links + nodegroup_delete_links))
 
83
 
 
84
    def make_listing_view(self, status):
 
85
        view = ClusterListView()
 
86
        view.status = status
 
87
        return view
 
88
 
 
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]
 
99
        self.assertEqual(
 
100
            '<a href="%s">1 %s cluster</a>' % (
 
101
                reverse(link_name), status_name.lower()),
 
102
            entry)
 
103
 
 
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]
 
113
        self.assertEqual(
 
114
            '0 %s clusters' % status_name.lower(), entry)
 
115
 
 
116
    def test_title_displays_number_of_clusters(self):
 
117
        for _ in range(3):
 
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)
 
123
 
 
124
    def test_title_contains_links_to_other_listings(self):
 
125
        view = self.make_listing_view(self.status)
 
126
        other_statuses = []
 
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)
 
137
 
 
138
    def test_listing_is_paginated(self):
 
139
        self.patch(ClusterListView, "paginate_by", 2)
 
140
        self.client_log_in(as_admin=True)
 
141
        for _ in range(3):
 
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)
 
146
        self.assertThat(
 
147
            doc.cssselect('div.pagination'),
 
148
            HasLength(1),
 
149
            "Couldn't find pagination tag.")
 
150
 
 
151
 
 
152
class ClusterListingAccess(MAASServerTestCase):
 
153
 
 
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)
 
159
 
 
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)
 
165
 
 
166
 
 
167
class ClusterPendingListingTest(MAASServerTestCase):
 
168
 
 
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))
 
176
 
 
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))
 
184
 
 
185
    def test_pending_listing_accepts_all_pending_nodegroups_POST(self):
 
186
        self.client_log_in(as_admin=True)
 
187
        nodegroups = {
 
188
            factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
 
189
            factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
 
190
        }
 
191
        response = self.client.post(
 
192
            reverse('cluster-list-pending'), {'mass_accept_submit': 1})
 
193
        self.assertEqual(httplib.FOUND, response.status_code)
 
194
        self.assertEqual(
 
195
            [reload_object(nodegroup).status for nodegroup in nodegroups],
 
196
            [NODEGROUP_STATUS.ACCEPTED] * 2)
 
197
 
 
198
    def test_pending_listing_rejects_all_pending_nodegroups_POST(self):
 
199
        self.client_log_in(as_admin=True)
 
200
        nodegroups = {
 
201
            factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
 
202
            factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
 
203
        }
 
204
        response = self.client.post(
 
205
            reverse('cluster-list-pending'), {'mass_reject_submit': 1})
 
206
        self.assertEqual(httplib.FOUND, response.status_code)
 
207
        self.assertEqual(
 
208
            [reload_object(nodegroup).status for nodegroup in nodegroups],
 
209
            [NODEGROUP_STATUS.REJECTED] * 2)
 
210
 
 
211
 
 
212
class ClusterAcceptedListingTest(MAASServerTestCase):
 
213
 
 
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),
 
220
        ]
 
221
        response = self.client.post(
 
222
            reverse('cluster-list'), {'import_all_boot_images': 1})
 
223
        self.assertEqual(httplib.FOUND, response.status_code)
 
224
        calls = [
 
225
            call(queue=nodegroup.work_queue, kwargs=ANY)
 
226
            for nodegroup in accepted_nodegroups
 
227
        ]
 
228
        self.assertItemsEqual(calls, recorder.apply_async.call_args_list)
 
229
 
 
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'))
 
238
        warning_elems = (
 
239
            nodegroup_row.xpath(
 
240
                "//img[@title='Warning: this cluster has no boot images.']"))
 
241
        self.assertEqual(
 
242
            1, len(warning_elems), "No warning about missing boot images.")
 
243
 
 
244
 
 
245
class ClusterDeleteTest(MAASServerTestCase):
 
246
 
 
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'})
 
252
        self.assertEqual(
 
253
            (httplib.FOUND, reverse('cluster-list')),
 
254
            (response.status_code, extract_redirect(response)))
 
255
        self.assertFalse(
 
256
            NodeGroup.objects.filter(uuid=nodegroup.uuid).exists())
 
257
 
 
258
 
 
259
class ClusterEditTest(MAASServerTestCase):
 
260
 
 
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()
 
264
        interfaces = set()
 
265
        for i in range(3):
 
266
            interface = factory.make_node_group_interface(
 
267
                nodegroup=nodegroup,
 
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 = [
 
273
            reverse(
 
274
                'cluster-interface-edit',
 
275
                args=[nodegroup.uuid, interface.interface])
 
276
            for interface in interfaces]
 
277
        interface_delete_links = [
 
278
            reverse(
 
279
                'cluster-interface-delete',
 
280
                args=[nodegroup.uuid, interface.interface])
 
281
            for interface in interfaces]
 
282
        self.assertThat(
 
283
            links,
 
284
            ContainsAll(interface_edit_links + interface_delete_links))
 
285
 
 
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])
 
290
        data = {
 
291
            'cluster_name': factory.make_name('cluster_name'),
 
292
            'name': factory.make_name('name'),
 
293
            'status': factory.getRandomEnum(NODEGROUP_STATUS),
 
294
            }
 
295
        response = self.client.post(edit_link, data)
 
296
        self.assertEqual(httplib.FOUND, response.status_code, response.content)
 
297
        self.assertThat(
 
298
            reload_object(nodegroup),
 
299
            MatchesStructure.byEquality(**data))
 
300
 
 
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])))
 
306
        self.assertIn(
 
307
            reverse('cluster-interface-create', args=[nodegroup.uuid]), links)
 
308
 
 
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]))
 
315
        self.assertEqual(
 
316
            httplib.OK, response.status_code, response.content)
 
317
        links = get_content_links(response)
 
318
        self.assertIn(
 
319
            reverse('cluster-bootimages-list', args=[nodegroup.uuid]), links)
 
320
 
 
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)
 
330
        self.assertEqual(
 
331
            1, len(doc.cssselect('#no_boot_images_warning')),
 
332
            "Warning about missing images not present")
 
333
        links = get_content_links(response)
 
334
        self.assertNotIn(
 
335
            reverse('cluster-bootimages-list', args=[nodegroup.uuid]), links)
 
336
 
 
337
 
 
338
class ClusterInterfaceDeleteTest(MAASServerTestCase):
 
339
 
 
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(
 
344
            nodegroup=nodegroup,
 
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'})
 
350
        self.assertEqual(
 
351
            (httplib.FOUND, reverse('cluster-edit', args=[nodegroup.uuid])),
 
352
            (response.status_code, extract_redirect(response)))
 
353
        self.assertFalse(
 
354
            NodeGroupInterface.objects.filter(id=interface.id).exists())
 
355
 
 
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))
 
368
 
 
369
 
 
370
class ClusterInterfaceEditTest(MAASServerTestCase):
 
371
 
 
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(
 
377
            nodegroup=nodegroup)
 
378
        edit_link = reverse(
 
379
            'cluster-interface-edit',
 
380
            args=[nodegroup.uuid, interface.interface])
 
381
        data = factory.get_interface_fields()
 
382
        response = self.client.post(edit_link, data)
 
383
        self.assertEqual(
 
384
            (httplib.FOUND, reverse('cluster-edit', args=[nodegroup.uuid])),
 
385
            (response.status_code, extract_redirect(response)))
 
386
        self.assertThat(
 
387
            reload_object(interface),
 
388
            MatchesStructure.byEquality(**data))
 
389
 
 
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")
 
396
        edit_link = reverse(
 
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))
 
402
 
 
403
 
 
404
class ClusterInterfaceCreateTest(MAASServerTestCase):
 
405
 
 
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)
 
414
        self.assertEqual(
 
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'])
 
419
        self.assertThat(
 
420
            reload_object(interface),
 
421
            MatchesStructure.byEquality(**data))
 
422
 
 
423
 
 
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
 
426
# name.
 
427
class ClusterInterfaceDoubleSlashBugTest(MAASServerTestCase):
 
428
 
 
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)
 
435
        edit_link = reverse(
 
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
 
447
        # urls.
 
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)))