From 9f9adbb83f5b521b6f14dcf3019e25b216be7d40 Mon Sep 17 00:00:00 2001 From: Cao Xuan Hoang Date: Thu, 13 Jul 2017 13:57:31 +0700 Subject: [PATCH] Add support for Endpoint Group Current VPNaaS is lacking of endpoint group interaction and new way of IPsec Site Connection creating with endpoint group. This patch filed the gap. Co-Authored-By: Akihiro Motoki Change-Id: Ibdeabd667e00617c7c4d3cee611fa75ac2877d9b Closes-Bug: 1704066 --- neutron_vpnaas_dashboard/api/vpn.py | 128 +++++++++-- .../dashboards/project/vpn/forms.py | 84 ++++++-- .../dashboards/project/vpn/tables.py | 94 ++++++++- .../dashboards/project/vpn/tabs.py | 44 +++- .../vpn/_add_endpoint_group_help.html | 3 + .../templates/vpn/_add_vpn_service_help.html | 14 +- .../templates/vpn/_endpointgroup_details.html | 32 +++ .../vpn/_ipsecsiteconnection_details.html | 8 + .../templates/vpn/_update_endpointgroup.html | 7 + .../templates/vpn/_vpnservice_details.html | 8 +- .../templates/vpn/update_endpointgroup.html | 7 + .../dashboards/project/vpn/tests.py | 198 ++++++++++++++++-- .../dashboards/project/vpn/urls.py | 6 + .../dashboards/project/vpn/views.py | 118 +++++++++-- .../dashboards/project/vpn/workflows.py | 159 +++++++++++++- .../test/api_tests/vpnaas_tests.py | 58 +++++ .../test/test_data/vpnaas_data.py | 13 ++ .../endpoint-group-3bb4083130952d17.yaml | 4 + 18 files changed, 906 insertions(+), 79 deletions(-) create mode 100644 neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_add_endpoint_group_help.html create mode 100644 neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_endpointgroup_details.html create mode 100644 neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/_update_endpointgroup.html create mode 100644 neutron_vpnaas_dashboard/dashboards/project/vpn/templates/vpn/update_endpointgroup.html create mode 100644 releasenotes/notes/endpoint-group-3bb4083130952d17.yaml 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.