Merge "Add Extra Routes to Router Tabs View"

This commit is contained in:
Jenkins 2015-04-28 14:00:29 +00:00 committed by Gerrit Code Review
commit 6715d794c2
18 changed files with 541 additions and 7 deletions

View File

@ -142,6 +142,15 @@ class Router(NeutronAPIDictWrapper):
super(Router, self).__init__(apiresource)
class RouterStaticRoute(NeutronAPIDictWrapper):
"""Wrapper for neutron routes extra route."""
def __init__(self, route):
super(RouterStaticRoute, self).__init__(route)
# Horizon references id property for table operations
self.id = route['nexthop'] + ":" + route['destination']
class SecurityGroup(NeutronAPIDictWrapper):
# Required attributes: id, name, description, tenant_id, rules
@ -896,6 +905,39 @@ def router_remove_gateway(request, router_id):
neutronclient(request).remove_gateway_router(router_id)
def router_static_route_list(request, router_id=None):
router = router_get(request, router_id)
try:
routes = [RouterStaticRoute(r) for r in router.routes]
except AttributeError:
LOG.debug("router_static_route_list(): router_id=%s, "
"router=%s", (router_id, router))
return []
return routes
def router_static_route_remove(request, router_id, route_ids):
currentroutes = router_static_route_list(request, router_id=router_id)
newroutes = []
for oldroute in currentroutes:
if oldroute.id not in route_ids:
newroutes.append({'nexthop': oldroute.nexthop,
'destination': oldroute.destination})
body = {'routes': newroutes}
new = router_update(request, router_id, **body)
return new
def router_static_route_add(request, router_id, newroute):
body = {}
currentroutes = router_static_route_list(request, router_id=router_id)
body['routes'] = [newroute] + [{'nexthop': r.nexthop,
'destination': r.destination}
for r in currentroutes]
new = router_update(request, router_id, **body)
return new
def tenant_quota_get(request, tenant_id):
return base.QuotaSet(neutronclient(request).show_quota(tenant_id)['quota'])

View File

@ -0,0 +1,27 @@
# Copyright 2015, Thales Services SAS
# All Rights Reserved.
#
# 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.utils.translation import ugettext_lazy as _
from openstack_dashboard.dashboards.project.routers.extensions.extraroutes\
import tables as routes_table
class AdminRouterRoutesTable(routes_table.ExtraRoutesTable):
class Meta(object):
# Redifine Meta class to disable action (admin)
name = "extra_routes"
verbose_name = _("Static Routes")

View File

