Commit 72e1a7ba by Bach Dániel

Merge branch 'feature-port-operations' into 'master'

Feature port operations

👌

See merge request !256
parents 833d5490 fc553851
...@@ -40,7 +40,7 @@ from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm ...@@ -40,7 +40,7 @@ from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm
from django.forms.widgets import TextInput, HiddenInput from django.forms.widgets import TextInput, HiddenInput
from django.template import Context from django.template import Context
from django.template.loader import render_to_string 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 django.utils.translation import ugettext_lazy as _
from sizefield.widgets import FileSizeWidget from sizefield.widgets import FileSizeWidget
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
...@@ -935,6 +935,56 @@ class VmDeployForm(OperationForm): ...@@ -935,6 +935,56 @@ class VmDeployForm(OperationForm):
"(blank allows scheduling automatically)."))) "(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()
class VmPortAddForm(OperationForm):
port = forms.IntegerField(required=True, label=_('Port'),
min_value=1, max_value=65535)
proto = forms.ChoiceField((('tcp', 'tcp'), ('udp', 'udp')),
required=True, label=_('Protocol'))
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
self.host = kwargs.pop('default')
super(VmPortAddForm, self).__init__(*args, **kwargs)
self.fields.insert(0, 'host', forms.ModelChoiceField(
queryset=choices, initial=self.host, required=True,
empty_label=None, label=_('Host')))
if self.host:
self.fields['host'].widget = HiddenInput()
@property
def helper(self):
helper = super(VmPortAddForm, self).helper
if self.host:
helper.layout = Layout(
AnyTag(
"div",
HTML(format_html(
_("<label>Host:</label> {0}"), self.host)),
css_class="form-group",
),
Field("host"),
Field("proto"),
Field("port"),
)
return helper
class CircleAuthenticationForm(AuthenticationForm): class CircleAuthenticationForm(AuthenticationForm):
# fields: username, password # fields: username, password
......
{% extends "dashboard/operate.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block formfields %}
{% if form %}
{% crispy form %}
{% endif %}
{% if form.fields.rule.initial != None %}
{% with rule=form.fields.rule.initial %}
<dl>
<dt>{% trans "Port" %}:</dt>
<dd>{{ rule.dport }}/{{ rule.proto }}</dd>
<dt>{% trans "Host" %}:</dt>
<dd>{{ rule.host.hostname }}</dd>
<dt>{% trans "Vlan" %}:</dt>
<dd>{{ rule.host.vlan.name }}</dd>
</dl>
{% endwith %}
{% endif %}
{% endblock %}
{% load i18n %} {% load i18n %}
<div class="vm-details-network-port-add pull-right"> <div class="vm-details-network-port-add pull-right">
<form action="" method="POST"> <form action="{{ op.add_port.get_url }}" method="POST">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="host_pk" value="{{ i.host.pk }}"/> <input type="hidden" name="host" value="{{ i.host.pk }}"/>
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<span class="input-group-addon"> <span class="input-group-addon">
<i class="fa fa-plus"></i> <i class="fa fa-long-arrow-right"></i> <i class="fa fa-plus"></i> <i class="fa fa-long-arrow-right"></i>
</span> </span>
<input type="text" class="form-control" size="5" style="width: 80px;" name="port"/> <input type="number" class="form-control" size="5" min="1" max="65535"
style="width: 80px;" name="port" required/>
<span class="input-group-addon">/</span> <span class="input-group-addon">/</span>
<select class="form-control" name="proto" style="width: 70px;"><option>tcp</option><option>udp</option></select> <select class="form-control" name="proto" style="width: 70px;"><option>tcp</option><option>udp</option></select>
<div class="input-group-btn"> <div class="input-group-btn">
......
...@@ -78,7 +78,7 @@ ...@@ -78,7 +78,7 @@
{{ l.private }}/{{ l.proto }} {{ l.private }}/{{ l.proto }}
</td> </td>
<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> </td>
</tr> </tr>
{% endif %} {% endif %}
...@@ -110,7 +110,7 @@ ...@@ -110,7 +110,7 @@
{{ l.private }}/{{ l.proto }} {{ l.private }}/{{ l.proto }}
</td> </td>
<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> </td>
</tr> </tr>
{% endif %} {% endif %}
......
...@@ -26,7 +26,8 @@ from django.contrib.auth import authenticate ...@@ -26,7 +26,8 @@ from django.contrib.auth import authenticate
from dashboard.views import VmAddInterfaceView from dashboard.views import VmAddInterfaceView
from vm.models import Instance, InstanceTemplate, Lease, Node, Trait from vm.models import Instance, InstanceTemplate, Lease, Node, Trait
from vm.operations import WakeUpOperation, AddInterfaceOperation from vm.operations import (WakeUpOperation, AddInterfaceOperation,
AddPortOperation)
from ..models import Profile from ..models import Profile
from firewall.models import Vlan, Host, VlanGroup from firewall.models import Vlan, Host, VlanGroup
from mock import Mock, patch from mock import Mock, patch
...@@ -335,51 +336,48 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -335,51 +336,48 @@ class VmDetailTest(LoginMixin, TestCase):
self.login(c, "user2") self.login(c, "user2")
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner') inst.set_level(self.u2, 'owner')
response = c.post("/dashboard/vm/1/", {'port': True, vlan = Vlan.objects.get(id=1)
'proto': 'tcp', vlan.set_level(self.u2, 'user')
'port': '1337'}) inst.add_interface(user=self.u2, vlan=vlan)
host = Host.objects.get(
interface__in=inst.interface_set.all())
with patch.object(AddPortOperation, 'async') as mock_method:
mock_method.side_effect = inst.add_port
response = c.post("/dashboard/vm/1/op/add_port/", {
'proto': 'tcp', 'host': host.pk, 'port': '1337'})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_unpermitted_add_port_wo_obj_levels(self): def test_unpermitted_add_port_wo_obj_levels(self):
c = Client() c = Client()
self.login(c, "user2") self.login(c, "user2")
self.u2.user_permissions.add(Permission.objects.get(
name='Can configure port forwards.'))
response = c.post("/dashboard/vm/1/", {'port': True,
'proto': 'tcp',
'port': '1337'})
self.assertEqual(response.status_code, 403)
def test_unpermitted_add_port_w_bad_host(self):
c = Client()
self.login(c, "user2")
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner') vlan = Vlan.objects.get(id=1)
vlan.set_level(self.u2, 'user')
inst.add_interface(user=self.u2, vlan=vlan, system=True)
host = Host.objects.get(
interface__in=inst.interface_set.all())
self.u2.user_permissions.add(Permission.objects.get( self.u2.user_permissions.add(Permission.objects.get(
name='Can configure port forwards.')) name='Can configure port forwards.'))
response = c.post("/dashboard/vm/1/", {'proto': 'tcp', with patch.object(AddPortOperation, 'async') as mock_method:
'host_pk': '9999', mock_method.side_effect = inst.add_port
'port': '1337'}) response = c.post("/dashboard/vm/1/op/add_port/", {
'proto': 'tcp', 'host': host.pk, 'port': '1337'})
assert not mock_method.called
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_permitted_add_port_w_unhandled_exception(self): def test_unpermitted_add_port_w_bad_host(self):
c = Client() c = Client()
self.login(c, "user2") self.login(c, "user2")
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner') inst.set_level(self.u2, 'owner')
vlan = Vlan.objects.get(id=1)
vlan.set_level(self.u2, 'user')
inst.add_interface(user=self.u2, vlan=vlan)
host = Host.objects.get(
interface__in=inst.interface_set.all())
self.u2.user_permissions.add(Permission.objects.get( self.u2.user_permissions.add(Permission.objects.get(
name='Can configure port forwards.')) name='Can configure port forwards.'))
port_count = len(host.list_ports()) with patch.object(AddPortOperation, 'async') as mock_method:
response = c.post("/dashboard/vm/1/", {'proto': 'tcp', mock_method.side_effect = inst.add_port
'host_pk': host.pk, response = c.post("/dashboard/vm/1/op/add_port/", {
'port': 'invalid_port'}) 'proto': 'tcp', 'host': '9999', 'port': '1337'})
self.assertEqual(response.status_code, 302) assert not mock_method.called
self.assertEqual(len(host.list_ports()), port_count) self.assertEqual(response.status_code, 200)
def test_permitted_add_port(self): def test_permitted_add_port(self):
c = Client() c = Client()
...@@ -394,9 +392,11 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -394,9 +392,11 @@ class VmDetailTest(LoginMixin, TestCase):
self.u2.user_permissions.add(Permission.objects.get( self.u2.user_permissions.add(Permission.objects.get(
name='Can configure port forwards.')) name='Can configure port forwards.'))
port_count = len(host.list_ports()) port_count = len(host.list_ports())
response = c.post("/dashboard/vm/1/", {'proto': 'tcp', with patch.object(AddPortOperation, 'async') as mock_method:
'host_pk': host.pk, mock_method.side_effect = inst.add_port
'port': '1337'}) response = c.post("/dashboard/vm/1/op/add_port/", {
'proto': 'tcp', 'host': host.pk, 'port': '1337'})
assert mock_method.called
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(len(host.list_ports()), port_count + 1) self.assertEqual(len(host.list_ports()), port_count + 1)
......
...@@ -26,7 +26,7 @@ from .views import ( ...@@ -26,7 +26,7 @@ from .views import (
InstanceActivityDetail, LeaseCreate, LeaseDelete, LeaseDetail, InstanceActivityDetail, LeaseCreate, LeaseDelete, LeaseDetail,
MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete, MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete,
NodeDetailView, NodeList, NodeStatus, NodeDetailView, NodeList, NodeStatus,
NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate, NotificationView, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TemplateDelete, TemplateDetail, TemplateList,
vm_activity, VmCreate, VmDetailView, vm_activity, VmCreate, VmDetailView,
VmDetailVncTokenView, VmList, VmDetailVncTokenView, VmList,
...@@ -82,8 +82,6 @@ urlpatterns = patterns( ...@@ -82,8 +82,6 @@ urlpatterns = patterns(
name="dashboard.views.template-delete"), name="dashboard.views.template-delete"),
url(r'^template/(?P<pk>\d+)/tx/$', TransferTemplateOwnershipView.as_view(), url(r'^template/(?P<pk>\d+)/tx/$', TransferTemplateOwnershipView.as_view(),
name='dashboard.views.template-transfer-ownership'), name='dashboard.views.template-transfer-ownership'),
url(r'^vm/(?P<pk>\d+)/remove_port/(?P<rule>\d+)/$', PortDelete.as_view(),
name='dashboard.views.remove-port'),
url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(), url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(),
name='dashboard.views.detail'), name='dashboard.views.detail'),
url(r'^vm/(?P<pk>\d+)/vnctoken/$', VmDetailVncTokenView.as_view(), url(r'^vm/(?P<pk>\d+)/vnctoken/$', VmDetailVncTokenView.as_view(),
......
...@@ -63,6 +63,7 @@ from ..forms import ( ...@@ -63,6 +63,7 @@ from ..forms import (
VmRenewForm, VmStateChangeForm, VmListSearchForm, VmCustomizeForm, VmRenewForm, VmStateChangeForm, VmListSearchForm, VmCustomizeForm,
VmDiskResizeForm, RedeployForm, VmDiskRemoveForm, VmDiskResizeForm, RedeployForm, VmDiskRemoveForm,
VmMigrateForm, VmDeployForm, VmMigrateForm, VmDeployForm,
VmPortRemoveForm, VmPortAddForm,
) )
from ..models import Favourite from ..models import Favourite
...@@ -175,7 +176,6 @@ class VmDetailView(GraphMixin, CheckedDetailView): ...@@ -175,7 +176,6 @@ class VmDetailView(GraphMixin, CheckedDetailView):
'new_description': self.__set_description, 'new_description': self.__set_description,
'new_tag': self.__add_tag, 'new_tag': self.__add_tag,
'to_remove': self.__remove_tag, 'to_remove': self.__remove_tag,
'port': self.__add_port,
'abort_operation': self.__abort_operation, 'abort_operation': self.__abort_operation,
} }
for k, v in options.iteritems(): for k, v in options.iteritems():
...@@ -271,40 +271,6 @@ class VmDetailView(GraphMixin, CheckedDetailView): ...@@ -271,40 +271,6 @@ class VmDetailView(GraphMixin, CheckedDetailView):
return redirect(reverse_lazy("dashboard.views.detail", return redirect(reverse_lazy("dashboard.views.detail",
kwargs={'pk': self.object.pk})) kwargs={'pk': self.object.pk}))
def __add_port(self, request):
object = self.get_object()
if not (object.has_level(request.user, "operator") and
request.user.has_perm('vm.config_ports')):
raise PermissionDenied()
port = request.POST.get("port")
proto = request.POST.get("proto")
try:
error = None
interfaces = object.interface_set.all()
host = Host.objects.get(pk=request.POST.get("host_pk"),
interface__in=interfaces)
host.add_port(proto, private=port)
except Host.DoesNotExist:
logger.error('Tried to add port to nonexistent host %d. User: %s. '
'Instance: %s', request.POST.get("host_pk"),
unicode(request.user), object)
raise PermissionDenied()
except ValueError:
error = _("There is a problem with your input.")
except Exception as e:
error = _("Unknown error.")
logger.error(e)
if request.is_ajax():
pass
else:
if error:
messages.error(request, error)
return redirect(reverse_lazy("dashboard.views.detail",
kwargs={'pk': self.get_object().pk}))
def __abort_operation(self, request): def __abort_operation(self, request):
self.object = self.get_object() self.object = self.get_object()
...@@ -451,6 +417,62 @@ class VmMigrateView(FormOperationMixin, VmOperationView): ...@@ -451,6 +417,62 @@ class VmMigrateView(FormOperationMixin, VmOperationView):
return val return val
class VmPortRemoveView(FormOperationMixin, VmOperationView):
template_name = 'dashboard/_vm-remove-port.html'
op = 'remove_port'
show_in_toolbar = False
with_reload = True
wait_for_result = 0.5
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 VmPortAddView(FormOperationMixin, VmOperationView):
op = 'add_port'
show_in_toolbar = False
with_reload = True
wait_for_result = 0.5
icon = 'plus'
effect = "success"
form_class = VmPortAddForm
def get_form_kwargs(self):
instance = self.get_op().instance
choices = Host.objects.filter(interface__instance=instance)
host_pk = self.request.GET.get('host')
if host_pk:
try:
default = choices.get(pk=host_pk)
except (ValueError, Host.DoesNotExist):
raise Http404()
else:
default = None
val = super(VmPortAddView, self).get_form_kwargs()
val.update({'choices': choices, 'default': default})
return val
class VmSaveView(FormOperationMixin, VmOperationView): class VmSaveView(FormOperationMixin, VmOperationView):
op = 'save_as_template' op = 'save_as_template'
...@@ -684,6 +706,8 @@ vm_ops = OrderedDict([ ...@@ -684,6 +706,8 @@ vm_ops = OrderedDict([
op='remove_disk', form_class=VmDiskRemoveForm, op='remove_disk', form_class=VmDiskRemoveForm,
icon='times', effect="danger")), icon='times', effect="danger")),
('add_interface', VmAddInterfaceView), ('add_interface', VmAddInterfaceView),
('remove_port', VmPortRemoveView),
('add_port', VmPortAddView),
('renew', VmRenewView), ('renew', VmRenewView),
('resources_change', VmResourcesChangeView), ('resources_change', VmResourcesChangeView),
('password_reset', VmOperationView.factory( ('password_reset', VmOperationView.factory(
...@@ -1167,52 +1191,6 @@ def get_disk_download_status(request, pk): ...@@ -1167,52 +1191,6 @@ def get_disk_download_status(request, pk):
) )
class PortDelete(LoginRequiredMixin, DeleteView):
model = Rule
pk_url_kwarg = 'rule'
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def get_context_data(self, **kwargs):
context = super(PortDelete, self).get_context_data(**kwargs)
rule = kwargs.get('object')
instance = rule.host.interface_set.get().instance
context['title'] = _("Port delete confirmation")
context['text'] = _("Are you sure you want to close %(port)d/"
"%(proto)s on %(vm)s?" % {'port': rule.dport,
'proto': rule.proto,
'vm': instance})
return context
def delete(self, request, *args, **kwargs):
rule = Rule.objects.get(pk=kwargs.get("rule"))
instance = rule.host.interface_set.get().instance
if not instance.has_level(request.user, 'owner'):
raise PermissionDenied()
super(PortDelete, self).delete(request, *args, **kwargs)
success_url = self.get_success_url()
success_message = _("Port successfully removed.")
if request.is_ajax():
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect("%s#network" % success_url)
def get_success_url(self):
return reverse_lazy('dashboard.views.detail',
kwargs={'pk': self.kwargs.get("pk")})
class ClientCheck(LoginRequiredMixin, TemplateView): class ClientCheck(LoginRequiredMixin, TemplateView):
def get_template_names(self): def get_template_names(self):
......
...@@ -34,6 +34,10 @@ reverse_domain_re = re.compile(r'^(%\([abcd]\)d|[a-z0-9.-])+$') ...@@ -34,6 +34,10 @@ reverse_domain_re = re.compile(r'^(%\([abcd]\)d|[a-z0-9.-])+$')
ipv6_template_re = re.compile(r'^(%\([abcd]\)[dxX]|[A-Za-z0-9:-])+$') ipv6_template_re = re.compile(r'^(%\([abcd]\)[dxX]|[A-Za-z0-9:-])+$')
class mac_custom(mac_unix):
word_fmt = '%.2X'
class MACAddressFormField(forms.Field): class MACAddressFormField(forms.Field):
default_error_messages = { default_error_messages = {
'invalid': _(u'Enter a valid MAC address. %s'), 'invalid': _(u'Enter a valid MAC address. %s'),
...@@ -51,9 +55,6 @@ class MACAddressField(models.Field): ...@@ -51,9 +55,6 @@ class MACAddressField(models.Field):
description = _('MAC Address object') description = _('MAC Address object')
__metaclass__ = models.SubfieldBase __metaclass__ = models.SubfieldBase
class mac_custom(mac_unix):
word_fmt = '%.2X'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs['max_length'] = 17 kwargs['max_length'] = 17
super(MACAddressField, self).__init__(*args, **kwargs) super(MACAddressField, self).__init__(*args, **kwargs)
...@@ -65,7 +66,7 @@ class MACAddressField(models.Field): ...@@ -65,7 +66,7 @@ class MACAddressField(models.Field):
if isinstance(value, EUI): if isinstance(value, EUI):
return value return value
return EUI(value, dialect=MACAddressField.mac_custom) return EUI(value, dialect=mac_custom)
def get_internal_type(self): def get_internal_type(self):
return 'CharField' return 'CharField'
......
...@@ -243,6 +243,13 @@ class Rule(models.Model): ...@@ -243,6 +243,13 @@ class Rule(models.Model):
return retval 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: class Meta:
verbose_name = _("rule") verbose_name = _("rule")
verbose_name_plural = _("rules") verbose_name_plural = _("rules")
...@@ -762,7 +769,7 @@ class Host(models.Model): ...@@ -762,7 +769,7 @@ class Host(models.Model):
Return a list of ports with forwarding rules set. Return a list of ports with forwarding rules set.
""" """
retval = [] retval = []
for rule in self.rules.filter(dport__isnull=False, direction='in'): for rule in Rule.portforwards(host=self):
forward = { forward = {
'proto': rule.proto, 'proto': rule.proto,
'private': rule.dport, 'private': rule.dport,
......
...@@ -817,7 +817,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -817,7 +817,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
return acts return acts
def get_merged_activities(self, user=None): def get_merged_activities(self, user=None):
whitelist = ("create_disk", "download_disk") whitelist = ("create_disk", "download_disk", "add_port", "remove_port")
acts = self.get_activities(user) acts = self.get_activities(user)
merged_acts = [] merged_acts = []
latest = None latest = None
......
...@@ -27,7 +27,7 @@ from tarfile import TarFile, TarInfo ...@@ -27,7 +27,7 @@ from tarfile import TarFile, TarInfo
import time import time
from urlparse import urlsplit from urlparse import urlsplit
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, ugettext_noop from django.utils.translation import ugettext_lazy as _, ugettext_noop
from django.conf import settings from django.conf import settings
...@@ -606,6 +606,41 @@ class RemoveInterfaceOperation(InstanceOperation): ...@@ -606,6 +606,41 @@ class RemoveInterfaceOperation(InstanceOperation):
@register_operation @register_operation
class RemovePortOperation(InstanceOperation):
id = 'remove_port'
name = _("close port")
description = _("Close the specified port.")
concurrency_check = False
required_perms = ('vm.config_ports', )
def _operation(self, activity, rule):
interface = rule.host.interface_set.get()
if interface.instance != self.instance:
raise SuspiciousOperation()
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 AddPortOperation(InstanceOperation):
id = 'add_port'
name = _("open port")
description = _("Open the specified port.")
concurrency_check = False
required_perms = ('vm.config_ports', )
def _operation(self, activity, host, proto, port):
if host.interface_set.get().instance != self.instance:
raise SuspiciousOperation()
host.add_port(proto, private=port)
activity.readable_name = create_readable(
ugettext_noop("open %(proto)s/%(port)d on %(host)s"),
proto=proto, port=port, host=host)
@register_operation
class RemoveDiskOperation(InstanceOperation): class RemoveDiskOperation(InstanceOperation):
id = 'remove_disk' id = 'remove_disk'
name = _("remove disk") name = _("remove disk")
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment