From 5816bf02048fe9163ed61e318579f78af4f11a82 Mon Sep 17 00:00:00 2001
From: Bach Dániel <bd@ik.bme.hu>
Date: Sat, 18 Oct 2014 18:34:34 +0200
Subject: [PATCH] dashboard: add RemovePortOperation

---
 circle/dashboard/forms.py                                   | 31 ++++++++++++++++++++++++++++++-
 circle/dashboard/templates/dashboard/vm-detail/network.html |  4 ++--
 circle/dashboard/views/vm.py                                | 29 +++++++++++++++++++++++++++++
 circle/firewall/models.py                                   |  9 ++++++++-
 circle/vm/operations.py                                     | 19 +++++++++++++++++++
 5 files changed, 88 insertions(+), 4 deletions(-)

diff --git a/circle/dashboard/forms.py b/circle/dashboard/forms.py
index 4000354..e524474 100644
--- a/circle/dashboard/forms.py
+++ b/circle/dashboard/forms.py
@@ -40,7 +40,7 @@ from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm
 from django.forms.widgets import TextInput, HiddenInput
 from django.template import Context
 from django.template.loader import render_to_string
-from django.utils.html import escape
+from django.utils.html import escape, format_html
 from django.utils.translation import ugettext_lazy as _
 from sizefield.widgets import FileSizeWidget
 from django.core.urlresolvers import reverse_lazy
@@ -935,6 +935,35 @@ class VmDeployForm(OperationForm):
                     "(blank allows scheduling automatically).")))
 
 
+class VmPortRemoveForm(OperationForm):
+    def __init__(self, *args, **kwargs):
+        choices = kwargs.pop('choices')
+        self.rule = kwargs.pop('default')
+
+        super(VmPortRemoveForm, self).__init__(*args, **kwargs)
+
+        self.fields.insert(0, 'rule', forms.ModelChoiceField(
+            queryset=choices, initial=self.rule, required=True,
+            empty_label=None, label=_('Port')))
+        if self.rule:
+            self.fields['rule'].widget = HiddenInput()
+
+    @property
+    def helper(self):
+        helper = super(VmPortRemoveForm, self).helper
+        if self.rule:
+            helper.layout = Layout(
+                AnyTag(
+                    "div",
+                    HTML(format_html(_("<label>Port:</label> {0}/{1}"),
+                        escape(self.rule.dport), escape(self.rule.proto))),
+                    css_class="form-group",
+                ),
+                Field("rule"),
+            )
+        return helper
+
+
 class CircleAuthenticationForm(AuthenticationForm):
     # fields: username, password
 
diff --git a/circle/dashboard/templates/dashboard/vm-detail/network.html b/circle/dashboard/templates/dashboard/vm-detail/network.html
index 9e49384..d4d63b8 100644
--- a/circle/dashboard/templates/dashboard/vm-detail/network.html
+++ b/circle/dashboard/templates/dashboard/vm-detail/network.html
@@ -78,7 +78,7 @@
                         {{ l.private }}/{{ l.proto }}
                       </td>
                       <td>
-                        <a href="{% url "dashboard.views.remove-port" pk=instance.pk rule=l.ipv4.pk %}" class="btn btn-link btn-xs vm-details-remove-port" data-rule="{{ l.ipv4.pk }}" title="{% trans "Remove" %}"><i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i></a>
+                        <a href="{{ op.remove_port.get_url }}?rule={{ l.ipv4.pk }}" class="btn btn-link btn-xs vm-details-remove-port" data-rule="{{ l.ipv4.pk }}" title="{% trans "Remove" %}"><i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i></a>
                       </td>
                     </tr>
                   {% endif %}
@@ -110,7 +110,7 @@
                         {{ l.private }}/{{ l.proto }}
                       </td>
                       <td>
-                        <a href="{% url "dashboard.views.remove-port" pk=instance.pk rule=l.ipv4.pk %}" class="btn btn-link btn-xs vm-details-remove-port" data-rule="{{ l.ipv6.pk }}" title="{% trans "Remove" %}"><i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i></a>
+                        <a href="{{ op.remove_port.get_url }}?rule={{ l.ipv4.pk }}" class="btn btn-link btn-xs vm-details-remove-port" data-rule="{{ l.ipv6.pk }}" title="{% trans "Remove" %}"><i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i></a>
                       </td>
                     </tr>
                   {% endif %}