@ -12,7 +12,11 @@
# License for the specific language governing permissions and limitations
# under the License.
from openstack_dashboard.dashboards.admin.routers.extensions.extraroutes\
import tables as ertbl
from openstack_dashboard.dashboards.admin.routers.ports import tables as ptbl
from openstack_dashboard.dashboards.project.routers.extensions.extraroutes\
import tabs as er_tabs
from openstack_dashboard.dashboards.project.routers.extensions.routerrules\
import tabs as rr_tabs
from openstack_dashboard.dashboards.project.routers import tabs as r_tabs
@ -22,11 +26,15 @@ class OverviewTab(r_tabs.OverviewTab):
template_name = "project/routers/_detail_overview.html"
class ExtraRoutesTab(er_tabs.ExtraRoutesTab):
table_classes = (ertbl.AdminRouterRoutesTable,)
class InterfacesTab(r_tabs.InterfacesTab):
table_classes = (ptbl.PortsTable,)
class RouterDetailTabs(r_tabs.RouterDetailTabs):
tabs = (OverviewTab, InterfacesTab, rr_tabs.RulesGridTab,
tabs = (OverviewTab, InterfacesTab, ExtraRoutesTab, rr_tabs.RulesGridTab,
rr_tabs.RouterRulesTab)
sticky = True

View File

@ -162,3 +162,9 @@ class RouterTests(test.BaseAdminViewTests, r_test.RouterTests):
self.assertNoFormErrors(res)
self.assertMessageCount(response=res, success=1)
self.assertIn('Deleted Router: ' + router.name, res.content)
class RouterRouteTest(test.BaseAdminViewTests, r_test.RouterRouteTests):
DASHBOARD = 'admin'
INDEX_URL = reverse('horizon:%s:routers:index' % DASHBOARD)
DETAIL_PATH = 'horizon:%s:routers:detail' % DASHBOARD

View File

@ -0,0 +1,60 @@
# Copyright 2015, Thales Services SAS
# All Rights Reserved.
#
# 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.
import logging
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import messages
from neutronclient.common import exceptions as neutron_exc
from openstack_dashboard.api import neutron as api
LOG = logging.getLogger(__name__)
class AddRouterRoute(forms.SelfHandlingForm):
destination = forms.IPField(label=_("Destination CIDR"), mask=True)
nexthop = forms.IPField(label=_("Next Hop"))
failure_url = 'horizon:project:routers:detail'
def handle(self, request, data, **kwargs):
router_id = self.initial['router_id']
try:
route = {'nexthop': data['nexthop'],
'destination': data['destination']}
api.router_static_route_add(request,
router_id,
route)
msg = _('Static route added')
LOG.debug(msg)
messages.success(request, msg)
return True
except neutron_exc.BadRequest as e:
msg = (_('Invalid format for routes : %s') % e)
LOG.info(msg)
messages.error(request, msg)
redirect = reverse(self.failure_url, args=[router_id])
exceptions.handle(request, msg, redirect=redirect)
except Exception as e:
msg = (_('Failed to add route : %s') % e)
LOG.info(msg)
messages.error(request, msg)
redirect = reverse(self.failure_url, args=[router_id])
exceptions.handle(request, msg, redirect=redirect)

View File

@ -0,0 +1,77 @@
# Copyright 2015, Thales Services SAS
# All Rights Reserved.
#
# 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.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from openstack_dashboard.api import neutron as api
from openstack_dashboard import policy
from horizon import tables
class AddRouterRoute(policy.PolicyTargetMixin, tables.LinkAction):
name = "create"
verbose_name = _("Add Static Route")
url = "horizon:project:routers:addrouterroute"
classes = ("ajax-modal",)
icon = "plus"
policy_rules = (("network", "update_router"),)
def get_link_url(self, datum=None):
router_id = self.table.kwargs['router_id']
return reverse(self.url, args=(router_id,))
class RemoveRouterRoute(policy.PolicyTargetMixin, tables.DeleteAction):
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Delete Static Route",
u"Delete Static Routes",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Deleted Static Route",
u"Deleted Static Routes",
count
)
failure_url = 'horizon:project:routers:detail'
policy_rules = (("network", "update_router"),)
def delete(self, request, obj_id):
router_id = self.table.kwargs['router_id']
api.router_static_route_remove(request, router_id, [obj_id])
class ExtraRoutesTable(tables.DataTable):
destination = tables.Column("destination",
verbose_name=_("Destination CIDR"))
nexthop = tables.Column("nexthop", verbose_name=_("Next Hop"))
def get_object_display(self, datum):
"""Display ExtraRoutes when deleted."""
return (super(ExtraRoutesTable, self).get_object_display(datum)
or datum.destination + " -> " + datum.nexthop)
class Meta(object):
name = "extra_routes"
verbose_name = _("Static Routes")
table_actions = (AddRouterRoute, RemoveRouterRoute)
row_actions = (RemoveRouterRoute, )

View File

@ -0,0 +1,49 @@
# Copyright 2015, Thales Services SAS
# All Rights Reserved.
#
# 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.
import logging
from django.utils.translation import ugettext_lazy as _
from horizon import tabs
from openstack_dashboard.api import neutron as api
from openstack_dashboard.dashboards.project.routers.extensions.extraroutes\
import tables as ertbl
LOG = logging.getLogger(__name__)
class ExtraRoutesTab(tabs.TableTab):
table_classes = (ertbl.ExtraRoutesTable,)
name = _("Static Routes")
slug = "extraroutes"
template_name = "horizon/common/_detail_table.html"
def allowed(self, request):
try:
return api.is_extension_supported(request, 'extraroute')
except Exception:
LOG.info(_("Failed to check if Neutron extraroute extension is "
"supported"))
return False
def get_extra_routes_data(self):
try:
extraroutes = getattr(self.tab_group.kwargs['router'], 'routes')
except AttributeError:
extraroutes = []
return [api.RouterStaticRoute(r) for r in extraroutes]

View File

