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. +