diff --git a/neutron_vpnaas_dashboard/api/vpn.py b/neutron_vpnaas_dashboard/api/vpn.py
index 90b8a09..eb34a0d 100644
--- a/neutron_vpnaas_dashboard/api/vpn.py
+++ b/neutron_vpnaas_dashboard/api/vpn.py
@@ -40,6 +40,10 @@ class VPNService(neutron.NeutronAPIDictWrapper):
"""Wrapper for neutron VPNService."""
+class EndpointGroup(neutron.NeutronAPIDictWrapper):
+ """Wrapper for neutron Endpoint Group."""
+
+
@profiler.trace
def vpnservice_create(request, **kwargs):
"""Create VPNService
@@ -55,9 +59,11 @@ def vpnservice_create(request, **kwargs):
{'admin_state_up': kwargs['admin_state_up'],
'name': kwargs['name'],
'description': kwargs['description'],
- 'router_id': kwargs['router_id'],
- 'subnet_id': kwargs['subnet_id']}
+ 'router_id': kwargs['router_id']
+ }
}
+ if kwargs.get('subnet_id'):
+ body['vpnservice']['subnet_id'] = kwargs['subnet_id']
vpnservice = neutronclient(request).create_vpnservice(body).get(
'vpnservice')
return VPNService(vpnservice)
@@ -65,7 +71,8 @@ def vpnservice_create(request, **kwargs):
@profiler.trace
def vpnservice_list(request, **kwargs):
- return _vpnservice_list(request, expand_subnet=True, expand_router=True,
+ return _vpnservice_list(request, expand_subnet=True,
+ expand_router=True,
expand_conns=True, **kwargs)
@@ -77,7 +84,8 @@ def _vpnservice_list(request, expand_subnet=False, expand_router=False,
subnets = neutron.subnet_list(request)
subnet_dict = OrderedDict((s.id, s) for s in subnets)
for s in vpnservices:
- s['subnet_name'] = subnet_dict.get(s['subnet_id']).cidr
+ if s.get('subnet_id'):
+ s['subnet_name'] = subnet_dict.get(s['subnet_id']).cidr
if expand_router:
routers = neutron.router_list(request)
router_dict = OrderedDict((r.id, r) for r in routers)
@@ -101,9 +109,10 @@ def _vpnservice_get(request, vpnservice_id, expand_subnet=False,
expand_router=False, expand_conns=False):
vpnservice = neutronclient(request).show_vpnservice(vpnservice_id).get(
'vpnservice')
- if expand_subnet:
- vpnservice['subnet'] = neutron.subnet_get(
- request, vpnservice['subnet_id'])
+ if expand_subnet and ('subnet_id' in vpnservice):
+ if vpnservice['subnet_id'] is not None:
+ vpnservice['subnet'] = neutron.subnet_get(
+ request, vpnservice['subnet_id'])
if expand_router:
vpnservice['router'] = neutron.router_get(
request, vpnservice['router_id'])
@@ -126,6 +135,74 @@ def vpnservice_delete(request, vpnservice_id):
neutronclient(request).delete_vpnservice(vpnservice_id)
+@profiler.trace
+def endpointgroup_create(request, **kwargs):
+ """Create Endpoint Group
+
+ :param request: request context
+ :param name: name for Endpoint Group
+ :param description: description for Endpoint Group
+ :param type: type of Endpoint Group
+ :param endpoints: endpoint(s) of Endpoint Group
+ """
+ body = {'endpoint_group':
+ {'name': kwargs['name'],
+ 'description': kwargs['description'],
+ 'type': kwargs['type'],
+ 'endpoints': kwargs['endpoints']}
+ }
+ endpointgroup = neutronclient(request).create_endpoint_group(body).get(
+ 'endpoint_group')
+ return EndpointGroup(endpointgroup)
+
+
+@profiler.trace
+def endpointgroup_list(request, **kwargs):
+ return _endpointgroup_list(request, expand_conns=True, **kwargs)
+
+
+def _endpointgroup_list(request, expand_conns=False, **kwargs):
+ endpointgroups = neutronclient(request).list_endpoint_groups(
+ **kwargs).get('endpoint_groups')
+ if expand_conns:
+ ipsecsiteconns = _ipsecsiteconnection_list(request)
+ for g in endpointgroups:
+ g['ipsecsiteconns'] = [
+ c.id for c in ipsecsiteconns
+ if (c.get('local_ep_group_id') == g['id'] or
+ c.get('peer_ep_group_id') == g['id'])]
+ return [EndpointGroup(v) for v in endpointgroups]
+
+
+@profiler.trace
+def endpointgroup_get(request, endpoint_group_id):
+ return _endpointgroup_get(request, endpoint_group_id, expand_conns=True)
+
+
+def _endpointgroup_get(request, endpoint_group_id, expand_conns=False):
+ endpointgroup = neutronclient(request).show_endpoint_group(
+ endpoint_group_id).get('endpoint_group')
+ if expand_conns:
+ ipsecsiteconns = _ipsecsiteconnection_list(request)
+ endpointgroup['ipsecsiteconns'] = [
+ c for c in ipsecsiteconns
+ if (c.get('local_ep_group_id') == endpointgroup['id'] or
+ c.get('peer_ep_group_id') == endpointgroup['id'])]
+ return EndpointGroup(endpointgroup)
+
+
+@profiler.trace
+def endpointgroup_update(request, endpoint_group_id, **kwargs):
+ endpointgroup = neutronclient(request).update_endpoint_group(
+ endpoint_group_id, kwargs).get('endpoint_group')
+ return EndpointGroup(endpointgroup)
+
+
+@profiler.trace
+def endpointgroup_delete(request, endpoint_group_id):
+ neutronclient(request).delete_endpoint_group(endpoint_group_id)
+
+
@profiler.trace
def ikepolicy_create(request, **kwargs):
"""Create IKEPolicy
@@ -290,23 +367,28 @@ def ipsecsiteconnection_create(request, **kwargs):
:param vpnservice_id: VPNService associated with this connection
:param admin_state_up: admin state (default on)
"""
- body = {'ipsec_site_connection':
- {'name': kwargs['name'],
- 'description': kwargs['description'],
- 'dpd': kwargs['dpd'],
- 'ikepolicy_id': kwargs['ikepolicy_id'],
- 'initiator': kwargs['initiator'],
- 'ipsecpolicy_id': kwargs['ipsecpolicy_id'],
- 'mtu': kwargs['mtu'],
- 'peer_address': kwargs['peer_address'],
- 'peer_cidrs': kwargs['peer_cidrs'],
- 'peer_id': kwargs['peer_id'],
- 'psk': kwargs['psk'],
- 'vpnservice_id': kwargs['vpnservice_id'],
- 'admin_state_up': kwargs['admin_state_up']}
- }
+ body = {
+ 'name': kwargs['name'],
+ 'description': kwargs['description'],
+ 'dpd': kwargs['dpd'],
+ 'ikepolicy_id': kwargs['ikepolicy_id'],
+ 'initiator': kwargs['initiator'],
+ 'ipsecpolicy_id': kwargs['ipsecpolicy_id'],
+ 'mtu': kwargs['mtu'],
+ 'peer_address': kwargs['peer_address'],
+ 'peer_id': kwargs['peer_id'],
+ 'psk': kwargs['psk'],
+ 'vpnservice_id': kwargs['vpnservice_id'],
+ 'admin_state_up': kwargs['admin_state_up']
+ }
+ cidrs = kwargs.get('peer_cidrs', [])
+ if not cidrs:
+ body['local_ep_group_id'] = kwargs['local_ep_group_id']
+ body['peer_ep_group_id'] = kwargs['peer_ep_group_id']
+ else:
+ body['peer_cidrs'] = kwargs['peer_cidrs']
ipsecsiteconnection = neutronclient(request).create_ipsec_site_connection(
- body).get('ipsec_site_connection')
+ {'ipsec_site_connection': body}).get('ipsec_site_connection')
return IPSecSiteConnection(ipsecsiteconnection)
diff --git a/neutron_vpnaas_dashboard/dashboards/project/vpn/forms.py b/neutron_vpnaas_dashboard/dashboards/project/vpn/forms.py
index d1f0e29..c6ffdce 100644
--- a/neutron_vpnaas_dashboard/dashboards/project/vpn/forms.py
+++ b/neutron_vpnaas_dashboard/dashboards/project/vpn/forms.py
@@ -59,6 +59,41 @@ class UpdateVPNService(forms.SelfHandlingForm):
exceptions.handle(request, msg, redirect=redirect)
+class UpdateEndpointGroup(forms.SelfHandlingForm):
+ name = forms.CharField(
+ max_length=80,
+ label=_("Name"),
+ required=False)
+ endpoint_group_id = forms.CharField(
+ label=_("ID"),
+ widget=forms.TextInput(attrs={'readonly': 'readonly'}))
+ description = forms.CharField(
+ required=False,
+ max_length=80,
+ label=_("Description"))
+
+ failure_url = 'horizon:project:vpn:index'
+
+ def handle(self, request, context):
+ try:
+ data = {'endpoint_group':
+ {'name': context['name'],
+ 'description': context['description']}
+ }
+ endpointgroup = api_vpn.endpointgroup_update(
+ request, context['endpoint_group_id'], **data)
+ msg = (_('Endpoint Group %s was successfully updated.')
+ % context['name'])
+ messages.success(request, msg)
+ return endpointgroup
+ except Exception as e:
+ LOG.info('Failed to update Endpint Group %(id)s: %(exc)s',
+ {'id': context['endpoint_group_id'], 'exc': e})
+ msg = _('Failed to update Endpint Group %s') % context['name']
+ redirect = reverse(self.failure_url)
+ exceptions.handle(request, msg, redirect=redirect)
+
+
class UpdateIKEPolicy(forms.SelfHandlingForm):
name = forms.CharField(max_length=80, label=_("Name"), required=False)
ikepolicy_id = forms.CharField(
@@ -236,6 +271,7 @@ class UpdateIPSecSiteConnection(forms.SelfHandlingForm):
version=forms.IPv4 | forms.IPv6,
mask=False)
peer_cidrs = forms.MultiIPField(
+ required=False,
label=_("Remote peer subnet(s)"),
help_text=_("Remote peer subnet(s) address(es) "
"with mask(s) in CIDR format "
@@ -243,6 +279,16 @@ class UpdateIPSecSiteConnection(forms.SelfHandlingForm):
"(e.g. 20.1.0.0/24, 21.1.0.0/24)"),
version=forms.IPv4 | forms.IPv6,
mask=True)
+ local_ep_group_id = forms.CharField(
+ required=False,
+ label=_("Local Endpoint Group(s)"),
+ help_text=_("IPsec connection validation requires "
+ "that local endpoints are subnets"))
+ peer_ep_group_id = forms.CharField(
+ required=False,
+ label=_("Peer Endpoint Group(s)"),
+ help_text=_("IPSec connection validation requires "
+ "that peer endpoints are CIDRs"))
psk = forms.CharField(
widget=forms.PasswordInput(render_value=True),
max_length=80, label=_("Pre-Shared Key (PSK) string"))
@@ -293,23 +339,29 @@ class UpdateIPSecSiteConnection(forms.SelfHandlingForm):
def handle(self, request, context):
try:
- data = {'ipsec_site_connection':
- {'name': context['name'],
- 'description': context['description'],
- 'peer_address': context['peer_address'],
- 'peer_id': context['peer_id'],
- 'peer_cidrs': context[
- 'peer_cidrs'].replace(" ", "").split(","),
- 'psk': context['psk'],
- 'mtu': context['mtu'],
- 'dpd': {'action': context['dpd_action'],
- 'interval': context['dpd_interval'],
- 'timeout': context['dpd_timeout']},
- 'initiator': context['initiator'],
- 'admin_state_up': context['admin_state_up'],
- }}
+ data = {
+ 'name': context['name'],
+ 'description': context['description'],
+ 'peer_address': context['peer_address'],
+ 'peer_id': context['peer_id'],
+ 'psk': context['psk'],
+ 'mtu': context['mtu'],
+ 'dpd': {'action': context['dpd_action'],
+ 'interval': context['dpd_interval'],
+ 'timeout': context['dpd_timeout']},
+ 'initiator': context['initiator'],
+ 'admin_state_up': context['admin_state_up']
+ }
+ if not context['peer_cidrs']:
+ data['local_ep_group_id'] = context['local_ep_group_id']
+ data['peer_ep_group_id'] = context['peer_ep_group_id']
+ else:
+ cidrs = context['peer_cidrs']
+ data['peer_cidrs'] = [cidr.strip() for cidr in cidrs.split(',')
+ if cidr.strip()]
ipsecsiteconnection = api_vpn.ipsecsiteconnection_update(
- request, context['ipsecsiteconnection_id'], **data)
+ request, context['ipsecsiteconnection_id'],
+ ipsec_site_connection=data)
msg = (_('IPSec Site Connection %s was successfully updated.')
% context['name'])
messages.success(request, msg)
diff --git a/neutron_vpnaas_dashboard/dashboards/project/vpn/tables.py b/neutron_vpnaas_dashboard/dashboards/project/vpn/tables.py
index b45fbad..70d141d 100644
--- a/neutron_vpnaas_dashboard/dashboards/project/vpn/tables.py
+++ b/neutron_vpnaas_dashboard/dashboards/project/vpn/tables.py
@@ -57,6 +57,15 @@ class AddVPNServiceLink(tables.LinkAction):
policy_rules = (("network", "create_vpnservice"),)
+class AddEndpointGroupLink(tables.LinkAction):
+ name = "addendpointgroup"
+ verbose_name = _("Add Endpoint Group")
+ url = "horizon:project:vpn:addendpointgroup"
+ classes = ("ajax-modal",)
+ icon = "plus"
+ policy_rules = (("network", "create_endpointgroup"),)
+
+
class AddIPSecSiteConnectionLink(tables.LinkAction):
name = "addipsecsiteconnection"
verbose_name = _("Add IPSec Site Connection")
@@ -99,6 +108,34 @@ class DeleteVPNServiceLink(policy.PolicyTargetMixin, tables.DeleteAction):
request, _('Unable to delete VPN Service. %s') % e)
+class DeleteEndpointGroupLink(policy.PolicyTargetMixin, tables.DeleteAction):
+ name = "deleteendpointgroup"
+ policy_rules = (("network", "delete_endpointgroup"),)
+
+ @staticmethod
+ def action_present(count):
+ return ungettext_lazy(
+ u"Delete Endpoint Group",
+ u"Delete Endpoint Groups",
+ count
+ )
+
+ @staticmethod
+ def action_past(count):
+ return ungettext_lazy(
+ u"Scheduled deletion of Endpoint Group",
+ u"Scheduled deletion of Endpoint Groups",
+ count
+ )
+
+ def delete(self, request, obj_id):
+ try:
+ api_vpn.endpointgroup_delete(request, obj_id)
+ except Exception as e:
+ exceptions.handle(
+ request, _('Unable to delete Endpoint Group. %s') % e)
+
+
class DeleteIKEPolicyLink(policy.PolicyTargetMixin, tables.DeleteAction):
name = "deleteikepolicy"
policy_rules = (("network", "delete_ikepolicy"),)
@@ -210,6 +247,17 @@ class UpdateVPNServiceLink(tables.LinkAction):
return False
+class UpdateEndpointGroupLink(tables.LinkAction):
+ name = "updateendpointgroup"
+ verbose_name = _("Edit Endpoint Group")
+ classes = ("ajax-modal", "btn-update",)
+ policy_rules = (("network", "update_endpointgroup"),)
+
+ def get_link_url(self, endpoint_group):
+ return reverse("horizon:project:vpn:update_endpointgroup",
+ kwargs={'endpoint_group_id': endpoint_group.id})
+
+
class UpdateIKEPolicyLink(tables.LinkAction):
name = "updateikepolicy"
verbose_name = _("Edit IKE Policy")
@@ -355,13 +403,21 @@ def get_local_ips(vpn):
return template.loader.render_to_string(template_name, context)
+def get_subnet_name(vpn):
+ try:
+ return vpn.subnet_name
+ except AttributeError:
+ return _("-")
+
+
class UpdateVPNServiceRow(tables.Row):
ajax = True
def get_data(self, request, vpn_id):
vpn = api_vpn.vpnservice_get(request, vpn_id)
vpn.router_name = vpn['router'].get('name', vpn['router_id'])
- vpn.subnet_name = vpn['subnet'].get('cidr', vpn['subnet_id'])
+ if 'subnet' in vpn:
+ vpn.subnet_name = vpn['subnet'].get('cidr', vpn['subnet_id'])
return vpn
@@ -384,7 +440,7 @@ class VPNServicesTable(tables.DataTable):
description = tables.Column('description', verbose_name=_('Description'))
local_ips = tables.Column(get_local_ips,
verbose_name=_("Local Side Public IPs"))
- subnet_name = tables.Column('subnet_name', verbose_name=_('Subnet'))
+ subnet_name = tables.Column(get_subnet_name, verbose_name=_('Subnet'))
router_name = tables.Column('router_name', verbose_name=_('Router'))
status = tables.Column("status",
verbose_name=_("Status"),
@@ -406,6 +462,40 @@ class VPNServicesTable(tables.DataTable):
row_actions = (UpdateVPNServiceLink, DeleteVPNServiceLink)
+class EndpointGroupFilterAction(tables.FilterAction):
+ name = 'endpointgroups_project'
+ filter_type = 'server'
+ filter_choices = (
+ ('name', _("Name ="), True),
+ ('type', _("Type ="), True),
+ ('endpoints', _("Endpoints ="), True),
+ )
+
+
+def _get_endpoints(epg):
+ return ', '.join(epg.endpoints)
+
+
+class EndpointGroupTable(tables.DataTable):
+ id = tables.Column('id', hidden=True)
+ name = tables.Column("name_or_id", verbose_name=_('Name'),
+ link="horizon:project:vpn:endpointgroupdetails")
+ description = tables.Column('description', verbose_name=_('Description'))
+ type = tables.Column('type', verbose_name=_('Type'))
+ endpoints = tables.Column(_get_endpoints, verbose_name=_('Endpoints'))
+
+ class Meta(object):
+ name = "endpointgroupstable"
+ verbose_name = _("Endpoint Groups")
+ table_actions = (AddEndpointGroupLink,
+ DeleteEndpointGroupLink,
+ EndpointGroupFilterAction)
+ row_actions = (UpdateEndpointGroupLink, DeleteEndpointGroupLink)
+
+ def get_object_display(self, endpoitgroup):
+ return endpoitgroup.name_or_id
+
+
class PoliciesFilterAction(tables.FilterAction):
name = 'filter_project_IKEPolicies'
filter_type = 'server'
diff --git a/neutron_vpnaas_dashboard/dashboards/project/vpn/tabs.py b/neutron_vpnaas_dashboard/dashboards/project/vpn/tabs.py
index 58fd406..f7dd6c1 100644
--- a/neutron_vpnaas_dashboard/dashboards/project/vpn/tabs.py
+++ b/neutron_vpnaas_dashboard/dashboards/project/vpn/tabs.py
@@ -114,6 +114,32 @@ class VPNServicesTab(tabs.TableTab, htables.DataTableView):
return super(VPNServicesTab, self).get_filters()
+class EndpointGroupTab(tabs.TableTab, htables.DataTableView):
+ table_classes = (tables.EndpointGroupTable,)
+ name = _("Endpoint Groups")
+ slug = "endpointgroups"
+ template_name = ("horizon/common/_detail_table.html")
+
+ def get_endpointgroupstable_data(self):
+ try:
+ filters = self.get_filters()
+ tenant_id = self.request.user.tenant_id
+ endpointgroups = api_vpn.endpointgroup_list(
+ self.tab_group.request, tenant_id=tenant_id, **filters)
+ except Exception:
+ endpointgroups = []
+ exceptions.handle(self.tab_group.request,
+ _('Unable to retrieve endpoint group list.'))
+ return endpointgroups
+
+ def get_filters(self):
+ self.table = self._tables['endpointgroupstable']
+ self.handle_server_filter(self.request, table=self.table)
+ self.update_server_filter_action(self.request, table=self.table)
+
+ return super(EndpointGroupTab, self).get_filters()
+
+
class IKEPoliciesTab(tabs.TableTab, htables.DataTableView):
table_classes = (tables.IKEPoliciesTable,)
name = _("IKE Policies")
@@ -169,7 +195,8 @@ class IPSecPoliciesTab(tabs.TableTab, htables.DataTableView):
class VPNTabs(tabs.TabGroup):
slug = "vpntabs"
tabs = (IKEPoliciesTab, IPSecPoliciesTab,
- VPNServicesTab, IPSecSiteConnectionsTab,)
+ VPNServicesTab, EndpointGroupTab,
+ IPSecSiteConnectionsTab,)
sticky = True
@@ -218,6 +245,21 @@ class VPNServiceDetailsTabs(tabs.TabGroup):
tabs = (VPNServiceDetailsTab,)
+class EndpointGroupDetailsTab(tabs.Tab):
+ name = _("Endpoint Groups Details")
+ slug = "endpointgroupdetails"
+ template_name = "project/vpn/_endpointgroup_details.html"
+
+ def get_context_data(self, request):
+ endpointgroup = self.tab_group.kwargs['endpointgroup']
+ return {'endpointgroup': endpointgroup}
+
+
+class EndpointGroupDetailsTabs(tabs.TabGroup):
+ slug = "endpointgrouptabs"
+ tabs = (EndpointGroupDetailsTab,)
+
+
class IPSecSiteConnectionDetailsTab(tabs.Tab):
name = _("IPSec Site Connection Details")
slug = "ipsecsiteconnectiondetails"
diff --git a/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_add_endpoint_group_help.html b/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_add_endpoint_group_help.html
new file mode 100644
index 0000000..308012c
--- /dev/null
+++ b/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_add_endpoint_group_help.html
@@ -0,0 +1,3 @@
+{% load i18n %}
+
+
{% trans "Create endpoint group for current project." %}
diff --git a/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_add_vpn_service_help.html b/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_add_vpn_service_help.html
index 4d682dc..d5823a6 100644
--- a/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_add_vpn_service_help.html
+++ b/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_add_vpn_service_help.html
@@ -1,7 +1,15 @@
{% load i18n %}
{% trans "Create VPN service for current project." %}
-{% trans "The VPN service is attached to a router and references to a single subnet to push to a remote site." %}
-{% trans "Specify a name, description, router, and subnet for the VPN Service." %}
+{% blocktrans trimmed %}
+The VPN service is attached to a router and references to endpoint group
+or a single subnet to push to a remote site.
+{% endblocktrans %}
+{% trans "Specify a name, description, router, and subnet (optional) for the VPN Service." %}
{% trans "Admin State is enabled by default." %}
-{% trans "The router, subnet and admin state fields require to be enabled. All others are optional." %}
+{% trans "The router and admin state fields require to be enabled. All others are optional." %}
+{% blocktrans trimmed %}
+Note: The recommended way to specify local subnets is to use endpoint groups
+in IPsec site connection. It is deprecated to specify subnet in VPN service.
+For a new VPN service or IPsec site connection, using endpoint group is recommended.
+{% endblocktrans %}
diff --git a/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_endpointgroup_details.html b/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_endpointgroup_details.html
new file mode 100644
index 0000000..22ccf66
--- /dev/null
+++ b/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_endpointgroup_details.html
@@ -0,0 +1,32 @@
+{% load i18n sizeformat parse_date %}
+
+
+
+ - {% trans "Name" %}
+ - {{ endpointgroup.name|default:_("None") }}
+
+ - {% trans "Description" %}
+ - {{ endpointgroup.description|default:_("None") }}
+
+ - {% trans "ID" %}
+ - {{ endpointgroup.id }}
+
+ - {% trans "Project ID" %}
+ - {{ endpointgroup.tenant_id }}
+
+ - {% trans "Type" %}
+ - {{ endpointgroup.type }}
+
+ - {% trans "Endpoints" %}
+ {% if endpointgroup.type == 'subnet' %}
+ {% for ep in endpointgroup.endpoints %}
+ {% url 'horizon:project:networks:subnets:detail' ep as subnet_url %}
+ - {{ ep }}
+ {% endfor %}
+ {% else %}
+ {% for cidr in endpointgroup.endpoints %}
+ - {{ cidr }}
+ {% endfor %}
+ {% endif %}
+
+
diff --git a/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_ipsecsiteconnection_details.html b/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_ipsecsiteconnection_details.html
index 307a30b..042c58f 100644
--- a/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_ipsecsiteconnection_details.html
+++ b/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_ipsecsiteconnection_details.html
@@ -18,6 +18,10 @@
{% url 'horizon:project:vpn:vpnservicedetails' ipsecsiteconnection.vpnservice_id as vpnservice_url %}
{{ ipsecsiteconnection.vpnservice.name_or_id }}
+ {% trans "Local Endpoint Group" %}
+ {% url 'horizon:project:vpn:endpointgroupdetails' ipsecsiteconnection.local_ep_group_id as local_epg_url %}
+ {{ ipsecsiteconnection.local_ep_group_id }}
+
{% trans "IKE Policy" %}
{% url 'horizon:project:vpn:ikepolicydetails' ipsecsiteconnection.ikepolicy_id as ikepolicy_url %}
{{ ipsecsiteconnection.ikepolicy.name_or_id }}
@@ -39,6 +43,10 @@
{% endfor %}
+ {% trans "Peer Endpoint Group" %}
+ {% url 'horizon:project:vpn:endpointgroupdetails' ipsecsiteconnection.peer_ep_group_id as peer_epg_url %}
+ {{ ipsecsiteconnection.peer_ep_group_id }}
+
{% trans "MTU" %}
{{ ipsecsiteconnection.mtu }}
diff --git a/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_update_endpointgroup.html b/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_update_endpointgroup.html
new file mode 100644
index 0000000..64afd89
--- /dev/null
+++ b/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_update_endpointgroup.html
@@ -0,0 +1,7 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+
+{% block modal-body-right %}
+ {% trans "Description:" %}
+ {% trans "You may update endpoint group details here." %}
+{% endblock %}
diff --git a/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_vpnservice_details.html b/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_vpnservice_details.html
index 4db1bb6..076bcb7 100644
--- a/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_vpnservice_details.html
+++ b/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_vpnservice_details.html
@@ -19,8 +19,12 @@
{{ vpnservice.router.name_or_id }}
{% trans "Subnet ID" %}
- {% url 'horizon:project:networks:subnets:detail' vpnservice.subnet_id as subnet_url %}
- {{ vpnservice.subnet.name_or_id }} {{ vpnservice.subnet.cidr }}
+ {% if vpnservice.subnet_id %}
+ {% url 'horizon:project:networks:subnets:detail' vpnservice.subnet_id as subnet_url %}
+ {{ vpnservice.subnet.name_or_id }} {{ vpnservice.subnet.cidr }}
+ {% else %}
+ {% trans "None" %}
+ {% endif %}
{% trans "VPN Connections" %}
diff --git a/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/update_endpointgroup.html b/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/update_endpointgroup.html
new file mode 100644
index 0000000..7d0547b
--- /dev/null
+++ b/neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/update_endpointgroup.html
@@ -0,0 +1,7 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Edit Endpoint Group" %}{% endblock %}
+
+{% block main %}
+ {% include 'project/vpn/_update_endpointgroup.html' %}
+{% endblock %}
diff --git a/neutron_vpnaas_dashboard/dashboards/project/vpn/tests.py b/neutron_vpnaas_dashboard/dashboards/project/vpn/tests.py
index 75bfd2c..79a6b5a 100644
--- a/neutron_vpnaas_dashboard/dashboards/project/vpn/tests.py
+++ b/neutron_vpnaas_dashboard/dashboards/project/vpn/tests.py
@@ -40,17 +40,22 @@ class VPNTests(test.TestCase):
ADDIKEPOLICY_PATH = 'horizon:%s:vpn:addikepolicy' % DASHBOARD
ADDIPSECPOLICY_PATH = 'horizon:%s:vpn:addipsecpolicy' % DASHBOARD
ADDVPNSERVICE_PATH = 'horizon:%s:vpn:addvpnservice' % DASHBOARD
+ ADDENDPOINTGROUP_PATH = 'horizon:%s:vpn:addendpointgroup' % DASHBOARD
ADDVPNCONNECTION_PATH = 'horizon:%s:vpn:addipsecsiteconnection' % DASHBOARD
IKEPOLICY_DETAIL_PATH = 'horizon:%s:vpn:ikepolicydetails' % DASHBOARD
IPSECPOLICY_DETAIL_PATH = 'horizon:%s:vpn:ipsecpolicydetails' % DASHBOARD
VPNSERVICE_DETAIL_PATH = 'horizon:%s:vpn:vpnservicedetails' % DASHBOARD
+ ENDPOINTGROUP_DETAIL_PATH = 'horizon:%s:vpn:endpointgroupdetails' %\
+ DASHBOARD
VPNCONNECTION_DETAIL_PATH = 'horizon:%s:vpn:ipsecsiteconnectiondetails' %\
DASHBOARD
UPDATEIKEPOLICY_PATH = 'horizon:%s:vpn:update_ikepolicy' % DASHBOARD
UPDATEIPSECPOLICY_PATH = 'horizon:%s:vpn:update_ipsecpolicy' % DASHBOARD
UPDATEVPNSERVICE_PATH = 'horizon:%s:vpn:update_vpnservice' % DASHBOARD
+ UPDATEENDPOINTGROUP_PATH = 'horizon:%s:vpn:update_endpointgroup' %\
+ DASHBOARD
UPDATEVPNCONNECTION_PATH = 'horizon:%s:vpn:update_ipsecsiteconnection' %\
DASHBOARD
@@ -60,6 +65,11 @@ class VPNTests(test.TestCase):
IsA(http.HttpRequest), tenant_id=self.tenant.id) \
.AndReturn(self.vpnservices.list())
+ # retrieves endpoint groups
+ api_vpn.endpointgroup_list(
+ IsA(http.HttpRequest), tenant_id=self.tenant.id) \
+ .AndReturn(self.endpointgroups.list())
+
# retrieves ikepolicies
api_vpn.ikepolicy_list(
IsA(http.HttpRequest), tenant_id=self.tenant.id) \
@@ -79,6 +89,9 @@ class VPNTests(test.TestCase):
api_vpn.vpnservice_list(
IsA(http.HttpRequest),
tenant_id=self.tenant.id).AndRaise(self.exceptions.neutron)
+ api_vpn.endpointgroup_list(
+ IsA(http.HttpRequest),
+ tenant_id=self.tenant.id).AndRaise(self.exceptions.neutron)
api_vpn.ikepolicy_list(
IsA(http.HttpRequest),
tenant_id=self.tenant.id).AndRaise(self.exceptions.neutron)
@@ -90,7 +103,7 @@ class VPNTests(test.TestCase):
tenant_id=self.tenant.id).AndRaise(self.exceptions.neutron)
@test.create_stubs({api_vpn: ('ikepolicy_list', 'ipsecpolicy_list',
- 'vpnservice_list',
+ 'vpnservice_list', 'endpointgroup_list',
'ipsecsiteconnection_list')})
def test_index_vpnservices(self):
self.set_up_expect()
@@ -106,7 +119,23 @@ class VPNTests(test.TestCase):
len(self.vpnservices.list()))
@test.create_stubs({api_vpn: ('ikepolicy_list', 'ipsecpolicy_list',
- 'vpnservice_list',
+ 'vpnservice_list', 'endpointgroup_list',
+ 'ipsecsiteconnection_list')})
+ def test_index_endpointgroups(self):
+ self.set_up_expect()
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(self.INDEX_URL + '?tab=vpntabs__endpointgroups')
+
+ self.assertTemplateUsed(res, '%s/vpn/index.html'
+ % self.DASHBOARD)
+ self.assertTemplateUsed(res, 'horizon/common/_detail_table.html')
+ self.assertEqual(len(res.context['endpointgroupstable_table'].data),
+ len(self.endpointgroups.list()))
+
+ @test.create_stubs({api_vpn: ('ikepolicy_list', 'ipsecpolicy_list',
+ 'vpnservice_list', 'endpointgroup_list',
'ipsecsiteconnection_list')})
def test_index_ikepolicies(self):
self.set_up_expect()
@@ -122,7 +151,7 @@ class VPNTests(test.TestCase):
len(self.ikepolicies.list()))
@test.create_stubs({api_vpn: ('ikepolicy_list', 'ipsecpolicy_list',
- 'vpnservice_list',
+ 'vpnservice_list', 'endpointgroup_list',
'ipsecsiteconnection_list')})
def test_index_ipsecpolicies(self):
self.set_up_expect()
@@ -138,7 +167,7 @@ class VPNTests(test.TestCase):
len(self.ipsecpolicies.list()))
@test.create_stubs({api_vpn: ('ikepolicy_list', 'ipsecpolicy_list',
- 'vpnservice_list',
+ 'vpnservice_list', 'endpointgroup_list',
'ipsecsiteconnection_list')})
def test_index_ipsecsiteconnections(self):
self.set_up_expect()
@@ -156,7 +185,7 @@ class VPNTests(test.TestCase):
len(self.ipsecsiteconnections.list()))
@test.create_stubs({api_vpn: ('ikepolicy_list', 'ipsecpolicy_list',
- 'vpnservice_list',
+ 'vpnservice_list', 'endpointgroup_list',
'ipsecsiteconnection_list')})
def test_index_exception_vpnservices(self):
self.set_up_expect_with_exception()
@@ -172,7 +201,23 @@ class VPNTests(test.TestCase):
self.assertEqual(len(res.context['table'].data), 0)
@test.create_stubs({api_vpn: ('ikepolicy_list', 'ipsecpolicy_list',
- 'vpnservice_list',
+ 'vpnservice_list', 'endpointgroup_list',
+ 'ipsecsiteconnection_list')})
+ def test_index_exception_endpointgroups(self):
+ self.set_up_expect_with_exception()
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(self.INDEX_URL + '?tab=vpntabs__endpointgroups')
+
+ self.assertTemplateUsed(res, '%s/vpn/index.html'
+ % self.DASHBOARD)
+ self.assertTemplateUsed(res,
+ 'horizon/common/_detail_table.html')
+ self.assertEqual(len(res.context['table'].data), 0)
+
+ @test.create_stubs({api_vpn: ('ikepolicy_list', 'ipsecpolicy_list',
+ 'vpnservice_list', 'endpointgroup_list',
'ipsecsiteconnection_list')})
def test_index_exception_ikepolicies(self):
self.set_up_expect_with_exception()
@@ -188,7 +233,7 @@ class VPNTests(test.TestCase):
self.assertEqual(len(res.context['table'].data), 0)
@test.create_stubs({api_vpn: ('ikepolicy_list', 'ipsecpolicy_list',
- 'vpnservice_list',
+ 'vpnservice_list', 'endpointgroup_list',
'ipsecsiteconnection_list')})
def test_index_exception_ipsecpolicies(self):
self.set_up_expect_with_exception()
@@ -204,7 +249,7 @@ class VPNTests(test.TestCase):
self.assertEqual(len(res.context['table'].data), 0)
@test.create_stubs({api_vpn: ('ikepolicy_list', 'ipsecpolicy_list',
- 'vpnservice_list',
+ 'vpnservice_list', 'endpointgroup_list',
'ipsecsiteconnection_list')})
def test_index_exception_ipsecsiteconnections(self):
self.set_up_expect_with_exception()
@@ -293,7 +338,68 @@ class VPNTests(test.TestCase):
res = self.client.post(reverse(self.ADDVPNSERVICE_PATH), form_data)
- self.assertFormErrors(res, 2)
+ self.assertFormErrors(res, 1)
+
+ @test.create_stubs({api.neutron: ('network_list_for_tenant', )})
+ def test_add_endpointgroup_get(self):
+ networks = [{'subnets': [self.subnets.first(), ]}, ]
+
+ api.neutron.network_list_for_tenant(
+ IsA(http.HttpRequest), self.tenant.id).AndReturn(networks)
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(reverse(self.ADDENDPOINTGROUP_PATH))
+
+ workflow = res.context['workflow']
+ self.assertTemplateUsed(res, views.WorkflowView.template_name)
+ self.assertEqual(workflow.name, workflows.AddEndpointGroup.name)
+
+ expected_objs = ['', ]
+ self.assertQuerysetEqual(workflow.steps, expected_objs)
+
+ @test.create_stubs({api.neutron: ('network_list_for_tenant', ),
+ api_vpn: ('endpointgroup_create', )})
+ def test_add_endpointgroup_post(self):
+ endpointgroup = self.endpointgroups.first()
+ networks = [{'subnets': [self.subnets.first(), ]}, ]
+
+ api.neutron.network_list_for_tenant(
+ IsA(http.HttpRequest), self.tenant.id).AndReturn(networks)
+
+ form_data = {'name': endpointgroup['name'],
+ 'description': endpointgroup['description'],
+ 'endpoints': endpointgroup['endpoints'],
+ 'type': endpointgroup['type']}
+
+ api_vpn.endpointgroup_create(
+ IsA(http.HttpRequest), **form_data).AndReturn(endpointgroup)
+
+ self.mox.ReplayAll()
+
+ res = self.client.post(reverse(self.ADDENDPOINTGROUP_PATH), form_data)
+
+ self.assertNoFormErrors(res)
+ self.assertRedirectsNoFollow(res, str(self.INDEX_URL))
+
+ @test.create_stubs({api.neutron: ('network_list_for_tenant', )})
+ def test_add_endpointgroup_post_error(self):
+ endpointgroup = self.endpointgroups.first()
+ networks = [{'subnets': [self.subnets.first(), ]}, ]
+
+ api.neutron.network_list_for_tenant(
+ IsA(http.HttpRequest), self.tenant.id).AndReturn(networks)
+
+ self.mox.ReplayAll()
+
+ form_data = {'name': endpointgroup['name'],
+ 'description': endpointgroup['description'],
+ 'endpoints': endpointgroup['endpoints'],
+ 'type': ''}
+
+ res = self.client.post(reverse(self.ADDENDPOINTGROUP_PATH), form_data)
+
+ self.assertFormErrors(res, 1)
def test_add_ikepolicy_get(self):
res = self.client.get(reverse(self.ADDIKEPOLICY_PATH))
@@ -408,7 +514,7 @@ class VPNTests(test.TestCase):
self.assertFormErrors(res, 1)
@test.create_stubs({api_vpn: ('ikepolicy_list', 'ipsecpolicy_list',
- 'vpnservice_list')})
+ 'vpnservice_list', 'endpointgroup_list',)})
def test_add_ipsecsiteconnection_get(self):
ikepolicies = self.ikepolicies.list()
ipsecpolicies = self.ipsecpolicies.list()
@@ -439,13 +545,13 @@ class VPNTests(test.TestCase):
self.assertQuerysetEqual(workflow.steps, expected_objs)
@test.create_stubs({api_vpn: ('ikepolicy_list', 'ipsecpolicy_list',
- 'vpnservice_list',
+ 'vpnservice_list', 'endpointgroup_list',
'ipsecsiteconnection_create')})
def test_add_ipsecsiteconnection_post(self):
self._test_add_ipsecsiteconnection_post()
@test.create_stubs({api_vpn: ('ikepolicy_list', 'ipsecpolicy_list',
- 'vpnservice_list',
+ 'vpnservice_list', 'endpointgroup_list',
'ipsecsiteconnection_create')})
def test_add_ipsecsiteconnection_post_single_subnet(self):
self._test_add_ipsecsiteconnection_post(subnet_list=False)
@@ -498,13 +604,13 @@ class VPNTests(test.TestCase):
self.assertRedirectsNoFollow(res, str(self.INDEX_URL))
@test.create_stubs({api_vpn: ('ikepolicy_list', 'ipsecpolicy_list',
- 'vpnservice_list',
+ 'vpnservice_list', 'endpointgroup_list',
'ipsecsiteconnection_create')})
def test_add_ipsecsiteconnection_post_required_fields_error(self):
self._test_add_ipsecsiteconnection_post_error()
@test.create_stubs({api_vpn: ('ikepolicy_list', 'ipsecpolicy_list',
- 'vpnservice_list',
+ 'vpnservice_list', 'endpointgroup_list',
'ipsecsiteconnection_create')})
def test_add_ipsecsiteconnection_post_peer_cidrs_error(self):
self._test_add_ipsecsiteconnection_post_error(subnets=True)
@@ -548,7 +654,10 @@ class VPNTests(test.TestCase):
res = self.client.post(reverse(self.ADDVPNCONNECTION_PATH), form_data)
- self.assertFormErrors(res, 7)
+ if subnets:
+ self.assertFormErrors(res, 7)
+ else:
+ self.assertFormErrors(res, 6)
@test.create_stubs({api_vpn: ('vpnservice_get', )})
def test_update_vpnservice_get(self):
@@ -590,6 +699,48 @@ class VPNTests(test.TestCase):
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, str(self.INDEX_URL))
+ @test.create_stubs({api_vpn: ('endpointgroup_get', )})
+ def test_update_endpointgroup_get(self):
+ endpointgroup = self.endpointgroups.first()
+
+ api_vpn.endpointgroup_get(IsA(http.HttpRequest), endpointgroup.id)\
+ .AndReturn(endpointgroup)
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(
+ reverse(self.UPDATEENDPOINTGROUP_PATH, args=(endpointgroup.id,)))
+
+ self.assertTemplateUsed(
+ res, 'project/vpn/update_endpointgroup.html')
+
+ @test.create_stubs({api_vpn: ('endpointgroup_get',
+ 'endpointgroup_update')})
+ def test_update_endpointgroup_post(self):
+ endpointgroup = self.endpointgroups.first()
+
+ api_vpn.endpointgroup_get(IsA(http.HttpRequest), endpointgroup.id)\
+ .AndReturn(endpointgroup)
+
+ data = {'name': endpointgroup.name,
+ 'description': endpointgroup.description}
+
+ api_vpn.endpointgroup_update(IsA(http.HttpRequest), endpointgroup.id,
+ endpointgroup=data
+ ).AndReturn(endpointgroup)
+
+ self.mox.ReplayAll()
+
+ form_data = data.copy()
+ form_data['endpoint_group_id'] = endpointgroup.id
+
+ res = self.client.post(reverse(self.UPDATEENDPOINTGROUP_PATH,
+ args=(endpointgroup.id, )
+ ), form_data)
+
+ self.assertNoFormErrors(res)
+ self.assertRedirectsNoFollow(res, str(self.INDEX_URL))
+
@test.create_stubs({api_vpn: ('ikepolicy_get', )})
def test_update_ikepolicy_get(self):
ikepolicy = self.ikepolicies.first()
@@ -764,6 +915,23 @@ class VPNTests(test.TestCase):
self.assertNoFormErrors(res)
+ @test.create_stubs({api_vpn: ('endpointgroup_list',
+ 'endpointgroup_delete',)})
+ def test_delete_endpointgroup(self):
+ endpointgroup = self.endpointgroups.list()[0]
+ api_vpn.endpointgroup_list(
+ IsA(http.HttpRequest), tenant_id=self.tenant.id) \
+ .AndReturn(self.endpointgroups.list())
+ api_vpn.endpointgroup_delete(IsA(http.HttpRequest), endpointgroup.id)
+ self.mox.ReplayAll()
+
+ form_data = {"action":
+ "endpointgroupstable__deleteendpointgroup__%s"
+ % endpointgroup.id}
+ res = self.client.post(self.INDEX_URL, form_data)
+
+ self.assertNoFormErrors(res)
+
@test.create_stubs({api_vpn: ('ikepolicy_list', 'ikepolicy_delete',)})
def test_delete_ikepolicy(self):
ikepolicy = self.ikepolicies.list()[1]
diff --git a/neutron_vpnaas_dashboard/dashboards/project/vpn/urls.py b/neutron_vpnaas_dashboard/dashboards/project/vpn/urls.py
index a99195b..62a43b0 100644
--- a/neutron_vpnaas_dashboard/dashboards/project/vpn/urls.py
+++ b/neutron_vpnaas_dashboard/dashboards/project/vpn/urls.py
@@ -36,12 +36,18 @@ urlpatterns = [
views.AddVPNServiceView.as_view(), name='addvpnservice'),
url(r'^update_vpnservice/(?P[^/]+)/$',
views.UpdateVPNServiceView.as_view(), name='update_vpnservice'),
+ url(r'^addendpointgroup$',
+ views.AddEndpointGroupView.as_view(), name='addendpointgroup'),
+ url(r'^update_endpointgroup/(?P[^/]+)/$',
+ views.UpdateEndpointGroupView.as_view(), name='update_endpointgroup'),
url(r'^ikepolicy/(?P[^/]+)/$',
views.IKEPolicyDetailsView.as_view(), name='ikepolicydetails'),
url(r'^ipsecpolicy/(?P[^/]+)/$',
views.IPSecPolicyDetailsView.as_view(), name='ipsecpolicydetails'),
url(r'^vpnservice/(?P[^/]+)/$',
views.VPNServiceDetailsView.as_view(), name='vpnservicedetails'),
+ url(r'^endpointgroup/(?P[^/]+)/$',
+ views.EndpointGroupDetailsView.as_view(), name='endpointgroupdetails'),
url(r'^ipsecsiteconnection/(?P[^/]+)/$',
views.IPSecSiteConnectionDetailsView.as_view(),
name='ipsecsiteconnectiondetails'),
diff --git a/neutron_vpnaas_dashboard/dashboards/project/vpn/views.py b/neutron_vpnaas_dashboard/dashboards/project/vpn/views.py
index c792826..1dcfa87 100644
--- a/neutron_vpnaas_dashboard/dashboards/project/vpn/views.py
+++ b/neutron_vpnaas_dashboard/dashboards/project/vpn/views.py
@@ -39,6 +39,10 @@ class AddVPNServiceView(horizon_workflows.WorkflowView):
workflow_class = workflows.AddVPNService
+class AddEndpointGroupView(horizon_workflows.WorkflowView):
+ workflow_class = workflows.AddEndpointGroup
+
+
class AddIPSecSiteConnectionView(horizon_workflows.WorkflowView):
workflow_class = workflows.AddIPSecSiteConnection
@@ -161,6 +165,50 @@ class VPNServiceDetailsView(horizon_tabs.TabView):
return reverse_lazy('horizon:project:vpn:index')
+class EndpointGroupDetailsView(horizon_tabs.TabView):
+ tab_group_class = tabs.EndpointGroupDetailsTabs
+ template_name = 'horizon/common/_detail.html'
+ page_title = "{{ endpointgroup.name|default:endpointgroup.id }}"
+
+ @memoized.memoized_method
+ def get_data(self):
+ gid = self.kwargs['endpoint_group_id']
+
+ try:
+ endpointgroup = api_vpn.endpointgroup_get(self.request, gid)
+ except Exception:
+ msg = _('Unable to retrieve endpoint group details.')
+ exceptions.handle(self.request, msg,
+ redirect=self.get_redirect_url())
+ try:
+ connections = api_vpn.ipsecsiteconnection_list(
+ self.request, endpoint_group_id=gid)
+ endpointgroup.vpnconnections = connections
+ except Exception:
+ endpointgroup.vpnconnections = []
+
+ return endpointgroup
+
+ def get_context_data(self, **kwargs):
+ context = super(EndpointGroupDetailsView, self).get_context_data(
+ **kwargs)
+ endpointgroup = self.get_data()
+ table = tables.EndpointGroupTable(self.request)
+ context["endpointgroup"] = endpointgroup
+ context["url"] = self.get_redirect_url()
+ context["actions"] = table.render_row_actions(endpointgroup)
+ return context
+
+ def get_tabs(self, request, *args, **kwargs):
+ endpointgroup = self.get_data()
+ return self.tab_group_class(request, endpointgroup=endpointgroup,
+ **kwargs)
+
+ @staticmethod
+ def get_redirect_url():
+ return reverse('horizon:project:vpn:index')
+
+
class IPSecSiteConnectionDetailsView(horizon_tabs.TabView):
tab_group_class = tabs.IPSecSiteConnectionDetailsTabs
template_name = 'horizon/common/_detail.html'
@@ -232,6 +280,41 @@ class UpdateVPNServiceView(horizon_forms.ModalFormView):
'admin_state_up': vpnservice['admin_state_up']}
+class UpdateEndpointGroupView(horizon_forms.ModalFormView):
+ form_class = forms.UpdateEndpointGroup
+ form_id = "update_endpointgroup_form"
+ template_name = "project/vpn/update_endpointgroup.html"
+ context_object_name = 'endpointgroup'
+ submit_label = _("Save Changes")
+ submit_url = "horizon:project:vpn:update_endpointgroup"
+ success_url = reverse_lazy("horizon:project:vpn:index")
+ page_title = _("Edit Endpoint Group")
+
+ def get_context_data(self, **kwargs):
+ context = super(UpdateEndpointGroupView, self).get_context_data(
+ **kwargs)
+ context["endpoint_group_id"] = self.kwargs['endpoint_group_id']
+ args = (self.kwargs['endpoint_group_id'],)
+ context['submit_url'] = reverse(self.submit_url, args=args)
+ return context
+
+ @memoized.memoized_method
+ def _get_object(self, *args, **kwargs):
+ endpoint_group_id = self.kwargs['endpoint_group_id']
+ try:
+ return api_vpn.endpointgroup_get(self.request, endpoint_group_id)
+ except Exception as e:
+ redirect = self.success_url
+ msg = _('Unable to retrieve Endpoint Group details. %s') % e
+ exceptions.handle(self.request, msg, redirect=redirect)
+
+ def get_initial(self):
+ endpointgroup = self._get_object()
+ return {'name': endpointgroup['name'],
+ 'endpoint_group_id': endpointgroup['id'],
+ 'description': endpointgroup['description']}
+
+
class UpdateIKEPolicyView(horizon_forms.ModalFormView):
form_class = forms.UpdateIKEPolicy
form_id = "update_ikepolicy_form"
@@ -346,16 +429,25 @@ class UpdateIPSecSiteConnectionView(horizon_forms.ModalFormView):
def get_initial(self):
ipsecsiteconnection = self._get_object()
- return {'name': ipsecsiteconnection['name'],
- 'ipsecsiteconnection_id': ipsecsiteconnection['id'],
- 'description': ipsecsiteconnection['description'],
- 'peer_address': ipsecsiteconnection['peer_address'],
- 'peer_id': ipsecsiteconnection['peer_id'],
- 'peer_cidrs': ", ".join(ipsecsiteconnection['peer_cidrs']),
- 'psk': ipsecsiteconnection['psk'],
- 'mtu': ipsecsiteconnection['mtu'],
- 'dpd_action': ipsecsiteconnection['dpd']['action'],
- 'dpd_interval': ipsecsiteconnection['dpd']['interval'],
- 'dpd_timeout': ipsecsiteconnection['dpd']['timeout'],
- 'initiator': ipsecsiteconnection['initiator'],
- 'admin_state_up': ipsecsiteconnection['admin_state_up']}
+ data = {
+ 'name': ipsecsiteconnection['name'],
+ 'ipsecsiteconnection_id': ipsecsiteconnection['id'],
+ 'description': ipsecsiteconnection['description'],
+ 'peer_address': ipsecsiteconnection['peer_address'],
+ 'peer_id': ipsecsiteconnection['peer_id'],
+ 'psk': ipsecsiteconnection['psk'],
+ 'mtu': ipsecsiteconnection['mtu'],
+ 'dpd_action': ipsecsiteconnection['dpd']['action'],
+ 'dpd_interval': ipsecsiteconnection['dpd']['interval'],
+ 'dpd_timeout': ipsecsiteconnection['dpd']['timeout'],
+ 'initiator': ipsecsiteconnection['initiator'],
+ 'admin_state_up': ipsecsiteconnection['admin_state_up']
+ }
+ if 'local_ep_group_id' in ipsecsiteconnection:
+ data['local_ep_group_id'] = \
+ ipsecsiteconnection['local_ep_group_id']
+ data['peer_ep_group_id'] = ipsecsiteconnection['peer_ep_group_id']
+ return data
+ else:
+ data['peer_cidrs'] = ", ".join(ipsecsiteconnection['peer_cidrs'])
+ return data
diff --git a/neutron_vpnaas_dashboard/dashboards/project/vpn/workflows.py b/neutron_vpnaas_dashboard/dashboards/project/vpn/workflows.py
index 2c3745f..2506db6 100644
--- a/neutron_vpnaas_dashboard/dashboards/project/vpn/workflows.py
+++ b/neutron_vpnaas_dashboard/dashboards/project/vpn/workflows.py
@@ -29,7 +29,11 @@ class AddVPNServiceAction(workflows.Action):
initial="", required=False,
max_length=80, label=_("Description"))
router_id = forms.ChoiceField(label=_("Router"))
- subnet_id = forms.ChoiceField(label=_("Subnet"))
+ subnet_id = forms.ChoiceField(
+ label=_("Subnet"),
+ help_text=_("Optional. No need to be specified "
+ "when you use endpoint groups."),
+ required=False)
admin_state_up = forms.BooleanField(
label=_("Enable Admin State"),
help_text=_("The state of VPN service to start in. If disabled "
@@ -106,6 +110,109 @@ class AddVPNService(workflows.Workflow):
return False
+class AddEndpointGroupAction(workflows.Action):
+ name = forms.CharField(
+ max_length=80,
+ label=_("Name"),
+ required=False)
+ description = forms.CharField(
+ initial="",
+ required=False,
+ max_length=80,
+ label=_("Description"))
+ type = forms.ThemableChoiceField(
+ label=_("Type"),
+ help_text=_("IPSec connection validation requires that local "
+ "endpoints are subnets, and peer endpoints are CIDRs."),
+ choices=[('cidr', _('CIDR (for external systems)')),
+ ('subnet', _('Subnet (for local systems)'))],
+ widget=forms.ThemableSelectWidget(attrs={
+ 'class': 'switchable',
+ 'data-slug': 'type', }))
+ cidrs = forms.MultiIPField(
+ required=False,
+ label=_("External System CIDRs"),
+ widget=forms.TextInput(attrs={
+ 'class': 'switched',
+ 'data-switch-on': 'type',
+ 'data-type-cidr': _("External System CIDRs"),
+ }),
+ help_text=_("Remote peer subnet(s) address(es) "
+ "with mask(s) in CIDR format "
+ "separated with commas if needed "
+ "(e.g. 20.1.0.0/24, 21.1.0.0/24). "
+ "This field is valid if type is CIDR"),
+ version=forms.IPv4 | forms.IPv6,
+ mask=True)
+ subnets = forms.MultipleChoiceField(
+ required=False,
+ label=_("Local System Subnets"),
+ widget=forms.ThemableCheckboxSelectMultiple(attrs={
+ 'class': 'switched',
+ 'data-switch-on': 'type',
+ 'data-type-subnet': _("External System Subnets"),
+ }),
+ help_text=_("Local subnet(s). "
+ "This field is valid if type is Subnet"),)
+
+ def populate_subnets_choices(self, request, context):
+ subnets_choices = []
+ try:
+ tenant_id = request.user.tenant_id
+ networks = api.neutron.network_list_for_tenant(request, tenant_id)
+ except Exception:
+ exceptions.handle(request,
+ _('Unable to retrieve networks list.'))
+ networks = []
+ for n in networks:
+ for s in n['subnets']:
+ subnets_choices.append((s.id, s.cidr))
+ self.fields['subnets'].choices = subnets_choices
+ return subnets_choices
+
+ class Meta(object):
+ name = _("Add New Endpoint Groups")
+ permissions = ('openstack.services.network',)
+ help_text_template = "project/vpn/_add_endpoint_group_help.html"
+
+
+class AddEndpointGroupStep(workflows.Step):
+ action_class = AddEndpointGroupAction
+ contributes = ("name", "description", "type",
+ "cidrs", "subnets", "endpoints")
+
+ def contribute(self, data, context):
+ context = super(AddEndpointGroupStep, self).contribute(data, context)
+ if context['type'] == 'cidr':
+ cidrs = context['cidrs']
+ context['endpoints'] = [
+ cidr.strip() for cidr in cidrs.split(',') if cidr.strip()]
+ else:
+ context['endpoints'] = context['subnets']
+ if data:
+ return context
+
+
+class AddEndpointGroup(workflows.Workflow):
+ slug = "addendpointgroup"
+ name = _("Add Endpoint Group")
+ finalize_button_name = _("Add")
+ success_message = _('Added Endpoint Group "%s".')
+ failure_message = _('Unable to add Endpoint Group "%s".')
+ success_url = "horizon:project:vpn:index"
+ default_steps = (AddEndpointGroupStep,)
+
+ def format_status_message(self, message):
+ return message % self.context.get('name')
+
+ def handle(self, request, context):
+ try:
+ api_vpn.endpointgroup_create(request, **context)
+ return True
+ except Exception:
+ return False
+
+
class AddIKEPolicyAction(workflows.Action):
name = forms.CharField(max_length=80, label=_("Name"), required=False)
description = forms.CharField(
@@ -315,6 +422,12 @@ class AddIPSecSiteConnectionAction(workflows.Action):
max_length=80, label=_("Description"))
vpnservice_id = forms.ChoiceField(
label=_("VPN Service associated with this connection"))
+ local_ep_group_id = forms.ChoiceField(
+ required=False,
+ label=_("Endpoint Group for local subnet(s)"),
+ help_text=_("Local subnets which the new IPsec connection is "
+ "connected to. Required if no subnet is specified "
+ "in a VPN service selected."))
ikepolicy_id = forms.ChoiceField(
label=_("IKE Policy associated with this connection"))
ipsecpolicy_id = forms.ChoiceField(
@@ -331,9 +444,15 @@ class AddIPSecSiteConnectionAction(workflows.Action):
"Can be IPv4/IPv6 address, e-mail, key ID, or FQDN"),
version=forms.IPv4 | forms.IPv6,
mask=False)
+ peer_ep_group_id = forms.ChoiceField(
+ required=False,
+ label=_("Endpoint Group for remote peer CIDR(s)"),
+ help_text=_("Remove peer CIDR(s) connected to the new IPSec "
+ "connection."))
peer_cidrs = forms.MultiIPField(
+ required=False,
label=_("Remote peer subnet(s)"),
- help_text=_("Remote peer subnet(s) address(es) "
+ help_text=_("(Deprecated) Remote peer subnet(s) address(es) "
"with mask(s) in CIDR format "
"separated with commas if needed "
"(e.g. 20.1.0.0/24, 21.1.0.0/24)"),
@@ -389,6 +508,36 @@ class AddIPSecSiteConnectionAction(workflows.Action):
self.fields['vpnservice_id'].choices = vpnservice_id_choices
return vpnservice_id_choices
+ def populate_local_ep_group_id_choices(self, request, context):
+ try:
+ tenant_id = self.request.user.tenant_id
+ endpointgroups = api_vpn.endpointgroup_list(request,
+ tenant_id=tenant_id)
+ except Exception:
+ exceptions.handle(request,
+ _('Unable to retrieve endpoint group list.'))
+ endpointgroups = []
+ local_ep_group_ids = [(s.id, s.name) for s in endpointgroups
+ if s.type == 'subnet']
+ local_ep_group_ids.insert(0, ('', _("Select local endpoint group")))
+ self.fields['local_ep_group_id'].choices = local_ep_group_ids
+ return local_ep_group_ids
+
+ def populate_peer_ep_group_id_choices(self, request, context):
+ try:
+ tenant_id = self.request.user.tenant_id
+ endpointgroups = api_vpn.endpointgroup_list(request,
+ tenant_id=tenant_id)
+ except Exception:
+ exceptions.handle(request,
+ _('Unable to retrieve endpoint group list.'))
+ endpointgroups = []
+ peer_ep_group_ids = [(s.id, s.name) for s in endpointgroups
+ if s.type == 'cidr']
+ peer_ep_group_ids.insert(0, ('', _("Select peer endpoint group")))
+ self.fields['peer_ep_group_id'].choices = peer_ep_group_ids
+ return peer_ep_group_ids
+
class Meta(object):
name = _("Add New IPSec Site Connection")
permissions = ('openstack.services.network',)
@@ -403,7 +552,8 @@ class AddIPSecSiteConnectionStep(workflows.Step):
action_class = AddIPSecSiteConnectionAction
contributes = ("name", "description",
"vpnservice_id", "ikepolicy_id", "ipsecpolicy_id",
- "peer_address", "peer_id", "peer_cidrs", "psk")
+ "peer_address", "peer_id", "peer_cidrs", "psk",
+ "local_ep_group_id", "peer_ep_group_id")
class AddIPSecSiteConnectionOptionalAction(workflows.Action):
@@ -489,7 +639,8 @@ class AddIPSecSiteConnectionOptionalStep(workflows.Step):
context.pop('dpd_timeout')
cidrs = context['peer_cidrs']
- context['peer_cidrs'] = cidrs.replace(" ", "").split(",")
+ context['peer_cidrs'] = [cidr.strip() for cidr in cidrs.split(',')
+ if cidr.strip()]
if data:
return context
diff --git a/neutron_vpnaas_dashboard/test/api_tests/vpnaas_tests.py b/neutron_vpnaas_dashboard/test/api_tests/vpnaas_tests.py
index 38a37c6..13f358f 100644
--- a/neutron_vpnaas_dashboard/test/api_tests/vpnaas_tests.py
+++ b/neutron_vpnaas_dashboard/test/api_tests/vpnaas_tests.py
@@ -88,6 +88,64 @@ class VPNaasApiTests(test.APITestCase):
ret_val = api_vpn.vpnservice_get(self.request, vpnservice.id)
self.assertIsInstance(ret_val, api_vpn.VPNService)
+ @test.create_stubs({neutronclient: ('create_endpoint_group',)})
+ def test_endpointgroup_create(self):
+ endpointgroup = self.api_endpointgroups.first()
+ form_data = {
+ 'name': endpointgroup['name'],
+ 'description': endpointgroup['description'],
+ 'type': endpointgroup['type'],
+ 'endpoints': endpointgroup['endpoints']
+ }
+
+ endpoint_group = {'endpoint_group': self.api_endpointgroups.first()}
+ neutronclient.create_endpoint_group(
+ {'endpoint_group': form_data}).AndReturn(endpoint_group)
+ self.mox.ReplayAll()
+
+ ret_val = api_vpn.endpointgroup_create(self.request, **form_data)
+ self.assertIsInstance(ret_val, api_vpn.EndpointGroup)
+
+ @test.create_stubs({neutronclient: ('list_endpoint_groups',
+ 'list_ipsec_site_connections')})
+ def test_endpointgroup_list(self):
+ endpointgroups = {'endpoint_groups': self.endpointgroups.list()}
+ endpointgroups_dict = {
+ 'endpoint_groups': self.api_endpointgroups.list()}
+ ipsecsiteconnections_dict = {
+ 'ipsec_site_connections': self.api_ipsecsiteconnections.list()}
+
+ neutronclient.list_endpoint_groups().AndReturn(endpointgroups_dict)
+ neutronclient.list_ipsec_site_connections().AndReturn(
+ ipsecsiteconnections_dict)
+
+ self.mox.ReplayAll()
+
+ ret_val = api_vpn.endpointgroup_list(self.request)
+ for (v, d) in zip(ret_val, endpointgroups['endpoint_groups']):
+ self.assertIsInstance(v, api_vpn.EndpointGroup)
+ self.assertTrue(v.name, d.name)
+ self.assertTrue(v.id)
+
+ @test.create_stubs({neutronclient: ('show_endpoint_group',
+ 'list_ipsec_site_connections')})
+ def test_endpointgroup_get(self):
+ endpoint_group = self.endpointgroups.first()
+ endpoint_group_dict = {
+ 'endpoint_group': self.api_endpointgroups.first()}
+ ipsecsiteconnections_dict = {
+ 'ipsec_site_connections': self.api_ipsecsiteconnections.list()}
+
+ neutronclient.show_endpoint_group(
+ endpoint_group.id).AndReturn(endpoint_group_dict)
+ neutronclient.list_ipsec_site_connections().AndReturn(
+ ipsecsiteconnections_dict)
+
+ self.mox.ReplayAll()
+
+ ret_val = api_vpn.endpointgroup_get(self.request, endpoint_group.id)
+ self.assertIsInstance(ret_val, api_vpn.EndpointGroup)
+
@test.create_stubs({neutronclient: ('create_ikepolicy',)})
def test_ikepolicy_create(self):
ikepolicy1 = self.api_ikepolicies.first()
diff --git a/neutron_vpnaas_dashboard/test/test_data/vpnaas_data.py b/neutron_vpnaas_dashboard/test/test_data/vpnaas_data.py
index 8d0e0c7..231d787 100644
--- a/neutron_vpnaas_dashboard/test/test_data/vpnaas_data.py
+++ b/neutron_vpnaas_dashboard/test/test_data/vpnaas_data.py
@@ -23,12 +23,14 @@ def data(TEST):
TEST.ikepolicies = utils.TestDataContainer()
TEST.ipsecpolicies = utils.TestDataContainer()
TEST.ipsecsiteconnections = utils.TestDataContainer()
+ TEST.endpointgroups = utils.TestDataContainer()
# Data return by neutronclient.
TEST.api_vpnservices = utils.TestDataContainer()
TEST.api_ikepolicies = utils.TestDataContainer()
TEST.api_ipsecpolicies = utils.TestDataContainer()
TEST.api_ipsecsiteconnections = utils.TestDataContainer()
+ TEST.api_endpointgroups = utils.TestDataContainer()
# 1st VPNService.
vpnservice_dict = {'id': '09a26949-6231-4f72-942a-0c8c0ddd4d61',
@@ -64,6 +66,17 @@ def data(TEST):
TEST.api_vpnservices.add(vpnservice_dict)
TEST.vpnservices.add(vpn.VPNService(vpnservice_dict))
+ # 1st Endpoint Group
+ endpointgroup_dict = {'id': 'baa588ff-e1b9-4256-8687-9f06315f64b7',
+ 'tenant_id': '1',
+ 'name': 'endpoint_group_one',
+ 'description': 'the first test endpoint group',
+ 'type': 'subnet',
+ 'endpoints': [TEST.subnets.first().id]
+ }
+ TEST.api_endpointgroups.add(endpointgroup_dict)
+ TEST.endpointgroups.add(vpn.EndpointGroup(endpointgroup_dict))
+
# 1st IKEPolicy
ikepolicy_dict = {'id': 'a1f009b7-0ffa-43a7-ba19-dcabb0b4c981',
'tenant_id': '1',
diff --git a/releasenotes/notes/endpoint-group-3bb4083130952d17.yaml b/releasenotes/notes/endpoint-group-3bb4083130952d17.yaml
new file mode 100644
index 0000000..eeecb32
--- /dev/null
+++ b/releasenotes/notes/endpoint-group-3bb4083130952d17.yaml
@@ -0,0 +1,4 @@
+---
+features:
+ - |
+ Add support for Endpoint Group feature.