@ -0,0 +1,59 @@
# Copyright 2015, Thales Services SAS
# All Rights Reserved.
#
# 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.
import logging
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon.utils import memoized
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.routers.extensions.extraroutes\
import forms as erforms
LOG = logging.getLogger(__name__)
class AddRouterRouteView(forms.ModalFormView):
form_class = erforms.AddRouterRoute
template_name = 'project/routers/extensions/routerroutes/create.html'
url = 'horizon:project:routers:detail'
def get_success_url(self):
return reverse(self.url,
args=(self.kwargs['router_id'],))
@memoized.memoized_method
def get_object(self):
try:
router_id = self.kwargs["router_id"]
return api.neutron.router_get(self.request, router_id)
except Exception:
redirect = reverse(self.url, args=[router_id])
msg = _("Unable to retrieve router.")
exceptions.handle(self.request, msg, redirect=redirect)
def get_context_data(self, **kwargs):
context = super(AddRouterRouteView, self).get_context_data(**kwargs)
context['router'] = self.get_object()
return context
def get_initial(self):
router = self.get_object()
return {"router_id": self.kwargs['router_id'],
"router_name": router.name}

View File

@ -16,6 +16,8 @@ from django.utils.translation import ugettext_lazy as _
from horizon import tabs
from openstack_dashboard.dashboards.project.routers.extensions.extraroutes\
import tabs as er_tabs
from openstack_dashboard.dashboards.project.routers.extensions.routerrules\
import tabs as rr_tabs
from openstack_dashboard.dashboards.project.routers.ports import tables as ptbl
@ -42,6 +44,6 @@ class InterfacesTab(tabs.TableTab):
class RouterDetailTabs(tabs.TabGroup):
slug = "router_details"
tabs = (OverviewTab, InterfacesTab, rr_tabs.RulesGridTab,
rr_tabs.RouterRulesTab)
tabs = (OverviewTab, InterfacesTab, er_tabs.ExtraRoutesTab,
rr_tabs.RulesGridTab, rr_tabs.RouterRulesTab)
sticky = True

View File

@ -0,0 +1,29 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}add_routerroute_form{% endblock %}
{% block form_action %}{% url 'horizon:project:routers:addrouterroute' router.id %}
{% endblock %}
{% block modal-header %}{% trans "Add Static Route" %}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description:" %}</h3>
<p>
{% trans "Add static route to the router." %}<br/>
{% trans "Next Hop IP must be a part of one of the subnets to which the router interfaces are connected." %}
</p>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Add route" %}" />
<a href="{% url 'horizon:project:routers:index' %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Add Router Route" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Add Router Route") %}
{% endblock page_header %}
{% block main %}
{% include "project/routers/extensions/routerroutes/_create.html" %}
{% endblock %}

View File

