From 646fee8ccd99244c76d05866fa9e3b75dfb8390f Mon Sep 17 00:00:00 2001
From: Miriam Yumi
Date: Thu, 22 Dec 2016 16:32:14 -0200
Subject: [PATCH] Add mountable snapshots support to manila-ui
This patch adds support to manila-ui for the mountable snapshots
feature.
Manage Rules row buttons are included in Snapshots tables from
projects section. Snapshots Rules table is accessible from Manage
Rules row buttons.
Snapshot and Share details views are updated to show information
about mountable snapshots.
Implements: blueprint manila-mountable-snapshots
Change-Id: I2804bf20767ac60a0b9566346465e69d74d89d45
Depends-On: Idb2eb5ee18ce55edb77545bcdf4df4ec4dd90135
Depends-On: I785a784bcae7cf3bcef4fa6c64ba28ee58328389
---
manila_ui/api/manila.py | 25 ++-
manila_ui/dashboards/admin/shares/views.py | 24 +--
.../project/shares/replicas/views.py | 16 +-
.../dashboards/project/shares/shares/views.py | 23 +--
.../project/shares/snapshots/forms.py | 25 +++
.../project/shares/snapshots/tables.py | 78 ++++++++
.../project/shares/snapshots/views.py | 91 ++++++++-
.../shares/shares/_detail_overview.html | 2 +
.../templates/shares/snapshots/_rule_add.html | 11 ++
.../snapshots/_snapshot_detail_overview.html | 34 ++++
.../shares/snapshots/manage_rules.html | 7 +
.../templates/shares/snapshots/rule_add.html | 7 +
manila_ui/dashboards/project/shares/urls.py | 6 +
manila_ui/dashboards/utils.py | 9 +
manila_ui/tests/api/test_manila.py | 37 ++++
.../tests/dashboards/admin/shares/tests.py | 53 +++++
.../project/shares/snapshots/tests.py | 181 +++++++++++++++++-
.../dashboards/project/shares/test_data.py | 72 ++++++-
.../tests/dashboards/project/shares/tests.py | 12 +-
...-mountable-snapshots-93a732ad0dc95ade.yaml | 4 +
20 files changed, 668 insertions(+), 49 deletions(-)
create mode 100644 manila_ui/dashboards/project/shares/templates/shares/snapshots/_rule_add.html
create mode 100644 manila_ui/dashboards/project/shares/templates/shares/snapshots/manage_rules.html
create mode 100644 manila_ui/dashboards/project/shares/templates/shares/snapshots/rule_add.html
create mode 100644 releasenotes/notes/bp-manila-mountable-snapshots-93a732ad0dc95ade.yaml
diff --git a/manila_ui/api/manila.py b/manila_ui/api/manila.py
index 6f0ac6f6..af693d60 100644
--- a/manila_ui/api/manila.py
+++ b/manila_ui/api/manila.py
@@ -33,7 +33,7 @@ from openstack_dashboard.api import base
LOG = logging.getLogger(__name__)
MANILA_UI_USER_AGENT_REPR = "manila_ui_plugin_for_horizon"
-MANILA_VERSION = "2.29" # requires manilaclient 1.12.0 or newer
+MANILA_VERSION = "2.32" # requires manilaclient 1.13.0 or newer
MANILA_SERVICE_TYPE = "sharev2"
# API static values
@@ -208,6 +208,29 @@ def share_snapshot_delete(request, snapshot_id):
return manilaclient(request).share_snapshots.delete(snapshot_id)
+def share_snapshot_allow(request, snapshot_id, access_type, access_to):
+ return manilaclient(request).share_snapshots.allow(
+ snapshot_id, access_type, access_to)
+
+
+def share_snapshot_deny(request, snapshot_id, rule_id):
+ return manilaclient(request).share_snapshots.deny(snapshot_id, rule_id)
+
+
+def share_snapshot_rules_list(request, snapshot_id):
+ return manilaclient(request).share_snapshots.access_list(snapshot_id)
+
+
+def share_snap_export_location_list(request, snapshot):
+ return manilaclient(request).share_snapshot_export_locations.list(
+ snapshot=snapshot)
+
+
+def share_snap_instance_export_location_list(request, snapshot_instance):
+ return manilaclient(request).share_snapshot_export_locations.list(
+ snapshot_instance=snapshot_instance)
+
+
def share_server_list(request, search_opts=None):
return manilaclient(request).share_servers.list(search_opts=search_opts)
diff --git a/manila_ui/dashboards/admin/shares/views.py b/manila_ui/dashboards/admin/shares/views.py
index ce163f95..53f6166a 100644
--- a/manila_ui/dashboards/admin/shares/views.py
+++ b/manila_ui/dashboards/admin/shares/views.py
@@ -37,6 +37,7 @@ from manila_ui.dashboards.project.shares.share_networks import \
from manila_ui.dashboards.project.shares.shares import views as share_views
from manila_ui.dashboards.project.shares.snapshots import \
views as snapshot_views
+from manila_ui.dashboards import utils as ui_utils
from manila_ui.utils import filters
filters = (filters.get_item,)
@@ -53,8 +54,8 @@ class DetailView(share_views.DetailView):
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
- context["page_title"] = _("Share Details: %(share_name)s") % \
- {'share_name': context["share_display_name"]}
+ context["page_title"] = _("Share Details: %(share_name)s") % {
+ 'share_name': context["share_display_name"]}
return context
@@ -349,8 +350,8 @@ class ShareServDetail(tabs.TabView):
share_server_display_name = share_server.id
context["share_server"] = share_server
context["share_server_display_name"] = share_server_display_name
- context["page_title"] = _("Share Server Details: %(server_name)s") % \
- {'server_name': share_server_display_name}
+ context["page_title"] = _("Share Server Details: %(server_name)s") % {
+ 'server_name': share_server_display_name}
return context
@memoized.memoized_method
@@ -384,13 +385,6 @@ class ShareInstanceDetailView(tabs.TabView):
tab_group_class = project_tabs.ShareInstanceDetailTabs
template_name = 'admin/shares/share_instance_detail.html'
- def _calculate_size_of_longest_export_location(self, export_locations):
- size = 40
- for export_location in export_locations:
- if len(export_location["path"]) > size:
- size = len(export_location["path"])
- return size
-
def get_context_data(self, **kwargs):
context = super(self.__class__, self).get_context_data(**kwargs)
share_instance = self.get_data()
@@ -408,9 +402,11 @@ class ShareInstanceDetailView(tabs.TabView):
share_instance.export_locations = (
manila.share_instance_export_location_list(
self.request, share_instance_id))
- share_instance.el_size = (
- self._calculate_size_of_longest_export_location(
- share_instance.export_locations))
+ export_locations = [
+ exp['path'] for exp in share_instance.export_locations
+ ]
+ share_instance.el_size = ui_utils.calculate_longest_str_size(
+ export_locations)
return share_instance
except Exception:
redirect = reverse('horizon:admin:shares:index')
diff --git a/manila_ui/dashboards/project/shares/replicas/views.py b/manila_ui/dashboards/project/shares/replicas/views.py
index ff37fcee..5acbfd2d 100644
--- a/manila_ui/dashboards/project/shares/replicas/views.py
+++ b/manila_ui/dashboards/project/shares/replicas/views.py
@@ -28,6 +28,7 @@ from manila_ui.dashboards.project.shares.replicas import (
from manila_ui.dashboards.project.shares.replicas import (
tables as replicas_tables)
from manila_ui.dashboards.project.shares.replicas import tabs as replicas_tabs
+from manila_ui.dashboards import utils as ui_utils
class ManageReplicasView(tables.DataTableView):
@@ -70,14 +71,6 @@ class DetailReplicaView(tabs.TabView):
template_name = 'project/shares/replicas/detail.html'
_redirect_url = 'horizon:project:shares:index'
- def _calculate_size_of_longest_export_location(self, export_locations):
- size = 40
- for export_location in export_locations:
- current_size = len(export_location["path"])
- if current_size > size:
- size = current_size
- return size
-
def get_context_data(self, **kwargs):
context = super(DetailReplicaView, self).get_context_data(**kwargs)
replica = self.get_data()
@@ -97,8 +90,11 @@ class DetailReplicaView(tabs.TabView):
replica.export_locations = (
manila.share_instance_export_location_list(
self.request, replica_id))
- replica.el_size = self._calculate_size_of_longest_export_location(
- replica.export_locations)
+ export_locations = [
+ exp['path'] for exp in replica.export_locations
+ ]
+ replica.el_size = ui_utils.calculate_longest_str_size(
+ export_locations)
except Exception:
redirect = reverse(self._redirect_url)
exceptions.handle(
diff --git a/manila_ui/dashboards/project/shares/shares/views.py b/manila_ui/dashboards/project/shares/shares/views.py
index 1e63b6d8..0f20e1c0 100644
--- a/manila_ui/dashboards/project/shares/shares/views.py
+++ b/manila_ui/dashboards/project/shares/shares/views.py
@@ -29,6 +29,7 @@ from manila_ui.dashboards.project.shares.shares \
import tables as shares_tables
from manila_ui.dashboards.project.shares.shares \
import tabs as shares_tabs
+from manila_ui.dashboards import utils as ui_utils
from openstack_dashboard.usage import quotas
@@ -52,14 +53,6 @@ class DetailView(tabs.TabView):
tab_group_class = shares_tabs.ShareDetailTabs
template_name = 'project/shares/shares/detail.html'
- def _calculate_size_of_longest_export_location(self, export_locations):
- size = 40
- for export_location in export_locations:
- current_size = len(export_location["path"])
- if current_size > size:
- size = current_size
- return size
-
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
share = self.get_data()
@@ -67,8 +60,8 @@ class DetailView(tabs.TabView):
context["share"] = share
context["share_display_name"] = share_display_name
context["page_title"] = _("Share Details: "
- "%(share_display_name)s") % \
- {'share_display_name': share_display_name}
+ "%(share_display_name)s") % {
+ 'share_display_name': share_display_name}
return context
@memoized.memoized_method
@@ -79,8 +72,10 @@ class DetailView(tabs.TabView):
share.rules = manila.share_rules_list(self.request, share_id)
share.export_locations = manila.share_export_location_list(
self.request, share_id)
- share.el_size = self._calculate_size_of_longest_export_location(
- share.export_locations)
+ export_locations = [
+ exp['path'] for exp in share.export_locations]
+ share.el_size = ui_utils.calculate_longest_str_size(
+ export_locations)
except Exception:
redirect = reverse('horizon:project:shares:index')
exceptions.handle(self.request,
@@ -230,8 +225,8 @@ class ManageRulesView(tables.DataTableView):
context['share_display_name'] = share.name or share.id
context["share"] = self.get_data()
context["page_title"] = _("Share Rules: "
- "%(share_display_name)s") % \
- {'share_display_name': context['share_display_name']}
+ "%(share_display_name)s") % {
+ 'share_display_name': context['share_display_name']}
return context
@memoized.memoized_method
diff --git a/manila_ui/dashboards/project/shares/snapshots/forms.py b/manila_ui/dashboards/project/shares/snapshots/forms.py
index 31b4d221..591c2a5a 100644
--- a/manila_ui/dashboards/project/shares/snapshots/forms.py
+++ b/manila_ui/dashboards/project/shares/snapshots/forms.py
@@ -73,3 +73,28 @@ class UpdateForm(forms.SelfHandlingForm):
return True
except Exception:
exceptions.handle(request, _('Unable to update snapshot.'))
+
+
+class AddRule(forms.SelfHandlingForm):
+ access_type = forms.ChoiceField(
+ label=_("Access Type"), required=True,
+ choices=(('ip', 'ip'), ('user', 'user'), ('cephx', 'cephx'),
+ ('cert', 'cert')))
+ access_to = forms.CharField(
+ label=_("Access To"), max_length="255", required=True)
+
+ def handle(self, request, data):
+ snapshot_id = self.initial['snapshot_id']
+ try:
+ manila.share_snapshot_allow(
+ request, snapshot_id,
+ access_to=data['access_to'],
+ access_type=data['access_type'])
+ message = _('Creating snapshot rule for "%s"') % data['access_to']
+ messages.success(request, message)
+ return True
+ except Exception:
+ redirect = reverse("horizon:project:shares:snapshot_manage_rules",
+ args=[self.initial['snapshot_id']])
+ exceptions.handle(
+ request, _('Unable to add snapshot rule.'), redirect=redirect)
diff --git a/manila_ui/dashboards/project/shares/snapshots/tables.py b/manila_ui/dashboards/project/shares/snapshots/tables.py
index 407dd05e..956668a1 100644
--- a/manila_ui/dashboards/project/shares/snapshots/tables.py
+++ b/manila_ui/dashboards/project/shares/snapshots/tables.py
@@ -137,6 +137,83 @@ class SnapshotShareNameColumn(tables.Column):
return reverse(self.link, args=(snapshot.share_id,))
+class ManageRules(tables.LinkAction):
+ name = "snapshot_manage_rules"
+ verbose_name = _("Manage Rules")
+ url = "horizon:project:shares:snapshot_manage_rules"
+ classes = ("btn-edit",)
+ policy_rules = (("share", "share:access_get_all"),)
+
+ def allowed(self, request, snapshot=None):
+ share = manila.share_get(request, snapshot.share_id)
+ return share.mount_snapshot_support
+
+
+class AddRule(tables.LinkAction):
+ name = "snapshot_rule_add"
+ verbose_name = _("Add rule")
+ url = 'horizon:project:shares:snapshot_rule_add'
+ classes = ("ajax-modal", "btn-create")
+ icon = "plus"
+ policy_rules = (("share", "share:allow_access"),)
+
+ def allowed(self, request, snapshot=None):
+ snapshot = manila.share_snapshot_get(
+ request, self.table.kwargs['snapshot_id'])
+ return snapshot.status in ("available", "in-use")
+
+ def get_link_url(self):
+ return reverse(self.url, args=[self.table.kwargs['snapshot_id']])
+
+
+class DeleteRule(tables.DeleteAction):
+ data_type_singular = _("Rule")
+ data_type_plural = _("Rules")
+ action_past = _("Scheduled deletion of %(data_type)s")
+ policy_rules = (("share", "share:deny_access"),)
+
+ def delete(self, request, obj_id):
+ try:
+ manila.share_snapshot_deny(
+ request, self.table.kwargs['snapshot_id'], obj_id)
+ except Exception:
+ msg = _('Unable to delete snapshot rule "%s".') % obj_id
+ exceptions.handle(request, msg)
+
+
+class UpdateRuleRow(tables.Row):
+ ajax = True
+
+ def get_data(self, request, rule_id):
+ rules = manila.share_snapshot_rules_list(
+ request, self.table.kwargs['snapshot_id'])
+ if rules:
+ for rule in rules:
+ if rule.id == rule_id:
+ return rule
+ raise exceptions.NotFound
+
+
+class RulesTable(tables.DataTable):
+ access_type = tables.Column("access_type", verbose_name=_("Access Type"))
+ access_to = tables.Column("access_to", verbose_name=_("Access to"))
+ status = tables.Column("state", verbose_name=_("Status"))
+
+ def get_object_display(self, obj):
+ return obj.id
+
+ class Meta(object):
+ name = "rules"
+ verbose_name = _("Rules")
+ status_columns = ["status"]
+ row_class = UpdateRuleRow
+ table_actions = (
+ AddRule,
+ DeleteRule)
+ row_actions = (
+ DeleteRule,)
+
+
class SnapshotsTable(tables.DataTable):
STATUS_CHOICES = (
("in-use", True),
@@ -184,4 +261,5 @@ class SnapshotsTable(tables.DataTable):
row_actions = (
EditSnapshot,
CreateShareFromSnapshot,
+ ManageRules,
DeleteSnapshot)
diff --git a/manila_ui/dashboards/project/shares/snapshots/views.py b/manila_ui/dashboards/project/shares/snapshots/views.py
index 8ff4302a..282ff7a9 100644
--- a/manila_ui/dashboards/project/shares/snapshots/views.py
+++ b/manila_ui/dashboards/project/shares/snapshots/views.py
@@ -18,14 +18,18 @@ from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
+from horizon import tables
from horizon import tabs
from horizon.utils import memoized
from manila_ui.api import manila
from manila_ui.dashboards.project.shares.snapshots import forms \
as snapshot_forms
-from manila_ui.dashboards.project.shares.snapshots\
+from manila_ui.dashboards.project.shares.snapshots \
+ import tables as snapshot_tables
+from manila_ui.dashboards.project.shares.snapshots \
import tabs as snapshot_tabs
+from manila_ui.dashboards import utils as ui_utils
from openstack_dashboard.usage import quotas
@@ -41,8 +45,8 @@ class SnapshotDetailView(tabs.TabView):
context["snapshot"] = snapshot
context["snapshot_display_name"] = snapshot_display_name
context["page_title"] = _("Snapshot Details: "
- "%(snapshot_display_name)s") % \
- {'snapshot_display_name': snapshot_display_name}
+ "%(snapshot_display_name)s") % (
+ {'snapshot_display_name': snapshot_display_name})
return context
@memoized.memoized_method
@@ -51,6 +55,18 @@ class SnapshotDetailView(tabs.TabView):
snapshot_id = self.kwargs['snapshot_id']
snapshot = manila.share_snapshot_get(self.request, snapshot_id)
share = manila.share_get(self.request, snapshot.share_id)
+ if share.mount_snapshot_support:
+ snapshot.rules = manila.share_snapshot_rules_list(
+ self.request, snapshot_id)
+ snapshot.export_locations = (
+ manila.share_snap_export_location_list(
+ self.request, snapshot))
+ export_locations = [
+ exp['path'] for exp in snapshot.export_locations
+ ]
+ snapshot.el_size = ui_utils.calculate_longest_str_size(
+ export_locations)
+
snapshot.share_name_or_id = share.name or share.id
except Exception:
exceptions.handle(self.request,
@@ -122,3 +138,72 @@ class UpdateView(forms.ModalFormView):
return {'snapshot_id': self.kwargs["snapshot_id"],
'name': snapshot.name,
'description': snapshot.description}
+
+
+class AddRuleView(forms.ModalFormView):
+ form_class = snapshot_forms.AddRule
+ form_id = "rule_add_snap"
+ template_name = 'project/shares/snapshots/rule_add.html'
+ modal_header = _("Add Rule")
+ modal_id = "rule_add_snap_modal"
+ submit_label = _("Add")
+ submit_url = "horizon:project:shares:snapshot_rule_add"
+ success_url = reverse_lazy("horizon:project:shares:index")
+ page_title = _('Add Rule')
+
+ def get_object(self):
+ if not hasattr(self, "_object"):
+ snapshot_id = self.kwargs['snapshot_id']
+ try:
+ self._object = manila.share_snapshot_get(
+ self.request, snapshot_id)
+ except Exception:
+ msg = _('Unable to retrieve snapshot.')
+ url = reverse('horizon:project:shares:index')
+ exceptions.handle(self.request, msg, redirect=url)
+ return self._object
+
+ def get_context_data(self, **kwargs):
+ context = super(AddRuleView, self).get_context_data(**kwargs)
+ args = (self.get_object().id,)
+ context['submit_url'] = reverse(self.submit_url, args=args)
+ return context
+
+ def get_initial(self):
+ snapshot = self.get_object()
+ return {'snapshot_id': self.kwargs["snapshot_id"],
+ 'name': snapshot.name,
+ 'description': snapshot.description}
+
+ def get_success_url(self):
+ return reverse("horizon:project:shares:snapshot_manage_rules",
+ args=[self.kwargs['snapshot_id']])
+
+
+class ManageRulesView(tables.DataTableView):
+ table_class = snapshot_tables.RulesTable
+ template_name = 'project/shares/snapshots/manage_rules.html'
+
+ def get_context_data(self, **kwargs):
+ context = super(ManageRulesView, self).get_context_data(**kwargs)
+ snapshot = manila.share_snapshot_get(
+ self.request, self.kwargs['snapshot_id'])
+ context['snapshot_display_name'] = snapshot.name or snapshot.id
+ context["snapshot"] = self.get_data()
+ context["page_title"] = _("Snapshot Rules: "
+ "%(snapshot_display_name)s") % {
+ 'snapshot_display_name': context['snapshot_display_name']}
+ return context
+
+ @memoized.memoized_method
+ def get_data(self):
+ try:
+ snapshot_id = self.kwargs['snapshot_id']
+ rules = manila.share_snapshot_rules_list(
+ self.request, snapshot_id)
+ except Exception:
+ redirect = reverse('horizon:project:shares:index')
+ exceptions.handle(self.request,
+ _('Unable to retrieve snapshot rules.'),
+ redirect=redirect)
+ return rules
diff --git a/manila_ui/dashboards/project/shares/templates/shares/shares/_detail_overview.html b/manila_ui/dashboards/project/shares/templates/shares/shares/_detail_overview.html
index 17a13e14..ae95363e 100644
--- a/manila_ui/dashboards/project/shares/templates/shares/shares/_detail_overview.html
+++ b/manila_ui/dashboards/project/shares/templates/shares/shares/_detail_overview.html
@@ -61,6 +61,8 @@
{% url 'horizon:project:shares:share_network_detail' share.share_network_id as sn_url%}
{{ share.share_network_id }}
{% endif %}
+ {% trans "Mount snapshot support" %}
+ {{ share.mount_snapshot_support }}
{% trans "Created" %}
{{ share.created_at|parse_date }}
{% trans "Host" %}
diff --git a/manila_ui/dashboards/project/shares/templates/shares/snapshots/_rule_add.html b/manila_ui/dashboards/project/shares/templates/shares/snapshots/_rule_add.html
new file mode 100644
index 00000000..db417421
--- /dev/null
+++ b/manila_ui/dashboards/project/shares/templates/shares/snapshots/_rule_add.html
@@ -0,0 +1,11 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+{% block modal-body-right %}
+ {% trans "Description" %}:
+ {% blocktrans %}
+ Add policy rule to snapshot, 'ip' rule represents ip address,
+ 'user' rule represents username or usergroup,
+ 'cephx' rule represents ceph auth ID, and 'cert' rule represents
+ certificate.
+ {% endblocktrans %}
+{% endblock %}
diff --git a/manila_ui/dashboards/project/shares/templates/shares/snapshots/_snapshot_detail_overview.html b/manila_ui/dashboards/project/shares/templates/shares/snapshots/_snapshot_detail_overview.html
index 1f00bdf0..23002ced 100644
--- a/manila_ui/dashboards/project/shares/templates/shares/snapshots/_snapshot_detail_overview.html
+++ b/manila_ui/dashboards/project/shares/templates/shares/snapshots/_snapshot_detail_overview.html
@@ -17,9 +17,43 @@
{% endif %}
{% trans "Status" %}
{{ snapshot.status|capfirst }}
+ {% if snapshot.export_locations %}
+ {% trans "Export locations" %}
+ {% for el in snapshot.export_locations %}
+
+
Path:
+
+
+ {% if el.is_admin_only != None %}
+ Is admin only: {{ el.is_admin_only }}
+ {% endif %}
+ {% if el.share_snapshot_instance_id %}
+ Snapshot Replica ID: {{ el.share_snapshot_instance_id }}
+ {% endif %}
+
+ {% endfor %}
+ {% endif %}
+{% if snapshot.rules != None %}
+
+
{% trans "Access Rules" %}
+
+
+ {% for rule in snapshot.rules %}
+ {{ rule.access_type }}
+
+
Access to: {{ rule.access_to }}
+ Status: {{ rule.state }}
+
+ {% endfor %}
+
+
+{% endif %}
+
{% trans "Specs" %}
diff --git a/manila_ui/dashboards/project/shares/templates/shares/snapshots/manage_rules.html b/manila_ui/dashboards/project/shares/templates/shares/snapshots/manage_rules.html
new file mode 100644
index 00000000..fed2c700
--- /dev/null
+++ b/manila_ui/dashboards/project/shares/templates/shares/snapshots/manage_rules.html
@@ -0,0 +1,7 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Share Rules" %}{% endblock %}
+
+{% block main %}
+ {{ table.render }}
+{% endblock %}
diff --git a/manila_ui/dashboards/project/shares/templates/shares/snapshots/rule_add.html b/manila_ui/dashboards/project/shares/templates/shares/snapshots/rule_add.html
new file mode 100644
index 00000000..4c6b9c26
--- /dev/null
+++ b/manila_ui/dashboards/project/shares/templates/shares/snapshots/rule_add.html
@@ -0,0 +1,7 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Add Rule" %}{% endblock %}
+
+{% block main %}
+ {% include 'project/shares/snapshots/_rule_add.html' %}
+{% endblock %}
diff --git a/manila_ui/dashboards/project/shares/urls.py b/manila_ui/dashboards/project/shares/urls.py
index 91b65c04..ac625193 100644
--- a/manila_ui/dashboards/project/shares/urls.py
+++ b/manila_ui/dashboards/project/shares/urls.py
@@ -79,6 +79,12 @@ urlpatterns = [
url(r'^(?P
[^/]+)/extend/$',
shares_views.ExtendView.as_view(),
name='extend'),
+ url(r'^(?P[^/]+)/snapshot_rules/$',
+ snapshot_views.ManageRulesView.as_view(),
+ name='snapshot_manage_rules'),
+ url(r'^(?P[^/]+)/snapshot_rule_add/$',
+ snapshot_views.AddRuleView.as_view(),
+ name='snapshot_rule_add'),
]
if manila.is_replication_enabled():
diff --git a/manila_ui/dashboards/utils.py b/manila_ui/dashboards/utils.py
index 944af1d1..98236a5b 100644
--- a/manila_ui/dashboards/utils.py
+++ b/manila_ui/dashboards/utils.py
@@ -103,3 +103,12 @@ def get_nice_security_service_type(security_service):
'kerberos': 'Kerberos',
}
return type_mapping.get(security_service.type, security_service.type)
+
+
+def calculate_longest_str_size(str_list):
+ size = 40
+ for str_val in str_list:
+ current_size = len(str_val)
+ if current_size > size:
+ size = current_size
+ return size
diff --git a/manila_ui/tests/api/test_manila.py b/manila_ui/tests/api/test_manila.py
index b1457282..d218d38d 100644
--- a/manila_ui/tests/api/test_manila.py
+++ b/manila_ui/tests/api/test_manila.py
@@ -199,6 +199,43 @@ class ManilaApiTests(base.APITestCase):
mock_reset_state = self.manilaclient.share_replicas.reset_replica_state
mock_reset_state.assert_called_once_with(replica, state)
+ def test_allow_snapshot(self):
+ access_type = "fake_type"
+ access_to = "fake_value"
+
+ api.share_snapshot_allow(self.request, self.id, access_type,
+ access_to)
+
+ client = self.manilaclient
+ client.share_snapshots.allow.assert_called_once_with(
+ self.id, access_type, access_to)
+
+ def test_deny_snapshot(self):
+ api.share_snapshot_deny(self.request, self.id, self.id)
+
+ client = self.manilaclient
+ client.share_snapshots.deny.assert_called_once_with(self.id, self.id)
+
+ def test_list_snapshot_rules(self):
+ api.share_snapshot_rules_list(self.request, self.id)
+
+ client = self.manilaclient
+ client.share_snapshots.access_list.assert_called_once_with(self.id)
+
+ def test_list_snapshot_export_locations(self):
+ api.share_snap_export_location_list(self.request, self.id)
+
+ client = self.manilaclient
+ client.share_snapshot_export_locations.list.assert_called_once_with(
+ snapshot=self.id)
+
+ def test_list_snapshot_instance_export_locations(self):
+ api.share_snap_instance_export_location_list(self.request, self.id)
+
+ client = self.manilaclient
+ client.share_snapshot_export_locations.list.assert_called_once_with(
+ snapshot_instance=self.id)
+
def test_migration_start(self):
api.migration_start(self.request, 'fake_share', 'fake_host', False,
True, True, True, True, 'fake_net_id',
diff --git a/manila_ui/tests/dashboards/admin/shares/tests.py b/manila_ui/tests/dashboards/admin/shares/tests.py
index ad593942..964b7d3e 100644
--- a/manila_ui/tests/dashboards/admin/shares/tests.py
+++ b/manila_ui/tests/dashboards/admin/shares/tests.py
@@ -750,6 +750,59 @@ class SnapshotsTests(test.BaseAdminViewTests):
api_manila.share_snapshot_get.assert_called_once_with(
mock.ANY, snapshot.id)
+ def test_detail_view_with_mount_support(self):
+ snapshot = test_data.snapshot_mount_support
+ rules = [test_data.ip_rule, test_data.user_rule, test_data.cephx_rule]
+ export_locations = test_data.admin_snapshot_export_locations
+ share = test_data.share_mount_snapshot
+ url = reverse('horizon:project:shares:snapshot-detail',
+ args=[snapshot.id])
+ self.mock_object(
+ api_manila, "share_snapshot_get", mock.Mock(return_value=snapshot))
+ self.mock_object(
+ api_manila, "share_snapshot_rules_list", mock.Mock(
+ return_value=rules))
+ self.mock_object(
+ api_manila, "share_snap_export_location_list", mock.Mock(
+ return_value=export_locations))
+ self.mock_object(
+ api_manila, "share_get", mock.Mock(return_value=share))
+
+ res = self.client.get(url)
+
+ self.assertContains(res, "Snapshot Details: %s "
+ % snapshot.name,
+ 1, 200)
+ self.assertContains(res, "%s " % snapshot.name, 1, 200)
+ self.assertContains(res, "%s " % snapshot.id, 1, 200)
+ self.assertContains(res,
+ "%s " %
+ (snapshot.share_id, share.name), 1, 200)
+ self.assertContains(res, "%s GiB " % snapshot.size, 1, 200)
+ for el in export_locations:
+ self.assertContains(res, "value=\"%s\"" % el.path, 1, 200)
+ self.assertContains(
+ res, "Is admin only: %s
" % el.is_admin_only,
+ 1, 200)
+ self.assertContains(
+ res, ("Snapshot Replica ID: %s
" %
+ el.share_snapshot_instance_id), 1, 200)
+ for rule in rules:
+ self.assertContains(res, "%s " % rule.access_type, 1, 200)
+ self.assertContains(
+ res, "Access to: %s
" % rule.access_to,
+ 1, 200)
+ self.assertContains(
+ res, "Status: active
", len(rules), 200)
+ self.assertNoMessages()
+ api_manila.share_get.assert_called_once_with(mock.ANY, share.id)
+ api_manila.share_snapshot_get.assert_called_once_with(
+ mock.ANY, snapshot.id)
+ api_manila.share_snapshot_rules_list.assert_called_once_with(
+ mock.ANY, snapshot.id)
+ api_manila.share_snap_export_location_list.assert_called_once_with(
+ mock.ANY, snapshot)
+
def test_detail_view_with_exception(self):
url = reverse('horizon:admin:shares:snapshot-detail',
args=[test_data.snapshot.id])
diff --git a/manila_ui/tests/dashboards/project/shares/snapshots/tests.py b/manila_ui/tests/dashboards/project/shares/snapshots/tests.py
index fa5a6a2f..226e413b 100644
--- a/manila_ui/tests/dashboards/project/shares/snapshots/tests.py
+++ b/manila_ui/tests/dashboards/project/shares/snapshots/tests.py
@@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+import ddt
from django.core.urlresolvers import reverse
import mock
@@ -26,6 +27,7 @@ SHARE_INDEX_URL = reverse('horizon:project:shares:index')
SHARE_SNAPSHOTS_TAB_URL = reverse('horizon:project:shares:snapshots_tab')
+@ddt.ddt
class SnapshotSnapshotViewTests(test.TestCase):
def test_create_snapshot_get(self):
@@ -116,6 +118,53 @@ class SnapshotSnapshotViewTests(test.TestCase):
api_manila.share_snapshot_get.assert_called_once_with(
mock.ANY, snapshot.id)
+ def test_detail_view_with_mount_support(self):
+ snapshot = test_data.snapshot_mount_support
+ share = test_data.share_mount_snapshot
+ rules = [test_data.ip_rule, test_data.user_rule, test_data.cephx_rule]
+ export_locations = test_data.user_snapshot_export_locations
+ url = reverse('horizon:project:shares:snapshot-detail',
+ args=[snapshot.id])
+ self.mock_object(
+ api_manila, "share_snapshot_get", mock.Mock(return_value=snapshot))
+ self.mock_object(
+ api_manila, "share_snapshot_rules_list", mock.Mock(
+ return_value=rules))
+ self.mock_object(
+ api_manila, "share_snap_export_location_list", mock.Mock(
+ return_value=export_locations))
+ self.mock_object(
+ api_manila, "share_get", mock.Mock(return_value=share))
+
+ res = self.client.get(url)
+
+ self.assertContains(res, "Snapshot Details: %s "
+ % snapshot.name,
+ 1, 200)
+ self.assertContains(res, "%s " % snapshot.name, 1, 200)
+ self.assertContains(res, "%s " % snapshot.id, 1, 200)
+ self.assertContains(res,
+ "%s " %
+ (snapshot.share_id, share.name), 1, 200)
+ self.assertContains(res, "%s GiB " % snapshot.size, 1, 200)
+ for el in export_locations:
+ self.assertContains(res, "value=\"%s\"" % el.path, 1, 200)
+ for rule in rules:
+ self.assertContains(res, "%s " % rule.access_type, 1, 200)
+ self.assertContains(
+ res, "Access to: %s
" % rule.access_to,
+ 1, 200)
+ self.assertContains(
+ res, "Status: active
", len(rules), 200)
+ self.assertNoMessages()
+ api_manila.share_get.assert_called_once_with(mock.ANY, share.id)
+ api_manila.share_snapshot_get.assert_called_once_with(
+ mock.ANY, snapshot.id)
+ api_manila.share_snapshot_rules_list.assert_called_once_with(
+ mock.ANY, snapshot.id)
+ api_manila.share_snap_export_location_list.assert_called_once_with(
+ mock.ANY, snapshot)
+
def test_update_snapshot_get(self):
snapshot = test_data.snapshot
url = reverse('horizon:project:shares:edit_snapshot',
@@ -142,8 +191,8 @@ class SnapshotSnapshotViewTests(test.TestCase):
'description': snapshot.description,
}
self.mock_object(api_manila, "share_snapshot_update")
- self.mock_object(
- api_manila, "share_snapshot_get", mock.Mock(return_value=snapshot))
+ self.mock_object(api_manila, "share_snapshot_get",
+ mock.Mock(return_value=snapshot))
res = self.client.post(url, formData)
@@ -152,3 +201,131 @@ class SnapshotSnapshotViewTests(test.TestCase):
mock.ANY, snapshot.id)
api_manila.share_snapshot_update.assert_called_once_with(
mock.ANY, snapshot.id, formData['name'], formData['description'])
+
+ def test_list_rules(self):
+ snapshot = test_data.snapshot
+ rules = [test_data.ip_rule, test_data.user_rule, test_data.cephx_rule]
+
+ self.mock_object(
+ api_manila, "share_snapshot_get", mock.Mock(
+ return_value=snapshot))
+ self.mock_object(
+ api_manila, "share_snapshot_rules_list", mock.Mock(
+ return_value=rules))
+ url = reverse('horizon:project:shares:snapshot_manage_rules',
+ args=[snapshot.id])
+
+ res = self.client.get(url)
+
+ self.assertEqual(res.status_code, 200)
+ self.assertTemplateUsed(res,
+ 'project/shares/snapshots/manage_rules.html')
+ api_manila.share_snapshot_rules_list.assert_called_once_with(
+ mock.ANY, snapshot.id)
+
+ def test_list_rules_exception(self):
+ snapshot = test_data.snapshot
+
+ self.mock_object(
+ api_manila, "share_snapshot_get", mock.Mock(
+ return_value=snapshot))
+ self.mock_object(
+ api_manila, "share_snapshot_rules_list",
+ mock.Mock(side_effect=Exception('fake')))
+ url = reverse('horizon:project:shares:snapshot_manage_rules',
+ args=[snapshot.id])
+
+ res = self.client.get(url)
+
+ self.assertEqual(res.status_code, 302)
+ self.assertTemplateNotUsed(
+ res, 'project/shares/snapshots/manage_rules.html')
+ api_manila.share_snapshot_rules_list.assert_called_once_with(
+ mock.ANY, snapshot.id)
+
+ def test_create_rule_get(self):
+ snapshot = test_data.snapshot
+ url = reverse('horizon:project:shares:snapshot_rule_add',
+ args=[snapshot.id])
+
+ self.mock_object(
+ api_manila, "share_snapshot_get", mock.Mock(
+ return_value=snapshot))
+ self.mock_object(
+ neutron, "is_service_enabled", mock.Mock(return_value=[True]))
+
+ res = self.client.get(url)
+
+ self.assertNoMessages()
+ self.assertTemplateUsed(res, 'project/shares/snapshots/rule_add.html')
+
+ def test_create_rule_get_exception(self):
+ snapshot = test_data.snapshot
+ url = reverse('horizon:project:shares:snapshot_rule_add',
+ args=[snapshot.id])
+
+ self.mock_object(
+ api_manila, "share_snapshot_get", mock.Mock(
+ side_effect=Exception('fake')))
+
+ res = self.client.get(url)
+
+ self.assertEqual(res.status_code, 302)
+ self.assertTemplateNotUsed(
+ res, 'project/shares/snapshots/rule_add.html')
+
+ @ddt.data(None, Exception('fake'))
+ def test_create_rule_post(self, exc):
+ snapshot = test_data.snapshot
+
+ self.mock_object(
+ api_manila, "share_snapshot_get", mock.Mock(
+ return_value=snapshot))
+ url = reverse('horizon:project:shares:snapshot_rule_add',
+ args=[snapshot.id])
+ self.mock_object(api_manila, "share_snapshot_allow",
+ mock.Mock(side_effect=exc))
+
+ formData = {
+ 'access_type': 'user',
+ 'method': u'CreateForm',
+ 'access_to': 'someuser',
+ }
+
+ res = self.client.post(url, formData)
+
+ self.assertEqual(res.status_code, 302)
+ api_manila.share_snapshot_allow.assert_called_once_with(
+ mock.ANY, snapshot.id, access_type=formData['access_type'],
+ access_to=formData['access_to'])
+ self.assertRedirectsNoFollow(
+ res,
+ reverse('horizon:project:shares:snapshot_manage_rules',
+ args=[snapshot.id])
+ )
+
+ @ddt.data(None, Exception('fake'))
+ def test_delete_rule(self, exc):
+ snapshot = test_data.snapshot
+ rule = test_data.ip_rule
+ formData = {'action': 'rules__delete__%s' % rule.id}
+
+ self.mock_object(
+ api_manila, "share_snapshot_get", mock.Mock(
+ return_value=snapshot))
+ self.mock_object(api_manila, "share_snapshot_deny",
+ mock.Mock(side_effect=exc))
+ self.mock_object(
+ api_manila, "share_snapshot_rules_list", mock.Mock(
+ return_value=[rule]))
+ url = reverse(
+ 'horizon:project:shares:snapshot_manage_rules',
+ args=[snapshot.id])
+
+ res = self.client.post(url, formData)
+
+ self.assertEqual(res.status_code, 302)
+ api_manila.share_snapshot_deny.assert_called_with(
+ mock.ANY, snapshot.id, rule.id)
+ api_manila.share_snapshot_rules_list.assert_called_with(
+ mock.ANY, snapshot.id)
diff --git a/manila_ui/tests/dashboards/project/shares/test_data.py b/manila_ui/tests/dashboards/project/shares/test_data.py
index 20e86c45..fe5a3f25 100644
--- a/manila_ui/tests/dashboards/project/shares/test_data.py
+++ b/manila_ui/tests/dashboards/project/shares/test_data.py
@@ -35,6 +35,7 @@ from manilaclient.v2 import share_export_locations
from manilaclient.v2 import share_instances
from manilaclient.v2 import share_replicas
from manilaclient.v2 import share_servers
+from manilaclient.v2 import share_snapshot_export_locations
from openstack_dashboard import api
from openstack_dashboard.usage import quotas as usage_quotas
@@ -57,7 +58,8 @@ share = shares.Share(
'share_server_id': '1',
'share_network_id': '7f3d1c33-8d00-4511-29df-a2def31f3b5d',
'availability_zone': 'Test AZ',
- 'replication_type': 'readable'})
+ 'replication_type': 'readable',
+ 'mount_snapshot_support': False})
nameless_share = shares.Share(
shares.ShareManager(FakeAPIClient),
@@ -73,7 +75,8 @@ nameless_share = shares.Share(
'share_type': 'vol_type_1',
'share_server_id': '1',
'share_network_id': '7f3d1c33-8d00-4511-29df-a2def31f3b5d',
- 'availability_zone': 'Test AZ'})
+ 'availability_zone': 'Test AZ',
+ 'mount_snapshot_support': False})
share_with_metadata = shares.Share(
shares.ShareManager(FakeAPIClient),
@@ -87,7 +90,8 @@ share_with_metadata = shares.Share(
'created_at': '2016-06-31 00:00:00',
'share_server_id': '1',
'share_network_id': '7f3d1c33-8d00-4511-29df-a2def31f3b5d',
- 'availability_zone': 'Test AZ'})
+ 'availability_zone': 'Test AZ',
+ 'mount_snapshot_support': False})
other_share = shares.Share(
shares.ShareManager(FakeAPIClient),
@@ -102,7 +106,8 @@ other_share = shares.Share(
'share_type': None,
'share_server_id': '1',
'share_network_id': '7f3d1c33-8d00-4511-29df-a2def31f3b5d',
- 'availability_zone': 'Test AZ'})
+ 'availability_zone': 'Test AZ',
+ 'mount_snapshot_support': False})
share_replica = share_replicas.ShareReplica(
share_replicas.ShareReplicaManager(FakeAPIClient),
@@ -140,6 +145,22 @@ share_replica3 = share_replicas.ShareReplica(
'updated_at': '2016-07-19 21:47:14'}
)
+share_mount_snapshot = shares.Share(
+ shares.ShareManager(FakeAPIClient),
+ {'id': "11023e92-8008-4c8b-8059-7f2293ff3888",
+ 'status': 'available',
+ 'size': 40,
+ 'name': 'Share name',
+ 'description': 'Share description',
+ 'share_proto': 'NFS',
+ 'metadata': {},
+ 'created_at': '2014-01-27 10:30:00',
+ 'share_server_id': '1',
+ 'share_network_id': '7f3d1c33-8d00-4511-29df-a2def31f3b5d',
+ 'availability_zone': 'Test AZ',
+ 'replication_type': 'readable',
+ 'mount_snapshot_support': True})
+
admin_export_location = share_export_locations.ShareExportLocation(
share_export_locations.ShareExportLocationManager(FakeAPIClient),
{'id': '6921e862-88bc-49a5-a2df-efeed9acd583',
@@ -160,6 +181,40 @@ user_export_location = share_export_locations.ShareExportLocation(
export_locations = [admin_export_location, user_export_location]
+admin_snapshot_export_locations = [
+ share_snapshot_export_locations.ShareSnapshotExportLocation(
+ share_snapshot_export_locations.ShareSnapshotExportLocationManager(
+ FakeAPIClient),
+ {'id': '6921e862-88bc-49a5-a2df-efeed9acd584',
+ 'path': '1.1.1.1:/path/to/admin/share',
+ 'is_admin_only': True,
+ 'share_snapshot_instance_id': 'e1c2d35e-fe67-4028-ad7a-45f668732b1e'}
+ ),
+ share_snapshot_export_locations.ShareSnapshotExportLocation(
+ share_snapshot_export_locations.ShareSnapshotExportLocationManager(
+ FakeAPIClient),
+ {'id': '6921e862-88bc-49a5-a2df-efeed9acd585',
+ 'path': '1.1.1.2:/path/to/admin/share',
+ 'is_admin_only': False,
+ 'share_snapshot_instance_id': 'e1c2d35e-fe67-4028-ad7a-45f668732b1f'}
+ )
+]
+
+user_snapshot_export_locations = [
+ share_snapshot_export_locations.ShareSnapshotExportLocation(
+ share_snapshot_export_locations.ShareSnapshotExportLocationManager(
+ FakeAPIClient),
+ {'id': 'b6bd76ce-12a2-42a9-a30a-8a43b503867e',
+ 'path': '1.1.1.1:/path/to/user/share_snapshot'}
+ ),
+ share_snapshot_export_locations.ShareSnapshotExportLocation(
+ share_snapshot_export_locations.ShareSnapshotExportLocationManager(
+ FakeAPIClient),
+ {'id': 'b6bd76ce-12a2-42a9-a30a-8a43b503867f',
+ 'path': '1.1.1.2:/not/too/long/path/to/user/share_snapshot'}
+ )
+]
+
rule = collections.namedtuple('Access', ['access_type', 'access_to', 'state',
'id', 'access_level', 'access_key'])
@@ -180,6 +235,15 @@ snapshot = share_snapshots.ShareSnapshot(
'status': 'available',
'share_id': '11023e92-8008-4c8b-8059-7f2293ff3887'})
+snapshot_mount_support = share_snapshots.ShareSnapshot(
+ share_snapshots.ShareSnapshotManager(FakeAPIClient),
+ {'id': '5f3d1c33-7d00-4511-99df-a2def31f3b5e',
+ 'name': 'test snapshot',
+ 'description': 'share snapshot',
+ 'size': 40,
+ 'status': 'available',
+ 'share_id': '11023e92-8008-4c8b-8059-7f2293ff3888'})
+
inactive_share_network = share_networks.ShareNetwork(
share_networks.ShareNetworkManager(FakeAPIClient),
{'id': '6f3d1c33-8d00-4511-29df-a2def31f3b5d',
diff --git a/manila_ui/tests/dashboards/project/shares/tests.py b/manila_ui/tests/dashboards/project/shares/tests.py
index 399aee05..c621406c 100644
--- a/manila_ui/tests/dashboards/project/shares/tests.py
+++ b/manila_ui/tests/dashboards/project/shares/tests.py
@@ -30,16 +30,22 @@ INDEX_URL = reverse('horizon:project:shares:index')
class SharesTests(test.TestCase):
def test_index_with_all_tabs(self):
- snaps = [test_data.snapshot]
+ snaps = [test_data.snapshot, test_data.snapshot_mount_support]
shares = [test_data.share, test_data.nameless_share,
test_data.other_share]
share_networks = [test_data.inactive_share_network,
test_data.active_share_network]
security_services = [test_data.sec_service]
+ snap_shares = [test_data.share, test_data.share_mount_snapshot]
+
self.mock_object(
api_manila, "share_list", mock.Mock(return_value=shares))
self.mock_object(
api_manila, "share_snapshot_list", mock.Mock(return_value=snaps))
+ self.mock_object(
+ api_manila, "share_get", mock.Mock(return_value=snap_shares[0]))
+ self.mock_object(
+ api_manila, "share_get", mock.Mock(return_value=snap_shares[1]))
self.mock_object(
api_manila, "share_network_list",
mock.Mock(return_value=share_networks))
@@ -67,6 +73,10 @@ class SharesTests(test.TestCase):
api_neutron.subnet_list.assert_called_once_with(mock.ANY)
api_manila.security_service_list.assert_called_once_with(mock.ANY)
api_manila.share_snapshot_list.assert_called_with(mock.ANY)
+ api_manila.share_get.assert_has_calls([
+ mock.call(mock.ANY, snaps[0].share_id),
+ mock.call(mock.ANY, snaps[1].share_id)
+ ])
api_manila.share_list.assert_called_with(mock.ANY)
api_manila.share_network_list.assert_has_calls([
mock.call(mock.ANY),
diff --git a/releasenotes/notes/bp-manila-mountable-snapshots-93a732ad0dc95ade.yaml b/releasenotes/notes/bp-manila-mountable-snapshots-93a732ad0dc95ade.yaml
new file mode 100644
index 00000000..6d95ec0c
--- /dev/null
+++ b/releasenotes/notes/bp-manila-mountable-snapshots-93a732ad0dc95ade.yaml
@@ -0,0 +1,4 @@
+---
+features:
+ - Added support for the mountable snapshots feature to manila-ui.
+