diff --git a/openstack_dashboard/dashboards/admin/routers/forms.py b/openstack_dashboard/dashboards/admin/routers/forms.py index 23ebba31c7..f32c6aaf1d 100644 --- a/openstack_dashboard/dashboards/admin/routers/forms.py +++ b/openstack_dashboard/dashboards/admin/routers/forms.py @@ -11,9 +11,30 @@ # under the License. from django.urls import reverse_lazy +from django.utils.translation import ugettext_lazy as _ +from horizon import forms + +from openstack_dashboard import api from openstack_dashboard.dashboards.project.routers import forms as r_forms +class CreateForm(r_forms.CreateForm): + tenant_id = forms.ThemableChoiceField(label=_("Project")) + # Other fields which are not defined in field_order will be + # placed in the default order. + field_order = ['name', 'tenant_id'] + failure_url = 'horizon:admin:routers:index' + + def __init__(self, request, *args, **kwargs): + super(CreateForm, self).__init__(request, *args, **kwargs) + tenant_choices = [('', _("Select a project"))] + tenants, __ = api.keystone.tenant_list(request) + for tenant in tenants: + if tenant.enabled: + tenant_choices.append((tenant.id, tenant.name)) + self.fields['tenant_id'].choices = tenant_choices + + class UpdateForm(r_forms.UpdateForm): redirect_url = reverse_lazy('horizon:admin:routers:index') diff --git a/openstack_dashboard/dashboards/admin/routers/tables.py b/openstack_dashboard/dashboards/admin/routers/tables.py index 478c1a9987..2a9d409dd7 100644 --- a/openstack_dashboard/dashboards/admin/routers/tables.py +++ b/openstack_dashboard/dashboards/admin/routers/tables.py @@ -23,6 +23,10 @@ class DeleteRouter(r_tables.DeleteRouter): redirect_url = "horizon:admin:routers:index" +class CreateRouter(r_tables.CreateRouter): + url = "horizon:admin:routers:create" + + class EditRouter(r_tables.EditRouter): url = "horizon:admin:routers:update" @@ -52,7 +56,8 @@ class RoutersTable(r_tables.RoutersTable): verbose_name = _("Routers") status_columns = ["status"] row_class = UpdateRow - table_actions = (DeleteRouter, AdminRoutersFilterAction) + table_actions = (CreateRouter, DeleteRouter, + AdminRoutersFilterAction) row_actions = (EditRouter, DeleteRouter,) columns = ('tenant', 'name', 'status', 'distributed', 'ext_net', 'ha', 'availability_zones', 'admin_state',) diff --git a/openstack_dashboard/dashboards/admin/routers/tests.py b/openstack_dashboard/dashboards/admin/routers/tests.py index cef42a7be3..4c25de5b81 100644 --- a/openstack_dashboard/dashboards/admin/routers/tests.py +++ b/openstack_dashboard/dashboards/admin/routers/tests.py @@ -19,6 +19,7 @@ import mock from openstack_dashboard import api from openstack_dashboard.dashboards.project.routers import tests as r_test from openstack_dashboard.test import helpers as test +from openstack_dashboard.usage import quotas INDEX_TEMPLATE = 'horizon/common/_data_table_view.html' @@ -72,10 +73,13 @@ class RouterTests(RouterMixin, r_test.RouterTestCase, test.BaseAdminViewTests): @test.create_mocks({api.neutron: ('router_list', 'network_list', 'is_extension_supported'), - api.keystone: ('tenant_list',)}) + api.keystone: ('tenant_list',), + quotas: ('tenant_quota_usages',)}) def test_index(self): tenants = self.tenants.list() + quota_data = self.neutron_quota_usages.first() self.mock_router_list.return_value = self.routers.list() + self.mock_tenant_quota_usages.return_value = quota_data self.mock_tenant_list.return_value = [tenants, False] self.mock_is_extension_supported.return_value = True self._mock_external_network_list() @@ -88,14 +92,20 @@ class RouterTests(RouterMixin, r_test.RouterTestCase, test.BaseAdminViewTests): self.mock_router_list.assert_called_once_with(test.IsHttpRequest()) self.mock_tenant_list.assert_called_once_with(test.IsHttpRequest()) + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_tenant_quota_usages, 2, + mock.call(test.IsHttpRequest(), targets=('router',))) self.mock_is_extension_supported.assert_called_once_with( test.IsHttpRequest(), "router_availability_zone") self._check_mock_external_network_list() @test.create_mocks({api.neutron: ('router_list', - 'is_extension_supported')}) + 'is_extension_supported'), + quotas: ('tenant_quota_usages',)}) def test_index_router_list_exception(self): + quota_data = self.neutron_quota_usages.first() self.mock_router_list.side_effect = self.exceptions.neutron + self.mock_tenant_quota_usages.return_value = quota_data self.mock_is_extension_supported.return_value = True res = self.client.get(self.INDEX_URL) @@ -104,6 +114,9 @@ class RouterTests(RouterMixin, r_test.RouterTestCase, test.BaseAdminViewTests): self.assertEqual(len(res.context['table'].data), 0) self.assertMessageCount(res, error=1) self.mock_router_list.assert_called_once_with(test.IsHttpRequest()) + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_tenant_quota_usages, 2, + mock.call(test.IsHttpRequest(), targets=('router',))) self.mock_is_extension_supported.assert_called_once_with( test.IsHttpRequest(), "router_availability_zone") @@ -111,13 +124,16 @@ class RouterTests(RouterMixin, r_test.RouterTestCase, test.BaseAdminViewTests): 'router_list_on_l3_agent', 'network_list', 'is_extension_supported'), - api.keystone: ('tenant_list',)}) + api.keystone: ('tenant_list',), + quotas: ('tenant_quota_usages',)}) def test_list_by_l3_agent(self): tenants = self.tenants.list() + quota_data = self.neutron_quota_usages.first() agent = self.agents.list()[1] self.mock_agent_list.return_value = [agent] self.mock_router_list_on_l3_agent.return_value = self.routers.list() self.mock_tenant_list.return_value = [tenants, False] + self.mock_tenant_quota_usages.return_value = quota_data self.mock_is_extension_supported.return_value = True self._mock_external_network_list() @@ -134,6 +150,9 @@ class RouterTests(RouterMixin, r_test.RouterTestCase, test.BaseAdminViewTests): self.mock_router_list_on_l3_agent.assert_called_once_with( test.IsHttpRequest(), agent.id, search_opts=None) self.mock_tenant_list.assert_called_once_with(test.IsHttpRequest()) + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_tenant_quota_usages, 2, + mock.call(test.IsHttpRequest(), targets=('router',))) self.mock_is_extension_supported.assert_called_once_with( test.IsHttpRequest(), "router_availability_zone") self._check_mock_external_network_list() @@ -141,10 +160,13 @@ class RouterTests(RouterMixin, r_test.RouterTestCase, test.BaseAdminViewTests): @test.create_mocks({api.neutron: ('router_list', 'network_list', 'is_extension_supported'), - api.keystone: ('tenant_list',)}) + api.keystone: ('tenant_list',), + quotas: ('tenant_quota_usages',)}) def test_set_external_network_empty(self): router = self.routers.first() + quota_data = self.neutron_quota_usages.first() self.mock_router_list.return_value = [router] + self.mock_tenant_quota_usages.return_value = quota_data self.mock_is_extension_supported.return_value = True self.mock_tenant_list.return_value = [self.tenants.list(), False] self._mock_external_network_list(alter_ids=True) @@ -159,6 +181,9 @@ class RouterTests(RouterMixin, r_test.RouterTestCase, test.BaseAdminViewTests): self.assertMessageCount(res, error=1) self.mock_router_list.assert_called_once_with(test.IsHttpRequest()) + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_tenant_quota_usages, 2, + mock.call(test.IsHttpRequest(), targets=('router',))) self.mock_is_extension_supported.assert_called_once_with( test.IsHttpRequest(), "router_availability_zone") self.mock_tenant_list.assert_called_once_with(test.IsHttpRequest()) @@ -173,14 +198,17 @@ class RouterTests(RouterMixin, r_test.RouterTestCase, test.BaseAdminViewTests): 'port_list', 'router_delete', 'is_extension_supported'), - api.keystone: ('tenant_list',)}) + api.keystone: ('tenant_list',), + quotas: ('tenant_quota_usages',)}) def test_router_delete(self): router = self.routers.first() tenants = self.tenants.list() + quota_data = self.neutron_quota_usages.first() self.mock_router_list.return_value = self.routers.list() self.mock_tenant_list.return_value = [tenants, False] self._mock_external_network_list(count=3) + self.mock_tenant_quota_usages.return_value = quota_data self.mock_is_extension_supported.return_value = True self.mock_port_list.return_value = [] self.mock_router_delete.return_value = None @@ -201,6 +229,9 @@ class RouterTests(RouterMixin, r_test.RouterTestCase, test.BaseAdminViewTests): self.mock_tenant_list, 3, mock.call(test.IsHttpRequest())) self._check_mock_external_network_list(count=3) + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_tenant_quota_usages, 4, + mock.call(test.IsHttpRequest(), targets=('router',))) self.assert_mock_multiple_calls_with_same_arguments( self.mock_is_extension_supported, 3, mock.call(test.IsHttpRequest(), 'router_availability_zone')) @@ -215,15 +246,18 @@ class RouterTests(RouterMixin, r_test.RouterTestCase, test.BaseAdminViewTests): 'router_remove_interface', 'router_delete', 'is_extension_supported'), - api.keystone: ('tenant_list',)}) + api.keystone: ('tenant_list',), + quotas: ('tenant_quota_usages',)}) def test_router_with_interface_delete(self): router = self.routers.first() ports = self.ports.list() tenants = self.tenants.list() + quota_data = self.neutron_quota_usages.first() self.mock_router_list.return_value = self.routers.list() self.mock_tenant_list.return_value = [tenants, False] self._mock_external_network_list(count=3) + self.mock_tenant_quota_usages.return_value = quota_data self.mock_is_extension_supported.return_value = True self.mock_port_list.return_value = ports self.mock_router_remove_interface.return_value = None @@ -245,6 +279,9 @@ class RouterTests(RouterMixin, r_test.RouterTestCase, test.BaseAdminViewTests): self.mock_tenant_list, 3, mock.call(test.IsHttpRequest())) self._check_mock_external_network_list(count=3) + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_tenant_quota_usages, 4, + mock.call(test.IsHttpRequest(), targets=('router',))) self.assert_mock_multiple_calls_with_same_arguments( self.mock_is_extension_supported, 3, mock.call(test.IsHttpRequest(), 'router_availability_zone')) @@ -257,9 +294,12 @@ class RouterTests(RouterMixin, r_test.RouterTestCase, test.BaseAdminViewTests): self.mock_router_delete.assert_called_once_with( test.IsHttpRequest(), router.id) - @test.create_mocks({api.neutron: ('is_extension_supported',)}) + @test.create_mocks({api.neutron: ('is_extension_supported',), + quotas: ('tenant_quota_usages',)}) @test.update_settings(FILTER_DATA_FIRST={'admin.routers': True}) def test_routers_list_with_admin_filter_first(self): + quota_data = self.neutron_quota_usages.first() + self.mock_tenant_quota_usages.return_value = quota_data self.mock_is_extension_supported.return_value = True res = self.client.get(self.INDEX_URL) @@ -267,14 +307,20 @@ class RouterTests(RouterMixin, r_test.RouterTestCase, test.BaseAdminViewTests): routers = res.context['table'].data self.assertItemsEqual(routers, []) + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_tenant_quota_usages, 2, + mock.call(test.IsHttpRequest(), targets=('router',))) self.mock_is_extension_supported.assert_called_once_with( test.IsHttpRequest(), 'router_availability_zone') @test.create_mocks({api.neutron: ('is_extension_supported',), - api.keystone: ('tenant_list',)}) + api.keystone: ('tenant_list',), + quotas: ('tenant_quota_usages',)}) def test_routers_list_with_non_exist_tenant_filter(self): self.mock_is_extension_supported.return_value = True self.mock_tenant_list.return_value = [self.tenants.list(), False] + quota_data = self.neutron_quota_usages.first() + self.mock_tenant_quota_usages.return_value = quota_data self.client.post( self.INDEX_URL, @@ -285,12 +331,19 @@ class RouterTests(RouterMixin, r_test.RouterTestCase, test.BaseAdminViewTests): routers = res.context['table'].data self.assertItemsEqual(routers, []) + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_tenant_quota_usages, 2, + mock.call(test.IsHttpRequest(), targets=('router',))) self.assert_mock_multiple_calls_with_same_arguments( self.mock_is_extension_supported, 2, mock.call(test.IsHttpRequest(), "router_availability_zone")) self.mock_tenant_list.assert_called_once_with(test.IsHttpRequest()) +class RouterViewTests(r_test.RouterViewTests): + DASHBOARD = 'admin' + + class RouterTestsNoL3Agent(RouterTests): support_l3_agent = False diff --git a/openstack_dashboard/dashboards/admin/routers/urls.py b/openstack_dashboard/dashboards/admin/routers/urls.py index 33363b9fdd..e2a600f280 100644 --- a/openstack_dashboard/dashboards/admin/routers/urls.py +++ b/openstack_dashboard/dashboards/admin/routers/urls.py @@ -22,6 +22,7 @@ ROUTER_URL = r'^(?P[^/]+)/%s' urlpatterns = [ url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^create/$', views.CreateView.as_view(), name='create'), url(ROUTER_URL % '$', views.DetailView.as_view(), name='detail'), diff --git a/openstack_dashboard/dashboards/admin/routers/views.py b/openstack_dashboard/dashboards/admin/routers/views.py index b4b3c8a379..2e74e99728 100644 --- a/openstack_dashboard/dashboards/admin/routers/views.py +++ b/openstack_dashboard/dashboards/admin/routers/views.py @@ -120,6 +120,13 @@ class DetailView(r_views.DetailView): return context +class CreateView(r_views.CreateView): + form_class = rforms.CreateForm + template_name = 'project/routers/create.html' + success_url = reverse_lazy("horizon:admin:routers:index") + submit_url = reverse_lazy("horizon:admin:routers:create") + + class UpdateView(r_views.UpdateView): form_class = rforms.UpdateForm template_name = 'project/routers/update.html' diff --git a/openstack_dashboard/dashboards/project/routers/forms.py b/openstack_dashboard/dashboards/project/routers/forms.py index a1bc78ab65..c27dccaf7c 100644 --- a/openstack_dashboard/dashboards/project/routers/forms.py +++ b/openstack_dashboard/dashboards/project/routers/forms.py @@ -119,6 +119,10 @@ class CreateForm(forms.SelfHandlingForm): try: params = {'name': data['name'], 'admin_state_up': data['admin_state_up']} + # NOTE: admin form allows to specify tenant_id. + # We have the logic here to simplify the logic. + if 'tenant_id' in data and data['tenant_id']: + params['tenant_id'] = data['tenant_id'] if 'external_network' in data and data['external_network']: params['external_gateway_info'] = {'network_id': data['external_network']} diff --git a/releasenotes/notes/add-create-button-to-admin-panel-02cbe5d96036ac4c.yaml b/releasenotes/notes/add-create-button-to-admin-panel-02cbe5d96036ac4c.yaml new file mode 100644 index 0000000000..cf1f23c3fc --- /dev/null +++ b/releasenotes/notes/add-create-button-to-admin-panel-02cbe5d96036ac4c.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add "Create Router" button to Admin/Network/Routers panel.