Merge "Support simple FIP disassociation (with FIP release)"
This commit is contained in:
commit
0e0db28969
doc/source/configuration
horizon/conf
openstack_dashboard
dashboards/project/instances
local
releasenotes/notes
@ -428,31 +428,6 @@ there are any.
|
||||
This setting allows you to set rules for passwords if your organization
|
||||
requires them.
|
||||
|
||||
simple_ip_management
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionadded:: 2013.1(Grizzly)
|
||||
|
||||
Default: ``True``
|
||||
|
||||
Enable or disable simplified floating IP address management.
|
||||
|
||||
"Simple" floating IP address management means that the user does not ever have
|
||||
to select the specific IP addresses they wish to use, and the process of
|
||||
allocating an IP and assigning it to an instance is one-click.
|
||||
|
||||
The "advanced" floating IP management allows users to select the floating IP
|
||||
pool from which the IP should be allocated and to select a specific IP address
|
||||
when associating one with an instance.
|
||||
|
||||
.. note::
|
||||
|
||||
Currently "simple" floating IP address management is not compatible with
|
||||
Neutron. There are two reasons for this. First, Neutron does not support
|
||||
the default floating IP pool at the moment. Second, a Neutron floating IP
|
||||
can be associated with each VIF and we need to check whether there is only
|
||||
one VIF for an instance to enable simple association support.
|
||||
|
||||
user_home
|
||||
~~~~~~~~~
|
||||
|
||||
|
@ -45,9 +45,6 @@ HORIZON_CONFIG = {
|
||||
|
||||
'password_autocomplete': 'off',
|
||||
|
||||
# Enable or disable simplified floating IP address management.
|
||||
'simple_ip_management': True,
|
||||
|
||||
'integration_tests_support':
|
||||
getattr(settings, 'INTEGRATION_TESTS_SUPPORT', False)
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
|
||||
from django.template.defaultfilters import filesizeformat
|
||||
from django.urls import reverse
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.decorators.debug import sensitive_variables
|
||||
|
||||
@ -418,3 +419,54 @@ class DetachInterface(forms.SelfHandlingForm):
|
||||
exceptions.handle(request, _("Unable to detach interface."),
|
||||
redirect=redirect)
|
||||
return True
|
||||
|
||||
|
||||
class Disassociate(forms.SelfHandlingForm):
|
||||
fip = forms.ThemableChoiceField(label=_('Floating IP'))
|
||||
is_release = forms.BooleanField(label=_('Release Floating IP'),
|
||||
required=False)
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(Disassociate, self).__init__(request, *args, **kwargs)
|
||||
instance_id = self.initial['instance_id']
|
||||
targets = api.neutron.floating_ip_target_list_by_instance(
|
||||
request, instance_id)
|
||||
|
||||
target_ids = [t.port_id for t in targets]
|
||||
|
||||
self.fips = [fip for fip
|
||||
in api.neutron.tenant_floating_ip_list(request)
|
||||
if fip.port_id in target_ids]
|
||||
|
||||
fip_choices = [(fip.id, fip.ip) for fip in self.fips]
|
||||
fip_choices.insert(0, ('', _('Select a floating IP to disassociate')))
|
||||
self.fields['fip'].choices = fip_choices
|
||||
self.fields['fip'].initial = self.fips[0].id
|
||||
|
||||
def handle(self, request, data):
|
||||
redirect = reverse_lazy('horizon:project:instances:index')
|
||||
fip_id = data['fip']
|
||||
fips = [fip for fip in self.fips if fip.id == fip_id]
|
||||
if not fips:
|
||||
messages.error(request,
|
||||
_("The specified floating IP no longer exists."),
|
||||
redirect=redirect)
|
||||
fip = fips[0]
|
||||
try:
|
||||
if data['is_release']:
|
||||
api.neutron.tenant_floating_ip_release(request, fip_id)
|
||||
messages.success(
|
||||
request,
|
||||
_("Successfully disassociated and released "
|
||||
"floating IP %s") % fip.ip)
|
||||
else:
|
||||
api.neutron.floating_ip_disassociate(request, fip_id)
|
||||
messages.success(
|
||||
request,
|
||||
_("Successfully disassociated floating IP %s") % fip.ip)
|
||||
except Exception:
|
||||
exceptions.handle(
|
||||
request,
|
||||
_('Unable to disassociate floating IP %s') % fip.ip,
|
||||
redirect=redirect)
|
||||
return True
|
||||
|
@ -17,7 +17,6 @@ import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
from django import shortcuts
|
||||
from django import template
|
||||
from django.template.defaultfilters import title
|
||||
from django import urls
|
||||
@ -29,7 +28,6 @@ from django.utils.translation import string_concat
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ungettext_lazy
|
||||
|
||||
from horizon import conf
|
||||
from horizon import exceptions
|
||||
from horizon import messages
|
||||
from horizon import tables
|
||||
@ -655,14 +653,11 @@ class AssociateIP(policy.PolicyTargetMixin, tables.LinkAction):
|
||||
return "?".join([base_url, params])
|
||||
|
||||
|
||||
# TODO(amotoki): [drop-nova-network] The current SimpleDisassociateIP
|
||||
# just disassociates the first found FIP. It looks better to have a form
|
||||
# which allows to choose which FIP should be disassociated.
|
||||
# HORIZON_CONFIG['simple_ip_management'] can be dropped then.
|
||||
class SimpleDisassociateIP(policy.PolicyTargetMixin, tables.Action):
|
||||
class DisassociateIP(tables.LinkAction):
|
||||
name = "disassociate"
|
||||
verbose_name = _("Disassociate Floating IP")
|
||||
classes = ("btn-disassociate",)
|
||||
url = "horizon:project:instances:disassociate"
|
||||
classes = ("btn-disassociate", 'ajax-modal')
|
||||
policy_rules = (("network", "update_floatingip"),)
|
||||
action_type = "danger"
|
||||
|
||||
@ -671,38 +666,12 @@ class SimpleDisassociateIP(policy.PolicyTargetMixin, tables.Action):
|
||||
return False
|
||||
if not api.neutron.floating_ip_supported(request):
|
||||
return False
|
||||
if not conf.HORIZON_CONFIG["simple_ip_management"]:
|
||||
return False
|
||||
for addresses in instance.addresses.values():
|
||||
for address in addresses:
|
||||
if address.get('OS-EXT-IPS:type') == "floating":
|
||||
return not is_deleting(instance)
|
||||
return False
|
||||
|
||||
def single(self, table, request, instance_id):
|
||||
try:
|
||||
targets = api.neutron.floating_ip_target_list_by_instance(
|
||||
request, instance_id)
|
||||
|
||||
target_ids = [t.port_id for t in targets]
|
||||
|
||||
fips = [fip for fip in api.neutron.tenant_floating_ip_list(request)
|
||||
if fip.port_id in target_ids]
|
||||
# Removing multiple floating IPs at once doesn't work, so this pops
|
||||
# off the first one.
|
||||
if fips:
|
||||
fip = fips.pop()
|
||||
api.neutron.floating_ip_disassociate(request, fip.id)
|
||||
messages.success(request,
|
||||
_("Successfully disassociated "
|
||||
"floating IP: %s") % fip.ip)
|
||||
else:
|
||||
messages.info(request, _("No floating IPs to disassociate."))
|
||||
except Exception:
|
||||
exceptions.handle(request,
|
||||
_("Unable to disassociate floating IP."))
|
||||
return shortcuts.redirect(request.get_full_path())
|
||||
|
||||
|
||||
class UpdateMetadata(policy.PolicyTargetMixin, tables.LinkAction):
|
||||
name = "update_metadata"
|
||||
@ -1280,10 +1249,10 @@ class InstancesTable(tables.DataTable):
|
||||
table_actions = launch_actions + (DeleteInstance,
|
||||
InstancesFilterAction)
|
||||
row_actions = (StartInstance, ConfirmResize, RevertResize,
|
||||
CreateSnapshot, AssociateIP,
|
||||
SimpleDisassociateIP, AttachInterface,
|
||||
DetachInterface, EditInstance, AttachVolume,
|
||||
DetachVolume, UpdateMetadata, DecryptInstancePassword,
|
||||
CreateSnapshot, AssociateIP, DisassociateIP,
|
||||
AttachInterface, DetachInterface, EditInstance,
|
||||
AttachVolume, DetachVolume,
|
||||
UpdateMetadata, DecryptInstancePassword,
|
||||
EditInstanceSecurityGroups, ConsoleLink, LogLink,
|
||||
TogglePause, ToggleSuspend, ToggleShelve,
|
||||
ResizeLink, LockInstance, UnlockInstance,
|
||||
|
28
openstack_dashboard/dashboards/project/instances/templates/instances/_disassociate.html
Normal file
28
openstack_dashboard/dashboards/project/instances/templates/instances/_disassociate.html
Normal file
@ -0,0 +1,28 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block form_id %}disassociate_fip_form{% endblock %}
|
||||
{% block form_action %}{% url "horizon:project:instances:disassociate" instance_id %}{% endblock %}
|
||||
|
||||
{% block modal_id %}disassocaite_fip_modal{% endblock %}
|
||||
{% block modal-header %}{% trans "Disassociate Floating IP" %}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="left">
|
||||
<fieldset>
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="right">
|
||||
<h3>{% trans "Description:" %}</h3>
|
||||
<p>{% blocktrans trimmed %}
|
||||
Select the floating IP to be disassociated from the instance.
|
||||
{% endblocktrans %}</p>
|
||||
<dl>
|
||||
<dt>{% trans "Release Floating IP" %}</dt>
|
||||
<dd>{% blocktrans trimmed %}
|
||||
If checked, the selected floating IP will be released at the same time.
|
||||
{% endblocktrans %}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
{% endblock %}
|
7
openstack_dashboard/dashboards/project/instances/templates/instances/disassociate.html
Normal file
7
openstack_dashboard/dashboards/project/instances/templates/instances/disassociate.html
Normal file
@ -0,0 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Disassociate Floating IP" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include "project/instances/_disassociate.html" %}
|
||||
{% endblock %}
|
@ -4208,13 +4208,10 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
|
||||
@helpers.create_mocks({
|
||||
api.neutron: ('floating_ip_target_list_by_instance',
|
||||
'tenant_floating_ip_list',
|
||||
'floating_ip_disassociate',),
|
||||
api.network: ('servers_update_addresses',),
|
||||
api.glance: ('image_list_detailed',),
|
||||
api.nova: ('server_list',
|
||||
'flavor_list'),
|
||||
'floating_ip_disassociate',
|
||||
'tenant_floating_ip_release'),
|
||||
})
|
||||
def test_disassociate_floating_ip(self):
|
||||
def _test_disassociate_floating_ip(self, is_release):
|
||||
servers = self.servers.list()
|
||||
server = servers[0]
|
||||
port = [p for p in self.ports.list() if p.device_id == server.id][0]
|
||||
@ -4223,35 +4220,40 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
|
||||
fip = self.floating_ips.first()
|
||||
fip.port_id = port.id
|
||||
|
||||
self.mock_server_list.return_value = [servers, False]
|
||||
self.mock_servers_update_addresses.return_value = None
|
||||
self.mock_flavor_list.return_value = self.flavors.list()
|
||||
self.mock_image_list_detailed.return_value = (self.images.list(),
|
||||
False, False)
|
||||
self.mock_floating_ip_target_list_by_instance.return_value = \
|
||||
[fip_target]
|
||||
self.mock_tenant_floating_ip_list.return_value = [fip]
|
||||
self.mock_floating_ip_disassociate.return_value = None
|
||||
self.mock_tenant_floating_ip_release.return_value = None
|
||||
|
||||
formData = {'action': 'instances__disassociate__%s' % server.id}
|
||||
res = self.client.post(INDEX_URL, formData)
|
||||
url = reverse('horizon:project:instances:disassociate',
|
||||
args=[server.id])
|
||||
form_data = {'fip': fip.id,
|
||||
'is_release': is_release}
|
||||
res = self.client.post(url, form_data)
|
||||
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
search_opts = {'marker': None, 'paginate': True}
|
||||
self.mock_server_list.assert_called_once_with(
|
||||
helpers.IsHttpRequest(), search_opts=search_opts)
|
||||
self.mock_servers_update_addresses.assert_called_once_with(
|
||||
helpers.IsHttpRequest(), servers)
|
||||
self.mock_flavor_list.assert_called_once_with(helpers.IsHttpRequest())
|
||||
self.mock_image_list_detailed.assert_called_once_with(
|
||||
helpers.IsHttpRequest())
|
||||
self.mock_floating_ip_target_list_by_instance.assert_called_once_with(
|
||||
helpers.IsHttpRequest(), server.id)
|
||||
self.mock_tenant_floating_ip_list.assert_called_once_with(
|
||||
helpers.IsHttpRequest())
|
||||
self.mock_floating_ip_disassociate.assert_called_once_with(
|
||||
helpers.IsHttpRequest(), fip.id)
|
||||
if is_release:
|
||||
self.mock_floating_ip_disassociate.assert_not_called()
|
||||
self.mock_tenant_floating_ip_release.assert_called_once_with(
|
||||
helpers.IsHttpRequest(), fip.id)
|
||||
else:
|
||||
self.mock_floating_ip_disassociate.assert_called_once_with(
|
||||
helpers.IsHttpRequest(), fip.id)
|
||||
self.mock_tenant_floating_ip_release.assert_not_called()
|
||||
|
||||
@helpers.create_mocks({api.neutron: ('floating_ip_disassociate',)})
|
||||
def test_disassociate_floating_ip(self):
|
||||
self._test_disassociate_floating_ip(is_release=False)
|
||||
|
||||
@helpers.create_mocks({api.neutron: ('tenant_floating_ip_release',)})
|
||||
def test_disassociate_floating_ip_with_release(self):
|
||||
self._test_disassociate_floating_ip(is_release=True)
|
||||
|
||||
@helpers.create_mocks({api.nova: ('server_get',
|
||||
'flavor_list',
|
||||
|
@ -41,6 +41,8 @@ urlpatterns = [
|
||||
url(INSTANCES % 'resize', views.ResizeView.as_view(), name='resize'),
|
||||
url(INSTANCES_KEYPAIR % 'decryptpassword',
|
||||
views.DecryptPasswordView.as_view(), name='decryptpassword'),
|
||||
url(INSTANCES % 'disassociate',
|
||||
views.DisassociateView.as_view(), name='disassociate'),
|
||||
url(INSTANCES % 'attach_interface',
|
||||
views.AttachInterfaceView.as_view(), name='attach_interface'),
|
||||
url(INSTANCES % 'detach_interface',
|
||||
|
@ -389,6 +389,22 @@ class DecryptPasswordView(forms.ModalFormView):
|
||||
'keypair_name': self.kwargs['keypair_name']}
|
||||
|
||||
|
||||
class DisassociateView(forms.ModalFormView):
|
||||
form_class = project_forms.Disassociate
|
||||
template_name = 'project/instances/disassociate.html'
|
||||
success_url = reverse_lazy('horizon:project:instances:index')
|
||||
page_title = _("Disassociate floating IP")
|
||||
submit_label = _("Disassocaite")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(DisassociateView, self).get_context_data(**kwargs)
|
||||
context['instance_id'] = self.kwargs['instance_id']
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
return {'instance_id': self.kwargs['instance_id']}
|
||||
|
||||
|
||||
class DetailView(tabs.TabView):
|
||||
tab_group_class = project_tabs.InstanceDetailTabs
|
||||
template_name = 'horizon/common/_detail.html'
|
||||
|
@ -128,10 +128,6 @@ WEBROOT = '/'
|
||||
# "help_text": _("Your password does not meet the requirements."),
|
||||
#}
|
||||
|
||||
# Disable simplified floating IP address management for deployments with
|
||||
# multiple floating IP pools or complex network requirements.
|
||||
#HORIZON_CONFIG["simple_ip_management"] = False
|
||||
|
||||
# Turn off browser autocompletion for forms including the login form and
|
||||
# the database creation workflow if so desired.
|
||||
#HORIZON_CONFIG["password_autocomplete"] = "off"
|
||||
|
@ -0,0 +1,12 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Floating IP can be released when it is disassociated from a server.
|
||||
"Release Floating IP" checkbox is now available in "Disassociate
|
||||
Floating IP" form.
|
||||
upgrade:
|
||||
- |
|
||||
``simple_ip_management`` setting in ``HORIZON_CONFIG`` was dropped.
|
||||
This actually has no meaning after nova-network support was dropped in Pike.
|
||||
If you use this setting to hide ``Disaccoaite Floating IP`` button in the
|
||||
instance table, use the policy file instead.
|
Loading…
x
Reference in New Issue
Block a user