Browse Source

Merge "Support simple FIP disassociation (with FIP release)"

tags/14.0.0.0b1^0
Zuul 1 year ago
parent
commit
0e0db28969

+ 0
- 25
doc/source/configuration/settings.rst View File

@@ -428,31 +428,6 @@ there are any.
428 428
 This setting allows you to set rules for passwords if your organization
429 429
 requires them.
430 430
 
431
-simple_ip_management
432
-~~~~~~~~~~~~~~~~~~~~
433
-
434
-.. versionadded:: 2013.1(Grizzly)
435
-
436
-Default: ``True``
437
-
438
-Enable or disable simplified floating IP address management.
439
-
440
-"Simple" floating IP address management means that the user does not ever have
441
-to select the specific IP addresses they wish to use, and the process of
442
-allocating an IP and assigning it to an instance is one-click.
443
-
444
-The "advanced" floating IP management allows users to select the floating IP
445
-pool from which the IP should be allocated and to select a specific IP address
446
-when 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
-
456 431
 user_home
457 432
 ~~~~~~~~~
458 433
 

+ 0
- 3
horizon/conf/default.py View File

@@ -45,9 +45,6 @@ HORIZON_CONFIG = {
45 45
 
46 46
     'password_autocomplete': 'off',
47 47
 
48
-    # Enable or disable simplified floating IP address management.
49
-    'simple_ip_management': True,
50
-
51 48
     'integration_tests_support':
52 49
         getattr(settings, 'INTEGRATION_TESTS_SUPPORT', False)
53 50
 }

+ 52
- 0
openstack_dashboard/dashboards/project/instances/forms.py View File

@@ -15,6 +15,7 @@
15 15
 
16 16
 from django.template.defaultfilters import filesizeformat
17 17
 from django.urls import reverse
18
+from django.urls import reverse_lazy
18 19
 from django.utils.translation import ugettext_lazy as _
19 20
 from django.views.decorators.debug import sensitive_variables
20 21
 
@@ -418,3 +419,54 @@ class DetachInterface(forms.SelfHandlingForm):
418 419
             exceptions.handle(request, _("Unable to detach interface."),
419 420
                               redirect=redirect)
420 421
         return True
422
+
423
+
424
+class 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

+ 7
- 38
openstack_dashboard/dashboards/project/instances/tables.py View File

@@ -17,7 +17,6 @@ import logging
17 17
 
18 18
 from django.conf import settings
19 19
 from django.http import HttpResponse
20
-from django import shortcuts
21 20
 from django import template
22 21
 from django.template.defaultfilters import title
23 22
 from django import urls
@@ -29,7 +28,6 @@ from django.utils.translation import string_concat
29 28
 from django.utils.translation import ugettext_lazy as _
30 29
 from django.utils.translation import ungettext_lazy
31 30
 
32
-from horizon import conf
33 31
 from horizon import exceptions
34 32
 from horizon import messages
35 33
 from horizon import tables
@@ -655,14 +653,11 @@ class AssociateIP(policy.PolicyTargetMixin, tables.LinkAction):
655 653
         return "?".join([base_url, params])
656 654
 
657 655
 
658
-# TODO(amotoki): [drop-nova-network] The current SimpleDisassociateIP
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.
662
-class SimpleDisassociateIP(policy.PolicyTargetMixin, tables.Action):
656
+class DisassociateIP(tables.LinkAction):
663 657
     name = "disassociate"
664 658
     verbose_name = _("Disassociate Floating IP")
665
-    classes = ("btn-disassociate",)
659
+    url = "horizon:project:instances:disassociate"
660
+    classes = ("btn-disassociate", 'ajax-modal')
666 661
     policy_rules = (("network", "update_floatingip"),)
667 662
     action_type = "danger"
668 663
 
@@ -671,38 +666,12 @@ class SimpleDisassociateIP(policy.PolicyTargetMixin, tables.Action):
671 666
             return False
672 667
         if not api.neutron.floating_ip_supported(request):
673 668
             return False
674
-        if not conf.HORIZON_CONFIG["simple_ip_management"]:
675
-            return False
676 669
         for addresses in instance.addresses.values():
677 670
             for address in addresses:
678 671
                 if address.get('OS-EXT-IPS:type') == "floating":
679 672
                     return not is_deleting(instance)
680 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
 
707 676
 class UpdateMetadata(policy.PolicyTargetMixin, tables.LinkAction):
708 677
     name = "update_metadata"
@@ -1280,10 +1249,10 @@ class InstancesTable(tables.DataTable):
1280 1249
         table_actions = launch_actions + (DeleteInstance,
1281 1250
                                           InstancesFilterAction)
