Add instance interfaces tab for easy security group edit
There is no easy way to edit security groups of ports attached to an instance. This commit adds a table of ports attached to an insntace to the instance detail. Users now can access attached ports easily and edit their security groups. Partial-Bug: #1750147 Change-Id: Ia2bd19f92251702878be3b12d0ea2a5c6618c65e
This commit is contained in:
parent
eea762663b
commit
ff5b622da5
@ -0,0 +1,62 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.http import urlencode
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from horizon import tables
|
||||||
|
|
||||||
|
from openstack_dashboard.dashboards.project.networks.ports \
|
||||||
|
import tables as port_tables
|
||||||
|
|
||||||
|
|
||||||
|
class UpdatePort(port_tables.UpdatePort):
|
||||||
|
url = "horizon:project:instances:update_port"
|
||||||
|
|
||||||
|
def get_link_url(self, port):
|
||||||
|
instance_id = self.table.kwargs['instance_id']
|
||||||
|
base_url = reverse(self.url, args=(instance_id, port.id))
|
||||||
|
params = {'step': 'update_info'}
|
||||||
|
param = urlencode(params)
|
||||||
|
return '?'.join([base_url, param])
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateSecurityGroups(port_tables.UpdatePort):
|
||||||
|
name = 'update_security_groups'
|
||||||
|
verbose_name = _('Edit Security Groups')
|
||||||
|
url = "horizon:project:instances:update_port"
|
||||||
|
classes = ("ajax-modal",)
|
||||||
|
icon = "pencil"
|
||||||
|
policy_rules = (("network", "update_port"),)
|
||||||
|
|
||||||
|
def get_link_url(self, port):
|
||||||
|
instance_id = self.table.kwargs['instance_id']
|
||||||
|
base_url = reverse(self.url, args=(instance_id, port.id))
|
||||||
|
params = {'step': 'update_security_groups'}
|
||||||
|
param = urlencode(params)
|
||||||
|
return '?'.join([base_url, param])
|
||||||
|
|
||||||
|
def allowed(self, request, datum=None):
|
||||||
|
return datum and datum.port_security_enabled
|
||||||
|
|
||||||
|
|
||||||
|
class InterfacesTable(port_tables.PortsTable):
|
||||||
|
network = tables.Column('network', verbose_name=_('Network'))
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "interfaces"
|
||||||
|
verbose_name = _("Interfaces")
|
||||||
|
table_actions = []
|
||||||
|
row_actions = [UpdateSecurityGroups, UpdatePort]
|
||||||
|
columns = ('name', 'network', 'fixed_ips', 'mac_address',
|
||||||
|
'status', 'admin_state', 'mac_state')
|
@ -25,6 +25,7 @@ from openstack_dashboard.dashboards.project.instances \
|
|||||||
|
|
||||||
from openstack_dashboard import api
|
from openstack_dashboard import api
|
||||||
from openstack_dashboard.dashboards.project.instances import console
|
from openstack_dashboard.dashboards.project.instances import console
|
||||||
|
from openstack_dashboard.dashboards.project.instances import interfaces_tables
|
||||||
|
|
||||||
|
|
||||||
class OverviewTab(tabs.Tab):
|
class OverviewTab(tabs.Tab):
|
||||||
@ -38,6 +39,32 @@ class OverviewTab(tabs.Tab):
|
|||||||
"is_superuser": request.user.is_superuser}
|
"is_superuser": request.user.is_superuser}
|
||||||
|
|
||||||
|
|
||||||
|
class InterfacesTab(tabs.TableTab):
|
||||||
|
name = _("Interfaces")
|
||||||
|
slug = "interfaces"
|
||||||
|
table_classes = (interfaces_tables.InterfacesTable, )
|
||||||
|
template_name = "horizon/common/_detail_table.html"
|
||||||
|
preload = False
|
||||||
|
|
||||||
|
def get_interfaces_data(self):
|
||||||
|
instance = self.tab_group.kwargs['instance']
|
||||||
|
try:
|
||||||
|
ports = api.neutron.port_list(self.request, device_id=instance.id)
|
||||||
|
if ports:
|
||||||
|
net_ids = [p.network_id for p in ports]
|
||||||
|
networks = api.neutron.network_list(self.request, id=net_ids)
|
||||||
|
net_dict = dict((n.id, n.name_or_id) for n in networks)
|
||||||
|
else:
|
||||||
|
net_dict = {}
|
||||||
|
for p in ports:
|
||||||
|
p.network = net_dict.get(p.network_id)
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(self.request,
|
||||||
|
_('Failed to get instance interfaces.'))
|
||||||
|
ports = []
|
||||||
|
return ports
|
||||||
|
|
||||||
|
|
||||||
class LogTab(tabs.Tab):
|
class LogTab(tabs.Tab):
|
||||||
name = _("Log")
|
name = _("Log")
|
||||||
slug = "log"
|
slug = "log"
|
||||||
@ -110,5 +137,5 @@ class AuditTab(tabs.TableTab):
|
|||||||
|
|
||||||
class InstanceDetailTabs(tabs.DetailTabsGroup):
|
class InstanceDetailTabs(tabs.DetailTabsGroup):
|
||||||
slug = "instance_details"
|
slug = "instance_details"
|
||||||
tabs = (OverviewTab, LogTab, ConsoleTab, AuditTab)
|
tabs = (OverviewTab, InterfacesTab, LogTab, ConsoleTab, AuditTab)
|
||||||
sticky = True
|
sticky = True
|
||||||
|
@ -1350,22 +1350,30 @@ class InstanceDetailTests(InstanceTestBase):
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@helpers.create_mocks({api.neutron: ['is_extension_supported']})
|
||||||
def test_instance_details_volumes(self):
|
def test_instance_details_volumes(self):
|
||||||
server = self.servers.first()
|
server = self.servers.first()
|
||||||
volumes = [self.volumes.list()[1]]
|
volumes = [self.volumes.list()[1]]
|
||||||
security_groups = self.security_groups.list()
|
security_groups = self.security_groups.list()
|
||||||
|
|
||||||
|
self.mock_is_extension_supported.return_value = False
|
||||||
|
|
||||||
res = self._get_instance_details(
|
res = self._get_instance_details(
|
||||||
server, volumes_return=volumes,
|
server, volumes_return=volumes,
|
||||||
security_groups_return=security_groups)
|
security_groups_return=security_groups)
|
||||||
|
|
||||||
self.assertItemsEqual(res.context['instance'].volumes, volumes)
|
self.assertItemsEqual(res.context['instance'].volumes, volumes)
|
||||||
|
self.mock_is_extension_supported.assert_called_once_with(
|
||||||
|
helpers.IsHttpRequest(), 'mac-learning')
|
||||||
|
|
||||||
|
@helpers.create_mocks({api.neutron: ['is_extension_supported']})
|
||||||
def test_instance_details_volume_sorting(self):
|
def test_instance_details_volume_sorting(self):
|
||||||
server = self.servers.first()
|
server = self.servers.first()
|
||||||
volumes = self.volumes.list()[1:3]
|
volumes = self.volumes.list()[1:3]
|
||||||
security_groups = self.security_groups.list()
|
security_groups = self.security_groups.list()
|
||||||
|
|
||||||
|
self.mock_is_extension_supported.return_value = False
|
||||||
|
|
||||||
res = self._get_instance_details(
|
res = self._get_instance_details(
|
||||||
server, volumes_return=volumes,
|
server, volumes_return=volumes,
|
||||||
security_groups_return=security_groups)
|
security_groups_return=security_groups)
|
||||||
@ -1375,10 +1383,15 @@ class InstanceDetailTests(InstanceTestBase):
|
|||||||
"/dev/hda")
|
"/dev/hda")
|
||||||
self.assertEqual(res.context['instance'].volumes[1].device,
|
self.assertEqual(res.context['instance'].volumes[1].device,
|
||||||
"/dev/hdk")
|
"/dev/hdk")
|
||||||
|
self.mock_is_extension_supported.assert_called_once_with(
|
||||||
|
helpers.IsHttpRequest(), 'mac-learning')
|
||||||
|
|
||||||
|
@helpers.create_mocks({api.neutron: ['is_extension_supported']})
|
||||||
def test_instance_details_metadata(self):
|
def test_instance_details_metadata(self):
|
||||||
server = self.servers.first()
|
server = self.servers.first()
|
||||||
|
|
||||||
|
self.mock_is_extension_supported.return_value = False
|
||||||
|
|
||||||
tg = tabs.InstanceDetailTabs(self.request, instance=server)
|
tg = tabs.InstanceDetailTabs(self.request, instance=server)
|
||||||
qs = "?%s=%s" % (tg.param_name, tg.get_tab("overview").get_id())
|
qs = "?%s=%s" % (tg.param_name, tg.get_tab("overview").get_id())
|
||||||
res = self._get_instance_details(server, qs)
|
res = self._get_instance_details(server, qs)
|
||||||
@ -1392,9 +1405,16 @@ class InstanceDetailTests(InstanceTestBase):
|
|||||||
self.assertContains(res, "<dt>empty</dt>", 1)
|
self.assertContains(res, "<dt>empty</dt>", 1)
|
||||||
self.assertContains(res, "<dd><em>N/A</em></dd>", 1)
|
self.assertContains(res, "<dd><em>N/A</em></dd>", 1)
|
||||||
|
|
||||||
|
self.assert_mock_multiple_calls_with_same_arguments(
|
||||||
|
self.mock_is_extension_supported, 2,
|
||||||
|
mock.call(helpers.IsHttpRequest(), 'mac-learning'))
|
||||||
|
|
||||||
|
@helpers.create_mocks({api.neutron: ['is_extension_supported']})
|
||||||
def test_instance_details_fault(self):
|
def test_instance_details_fault(self):
|
||||||
server = self.servers.first()
|
server = self.servers.first()
|
||||||
|
|
||||||
|
self.mock_is_extension_supported.return_value = False
|
||||||
|
|
||||||
server.status = 'ERROR'
|
server.status = 'ERROR'
|
||||||
server.fault = {"message": "NoValidHost",
|
server.fault = {"message": "NoValidHost",
|
||||||
"code": 500,
|
"code": 500,
|
||||||
@ -1408,8 +1428,11 @@ class InstanceDetailTests(InstanceTestBase):
|
|||||||
|
|
||||||
res = self._get_instance_details(server)
|
res = self._get_instance_details(server)
|
||||||
self.assertItemsEqual(res.context['instance'].fault, server.fault)
|
self.assertItemsEqual(res.context['instance'].fault, server.fault)
|
||||||
|
self.mock_is_extension_supported.assert_called_once_with(
|
||||||
|
helpers.IsHttpRequest(), 'mac-learning')
|
||||||
|
|
||||||
@helpers.create_mocks({console: ('get_console',)})
|
@helpers.create_mocks({console: ['get_console'],
|
||||||
|
api.neutron: ['is_extension_supported']})
|
||||||
def test_instance_details_console_tab(self):
|
def test_instance_details_console_tab(self):
|
||||||
server = self.servers.first()
|
server = self.servers.first()
|
||||||
CONSOLE_OUTPUT = '/vncserver'
|
CONSOLE_OUTPUT = '/vncserver'
|
||||||
@ -1417,6 +1440,7 @@ class InstanceDetailTests(InstanceTestBase):
|
|||||||
CONSOLE_URL = CONSOLE_OUTPUT + CONSOLE_TITLE
|
CONSOLE_URL = CONSOLE_OUTPUT + CONSOLE_TITLE
|
||||||
|
|
||||||
self.mock_get_console.return_value = ('VNC', CONSOLE_URL)
|
self.mock_get_console.return_value = ('VNC', CONSOLE_URL)
|
||||||
|
self.mock_is_extension_supported.return_value = False
|
||||||
|
|
||||||
tg = tabs.InstanceDetailTabs(self.request, instance=server)
|
tg = tabs.InstanceDetailTabs(self.request, instance=server)
|
||||||
qs = "?%s=%s" % (tg.param_name, tg.get_tab("console").get_id())
|
qs = "?%s=%s" % (tg.param_name, tg.get_tab("console").get_id())
|
||||||
@ -1432,11 +1456,17 @@ class InstanceDetailTests(InstanceTestBase):
|
|||||||
self.assertTrue(console_tab_rendered)
|
self.assertTrue(console_tab_rendered)
|
||||||
self.mock_get_console.assert_called_once_with(
|
self.mock_get_console.assert_called_once_with(
|
||||||
mock.ANY, 'AUTO', server)
|
mock.ANY, 'AUTO', server)
|
||||||
|
self.assert_mock_multiple_calls_with_same_arguments(
|
||||||
|
self.mock_is_extension_supported, 2,
|
||||||
|
mock.call(helpers.IsHttpRequest(), 'mac-learning'))
|
||||||
|
|
||||||
@django.test.utils.override_settings(CONSOLE_TYPE=None)
|
@django.test.utils.override_settings(CONSOLE_TYPE=None)
|
||||||
|
@helpers.create_mocks({api.neutron: ['is_extension_supported']})
|
||||||
def test_instance_details_console_tab_deactivated(self):
|
def test_instance_details_console_tab_deactivated(self):
|
||||||
server = self.servers.first()
|
server = self.servers.first()
|
||||||
|
|
||||||
|
self.mock_is_extension_supported.return_value = False
|
||||||
|
|
||||||
tg = tabs.InstanceDetailTabs(self.request, instance=server)
|
tg = tabs.InstanceDetailTabs(self.request, instance=server)
|
||||||
self.assertIsNone(tg.get_tab("console"))
|
self.assertIsNone(tg.get_tab("console"))
|
||||||
res = self._get_instance_details(server)
|
res = self._get_instance_details(server)
|
||||||
@ -1445,6 +1475,10 @@ class InstanceDetailTests(InstanceTestBase):
|
|||||||
for tab in res.context_data['tab_group'].get_loaded_tabs():
|
for tab in res.context_data['tab_group'].get_loaded_tabs():
|
||||||
self.assertNotIsInstance(tab, tabs.ConsoleTab)
|
self.assertNotIsInstance(tab, tabs.ConsoleTab)
|
||||||
|
|
||||||
|
self.assert_mock_multiple_calls_with_same_arguments(
|
||||||
|
self.mock_is_extension_supported, 2,
|
||||||
|
mock.call(helpers.IsHttpRequest(), 'mac-learning'))
|
||||||
|
|
||||||
@helpers.create_mocks({api.nova: ('server_get',)})
|
@helpers.create_mocks({api.nova: ('server_get',)})
|
||||||
def test_instance_details_exception(self):
|
def test_instance_details_exception(self):
|
||||||
server = self.servers.first()
|
server = self.servers.first()
|
||||||
@ -1475,19 +1509,25 @@ class InstanceDetailTests(InstanceTestBase):
|
|||||||
self.assertEqual(403, res.status_code)
|
self.assertEqual(403, res.status_code)
|
||||||
self.assertEqual(0, self.mock_server_get.call_count)
|
self.assertEqual(0, self.mock_server_get.call_count)
|
||||||
|
|
||||||
|
@helpers.create_mocks({api.neutron: ['is_extension_supported']})
|
||||||
def test_instance_details_flavor_not_found(self):
|
def test_instance_details_flavor_not_found(self):
|
||||||
server = self.servers.first()
|
server = self.servers.first()
|
||||||
|
self.mock_is_extension_supported.return_value = False
|
||||||
res = self._get_instance_details(server, flavor_exception=True)
|
res = self._get_instance_details(server, flavor_exception=True)
|
||||||
self.assertTemplateUsed(res,
|
self.assertTemplateUsed(res,
|
||||||
'project/instances/_detail_overview.html')
|
'project/instances/_detail_overview.html')
|
||||||
self.assertContains(res, "Not available")
|
self.assertContains(res, "Not available")
|
||||||
|
self.mock_is_extension_supported.assert_called_once_with(
|
||||||
|
helpers.IsHttpRequest(), 'mac-learning')
|
||||||
|
|
||||||
@helpers.create_mocks({api.nova: ('server_console_output',)})
|
@helpers.create_mocks({api.nova: ['server_console_output'],
|
||||||
|
api.neutron: ['is_extension_supported']})
|
||||||
def test_instance_log(self):
|
def test_instance_log(self):
|
||||||
server = self.servers.first()
|
server = self.servers.first()
|
||||||
CONSOLE_OUTPUT = 'output'
|
CONSOLE_OUTPUT = 'output'
|
||||||
|
|
||||||
self.mock_server_console_output.return_value = CONSOLE_OUTPUT
|
self.mock_server_console_output.return_value = CONSOLE_OUTPUT
|
||||||
|
self.mock_is_extension_supported.return_value = False
|
||||||
|
|
||||||
url = reverse('horizon:project:instances:console',
|
url = reverse('horizon:project:instances:console',
|
||||||
args=[server.id])
|
args=[server.id])
|
||||||
@ -1500,12 +1540,16 @@ class InstanceDetailTests(InstanceTestBase):
|
|||||||
self.assertContains(res, CONSOLE_OUTPUT)
|
self.assertContains(res, CONSOLE_OUTPUT)
|
||||||
self.mock_server_console_output.assert_called_once_with(
|
self.mock_server_console_output.assert_called_once_with(
|
||||||
helpers.IsHttpRequest(), server.id, tail_length=None)
|
helpers.IsHttpRequest(), server.id, tail_length=None)
|
||||||
|
self.mock_is_extension_supported.assert_called_once_with(
|
||||||
|
helpers.IsHttpRequest(), 'mac-learning')
|
||||||
|
|
||||||
@helpers.create_mocks({api.nova: ('server_console_output',)})
|
@helpers.create_mocks({api.nova: ['server_console_output'],
|
||||||
|
api.neutron: ['is_extension_supported']})
|
||||||
def test_instance_log_exception(self):
|
def test_instance_log_exception(self):
|
||||||
server = self.servers.first()
|
server = self.servers.first()
|
||||||
|
|
||||||
self.mock_server_console_output.side_effect = self.exceptions.nova
|
self.mock_server_console_output.side_effect = self.exceptions.nova
|
||||||
|
self.mock_is_extension_supported.return_value = False
|
||||||
|
|
||||||
url = reverse('horizon:project:instances:console',
|
url = reverse('horizon:project:instances:console',
|
||||||
args=[server.id])
|
args=[server.id])
|
||||||
@ -1516,9 +1560,13 @@ class InstanceDetailTests(InstanceTestBase):
|
|||||||
self.assertContains(res, "Unable to get log for")
|
self.assertContains(res, "Unable to get log for")
|
||||||
self.mock_server_console_output.assert_called_once_with(
|
self.mock_server_console_output.assert_called_once_with(
|
||||||
helpers.IsHttpRequest(), server.id, tail_length=None)
|
helpers.IsHttpRequest(), server.id, tail_length=None)
|
||||||
|
self.mock_is_extension_supported.assert_called_once_with(
|
||||||
|
helpers.IsHttpRequest(), 'mac-learning')
|
||||||
|
|
||||||
|
@helpers.create_mocks({api.neutron: ['is_extension_supported']})
|
||||||
def test_instance_log_invalid_input(self):
|
def test_instance_log_invalid_input(self):
|
||||||
server = self.servers.first()
|
server = self.servers.first()
|
||||||
|
self.mock_is_extension_supported.return_value = False
|
||||||
|
|
||||||
url = reverse('horizon:project:instances:console',
|
url = reverse('horizon:project:instances:console',
|
||||||
args=[server.id])
|
args=[server.id])
|
||||||
@ -1531,6 +1579,9 @@ class InstanceDetailTests(InstanceTestBase):
|
|||||||
|
|
||||||
self.assertContains(res, "Unable to get log for")
|
self.assertContains(res, "Unable to get log for")
|
||||||
|
|
||||||
|
self.mock_is_extension_supported.assert_called_once_with(
|
||||||
|
helpers.IsHttpRequest(), 'mac-learning')
|
||||||
|
|
||||||
@helpers.create_mocks({api.nova: ['server_get'],
|
@helpers.create_mocks({api.nova: ['server_get'],
|
||||||
console: ['get_console']})
|
console: ['get_console']})
|
||||||
def test_instance_auto_console(self):
|
def test_instance_auto_console(self):
|
||||||
|
@ -54,5 +54,7 @@ urlpatterns = [
|
|||||||
url(r'^(?P<instance_id>[^/]+)/detach_volume/$',
|
url(r'^(?P<instance_id>[^/]+)/detach_volume/$',
|
||||||
views.DetachVolumeView.as_view(),
|
views.DetachVolumeView.as_view(),
|
||||||
name='detach_volume'
|
name='detach_volume'
|
||||||
)
|
),
|
||||||
|
url(r'^(?P<instance_id>[^/]+)/ports/(?P<port_id>[^/]+)/update$',
|
||||||
|
views.UpdatePortView.as_view(), name='update_port'),
|
||||||
]
|
]
|
||||||
|
@ -51,6 +51,8 @@ from openstack_dashboard.dashboards.project.instances \
|
|||||||
import tabs as project_tabs
|
import tabs as project_tabs
|
||||||
from openstack_dashboard.dashboards.project.instances \
|
from openstack_dashboard.dashboards.project.instances \
|
||||||
import workflows as project_workflows
|
import workflows as project_workflows
|
||||||
|
from openstack_dashboard.dashboards.project.networks.ports \
|
||||||
|
import views as port_views
|
||||||
from openstack_dashboard.utils import futurist_utils
|
from openstack_dashboard.utils import futurist_utils
|
||||||
from openstack_dashboard.views import get_url_with_pagination
|
from openstack_dashboard.views import get_url_with_pagination
|
||||||
|
|
||||||
@ -655,3 +657,24 @@ class DetachInterfaceView(forms.ModalFormView):
|
|||||||
submit_url = "horizon:project:instances:detach_interface"
|
submit_url = "horizon:project:instances:detach_interface"
|
||||||
self.submit_url = reverse(submit_url, kwargs=args)
|
self.submit_url = reverse(submit_url, kwargs=args)
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
class UpdatePortView(port_views.UpdateView):
|
||||||
|
workflow_class = project_workflows.UpdatePort
|
||||||
|
failure_url = 'horizon:project:instances:detail'
|
||||||
|
|
||||||
|
@memoized.memoized_method
|
||||||
|
def _get_object(self, *args, **kwargs):
|
||||||
|
port_id = self.kwargs['port_id']
|
||||||
|
try:
|
||||||
|
return api.neutron.port_get(self.request, port_id)
|
||||||
|
except Exception:
|
||||||
|
redirect = reverse(self.failure_url,
|
||||||
|
args=(self.kwargs['instance_id'],))
|
||||||
|
msg = _('Unable to retrieve port details')
|
||||||
|
exceptions.handle(self.request, msg, redirect=redirect)
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
initial = super(UpdatePortView, self).get_initial()
|
||||||
|
initial['instance_id'] = self.kwargs['instance_id']
|
||||||
|
return initial
|
||||||
|
@ -16,9 +16,12 @@ from openstack_dashboard.dashboards.project.instances.workflows.\
|
|||||||
resize_instance import ResizeInstance
|
resize_instance import ResizeInstance
|
||||||
from openstack_dashboard.dashboards.project.instances.workflows.\
|
from openstack_dashboard.dashboards.project.instances.workflows.\
|
||||||
update_instance import UpdateInstance
|
update_instance import UpdateInstance
|
||||||
|
from openstack_dashboard.dashboards.project.instances.workflows.\
|
||||||
|
update_port import UpdatePort
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'LaunchInstance',
|
'LaunchInstance',
|
||||||
'ResizeInstance',
|
'ResizeInstance',
|
||||||
'UpdateInstance',
|
'UpdateInstance',
|
||||||
|
'UpdatePort',
|
||||||
]
|
]
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from openstack_dashboard.dashboards.project.networks.ports import workflows
|
||||||
|
|
||||||
|
|
||||||
|
class UpdatePortInfo(workflows.UpdatePortInfo):
|
||||||
|
depends_on = ('network_id', 'port_id', 'instance_id')
|
||||||
|
|
||||||
|
|
||||||
|
class UpdatePortSecurityGroup(workflows.UpdatePortSecurityGroup):
|
||||||
|
depends_on = ("port_id", 'target_tenant_id', 'instance_id')
|
||||||
|
|
||||||
|
|
||||||
|
class UpdatePort(workflows.UpdatePort):
|
||||||
|
default_steps = (UpdatePortInfo, UpdatePortSecurityGroup)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse("horizon:project:instances:detail",
|
||||||
|
args=(self.context['instance_id'],))
|
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
"Interfaces" tab is added to the instance detail page. The new tab shows
|
||||||
|
a list of ports attached to an instance. Users now have an easy way to
|
||||||
|
access the list of ports of the instance and edit security groups per port.
|
Loading…
x
Reference in New Issue
Block a user