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.dashboards.project.instances import console
|
||||
from openstack_dashboard.dashboards.project.instances import interfaces_tables
|
||||
|
||||
|
||||
class OverviewTab(tabs.Tab):
|
||||
@ -38,6 +39,32 @@ class OverviewTab(tabs.Tab):
|
||||
"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):
|
||||
name = _("Log")
|
||||
slug = "log"
|
||||
@ -110,5 +137,5 @@ class AuditTab(tabs.TableTab):
|
||||
|
||||
class InstanceDetailTabs(tabs.DetailTabsGroup):
|
||||
slug = "instance_details"
|
||||
tabs = (OverviewTab, LogTab, ConsoleTab, AuditTab)
|
||||
tabs = (OverviewTab, InterfacesTab, LogTab, ConsoleTab, AuditTab)
|
||||
sticky = True
|
||||
|
@ -1350,22 +1350,30 @@ class InstanceDetailTests(InstanceTestBase):
|
||||
|
||||
return res
|
||||
|
||||
@helpers.create_mocks({api.neutron: ['is_extension_supported']})
|
||||
def test_instance_details_volumes(self):
|
||||
server = self.servers.first()
|
||||
volumes = [self.volumes.list()[1]]
|
||||
security_groups = self.security_groups.list()
|
||||
|
||||
self.mock_is_extension_supported.return_value = False
|
||||
|
||||
res = self._get_instance_details(
|
||||
server, volumes_return=volumes,
|
||||
security_groups_return=security_groups)
|
||||
|
||||
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):
|
||||
server = self.servers.first()
|
||||
volumes = self.volumes.list()[1:3]
|
||||
security_groups = self.security_groups.list()
|
||||
|
||||
self.mock_is_extension_supported.return_value = False
|
||||
|
||||
res = self._get_instance_details(
|
||||
server, volumes_return=volumes,
|
||||
security_groups_return=security_groups)
|
||||
@ -1375,10 +1383,15 @@ class InstanceDetailTests(InstanceTestBase):
|
||||
"/dev/hda")
|
||||
self.assertEqual(res.context['instance'].volumes[1].device,
|
||||
"/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):
|
||||
server = self.servers.first()
|
||||
|
||||
self.mock_is_extension_supported.return_value = False
|
||||
|
||||
tg = tabs.InstanceDetailTabs(self.request, instance=server)
|
||||
qs = "?%s=%s" % (tg.param_name, tg.get_tab("overview").get_id())
|
||||
res = self._get_instance_details(server, qs)
|
||||
@ -1392,9 +1405,16 @@ class InstanceDetailTests(InstanceTestBase):
|
||||
self.assertContains(res, "<dt>empty</dt>", 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):
|
||||
server = self.servers.first()
|
||||
|
||||
self.mock_is_extension_supported.return_value = False
|
||||
|
||||
server.status = 'ERROR'
|
||||
server.fault = {"message": "NoValidHost",
|
||||
"code": 500,
|
||||
@ -1408,8 +1428,11 @@ class InstanceDetailTests(InstanceTestBase):
|
||||
|
||||
res = self._get_instance_details(server)
|
||||
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):
|
||||
server = self.servers.first()
|
||||
CONSOLE_OUTPUT = '/vncserver'
|
||||
@ -1417,6 +1440,7 @@ class InstanceDetailTests(InstanceTestBase):
|
||||
CONSOLE_URL = CONSOLE_OUTPUT + CONSOLE_TITLE
|
||||
|
||||
self.mock_get_console.return_value = ('VNC', CONSOLE_URL)
|
||||
self.mock_is_extension_supported.return_value = False
|
||||
|
||||
tg = tabs.InstanceDetailTabs(self.request, instance=server)
|
||||
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.mock_get_console.assert_called_once_with(
|
||||
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)
|
||||
@helpers.create_mocks({api.neutron: ['is_extension_supported']})
|
||||
def test_instance_details_console_tab_deactivated(self):
|
||||
server = self.servers.first()
|
||||
|
||||
self.mock_is_extension_supported.return_value = False
|
||||
|
||||
tg = tabs.InstanceDetailTabs(self.request, instance=server)
|
||||
self.assertIsNone(tg.get_tab("console"))
|
||||
res = self._get_instance_details(server)
|
||||
@ -1445,6 +1475,10 @@ class InstanceDetailTests(InstanceTestBase):
|
||||
for tab in res.context_data['tab_group'].get_loaded_tabs():
|
||||
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',)})
|
||||
def test_instance_details_exception(self):
|
||||
server = self.servers.first()
|
||||
@ -1475,19 +1509,25 @@ class InstanceDetailTests(InstanceTestBase):
|
||||
self.assertEqual(403, res.status_code)
|
||||
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):
|
||||
server = self.servers.first()
|
||||
self.mock_is_extension_supported.return_value = False
|
||||
res = self._get_instance_details(server, flavor_exception=True)
|
||||
self.assertTemplateUsed(res,
|
||||
'project/instances/_detail_overview.html')
|
||||
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):
|
||||
server = self.servers.first()
|
||||
CONSOLE_OUTPUT = 'output'
|
||||
|
||||
self.mock_server_console_output.return_value = CONSOLE_OUTPUT
|
||||
self.mock_is_extension_supported.return_value = False
|
||||
|
||||
url = reverse('horizon:project:instances:console',
|
||||
args=[server.id])
|
||||
@ -1500,12 +1540,16 @@ class InstanceDetailTests(InstanceTestBase):
|
||||
self.assertContains(res, CONSOLE_OUTPUT)
|
||||
self.mock_server_console_output.assert_called_once_with(
|
||||
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):
|
||||
server = self.servers.first()
|
||||
|
||||
self.mock_server_console_output.side_effect = self.exceptions.nova
|
||||
self.mock_is_extension_supported.return_value = False
|
||||
|
||||
url = reverse('horizon:project:instances:console',
|
||||
args=[server.id])
|
||||
@ -1516,9 +1560,13 @@ class InstanceDetailTests(InstanceTestBase):
|
||||
self.assertContains(res, "Unable to get log for")
|
||||
self.mock_server_console_output.assert_called_once_with(
|
||||
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):
|
||||
server = self.servers.first()
|
||||
self.mock_is_extension_supported.return_value = False
|
||||
|
||||
url = reverse('horizon:project:instances:console',
|
||||
args=[server.id])
|
||||
@ -1531,6 +1579,9 @@ class InstanceDetailTests(InstanceTestBase):
|
||||
|
||||
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'],
|
||||
console: ['get_console']})
|
||||
def test_instance_auto_console(self):
|
||||
|
@ -54,5 +54,7 @@ urlpatterns = [
|
||||
url(r'^(?P<instance_id>[^/]+)/detach_volume/$',
|
||||
views.DetachVolumeView.as_view(),
|
||||
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
|
||||
from openstack_dashboard.dashboards.project.instances \
|
||||
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.views import get_url_with_pagination
|
||||
|
||||
@ -655,3 +657,24 @@ class DetachInterfaceView(forms.ModalFormView):
|
||||
submit_url = "horizon:project:instances:detach_interface"
|
||||
self.submit_url = reverse(submit_url, kwargs=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
|
||||
from openstack_dashboard.dashboards.project.instances.workflows.\
|
||||
update_instance import UpdateInstance
|
||||
from openstack_dashboard.dashboards.project.instances.workflows.\
|
||||
update_port import UpdatePort
|
||||
|
||||
__all__ = [
|
||||
'LaunchInstance',
|
||||
'ResizeInstance',
|
||||
'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…
Reference in New Issue
Block a user