From b9664a1bf139e6714aa4d507a68ce03d551779aa Mon Sep 17 00:00:00 2001 From: Trygve Vea Date: Fri, 13 Oct 2017 23:30:16 +0200 Subject: [PATCH] Routers can be created with availability zone When 'availability_zone'-extension is enabled, we present the user with a drop-down menu containing a list of available availability zones. This sets the 'availability_zone_hints' parameter on router creation. Change-Id: I96293202ddd855823e89c4c7ba0b1f6a6423aab2 Partial-bug: #1716638 --- openstack_dashboard/api/neutron.py | 12 ++++ .../dashboards/project/routers/forms.py | 19 ++++++ .../dashboards/project/routers/tests.py | 67 +++++++++++++++++-- .../test/test_data/neutron_data.py | 13 +++- 4 files changed, 106 insertions(+), 5 deletions(-) diff --git a/openstack_dashboard/api/neutron.py b/openstack_dashboard/api/neutron.py index 49e9478e27..a57350041e 100644 --- a/openstack_dashboard/api/neutron.py +++ b/openstack_dashboard/api/neutron.py @@ -1728,3 +1728,15 @@ def policy_get(request, policy_id, **kwargs): policy = neutronclient(request).show_qos_policy( policy_id, **kwargs).get('policy') return QoSPolicy(policy) + + +@profiler.trace +def list_availability_zones(request, resource=None, state=None): + az_list = neutronclient(request).list_availability_zones().get( + 'availability_zones') + if resource: + az_list = [az for az in az_list if az['resource'] == resource] + if state: + az_list = [az for az in az_list if az['state'] == state] + + return sorted(az_list, key=lambda zone: zone['name']) diff --git a/openstack_dashboard/dashboards/project/routers/forms.py b/openstack_dashboard/dashboards/project/routers/forms.py index 27e9d9592f..f5ad224af3 100644 --- a/openstack_dashboard/dashboards/project/routers/forms.py +++ b/openstack_dashboard/dashboards/project/routers/forms.py @@ -41,6 +41,12 @@ class CreateForm(forms.SelfHandlingForm): required=False) mode = forms.ChoiceField(label=_("Router Type")) ha = forms.ChoiceField(label=_("High Availability Mode")) + az_hints = forms.MultipleChoiceField( + label=_("Availability Zone Hints"), + required=False, + help_text=_("Availability Zones where the router may be scheduled. " + "Leaving this unset is equivalent to selecting all " + "Availability Zones")) failure_url = 'horizon:project:routers:index' def __init__(self, request, *args, **kwargs): @@ -70,6 +76,17 @@ class CreateForm(forms.SelfHandlingForm): else: del self.fields['external_network'] + az_supported = api.neutron.is_extension_supported( + self.request, 'router_availability_zone') + + if az_supported: + zones = api.neutron.list_availability_zones(self.request, 'router', + 'available') + self.fields['az_hints'].choices = [(zone['name'], zone['name']) + for zone in zones] + else: + del self.fields['az_hints'] + def _get_network_list(self, request): search_opts = {'router:external': True} try: @@ -94,6 +111,8 @@ class CreateForm(forms.SelfHandlingForm): if 'external_network' in data and data['external_network']: params['external_gateway_info'] = {'network_id': data['external_network']} + if 'az_hints' in data and data['az_hints']: + params['availability_zone_hints'] = data['az_hints'] if (self.dvr_allowed and data['mode'] != 'server_default'): params['distributed'] = (data['mode'] == 'distributed') if (self.ha_allowed and data['ha'] != 'server_default'): diff --git a/openstack_dashboard/dashboards/project/routers/tests.py b/openstack_dashboard/dashboards/project/routers/tests.py index 4a9812dc5f..b18bea5144 100644 --- a/openstack_dashboard/dashboards/project/routers/tests.py +++ b/openstack_dashboard/dashboards/project/routers/tests.py @@ -257,7 +257,8 @@ class RouterActionTests(RouterMixin, test.TestCase): @test.create_stubs({api.neutron: ('router_create', 'get_feature_permission', - 'network_list')}) + 'network_list', + 'is_extension_supported')}) def test_router_create_post(self): router = self.routers.first() api.neutron.get_feature_permission(IsA(http.HttpRequest), @@ -268,6 +269,9 @@ class RouterActionTests(RouterMixin, test.TestCase): .AndReturn(False) api.neutron.network_list(IsA(http.HttpRequest))\ .AndReturn(self.networks.list()) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + "router_availability_zone")\ + .AndReturn(False) params = {'name': router.name, 'admin_state_up': router.admin_state_up} api.neutron.router_create(IsA(http.HttpRequest), **params)\ @@ -284,7 +288,8 @@ class RouterActionTests(RouterMixin, test.TestCase): @test.create_stubs({api.neutron: ('router_create', 'get_feature_permission', - 'network_list')}) + 'network_list', + 'is_extension_supported')}) def test_router_create_post_mode_server_default(self): router = self.routers.first() api.neutron.get_feature_permission(IsA(http.HttpRequest), @@ -295,6 +300,9 @@ class RouterActionTests(RouterMixin, test.TestCase): .AndReturn(True) api.neutron.network_list(IsA(http.HttpRequest))\ .AndReturn(self.networks.list()) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + "router_availability_zone")\ + .AndReturn(False) params = {'name': router.name, 'admin_state_up': router.admin_state_up} api.neutron.router_create(IsA(http.HttpRequest), **params)\ @@ -313,7 +321,8 @@ class RouterActionTests(RouterMixin, test.TestCase): @test.create_stubs({api.neutron: ('router_create', 'get_feature_permission', - 'network_list')}) + 'network_list', + 'is_extension_supported')}) def test_dvr_ha_router_create_post(self): router = self.routers.first() api.neutron.get_feature_permission(IsA(http.HttpRequest), @@ -324,6 +333,9 @@ class RouterActionTests(RouterMixin, test.TestCase): .MultipleTimes().AndReturn(True) api.neutron.network_list(IsA(http.HttpRequest))\ .AndReturn(self.networks.list()) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + "router_availability_zone")\ + .AndReturn(False) param = {'name': router.name, 'distributed': True, 'ha': True, @@ -344,7 +356,47 @@ class RouterActionTests(RouterMixin, test.TestCase): @test.create_stubs({api.neutron: ('router_create', 'get_feature_permission', - 'network_list')}) + 'network_list', + 'is_extension_supported', + 'list_availability_zones')}) + def test_az_router_create_post(self): + router = self.routers.first() + api.neutron.get_feature_permission(IsA(http.HttpRequest), + "dvr", "create")\ + .MultipleTimes().AndReturn(False) + api.neutron.get_feature_permission(IsA(http.HttpRequest), + "l3-ha", "create")\ + .AndReturn(False) + api.neutron.network_list(IsA(http.HttpRequest))\ + .AndReturn(self.networks.list()) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + "router_availability_zone")\ + .AndReturn(True) + api.neutron.list_availability_zones(IsA(http.HttpRequest), + "router", "available")\ + .AndReturn(self.neutron_availability_zones.list()) + param = {'name': router.name, + 'availability_zone_hints': ['nova'], + 'admin_state_up': router.admin_state_up} + api.neutron.router_create(IsA(http.HttpRequest), **param)\ + .AndReturn(router) + self.mox.ReplayAll() + + form_data = {'name': router.name, + 'mode': 'server_default', + 'ha': 'server_default', + 'az_hints': 'nova', + 'admin_state_up': router.admin_state_up} + url = reverse('horizon:%s:routers:create' % self.DASHBOARD) + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, self.INDEX_URL) + + @test.create_stubs({api.neutron: ('router_create', + 'get_feature_permission', + 'network_list', + 'is_extension_supported')}) def test_router_create_post_exception_error_case_409(self): router = self.routers.first() api.neutron.get_feature_permission(IsA(http.HttpRequest), @@ -356,6 +408,9 @@ class RouterActionTests(RouterMixin, test.TestCase): self.exceptions.neutron.status_code = 409 api.neutron.network_list(IsA(http.HttpRequest))\ .MultipleTimes().AndReturn(self.networks.list()) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + "router_availability_zone")\ + .AndReturn(False) params = {'name': router.name, 'admin_state_up': router.admin_state_up} api.neutron.router_create(IsA(http.HttpRequest), **params)\ @@ -372,6 +427,7 @@ class RouterActionTests(RouterMixin, test.TestCase): @test.create_stubs({api.neutron: ('router_create', 'get_feature_permission', + 'is_extension_supported', 'network_list')}) def test_router_create_post_exception_error_case_non_409(self): router = self.routers.first() @@ -384,6 +440,9 @@ class RouterActionTests(RouterMixin, test.TestCase): self.exceptions.neutron.status_code = 999 api.neutron.network_list(IsA(http.HttpRequest))\ .MultipleTimes().AndReturn(self.networks.list()) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + "router_availability_zone")\ + .MultipleTimes().AndReturn(False) params = {'name': router.name, 'admin_state_up': router.admin_state_up} api.neutron.router_create(IsA(http.HttpRequest), **params)\ diff --git a/openstack_dashboard/test/test_data/neutron_data.py b/openstack_dashboard/test/test_data/neutron_data.py index 62297e576c..25928b55f9 100644 --- a/openstack_dashboard/test/test_data/neutron_data.py +++ b/openstack_dashboard/test/test_data/neutron_data.py @@ -46,6 +46,7 @@ def data(TEST): TEST.ip_availability = utils.TestDataContainer() TEST.qos_policies = utils.TestDataContainer() TEST.tp_ports = utils.TestDataContainer() + TEST.neutron_availability_zones = utils.TestDataContainer() # Data return by neutronclient. TEST.api_agents = utils.TestDataContainer() @@ -361,7 +362,8 @@ def data(TEST): 'distributed': True, 'external_gateway_info': {'network_id': ext_net['id']}, - 'tenant_id': '1'} + 'tenant_id': '1', + 'availability_zone_hints': ['nova']} TEST.api_routers.add(router_dict) TEST.routers.add(neutron.Router(router_dict)) router_dict = {'id': '10e3dc42-1ce1-4d48-87cf-7fc333055d6c', @@ -879,3 +881,12 @@ def data(TEST): {'trunk_id': tdata['trunk_id'], 'segmentation_type': 'vlan', 'segmentation_id': tdata['tag_2']})) + + # Availability Zones + TEST.neutron_availability_zones.add( + { + 'state': 'available', + 'resource': 'router', + 'name': 'nova' + } + )