@ -30,9 +30,11 @@ from openstack_dashboard.usage import quotas
class RouterMixin(object):
@test.create_stubs({
api.neutron: ('router_get', 'port_list',
'network_get'),
'network_get', 'is_extension_supported'),
})
def _get_detail(self, router):
def _get_detail(self, router, extraroute=True):
api.neutron.is_extension_supported(IsA(http.HttpRequest), 'extraroute')\
.MultipleTimes().AndReturn(extraroute)
api.neutron.router_get(IsA(http.HttpRequest), router.id)\
.AndReturn(router)
api.neutron.port_list(IsA(http.HttpRequest),
@ -750,11 +752,14 @@ class RouterRuleTests(RouterMixin, test.TestCase):
self._test_router_addrouterrule(raise_error=True)
@test.create_stubs({api.neutron: ('router_get', 'router_update',
'port_list', 'network_get')})
'port_list', 'network_get',
'is_extension_supported')})
def test_router_removerouterrule(self):
pre_router = self.routers_with_rules.first()
post_router = copy.deepcopy(pre_router)
rule = post_router['router_rules'].pop()
api.neutron.is_extension_supported(IsA(http.HttpRequest), 'extraroute')\
.AndReturn(False)
api.neutron.router_get(IsA(http.HttpRequest),
pre_router.id).AndReturn(pre_router)
params = {}
@ -779,7 +784,8 @@ class RouterRuleTests(RouterMixin, test.TestCase):
@test.create_stubs({api.neutron: ('router_get', 'router_update',
'network_list', 'port_list',
'network_get')})
'network_get',
'is_extension_supported')})
def test_router_resetrouterrules(self):
pre_router = self.routers_with_rules.first()
post_router = copy.deepcopy(pre_router)
@ -787,6 +793,8 @@ class RouterRuleTests(RouterMixin, test.TestCase):
'action': 'permit', 'nexthops': [], 'id': '2'}]
del post_router['router_rules'][:]
post_router['router_rules'].extend(default_rules)
api.neutron.is_extension_supported(IsA(http.HttpRequest), 'extraroute')\
.AndReturn(False)
api.neutron.router_get(IsA(http.HttpRequest),
pre_router.id).AndReturn(post_router)
params = {}
@ -810,6 +818,96 @@ class RouterRuleTests(RouterMixin, test.TestCase):
self.assertNoFormErrors(res)
class RouterRouteTests(RouterMixin, test.TestCase):
DASHBOARD = 'project'
INDEX_URL = reverse('horizon:%s:routers:index' % DASHBOARD)
DETAIL_PATH = 'horizon:%s:routers:detail' % DASHBOARD
def test_extension_hides_without_routes(self):
router = self.routers_with_routes.first()
res = self._get_detail(router, extraroute=False)
self.assertTemplateUsed(res, '%s/routers/detail.html' % self.DASHBOARD)
self.assertNotIn('extra_routes_table', res.context)
def test_routerroute_detail(self):
router = self.routers_with_routes.first()
res = self._get_detail(router, extraroute=True)
self.assertTemplateUsed(res, '%s/routers/detail.html' % self.DASHBOARD)
routes = res.context['extra_routes_table'].data
routes_dict = [r._apidict for r in routes]
self.assertItemsEqual(routes_dict, router['routes'])
@test.create_stubs({api.neutron: ('router_get', 'router_update')})
def _test_router_addrouterroute(self, raise_error=False):
pre_router = self.routers_with_routes.first()
post_router = copy.deepcopy(pre_router)
route = {'nexthop': '10.0.0.5', 'destination': '40.0.1.0/24'}
post_router['routes'].insert(0, route)
api.neutron.router_get(IsA(http.HttpRequest), pre_router.id)\
.MultipleTimes().AndReturn(pre_router)
params = {}
params['routes'] = post_router['routes']
router_update = api.neutron.router_update(IsA(http.HttpRequest),
pre_router.id, **params)
if raise_error:
router_update.AndRaise(self.exceptions.neutron)
else:
router_update.AndReturn({'router': post_router})
self.mox.ReplayAll()
form_data = copy.deepcopy(route)
form_data['router_id'] = pre_router.id
url = reverse('horizon:%s:routers:addrouterroute' % self.DASHBOARD,
args=[pre_router.id])
res = self.client.post(url, form_data)
self.assertNoFormErrors(res)
detail_url = reverse(self.DETAIL_PATH, args=[pre_router.id])
self.assertRedirectsNoFollow(res, detail_url)
def test_router_addrouterroute(self):
if self.DASHBOARD == 'project':
self._test_router_addrouterroute()
self.assertMessageCount(success=1)
def test_router_addrouterroute_exception(self):
if self.DASHBOARD == 'project':
self._test_router_addrouterroute(raise_error=True)
self.assertMessageCount(error=1)
@test.create_stubs({api.neutron: ('router_get', 'router_update',
'network_get', 'port_list',
'is_extension_supported')})
def test_router_removeroute(self):
if self.DASHBOARD == 'admin':
return
pre_router = self.routers_with_routes.first()
post_router = copy.deepcopy(pre_router)
route = post_router['routes'].pop()
api.neutron.is_extension_supported(IsA(http.HttpRequest), 'extraroute')\
.MultipleTimes().AndReturn(True)
api.neutron.router_get(IsA(http.HttpRequest),
pre_router.id).AndReturn(pre_router)
params = {}
params['routes'] = post_router['routes']
api.neutron.router_get(IsA(http.HttpRequest),
pre_router.id).AndReturn(pre_router)
router_update = api.neutron.router_update(IsA(http.HttpRequest),
pre_router.id, **params)
router_update.AndReturn({'router': post_router})
api.neutron.port_list(IsA(http.HttpRequest),
device_id=pre_router.id)\
.AndReturn([self.ports.first()])
self._mock_external_network_get(pre_router)
self.mox.ReplayAll()
form_route_id = route['nexthop'] + ":" + route['destination']
form_data = {'action': 'extra_routes__delete__%s' % form_route_id}
url = reverse(self.DETAIL_PATH, args=[pre_router.id])
res = self.client.post(url, form_data)
self.assertNoFormErrors(res)
class RouterViewTests(RouterMixin, test.TestCase):
DASHBOARD = 'project'
INDEX_URL = reverse('horizon:%s:routers:index' % DASHBOARD)

View File