1282 1251
         row_actions = (StartInstance, ConfirmResize, RevertResize,
1283
-                       CreateSnapshot, AssociateIP,
1284
-                       SimpleDisassociateIP, AttachInterface,
1285
-                       DetachInterface, EditInstance, AttachVolume,
1286
-                       DetachVolume, UpdateMetadata, DecryptInstancePassword,
1252
+                       CreateSnapshot, AssociateIP, DisassociateIP,
1253
+                       AttachInterface, DetachInterface, EditInstance,
1254
+                       AttachVolume, DetachVolume,
1255
+                       UpdateMetadata, DecryptInstancePassword,
1287 1256
                        EditInstanceSecurityGroups, ConsoleLink, LogLink,
1288 1257
                        TogglePause, ToggleSuspend, ToggleShelve,
1289 1258
                        ResizeLink, LockInstance, UnlockInstance,

+ 28
- 0
openstack_dashboard/dashboards/project/instances/templates/instances/_disassociate.html View File

@@ -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 %}

+ 7
- 0
openstack_dashboard/dashboards/project/instances/templates/instances/disassociate.html View File

@@ -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 %}

+ 25
- 23
openstack_dashboard/dashboards/project/instances/tests.py View File

@@ -4208,13 +4208,10 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
4208 4208
     @helpers.create_mocks({
4209 4209
         api.neutron: ('floating_ip_target_list_by_instance',
4210 4210
                       'tenant_floating_ip_list',
4211
-                      'floating_ip_disassociate',),
4212
-        api.network: ('servers_update_addresses',),
4213
-        api.glance: ('image_list_detailed',),
4214
-        api.nova: ('server_list',
4215
-                   'flavor_list'),
4211
+                      'floating_ip_disassociate',
4212
+                      'tenant_floating_ip_release'),
4216 4213
     })
4217
-    def test_disassociate_floating_ip(self):
4214
+    def _test_disassociate_floating_ip(self, is_release):
4218 4215
         servers = self.servers.list()
4219 4216
         server = servers[0]
4220 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 4220
         fip = self.floating_ips.first()
4224 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 4223
         self.mock_floating_ip_target_list_by_instance.return_value = \
4232 4224
             [fip_target]
4233 4225
         self.mock_tenant_floating_ip_list.return_value = [fip]
4234 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}
4237
-        res = self.client.post(INDEX_URL, formData)
4229
+        url = reverse('horizon:project:instances:disassociate',
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 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 4237
         self.mock_floating_ip_target_list_by_instance.assert_called_once_with(
4250 4238
             helpers.IsHttpRequest(), server.id)
4251 4239
         self.mock_tenant_floating_ip_list.assert_called_once_with(
4252 4240
             helpers.IsHttpRequest())
4253
-        self.mock_floating_ip_disassociate.assert_called_once_with(
4254
-            helpers.IsHttpRequest(), fip.id)
4241
+        if is_release:
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 4258
     @helpers.create_mocks({api.nova: ('server_get',
4257 4259
                                       'flavor_list',

+ 2
- 0
openstack_dashboard/dashboards/project/instances/urls.py View File

@@ -41,6 +41,8 @@ urlpatterns = [
41 41
     url(INSTANCES % 'resize', views.ResizeView.as_view(), name='resize'),
42 42
     url(INSTANCES_KEYPAIR % 'decryptpassword',
43 43
         views.DecryptPasswordView.as_view(), name='decryptpassword'),
44
+    url(INSTANCES % 'disassociate',
45
+        views.DisassociateView.as_view(), name='disassociate'),
44 46
     url(INSTANCES % 'attach_interface',
45 47
         views.AttachInterfaceView.as_view(), name='attach_interface'),
46 48
     url(INSTANCES % 'detach_interface',

+ 16
- 0
openstack_dashboard/dashboards/project/instances/views.py View File

@@ -389,6 +389,22 @@ class DecryptPasswordView(forms.ModalFormView):
389 389
                 'keypair_name': self.kwargs['keypair_name']}
390 390
 
391 391
 
392
+class 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
+
392 408
 class DetailView(tabs.TabView):
393 409
     tab_group_class = project_tabs.InstanceDetailTabs
394 410
     template_name = 'horizon/common/_detail.html'

+ 0
- 4
openstack_dashboard/local/local_settings.py.example View File

@@ -128,10 +128,6 @@ WEBROOT = '/'
128 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 131
 # Turn off browser autocompletion for forms including the login form and
136 132
 # the database creation workflow if so desired.
137 133
 #HORIZON_CONFIG["password_autocomplete"] = "off"

+ 12
- 0
releasenotes/notes/bug-1226003-drop-simple-fip-disassociation-3c751297b467597e.yaml View File

@@ -0,0 +1,12 @@
1
+---
2
+features:
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.
7
+upgrade:
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.

Loading…
Cancel
Save