From a9d92882e619902f15bf64ec87dbabaf60274dc1 Mon Sep 17 00:00:00 2001 From: Cedric Savignan Date: 2016年6月24日 17:07:10 +0200 Subject: [PATCH] Horizon plugin patch to let user handle BGPVPN resources Add a new panel BGPVPN in project section to display BGPVPN created for non admin user. The page allow actions as edit and update associations networks or routers for BGPVPN resources. Some part of code has been refactored to put in common methods or classes between project and admin panels. Change-Id: I49bfdfe1427c67f6b19a7b4faec0ab77a15ce8a5 Implements: blueprint horizon-tenant-workflow --- bgpvpn_dashboard/api/bgpvpn.py | 30 ++- .../dashboards/admin/bgpvpn/forms.py | 136 ++-------- .../dashboards/admin/bgpvpn/panel.py | 3 - .../dashboards/admin/bgpvpn/tables.py | 53 +--- .../templates/bgpvpn/_detail_overview.html | 28 +- .../templates/bgpvpn/update-associations.html | 7 - .../dashboards/admin/bgpvpn/views.py | 93 +------ .../dashboards/admin/bgpvpn/workflows.py | 222 +--------------- .../dashboards/project/__init__.py | 0 .../dashboards/project/bgpvpn/__init__.py | 0 .../dashboards/project/bgpvpn/forms.py | 136 ++++++++++ .../dashboards/project/bgpvpn/panel.py | 23 ++ .../dashboards/project/bgpvpn/tables.py | 102 ++++++++ .../bgpvpn/_associated_networks.html | 15 ++ .../templates/bgpvpn/_associated_routers.html | 15 ++ .../templates/bgpvpn/_detail_overview.html | 14 + .../bgpvpn/templates/bgpvpn/_modify.html | 0 .../bgpvpn/templates/bgpvpn/detail.html | 16 ++ .../bgpvpn/templates/bgpvpn/index.html | 0 .../bgpvpn/templates/bgpvpn/modify.html | 2 +- .../dashboards/project/bgpvpn/urls.py | 32 +++ .../dashboards/project/bgpvpn/views.py | 139 ++++++++++ .../dashboards/project/bgpvpn/workflows.py | 241 ++++++++++++++++++ .../enabled/_1495_project_bgpvpn_panel.py | 10 + devstack/plugin.sh | 5 +- devstack/settings | 2 +- doc/source/horizon.rst | 10 +- .../horizon-support-06a7b21286002949.yaml | 17 ++ 28 files changed, 839 insertions(+), 512 deletions(-) delete mode 100755 bgpvpn_dashboard/dashboards/admin/bgpvpn/templates/bgpvpn/update-associations.html create mode 100755 bgpvpn_dashboard/dashboards/project/__init__.py create mode 100755 bgpvpn_dashboard/dashboards/project/bgpvpn/__init__.py create mode 100755 bgpvpn_dashboard/dashboards/project/bgpvpn/forms.py create mode 100755 bgpvpn_dashboard/dashboards/project/bgpvpn/panel.py create mode 100755 bgpvpn_dashboard/dashboards/project/bgpvpn/tables.py create mode 100755 bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/_associated_networks.html create mode 100755 bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/_associated_routers.html create mode 100755 bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/_detail_overview.html rename bgpvpn_dashboard/dashboards/{admin => project}/bgpvpn/templates/bgpvpn/_modify.html (100%) create mode 100755 bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/detail.html rename bgpvpn_dashboard/dashboards/{admin => project}/bgpvpn/templates/bgpvpn/index.html (100%) rename bgpvpn_dashboard/dashboards/{admin => project}/bgpvpn/templates/bgpvpn/modify.html (71%) create mode 100755 bgpvpn_dashboard/dashboards/project/bgpvpn/urls.py create mode 100755 bgpvpn_dashboard/dashboards/project/bgpvpn/views.py create mode 100755 bgpvpn_dashboard/dashboards/project/bgpvpn/workflows.py create mode 100755 bgpvpn_dashboard/enabled/_1495_project_bgpvpn_panel.py create mode 100755 releasenotes/notes/horizon-support-06a7b21286002949.yaml diff --git a/bgpvpn_dashboard/api/bgpvpn.py b/bgpvpn_dashboard/api/bgpvpn.py index fc1f45c5..d7724b4f 100755 --- a/bgpvpn_dashboard/api/bgpvpn.py +++ b/bgpvpn_dashboard/api/bgpvpn.py @@ -41,21 +41,21 @@ def bgpvpns_list(request, **kwargs): def bgpvpn_get(request, bgpvpn_id, **kwargs): - LOG.debug("bgpvpn_get(): bgpvpnid=%s, kwargs=%s" % (bgpvpn_id, kwargs)) + LOG.debug("bgpvpn_get(): bgpvpnid=%s, kwargs=%s", bgpvpn_id, kwargs) bgpvpn = neutronclient(request).show_bgpvpn(bgpvpn_id, **kwargs).get('bgpvpn') return Bgpvpn(bgpvpn) def bgpvpn_create(request, **kwargs): - LOG.debug("bgpvpn_create(): params=%s" % kwargs) + LOG.debug("bgpvpn_create(): params=%s", kwargs) body = {'bgpvpn': kwargs} bgpvpn = neutronclient(request).create_bgpvpn(body=body) return Bgpvpn(bgpvpn) def bgpvpn_update(request, bgpvpn_id, **kwargs): - LOG.debug("bgpvpn_update(): bgpvpnid=%s, kwargs=%s" % (bgpvpn_id, kwargs)) + LOG.debug("bgpvpn_update(): bgpvpnid=%s, kwargs=%s", bgpvpn_id, kwargs) body = {'bgpvpn': kwargs} bgpvpn = neutronclient(request).update_bgpvpn(bgpvpn_id, body=body).get('bgpvpn') @@ -63,12 +63,13 @@ def bgpvpn_update(request, bgpvpn_id, **kwargs): def bgpvpn_delete(request, bgpvpn_id): - LOG.debug("bgpvpn_delete(): bgpvpnid=%s" % bgpvpn_id) + LOG.debug("bgpvpn_delete(): bgpvpnid=%s", bgpvpn_id) neutronclient(request).delete_bgpvpn(bgpvpn_id) def network_association_list(request, bgpvpn_id, **kwargs): - LOG.debug("network_association_list(): params=%s", kwargs) + LOG.debug("network_association_list(): bgpvpn_id=%s, kwargs=%s", + bgpvpn_id, kwargs) network_associations = neutronclient( request).list_network_associations( bgpvpn_id, @@ -77,8 +78,8 @@ def network_association_list(request, bgpvpn_id, **kwargs): def network_association_create(request, bgpvpn_id, **kwargs): - LOG.debug("network_association_create(): bgpvpnid=%s params=%s" % - (bgpvpn_id, kwargs)) + LOG.debug("network_association_create(): bgpvpnid=%s kwargs=%s", + bgpvpn_id, kwargs) body = {'network_association': kwargs} network_association = neutronclient( request).create_network_association(bgpvpn_id, body=body) @@ -86,13 +87,14 @@ def network_association_create(request, bgpvpn_id, **kwargs): def network_association_delete(request, resource_id, bgpvpn_id): - LOG.debug("router_association_delete(): resource_id=%s bgpvpnid=%s" % - (resource_id, bgpvpn_id)) + LOG.debug("router_association_delete(): resource_id=%s bgpvpnid=%s", + resource_id, bgpvpn_id) neutronclient(request).delete_network_association(resource_id, bgpvpn_id) def router_association_list(request, bgpvpn_id, **kwargs): - LOG.debug("router_association_list(): params=%s", kwargs) + LOG.debug("router_association_list(): bgpvpn_id=%s, kwargs=%s", + bgpvpn_id, kwargs) router_associations = neutronclient( request).list_router_associations(bgpvpn_id, **kwargs).get('router_associations') @@ -100,8 +102,8 @@ def router_association_list(request, bgpvpn_id, **kwargs): def router_association_create(request, bgpvpn_id, **kwargs): - LOG.debug("router_association_create(): bgpvpnid=%s params=%s" % - (bgpvpn_id, kwargs)) + LOG.debug("router_association_create(): bgpvpnid=%s params=%s", + bgpvpn_id, kwargs) body = {'router_association': kwargs} router_associations = neutronclient( request).create_router_association(bgpvpn_id, body=body) @@ -109,6 +111,6 @@ def router_association_create(request, bgpvpn_id, **kwargs): def router_association_delete(request, resource_id, bgpvpn_id): - LOG.debug("router_association_delete(): resource_id=%s bgpvpnid=%s" % - (resource_id, bgpvpn_id)) + LOG.debug("router_association_delete(): resource_id=%s bgpvpnid=%s", + resource_id, bgpvpn_id) neutronclient(request).delete_router_association(resource_id, bgpvpn_id) diff --git a/bgpvpn_dashboard/dashboards/admin/bgpvpn/forms.py b/bgpvpn_dashboard/dashboards/admin/bgpvpn/forms.py index 7d106993..32bb4e71 100755 --- a/bgpvpn_dashboard/dashboards/admin/bgpvpn/forms.py +++ b/bgpvpn_dashboard/dashboards/admin/bgpvpn/forms.py @@ -13,21 +13,20 @@ # License for the specific language governing permissions and limitations # under the License. -import collections import logging -from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse_lazy from django.core.validators import RegexValidator from django.utils.translation import ugettext_lazy as _ -from horizon import exceptions from horizon import forms -from horizon import messages from openstack_dashboard import api -from bgpvpn_dashboard.api import bgpvpn as bgpvpn_api from bgpvpn_dashboard.common import bgpvpn as bgpvpn_common from networking_bgpvpn.neutron.services.common import constants +from bgpvpn_dashboard.dashboards.project.bgpvpn import forms \ + as project_forms + LOG = logging.getLogger(__name__) @@ -35,11 +34,7 @@ RT_REGEX = constants.RT_REGEX[1:-1] RTS_REGEX = '^%s( *, *%s)*$' % (RT_REGEX, RT_REGEX) -class BgpvpnAttributes(forms.SelfHandlingForm): - name = forms.CharField(max_length=255, - label=_("Name"), - required=False) - +class CommonData(project_forms.CommonData): route_targets = forms.CharField( max_length=255, validators=[RegexValidator(regex=RTS_REGEX, @@ -64,41 +59,13 @@ class BgpvpnAttributes(forms.SelfHandlingForm): required=False, help_text=bgpvpn_common.ROUTE_TARGET_HELP + ' To use only on export.') - def clean(self): - cleaned_data = super(BgpvpnAttributes, self).clean() - # Route targets can be empty - if not cleaned_data.get('route_targets'): - cleaned_data['route_targets'] = None - if not cleaned_data.get('import_targets'): - cleaned_data['import_targets'] = None - if not cleaned_data.get('export_targets'): - cleaned_data['export_targets'] = None - return cleaned_data + failure_url = reverse_lazy('horizon:admin:bgpvpn:index') - @staticmethod - def handle_data(data, action): - params = {} - for key in bgpvpn_common.RT_FORMAT_ATTRIBUTES: - params[key] = bgpvpn_common.format_rt(data.pop(key)) - params.update(data) - if action == 'update': - del params['bgpvpn_id'] - del params['type'] - del params['tenant_id'] - return params - - @staticmethod - def order_fields(fields, fields_order): - if 'keyOrder' in fields: - fields.keyOrder = fields_order - else: - fields = collections.OrderedDict( - (k, fields[k]) for k in fields_order) - return fields + def __init__(self, request, *args, **kwargs): + super(CommonData, self).__init__(request, *args, **kwargs) -class CreateBgpVpn(BgpvpnAttributes): - +class CreateBgpVpn(CommonData): tenant_id = forms.ChoiceField(label=_("Project")) type = forms.ChoiceField(choices=[("l3", _('l3')), @@ -107,97 +74,30 @@ class CreateBgpVpn(BgpvpnAttributes): help_text=_("The type of VPN " " and the technology behind it.")) + fields_order = ['name', 'tenant_id', 'type', + 'route_targets', 'import_targets', 'export_targets'] + def __init__(self, request, *args, **kwargs): super(CreateBgpVpn, self).__init__(request, *args, **kwargs) - fields_order = ['name', 'tenant_id', 'type', - 'route_targets', 'import_targets', 'export_targets'] - self.fields = self.order_fields(self.fields, fields_order) - tenant_choices = [('', _("Select a project"))] tenants, has_more = api.keystone.tenant_list(request) for tenant in tenants: if tenant.enabled: tenant_choices.append((tenant.id, tenant.name)) self.fields['tenant_id'].choices = tenant_choices - - def clean(self): - cleaned_data = super(CreateBgpVpn, self).clean() - name = cleaned_data.get('name') - tenant_id = cleaned_data.get('tenant_id') - try: - bgpvpns = bgpvpn_api.bgpvpns_list(self.request, - name=name, - tenant_id=tenant_id) - except Exception: - msg = _('Unable to get BGPVPN with name %s') % name - exceptions.check_message(["Connection", "refused"], msg) - raise - if bgpvpns: - raise forms.ValidationError( - _('The name "%s" is already used by another BGPVPN.') % name) - return cleaned_data - - def handle(self, request, data): - try: - params = self.handle_data(data, 'create') - bgpvpn = bgpvpn_api.bgpvpn_create(request, **params) - msg = _('BGPVPN %s was successfully created.') % data['name'] - LOG.debug(msg) - messages.success(request, msg) - return bgpvpn - except Exception: - redirect = reverse('horizon:admin:bgpvpn:index') - msg = _('Failed to create the BGPVPN %s') % data['name'] - exceptions.handle(request, msg, redirect=redirect) + self.action = 'create' -class EditDataBgpVpn(BgpvpnAttributes): - +class EditDataBgpVpn(CommonData): bgpvpn_id = forms.CharField(label=_("ID"), widget=forms.TextInput( attrs={'readonly': 'readonly'})) type = forms.CharField(label=_("Type"), widget=forms.TextInput( attrs={'readonly': 'readonly'})) tenant_id = forms.CharField(widget=forms.HiddenInput()) + fields_order = ['name', 'bgpvpn_id', 'tenant_id', 'type', + 'route_targets', 'import_targets', 'export_targets'] + def __init__(self, request, *args, **kwargs): super(EditDataBgpVpn, self).__init__(request, *args, **kwargs) - fields_order = ['name', 'bgpvpn_id', 'tenant_id', 'type', - 'route_targets', 'import_targets', 'export_targets'] - self.fields = self.order_fields(self.fields, fields_order) - - def clean(self): - cleaned_data = super(EditDataBgpVpn, self).clean() - name = cleaned_data.get('name') - bgpvpn_id = cleaned_data.get('bgpvpn_id') - tenant_id = cleaned_data.get('tenant_id') - try: - bgpvpns = bgpvpn_api.bgpvpns_list(self.request, - name=name, - tenant_id=tenant_id) - except Exception: - msg = _('Unable to get BGPVPN with name %s') % name - exceptions.check_message(["Connection", "refused"], msg) - raise - if bgpvpns: - for bgpvpn in bgpvpns: - if bgpvpn.id != bgpvpn_id: - raise forms.ValidationError( - _('The name "%s" is already used by another BGPVPN.') % - name) - return cleaned_data - - def handle(self, request, data): - try: - params = self.handle_data(data, 'update') - bgpvpn = bgpvpn_api.bgpvpn_update(request, - data['bgpvpn_id'], - **params) - msg = _('BGPVPN %s was successfully updated.') % data['name'] - LOG.debug(msg) - messages.success(request, msg) - except Exception: - redirect = reverse('horizon:admin:bgpvpn:index') - msg = _('Failed to modify BGPVPN %s') % data['name'] - exceptions.handle(request, msg, redirect=redirect) - return False - return bgpvpn + self.action = 'update' diff --git a/bgpvpn_dashboard/dashboards/admin/bgpvpn/panel.py b/bgpvpn_dashboard/dashboards/admin/bgpvpn/panel.py index e54421ee..77c4b810 100755 --- a/bgpvpn_dashboard/dashboards/admin/bgpvpn/panel.py +++ b/bgpvpn_dashboard/dashboards/admin/bgpvpn/panel.py @@ -16,11 +16,8 @@ from django.utils.translation import ugettext_lazy as _ import horizon -from openstack_dashboard.dashboards.admin import dashboard class BGPVPNInterconnections(horizon.Panel): name = _("BGPVPN Interconnections") slug = "bgpvpn" - -dashboard.Admin.register(BGPVPNInterconnections) diff --git a/bgpvpn_dashboard/dashboards/admin/bgpvpn/tables.py b/bgpvpn_dashboard/dashboards/admin/bgpvpn/tables.py index a6766823..a32e7b7c 100755 --- a/bgpvpn_dashboard/dashboards/admin/bgpvpn/tables.py +++ b/bgpvpn_dashboard/dashboards/admin/bgpvpn/tables.py @@ -17,8 +17,6 @@ import logging from django.core.urlresolvers import reverse from django.utils import html -from django.utils.http import urlencode -from django.utils import safestring from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext_lazy from horizon import exceptions @@ -26,6 +24,7 @@ from horizon import tables from openstack_dashboard import policy from bgpvpn_dashboard.api import bgpvpn as bgpvpn_api +from bgpvpn_dashboard.dashboards.project.bgpvpn import tables as project_tables LOG = logging.getLogger(__name__) @@ -61,40 +60,16 @@ class CreateBgpVpn(tables.LinkAction): icon = "plus" -class EditInfoBgpVpn(tables.LinkAction): - name = "update_info" - verbose_name = _("Edit BGPVPN") +class EditInfoBgpVpn(project_tables.EditInfoBgpVpn): url = "horizon:admin:bgpvpn:edit" - classes = ("ajax-modal",) - icon = "pencil" -class UpdateNetworkAssociations(tables.LinkAction): - name = "update_network_associations" - verbose_name = _("Update Network Associations") +class UpdateNetworkAssociations(project_tables.UpdateNetworkAssociations): url = "horizon:admin:bgpvpn:update-associations" - classes = ("ajax-modal",) - icon = "pencil" - - def get_link_url(self, bgpvpn): - step = 'update_bgpvpn_network' - base_url = reverse(self.url, args=[bgpvpn.id]) - param = urlencode({"step": step}) - return "?".join([base_url, param]) -class UpdateRouterAssociations(tables.LinkAction): - name = "update_router_associations" - verbose_name = _("Update Router Associations") +class UpdateRouterAssociations(project_tables.UpdateRouterAssociations): url = "horizon:admin:bgpvpn:update-associations" - classes = ("ajax-modal",) - icon = "pencil" - - def get_link_url(self, bgpvpn): - step = 'update_bgpvpn_router' - base_url = reverse(self.url, args=[bgpvpn.id]) - param = urlencode({"step": step}) - return "?".join([base_url, param]) def get_route_targets(bgpvpn): @@ -125,18 +100,6 @@ def get_tenant(bgpvpn): return bgpvpn.tenant.name -class NetworksColumn(tables.Column): - def get_raw_data(self, bgpvpn): - networks = [get_network_url(network) for network in bgpvpn.networks] - return safestring.mark_safe(', '.join(networks)) - - -class RoutersColumn(tables.Column): - def get_raw_data(self, bgpvpn): - routers = [get_router_url(router) for router in bgpvpn.routers] - return safestring.mark_safe(', '.join(routers)) - - class BgpvpnTable(tables.DataTable): tenant_id = tables.Column(get_tenant, verbose_name=_("Project")) name = tables.Column("name_or_id", @@ -149,12 +112,12 @@ class BgpvpnTable(tables.DataTable): verbose_name=_("Import Targets")) export_targets = tables.Column(get_export_targets, verbose_name=_("Export Targets")) - networks = NetworksColumn("networks", verbose_name=_("Networks")) - routers = RoutersColumn("routers", verbose_name=_("Routers")) + networks = project_tables.NetworksColumn("networks", + verbose_name=_("Networks")) + routers = project_tables.RoutersColumn("routers", + verbose_name=_("Routers")) class Meta(object): - name = "bgpvpns" - verbose_name = _("BGPVPN") table_actions = (CreateBgpVpn, DeleteBgpvpn) row_actions = (EditInfoBgpVpn, UpdateNetworkAssociations, diff --git a/bgpvpn_dashboard/dashboards/admin/bgpvpn/templates/bgpvpn/_detail_overview.html b/bgpvpn_dashboard/dashboards/admin/bgpvpn/templates/bgpvpn/_detail_overview.html index 458c0898..d1dfa138 100755 --- a/bgpvpn_dashboard/dashboards/admin/bgpvpn/templates/bgpvpn/_detail_overview.html +++ b/bgpvpn_dashboard/dashboards/admin/bgpvpn/templates/bgpvpn/_detail_overview.html @@ -28,31 +28,7 @@
{% trans "Project ID" %}
{{ bgpvpn.tenant_id }}
-
- {% if bgpvpn.networks|length> 1 %} - {% trans "Associated Networks" %} - {% else %} - {% trans "Associated Network" %} - {% endif %} -
- {% for network in bgpvpn.networks %} -
- {% url 'horizon:admin:networks:detail' network as network_url %} - {{ network }} -
- {% endfor %} -
- {% if bgpvpn.routers|length> 1 %} - {% trans "Associated Routers" %} - {% else %} - {% trans "Associated Router" %} - {% endif %} -
- {% for router in bgpvpn.routers %} -
- {% url 'horizon:admin:routers:detail' router as router_url %} - {{ router }} -
- {% endfor %} + {% include "project/bgpvpn/_associated_networks.html" %} + {% include "project/bgpvpn/_associated_routers.html" %} diff --git a/bgpvpn_dashboard/dashboards/admin/bgpvpn/templates/bgpvpn/update-associations.html b/bgpvpn_dashboard/dashboards/admin/bgpvpn/templates/bgpvpn/update-associations.html deleted file mode 100755 index a079cabd..00000000 --- a/bgpvpn_dashboard/dashboards/admin/bgpvpn/templates/bgpvpn/update-associations.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} -{% block title %}{% trans "Edit BGPVPN associations" %}{% endblock %} - -{% block main %} - {% include 'horizon/common/_workflow.html' %} -{% endblock %} \ No newline at end of file diff --git a/bgpvpn_dashboard/dashboards/admin/bgpvpn/views.py b/bgpvpn_dashboard/dashboards/admin/bgpvpn/views.py index b40551ac..adf9902c 100755 --- a/bgpvpn_dashboard/dashboards/admin/bgpvpn/views.py +++ b/bgpvpn_dashboard/dashboards/admin/bgpvpn/views.py @@ -13,30 +13,24 @@ # License for the specific language governing permissions and limitations # under the License. -from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse_lazy from django.utils.translation import ugettext_lazy as _ -from horizon import exceptions from horizon import forms -from horizon import tables from horizon.utils import memoized -from horizon import views -from horizon import workflows from openstack_dashboard import api import bgpvpn_dashboard.api.bgpvpn as bgpvpn_api -from bgpvpn_dashboard.common import bgpvpn as bgpvpn_common from bgpvpn_dashboard.dashboards.admin.bgpvpn import forms as bgpvpn_forms from bgpvpn_dashboard.dashboards.admin.bgpvpn import tables as bgpvpn_tables from bgpvpn_dashboard.dashboards.admin.bgpvpn import workflows \ as bgpvpn_workflows +from bgpvpn_dashboard.dashboards.project.bgpvpn import views as project_views -class IndexView(tables.DataTableView): +class IndexView(project_views.IndexView): table_class = bgpvpn_tables.BgpvpnTable - template_name = 'admin/bgpvpn/index.html' - page_title = _("BGP VPNs") + @memoized.memoized_method def get_data(self): bgpvpns = bgpvpn_api.bgpvpns_list(self.request) networks = api.neutron.network_list(self.request) @@ -61,89 +55,18 @@ class CreateView(forms.ModalFormView): page_title = _("Create BGPVPN") -class EditDataView(forms.ModalFormView): +class EditDataView(project_views.EditDataView): form_class = bgpvpn_forms.EditDataBgpVpn - form_id = "edit_data_bgpvpn_form" - modal_header = _("Edit BGPVPN") - submit_label = _("Save Changes") submit_url = 'horizon:admin:bgpvpn:edit' success_url = reverse_lazy('horizon:admin:bgpvpn:index') - template_name = 'admin/bgpvpn/modify.html' - page_title = _("Edit BGPVPN") - - @staticmethod - def _join_rts(route_targets_list): - return ','.join(route_targets_list) - - def get_context_data(self, **kwargs): - context = super(EditDataView, self).get_context_data(**kwargs) - args = (self.kwargs['bgpvpn_id'],) - context["bgpvpn_id"] = self.kwargs['bgpvpn_id'] - context["submit_url"] = reverse(self.submit_url, args=args) - return context - - def get_initial(self): - bgpvpn_id = self.kwargs['bgpvpn_id'] - try: - # Get initial bgpvpn information - bgpvpn = bgpvpn_api.bgpvpn_get(self.request, bgpvpn_id) - except Exception: - exceptions.handle( - self.request, - _('Unable to retrieve BGPVPN details.'), - redirect=reverse_lazy("horizon:project:bgpvpn:index")) - data = bgpvpn.to_dict() - for attribute in bgpvpn_common.RT_FORMAT_ATTRIBUTES: - data[attribute] = self._join_rts(bgpvpn[attribute]) - data['bgpvpn_id'] = data.pop('id') - return data -class UpdateAssociationsView(workflows.WorkflowView): +class UpdateAssociationsView(project_views.UpdateAssociationsView): workflow_class = bgpvpn_workflows.UpdateBgpVpnAssociations - template_name = 'admin/bgpvpn/update-associations.html' page_title = _("Edit BGPVPN associations") - - def get_initial(self): - bgpvpn_id = self.kwargs['bgpvpn_id'] - - try: - # Get initial bgpvpn information - bgpvpn = bgpvpn_api.bgpvpn_get(self.request, bgpvpn_id) - except Exception: - exceptions.handle( - self.request, - _('Unable to retrieve BGPVPN details.'), - redirect=reverse_lazy("horizon:admin:bgpvpn:index")) - data = bgpvpn.to_dict() - data['bgpvpn_id'] = data.pop('id') - return data + failure_url = reverse_lazy("horizon:admin:bgpvpn:index") -class DetailProjectView(views.HorizonTemplateView): +class DetailProjectView(project_views.DetailProjectView): template_name = 'admin/bgpvpn/detail.html' - page_title = "{{ bgpvpn.name }}" - - def get_context_data(self, **kwargs): - context = super(DetailProjectView, self).get_context_data(**kwargs) - bgpvpn = self.get_data() - table = bgpvpn_tables.BgpvpnTable(self.request) - context["bgpvpn"] = bgpvpn - context["url"] = self.get_redirect_url() - context["actions"] = table.render_row_actions(bgpvpn) - return context - - @memoized.memoized_method - def get_data(self): - try: - bgpvpn_id = self.kwargs['bgpvpn_id'] - bgpvpn = bgpvpn_api.bgpvpn_get(self.request, bgpvpn_id) - except Exception: - redirect = self.get_redirect_url() - exceptions.handle(self.request, - _('Unable to retrieve BGPVPN details.'), - redirect=redirect) - return bgpvpn - - def get_redirect_url(self): - return reverse('horizon:admin:bgpvpn:index') + redirect_url = 'horizon:admin:bgpvpn:index' diff --git a/bgpvpn_dashboard/dashboards/admin/bgpvpn/workflows.py b/bgpvpn_dashboard/dashboards/admin/bgpvpn/workflows.py index 6dd72287..8f944a20 100755 --- a/bgpvpn_dashboard/dashboards/admin/bgpvpn/workflows.py +++ b/bgpvpn_dashboard/dashboards/admin/bgpvpn/workflows.py @@ -13,223 +13,29 @@ # License for the specific language governing permissions and limitations # under the License. -from django.utils.translation import ugettext_lazy as _ -from horizon import exceptions -from horizon import forms -from horizon import workflows -from openstack_dashboard import api - -from bgpvpn_dashboard.api import bgpvpn as bgpvpn_api +from bgpvpn_dashboard.dashboards.project.bgpvpn import workflows \ + as project_workflows -class UpdateAssociations(workflows.MembershipAction): - def __init__(self, request, resource_type, *args, **kwargs): - super(UpdateAssociations, self).__init__(request, - *args, - **kwargs) - err_msg = _('Unable to retrieve %ss list. ' - 'Please try again later.') % resource_type - context = args[0] - - default_role_field_name = self.get_default_role_field_name() - self.fields[default_role_field_name] = forms.CharField(required=False) - self.fields[default_role_field_name].initial = 'member' - - field_name = self.get_member_field_name('member') - self.fields[field_name] = forms.MultipleChoiceField(required=False) - - # Get list of available resources. - all_resources = [] - try: - if resource_type == 'router': - all_resources = api.neutron.router_list( - request, - tenant_id=context.get('tenant_id')) - elif resource_type == 'network': - all_resources = api.neutron.network_list_for_tenant( - request, - context.get('tenant_id')) - else: - raise Exception( - 'Resource type %s is not supported' % resource_type) - except Exception: - exceptions.handle(request, err_msg) - - resources_list = [(resource.id, resource.name) - for resource in all_resources] - - self.fields[field_name].choices = resources_list - - bgpvpn_id = context.get('bgpvpn_id') - - try: - if bgpvpn_id: - associations = [] - list_method = getattr(bgpvpn_api, '%s_association_list' % - resource_type) - associations = [ - getattr(association, '%s_id' % - resource_type) for association in - list_method(request, bgpvpn_id) - ] - - except Exception: - exceptions.handle(request, err_msg) - - self.fields[field_name].initial = associations - - -class UpdateBgpVpnRoutersAction(UpdateAssociations): - def __init__(self, request, *args, **kwargs): - super(UpdateBgpVpnRoutersAction, self).__init__(request, - 'router', - *args, - **kwargs) - - class Meta(object): - name = _("Router Associations") - slug = "update_bgpvpn_router" - - -class UpdateBgpVpnRouters(workflows.UpdateMembersStep): - action_class = UpdateBgpVpnRoutersAction - help_text = _("Select the routers to be associated.") - available_list_title = _("All Routers") - members_list_title = _("Selected Routers") - no_available_text = _("No router found.") - no_members_text = _("No router selected.") - show_roles = False +class UpdateBgpVpnRouters(project_workflows.UpdateBgpVpnRouters): + action_class = project_workflows.UpdateBgpVpnRoutersAction depends_on = ("bgpvpn_id", "tenant_id", "name") - contributes = ("routers_association",) - - def contribute(self, data, context): - if data: - member_field_name = self.get_member_field_name('member') - context['routers_association'] = data.get(member_field_name, []) - return context -class UpdateBgpVpnNetworksAction(UpdateAssociations): - def __init__(self, request, *args, **kwargs): - super(UpdateBgpVpnNetworksAction, self).__init__(request, - 'network', - *args, - **kwargs) - - class Meta(object): - name = _("Network Associations") - slug = "update_bgpvpn_network" - - -class UpdateBgpVpnNetworks(workflows.UpdateMembersStep): - action_class = UpdateBgpVpnNetworksAction - help_text = _("Select the networks to be associated.") - available_list_title = _("All Networks") - members_list_title = _("Selected Networks") - no_available_text = _("No network found.") - no_members_text = _("No network selected.") - show_roles = False +class UpdateBgpVpnNetworks(project_workflows.UpdateBgpVpnNetworks): + action_class = project_workflows.UpdateBgpVpnNetworksAction depends_on = ("bgpvpn_id", "tenant_id", "name") - contributes = ("networks_association",) - - def contribute(self, data, context): - if data: - member_field_name = self.get_member_field_name('member') - context['networks_association'] = data.get(member_field_name, []) - return context -class UpdateBgpVpnAssociations(workflows.Workflow): - slug = "update_bgpvpn_associations" - name = _("Edit BGPVPN associations") - finalize_button_name = _("Save") - success_message = _('Modified BGPVPN associations "%s".') - failure_message = _('Unable to modify BGPVPN associations "%s".') +class UpdateBgpVpnAssociations(project_workflows.UpdateBgpVpnAssociations): success_url = "horizon:admin:bgpvpn:index" default_steps = (UpdateBgpVpnNetworks, UpdateBgpVpnRouters) - def format_status_message(self, message): - return message % self.context['name'] - - @staticmethod - def _handle_type(request, data, association_type): - tenant_id = data["tenant_id"] - list_method = getattr(bgpvpn_api, - '%s_association_list' % association_type) - associations = data["%ss_association" % association_type] - try: - old_associations = [ - getattr(association, - '%s_id' % association_type) for association in - list_method(request, data['bgpvpn_id'])] - except Exception: - exceptions.handle(request, - _('Unable to retrieve %ss associations') % - association_type) - raise - - to_remove_associations = list(set(old_associations) - - set(associations)) - to_add_associations = list(set(associations) - - set(old_associations)) - - # If new resource added to the list - if len(to_add_associations)> 0: - for resource in to_add_associations: - try: - create_asso = getattr(bgpvpn_api, - '%s_association_create' % - association_type) - params = dict() - params['%s_id' % association_type] = resource - params['tenant_id'] = tenant_id - create_asso(request, - data['bgpvpn_id'], - **params) - except Exception as e: - exceptions.handle( - request, - _('Unable to associate {} {} {}').format( - association_type, - resource, str(e))) - raise - - # If resource has been deleted from the list - if len(to_remove_associations)> 0: - for resource in to_remove_associations: - try: - list_method = getattr(bgpvpn_api, - '%s_association_list' % - association_type) - asso_list = list_method(request, data['bgpvpn_id']) - for association in asso_list: - if getattr(association, - '%s_id' % association_type) == resource: - delete_method = getattr(bgpvpn_api, - '%s_association_delete' % - association_type) - delete_method(request, - association.id, data['bgpvpn_id']) - except Exception: - exceptions.handle( - request, - _('Unable to disassociate {}s {}').format( - association_type, - resource)) - raise - - def handle(self, request, data): - action = False - try: - if 'networks_association' in data: - self._handle_type(request, data, 'network') - action = True - if 'routers_association' in data: - self._handle_type(request, data, 'router') - action = True - if not action: - raise Exception('Associations type is not supported') - except Exception: - return False - return True + def _set_params(self, data, association_type, resource): + params = super( + UpdateBgpVpnAssociations, self)._set_params(data, + association_type, + resource) + params['tenant_id'] = data['tenant_id'] + return params diff --git a/bgpvpn_dashboard/dashboards/project/__init__.py b/bgpvpn_dashboard/dashboards/project/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/bgpvpn_dashboard/dashboards/project/bgpvpn/__init__.py b/bgpvpn_dashboard/dashboards/project/bgpvpn/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/bgpvpn_dashboard/dashboards/project/bgpvpn/forms.py b/bgpvpn_dashboard/dashboards/project/bgpvpn/forms.py new file mode 100755 index 00000000..48a5337b --- /dev/null +++ b/bgpvpn_dashboard/dashboards/project/bgpvpn/forms.py @@ -0,0 +1,136 @@ +# Copyright (c) 2016 Orange. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +import collections +import logging + +from django.core.urlresolvers import reverse_lazy +from django.utils.translation import ugettext_lazy as _ +from horizon import exceptions +from horizon import forms +from horizon import messages + +from bgpvpn_dashboard.api import bgpvpn as bgpvpn_api +from bgpvpn_dashboard.common import bgpvpn as bgpvpn_common + +LOG = logging.getLogger(__name__) + + +class CommonData(forms.SelfHandlingForm): + fields_order = [] + name = forms.CharField(max_length=255, + label=_("Name"), + required=False) + + failure_url = reverse_lazy('horizon:project:bgpvpn:index') + + def __init__(self, request, *args, **kwargs): + super(CommonData, self).__init__(request, *args, **kwargs) + if 'keyOrder' in self.fields: + self.fields.keyOrder = self.fields_order + else: + self.fields = collections.OrderedDict( + (k, self.fields[k]) for k in self.fields_order) + + def clean(self): + cleaned_data = super(CommonData, self).clean() + name = cleaned_data.get('name') + bgpvpn_id = cleaned_data.get('bgpvpn_id') + try: + if self.request.user.is_superuser: + for attribute in bgpvpn_common.RT_FORMAT_ATTRIBUTES: + if not cleaned_data.get(attribute): + cleaned_data[attribute] = None + # if an admin user use the bgpvpn panel project + # tenant_id field doesn't exist + if not cleaned_data.get('tenant_id'): + tenant_id = self.request.user.tenant_id + else: + tenant_id = cleaned_data.get('tenant_id') + bgpvpns = bgpvpn_api.bgpvpns_list(self.request, + name=name, + tenant_id=tenant_id) + else: + bgpvpns = bgpvpn_api.bgpvpns_list(self.request, + name=name) + except Exception: + msg = _('Unable to get BGPVPN with name %s') % name + exceptions.check_message(["Connection", "refused"], msg) + raise + if bgpvpns: + if self.action == 'update': + for bgpvpn in bgpvpns: + if bgpvpn.id != bgpvpn_id: + raise forms.ValidationError( + _('The name "%s" is already ' + 'used by another BGPVPN.') % name) + else: + raise forms.ValidationError( + _('The name "%s" is already ' + 'used by another BGPVPN.') % name) + return cleaned_data + + @staticmethod + def _del_attributes(attributes, data): + for attribute in attributes: + del data[attribute] + + def handle(self, request, data): + params = {} + if request.user.is_superuser: + for key in bgpvpn_common.RT_FORMAT_ATTRIBUTES: + params[key] = bgpvpn_common.format_rt(data.pop(key)) + params.update(data) + try: + if self.action == 'update': + if request.user.is_superuser and data.get('tenant_id'): + attributes = ('bgpvpn_id', 'type', 'tenant_id') + else: + attributes = ('bgpvpn_id', 'type') + self._del_attributes(attributes, params) + bgpvpn = bgpvpn_api.bgpvpn_update(request, + data['bgpvpn_id'], + **params) + success_action = 'modified' + elif self.action == 'create': + success_action = 'created' + bgpvpn = bgpvpn_api.bgpvpn_create(request, **params) + else: + raise Exception( + 'Action type %s is not supported' % self.action) + msg = _('BGPVPN {name} was successfully {action}.').format( + name=data['name'], + action=success_action) + LOG.debug(msg) + messages.success(request, msg) + return bgpvpn + except Exception: + msg = _('Failed to {action} BGPVPN {name}').format( + action=self.action, + name=data['name']) + exceptions.handle(request, msg, redirect=self.failure_url) + return False + + +class EditDataBgpVpn(CommonData): + bgpvpn_id = forms.CharField(label=_("ID"), widget=forms.TextInput( + attrs={'readonly': 'readonly'})) + type = forms.CharField(label=_("Type"), widget=forms.TextInput( + attrs={'readonly': 'readonly'})) + fields_order = ['name', 'bgpvpn_id', 'type'] + + def __init__(self, request, *args, **kwargs): + super(EditDataBgpVpn, self).__init__(request, *args, **kwargs) + self.action = 'update' diff --git a/bgpvpn_dashboard/dashboards/project/bgpvpn/panel.py b/bgpvpn_dashboard/dashboards/project/bgpvpn/panel.py new file mode 100755 index 00000000..77c4b810 --- /dev/null +++ b/bgpvpn_dashboard/dashboards/project/bgpvpn/panel.py @@ -0,0 +1,23 @@ +# Copyright (c) 2016 Orange. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.utils.translation import ugettext_lazy as _ + +import horizon + + +class BGPVPNInterconnections(horizon.Panel): + name = _("BGPVPN Interconnections") + slug = "bgpvpn" diff --git a/bgpvpn_dashboard/dashboards/project/bgpvpn/tables.py b/bgpvpn_dashboard/dashboards/project/bgpvpn/tables.py new file mode 100755 index 00000000..c96af856 --- /dev/null +++ b/bgpvpn_dashboard/dashboards/project/bgpvpn/tables.py @@ -0,0 +1,102 @@ +# Copyright (c) 2016 Orange. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django.core.urlresolvers import reverse +from django.utils import html +from django.utils.http import urlencode +from django.utils import safestring +from django.utils.translation import ugettext_lazy as _ +from horizon import tables + + +LOG = logging.getLogger(__name__) + + +class EditInfoBgpVpn(tables.LinkAction): + name = "update_info" + verbose_name = _("Edit BGPVPN") + url = "horizon:project:bgpvpn:edit" + classes = ("ajax-modal",) + icon = "pencil" + + +class UpdateNetworkAssociations(tables.LinkAction): + name = "update_network_associations" + verbose_name = _("Update Network Associations") + url = "horizon:project:bgpvpn:update-associations" + classes = ("ajax-modal",) + icon = "pencil" + + def get_link_url(self, bgpvpn): + step = 'update_bgpvpn_network' + base_url = reverse(self.url, args=[bgpvpn.id]) + param = urlencode({"step": step}) + return "?".join([base_url, param]) + + +class UpdateRouterAssociations(tables.LinkAction): + name = "update_router_associations" + verbose_name = _("Update Router Associations") + url = "horizon:project:bgpvpn:update-associations" + classes = ("ajax-modal",) + icon = "pencil" + + def get_link_url(self, bgpvpn): + step = 'update_bgpvpn_router' + base_url = reverse(self.url, args=[bgpvpn.id]) + param = urlencode({"step": step}) + return "?".join([base_url, param]) + + +def get_network_url(network): + url = reverse('horizon:project:networks:detail', args=[network.id]) + instance = '%s' % (url, html.escape(network.name_or_id)) + return instance + + +def get_router_url(router): + url = reverse('horizon:project:routers:detail', args=[router.id]) + instance = '%s' % (url, html.escape(router.name_or_id)) + return instance + + +class NetworksColumn(tables.Column): + def get_raw_data(self, bgpvpn): + networks = [get_network_url(network) for network in bgpvpn.networks] + return safestring.mark_safe(', '.join(networks)) + + +class RoutersColumn(tables.Column): + def get_raw_data(self, bgpvpn): + routers = [get_router_url(router) for router in bgpvpn.routers] + return safestring.mark_safe(', '.join(routers)) + + +class BgpvpnTable(tables.DataTable): + name = tables.Column("name_or_id", + verbose_name=_("Name"), + link=("horizon:project:bgpvpn:detail")) + type = tables.Column("type", verbose_name=_("Type")) + networks = NetworksColumn("networks", verbose_name=_("Networks")) + routers = RoutersColumn("routers", verbose_name=_("Routers")) + + class Meta(object): + name = "bgpvpns" + verbose_name = _("BGPVPN") + row_actions = (EditInfoBgpVpn, + UpdateNetworkAssociations, + UpdateRouterAssociations) diff --git a/bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/_associated_networks.html b/bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/_associated_networks.html new file mode 100755 index 00000000..2e0ca988 --- /dev/null +++ b/bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/_associated_networks.html @@ -0,0 +1,15 @@ +{% load i18n %} + +
+ {% if bgpvpn.networks|length> 1 %} + {% trans "Associated Networks" %} + {% else %} + {% trans "Associated Network" %} + {% endif %} +
+ {% for network in bgpvpn.networks %} +
+ {% url 'horizon:project:networks:detail' network as network_url %} + {{ network }} +
+ {% endfor %} \ No newline at end of file diff --git a/bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/_associated_routers.html b/bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/_associated_routers.html new file mode 100755 index 00000000..fb619526 --- /dev/null +++ b/bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/_associated_routers.html @@ -0,0 +1,15 @@ +{% load i18n %} + +
+ {% if bgpvpn.routers|length> 1 %} + {% trans "Associated Routers" %} + {% else %} + {% trans "Associated Router" %} + {% endif %} +
+ {% for router in bgpvpn.routers %} +
+ {% url 'horizon:project:routers:detail' router as router_url %} + {{ router }} +
+ {% endfor %} \ No newline at end of file diff --git a/bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/_detail_overview.html b/bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/_detail_overview.html new file mode 100755 index 00000000..370962a6 --- /dev/null +++ b/bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/_detail_overview.html @@ -0,0 +1,14 @@ +{% load i18n %} + +
+
+
{% trans "BGPVPN Name" %}
+
{{ bgpvpn.name }}
+
{% trans "BGPVPN ID" %}
+
{{ bgpvpn.id }}
+
{% trans "Type" %}
+
{{ bgpvpn.type }}
+ {% include "project/bgpvpn/_associated_networks.html" %} + {% include "project/bgpvpn/_associated_routers.html" %} +
+
diff --git a/bgpvpn_dashboard/dashboards/admin/bgpvpn/templates/bgpvpn/_modify.html b/bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/_modify.html similarity index 100% rename from bgpvpn_dashboard/dashboards/admin/bgpvpn/templates/bgpvpn/_modify.html rename to bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/_modify.html diff --git a/bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/detail.html b/bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/detail.html new file mode 100755 index 00000000..87d347f7 --- /dev/null +++ b/bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/detail.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans "BGPVPN Details" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_detail_header.html" %} +{% endblock %} + +{% block main %} +
+
+ {% include "project/bgpvpn/_detail_overview.html" %} +
+
+{% endblock %} diff --git a/bgpvpn_dashboard/dashboards/admin/bgpvpn/templates/bgpvpn/index.html b/bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/index.html similarity index 100% rename from bgpvpn_dashboard/dashboards/admin/bgpvpn/templates/bgpvpn/index.html rename to bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/index.html diff --git a/bgpvpn_dashboard/dashboards/admin/bgpvpn/templates/bgpvpn/modify.html b/bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/modify.html similarity index 71% rename from bgpvpn_dashboard/dashboards/admin/bgpvpn/templates/bgpvpn/modify.html rename to bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/modify.html index cbd01a1d..a7b35a97 100755 --- a/bgpvpn_dashboard/dashboards/admin/bgpvpn/templates/bgpvpn/modify.html +++ b/bgpvpn_dashboard/dashboards/project/bgpvpn/templates/bgpvpn/modify.html @@ -3,5 +3,5 @@ {% block title %}{% trans "Update BGPVPN" %}{% endblock %} {% block main %} - {% include "admin/bgpvpn/_modify.html" %} + {% include "project/bgpvpn/_modify.html" %} {% endblock %} diff --git a/bgpvpn_dashboard/dashboards/project/bgpvpn/urls.py b/bgpvpn_dashboard/dashboards/project/bgpvpn/urls.py new file mode 100755 index 00000000..4a7f4a45 --- /dev/null +++ b/bgpvpn_dashboard/dashboards/project/bgpvpn/urls.py @@ -0,0 +1,32 @@ +# Copyright (c) 2016 Orange. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls import patterns +from django.conf.urls import url + +import bgpvpn_dashboard.dashboards.project.bgpvpn.views as bgpvpn_views + +BGPVPN = r'^(?P[^/]+)/%s$' + +urlpatterns = patterns( + '', + url(r'^$', bgpvpn_views.IndexView.as_view(), name='index'), + url(BGPVPN % 'edit', bgpvpn_views.EditDataView.as_view(), name='edit'), + url(BGPVPN % 'update-associations', + bgpvpn_views.UpdateAssociationsView.as_view(), + name='update-associations'), + url(r'^(?P[^/]+)/detail/$', + bgpvpn_views.DetailProjectView.as_view(), name='detail'), +) diff --git a/bgpvpn_dashboard/dashboards/project/bgpvpn/views.py b/bgpvpn_dashboard/dashboards/project/bgpvpn/views.py new file mode 100755 index 00000000..fc9994fe --- /dev/null +++ b/bgpvpn_dashboard/dashboards/project/bgpvpn/views.py @@ -0,0 +1,139 @@ +# Copyright (c) 2016 Orange. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse_lazy +from django.utils.translation import ugettext_lazy as _ +from horizon import exceptions +from horizon import forms +from horizon import tables +from horizon.utils import memoized +from horizon import views +from horizon import workflows +from openstack_dashboard import api + +import bgpvpn_dashboard.api.bgpvpn as bgpvpn_api +from bgpvpn_dashboard.common import bgpvpn as bgpvpn_common +from bgpvpn_dashboard.dashboards.project.bgpvpn import forms as bgpvpn_forms +from bgpvpn_dashboard.dashboards.project.bgpvpn import tables as bgpvpn_tables +from bgpvpn_dashboard.dashboards.project.bgpvpn import workflows \ + as bgpvpn_workflows + + +class IndexView(tables.DataTableView): + table_class = bgpvpn_tables.BgpvpnTable + template_name = 'project/bgpvpn/index.html' + page_title = _("BGP VPNs") + + @memoized.memoized_method + def get_data(self): + tenant_id = self.request.user.tenant_id + bgpvpns = bgpvpn_api.bgpvpns_list(self.request, tenant_id=tenant_id) + networks = api.neutron.network_list_for_tenant(self.request, + tenant_id) + routers = api.neutron.router_list(self.request, tenant_id=tenant_id) + for bgpvpn in bgpvpns: + networks_list = [network for network in networks + if network.id in bgpvpn.networks] + routers_list = [router for router in routers + if router.id in bgpvpn.routers] + bgpvpn.networks = networks_list + bgpvpn.routers = routers_list + return bgpvpns + + +class EditDataView(forms.ModalFormView): + form_class = bgpvpn_forms.EditDataBgpVpn + form_id = "edit_data_bgpvpn_form" + modal_header = _("Edit BGPVPN") + submit_label = _("Update Change") + submit_url = 'horizon:project:bgpvpn:edit' + success_url = reverse_lazy('horizon:project:bgpvpn:index') + template_name = 'project/bgpvpn/modify.html' + page_title = _("Edit BGPVPN") + + @staticmethod + def _join_rts(route_targets_list): + return ','.join(route_targets_list) + + def get_context_data(self, **kwargs): + context = super(EditDataView, self).get_context_data(**kwargs) + args = (self.kwargs['bgpvpn_id'],) + context["bgpvpn_id"] = self.kwargs['bgpvpn_id'] + context["submit_url"] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + bgpvpn_id = self.kwargs['bgpvpn_id'] + try: + # Get initial bgpvpn information + bgpvpn = bgpvpn_api.bgpvpn_get(self.request, bgpvpn_id) + except Exception: + exceptions.handle( + self.request, + _('Unable to retrieve BGPVPN details.'), + redirect=self.success_url) + else: + data = bgpvpn.to_dict() + if self.request.user.is_superuser: + for attribute in bgpvpn_common.RT_FORMAT_ATTRIBUTES: + data[attribute] = self._join_rts(bgpvpn[attribute]) + data['bgpvpn_id'] = data.pop('id') + return data + + +class UpdateAssociationsView(workflows.WorkflowView): + workflow_class = bgpvpn_workflows.UpdateBgpVpnAssociations + page_title = _("Edit BGPVPN associations") + failure_url = reverse_lazy("horizon:project:bgpvpn:index") + + def get_initial(self): + bgpvpn_id = self.kwargs['bgpvpn_id'] + try: + # Get initial bgpvpn information + bgpvpn = bgpvpn_api.bgpvpn_get(self.request, bgpvpn_id) + data = bgpvpn.to_dict() + data['bgpvpn_id'] = data.pop('id') + return data + except Exception: + exceptions.handle( + self.request, + _('Unable to retrieve BGPVPN details.'), + redirect=self.failure_url) + + +class DetailProjectView(views.HorizonTemplateView): + template_name = 'project/bgpvpn/detail.html' + page_title = "{{ bgpvpn.name }}" + redirect_url = 'horizon:project:bgpvpn:index' + + def get_context_data(self, **kwargs): + context = super(DetailProjectView, self).get_context_data(**kwargs) + bgpvpn = self.get_data() + table = bgpvpn_tables.BgpvpnTable(self.request) + context["bgpvpn"] = bgpvpn + context["url"] = reverse(self.redirect_url) + context["actions"] = table.render_row_actions(bgpvpn) + return context + + @memoized.memoized_method + def get_data(self): + try: + bgpvpn_id = self.kwargs['bgpvpn_id'] + return bgpvpn_api.bgpvpn_get(self.request, bgpvpn_id) + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve BGPVPN details.'), + redirect=reverse(self.redirect_url)) diff --git a/bgpvpn_dashboard/dashboards/project/bgpvpn/workflows.py b/bgpvpn_dashboard/dashboards/project/bgpvpn/workflows.py new file mode 100755 index 00000000..74dd70a1 --- /dev/null +++ b/bgpvpn_dashboard/dashboards/project/bgpvpn/workflows.py @@ -0,0 +1,241 @@ +# Copyright (c) 2016 Orange. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.utils.translation import ugettext_lazy as _ +from horizon import exceptions +from horizon import forms +from horizon import workflows +from openstack_dashboard import api + +from bgpvpn_dashboard.api import bgpvpn as bgpvpn_api + + +class UpdateAssociations(workflows.MembershipAction): + def __init__(self, request, resource_type, *args, **kwargs): + super(UpdateAssociations, self).__init__(request, + *args, + **kwargs) + err_msg = _('Unable to retrieve %ss list. ' + 'Please try again later.') % resource_type + context = args[0] + + default_role_field_name = self.get_default_role_field_name() + self.fields[default_role_field_name] = forms.CharField(required=False) + self.fields[default_role_field_name].initial = 'member' + + field_name = self.get_member_field_name('member') + self.fields[field_name] = forms.MultipleChoiceField(required=False) + + all_resources = self._get_resources(request, context, resource_type, + err_msg) + + resources_list = [(resource.id, resource.name) + for resource in all_resources] + + self.fields[field_name].choices = resources_list + + bgpvpn_id = context.get('bgpvpn_id') + + try: + if bgpvpn_id: + associations = [] + list_method = getattr(bgpvpn_api, '%s_association_list' % + resource_type) + associations = [ + getattr(association, '%s_id' % + resource_type) for association in + list_method(request, bgpvpn_id) + ] + + except Exception: + exceptions.handle(request, err_msg) + + self.fields[field_name].initial = associations + + def _get_resources(self, request, context, resource_type, err_msg): + """Get list of available resources.""" + # when an admin user uses the project panel BGPVPN, there is no + # tenant_id in context because bgpvpn_get doesn't return it + if request.user.is_superuser and context.get('tenant_id'): + tenant_id = context.get('tenant_id') + else: + tenant_id = self.request.user.tenant_id + try: + if resource_type == 'router': + return api.neutron.router_list(request, tenant_id=tenant_id) + elif resource_type == 'network': + return api.neutron.network_list_for_tenant(request, tenant_id) + else: + raise Exception( + 'Resource type %s is not supported' % resource_type) + except Exception: + exceptions.handle(request, err_msg % resource_type) + + +class UpdateBgpVpnRoutersAction(UpdateAssociations): + def __init__(self, request, *args, **kwargs): + super(UpdateBgpVpnRoutersAction, self).__init__(request, + 'router', + *args, + **kwargs) + + class Meta(object): + name = _("Router Associations") + slug = "update_bgpvpn_router" + + +class UpdateBgpVpnRouters(workflows.UpdateMembersStep): + action_class = UpdateBgpVpnRoutersAction + help_text = _("Select the routers to be associated.") + available_list_title = _("All Routers") + members_list_title = _("Selected Routers") + no_available_text = _("No router found.") + no_members_text = _("No router selected.") + show_roles = False + depends_on = ("bgpvpn_id", "name") + contributes = ("routers_association",) + + def contribute(self, data, context): + if data: + member_field_name = self.get_member_field_name('member') + context['routers_association'] = data.get(member_field_name, []) + return context + + +class UpdateBgpVpnNetworksAction(UpdateAssociations): + def __init__(self, request, *args, **kwargs): + super(UpdateBgpVpnNetworksAction, self).__init__(request, + 'network', + *args, + **kwargs) + + class Meta(object): + name = _("Network Associations") + slug = "update_bgpvpn_network" + + +class UpdateBgpVpnNetworks(workflows.UpdateMembersStep): + action_class = UpdateBgpVpnNetworksAction + help_text = _("Select the networks to be associated.") + available_list_title = _("All Networks") + members_list_title = _("Selected Networks") + no_available_text = _("No network found.") + no_members_text = _("No network selected.") + show_roles = False + depends_on = ("bgpvpn_id", "name") + contributes = ("networks_association",) + + def contribute(self, data, context): + if data: + member_field_name = self.get_member_field_name('member') + context['networks_association'] = data.get(member_field_name, []) + return context + + +class UpdateBgpVpnAssociations(workflows.Workflow): + slug = "update_bgpvpn_associations" + name = _("Edit BGPVPN associations") + finalize_button_name = _("Save") + success_message = _('Modified BGPVPN associations "%s".') + failure_message = _('Unable to modify BGPVPN associations "%s".') + success_url = "horizon:project:bgpvpn:index" + default_steps = (UpdateBgpVpnNetworks, + UpdateBgpVpnRouters) + + def format_status_message(self, message): + return message % self.context['name'] + + def _handle_type(self, request, data, association_type): + list_method = getattr(bgpvpn_api, + '%s_association_list' % association_type) + associations = data["%ss_association" % association_type] + try: + old_associations = [ + getattr(association, + '%s_id' % association_type) for association in + list_method(request, data['bgpvpn_id'])] + except Exception: + exceptions.handle(request, + _('Unable to retrieve %ss associations') % + association_type) + raise + + to_remove_associations = list(set(old_associations) - + set(associations)) + to_add_associations = list(set(associations) - + set(old_associations)) + + # If new resource added to the list + if len(to_add_associations)> 0: + for resource in to_add_associations: + try: + create_asso = getattr(bgpvpn_api, + '%s_association_create' % + association_type) + params = self._set_params(data, association_type, resource) + create_asso(request, + data['bgpvpn_id'], + **params) + except Exception as e: + exceptions.handle( + request, + _('Unable to associate {} {} {}').format( + association_type, + resource, str(e))) + raise + + # If resource has been deleted from the list + if len(to_remove_associations)> 0: + for resource in to_remove_associations: + try: + list_method = getattr(bgpvpn_api, + '%s_association_list' % + association_type) + asso_list = list_method(request, data['bgpvpn_id']) + for association in asso_list: + if getattr(association, + '%s_id' % association_type) == resource: + delete_method = getattr(bgpvpn_api, + '%s_association_delete' % + association_type) + delete_method(request, + association.id, data['bgpvpn_id']) + except Exception: + exceptions.handle( + request, + _('Unable to disassociate {}s {}').format( + association_type, + resource)) + raise + + def _set_params(self, data, association_type, resource): + params = dict() + params['%s_id' % association_type] = resource + return params + + def handle(self, request, data): + action = False + try: + if 'networks_association' in data: + self._handle_type(request, data, 'network') + action = True + if 'routers_association' in data: + self._handle_type(request, data, 'router') + action = True + if not action: + raise Exception('Associations type is not supported') + except Exception: + return False + return True diff --git a/bgpvpn_dashboard/enabled/_1495_project_bgpvpn_panel.py b/bgpvpn_dashboard/enabled/_1495_project_bgpvpn_panel.py new file mode 100755 index 00000000..a6c085c0 --- /dev/null +++ b/bgpvpn_dashboard/enabled/_1495_project_bgpvpn_panel.py @@ -0,0 +1,10 @@ +# The slug of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'BGPVPN Interconnections' +# The slug of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'project' +# The slug of the panel group the PANEL is associated with. +PANEL_GROUP = 'network' + +# Python panel class of the PANEL to be added. +ADD_PANEL = ('bgpvpn_dashboard.' + 'dashboards.project.bgpvpn.panel.BGPVPNInterconnections') diff --git a/devstack/plugin.sh b/devstack/plugin.sh index c96f0980..7039da2f 100755 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -39,8 +39,7 @@ elif [[ "1ドル" == "stack" && "2ドル" == "post-config" ]]; then iniset $HEAT_CONF DEFAULT plugin_dirs $NETWORKING_BGPVPN_DIR/networking_bgpvpn_heat fi if is_service_enabled horizon; then - cp $BGPVPN_DASHBOARD_ENABLE_ADMIN \ - $HORIZON_DIR/openstack_dashboard/local/enabled/ + cp $BGPVPN_DASHBOARD_ENABLE $HORIZON_DIR/openstack_dashboard/local/enabled/ fi fi if [[ "1ドル" == "unstack" ]]; then @@ -49,7 +48,7 @@ if [[ "1ドル" == "unstack" ]]; then fi if [[ "1ドル" == "clean" ]]; then # Remove bgpvpn-dashboard enabled files and pyc - rm -f ${BGPVPN_DASHBOARD_ENABLE_ADMIN}* + rm -f $HORIZON_DIR/openstack_dashboard/local/enabled/*_bgpvpn_panel* fi # Restore XTRACE diff --git a/devstack/settings b/devstack/settings index 5d1e4e22..e2473f48 100755 --- a/devstack/settings +++ b/devstack/settings @@ -1,6 +1,6 @@ NETWORKING_BGPVPN_DIR="$DEST/networking-bgpvpn" NETWORKING_BGPVPN_CONF="$NEUTRON_CONF_DIR/networking_bgpvpn.conf" -BGPVPN_DASHBOARD_ENABLE_ADMIN="$NETWORKING_BGPVPN_DIR/bgpvpn_dashboard/enabled/_2115_admin_bgpvpn_panel.py" +BGPVPN_DASHBOARD_ENABLE="$NETWORKING_BGPVPN_DIR/bgpvpn_dashboard/enabled/*" BGPVPN_PLUGIN_CLASS="networking_bgpvpn.neutron.services.plugin.BGPVPNPlugin" NETWORKING_BGPVPN_DRIVER=${NETWORKING_BGPVPN_DRIVER:-BGPVPN:Dummy:networking_bgpvpn.neutron.services.service_drivers.driver_api.BGPVPNDriver:default} diff --git a/doc/source/horizon.rst b/doc/source/horizon.rst index 7d487467..880ff63c 100755 --- a/doc/source/horizon.rst +++ b/doc/source/horizon.rst @@ -17,6 +17,14 @@ The operations possible for admin users are: * associating or disassociating a BGPVPN to router(s) * deleting a BGPVPN +For non admin users the plugin adds a BGPVPN Interconnections panel in the Project +section under the Network subsection. +The operations possible for non admin users are: + +* listing BGPVPN (display only name, type, networks and routers associations) +* editing a BGPVPN (only the name) +* associating or disassociating a BGPVPN to network(s) +* associating or disassociating a BGPVPN to router(s) Installation and Configuration ============================== @@ -36,4 +44,4 @@ Copy configuration file: Restart the web server hosting Horizon. -The BGPVPN Interconnections panel will now be in your Horizon dashboard. \ No newline at end of file +The BGPVPN Interconnections panels will now be in your Horizon dashboard. \ No newline at end of file diff --git a/releasenotes/notes/horizon-support-06a7b21286002949.yaml b/releasenotes/notes/horizon-support-06a7b21286002949.yaml new file mode 100755 index 00000000..0d6d2cb1 --- /dev/null +++ b/releasenotes/notes/horizon-support-06a7b21286002949.yaml @@ -0,0 +1,17 @@ +--- +prelude:> + New Horizon panels for BGPVPN resources, allowing you to create a + bgpvpn and to associate related resources such as a network or a + router. +features: + - | + Horizon: + * a view of all the existing BGPVPNs. + * ability to view details of a BGPVPN. + * ability to create, update and delete BGPVPN resources for an + admin user. + * ability to update BGPVPN resources for a tenant user. + (with restrictions, compared to what an admin user can change) + * abiity to associate/disassociate BGPVPN to/from networks and + routers (for both tenant and admin users) +

AltStyle によって変換されたページ (->オリジナル) /