@ -15,6 +15,8 @@
from django.conf.urls import patterns
from django.conf.urls import url
from openstack_dashboard.dashboards.project.routers.extensions.extraroutes\
import views as er_views
from openstack_dashboard.dashboards.project.routers.extensions.routerrules\
import views as rr_views
from openstack_dashboard.dashboards.project.routers.ports \
@ -41,6 +43,9 @@ urlpatterns = patterns(
url(ROUTER_URL % 'addrouterrule',
rr_views.AddRouterRuleView.as_view(),
name='addrouterrule'),
url(ROUTER_URL % 'addrouterroute',
er_views.AddRouterRouteView.as_view(),
name='addrouterroute'),
url(ROUTER_URL % 'setgateway',
port_views.SetGatewayView.as_view(),
name='setgateway'),

View File

@ -11,6 +11,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
import uuid
@ -289,6 +290,50 @@ class NeutronApiTests(test.APITestCase):
self.assertFalse(
api.neutron.is_extension_supported(self.request, 'doesntexist'))
def test_router_static_route_list(self):
router = {'router': self.api_routers_with_routes.first()}
router_id = self.api_routers_with_routes.first()['id']
neutronclient = self.stub_neutronclient()
neutronclient.show_router(router_id).AndReturn(router)
self.mox.ReplayAll()
ret_val = api.neutron.router_static_route_list(self.request, router_id)
self.assertIsInstance(ret_val[0], api.neutron.RouterStaticRoute)
def test_router_static_route_remove(self):
router = {'router': self.api_routers_with_routes.first()}
router_id = self.api_routers_with_routes.first()['id']
post_router = copy.deepcopy(router)
route = api.neutron.RouterStaticRoute(post_router['router']
['routes'].pop())
neutronclient = self.stub_neutronclient()
neutronclient.show_router(router_id).AndReturn(router)
body = {'router': {'routes': post_router['router']['routes']}}
neutronclient.update_router(router_id, body=body)\
.AndReturn(post_router)
self.mox.ReplayAll()
api.neutron.router_static_route_remove(self.request,
router_id, route.id)
def test_router_static_route_add(self):
router = {'router': self.api_routers_with_routes.first()}
router_id = self.api_routers_with_routes.first()['id']
post_router = copy.deepcopy(router)
route = {'nexthop': '10.0.0.5', 'destination': '40.0.1.0/24'}
post_router['router']['routes'].insert(0, route)
body = {'router': {'routes': post_router['router']['routes']}}
neutronclient = self.stub_neutronclient()
neutronclient.show_router(router_id).AndReturn(router)
neutronclient.update_router(router_id, body=body)\
.AndReturn(post_router)
self.mox.ReplayAll()
api.neutron.router_static_route_add(self.request, router_id, route)
# NOTE(amotoki): "dvr" permission tests check most of
# get_feature_permission features.
# These tests are not specific to "dvr" extension.

View File

@ -31,6 +31,7 @@ def data(TEST):
TEST.ports = utils.TestDataContainer()
TEST.routers = utils.TestDataContainer()
TEST.routers_with_rules = utils.TestDataContainer()
TEST.routers_with_routes = utils.TestDataContainer()
TEST.q_floating_ips = utils.TestDataContainer()
TEST.q_secgroups = utils.TestDataContainer()
TEST.q_secgroup_rules = utils.TestDataContainer()
@ -58,6 +59,7 @@ def data(TEST):
TEST.api_subnets = utils.TestDataContainer()
TEST.api_ports = utils.TestDataContainer()
TEST.api_routers = utils.TestDataContainer()
TEST.api_routers_with_routes = utils.TestDataContainer()
TEST.api_q_floating_ips = utils.TestDataContainer()
TEST.api_q_secgroups = utils.TestDataContainer()
TEST.api_q_secgroup_rules = utils.TestDataContainer()
@ -404,6 +406,20 @@ def data(TEST):
'nexthops': ['1.0.0.2', '1.0.0.1']}]}
TEST.api_routers.add(router_dict)
TEST.routers_with_rules.add(neutron.Router(router_dict))
router_dict_with_route = {'id': '725c24c9-061b-416b-b9d4-012392b32fd9',
'name': 'routerouter',
'status': 'ACTIVE',
'admin_state_up': True,
'distributed': False,
'external_gateway_info':
{'network_id': ext_net['id']},
'tenant_id': '1',
'routes': [{'nexthop': '10.0.0.1',
'destination': '172.0.0.0/24'},
{'nexthop': '10.0.0.2',
'destination': '172.1.0.0/24'}]}
TEST.api_routers_with_routes.add(router_dict_with_route)
TEST.routers_with_routes.add(neutron.Router(router_dict_with_route))
# Floating IP.
# Unassociated.