launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #12851
[Merge] lp:~rvb/maas/nodegroup-ui into lp:maas
Raphaël Badin has proposed merging lp:~rvb/maas/nodegroup-ui into lp:maas with lp:~rvb/maas/remove-ip-range-uniqueness as a prerequisite.
Commit message:
Add UI pages to edit and delete the clusters and the cluster interfaces.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~rvb/maas/nodegroup-ui/+merge/127603
This branch adds the UI pages to edit and delete the clusters and the cluster interfaces.
I followed Huw's mockups but I had to put the edition of the interfaces on a separate page: on Huw's mockups, only one interface can be edited but as soon as we have many interfaces, then having them all one one page is a mess.
= UI =
Cluster listing on the settings page:
http://people.canonical.com/~rvb/clusters_listing.png
Edition of a cluster:
http://people.canonical.com/~rvb/cluster_edit.png
Deletion of a cluster:
http://people.canonical.com/~rvb/cluster_delete.png
Edition of an interface:
http://people.canonical.com/~rvb/cluster_interface_edit.png
Deletion of an interface:
http://people.canonical.com/~rvb/cluster_interface_delete.png
= Notes =
I'm sure the UI can be improved but maybe it good to get that reviewed and landed and then have a UI specialist (hi Huw) have a look at it and fix things directly.
This branch revealed on problem but it can probably be fixed in a follow-up branch: when one has an approved cluster with a managed interface and sets a network config (like broadcast address) to None, then the dhcp writing task blows up when the form is saved. The validation of the form has to be more strict.
Also, I did not add the UI to add new clusters or new cluster interfaces. Not sure this is needed because that registration step should be done via the API, when a cluster registers itself. But anyway, this can be added later.
--
https://code.launchpad.net/~rvb/maas/nodegroup-ui/+merge/127603
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/nodegroup-ui into lp:maas.
=== modified file 'src/maasserver/enum.py'
--- src/maasserver/enum.py 2012-09-28 17:20:04 +0000
+++ src/maasserver/enum.py 2012-10-03 07:59:24 +0000
@@ -18,6 +18,7 @@
'NODEGROUP_STATUS_CHOICES',
'NODEGROUPINTERFACE_MANAGEMENT',
'NODEGROUPINTERFACE_MANAGEMENT_CHOICES',
+ 'NODEGROUPINTERFACE_MANAGEMENT_CHOICES_DICT',
'NODE_PERMISSION',
'NODE_STATUS',
'NODE_STATUS_CHOICES',
@@ -197,3 +198,7 @@
(NODEGROUPINTERFACE_MANAGEMENT.DHCP, "Manage DHCP"),
(NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS, "Manage DHCP and DNS"),
)
+
+
+NODEGROUPINTERFACE_MANAGEMENT_CHOICES_DICT = (
+ OrderedDict(NODEGROUPINTERFACE_MANAGEMENT_CHOICES))
=== modified file 'src/maasserver/forms.py'
--- src/maasserver/forms.py 2012-10-02 13:55:22 +0000
+++ src/maasserver/forms.py 2012-10-03 07:59:24 +0000
@@ -21,6 +21,7 @@
"MAASAndNetworkForm",
"NodeGroupInterfaceForm",
"NodeGroupWithInterfacesForm",
+ "NodeGroupEdit",
"NodeWithMACAddressesForm",
"SSHKeyForm",
"UbuntuForm",
@@ -652,14 +653,14 @@
class Meta:
model = NodeGroupInterface
fields = (
+ 'interface',
+ 'management',
'ip',
- 'interface',
'subnet_mask',
'broadcast_ip',
'router_ip',
'ip_range_low',
'ip_range_high',
- 'management',
)
def clean_management(self):
@@ -682,11 +683,11 @@
"nodegroup."]})
return management
- def save(self, nodegroup=None, *args, **kwargs):
+ def save(self, nodegroup=None, commit=True, *args, **kwargs):
interface = super(NodeGroupInterfaceForm, self).save(commit=False)
if nodegroup is not None:
interface.nodegroup = nodegroup
- if kwargs.get('commit', True):
+ if commit:
interface.save(*args, **kwargs)
return interface
@@ -709,8 +710,8 @@
class Meta:
model = NodeGroup
fields = (
+ 'cluster_name',
'name',
- 'cluster_name',
'uuid',
)
@@ -782,6 +783,25 @@
return nodegroup
+class NodeGroupEdit(ModelForm):
+
+ name = forms.CharField(
+ label="DNS zone name",
+ help_text=(
+ "Name of the related DNS zone. Note that this will only "
+ "be used if MAAS is managing a DNS zone for one of the interfaces "
+ "of this cluster. See the 'status' of the interfaces below."),
+ required=False)
+
+ class Meta:
+ model = NodeGroup
+ fields = (
+ 'cluster_name',
+ 'status',
+ 'name',
+ )
+
+
class TagForm(ModelForm):
class Meta:
=== modified file 'src/maasserver/models/nodegroup.py'
--- src/maasserver/models/nodegroup.py 2012-10-02 11:18:26 +0000
+++ src/maasserver/models/nodegroup.py 2012-10-03 07:59:24 +0000
@@ -130,7 +130,7 @@
max_length=80, unique=False, editable=True, blank=True, null=False)
status = IntegerField(
- choices=NODEGROUP_STATUS_CHOICES, editable=False,
+ choices=NODEGROUP_STATUS_CHOICES, editable=True,
default=NODEGROUP_STATUS.DEFAULT_STATUS)
# Credentials for the worker to access the API with.
=== modified file 'src/maasserver/models/nodegroupinterface.py'
--- src/maasserver/models/nodegroupinterface.py 2012-10-02 15:19:23 +0000
+++ src/maasserver/models/nodegroupinterface.py 2012-10-03 07:59:24 +0000
@@ -28,6 +28,7 @@
from maasserver.enum import (
NODEGROUPINTERFACE_MANAGEMENT,
NODEGROUPINTERFACE_MANAGEMENT_CHOICES,
+ NODEGROUPINTERFACE_MANAGEMENT_CHOICES_DICT,
)
from maasserver.models.cleansave import CleanSave
from maasserver.models.timestampedmodel import TimestampedModel
@@ -43,7 +44,9 @@
unique_together = ('nodegroup', 'interface')
# Static IP of the interface.
- ip = GenericIPAddressField(null=False, editable=True)
+ ip = GenericIPAddressField(
+ null=False, editable=True,
+ help_text="Static IP Address of the interface")
# The `NodeGroup` this interface belongs to.
nodegroup = ForeignKey(
@@ -55,7 +58,8 @@
# DHCP server settings.
interface = CharField(
- blank=True, editable=True, max_length=255, default='')
+ blank=True, editable=True, max_length=255, default='',
+ help_text="Name of this interface (e.g. 'em1').")
subnet_mask = GenericIPAddressField(
editable=True, unique=False, blank=True, null=True, default=None)
broadcast_ip = GenericIPAddressField(
@@ -80,6 +84,10 @@
return IPNetwork("%s/%s" % (self.broadcast_ip, self.subnet_mask))
return None
+ def display_management(self):
+ """Return management status text as displayed to the user."""
+ return NODEGROUPINTERFACE_MANAGEMENT_CHOICES_DICT[self.management]
+
def __repr__(self):
return "<NodeGroupInterface %r,%s>" % (self.nodegroup, self.interface)
=== added file 'src/maasserver/templates/maasserver/nodegroup_confirm_delete.html'
--- src/maasserver/templates/maasserver/nodegroup_confirm_delete.html 1970-01-01 00:00:00 +0000
+++ src/maasserver/templates/maasserver/nodegroup_confirm_delete.html 2012-10-03 07:59:24 +0000
@@ -0,0 +1,24 @@
+{% extends "maasserver/base.html" %}
+
+{% block nav-active-settings %}active{% endblock %}
+{% block title %}Delete Cluster Controller{% endblock %}
+{% block page-title %}Delete Cluster Controller{% endblock %}
+
+{% block content %}
+ <div class="block auto-width">
+ <h2>
+ Are you sure you want to delete the cluster controller
+ "{{ cluster_to_delete.cluster_name }}"?
+ </h2>
+ <p>This action is permanent and can not be undone.</p>
+ <p>
+ <form action="." method="post">{% csrf_token %}
+ <input type="hidden" name="post" value="yes" />
+ <input type="submit" value="Delete cluster controller"
+ class="right" />
+ <a href="{% url 'settings' %}">Cancel</a>
+ </form>
+ </p>
+ </div>
+{% endblock %}
+
=== added file 'src/maasserver/templates/maasserver/nodegroup_edit.html'
--- src/maasserver/templates/maasserver/nodegroup_edit.html 1970-01-01 00:00:00 +0000
+++ src/maasserver/templates/maasserver/nodegroup_edit.html 2012-10-03 07:59:24 +0000
@@ -0,0 +1,66 @@
+{% extends "maasserver/base.html" %}
+
+{% block nav-active-settings %}active{% endblock %}
+{% block title %}Edit Cluster Controller{% endblock %}
+{% block page-title %}Edit Cluster Controller{% endblock %}
+
+{% block content %}
+<div class="block size10 first">
+ <form action="." method="post" class="block">
+ {% csrf_token %}
+ <ul>
+ {% for field in form %}
+ {% include "maasserver/form_field.html" %}
+ {% endfor %}
+ </ul>
+ <input type="submit" value="Save cluster controller"
+ class="button right" />
+ <a class="link-button" href="{% url 'settings' %}">Cancel</a>
+ <h2>Interfaces</h2>
+ {% with nb_interfaces=cluster.nodegroupinterface_set.count %}
+ This cluster controller has {{ nb_interfaces }}
+ interface{{ nb_interfaces|pluralize }}.
+ {% endwith %}
+ <table class="list">
+ <thead>
+ <tr>
+ <th>Interface</th>
+ <th>Network</th>
+ <th>Status</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for interface in interfaces %}
+ <tr class="interface {% cycle 'even' 'odd' %}"
+ id="{{ interface.interface }}">
+ <td>{{ interface.interface }}</td>
+ <td>{{ interface.network|default_if_none:"Not configured" }}</td>
+ <td>{{ interface.display_management }}</td>
+ <td>
+ <a href="{% url 'cluster-interface-edit' uuid=cluster.uuid interface=interface.interface %}"
+ title="Edit interface {{ interface.interface }}">
+ <img src="{{ STATIC_URL }}img/edit.png"
+ alt="edit"
+ class="space-right-small" />
+ </a>
+ <a title="Delete interface {{ interface.interface }}"
+ href="{% url 'cluster-interface-delete' cluster.uuid interface.interface %}">
+ <img src="{{ STATIC_URL }}img/delete.png" alt="delete" />
+ </a>
+ <form method="POST"
+ action="{% url 'settings' %}">
+ {% csrf_token %}
+ <input type="hidden" name="uuid"
+ value="{{ interface.interface }}" />
+ </form>
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </form>
+ </div>
+ <div class="clear"></div>
+</div>
+{% endblock %}
=== added file 'src/maasserver/templates/maasserver/nodegroupinterface_confirm_delete.html'
--- src/maasserver/templates/maasserver/nodegroupinterface_confirm_delete.html 1970-01-01 00:00:00 +0000
+++ src/maasserver/templates/maasserver/nodegroupinterface_confirm_delete.html 2012-10-03 07:59:24 +0000
@@ -0,0 +1,23 @@
+{% extends "maasserver/base.html" %}
+
+{% block nav-active-settings %}active{% endblock %}
+{% block title %}Delete Interface{% endblock %}
+{% block page-title %}Delete Interface{% endblock %}
+
+{% block content %}
+ <div class="block auto-width">
+ <h2>
+ Are you sure you want to delete the interface
+ "{{ interface_to_delete.interface }}"?
+ </h2>
+ <p>This action is permanent and can not be undone.</p>
+ <p>
+ <form action="." method="post">{% csrf_token %}
+ <input type="hidden" name="post" value="yes" />
+ <input type="submit" value="Delete interface" class="right" />
+ <a href="{% url 'cluster-edit' interface_to_delete.nodegroup.uuid %}">Cancel</a>
+ </form>
+ </p>
+ </div>
+{% endblock %}
+
=== added file 'src/maasserver/templates/maasserver/nodegroupinterface_edit.html'
--- src/maasserver/templates/maasserver/nodegroupinterface_edit.html 1970-01-01 00:00:00 +0000
+++ src/maasserver/templates/maasserver/nodegroupinterface_edit.html 2012-10-03 07:59:24 +0000
@@ -0,0 +1,23 @@
+{% extends "maasserver/base.html" %}
+
+{% block nav-active-settings %}active{% endblock %}
+{% block title %}Edit Cluster Interface{% endblock %}
+{% block page-title %}Edit Cluster Interface{% endblock %}
+
+{% block content %}
+<div class="block size10 first">
+ <form action="." method="post" class="block">
+ {% csrf_token %}
+ <ul>
+ {% for field in form %}
+ {% include "maasserver/form_field.html" %}
+ {% endfor %}
+ </ul>
+ <input type="submit" value="Save interface" class="button right" />
+ <a class="link-button"
+ href="{% url 'cluster-edit' interface.nodegroup.uuid %}">Cancel</a>
+ </form>
+ </div>
+ <div class="clear"></div>
+</div>
+{% endblock %}
=== modified file 'src/maasserver/templates/maasserver/settings.html'
--- src/maasserver/templates/maasserver/settings.html 2012-09-27 13:50:34 +0000
+++ src/maasserver/templates/maasserver/settings.html 2012-10-03 07:59:24 +0000
@@ -69,6 +69,9 @@
</a>
<div class="clear"></div>
</div>
+ <div id="clusters" class="block size7 first">
+ {% include "maasserver/settings_cluster_listing.html" %}
+ </div>
<div id="commissioning" class="block size7 first">
<h2>Commissioning</h2>
<form action="{% url "settings" %}" method="post">
=== added file 'src/maasserver/templates/maasserver/settings_cluster_listing.html'
--- src/maasserver/templates/maasserver/settings_cluster_listing.html 1970-01-01 00:00:00 +0000
+++ src/maasserver/templates/maasserver/settings_cluster_listing.html 2012-10-03 07:59:24 +0000
@@ -0,0 +1,28 @@
+<h2 id="accepted-clusters">Cluster controllers</h2>
+<table class="list" id="accepted-clusters-list">
+ <tbody>
+ {% for cluster in accepted_clusters %}
+ {% include "maasserver/settings_cluster_listing_row.html" %}
+ {% endfor %}
+ </tbody>
+</table>
+{% if pending_clusters %}
+<h3 id="pending-clusters">Pending clusters</h3>
+<table class="list" id="pending-clusters-list">
+ <tbody>
+ {% for cluster in pending_clusters %}
+ {% include "maasserver/settings_cluster_listing_row.html" %}
+ {% endfor %}
+ </tbody>
+</table>
+{% endif %}
+{% if rejected_clusters %}
+<h3 id="rejected-clusters">Rejected clusters</h3>
+<table class="list" id="rejected-clusters-list">
+ <tbody>
+ {% for cluster in rejected_clusters %}
+ {% include "maasserver/settings_cluster_listing_row.html" %}
+ {% endfor %}
+ </tbody>
+</table>
+{% endif %}
=== added file 'src/maasserver/templates/maasserver/settings_cluster_listing_row.html'
--- src/maasserver/templates/maasserver/settings_cluster_listing_row.html 1970-01-01 00:00:00 +0000
+++ src/maasserver/templates/maasserver/settings_cluster_listing_row.html 2012-10-03 07:59:24 +0000
@@ -0,0 +1,22 @@
+<tr class="cluster {% cycle 'even' 'odd' %}"
+ id="{{ cluster.cluster_name }}">
+ <td>{{ cluster.cluster_name }}</td>
+ <td>
+ <a href="{% url 'cluster-edit' cluster.uuid %}"
+ title="Edit cluster {{ cluster.cluster_name }}">
+ <img src="{{ STATIC_URL }}img/edit.png"
+ alt="edit"
+ class="space-right-small" />
+ </a>
+ <a title="Delete cluster {{ cluster.cluster_name }}"
+ href="{% url 'cluster-delete' cluster.uuid %}">
+ <img src="{{ STATIC_URL }}img/delete.png" alt="delete" />
+ </a>
+ <form method="POST"
+ action="{% url 'settings' %}">
+ {% csrf_token %}
+ <input type="hidden" name="uuid"
+ value="{{ cluster.uuid }}" />
+ </form>
+ </td>
+</tr>
=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py 2012-09-30 19:48:23 +0000
+++ src/maasserver/testing/factory.py 2012-10-03 07:59:24 +0000
@@ -32,6 +32,7 @@
MACAddress,
Node,
NodeGroup,
+ NodeGroupInterface,
SSHKey,
Tag,
)
@@ -120,6 +121,38 @@
Node.objects.filter(id=node.id).update(created=created)
return reload_object(node)
+ def get_interface_fields(self, ip=None, router_ip=None, network=None,
+ subnet_mask=None, broadcast_ip=None,
+ ip_range_low=None, ip_range_high=None,
+ interface=None, management=None, **kwargs):
+ if network is None:
+ network = factory.getRandomNetwork()
+ if subnet_mask is None:
+ subnet_mask = str(network.netmask)
+ if broadcast_ip is None:
+ broadcast_ip = str(network.broadcast)
+ if ip_range_low is None:
+ ip_range_low = str(IPAddress(network.first))
+ if ip_range_high is None:
+ ip_range_high = str(IPAddress(network.last))
+ if router_ip is None:
+ router_ip = factory.getRandomIPInNetwork(network)
+ if ip is None:
+ ip = factory.getRandomIPInNetwork(network)
+ if management is None:
+ management = factory.getRandomEnum(NODEGROUPINTERFACE_MANAGEMENT)
+ if interface is None:
+ interface = self.make_name('interface')
+ return dict(
+ subnet_mask=subnet_mask,
+ broadcast_ip=broadcast_ip,
+ ip_range_low=ip_range_low,
+ ip_range_high=ip_range_high,
+ router_ip=router_ip,
+ ip=ip,
+ management=management,
+ interface=interface)
+
def make_node_group(self, name=None, uuid=None, ip=None,
router_ip=None, network=None, subnet_mask=None,
broadcast_ip=None, ip_range_low=None,
@@ -140,32 +173,34 @@
name = self.make_name('nodegroup')
if uuid is None:
uuid = factory.getRandomUUID()
- if network is None:
- network = factory.getRandomNetwork()
- if subnet_mask is None:
- subnet_mask = str(network.netmask)
- if broadcast_ip is None:
- broadcast_ip = str(network.broadcast)
- if ip_range_low is None:
- ip_range_low = str(IPAddress(network.first))
- if ip_range_high is None:
- ip_range_high = str(IPAddress(network.last))
- if router_ip is None:
- router_ip = factory.getRandomIPInNetwork(network)
- if ip is None:
- ip = factory.getRandomIPInNetwork(network)
- if interface is None:
- interface = self.make_name('interface')
+ interface_settings = self.get_interface_fields(
+ ip=ip, router_ip=router_ip, network=network,
+ subnet_mask=subnet_mask, broadcast_ip=broadcast_ip,
+ ip_range_low=ip_range_low, ip_range_high=ip_range_high,
+ interface=interface, management=management)
+ interface_settings.update(kwargs)
ng = NodeGroup.objects.new(
- name=name, uuid=uuid, ip=ip,
- subnet_mask=subnet_mask, broadcast_ip=broadcast_ip,
- router_ip=router_ip, ip_range_low=ip_range_low,
- ip_range_high=ip_range_high, interface=interface,
- management=management, **kwargs)
+ name=name, uuid=uuid, **interface_settings)
ng.status = status
ng.save()
return ng
+ def make_node_group_interface(self, nodegroup, ip=None,
+ router_ip=None, network=None,
+ subnet_mask=None, broadcast_ip=None,
+ ip_range_low=None, ip_range_high=None,
+ interface=None, management=None, **kwargs):
+ interface_settings = self.get_interface_fields(
+ ip=ip, router_ip=router_ip, network=network,
+ subnet_mask=subnet_mask, broadcast_ip=broadcast_ip,
+ ip_range_low=ip_range_low, ip_range_high=ip_range_high,
+ interface=interface, management=management)
+ interface_settings.update(**kwargs)
+ interface = NodeGroupInterface(
+ nodegroup=nodegroup, **interface_settings)
+ interface.save()
+ return interface
+
def make_node_commission_result(self, node=None, name=None, data=None):
if node is None:
node = self.make_node()
=== modified file 'src/maasserver/tests/test_nodegroupinterface.py'
--- src/maasserver/tests/test_nodegroupinterface.py 2012-10-01 19:55:34 +0000
+++ src/maasserver/tests/test_nodegroupinterface.py 2012-10-03 07:59:24 +0000
@@ -15,6 +15,7 @@
from maasserver.enum import (
NODEGROUP_STATUS,
NODEGROUPINTERFACE_MANAGEMENT,
+ NODEGROUPINTERFACE_MANAGEMENT_CHOICES_DICT,
)
from maasserver.testing.factory import factory
from maasserver.testing.testcase import TestCase
@@ -53,3 +54,9 @@
interface = make_interface()
interface.subnet_mask = ""
self.assertIsNone(interface.network)
+
+ def test_display_management_display_management(self):
+ interface = make_interface()
+ self.assertEqual(
+ NODEGROUPINTERFACE_MANAGEMENT_CHOICES_DICT[interface.management],
+ interface.display_management())
=== added file 'src/maasserver/tests/test_views_settings_clusters.py'
--- src/maasserver/tests/test_views_settings_clusters.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/tests/test_views_settings_clusters.py 2012-10-03 07:59:24 +0000
@@ -0,0 +1,144 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test maasserver clusters views."""
+
+from __future__ import (
+ absolute_import,
+ print_function,
+ unicode_literals,
+ )
+
+__metaclass__ = type
+__all__ = []
+
+import httplib
+
+from django.core.urlresolvers import reverse
+from maasserver.enum import (
+ NODEGROUP_STATUS,
+ NODEGROUPINTERFACE_MANAGEMENT,
+ )
+from maasserver.models import (
+ NodeGroup,
+ NodeGroupInterface,
+ )
+from maasserver.testing import (
+ extract_redirect,
+ get_content_links,
+ reload_object,
+ )
+from maasserver.testing.factory import factory
+from maasserver.testing.testcase import (
+ AdminLoggedInTestCase,
+ )
+from maastesting.matchers import ContainsAll
+from testtools.matchers import MatchesStructure
+
+
+class ClusterListingTest(AdminLoggedInTestCase):
+
+ def test_settings_contains_links_to_edit_and_delete_clusters(self):
+ nodegroups = {
+ factory.make_node_group(status=NODEGROUP_STATUS.ACCEPTED),
+ factory.make_node_group(status=NODEGROUP_STATUS.PENDING),
+ factory.make_node_group(status=NODEGROUP_STATUS.REJECTED),
+ }
+ links = get_content_links(self.client.get(reverse('settings')))
+ nodegroup_edit_links = [
+ reverse('cluster-edit', args=[nodegroup.uuid])
+ for nodegroup in nodegroups]
+ nodegroup_delete_links = [
+ reverse('cluster-delete', args=[nodegroup.uuid])
+ for nodegroup in nodegroups]
+ self.assertThat(
+ links,
+ ContainsAll(nodegroup_edit_links + nodegroup_delete_links))
+
+
+class ClusterDeleteTest(AdminLoggedInTestCase):
+
+ def test_can_delete_cluster(self):
+ nodegroup = factory.make_node_group()
+ delete_link = reverse('cluster-delete', args=[nodegroup.uuid])
+ response = self.client.post(delete_link, {'post': 'yes'})
+ self.assertEqual(
+ (httplib.FOUND, reverse('settings')),
+ (response.status_code, extract_redirect(response)))
+ self.assertFalse(
+ NodeGroup.objects.filter(uuid=nodegroup.uuid).exists())
+
+
+class ClusterEditTest(AdminLoggedInTestCase):
+
+ def test_cluster_page_contains_links_to_edit_and_delete_interfaces(self):
+ nodegroup = factory.make_node_group()
+ interfaces = set()
+ for i in range(3):
+ interface = factory.make_node_group_interface(
+ nodegroup=nodegroup,
+ management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
+ interfaces.add(interface)
+ links = get_content_links(
+ self.client.get(reverse('cluster-edit', args=[nodegroup.uuid])))
+ interface_edit_links = [
+ reverse('cluster-interface-edit',
+ args=[nodegroup.uuid, interface.interface])
+ for interface in interfaces]
+ interface_delete_links = [
+ reverse('cluster-interface-delete',
+ args=[nodegroup.uuid, interface.interface])
+ for interface in interfaces]
+ self.assertThat(
+ links,
+ ContainsAll(interface_edit_links + interface_delete_links))
+
+ def test_can_edit_cluster(self):
+ nodegroup = factory.make_node_group()
+ edit_link = reverse('cluster-edit', args=[nodegroup.uuid])
+ data = {
+ 'cluster_name': factory.make_name('cluster_name'),
+ 'name': factory.make_name('name'),
+ 'status': factory.getRandomEnum(NODEGROUP_STATUS),
+ }
+ response = self.client.post(edit_link, data)
+ self.assertEqual(httplib.FOUND, response.status_code, response.content)
+ self.assertThat(
+ reload_object(nodegroup),
+ MatchesStructure.byEquality(**data))
+
+
+class ClusterInterfaceDeleteTest(AdminLoggedInTestCase):
+
+ def test_can_delete_cluster_interface(self):
+ nodegroup = factory.make_node_group()
+ interface = factory.make_node_group_interface(
+ nodegroup=nodegroup,
+ management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
+ delete_link = reverse(
+ 'cluster-interface-delete',
+ args=[nodegroup.uuid, interface.interface])
+ response = self.client.post(delete_link, {'post': 'yes'})
+ self.assertEqual(
+ (httplib.FOUND, reverse('cluster-edit', args=[nodegroup.uuid])),
+ (response.status_code, extract_redirect(response)))
+ self.assertFalse(
+ NodeGroupInterface.objects.filter(id=interface.id).exists())
+
+
+class ClusterInterfaceEditTest(AdminLoggedInTestCase):
+
+ def test_can_edit_cluster_interface(self):
+ nodegroup = factory.make_node_group(
+ management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
+ interface = factory.make_node_group_interface(
+ nodegroup=nodegroup)
+ edit_link = reverse(
+ 'cluster-interface-edit',
+ args=[nodegroup.uuid, interface.interface])
+ data = factory.get_interface_fields()
+ response = self.client.post(edit_link, data)
+ self.assertEqual(httplib.FOUND, response.status_code, response.content)
+ self.assertThat(
+ reload_object(interface),
+ MatchesStructure.byEquality(**data))
=== modified file 'src/maasserver/urls.py'
--- src/maasserver/urls.py 2012-10-01 08:24:34 +0000
+++ src/maasserver/urls.py 2012-10-03 07:59:24 +0000
@@ -51,9 +51,13 @@
settings,
settings_add_archive,
)
-from maasserver.views.tags import (
- TagView,
+from maasserver.views.settings_clusters import (
+ ClusterDelete,
+ ClusterEdit,
+ ClusterInterfaceDelete,
+ ClusterInterfaceEdit,
)
+from maasserver.views.tags import TagView
def adminurl(regexp, view, *args, **kwargs):
@@ -129,6 +133,20 @@
## URLs for admin users.
# Settings views.
urlpatterns += patterns('maasserver.views',
+ adminurl(
+ r'^clusters/(?P<uuid>[\w\-]+)/edit/$', ClusterEdit.as_view(),
+ name='cluster-edit'),
+ adminurl(
+ r'^clusters/(?P<uuid>[\w\-]+)/delete/$', ClusterDelete.as_view(),
+ name='cluster-delete'),
+ adminurl(
+ r'^clusters/(?P<uuid>[\w\-]+)/interfaces/(?P<interface>[\w\-]*)/'
+ 'edit/$',
+ ClusterInterfaceEdit.as_view(), name='cluster-interface-edit'),
+ adminurl(
+ r'^clusters/(?P<uuid>[\w\-]+)/interfaces/(?P<interface>[\w\-]*)/'
+ 'delete/$',
+ ClusterInterfaceDelete.as_view(), name='cluster-interface-delete'),
adminurl(r'^settings/$', settings, name='settings'),
adminurl(
r'^settings/archives/add/$', settings_add_archive,
=== modified file 'src/maasserver/views/settings.py'
--- src/maasserver/views/settings.py 2012-04-23 14:02:51 +0000
+++ src/maasserver/views/settings.py 2012-10-03 07:59:24 +0000
@@ -37,6 +37,7 @@
from django.views.generic.base import TemplateView
from django.views.generic.detail import SingleObjectTemplateResponseMixin
from django.views.generic.edit import ModelFormMixin
+from maasserver.enum import NODEGROUP_STATUS
from maasserver.exceptions import CannotDeleteUserException
from maasserver.forms import (
AddArchiveForm,
@@ -46,7 +47,10 @@
NewUserCreationForm,
UbuntuForm,
)
-from maasserver.models import UserProfile
+from maasserver.models import (
+ NodeGroup,
+ UserProfile,
+ )
from maasserver.views import process_form
@@ -173,10 +177,21 @@
if response is not None:
return response
+ # Cluster settings:
+ accepted_clusters = NodeGroup.objects.filter(
+ status=NODEGROUP_STATUS.ACCEPTED).order_by('cluster_name')
+ pending_clusters = NodeGroup.objects.filter(
+ status=NODEGROUP_STATUS.PENDING).order_by('cluster_name')
+ rejected_clusters = NodeGroup.objects.filter(
+ status=NODEGROUP_STATUS.REJECTED).order_by('cluster_name')
+
return render_to_response(
'maasserver/settings.html',
{
'user_list': user_list,
+ 'accepted_clusters': accepted_clusters,
+ 'pending_clusters': pending_clusters,
+ 'rejected_clusters': rejected_clusters,
'maas_and_network_form': maas_and_network_form,
'commissioning_form': commissioning_form,
'ubuntu_form': ubuntu_form,
=== added file 'src/maasserver/views/settings_clusters.py'
--- src/maasserver/views/settings_clusters.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/views/settings_clusters.py 2012-10-03 07:59:24 +0000
@@ -0,0 +1,119 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Cluster Settings views."""
+
+from __future__ import (
+ absolute_import,
+ print_function,
+ unicode_literals,
+ )
+
+__metaclass__ = type
+__all__ = [
+ "ClusterDelete",
+ "ClusterEdit",
+ "ClusterInterfaceDelete",
+ "ClusterInterfaceEdit",
+ ]
+
+from django.contrib import messages
+from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect
+from django.shortcuts import get_object_or_404
+from django.views.generic import (
+ DeleteView,
+ UpdateView,
+ )
+from maasserver.forms import (
+ NodeGroupEdit,
+ NodeGroupInterfaceForm,
+ )
+from maasserver.models import (
+ NodeGroup,
+ NodeGroupInterface,
+ )
+
+
+class ClusterEdit(UpdateView):
+ model = NodeGroup
+ template_name = 'maasserver/nodegroup_edit.html'
+ form_class = NodeGroupEdit
+ context_object_name = 'cluster'
+
+ def get_context_data(self, **kwargs):
+ context = super(ClusterEdit, self).get_context_data(**kwargs)
+ context['interfaces'] = (
+ self.object.nodegroupinterface_set.all().order_by('interface'))
+ return context
+
+ def get_success_url(self):
+ return reverse('settings')
+
+ def get_object(self):
+ uuid = self.kwargs.get('uuid', None)
+ return get_object_or_404(NodeGroup, uuid=uuid)
+
+ def form_valid(self, form):
+ messages.info(self.request, "Cluster updated.")
+ return super(ClusterEdit, self).form_valid(form)
+
+
+class ClusterDelete(DeleteView):
+
+ template_name = 'maasserver/nodegroup_confirm_delete.html'
+ context_object_name = 'cluster_to_delete'
+
+ def get_object(self):
+ uuid = self.kwargs.get('uuid', None)
+ return get_object_or_404(NodeGroup, uuid=uuid)
+
+ def get_next_url(self):
+ return reverse('settings')
+
+ def delete(self, request, *args, **kwargs):
+ cluster = self.get_object()
+ cluster.delete()
+ messages.info(request, "Cluster %s deleted." % cluster.cluster_name)
+ return HttpResponseRedirect(self.get_next_url())
+
+
+class ClusterInterfaceDelete(DeleteView):
+ template_name = 'maasserver/nodegroupinterface_confirm_delete.html'
+ context_object_name = 'interface_to_delete'
+
+ def get_object(self):
+ uuid = self.kwargs.get('uuid', None)
+ interface = self.kwargs.get('interface', None)
+ return get_object_or_404(
+ NodeGroupInterface, nodegroup__uuid=uuid, interface=interface)
+
+ def get_next_url(self):
+ uuid = self.kwargs.get('uuid', None)
+ return reverse('cluster-edit', args=[uuid])
+
+ def delete(self, request, *args, **kwargs):
+ interface = self.get_object()
+ interface.delete()
+ messages.info(request, "Interface %s deleted." % interface.interface)
+ return HttpResponseRedirect(self.get_next_url())
+
+
+class ClusterInterfaceEdit(UpdateView):
+ template_name = 'maasserver/nodegroupinterface_edit.html'
+ form_class = NodeGroupInterfaceForm
+ context_object_name = 'interface'
+
+ def get_success_url(self):
+ uuid = self.kwargs.get('uuid', None)
+ return reverse('cluster-edit', args=[uuid])
+
+ def form_valid(self, form):
+ messages.info(self.request, "Interface updated.")
+ return super(ClusterInterfaceEdit, self).form_valid(form)
+
+ def get_object(self):
+ uuid = self.kwargs.get('uuid', None)
+ interface = self.kwargs.get('interface', None)
+ return get_object_or_404(
+ NodeGroupInterface, nodegroup__uuid=uuid, interface=interface)