Merge "Support simple FIP disassociation (with FIP release)"
This commit is contained in:
@@ -428,31 +428,6 @@ there are any.
|
|||||||
This setting allows you to set rules for passwords if your organization
|
This setting allows you to set rules for passwords if your organization
|
||||||
requires them.
|
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
|
user_home
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
|
||||||
|
@@ -45,9 +45,6 @@ HORIZON_CONFIG = {
|
|||||||
|
|
||||||
'password_autocomplete': 'off',
|
'password_autocomplete': 'off',
|
||||||
|
|
||||||
# Enable or disable simplified floating IP address management.
|
|
||||||
'simple_ip_management': True,
|
|
||||||
|
|
||||||
'integration_tests_support':
|
'integration_tests_support':
|
||||||
getattr(settings, 'INTEGRATION_TESTS_SUPPORT', False)
|
getattr(settings, 'INTEGRATION_TESTS_SUPPORT', False)
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
from django.template.defaultfilters import filesizeformat
|
from django.template.defaultfilters import filesizeformat
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.views.decorators.debug import sensitive_variables
|
from django.views.decorators.debug import sensitive_variables
|
||||||
|
|
||||||
@@ -418,3 +419,54 @@ class DetachInterface(forms.SelfHandlingForm):
|
|||||||
exceptions.handle(request, _("Unable to detach interface."),
|
exceptions.handle(request, _("Unable to detach interface."),
|
||||||
redirect=redirect)
|
redirect=redirect)
|
||||||
return True
|
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.conf import settings
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django import shortcuts
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.template.defaultfilters import title
|
from django.template.defaultfilters import title
|
||||||
from django import urls
|
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 ugettext_lazy as _
|
||||||
from django.utils.translation import ungettext_lazy
|
from django.utils.translation import ungettext_lazy
|
||||||
|
|
||||||
from horizon import conf
|
|
||||||
from horizon import exceptions
|
from horizon import exceptions
|
||||||
from horizon import messages
|
from horizon import messages
|
||||||
from horizon import tables
|
from horizon import tables
|
||||||
@@ -655,14 +653,11 @@ class AssociateIP(policy.PolicyTargetMixin, tables.LinkAction):
|
|||||||
return "?".join([base_url, params])
|
return "?".join([base_url, params])
|
||||||
|
|
||||||
|
|
||||||
# TODO(amotoki): [drop-nova-network] The current SimpleDisassociateIP
|
class DisassociateIP(tables.LinkAction):
|
||||||
# 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):
|
|
||||||
name = "disassociate"
|
name = "disassociate"
|
||||||
verbose_name = _("Disassociate Floating IP")
|
verbose_name = _("Disassociate Floating IP")
|
||||||
classes = ("btn-disassociate",)
|
url = "horizon:project:instances:disassociate"
|
||||||
|
classes = ("btn-disassociate", 'ajax-modal')
|
||||||
policy_rules = (("network", "update_floatingip"),)
|
policy_rules = (("network", "update_floatingip"),)
|
||||||
action_type = "danger"
|
action_type = "danger"
|
||||||
|
|
||||||
@@ -671,38 +666,12 @@ class SimpleDisassociateIP(policy.PolicyTargetMixin, tables.Action):
|
|||||||
return False
|
return False
|
||||||
if not api.neutron.floating_ip_supported(request):
|
if not api.neutron.floating_ip_supported(request):
|
||||||
return False
|
return False
|
||||||
if not conf.HORIZON_CONFIG["simple_ip_management"]:
|
|
||||||
return False
|
|
||||||
for addresses in instance.addresses.values():
|
for addresses in instance.addresses.values():
|
||||||
for address in addresses:
|
for address in addresses:
|
||||||
if address.get('OS-EXT-IPS:type') == "floating":
|
if address.get('OS-EXT-IPS:type') == "floating":
|
||||||
return not is_deleting(instance)
|
return not is_deleting(instance)
|
||||||
return False
|
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):
|
class UpdateMetadata(policy.PolicyTargetMixin, tables.LinkAction):
|
||||||
name = "update_metadata"
|
name = "update_metadata"
|
||||||
@@ -1280,10 +1249,10 @@ class InstancesTable(tables.DataTable):
|
|||||||
table_actions = launch_actions + (DeleteInstance,
|
table_actions = launch_actions + (DeleteInstance,
|
||||||
InstancesFilterAction)
|
InstancesFilterAction)
|
||||||
row_actions = (StartInstance, ConfirmResize, RevertResize,
|
row_actions = (StartInstance, ConfirmResize, RevertResize,
|
||||||
CreateSnapshot, AssociateIP,
|
CreateSnapshot, AssociateIP, DisassociateIP,
|
||||||
SimpleDisassociateIP, AttachInterface,
|
AttachInterface, DetachInterface, EditInstance,
|
||||||
DetachInterface, EditInstance, AttachVolume,
|
AttachVolume, DetachVolume,
|
||||||
DetachVolume, UpdateMetadata, DecryptInstancePassword,
|
UpdateMetadata, DecryptInstancePassword,
|
||||||
EditInstanceSecurityGroups, ConsoleLink, LogLink,
|
EditInstanceSecurityGroups, ConsoleLink, LogLink,
|
||||||
TogglePause, ToggleSuspend, ToggleShelve,
|
TogglePause, ToggleSuspend, ToggleShelve,
|
||||||
ResizeLink, LockInstance, UnlockInstance,
|
ResizeLink, LockInstance, UnlockInstance,
|
||||||
|
@@ -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 %}
|
@@ -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({
|
@helpers.create_mocks({
|
||||||
api.neutron: ('floating_ip_target_list_by_instance',
|
api.neutron: ('floating_ip_target_list_by_instance',
|
||||||
'tenant_floating_ip_list',
|
'tenant_floating_ip_list',
|
||||||
'floating_ip_disassociate',),
|
'floating_ip_disassociate',
|
||||||
api.network: ('servers_update_addresses',),
|
'tenant_floating_ip_release'),
|
||||||
api.glance: ('image_list_detailed',),
|
|
||||||
api.nova: ('server_list',
|
|
||||||
'flavor_list'),
|
|
||||||
})
|
})
|
||||||
def test_disassociate_floating_ip(self):
|
def _test_disassociate_floating_ip(self, is_release):
|
||||||
servers = self.servers.list()
|
servers = self.servers.list()
|
||||||
server = servers[0]
|
server = servers[0]
|
||||||
port = [p for p in self.ports.list() if p.device_id == server.id][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 = self.floating_ips.first()
|
||||||
fip.port_id = port.id
|
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 = \
|
self.mock_floating_ip_target_list_by_instance.return_value = \
|
||||||
[fip_target]
|
[fip_target]
|
||||||
self.mock_tenant_floating_ip_list.return_value = [fip]
|
self.mock_tenant_floating_ip_list.return_value = [fip]
|
||||||
self.mock_floating_ip_disassociate.return_value = None
|
self.mock_floating_ip_disassociate.return_value = None
|
||||||
|
self.mock_tenant_floating_ip_release.return_value = None
|
||||||
|
|
||||||
formData = {'action': 'instances__disassociate__%s' % server.id}
|
url = reverse('horizon:project:instances:disassociate',
|
||||||
res = self.client.post(INDEX_URL, formData)
|
args=[server.id])
|
||||||
|
form_data = {'fip': fip.id,
|
||||||
|
'is_release': is_release}
|
||||||
|
res = self.client.post(url, form_data)
|
||||||
|
|
||||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
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(
|
self.mock_floating_ip_target_list_by_instance.assert_called_once_with(
|
||||||
helpers.IsHttpRequest(), server.id)
|
helpers.IsHttpRequest(), server.id)
|
||||||
self.mock_tenant_floating_ip_list.assert_called_once_with(
|
self.mock_tenant_floating_ip_list.assert_called_once_with(
|
||||||
helpers.IsHttpRequest())
|
helpers.IsHttpRequest())
|
||||||
self.mock_floating_ip_disassociate.assert_called_once_with(
|
if is_release:
|
||||||
helpers.IsHttpRequest(), fip.id)
|
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',
|
@helpers.create_mocks({api.nova: ('server_get',
|
||||||
'flavor_list',
|
'flavor_list',
|
||||||
|
@@ -41,6 +41,8 @@ urlpatterns = [
|
|||||||
url(INSTANCES % 'resize', views.ResizeView.as_view(), name='resize'),
|
url(INSTANCES % 'resize', views.ResizeView.as_view(), name='resize'),
|
||||||
url(INSTANCES_KEYPAIR % 'decryptpassword',
|
url(INSTANCES_KEYPAIR % 'decryptpassword',
|
||||||
views.DecryptPasswordView.as_view(), name='decryptpassword'),
|
views.DecryptPasswordView.as_view(), name='decryptpassword'),
|
||||||
|
url(INSTANCES % 'disassociate',
|
||||||
|
views.DisassociateView.as_view(), name='disassociate'),
|
||||||
url(INSTANCES % 'attach_interface',
|
url(INSTANCES % 'attach_interface',
|
||||||
views.AttachInterfaceView.as_view(), name='attach_interface'),
|
views.AttachInterfaceView.as_view(), name='attach_interface'),
|
||||||
url(INSTANCES % 'detach_interface',
|
url(INSTANCES % 'detach_interface',
|
||||||
|
@@ -389,6 +389,22 @@ class DecryptPasswordView(forms.ModalFormView):
|
|||||||
'keypair_name': self.kwargs['keypair_name']}
|
'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):
|
class DetailView(tabs.TabView):
|
||||||
tab_group_class = project_tabs.InstanceDetailTabs
|
tab_group_class = project_tabs.InstanceDetailTabs
|
||||||
template_name = 'horizon/common/_detail.html'
|
template_name = 'horizon/common/_detail.html'
|
||||||
|
@@ -128,10 +128,6 @@ WEBROOT = '/'
|
|||||||
# "help_text": _("Your password does not meet the requirements."),
|
# "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
|
# Turn off browser autocompletion for forms including the login form and
|
||||||
# the database creation workflow if so desired.
|
# the database creation workflow if so desired.
|
||||||
#HORIZON_CONFIG["password_autocomplete"] = "off"
|
#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.
|
Reference in New Issue
Block a user