diff --git a/circle/dashboard/views/vm.py b/circle/dashboard/views/vm.py
index bbfa2cb..24ac55d 100644
--- a/circle/dashboard/views/vm.py
+++ b/circle/dashboard/views/vm.py
@@ -62,6 +62,7 @@ from ..forms import (
     VmRenewForm, VmStateChangeForm, VmListSearchForm, VmCustomizeForm,
     TransferOwnershipForm, VmDiskResizeForm, RedeployForm, VmDiskRemoveForm,
     VmMigrateForm, VmDeployForm,
+    VmPortRemoveForm,
 )
 from ..models import Favourite, Profile
 
@@ -450,6 +451,33 @@ class VmMigrateView(FormOperationMixin, VmOperationView):
         return val
 
 
+class VmPortRemoveView(FormOperationMixin, VmOperationView):
+
+    op = 'remove_port'
+    show_in_toolbar = False
+    with_reload = True
+    icon = 'times'
+    effect = "danger"
+    form_class = VmPortRemoveForm
+
+    def get_form_kwargs(self):
+        instance = self.get_op().instance
+        choices = Rule.portforwards().filter(
+            host__interface__instance=instance)
+        rule_pk = self.request.GET.get('rule')
+        if rule_pk:
+            try:
+                default = choices.get(pk=rule_pk)
+            except (ValueError, Rule.DoesNotExist):
+                raise Http404()
+        else:
+            default = None
+
+        val = super(VmPortRemoveView, self).get_form_kwargs()
+        val.update({'choices': choices, 'default': default})
+        return val
+
+
 class VmSaveView(FormOperationMixin, VmOperationView):
 
     op = 'save_as_template'
@@ -683,6 +711,7 @@ vm_ops = OrderedDict([
         op='remove_disk', form_class=VmDiskRemoveForm,
         icon='times', effect="danger")),
     ('add_interface', VmAddInterfaceView),
+    ('remove_port', VmPortRemoveView),
     ('renew', VmRenewView),
     ('resources_change', VmResourcesChangeView),
     ('password_reset', VmOperationView.factory(
diff --git a/circle/firewall/models.py b/circle/firewall/models.py
index cfa4a99..2a7e6ba 100644
--- a/circle/firewall/models.py
+++ b/circle/firewall/models.py
@@ -243,6 +243,13 @@ class Rule(models.Model):
 
         return retval
 
+    @classmethod
+    def portforwards(cls, host=None):
+        qs = cls.objects.filter(dport__isnull=False, direction='in')
+        if host is not None:
+            qs = qs.filter(host=host)
+        return qs
+
     class Meta:
         verbose_name = _("rule")
         verbose_name_plural = _("rules")
@@ -762,7 +769,7 @@ class Host(models.Model):
         Return a list of ports with forwarding rules set.
         """
         retval = []
-        for rule in self.rules.filter(dport__isnull=False, direction='in'):
+        for rule in Rule.portforwards(host=self):
             forward = {
                 'proto': rule.proto,
                 'private': rule.dport,
diff --git a/circle/vm/operations.py b/circle/vm/operations.py
index cfad163..9db81b6 100644
--- a/circle/vm/operations.py
+++ b/circle/vm/operations.py
@@ -606,6 +606,25 @@ class RemoveInterfaceOperation(InstanceOperation):
 
 
 @register_operation
+class RemovePortOperation(InstanceOperation):
+    id = 'remove_port'
+    name = _("close port")
+    description = _("Close the specified port.")
+    concurrency_check = False
+    required_perms = ()
+    accept_states = ()
+
+    def _operation(self, activity, rule):
+        interface = rule.host.interface_set.get()
+        if interface.instance != self.instance:
+            raise PermissionDenied()
+        activity.readable_name = create_readable(
+            ugettext_noop("close %(proto)s/%(port)d on %(host)s"),
+            proto=rule.proto, port=rule.dport, host=rule.host)
+        rule.delete()
+
+
+@register_operation
 class RemoveDiskOperation(InstanceOperation):
     id = 'remove_disk'
     name = _("remove disk")
--
libgit2 0.26.0