summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.openstack.org>2018-04-19 11:56:31 +0000
committerGerrit Code Review <review@openstack.org>2018-04-19 11:56:31 +0000
commit0e0db289694fe4c1f0d490e4e6ebf2b8275b4657 (patch)
tree077c7cd9e54372c0216b3e1a0ca5a3b321ed8435
parenteba5b0f88d7a62c71a2784753a56cfc5e88ac18d (diff)
parentdd0eba2128a892bddef563b09e4a28122c50f458 (diff)
Merge "Support simple FIP disassociation (with FIP release)"14.0.0.0b1
-rw-r--r--doc/source/configuration/settings.rst25
-rw-r--r--horizon/conf/default.py3
-rw-r--r--openstack_dashboard/dashboards/project/instances/forms.py52
-rw-r--r--openstack_dashboard/dashboards/project/instances/tables.py45
-rw-r--r--openstack_dashboard/dashboards/project/instances/templates/instances/_disassociate.html28
-rw-r--r--openstack_dashboard/dashboards/project/instances/templates/instances/disassociate.html7
-rw-r--r--openstack_dashboard/dashboards/project/instances/tests.py48
-rw-r--r--openstack_dashboard/dashboards/project/instances/urls.py2
-rw-r--r--openstack_dashboard/dashboards/project/instances/views.py16
-rw-r--r--openstack_dashboard/local/local_settings.py.example4
-rw-r--r--releasenotes/notes/bug-1226003-drop-simple-fip-disassociation-3c751297b467597e.yaml12
11 files changed, 149 insertions, 93 deletions
diff --git a/doc/source/configuration/settings.rst b/doc/source/configuration/settings.rst
index 0596f0d..c09e2e2 100644
--- a/doc/source/configuration/settings.rst
+++ b/doc/source/configuration/settings.rst
@@ -428,31 +428,6 @@ there are any.
428This setting allows you to set rules for passwords if your organization 428This setting allows you to set rules for passwords if your organization
429requires them. 429requires them.
430 430
431simple_ip_management
432~~~~~~~~~~~~~~~~~~~~
433
434.. versionadded:: 2013.1(Grizzly)
435
436Default: ``True``
437
438Enable or disable simplified floating IP address management.
439
440"Simple" floating IP address management means that the user does not ever have
441to select the specific IP addresses they wish to use, and the process of
442allocating an IP and assigning it to an instance is one-click.
443
444The "advanced" floating IP management allows users to select the floating IP
445pool from which the IP should be allocated and to select a specific IP address
446when associating one with an instance.
447
448.. note::
449
450 Currently "simple" floating IP address management is not compatible with
451 Neutron. There are two reasons for this. First, Neutron does not support
452 the default floating IP pool at the moment. Second, a Neutron floating IP
453 can be associated with each VIF and we need to check whether there is only
454 one VIF for an instance to enable simple association support.
455
456user_home 431user_home
457~~~~~~~~~ 432~~~~~~~~~
458 433
diff --git a/horizon/conf/default.py b/horizon/conf/default.py
index e6828dd..f740d53 100644
--- a/horizon/conf/default.py
+++ b/horizon/conf/default.py
@@ -45,9 +45,6 @@ HORIZON_CONFIG = {
45 45
46 'password_autocomplete': 'off', 46 'password_autocomplete': 'off',
47 47
48 # Enable or disable simplified floating IP address management.
49 'simple_ip_management': True,
50
51 'integration_tests_support': 48 'integration_tests_support':
52 getattr(settings, 'INTEGRATION_TESTS_SUPPORT', False) 49 getattr(settings, 'INTEGRATION_TESTS_SUPPORT', False)
53} 50}
diff --git a/openstack_dashboard/dashboards/project/instances/forms.py b/openstack_dashboard/dashboards/project/instances/forms.py
index d73e14b..6ba71ca 100644
--- a/openstack_dashboard/dashboards/project/instances/forms.py
+++ b/openstack_dashboard/dashboards/project/instances/forms.py
@@ -15,6 +15,7 @@
15 15
16from django.template.defaultfilters import filesizeformat 16from django.template.defaultfilters import filesizeformat
17from django.urls import reverse 17from django.urls import reverse
18from django.urls import reverse_lazy
18from django.utils.translation import ugettext_lazy as _ 19from django.utils.translation import ugettext_lazy as _
19from django.views.decorators.debug import sensitive_variables 20from django.views.decorators.debug import sensitive_variables
20 21
@@ -418,3 +419,54 @@ class DetachInterface(forms.SelfHandlingForm):
418 exceptions.handle(request, _("Unable to detach interface."), 419 exceptions.handle(request, _("Unable to detach interface."),
419 redirect=redirect) 420 redirect=redirect)
420 return True 421 return True
422
423
424class Disassociate(forms.SelfHandlingForm):
425 fip = forms.ThemableChoiceField(label=_('Floating IP'))
426 is_release = forms.BooleanField(label=_('Release Floating IP'),
427 required=False)
428
429 def __init__(self, request, *args, **kwargs):
430 super(Disassociate, self).__init__(request, *args, **kwargs)
431 instance_id = self.initial['instance_id']
432 targets = api.neutron.floating_ip_target_list_by_instance(
433 request, instance_id)
434
435 target_ids = [t.port_id for t in targets]
436
437 self.fips = [fip for fip
438 in api.neutron.tenant_floating_ip_list(request)
439 if fip.port_id in target_ids]
440
441 fip_choices = [(fip.id, fip.ip) for fip in self.fips]
442 fip_choices.insert(0, ('', _('Select a floating IP to disassociate')))
443 self.fields['fip'].choices = fip_choices
444 self.fields['fip'].initial = self.fips[0].id
445
446 def handle(self, request, data):
447 redirect = reverse_lazy('horizon:project:instances:index')
448 fip_id = data['fip']
449 fips = [fip for fip in self.fips if fip.id == fip_id]
450 if not fips:
451 messages.error(request,
452 _("The specified floating IP no longer exists."),
453 redirect=redirect)
454 fip = fips[0]
455 try:
456 if data['is_release']:
457 api.neutron.tenant_floating_ip_release(request, fip_id)
458 messages.success(
459 request,
460 _("Successfully disassociated and released "
461 "floating IP %s") % fip.ip)
462 else:
463 api.neutron.floating_ip_disassociate(request, fip_id)
464 messages.success(
465 request,
466 _("Successfully disassociated floating IP %s") % fip.ip)
467 except Exception:
468 exceptions.handle(
469 request,
470 _('Unable to disassociate floating IP %s') % fip.ip,
471 redirect=redirect)
472 return True
diff --git a/openstack_dashboard/dashboards/project/instances/tables.py b/openstack_dashboard/dashboards/project/instances/tables.py
index cc76326..7de62a3 100644
--- a/openstack_dashboard/dashboards/project/instances/tables.py
+++ b/openstack_dashboard/dashboards/project/instances/tables.py
@@ -17,7 +17,6 @@ import logging
17 17
18from django.conf import settings 18from django.conf import settings
19from django.http import HttpResponse 19from django.http import HttpResponse
20from django import shortcuts
21from django import template 20from django import template
22from django.template.defaultfilters import title 21from django.template.defaultfilters import title
23from django import urls 22from django import urls
@@ -29,7 +28,6 @@ from django.utils.translation import string_concat
29from django.utils.translation import ugettext_lazy as _ 28from django.utils.translation import ugettext_lazy as _
30from django.utils.translation import ungettext_lazy 29from django.utils.translation import ungettext_lazy
31 30
32from horizon import conf
33from horizon import exceptions 31from horizon import exceptions
34from horizon import messages 32from horizon import messages
35from horizon import tables 33from horizon import tables
@@ -655,14 +653,11 @@ class AssociateIP(policy.PolicyTargetMixin, tables.LinkAction):
655 return "?".join([base_url, params]) 653 return "?".join([base_url, params])
656 654
657 655
658# TODO(amotoki): [drop-nova-network] The current SimpleDisassociateIP 656class DisassociateIP(tables.LinkAction):
659# just disassociates the first found FIP. It looks better to have a form
660# which allows to choose which FIP should be disassociated.
661# HORIZON_CONFIG['simple_ip_management'] can be dropped then.
662class SimpleDisassociateIP(policy.PolicyTargetMixin, tables.Action):
663 name = "disassociate" 657 name = "disassociate"
664 verbose_name = _("Disassociate Floating IP") 658 verbose_name = _("Disassociate Floating IP")
665 classes = ("btn-disassociate",) 659 url = "horizon:project:instances:disassociate"
660 classes = ("btn-disassociate", 'ajax-modal')
666 policy_rules = (("network", "update_floatingip"),) 661 policy_rules = (("network", "update_floatingip"),)
667 action_type = "danger" 662 action_type = "danger"
668 663
@@ -671,38 +666,12 @@ class SimpleDisassociateIP(policy.PolicyTargetMixin, tables.Action):
671 return False 666 return False
672 if not api.neutron.floating_ip_supported(request): 667 if not api.neutron.floating_ip_supported(request):
673 return False 668 return False
674 if not conf.HORIZON_CONFIG["simple_ip_management"]:
675 return False
676 for addresses in instance.addresses.values(): 669 for addresses in instance.addresses.values():
677 for address in addresses: 670 for address in addresses:
678 if address.get('OS-EXT-IPS:type') == "floating": 671 if address.get('OS-EXT-IPS:type') == "floating":
679 return not is_deleting(instance) 672 return not is_deleting(instance)
680 return False 673 return False
681 674
682 def single(self, table, request, instance_id):
683 try:
684 targets = api.neutron.floating_ip_target_list_by_instance(
685 request, instance_id)
686
687 target_ids = [t.port_id for t in targets]
688
689 fips = [fip for fip in api.neutron.tenant_floating_ip_list(request)
690 if fip.port_id in target_ids]
691 # Removing multiple floating IPs at once doesn't work, so this pops
692 # off the first one.
693 if fips:
694 fip = fips.pop()
695 api.neutron.floating_ip_disassociate(request, fip.id)
696 messages.success(request,
697 _("Successfully disassociated "
698 "floating IP: %s") % fip.ip)
699 else:
700 messages.info(request, _("No floating IPs to disassociate."))
701 except Exception:
702 exceptions.handle(request,
703 _("Unable to disassociate floating IP."))
704 return shortcuts.redirect(request.get_full_path())
705
706 675
707class UpdateMetadata(policy.PolicyTargetMixin, tables.LinkAction): 676class UpdateMetadata(policy.PolicyTargetMixin, tables.LinkAction):
708 name = "update_metadata" 677 name = "update_metadata"
@@ -1280,10 +1249,10 @@ class InstancesTable(tables.DataTable):
1280 table_actions = launch_actions + (DeleteInstance, 1249 table_actions = launch_actions + (DeleteInstance,
1281 InstancesFilterAction) 1250 InstancesFilterAction)
1282 row_actions = (StartInstance, ConfirmResize, RevertResize, 1251 row_actions = (StartInstance, ConfirmResize, RevertResize,
1283 CreateSnapshot, AssociateIP, 1252 CreateSnapshot, AssociateIP, DisassociateIP,
1284 SimpleDisassociateIP, AttachInterface, 1253 AttachInterface, DetachInterface, EditInstance,
1285 DetachInterface, EditInstance, AttachVolume, 1254 AttachVolume, DetachVolume,
1286 DetachVolume, UpdateMetadata, DecryptInstancePassword, 1255 UpdateMetadata, DecryptInstancePassword,
1287 EditInstanceSecurityGroups, ConsoleLink, LogLink, 1256 EditInstanceSecurityGroups, ConsoleLink, LogLink,
1288 TogglePause, ToggleSuspend, ToggleShelve, 1257 TogglePause, ToggleSuspend, ToggleShelve,
1289 ResizeLink, LockInstance, UnlockInstance, 1258 ResizeLink, LockInstance, UnlockInstance,
diff --git a/openstack_dashboard/dashboards/project/instances/templates/instances/_disassociate.html b/openstack_dashboard/dashboards/project/instances/templates/instances/_disassociate.html
new file mode 100644
index 0000000..63007e9
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/instances/templates/instances/_disassociate.html
@@ -0,0 +1,28 @@
1{% extends "horizon/common/_modal_form.html" %}
2{% load i18n %}
3
4{% block form_id %}disassociate_fip_form{% endblock %}
5{% block form_action %}{% url "horizon:project:instances:disassociate" instance_id %}{% endblock %}
6
7{% block modal_id %}disassocaite_fip_modal{% endblock %}
8{% block modal-header %}{% trans "Disassociate Floating IP" %}{% endblock %}
9
10{% block modal-body %}
11<div class="left">
12 <fieldset>
13 {% include "horizon/common/_form_fields.html" %}
14 </fieldset>
15</div>
16<div class="right">
17 <h3>{% trans "Description:" %}</h3>
18 <p>{% blocktrans trimmed %}
19 Select the floating IP to be disassociated from the instance.
20 {% endblocktrans %}</p>
21 <dl>
22 <dt>{% trans "Release Floating IP" %}</dt>
23 <dd>{% blocktrans trimmed %}
24 If checked, the selected floating IP will be released at the same time.
25 {% endblocktrans %}</dd>
26 </dl>
27</div>
28{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/instances/templates/instances/disassociate.html b/openstack_dashboard/dashboards/project/instances/templates/instances/disassociate.html
new file mode 100644
index 0000000..7338335
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/instances/templates/instances/disassociate.html
@@ -0,0 +1,7 @@
1{% extends "base.html" %}
2{% load i18n %}
3{% block title %}{% trans "Disassociate Floating IP" %}{% endblock %}
4
5{% block main %}
6 {% include "project/instances/_disassociate.html" %}
7{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py
index 1700e00..1e48b3f 100644
--- a/openstack_dashboard/dashboards/project/instances/tests.py
+++ b/openstack_dashboard/dashboards/project/instances/tests.py
@@ -4208,13 +4208,10 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
4208 @helpers.create_mocks({ 4208 @helpers.create_mocks({
4209 api.neutron: ('floating_ip_target_list_by_instance', 4209 api.neutron: ('floating_ip_target_list_by_instance',
4210 'tenant_floating_ip_list', 4210 'tenant_floating_ip_list',
4211 'floating_ip_disassociate',), 4211 'floating_ip_disassociate',
4212 api.network: ('servers_update_addresses',), 4212 'tenant_floating_ip_release'),
4213 api.glance: ('image_list_detailed',),
4214 api.nova: ('server_list',
4215 'flavor_list'),
4216 }) 4213 })
4217 def test_disassociate_floating_ip(self): 4214 def _test_disassociate_floating_ip(self, is_release):
4218 servers = self.servers.list() 4215 servers = self.servers.list()
4219 server = servers[0] 4216 server = servers[0]
4220 port = [p for p in self.ports.list() if p.device_id == server.id][0] 4217 port = [p for p in self.ports.list() if p.device_id == server.id][0]
@@ -4223,35 +4220,40 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
4223 fip = self.floating_ips.first() 4220 fip = self.floating_ips.first()
4224 fip.port_id = port.id 4221 fip.port_id = port.id
4225 4222
4226 self.mock_server_list.return_value = [servers, False]
4227 self.mock_servers_update_addresses.return_value = None
4228 self.mock_flavor_list.return_value = self.flavors.list()
4229 self.mock_image_list_detailed.return_value = (self.images.list(),
4230 False, False)
4231 self.mock_floating_ip_target_list_by_instance.return_value = \ 4223 self.mock_floating_ip_target_list_by_instance.return_value = \
4232 [fip_target] 4224 [fip_target]
4233 self.mock_tenant_floating_ip_list.return_value = [fip] 4225 self.mock_tenant_floating_ip_list.return_value = [fip]
4234 self.mock_floating_ip_disassociate.return_value = None 4226 self.mock_floating_ip_disassociate.return_value = None
4227 self.mock_tenant_floating_ip_release.return_value = None
4235 4228
4236 formData = {'action': 'instances__disassociate__%s' % server.id} 4229 url = reverse('horizon:project:instances:disassociate',
4237 res = self.client.post(INDEX_URL, formData) 4230 args=[server.id])
4231 form_data = {'fip': fip.id,
4232 'is_release': is_release}
4233 res = self.client.post(url, form_data)
4238 4234
4239 self.assertRedirectsNoFollow(res, INDEX_URL) 4235 self.assertRedirectsNoFollow(res, INDEX_URL)
4240 4236
4241 search_opts = {'marker': None, 'paginate': True}
4242 self.mock_server_list.assert_called_once_with(
4243 helpers.IsHttpRequest(), search_opts=search_opts)
4244 self.mock_servers_update_addresses.assert_called_once_with(
4245 helpers.IsHttpRequest(), servers)
4246 self.mock_flavor_list.assert_called_once_with(helpers.IsHttpRequest())
4247 self.mock_image_list_detailed.assert_called_once_with(
4248 helpers.IsHttpRequest())
4249 self.mock_floating_ip_target_list_by_instance.assert_called_once_with( 4237 self.mock_floating_ip_target_list_by_instance.assert_called_once_with(
4250 helpers.IsHttpRequest(), server.id) 4238 helpers.IsHttpRequest(), server.id)
4251 self.mock_tenant_floating_ip_list.assert_called_once_with( 4239 self.mock_tenant_floating_ip_list.assert_called_once_with(
4252 helpers.IsHttpRequest()) 4240 helpers.IsHttpRequest())
4253 self.mock_floating_ip_disassociate.assert_called_once_with( 4241 if is_release:
4254 helpers.IsHttpRequest(), fip.id) 4242 self.mock_floating_ip_disassociate.assert_not_called()
4243 self.mock_tenant_floating_ip_release.assert_called_once_with(
4244 helpers.IsHttpRequest(), fip.id)
4245 else:
4246 self.mock_floating_ip_disassociate.assert_called_once_with(
4247 helpers.IsHttpRequest(), fip.id)
4248 self.mock_tenant_floating_ip_release.assert_not_called()
4249
4250 @helpers.create_mocks({api.neutron: ('floating_ip_disassociate',)})
4251 def test_disassociate_floating_ip(self):
4252 self._test_disassociate_floating_ip(is_release=False)
4253
4254 @helpers.create_mocks({api.neutron: ('tenant_floating_ip_release',)})
4255 def test_disassociate_floating_ip_with_release(self):
4256 self._test_disassociate_floating_ip(is_release=True)
4255 4257
4256 @helpers.create_mocks({api.nova: ('server_get', 4258 @helpers.create_mocks({api.nova: ('server_get',
4257 'flavor_list', 4259 'flavor_list',
diff --git a/openstack_dashboard/dashboards/project/instances/urls.py b/openstack_dashboard/dashboards/project/instances/urls.py
index efbe9e8..4e5a594 100644
--- a/openstack_dashboard/dashboards/project/instances/urls.py
+++ b/openstack_dashboard/dashboards/project/instances/urls.py
@@ -41,6 +41,8 @@ urlpatterns = [
41 url(INSTANCES % 'resize', views.ResizeView.as_view(), name='resize'), 41 url(INSTANCES % 'resize', views.ResizeView.as_view(), name='resize'),
42 url(INSTANCES_KEYPAIR % 'decryptpassword', 42 url(INSTANCES_KEYPAIR % 'decryptpassword',
43 views.DecryptPasswordView.as_view(), name='decryptpassword'), 43 views.DecryptPasswordView.as_view(), name='decryptpassword'),
44 url(INSTANCES % 'disassociate',
45 views.DisassociateView.as_view(), name='disassociate'),
44 url(INSTANCES % 'attach_interface', 46 url(INSTANCES % 'attach_interface',
45 views.AttachInterfaceView.as_view(), name='attach_interface'), 47 views.AttachInterfaceView.as_view(), name='attach_interface'),
46 url(INSTANCES % 'detach_interface', 48 url(INSTANCES % 'detach_interface',
diff --git a/openstack_dashboard/dashboards/project/instances/views.py b/openstack_dashboard/dashboards/project/instances/views.py
index 60bed23..548c83d 100644
--- a/openstack_dashboard/dashboards/project/instances/views.py
+++ b/openstack_dashboard/dashboards/project/instances/views.py
@@ -389,6 +389,22 @@ class DecryptPasswordView(forms.ModalFormView):
389 'keypair_name': self.kwargs['keypair_name']} 389 'keypair_name': self.kwargs['keypair_name']}
390 390
391 391
392class DisassociateView(forms.ModalFormView):
393 form_class = project_forms.Disassociate
394 template_name = 'project/instances/disassociate.html'
395 success_url = reverse_lazy('horizon:project:instances:index')
396 page_title = _("Disassociate floating IP")
397 submit_label = _("Disassocaite")
398
399 def get_context_data(self, **kwargs):
400 context = super(DisassociateView, self).get_context_data(**kwargs)
401 context['instance_id'] = self.kwargs['instance_id']
402 return context
403
404 def get_initial(self):
405 return {'instance_id': self.kwargs['instance_id']}
406
407
392class DetailView(tabs.TabView): 408class DetailView(tabs.TabView):
393 tab_group_class = project_tabs.InstanceDetailTabs 409 tab_group_class = project_tabs.InstanceDetailTabs
394 template_name = 'horizon/common/_detail.html' 410 template_name = 'horizon/common/_detail.html'
diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example
index 4c36ec2..5013f81 100644
--- a/openstack_dashboard/local/local_settings.py.example
+++ b/openstack_dashboard/local/local_settings.py.example
@@ -128,10 +128,6 @@ WEBROOT = '/'
128# "help_text": _("Your password does not meet the requirements."), 128# "help_text": _("Your password does not meet the requirements."),
129#} 129#}
130 130
131# Disable simplified floating IP address management for deployments with
132# multiple floating IP pools or complex network requirements.
133#HORIZON_CONFIG["simple_ip_management"] = False
134
135# Turn off browser autocompletion for forms including the login form and 131# Turn off browser autocompletion for forms including the login form and
136# the database creation workflow if so desired. 132# the database creation workflow if so desired.
137#HORIZON_CONFIG["password_autocomplete"] = "off" 133#HORIZON_CONFIG["password_autocomplete"] = "off"
diff --git a/releasenotes/notes/bug-1226003-drop-simple-fip-disassociation-3c751297b467597e.yaml b/releasenotes/notes/bug-1226003-drop-simple-fip-disassociation-3c751297b467597e.yaml
new file mode 100644
index 0000000..a63316d
--- /dev/null
+++ b/releasenotes/notes/bug-1226003-drop-simple-fip-disassociation-3c751297b467597e.yaml
@@ -0,0 +1,12 @@
1---
2features:
3 - |
4 Floating IP can be released when it is disassociated from a server.
5 "Release Floating IP" checkbox is now available in "Disassociate
6 Floating IP" form.
7upgrade:
8 - |
9 ``simple_ip_management`` setting in ``HORIZON_CONFIG`` was dropped.
10 This actually has no meaning after nova-network support was dropped in Pike.
11 If you use this setting to hide ``Disaccoaite Floating IP`` button in the
12 instance table, use the policy file instead.