Add HA mode support for Neutron router

HA (high availability) mode support is one of the important topics in
Neutron Juno, and this patch adds HA router mode support to Horizon.

This commit also changes the default value of enable_distributed_router
in the example local_settings.py to False. In Juno release of Neutron,
the distributed router and L3 HA mode cannot be enabled at the same
time and only L3-agent deployment with L3 Router service plugin
support both features. Thus I believe it is reasonable to make both
options default to False to avoid unnecessary confusions to operators.

Closes-Bug: #1370110
Change-Id: I77b0292b761f08b4580846f6d58443f7df9a1f6b
This commit is contained in:
Akihiro Motoki 2014-09-04 15:39:30 +09:00
parent 43bdf4647d
commit f06e401adf
12 changed files with 269 additions and 68 deletions

View File

@ -474,6 +474,7 @@ Default::
{
'enable_router': True,
'enable_distributed_router': False,
'enable_ha_router': False,
'enable_lb': True,
'enable_quotas': False,
'enable_firewall': True,
@ -512,6 +513,19 @@ when your Neutron plugin (like ML2 plugin) supports DVR feature, DVR
feature depends on l3-agent configuration, so deployers should set this
option appropriately depending on your deployment.
``enable_ha_router``:
.. versionadded:: 2014.2(Juno)
Default: ``False``
Enable or disable HA (High Availability) mode in Neutron virtual router
in the Router panel. For the HA router mode to be enabled, this option needs
to be set to True and your Neutron deployment must support HA router mode.
Even when your Neutron plugin (like ML2 plugin) supports HA router mode,
the feature depends on l3-agent configuration, so deployers should set this
option appropriately depending on your deployment.
``enable_lb``:
.. versionadded:: 2013.1(Grizzly)

View File

@ -943,31 +943,90 @@ def is_port_profiles_supported():
return True
def get_dvr_permission(request, operation):
"""Check if "distributed" field can be displayed.
# FEATURE_MAP is used to define:
# - related neutron extension name (key: "extension")
# - corresponding dashboard config (key: "config")
# - RBAC policies (key: "poclies")
# If a key is not contained, the corresponding permission check is skipped.
FEATURE_MAP = {
'dvr': {
'extension': 'dvr',
'config': {
'name': 'enable_distributed_router',
'default': False,
},
'policies': {
'get': 'get_router:distributed',
'create': 'create_router:distributed',
'update': 'update_router:distributed',
}
},
'l3-ha': {
'extension': 'l3-ha',
'config': {'name': 'enable_ha_router',
'default': False},
'policies': {
'get': 'get_router:ha',
'create': 'create_router:ha',
'update': 'update_router:ha',
}
},
}
def get_feature_permission(request, feature, operation=None):
"""Check if a feature-specific field can be displayed.
This method check a permission for a feature-specific field.
Such field is usually provided through Neutron extension.
:param request: Request Object
:param operation: Operation type. The valid value is "get" or "create"
:param feature: feature name defined in FEATURE_MAP
:param operation (optional): Operation type. The valid value should be
defined in FEATURE_MAP[feature]['policies']
It must be specified if FEATURE_MAP[feature] has 'policies'.
"""
network_config = getattr(settings, 'OPENSTACK_NEUTRON_NETWORK', {})
if not network_config.get('enable_distributed_router', False):
return False
feature_info = FEATURE_MAP.get(feature)
if not feature_info:
# Translators: Only used inside Horizon code and invisible to users
raise ValueError(_("The requested feature '%(feature)s' is unknown. "
"Please make sure to specify a feature defined "
"in FEATURE_MAP."))
# Check dashboard settings
feature_config = feature_info.get('config')
if feature_config:
if not network_config.get(feature_config['name'],
feature_config['default']):
return False
# Check policy
feature_policies = feature_info.get('policies')
policy_check = getattr(settings, "POLICY_CHECK_FUNCTION", None)
allowed_operations = ("get", "create", "update")
if operation not in allowed_operations:
raise ValueError(_("The 'operation' parameter for get_dvr_permission "
"is invalid. It should be one of %s")
% ' '.join(allowed_operations))
role = (("network", "%s_router:distributed" % operation),)
if policy_check:
has_permission = policy.check(role, request)
else:
has_permission = True
if not has_permission:
return False
try:
return is_extension_supported(request, 'dvr')
except Exception:
msg = _('Failed to check Neutron "dvr" extension is not supported')
LOG.info(msg)
return False
if feature_policies and policy_check:
policy_name = feature_policies.get(operation)
if not policy_name:
# Translators: Only used inside Horizon code and invisible to users
raise ValueError(_("The 'operation' parameter for "
"get_feature_permission '%(feature)s' "
"is invalid. It should be one of %(allowed)s")
% {'feature': feature,
'allowed': ' '.join(feature_policies.keys())})
role = (('network', policy_name),)
if not policy.check(role, request):
return False
# Check if a required extension is enabled
feature_extension = feature_info.get('extension')
if feature_extension:
try:
return is_extension_supported(request, feature_extension)
except Exception:
msg = (_("Failed to check Neutron '%s' extension is not supported")
% feature_extension)
LOG.info(msg)
return False
# If all checks are passed, now a given feature is allowed.
return True

View File

@ -148,6 +148,9 @@
"get_router:distributed": "rule:admin_only",
"create_router:distributed": "rule:admin_only",
"update_router:distributed": "rule:admin_only",
"get_router:ha": "rule:admin_only",
"create_router:ha": "rule:admin_only",
"update_router:ha": "rule:admin_only",
"create_floatingip": "rule:regular_user",
"update_floatingip": "rule:admin_or_owner",

View File

@ -18,6 +18,10 @@
<dt>{% trans "Distributed" %}</dt>
<dd>{{ router.distributed|yesno|capfirst }}</dd>
{% endif %}
{% if ha_supported %}
<dt>{% trans "High Availability Mode" %}</dt>
<dd>{{ router.ha|yesno|capfirst }}</dd>
{% endif %}
{% if router.external_gateway_info %}
<dt>{% trans "External Gateway Information" %}</dt>
<dd>{% trans "Connected External Network" %}:

View File

@ -34,26 +34,38 @@ LOG = logging.getLogger(__name__)
class CreateForm(forms.SelfHandlingForm):
name = forms.CharField(max_length=255, label=_("Router Name"))
mode = forms.ChoiceField(label=_("Router Type"))
ha = forms.ChoiceField(label=_("High Availability Mode"))
failure_url = 'horizon:project:routers:index'
def __init__(self, request, *args, **kwargs):
super(CreateForm, self).__init__(request, *args, **kwargs)
self.dvr_enabled = api.neutron.get_dvr_permission(self.request,
"create")
if self.dvr_enabled:
self.dvr_allowed = api.neutron.get_feature_permission(self.request,
"dvr", "create")
if self.dvr_allowed:
mode_choices = [('server_default', _('Use Server Default')),
('centralized', _('Centralized')),
('distributed', _('Distributed'))]
self.fields['mode'].choices = mode_choices
else:
self.fields['mode'].widget = forms.HiddenInput()
self.fields['mode'].required = False
del self.fields['mode']
self.ha_allowed = api.neutron.get_feature_permission(self.request,
"l3-ha", "create")
if self.ha_allowed:
ha_choices = [('server_default', _('Use Server Default')),
('enabled', _('Enable HA mode')),
('disabled', _('Disable HA mode'))]
self.fields['ha'].choices = ha_choices
else:
del self.fields['ha']
def handle(self, request, data):
try:
params = {'name': data['name']}
if (self.dvr_enabled and data['mode'] != 'server_default'):
if (self.dvr_allowed and data['mode'] != 'server_default'):
params['distributed'] = (data['mode'] == 'distributed')
if (self.ha_allowed and data['ha'] != 'server_default'):
params['ha'] = (data['ha'] == 'enabled')
router = api.neutron.router_create(request, **params)
message = _('Router %s was successfully created.') % data['name']
messages.success(request, message)
@ -77,13 +89,14 @@ class UpdateForm(forms.SelfHandlingForm):
router_id = forms.CharField(label=_("ID"),
widget=forms.HiddenInput())
mode = forms.ChoiceField(label=_("Router Type"))
ha = forms.BooleanField(label=_("High Availability Mode"), required=False)
redirect_url = reverse_lazy('horizon:project:routers:index')
def __init__(self, request, *args, **kwargs):
super(UpdateForm, self).__init__(request, *args, **kwargs)
self.dvr_allowed = api.neutron.get_dvr_permission(self.request,
"update")
self.dvr_allowed = api.neutron.get_feature_permission(self.request,
"dvr", "update")
if not self.dvr_allowed:
del self.fields['mode']
elif kwargs.get('initial', {}).get('mode') == 'distributed':
@ -98,12 +111,19 @@ class UpdateForm(forms.SelfHandlingForm):
('distributed', _('Distributed'))]
self.fields['mode'].choices = mode_choices
self.ha_allowed = api.neutron.get_feature_permission(self.request,
"l3-ha", "update")
if not self.ha_allowed:
del self.fields['ha']
def handle(self, request, data):
try:
params = {'admin_state_up': (data['admin_state'] == 'True'),
'name': data['name']}
if self.dvr_allowed:
params['distributed'] = (data['mode'] == 'distributed')
if self.ha_allowed:
params['ha'] = data['ha']
router = api.neutron.router_update(request, data['router_id'],
**params)
msg = _('Router %s was successfully updated.') % data['name']

View File

@ -159,6 +159,10 @@ class RoutersTable(tables.DataTable):
distributed = tables.Column("distributed",
filters=(filters.yesno, filters.capfirst),
verbose_name=_("Distributed"))
ha = tables.Column("ha",
filters=(filters.yesno, filters.capfirst),
# Translators: High Availability mode of Neutron router
verbose_name=_("HA mode"))
ext_net = tables.Column(get_external_network,
verbose_name=_("External Network"))
@ -168,8 +172,10 @@ class RoutersTable(tables.DataTable):
data=data,
needs_form_wrapper=needs_form_wrapper,
**kwargs)
if not api.neutron.get_dvr_permission(request, "get"):
if not api.neutron.get_feature_permission(request, "dvr", "get"):
del self.columns["distributed"]
if not api.neutron.get_feature_permission(request, "l3-ha", "get"):
del self.columns["ha"]
def get_object_display(self, obj):
return obj.name

View File

@ -16,6 +16,10 @@
<dt>{% trans "Distributed" %}</dt>
<dd>{{ router.distributed|yesno|capfirst }}</dd>
{% endif %}
{% if ha_supported %}
<dt>{% trans "High Availability Mode" %}</dt>
<dd>{{ router.ha|yesno|capfirst }}</dd>
{% endif %}
{% if router.external_gateway_info %}
<dt>{% trans "External Gateway Information" %}</dt>
<dd>{% trans "Connected External Network" %}:

View File

@ -133,10 +133,14 @@ class RouterActionTests(test.TestCase):
DETAIL_PATH = 'horizon:%s:routers:detail' % DASHBOARD
@test.create_stubs({api.neutron: ('router_create',
'get_dvr_permission',)})
'get_feature_permission',)})
def test_router_create_post(self):
router = self.routers.first()
api.neutron.get_dvr_permission(IsA(http.HttpRequest), "create")\
api.neutron.get_feature_permission(IsA(http.HttpRequest),
"dvr", "create")\
.AndReturn(False)
api.neutron.get_feature_permission(IsA(http.HttpRequest),
"l3-ha", "create")\
.AndReturn(False)
api.neutron.router_create(IsA(http.HttpRequest), name=router.name)\
.AndReturn(router)
@ -150,17 +154,22 @@ class RouterActionTests(test.TestCase):
self.assertRedirectsNoFollow(res, self.INDEX_URL)
@test.create_stubs({api.neutron: ('router_create',
'get_dvr_permission',)})
'get_feature_permission',)})
def test_router_create_post_mode_server_default(self):
router = self.routers.first()
api.neutron.get_dvr_permission(IsA(http.HttpRequest), "create")\
api.neutron.get_feature_permission(IsA(http.HttpRequest),
"dvr", "create")\
.AndReturn(True)
api.neutron.get_feature_permission(IsA(http.HttpRequest),
"l3-ha", "create")\
.AndReturn(True)
api.neutron.router_create(IsA(http.HttpRequest), name=router.name)\
.AndReturn(router)
self.mox.ReplayAll()
form_data = {'name': router.name,
'mode': 'server_default'}
'mode': 'server_default',
'ha': 'server_default'}
url = reverse('horizon:%s:routers:create' % self.DASHBOARD)
res = self.client.post(url, form_data)
@ -168,19 +177,25 @@ class RouterActionTests(test.TestCase):
self.assertRedirectsNoFollow(res, self.INDEX_URL)
@test.create_stubs({api.neutron: ('router_create',
'get_dvr_permission',)})
def test_dvr_router_create_post(self):
'get_feature_permission',)})
def test_dvr_ha_router_create_post(self):
router = self.routers.first()
api.neutron.get_dvr_permission(IsA(http.HttpRequest), "create")\
api.neutron.get_feature_permission(IsA(http.HttpRequest),
"dvr", "create")\
.MultipleTimes().AndReturn(True)
api.neutron.get_feature_permission(IsA(http.HttpRequest),
"l3-ha", "create")\
.MultipleTimes().AndReturn(True)
param = {'name': router.name,
'distributed': True}
'distributed': True,
'ha': True}
api.neutron.router_create(IsA(http.HttpRequest), **param)\
.AndReturn(router)
self.mox.ReplayAll()
form_data = {'name': router.name,
'mode': 'distributed'}
'mode': 'distributed',
'ha': 'enabled'}
url = reverse('horizon:%s:routers:create' % self.DASHBOARD)
res = self.client.post(url, form_data)
@ -188,11 +203,15 @@ class RouterActionTests(test.TestCase):
self.assertRedirectsNoFollow(res, self.INDEX_URL)
@test.create_stubs({api.neutron: ('router_create',
'get_dvr_permission',)})
'get_feature_permission',)})
def test_router_create_post_exception_error_case_409(self):
router = self.routers.first()
api.neutron.get_dvr_permission(IsA(http.HttpRequest), "create")\
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)
self.exceptions.neutron.status_code = 409
api.neutron.router_create(IsA(http.HttpRequest), name=router.name)\
.AndRaise(self.exceptions.neutron)
@ -206,10 +225,14 @@ class RouterActionTests(test.TestCase):
self.assertRedirectsNoFollow(res, self.INDEX_URL)
@test.create_stubs({api.neutron: ('router_create',
'get_dvr_permission',)})
'get_feature_permission',)})
def test_router_create_post_exception_error_case_non_409(self):
router = self.routers.first()
api.neutron.get_dvr_permission(IsA(http.HttpRequest), "create")\
api.neutron.get_feature_permission(IsA(http.HttpRequest),
"dvr", "create")\
.MultipleTimes().AndReturn(False)
api.neutron.get_feature_permission(IsA(http.HttpRequest),
"l3-ha", "create")\
.MultipleTimes().AndReturn(False)
self.exceptions.neutron.status_code = 999
api.neutron.router_create(IsA(http.HttpRequest), name=router.name)\
@ -224,15 +247,20 @@ class RouterActionTests(test.TestCase):
self.assertRedirectsNoFollow(res, self.INDEX_URL)
@test.create_stubs({api.neutron: ('router_get',
'get_dvr_permission')})
'get_feature_permission')})
def _test_router_update_get(self, dvr_enabled=False,
current_dvr=False):
current_dvr=False,
ha_enabled=False):
router = [r for r in self.routers.list()
if r.distributed == current_dvr][0]
api.neutron.router_get(IsA(http.HttpRequest), router.id)\
.AndReturn(router)
api.neutron.get_dvr_permission(IsA(http.HttpRequest), "update")\
api.neutron.get_feature_permission(IsA(http.HttpRequest),
"dvr", "update")\
.AndReturn(dvr_enabled)
api.neutron.get_feature_permission(IsA(http.HttpRequest),
"l3-ha", "update")\
.AndReturn(ha_enabled)
self.mox.ReplayAll()
url = reverse('horizon:%s:routers:update' % self.DASHBOARD,
@ -276,10 +304,14 @@ class RouterActionTests(test.TestCase):
@test.create_stubs({api.neutron: ('router_get',
'router_update',
'get_dvr_permission')})
def test_router_update_post_dvr_disabled(self):
'get_feature_permission')})
def test_router_update_post_dvr_ha_disabled(self):
router = self.routers.first()
api.neutron.get_dvr_permission(IsA(http.HttpRequest), "update")\
api.neutron.get_feature_permission(IsA(http.HttpRequest),
"dvr", "update")\
.AndReturn(False)
api.neutron.get_feature_permission(IsA(http.HttpRequest),
"l3-ha", "update")\
.AndReturn(False)
api.neutron.router_update(IsA(http.HttpRequest), router.id,
name=router.name,
@ -300,15 +332,20 @@ class RouterActionTests(test.TestCase):
@test.create_stubs({api.neutron: ('router_get',
'router_update',
'get_dvr_permission')})
def test_router_update_post_dvr_enabled(self):
'get_feature_permission')})
def test_router_update_post_dvr_ha_enabled(self):
router = self.routers.first()
api.neutron.get_dvr_permission(IsA(http.HttpRequest), "update")\
api.neutron.get_feature_permission(IsA(http.HttpRequest),
"dvr", "update")\
.AndReturn(True)
api.neutron.get_feature_permission(IsA(http.HttpRequest),
"l3-ha", "update")\
.AndReturn(True)
api.neutron.router_update(IsA(http.HttpRequest), router.id,
name=router.name,
admin_state_up=router.admin_state_up,
distributed=True)\
distributed=True,
ha=True)\
.AndReturn(router)
api.neutron.router_get(IsA(http.HttpRequest), router.id)\
.AndReturn(router)
@ -317,7 +354,8 @@ class RouterActionTests(test.TestCase):
form_data = {'router_id': router.id,
'name': router.name,
'admin_state': router.admin_state_up,
'mode': 'distributed'}
'mode': 'distributed',
'ha': True}
url = reverse('horizon:%s:routers:update' % self.DASHBOARD,
args=[router.id])
res = self.client.post(url, form_data)

View File

@ -126,8 +126,10 @@ class DetailView(tabs.TabbedTableView):
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
context["router"] = self._get_data()
context['dvr_supported'] = api.neutron.get_dvr_permission(self.request,
"get")
context['dvr_supported'] = api.neutron.get_feature_permission(
self.request, "dvr", "get")
context['ha_supported'] = api.neutron.get_feature_permission(
self.request, "l3-ha", "get")
return context
def get(self, request, *args, **kwargs):
@ -170,4 +172,6 @@ class UpdateView(forms.ModalFormView):
if hasattr(router, 'distributed'):
initial['mode'] = ('distributed' if router.distributed
else 'centralized')
if hasattr(router, 'ha'):
initial['ha'] = router.ha
return initial

View File

@ -182,7 +182,8 @@ OPENSTACK_NEUTRON_NETWORK = {
'enable_router': True,
'enable_quotas': True,
'enable_ipv6': True,
'enable_distributed_router': True,
'enable_distributed_router': False,
'enable_ha_router': False,
'enable_lb': True,
'enable_firewall': True,
'enable_vpn': True,

View File

@ -283,6 +283,11 @@ class NeutronApiTests(test.APITestCase):
self.assertFalse(
api.neutron.is_extension_supported(self.request, 'doesntexist'))
# NOTE(amotoki): "dvr" permission tests check most of
# get_feature_permission features.
# These tests are not specific to "dvr" extension.
# Please be careful if you drop "dvr" extension in future.
@override_settings(OPENSTACK_NEUTRON_NETWORK={'enable_distributed_router':
True},
POLICY_CHECK_FUNCTION=None)
@ -295,10 +300,11 @@ class NeutronApiTests(test.APITestCase):
.AndReturn({'extensions': extensions})
self.mox.ReplayAll()
self.assertEqual(dvr_enabled,
api.neutron.get_dvr_permission(self.request, 'get'))
api.neutron.get_feature_permission(self.request,
'dvr', 'get'))
def test_get_dvr_permission_dvr_supported(self):
self._test_get_dvr_permission_dvr_supported(dvr_enabled=True, )
self._test_get_dvr_permission_dvr_supported(dvr_enabled=True)
def test_get_dvr_permission_dvr_not_supported(self):
self._test_get_dvr_permission_dvr_supported(dvr_enabled=False)
@ -320,8 +326,8 @@ class NeutronApiTests(test.APITestCase):
.AndReturn({'extensions': self.api_extensions.list()})
self.mox.ReplayAll()
self.assertEqual(policy_check_allowed,
api.neutron.get_dvr_permission(self.request,
operation))
api.neutron.get_feature_permission(self.request,
'dvr', operation))
def test_get_dvr_permission_with_policy_check_allowed(self):
self._test_get_dvr_permission_with_policy_check(True, "get")
@ -338,11 +344,49 @@ class NeutronApiTests(test.APITestCase):
@override_settings(OPENSTACK_NEUTRON_NETWORK={'enable_distributed_router':
False})
def test_get_dvr_permission_dvr_disabled_by_config(self):
self.assertFalse(api.neutron.get_dvr_permission(self.request, 'get'))
self.assertFalse(api.neutron.get_feature_permission(self.request,
'dvr', 'get'))
@override_settings(OPENSTACK_NEUTRON_NETWORK={'enable_distributed_router':
True})
True},
POLICY_CHECK_FUNCTION=policy.check)
def test_get_dvr_permission_dvr_unsupported_operation(self):
self.assertRaises(ValueError,
api.neutron.get_dvr_permission,
self.request, 'unSupported')
api.neutron.get_feature_permission,
self.request, 'dvr', 'unSupported')
@override_settings(OPENSTACK_NEUTRON_NETWORK={})
def test_get_dvr_permission_dvr_default_config(self):
self.assertFalse(api.neutron.get_feature_permission(self.request,
'dvr', 'get'))
@override_settings(OPENSTACK_NEUTRON_NETWORK={})
def test_get_dvr_permission_router_ha_default_config(self):
self.assertFalse(api.neutron.get_feature_permission(self.request,
'l3-ha', 'get'))
# NOTE(amotoki): Most of get_feature_permission are checked by "dvr" check
# above. l3-ha check only checks l3-ha specific code.
@override_settings(OPENSTACK_NEUTRON_NETWORK={'enable_ha_router': True},
POLICY_CHECK_FUNCTION=policy.check)
def _test_get_router_ha_permission_with_policy_check(self, ha_enabled):
self.mox.StubOutWithMock(policy, 'check')
role = (("network", "create_router:ha"),)
policy.check(role, self.request).AndReturn(True)
neutronclient = self.stub_neutronclient()
if ha_enabled:
extensions = self.api_extensions.list()
else:
extensions = {}
neutronclient.list_extensions().AndReturn({'extensions': extensions})
self.mox.ReplayAll()
self.assertEqual(ha_enabled,
api.neutron.get_feature_permission(self.request,
'l3-ha', 'create'))
def test_get_router_ha_permission_with_l3_ha_extension(self):
self._test_get_router_ha_permission_with_policy_check(True)
def test_get_router_ha_permission_without_l3_ha_extension(self):
self._test_get_router_ha_permission_with_policy_check(False)

View File

@ -642,10 +642,14 @@ def data(TEST):
"alias": "dvr",
"description":
"Enables configuration of Distributed Virtual Routers."}
extension_5 = {"name": "HA Router extension",
"alias": "l3-ha",
"description": "Add HA capability to routers."}
TEST.api_extensions.add(extension_1)
TEST.api_extensions.add(extension_2)
TEST.api_extensions.add(extension_3)
TEST.api_extensions.add(extension_4)
TEST.api_extensions.add(extension_5)
# 1st agent.
agent_dict = {"binary": "neutron-openvswitch-agent",