diff --git a/README.rst b/README.rst index 905532b2..9613c506 100644 --- a/README.rst +++ b/README.rst @@ -93,9 +93,11 @@ It is possible to enable or disable some Manila UI features. To do so, look for files located in "manila_ui/local/local_settings.d/" directory, where you can redefine the values of the OPENSTACK_MANILA_FEATURES dict:: + * enable_share_groups * enable_replication * enable_migration * enable_public_share_type_creation + * enable_public_share_group_type_creation * enable_public_shares * enabled_share_protocols @@ -108,9 +110,11 @@ expected to redefine enabled_share_protocols as follows: .. code-block:: python OPENSTACK_MANILA_FEATURES = { + 'enable_share_groups': True, 'enable_replication': True, 'enable_migration': True, 'enable_public_share_type_creation': True, + 'enable_public_share_group_type_creation': True, 'enable_public_shares': True, 'enabled_share_protocols': ['NFS'], } diff --git a/manila_ui/api/manila.py b/manila_ui/api/manila.py index 1b3def35..0d9a533a 100644 --- a/manila_ui/api/manila.py +++ b/manila_ui/api/manila.py @@ -19,20 +19,19 @@ # under the License. from __future__ import absolute_import -import logging - from django.conf import settings -from manilaclient import client as manila_client +from horizon import exceptions +import logging +from openstack_dashboard.api import base import six -from horizon import exceptions -from horizon.utils.memoized import memoized # noqa -from openstack_dashboard.api import base - +from manilaclient import client as manila_client LOG = logging.getLogger(__name__) MANILA_UI_USER_AGENT_REPR = "manila_ui_plugin_for_horizon" +# NOTE(vponomaryov): update version to 2.34 when manilaclient is released with +# its support. It will allow to show 'availability zones' for share groups. MANILA_VERSION = "2.32" # requires manilaclient 1.13.0 or newer MANILA_SERVICE_TYPE = "sharev2" @@ -80,16 +79,21 @@ def share_get(request, share_id): def share_create(request, size, name, description, proto, snapshot_id=None, metadata=None, share_network=None, share_type=None, - is_public=None, availability_zone=None): + is_public=None, availability_zone=None, share_group_id=None): return manilaclient(request).shares.create( proto, size, name=name, description=description, share_network=share_network, snapshot_id=snapshot_id, metadata=metadata, share_type=share_type, is_public=is_public, - availability_zone=availability_zone) + availability_zone=availability_zone, + share_group_id=share_group_id, + ) -def share_delete(request, share_id): - return manilaclient(request).shares.delete(share_id) +def share_delete(request, share_id, share_group_id=None): + return manilaclient(request).shares.delete( + share_id, + share_group_id=share_group_id, + ) def share_update(request, share_id, name, description, is_public=''): @@ -481,13 +485,144 @@ def pool_list(request, detailed=False): return manilaclient(request).pools.list(detailed=detailed) -@memoized -def is_replication_enabled(): - manila_config = getattr(settings, 'OPENSTACK_MANILA_FEATURES', {}) - return manila_config.get('enable_replication', True) +# ####### Share Groups # ####### + +def share_group_create(request, name, description=None, + share_group_type=None, + share_types=None, + share_network=None, + source_share_group_snapshot=None, + availability_zone=None): + return manilaclient(request).share_groups.create( + name=name, + description=description, + share_group_type=share_group_type, + share_types=share_types, + share_network=share_network, + source_share_group_snapshot=source_share_group_snapshot, + availability_zone=availability_zone, + ) -@memoized -def is_migration_enabled(): - manila_config = getattr(settings, 'OPENSTACK_MANILA_FEATURES', {}) - return manila_config.get('enable_migration', True) +def share_group_get(request, share_group): + return manilaclient(request).share_groups.get(share_group) + + +def share_group_update(request, share_group, name, description): + return manilaclient(request).share_groups.update( + share_group, + name=name, + description=description, + ) + + +def share_group_delete(request, share_group, force=False): + return manilaclient(request).share_groups.delete(share_group, force=force) + + +def share_group_reset_state(request, share_group, state): + return manilaclient(request).share_groups.reset_state(share_group, state) + + +def share_group_list(request, detailed=True, search_opts=None, sort_key=None, + sort_dir=None): + return manilaclient(request).share_groups.list( + detailed=detailed, + search_opts=search_opts, + sort_key=sort_key, + sort_dir=sort_dir, + ) + + +# ####### Share Group Snapshots # ####### + +def share_group_snapshot_create(request, share_group, name, description=None): + return manilaclient(request).share_group_snapshots.create( + share_group=share_group, + name=name, + description=description, + ) + + +def share_group_snapshot_get(request, share_group_snapshot): + return manilaclient(request).share_group_snapshots.get( + share_group_snapshot) + + +def share_group_snapshot_update(request, share_group_snapshot, name, + description): + return manilaclient(request).share_group_snapshots.update( + share_group_snapshot, + name=name, + description=description, + ) + + +def share_group_snapshot_delete(request, share_group_snapshot, force=False): + return manilaclient(request).share_group_snapshots.delete( + share_group_snapshot, force=force) + + +def share_group_snapshot_reset_state(request, share_group_snapshot, state): + return manilaclient(request).share_group_snapshots.reset_state( + share_group_snapshot, state) + + +def share_group_snapshot_list(request, detailed=True, search_opts=None, + sort_key=None, sort_dir=None): + return manilaclient(request).share_group_snapshots.list( + detailed=detailed, + search_opts=search_opts, + sort_key=sort_key, + sort_dir=sort_dir, + ) + + +# ####### Share Group Types # ######## + +def share_group_type_create(request, name, share_types, is_public=False, + group_specs=None): + return manilaclient(request).share_group_types.create( + name=name, share_types=share_types, is_public=is_public, + group_specs=group_specs) + + +def share_group_type_get(request, share_group_type): + return manilaclient(request).share_group_types.get(share_group_type) + + +def share_group_type_list(request, show_all=True): + return manilaclient(request).share_group_types.list(show_all=show_all) + + +def share_group_type_delete(request, share_group_type): + return manilaclient(request).share_group_types.delete(share_group_type) + + +def share_group_type_access_list(request, share_group_type): + return manilaclient(request).share_group_type_access.list(share_group_type) + + +def share_group_type_access_add(request, share_group_type, project): + return manilaclient(request).share_group_type_access.add_project_access( + share_group_type, project) + + +def share_group_type_access_remove(request, share_group_type, project): + return manilaclient(request).share_group_type_access.remove_project_access( + share_group_type, project) + + +def share_group_type_set_specs(request, share_group_type, group_specs): + return manilaclient(request).share_group_types.get( + share_group_type).set_keys(group_specs) + + +def share_group_type_unset_specs(request, share_group_type, keys): + return manilaclient(request).share_group_types.get( + share_group_type).unset_keys(keys) + + +def share_group_type_get_specs(request, share_group_type): + return manilaclient(request).share_group_types.get( + share_group_type).get_keys() diff --git a/manila_ui/dashboards/admin/share_group_snapshots/__init__.py b/manila_ui/dashboards/admin/share_group_snapshots/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/manila_ui/dashboards/admin/share_group_snapshots/forms.py b/manila_ui/dashboards/admin/share_group_snapshots/forms.py new file mode 100644 index 00000000..a39dab83 --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_snapshots/forms.py @@ -0,0 +1,55 @@ +# Copyright (c) 2017 Mirantis, Inc. +# 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.utils.translation import ugettext_lazy as _ +from horizon import exceptions +from horizon import forms +from horizon import messages + +from manila_ui.api import manila + + +class ResetShareGroupSnapshotStatusForm(forms.SelfHandlingForm): + status = forms.ChoiceField( + label=_("Status"), + required=True, + choices=( + ('available', 'available'), + ('error', 'error'), + ) + ) + + def handle(self, request, data): + s_id = self.initial['share_group_snapshot_id'] + try: + manila.share_group_snapshot_reset_state( + request, s_id, data["status"]) + message = _( + "Reseting share group snapshot ('%(id)s') status " + "from '%(from)s' to '%(to)s'.") % { + "id": self.initial['share_group_snapshot_name'] or s_id, + "from": self.initial['share_group_snapshot_status'], + "to": data["status"]} + messages.success(request, message) + return True + except Exception: + redirect = reverse("horizon:admin:share_group_snapshots:index") + exceptions.handle( + request, + _("Unable to reset status of share group snapshot " + "'%s'.") % s_id, + redirect=redirect) + return False diff --git a/manila_ui/dashboards/admin/share_group_snapshots/panel.py b/manila_ui/dashboards/admin/share_group_snapshots/panel.py new file mode 100644 index 00000000..5455b5f0 --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_snapshots/panel.py @@ -0,0 +1,29 @@ +# Copyright 2017 Mirantis, Inc. +# 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 +from openstack_dashboard.dashboards.admin import dashboard + + +class ShareGroupSnapshots(horizon.Panel): + name = _("Share Group Snapshots") + slug = 'share_group_snapshots' + permissions = ( + 'openstack.services.share', + ) + + +dashboard.Admin.register(ShareGroupSnapshots) diff --git a/manila_ui/dashboards/admin/share_group_snapshots/tables.py b/manila_ui/dashboards/admin/share_group_snapshots/tables.py new file mode 100644 index 00000000..8e630513 --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_snapshots/tables.py @@ -0,0 +1,142 @@ +# Copyright 2017 Mirantis, Inc. +# 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.template.defaultfilters import title # noqa +from django.utils.translation import pgettext_lazy +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext_lazy +from horizon import exceptions +from horizon import tables +from horizon.utils import filters + +from manila_ui.api import manila + + +class ShareGroupSnapshotShareGroupNameColumn(tables.Column): + def get_link_url(self, snapshot): + return reverse(self.link, args=(snapshot.share_group_id,)) + + +class DeleteShareGroupSnapshot(tables.DeleteAction): + + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete Share Group Snapshot", + u"Delete Share Group Snapshots", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Deleted Share Group Snapshot", + u"Deleted Share Group Snapshots", + count + ) + + def delete(self, request, obj_id): + obj = self.table.get_object_by_id(obj_id) + name = self.table.get_object_display(obj) + try: + manila.share_group_snapshot_delete(request, obj_id) + except Exception: + msg = _('Unable to delete share group snapshot "%s". ' + 'One or more share groups depend on it.') + exceptions.check_message(["snapshots", "dependent"], msg % name) + raise + + def allowed(self, request, snapshot=None): + if snapshot: + return snapshot.status.lower() in ('available', 'error') + return True + + +class ResetShareGroupSnapshotStatus(tables.LinkAction): + name = "reset_share_group_snapshot_status" + verbose_name = _("Reset status") + url = "horizon:admin:share_group_snapshots:reset_status" + classes = ("ajax-modal", "btn-create") + + def allowed(self, request, share_group=None): + return True + + +class UpdateShareGroupSnapshotRow(tables.Row): + ajax = True + + def get_data(self, request, share_group_snapshot_id): + snapshot = manila.share_group_snapshot_get( + request, share_group_snapshot_id) + if not snapshot.name: + snapshot.name = share_group_snapshot_id + return snapshot + + +class ShareGroupSnapshotsTable(tables.DataTable): + STATUS_CHOICES = ( + ("available", True), + ("creating", None), + ("error", False), + ) + STATUS_DISPLAY_CHOICES = ( + ("available", + pgettext_lazy("Current status of snapshot", u"Available")), + ("creating", pgettext_lazy("Current status of snapshot", u"Creating")), + ("error", pgettext_lazy("Current status of snapshot", u"Error")), + ) + name = tables.WrappingColumn( + "name", verbose_name=_("Name"), + link="horizon:admin:share_group_snapshots:detail") + description = tables.Column( + "description", + verbose_name=_("Description"), + truncate=40) + created_at = tables.Column( + "created_at", + verbose_name=_("Created at"), + filters=( + filters.parse_isotime, + )) + status = tables.Column( + "status", + filters=(title,), + verbose_name=_("Status"), + status=True, + status_choices=STATUS_CHOICES, + display_choices=STATUS_DISPLAY_CHOICES) + source = ShareGroupSnapshotShareGroupNameColumn( + "share_group", + verbose_name=_("Source"), + link="horizon:admin:share_groups:detail") + + def get_object_display(self, obj): + return obj.name + + class Meta(object): + name = "share_group_snapshots" + verbose_name = _("Share Group Snapshots") + status_columns = ["status"] + row_class = UpdateShareGroupSnapshotRow + table_actions = ( + tables.NameFilterAction, + DeleteShareGroupSnapshot, + ) + row_actions = ( + ResetShareGroupSnapshotStatus, + DeleteShareGroupSnapshot, + ) diff --git a/manila_ui/dashboards/admin/share_group_snapshots/tabs.py b/manila_ui/dashboards/admin/share_group_snapshots/tabs.py new file mode 100644 index 00000000..428de6d4 --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_snapshots/tabs.py @@ -0,0 +1,34 @@ +# Copyright 2017 Mirantis, Inc. +# 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 tabs + + +class ShareGroupSnapshotOverviewTab(tabs.Tab): + name = _("Share Group Snapshot Overview") + slug = "share_group_snapshot_overview_tab" + template_name = "admin/share_group_snapshots/_detail.html" + + def get_context_data(self, request): + return {"share_group_snapshot": self.tab_group.kwargs[ + "share_group_snapshot"]} + + +class ShareGroupSnapshotDetailTabs(tabs.TabGroup): + slug = "share_group_snapshot_details" + tabs = ( + ShareGroupSnapshotOverviewTab, + ) diff --git a/manila_ui/dashboards/admin/share_group_snapshots/templates/share_group_snapshots/_detail.html b/manila_ui/dashboards/admin/share_group_snapshots/templates/share_group_snapshots/_detail.html new file mode 100644 index 00000000..a3d82982 --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_snapshots/templates/share_group_snapshots/_detail.html @@ -0,0 +1,23 @@ +{% load i18n sizeformat parse_date %} + +

{% trans "Share Group Snapshot Overview" %}

+
+
+
+
{% trans "Name" %}
+
{{ share_group_snapshot.name }}
+
{% trans "ID" %}
+
{{ share_group_snapshot.id }}
+ {% url 'horizon:admin:share_groups:detail' share_group_snapshot.share_group_id as sg_url %} +
{% trans "Source Share Group" %}
+
{{ share_group_snapshot.sg_name_or_id }}
+ {% if share_group_snapshot.description %} +
{% trans "Description" %}
+
{{ share_group_snapshot.description }}
+ {% endif %} +
{% trans "Status" %}
+
{{ share_group_snapshot.status|capfirst }}
+
{% trans "Created" %}
+
{{ share_group_snapshot.created_at|parse_date }}
+
+
diff --git a/manila_ui/dashboards/admin/share_group_snapshots/templates/share_group_snapshots/_reset_status.html b/manila_ui/dashboards/admin/share_group_snapshots/templates/share_group_snapshots/_reset_status.html new file mode 100644 index 00000000..d7bfbcb6 --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_snapshots/templates/share_group_snapshots/_reset_status.html @@ -0,0 +1,5 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% block modal-body-right %} +

"Reset status of '{{ share_group_snapshot_name }}' share group snapshot."

+{% endblock %} diff --git a/manila_ui/dashboards/admin/share_group_snapshots/templates/share_group_snapshots/detail.html b/manila_ui/dashboards/admin/share_group_snapshots/templates/share_group_snapshots/detail.html new file mode 100644 index 00000000..577d367c --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_snapshots/templates/share_group_snapshots/detail.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Share Group Snapshot Details" %}{% endblock %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} diff --git a/manila_ui/dashboards/admin/share_group_snapshots/templates/share_group_snapshots/index.html b/manila_ui/dashboards/admin/share_group_snapshots/templates/share_group_snapshots/index.html new file mode 100644 index 00000000..840225b0 --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_snapshots/templates/share_group_snapshots/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Share Group Snapshots" %}{% endblock %} + +{% block main %} +
+
+ {{ share_group_snapshots_table.render }} +
+
+{% endblock %} diff --git a/manila_ui/dashboards/admin/share_group_snapshots/templates/share_group_snapshots/reset_status.html b/manila_ui/dashboards/admin/share_group_snapshots/templates/share_group_snapshots/reset_status.html new file mode 100644 index 00000000..b7e8f850 --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_snapshots/templates/share_group_snapshots/reset_status.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Reset Share Group Snapshot Status" %}{% endblock %} + +{% block main %} + {% include 'admin/share_group_snapshots/_reset_status.html' %} +{% endblock %} diff --git a/manila_ui/dashboards/admin/share_group_snapshots/urls.py b/manila_ui/dashboards/admin/share_group_snapshots/urls.py new file mode 100644 index 00000000..6eadb9f3 --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_snapshots/urls.py @@ -0,0 +1,36 @@ +# Copyright 2017 Mirantis, Inc. +# 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 import urls + +from manila_ui.dashboards.admin.share_group_snapshots import views +from manila_ui import features + + +if features.is_share_groups_enabled(): + urlpatterns = [ + urls.url( + r'^$', + views.ShareGroupSnapshotsView.as_view(), + name='index'), + urls.url( + r'^(?P[^/]+)/$', + views.ShareGroupSnapshotDetailView.as_view(), + name='detail'), + urls.url( + r'^(?P[^/]+)/reset_status$', + views.ResetShareGroupSnapshotStatusView.as_view(), + name='reset_status'), + ] diff --git a/manila_ui/dashboards/admin/share_group_snapshots/views.py b/manila_ui/dashboards/admin/share_group_snapshots/views.py new file mode 100644 index 00000000..11132e87 --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_snapshots/views.py @@ -0,0 +1,137 @@ +# Copyright 2017 Mirantis, Inc. +# 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. + +""" +Admin views for managing share group snapshots. +""" + +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 import tabs +from horizon.utils import memoized + +from manila_ui.api import manila +import manila_ui.dashboards.admin.share_group_snapshots.forms as sgs_forms +import manila_ui.dashboards.admin.share_group_snapshots.tables as sgs_tables +import manila_ui.dashboards.admin.share_group_snapshots.tabs as sgs_tabs + + +class ShareGroupSnapshotsView(tables.MultiTableView): + table_classes = ( + sgs_tables.ShareGroupSnapshotsTable, + ) + template_name = "admin/share_group_snapshots/index.html" + page_title = _("Share Group Snapshots") + + @memoized.memoized_method + def get_share_group_snapshots_data(self): + share_group_snapshots = [] + try: + share_group_snapshots = manila.share_group_snapshot_list( + self.request, search_opts={'all_tenants': True}) + share_groups = manila.share_group_list(self.request) + sg_names = dict([(sg.id, sg.name or sg.id) for sg in share_groups]) + for snapshot in share_group_snapshots: + snapshot.share_group = sg_names.get(snapshot.share_group_id) + except Exception: + msg = _("Unable to retrieve share group snapshot list.") + exceptions.handle(self.request, msg) + return share_group_snapshots + + +class ShareGroupSnapshotDetailView(tabs.TabView): + tab_group_class = sgs_tabs.ShareGroupSnapshotDetailTabs + template_name = "admin/share_group_snapshots/detail.html" + redirect_url = reverse_lazy("horizon:admin:share_group_snapshots:index") + + def get_context_data(self, **kwargs): + context = super(ShareGroupSnapshotDetailView, self).get_context_data( + **kwargs) + snapshot = self.get_data() + snapshot_display_name = snapshot.name or snapshot.id + context["snapshot"] = snapshot + context["snapshot_display_name"] = snapshot_display_name + context["page_title"] = _( + "Share Group Snapshot Details: %(sgs_display_name)s") % ( + {'sgs_display_name': snapshot_display_name}) + return context + + @memoized.memoized_method + def get_data(self): + try: + share_group_snapshot = manila.share_group_snapshot_get( + self.request, self.kwargs['share_group_snapshot_id']) + sg = manila.share_group_get( + self.request, share_group_snapshot.share_group_id) + share_group_snapshot.sg_name_or_id = sg.name or sg.id + except Exception: + exceptions.handle( + self.request, + _('Unable to retrieve share group snapshot details.'), + redirect=self.redirect_url) + return share_group_snapshot + + def get_tabs(self, request, *args, **kwargs): + return self.tab_group_class( + request, share_group_snapshot=self.get_data(), **kwargs) + + +class ResetShareGroupSnapshotStatusView(forms.ModalFormView): + form_class = sgs_forms.ResetShareGroupSnapshotStatusForm + form_id = "reset_share_group_snapshot_status" + template_name = 'admin/share_group_snapshots/reset_status.html' + modal_header = _("Reset Status") + modal_id = "reset_share_group_snapshot_status_modal" + submit_label = _("Reset status") + submit_url = "horizon:admin:share_group_snapshots:reset_status" + success_url = reverse_lazy("horizon:admin:share_group_snapshots:index") + page_title = _("Reset Share Group Snapshot Status") + + def get_object(self): + if not hasattr(self, "_object"): + s_id = self.kwargs["share_group_snapshot_id"] + try: + self._object = manila.share_group_snapshot_get( + self.request, s_id) + except Exception: + msg = _("Unable to retrieve share group snapshot '%s'.") % s_id + url = reverse('horizon:admin:share_group_snapshots:index') + exceptions.handle(self.request, msg, redirect=url) + return self._object + + def get_context_data(self, **kwargs): + context = super(self.__class__, self).get_context_data(**kwargs) + sgs = self.get_object() + context['share_group_snapshot_id'] = self.kwargs[ + 'share_group_snapshot_id'] + context['share_group_snapshot_name'] = sgs.name or sgs.id + context['share_group_snapshot_status'] = sgs.status + context['share_group_id'] = sgs.share_group_id + context['submit_url'] = reverse( + self.submit_url, args=(context['share_group_snapshot_id'], )) + return context + + def get_initial(self): + sgs = self.get_object() + return { + "share_group_snapshot_id": self.kwargs["share_group_snapshot_id"], + "share_group_snapshot_name": sgs.name or sgs.id, + "share_group_snapshot_status": sgs.status, + "share_group_id": sgs.share_group_id, + } diff --git a/manila_ui/dashboards/admin/share_group_types/__init__.py b/manila_ui/dashboards/admin/share_group_types/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/manila_ui/dashboards/admin/share_group_types/forms.py b/manila_ui/dashboards/admin/share_group_types/forms.py new file mode 100644 index 00000000..96ff9ff7 --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_types/forms.py @@ -0,0 +1,141 @@ +# Copyright 2017 Mirantis, Inc. +# 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 import settings +from django.forms import ValidationError # noqa +from django.utils.translation import ugettext_lazy as _ +from horizon import exceptions +from horizon import forms +from horizon import messages + +from manila_ui.api import manila +from manila_ui.dashboards import utils + + +SGT_GROUP_SPECS_FORM_ATTRS = { + "rows": 5, + "cols": 40, + "style": "height: 135px; width: 100%;", # in case 'rows' not picked up +} + + +class CreateShareGroupTypeForm(forms.SelfHandlingForm): + name = forms.CharField(max_length="255", label=_("Name"), required=True) + group_specs = forms.CharField( + required=False, label=_("Group specs"), + widget=forms.widgets.Textarea(attrs=SGT_GROUP_SPECS_FORM_ATTRS)) + share_types = forms.MultipleChoiceField( + label=_("Share Types"), + required=True, + widget=forms.SelectMultiple(attrs={ + "style": "height: 155px;", + }), + error_messages={ + 'required': _("At least one share type must be specified.") + }) + is_public = forms.BooleanField( + label=_("Public"), required=False, initial=True, + help_text=("Defines whether this share group type is available for " + "all or not. List of allowed tenants should be set " + "separately.")) + + def __init__(self, request, *args, **kwargs): + super(self.__class__, self).__init__(request, *args, **kwargs) + manila_features = getattr(settings, 'OPENSTACK_MANILA_FEATURES', {}) + self.enable_public_share_group_type_creation = manila_features.get( + 'enable_public_share_group_type_creation', True) + if not self.enable_public_share_group_type_creation: + self.fields.pop('is_public') + share_type_choices = manila.share_type_list(request) + self.fields['share_types'].choices = [ + (choice.id, choice.name) for choice in share_type_choices] + + def clean(self): + cleaned_data = super(CreateShareGroupTypeForm, self).clean() + return cleaned_data + + def handle(self, request, data): + try: + set_dict, unset_list = utils.parse_str_meta(data['group_specs']) + if unset_list: + msg = _("Expected only pairs of key=value.") + raise ValidationError(message=msg) + + is_public = ( + self.enable_public_share_group_type_creation and + data["is_public"]) + share_group_type = manila.share_group_type_create( + request, data["name"], share_types=data['share_types'], + is_public=is_public) + if set_dict: + manila.share_group_type_set_specs( + request, share_group_type.id, set_dict) + + msg = _("Successfully created share group type: " + "%s") % share_group_type.name + messages.success(request, msg) + return True + except ValidationError as e: + # handle error without losing dialog + self.api_error(e.messages[0]) + return False + except Exception: + exceptions.handle(request, _('Unable to create share group type.')) + return False + + +class UpdateShareGroupTypeForm(forms.SelfHandlingForm): + + def __init__(self, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + # NOTE(vponomaryov): parse existing group specs + # to str view for textarea html element + es_str = "" + for k, v in self.initial["group_specs"].items(): + es_str += "%s=%s\r\n" % (k, v) + self.initial["group_specs"] = es_str + + group_specs = forms.CharField( + required=False, label=_("Group specs"), + widget=forms.widgets.Textarea(attrs=SGT_GROUP_SPECS_FORM_ATTRS)) + + def handle(self, request, data): + try: + set_dict, unset_list = utils.parse_str_meta(data['group_specs']) + if set_dict: + manila.share_group_type_set_specs( + request, self.initial["id"], set_dict) + if unset_list: + get = manila.share_group_type_get_specs( + request, self.initial["id"]) + + # NOTE(vponomaryov): skip keys that are already unset + to_unset = set(unset_list).intersection(set(get.keys())) + if to_unset: + manila.share_group_type_unset_specs( + request, self.initial["id"], to_unset) + msg = _( + "Successfully updated group specs for share group type '%s'.") + msg = msg % self.initial['name'] + messages.success(request, msg) + return True + except ValidationError as e: + # handle error without losing dialog + self.api_error(e.messages[0]) + return False + except Exception: + msg = _("Unable to update group_specs for share group type.") + exceptions.handle(request, msg) + return False diff --git a/manila_ui/dashboards/admin/share_group_types/panel.py b/manila_ui/dashboards/admin/share_group_types/panel.py new file mode 100644 index 00000000..2c4957d7 --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_types/panel.py @@ -0,0 +1,29 @@ +# Copyright 2017 Mirantis, Inc. +# 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 +from openstack_dashboard.dashboards.admin import dashboard + + +class ShareGroupTypes(horizon.Panel): + name = _("Share Group Types") + slug = 'share_group_types' + permissions = ( + 'openstack.services.share', + ) + + +dashboard.Admin.register(ShareGroupTypes) diff --git a/manila_ui/dashboards/admin/share_group_types/tables.py b/manila_ui/dashboards/admin/share_group_types/tables.py new file mode 100644 index 00000000..d608f335 --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_types/tables.py @@ -0,0 +1,111 @@ +# Copyright 2017 Mirantis, Inc. +# 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 django.utils.translation import ungettext_lazy +from horizon import tables +import six + +from manila_ui.api import manila + + +class CreateShareGroupType(tables.LinkAction): + name = "create" + verbose_name = _("Create Share Group Type") + url = "horizon:admin:share_group_types:create" + classes = ("ajax-modal", "btn-create") + icon = "plus" + + +class DeleteShareGroupType(tables.DeleteAction): + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete Share Group Type", + u"Delete Share Group Types", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Deleted Share Group Type", + u"Deleted Share Group Types", + count + ) + + def delete(self, request, obj_id): + manila.share_group_type_delete(request, obj_id) + + +class ManageShareGroupTypeAccess(tables.LinkAction): + name = "manage" + verbose_name = _("Manage Share Group Type Access") + url = "horizon:admin:share_group_types:manage_access" + classes = ("ajax-modal", "btn-create") + + def allowed(self, request, obj_id): + sgt = manila.share_group_type_get(request, obj_id) + # Enable it only for private share group types + return not sgt.is_public + + def get_policy_target(self, request, datum=None): + project_id = None + if datum: + project_id = getattr(datum, "os-share-tenant-attr:tenant_id", None) + return {"project_id": project_id} + + +class UpdateShareGroupType(tables.LinkAction): + name = "update share group type" + verbose_name = _("Update Share group Type") + url = "horizon:admin:share_group_types:update" + classes = ("ajax-modal", "btn-create") + + def get_policy_target(self, request, datum=None): + project_id = None + if datum: + project_id = getattr(datum, "os-share-tenant-attr:tenant_id", None) + return {"project_id": project_id} + + +class ShareGroupTypesTable(tables.DataTable): + name = tables.WrappingColumn("name", verbose_name=_("Name")) + group_specs = tables.Column("group_specs", verbose_name=_("Group specs")) + share_types = tables.Column("share_types", verbose_name=_("Share types")) + visibility = tables.Column( + "is_public", verbose_name=_("Visibility"), + filters=(lambda d: 'public' if d is True else 'private', ), + ) + + def get_object_display(self, share_group_type): + return share_group_type.name + + def get_object_id(self, share_group_type): + return six.text_type(share_group_type.id) + + class Meta(object): + name = "share_group_types" + verbose_name = _("Share Group Types") + table_actions = ( + tables.NameFilterAction, + CreateShareGroupType, + DeleteShareGroupType, + ) + row_actions = ( + UpdateShareGroupType, + ManageShareGroupTypeAccess, + DeleteShareGroupType, + ) diff --git a/manila_ui/dashboards/admin/share_group_types/templates/share_group_types/_create.html b/manila_ui/dashboards/admin/share_group_types/templates/share_group_types/_create.html new file mode 100644 index 00000000..1a793859 --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_types/templates/share_group_types/_create.html @@ -0,0 +1,11 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% block modal-body-right %} +

{% trans "Description" %}:

+

{% trans "The share group type defines the characteristics of a share group backend." %} +

Extra specs field:
+ {% trans "One line - one action. Empty strings will be ignored." %}
+ {% trans "To add group-specs use:" %} +

key=value
+

+{% endblock %} diff --git a/manila_ui/dashboards/admin/share_group_types/templates/share_group_types/_manage_access.html b/manila_ui/dashboards/admin/share_group_types/templates/share_group_types/_manage_access.html new file mode 100644 index 00000000..8d2734fa --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_types/templates/share_group_types/_manage_access.html @@ -0,0 +1,59 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_id %}{% endblock %} +{% block form_action %}{% url 'horizon:admin:share_group_types:manage_access' share_group_type.id %}{% endblock %} + +{% block modal_id %}manage_share_group_type_access_modal{% endblock %} +{% block modal-header %}{% trans "Manage Share Group Type Access" %}{% endblock %} + + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description" %}:

+

{% blocktrans %} + Placeholder for description of share group type access managing form. + {% endblocktrans %}

+
+ + + + + + +
+ +
    +
+ +
    +
+
+ + + + + + + + +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+ {{ step.get_help_text }} +
+ + +{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/manila_ui/dashboards/admin/share_group_types/templates/share_group_types/_update.html b/manila_ui/dashboards/admin/share_group_types/templates/share_group_types/_update.html new file mode 100644 index 00000000..08326d4f --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_types/templates/share_group_types/_update.html @@ -0,0 +1,14 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% block modal-body-right %} +

{% trans "Description" %}:

+

+ {% trans "Here can be modified group-specs for share group type." %}
+ {% trans "One line - one action. Empty strings will be ignored." %}
+ {% trans "To add group-specs use:" %} +

key=value
+ {% trans "To unset group-specs use:" %} +
key
+ {% trans "All pairs that are in field for left are set for this share group type." %} +

+{% endblock %} diff --git a/manila_ui/dashboards/admin/share_group_types/templates/share_group_types/create.html b/manila_ui/dashboards/admin/share_group_types/templates/share_group_types/create.html new file mode 100644 index 00000000..0ca18056 --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_types/templates/share_group_types/create.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Group Share Type" %}{% endblock %} + +{% block main %} + {% include 'admin/share_group_types/_create.html' %} +{% endblock %} diff --git a/manila_ui/dashboards/admin/share_group_types/templates/share_group_types/index.html b/manila_ui/dashboards/admin/share_group_types/templates/share_group_types/index.html new file mode 100644 index 00000000..f5d6d877 --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_types/templates/share_group_types/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Share Group Types" %}{% endblock %} + +{% block main %} +
+
+ {{ share_group_types_table.render }} +
+
+{% endblock %} diff --git a/manila_ui/dashboards/admin/share_group_types/templates/share_group_types/manage_access.html b/manila_ui/dashboards/admin/share_group_types/templates/share_group_types/manage_access.html new file mode 100644 index 00000000..7c98472f --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_types/templates/share_group_types/manage_access.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Manage Share Group Type Access" %}{% endblock %} + +{% block main %} + {% include 'admin/share_group_types/_manage_access.html' %} +{% endblock %} diff --git a/manila_ui/dashboards/admin/share_group_types/templates/share_group_types/update.html b/manila_ui/dashboards/admin/share_group_types/templates/share_group_types/update.html new file mode 100644 index 00000000..a8ea56b1 --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_types/templates/share_group_types/update.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update Share Group Type" %}{% endblock %} + +{% block main %} + {% include 'admin/share_group_types/_update.html' %} +{% endblock %} diff --git a/manila_ui/dashboards/admin/share_group_types/urls.py b/manila_ui/dashboards/admin/share_group_types/urls.py new file mode 100644 index 00000000..563a2c6b --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_types/urls.py @@ -0,0 +1,40 @@ +# Copyright 2017 Mirantis, Inc. +# 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 import urls + +from manila_ui.dashboards.admin.share_group_types import views +from manila_ui import features + + +if features.is_share_groups_enabled(): + urlpatterns = [ + urls.url( + r'^$', + views.ShareGroupTypesView.as_view(), + name='index'), + urls.url( + r'^create$', + views.CreateShareGroupTypeView.as_view(), + name='create'), + urls.url( + r'^(?P[^/]+)/update$', + views.UpdateShareGroupTypeView.as_view(), + name='update'), + urls.url( + r'^(?P[^/]+)/manage_access$', + views.ManageShareGroupTypeAccessView.as_view(), + name='manage_access'), + ] diff --git a/manila_ui/dashboards/admin/share_group_types/views.py b/manila_ui/dashboards/admin/share_group_types/views.py new file mode 100644 index 00000000..d12ac2f8 --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_types/views.py @@ -0,0 +1,130 @@ +# Copyright 2017 Mirantis, Inc. +# 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. + +""" +Admin views for managing share group types. +""" + +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 workflows + +from manila_ui.api import manila +from manila_ui.dashboards.admin.share_group_types import forms as sgt_forms +from manila_ui.dashboards.admin.share_group_types import tables as sgt_tables +import manila_ui.dashboards.admin.share_group_types.workflows as sgt_workflows +from manila_ui.dashboards import utils as common_utils + + +class ShareGroupTypesView(tables.MultiTableView): + table_classes = ( + sgt_tables.ShareGroupTypesTable, + ) + template_name = "admin/share_group_types/index.html" + page_title = _("Share Group Types") + + @memoized.memoized_method + def get_share_group_types_data(self): + try: + share_group_types = manila.share_group_type_list(self.request) + except Exception: + exceptions.handle( + self.request, _('Unable to retrieve share group types.')) + return [] + + share_types = manila.share_type_list(self.request) + st_mapping = {} + for st in share_types: + st_mapping[st.id] = st.name + for sgt in share_group_types: + sgt.group_specs = common_utils.metadata_to_str( + sgt.group_specs, 8, 45) + sgt.share_types = ", ".join( + [st_mapping[st] for st in sgt.share_types]) + return share_group_types + + +class CreateShareGroupTypeView(forms.ModalFormView): + form_class = sgt_forms.CreateShareGroupTypeForm + form_id = "create_share_group_type" + template_name = 'admin/share_group_types/create.html' + modal_header = _("Create Share Group Type") + modal_id = "create_share_group_type_modal" + submit_label = _("Create") + submit_url = reverse_lazy("horizon:admin:share_group_types:create") + success_url = reverse_lazy("horizon:admin:share_group_types:index") + page_title = _("Create Share Group Type") + + +class ManageShareGroupTypeAccessView(workflows.WorkflowView): + workflow_class = sgt_workflows.ManageShareGroupTypeAccessWorkflow + template_name = "admin/share_group_types/manage_access.html" + success_url = 'horizon:admin:share_group_types:index' + page_title = _("Manage Share Group Type Access") + + def get_initial(self): + return {'id': self.kwargs["share_group_type_id"]} + + def get_context_data(self, **kwargs): + context = super(ManageShareGroupTypeAccessView, self).get_context_data( + **kwargs) + context['id'] = self.kwargs['share_group_type_id'] + return context + + +class UpdateShareGroupTypeView(forms.ModalFormView): + form_class = sgt_forms.UpdateShareGroupTypeForm + form_id = "update_share_group_type" + template_name = "admin/share_group_types/update.html" + modal_header = _("Update Share Group Type") + modal_id = "update_share_group_type_modal" + submit_label = _("Update") + submit_url = "horizon:admin:share_group_types:update" + success_url = reverse_lazy("horizon:admin:share_group_types:index") + page_title = _("Update Share Group Type") + + def get_object(self): + if not hasattr(self, "_object"): + sgt_id = self.kwargs["share_group_type_id"] + try: + self._object = manila.share_group_type_get( + self.request, sgt_id) + except Exception: + msg = _("Unable to retrieve share_gruop_type.") + url = reverse("horizon:admin:share_group_types:index") + exceptions.handle(self.request, msg, redirect=url) + return self._object + + def get_context_data(self, **kwargs): + context = super(UpdateShareGroupTypeView, 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): + share_group_type = self.get_object() + return { + "id": self.kwargs["share_group_type_id"], + "name": share_group_type.name, + "group_specs": share_group_type.group_specs, + } diff --git a/manila_ui/dashboards/admin/share_group_types/workflows.py b/manila_ui/dashboards/admin/share_group_types/workflows.py new file mode 100644 index 00000000..68bd1fa6 --- /dev/null +++ b/manila_ui/dashboards/admin/share_group_types/workflows.py @@ -0,0 +1,115 @@ +# Copyright 2017 Mirantis, Inc. +# 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.api import keystone + +from manila_ui.api import manila + + +class AddProjectAction(workflows.MembershipAction): + + def __init__(self, request, *args, **kwargs): + super(AddProjectAction, self).__init__(request, *args, **kwargs) + 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) + share_group_type_id = self.initial['id'] + + # Get list of existing projects + try: + projects, __ = keystone.tenant_list(request) + except Exception: + err_msg = _('Unable to get list of projects.') + exceptions.handle(request, err_msg) + + # Get list of projects with access to this Share Group Type + try: + share_group_type = manila.share_group_type_get( + request, share_group_type_id) + self.share_group_type_name = share_group_type.name + projects_initial = manila.share_group_type_access_list( + request, share_group_type) + except Exception: + err_msg = _( + 'Unable to get information about share group type access.') + exceptions.handle(request, err_msg) + + self.fields[field_name].choices = [ + (project.id, project.name or project.id) for project in projects] + self.fields[field_name].initial = [ + pr.project_id for pr in projects_initial] + self.projects_initial = set(self.fields[field_name].initial) + + class Meta(object): + name = _("Projects with access to share group type") + slug = "update_members" + + def handle(self, request, context): + context.update({ + 'name': self.share_group_type_name, + 'projects_add': self.projects_allow - self.projects_initial, + 'projects_remove': self.projects_initial - self.projects_allow, + }) + return context + + def clean(self): + cleaned_data = super(AddProjectAction, self).clean() + self.projects_allow = set( + cleaned_data[self.get_member_field_name('member')]) + return cleaned_data + + +class AddProjectStep(workflows.UpdateMembersStep): + action_class = AddProjectAction + available_list_title = _("Available projects") + help_text = _("Allow project access to share group type.") + members_list_title = _("Selected projects") + no_available_text = _("No projects found.") + no_members_text = _("No projects selected.") + depends_on = ("id", ) + show_roles = False + + +class ManageShareGroupTypeAccessWorkflow(workflows.Workflow): + slug = "manage_share_group_type_access" + name = _("Manage Share Group Type Access") + finalize_button_name = _("Manage Share Group Type Access") + success_message = _('Updated access for share group type "%s".') + failure_message = _('Unable to update access for share group type "%s".') + success_url = 'horizon:admin:share_group_types:index' + default_steps = (AddProjectStep, ) + + def format_status_message(self, message): + return message % self.context['name'] + + def handle(self, request, context): + try: + for project in self.context['projects_remove']: + manila.share_group_type_access_remove( + request, self.context['id'], project) + for project in self.context['projects_add']: + manila.share_group_type_access_add( + request, self.context['id'], project) + return True + except Exception: + exceptions.handle(request, _('Unable to update share group type.')) + return False diff --git a/manila_ui/dashboards/admin/share_groups/__init__.py b/manila_ui/dashboards/admin/share_groups/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/manila_ui/dashboards/admin/share_groups/forms.py b/manila_ui/dashboards/admin/share_groups/forms.py new file mode 100644 index 00000000..78b2b6b9 --- /dev/null +++ b/manila_ui/dashboards/admin/share_groups/forms.py @@ -0,0 +1,54 @@ +# Copyright (c) 2017 Mirantis, Inc. +# 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.utils.translation import ugettext_lazy as _ +from horizon import exceptions +from horizon import forms +from horizon import messages + +from manila_ui.api import manila + + +class ResetShareGroupStatusForm(forms.SelfHandlingForm): + status = forms.ChoiceField( + label=_("Status"), + required=True, + choices=( + ('available', 'available'), + ('error', 'error'), + ) + ) + + def handle(self, request, data): + sg_id = self.initial['share_group_id'] + try: + manila.share_group_reset_state(request, sg_id, data["status"]) + message = _( + "Reseting share group ('%(id)s') status from '%(from)s' " + "to '%(to)s'.") % { + "id": (self.initial['share_group_name'] or + self.initial['share_group_id']), + "from": self.initial['share_group_status'], + "to": data["status"]} + messages.success(request, message) + return True + except Exception: + redirect = reverse("horizon:admin:share_groups:index") + exceptions.handle( + request, + _("Unable to reset status of share group '%s'.") % sg_id, + redirect=redirect) + return False diff --git a/manila_ui/dashboards/admin/share_groups/panel.py b/manila_ui/dashboards/admin/share_groups/panel.py new file mode 100644 index 00000000..cb515b2b --- /dev/null +++ b/manila_ui/dashboards/admin/share_groups/panel.py @@ -0,0 +1,29 @@ +# Copyright 2017 Mirantis, Inc. +# 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 +from openstack_dashboard.dashboards.admin import dashboard + + +class ShareGroups(horizon.Panel): + name = _("Share Groups") + slug = 'share_groups' + permissions = ( + 'openstack.services.share', + ) + + +dashboard.Admin.register(ShareGroups) diff --git a/manila_ui/dashboards/admin/share_groups/tables.py b/manila_ui/dashboards/admin/share_groups/tables.py new file mode 100644 index 00000000..9374bf71 --- /dev/null +++ b/manila_ui/dashboards/admin/share_groups/tables.py @@ -0,0 +1,127 @@ +# Copyright 2017 Mirantis, Inc. +# 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.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext_lazy +from horizon import tables +import six + +from manila_ui.api import manila + + +class DeleteShareGroup(tables.DeleteAction): + + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete Share Group", + u"Delete Share Groups", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Deleted Share Group", + u"Deleted Share Groups", + count + ) + + def delete(self, request, obj_id): + manila.share_group_delete(request, obj_id) + + +class ResetShareGroupStatus(tables.LinkAction): + name = "reset_share_group_status" + verbose_name = _("Reset status") + url = "horizon:admin:share_groups:reset_status" + classes = ("ajax-modal", "btn-create") + + def allowed(self, request, share_group=None): + return True + + +class ShareGroupsTable(tables.DataTable): + def get_share_network_link(share_group): + if getattr(share_group, 'share_network_id', None): + return reverse("horizon:admin:share_networks:share_network_detail", + args=(share_group.share_network_id,)) + else: + return None + + def get_share_server_link(share_group): + if getattr(share_group, 'share_server_id', None): + return reverse("horizon:admin:share_servers:share_server_detail", + args=(share_group.share_server_id,)) + else: + return None + + STATUS_CHOICES = ( + ("available", True), + ("creating", None), + ("deleting", None), + ("error", False), + ("error_deleting", False), + ) + STATUS_DISPLAY_CHOICES = ( + ("available", u"Available"), + ("creating", u"Creating"), + ("deleting", u"Deleting"), + ("error", u"Error"), + ("error_deleting", u"Error deleting"), + ) + name = tables.Column( + "name", + verbose_name=_("Name"), + link="horizon:admin:share_groups:detail") + host = tables.Column("host", verbose_name=_("Host")) + status = tables.Column( + "status", + verbose_name=_("Status"), + status=True, + status_choices=STATUS_CHOICES, + display_choices=STATUS_DISPLAY_CHOICES, + ) + availability_zone = tables.Column( + "availability_zone", + verbose_name=_("Availability Zone")) + share_network_id = tables.Column( + "share_network_id", + verbose_name=_("Share Network"), + link=get_share_network_link) + share_server_id = tables.Column( + "share_server_id", + verbose_name=_("Share Server"), + link=get_share_server_link) + + def get_object_display(self, share_group): + return six.text_type(share_group.id) + + def get_object_id(self, share_group): + return six.text_type(share_group.id) + + class Meta(object): + name = "share_groups" + verbose_name = _("Share Groups") + status_columns = ("status", ) + table_actions = ( + tables.NameFilterAction, + DeleteShareGroup, + ) + row_actions = ( + ResetShareGroupStatus, + DeleteShareGroup, + ) diff --git a/manila_ui/dashboards/admin/share_groups/tabs.py b/manila_ui/dashboards/admin/share_groups/tabs.py new file mode 100644 index 00000000..c584db88 --- /dev/null +++ b/manila_ui/dashboards/admin/share_groups/tabs.py @@ -0,0 +1,33 @@ +# Copyright 2017 Mirantis, Inc. +# 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 tabs + + +class ShareGroupOverviewTab(tabs.Tab): + name = _("Share Group Overview") + slug = "share_group_overview_tab" + template_name = "admin/share_groups/_detail.html" + + def get_context_data(self, request): + return {"share_group": self.tab_group.kwargs["share_group"]} + + +class ShareGroupDetailTabs(tabs.TabGroup): + slug = "share_group_details" + tabs = ( + ShareGroupOverviewTab, + ) diff --git a/manila_ui/dashboards/admin/share_groups/templates/share_groups/_detail.html b/manila_ui/dashboards/admin/share_groups/templates/share_groups/_detail.html new file mode 100644 index 00000000..008dbafe --- /dev/null +++ b/manila_ui/dashboards/admin/share_groups/templates/share_groups/_detail.html @@ -0,0 +1,61 @@ +{% load i18n sizeformat parse_date %} + +

{% trans "Share Group Overview" %}

+
+
+
+
{% trans "ID" %}
+
{{ share_group.id }}
+
{% trans "Name" %}
+
{{ share_group.name }}
+
{% trans "Description" %}
+
{{ share_group.description }}
+
{% trans "Status" %}
+
{{ share_group.status|capfirst }}
+
{% trans "Created" %}
+
{{ share_group.created_at|parse_date }}
+
{% trans "Host" %}
+
{{ share_group.host}}
+ {% if share_group.source_share_group_snapshot_id %} +
{% trans "Source share group snapshot" %}
+
{{ share_group.source_share_group_snapshot_id}}
+ {% endif %} +
{% trans "Share Group Type" %}
+
{{ share_group.share_group_type_id}}
+ {% if share_group.availability_zone %} +
{% trans "Availability Zone" %}
+
{{ share_group.availability_zone }}
+ {% endif %} + {% if share_group.share_network_id %} +
{% trans "Share network" %}
+ {% url 'horizon:admin:share_networks:share_network_detail' share_group.share_network_id as sn_url%} +
{{ share_group.share_network_id }}
+ {% endif %} + {% if share_group.share_server_id %} +
{% trans "Share server" %}
+ {% url 'horizon:admin:share_servers:share_server_detail' share_group.share_server_id as share_server_url%} +
{{ share_group.share_server_id }}
+ {% endif %} + {% if share_group.share_types %} +
{% trans "Share Types" %}
+ {% for st in share_group.share_types %} +

+

Share Type Name: {{ st.name }}
+
Share Type Visibility: {% if st.is_public %}Public{% else %}Private{% endif %}
+
Network handling enabled: {{ st.dhss }}
+

+ {% endfor %} + {% endif %} + {% if share_group.members %} +
{% trans "Shares" %}
+ {% for m in share_group.members %} + {% url 'horizon:admin:shares:detail' m.id as share_url%} +
+ +
+ {% endfor %} + {% endif %} +
+
diff --git a/manila_ui/dashboards/admin/share_groups/templates/share_groups/_reset_status.html b/manila_ui/dashboards/admin/share_groups/templates/share_groups/_reset_status.html new file mode 100644 index 00000000..ad186146 --- /dev/null +++ b/manila_ui/dashboards/admin/share_groups/templates/share_groups/_reset_status.html @@ -0,0 +1,5 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% block modal-body-right %} +

"Reset status of '{{ share_group_name }}' share group."

+{% endblock %} diff --git a/manila_ui/dashboards/admin/share_groups/templates/share_groups/detail.html b/manila_ui/dashboards/admin/share_groups/templates/share_groups/detail.html new file mode 100644 index 00000000..95114d91 --- /dev/null +++ b/manila_ui/dashboards/admin/share_groups/templates/share_groups/detail.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Share Group Details" %}{% endblock %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} diff --git a/manila_ui/dashboards/admin/share_groups/templates/share_groups/index.html b/manila_ui/dashboards/admin/share_groups/templates/share_groups/index.html new file mode 100644 index 00000000..93a16df5 --- /dev/null +++ b/manila_ui/dashboards/admin/share_groups/templates/share_groups/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Share Groups" %}{% endblock %} + +{% block main %} +
+
+ {{ share_groups_table.render }} +
+
+{% endblock %} diff --git a/manila_ui/dashboards/admin/share_groups/templates/share_groups/reset_status.html b/manila_ui/dashboards/admin/share_groups/templates/share_groups/reset_status.html new file mode 100644 index 00000000..f3529e7a --- /dev/null +++ b/manila_ui/dashboards/admin/share_groups/templates/share_groups/reset_status.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Reset Share Group Status" %}{% endblock %} + +{% block main %} + {% include 'admin/share_groups/_reset_status.html' %} +{% endblock %} diff --git a/manila_ui/dashboards/admin/share_groups/urls.py b/manila_ui/dashboards/admin/share_groups/urls.py new file mode 100644 index 00000000..ddc23897 --- /dev/null +++ b/manila_ui/dashboards/admin/share_groups/urls.py @@ -0,0 +1,36 @@ +# Copyright 2017 Mirantis Inc. +# 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 import urls + +from manila_ui.dashboards.admin.share_groups import views +from manila_ui import features + + +if features.is_share_groups_enabled(): + urlpatterns = [ + urls.url( + r'^$', + views.ShareGroupsView.as_view(), + name='index'), + urls.url( + r'^(?P[^/]+)/$', + views.ShareGroupDetailView.as_view(), + name='detail'), + urls.url( + r'^(?P[^/]+)/reset_status$', + views.ResetShareGroupStatusView.as_view(), + name='reset_status'), + ] diff --git a/manila_ui/dashboards/admin/share_groups/views.py b/manila_ui/dashboards/admin/share_groups/views.py new file mode 100644 index 00000000..b0ef705f --- /dev/null +++ b/manila_ui/dashboards/admin/share_groups/views.py @@ -0,0 +1,133 @@ +# Copyright 2017 Mirantis, Inc. +# 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. + +""" +Admin views for managing share groups. +""" + +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 import tabs +from horizon.utils import memoized + +from manila_ui.api import manila +from manila_ui.dashboards.admin.share_groups import forms as sg_forms +from manila_ui.dashboards.admin.share_groups import tables as sg_tables +from manila_ui.dashboards.admin.share_groups import tabs as sg_tabs + + +class ShareGroupsView(tables.MultiTableView): + table_classes = ( + sg_tables.ShareGroupsTable, + ) + template_name = "admin/share_groups/index.html" + page_title = _("Share Groups") + + @memoized.memoized_method + def get_share_groups_data(self): + try: + share_groups = manila.share_group_list( + self.request, detailed=True) + except Exception: + share_groups = [] + exceptions.handle( + self.request, _("Unable to retrieve share groups.")) + return share_groups + + +class ShareGroupDetailView(tabs.TabView): + tab_group_class = sg_tabs.ShareGroupDetailTabs + template_name = 'admin/share_groups/detail.html' + + def get_context_data(self, **kwargs): + context = super(self.__class__, self).get_context_data(**kwargs) + share_group = self.get_data() + context["share_group"] = share_group + context["page_title"] = ( + _("Share Group Details: %s") % share_group.id) + return context + + @memoized.memoized_method + def get_data(self): + try: + share_group_id = self.kwargs['share_group_id'] + share_group = manila.share_group_get(self.request, share_group_id) + members = manila.share_list( + self.request, search_opts={"share_group_id": share_group_id}) + share_group.members = members + share_types = manila.share_type_list(self.request) + share_group.share_types = [ + {"id": st.id, + "name": st.name, + "is_public": getattr(st, 'share_type_access:is_public'), + "dhss": st.extra_specs.get('driver_handles_share_servers')} + for st in share_types if st.id in share_group.share_types + ] + return share_group + except Exception: + redirect = reverse('horizon:admin:share_groups:index') + exceptions.handle( + self.request, + _('Unable to retrieve share group details.'), + redirect=redirect) + + def get_tabs(self, request, *args, **kwargs): + share_group = self.get_data() + return self.tab_group_class(request, share_group=share_group, **kwargs) + + +class ResetShareGroupStatusView(forms.ModalFormView): + form_class = sg_forms.ResetShareGroupStatusForm + form_id = "reset_share_group_status" + template_name = 'admin/share_groups/reset_status.html' + modal_header = _("Reset Status") + modal_id = "reset_share_group_status_modal" + submit_label = _("Reset status") + submit_url = "horizon:admin:share_groups:reset_status" + success_url = reverse_lazy("horizon:admin:share_groups:index") + page_title = _("Reset Share Group Status") + + def get_object(self): + if not hasattr(self, "_object"): + sg_id = self.kwargs["share_group_id"] + try: + self._object = manila.share_group_get(self.request, sg_id) + except Exception: + msg = _("Unable to retrieve share group '%s'.") % sg_id + url = reverse('horizon:admin:share_groups:index') + exceptions.handle(self.request, msg, redirect=url) + return self._object + + def get_context_data(self, **kwargs): + context = super(self.__class__, self).get_context_data(**kwargs) + sg = self.get_object() + context['share_group_id'] = self.kwargs['share_group_id'] + context['share_group_name'] = sg.name or sg.id + context['share_group_status'] = sg.status + context['submit_url'] = reverse( + self.submit_url, args=(context['share_group_id'], )) + return context + + def get_initial(self): + sg = self.get_object() + return { + "share_group_id": self.kwargs["share_group_id"], + "share_group_name": sg.name or sg.id, + "share_group_status": sg.status, + } diff --git a/manila_ui/dashboards/admin/shares/tables.py b/manila_ui/dashboards/admin/shares/tables.py index 32a66570..c40bc32e 100644 --- a/manila_ui/dashboards/admin/shares/tables.py +++ b/manila_ui/dashboards/admin/shares/tables.py @@ -14,8 +14,8 @@ from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ from horizon import tables -from manila_ui.api import manila from manila_ui.dashboards.project.shares import tables as shares_tables +from manila_ui import features class MigrationStartAction(tables.LinkAction): @@ -30,7 +30,7 @@ class MigrationStartAction(tables.LinkAction): if share: return (share.status.upper() == "AVAILABLE" and not getattr(share, 'has_snapshot', False) and - manila.is_migration_enabled()) + features.is_migration_enabled()) return False @@ -44,7 +44,7 @@ class MigrationCompleteAction(tables.LinkAction): def allowed(self, request, share=None): if (share and share.status.upper() == "MIGRATING" and - manila.is_migration_enabled()): + features.is_migration_enabled()): return True return False @@ -59,7 +59,7 @@ class MigrationCancelAction(tables.LinkAction): def allowed(self, request, share=None): if (share and share.status.upper() == "MIGRATING" and - manila.is_migration_enabled()): + features.is_migration_enabled()): return True return False @@ -74,7 +74,7 @@ class MigrationGetProgressAction(tables.LinkAction): def allowed(self, request, share=None): if (share and share.status.upper() == "MIGRATING" and - manila.is_migration_enabled()): + features.is_migration_enabled()): return True return False @@ -115,7 +115,7 @@ class ManageReplicas(tables.LinkAction): def allowed(self, request, share): share_replication_enabled = share.replication_type is not None - return manila.is_replication_enabled() and share_replication_enabled + return features.is_replication_enabled() and share_replication_enabled class SharesTable(shares_tables.SharesTable): @@ -156,7 +156,9 @@ class SharesTable(shares_tables.SharesTable): UnmanageShareAction, shares_tables.DeleteShare, ) - columns = ( + columns = [ 'tenant', 'host', 'name', 'size', 'status', 'visibility', 'share_type', 'protocol', 'share_server', - ) + ] + if features.is_share_groups_enabled(): + columns.append('share_group_id') diff --git a/manila_ui/dashboards/admin/shares/templates/shares/_detail.html b/manila_ui/dashboards/admin/shares/templates/shares/_detail.html index a31ec270..a318a135 100644 --- a/manila_ui/dashboards/admin/shares/templates/shares/_detail.html +++ b/manila_ui/dashboards/admin/shares/templates/shares/_detail.html @@ -57,10 +57,15 @@

{% endif %} {% if share.share_network_id %} -
{% trans "Share network" %}
+
{% trans "Share Network" %}
{% url 'horizon:admin:share_networks:share_network_detail' share.share_network_id as sn_url%}
{{ share.share_network_id }}
{% endif %} + {% if share.share_group_id %} +
{% trans "Share Group" %}
+ {% url 'horizon:admin:share_groups:detail' share.share_group_id as sg_url%} +
{{ share.share_group_id }}
+ {% endif %}
{% trans "Mount snapshot support" %}
{{ share.mount_snapshot_support }}
{% trans "Created" %}
diff --git a/manila_ui/dashboards/admin/shares/urls.py b/manila_ui/dashboards/admin/shares/urls.py index 15f3355f..aedb0977 100644 --- a/manila_ui/dashboards/admin/shares/urls.py +++ b/manila_ui/dashboards/admin/shares/urls.py @@ -12,9 +12,9 @@ from django.conf import urls -from manila_ui.api import manila from manila_ui.dashboards.admin.shares.replicas import views as replica_views from manila_ui.dashboards.admin.shares import views +from manila_ui import features urlpatterns = [ @@ -36,7 +36,7 @@ urlpatterns = [ name='unmanage'), ] -if manila.is_replication_enabled(): +if features.is_replication_enabled(): urlpatterns.extend([ urls.url( r'^(?P[^/]+)/replicas/$', @@ -60,7 +60,7 @@ if manila.is_replication_enabled(): name='reset_replica_state'), ]) -if manila.is_migration_enabled(): +if features.is_migration_enabled(): urlpatterns.extend([ urls.url( r'^migration_start/(?P[^/]+)$', diff --git a/manila_ui/dashboards/project/share_group_snapshots/__init__.py b/manila_ui/dashboards/project/share_group_snapshots/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/manila_ui/dashboards/project/share_group_snapshots/forms.py b/manila_ui/dashboards/project/share_group_snapshots/forms.py new file mode 100644 index 00000000..23a45849 --- /dev/null +++ b/manila_ui/dashboards/project/share_group_snapshots/forms.py @@ -0,0 +1,75 @@ +# Copyright 2017 Mirantis, Inc. +# 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.utils.translation import ugettext_lazy as _ +from horizon import exceptions +from horizon import forms +from horizon import messages + +from manila_ui.api import manila + + +class CreateShareGroupSnapshotForm(forms.SelfHandlingForm): + name = forms.CharField(max_length="255", label=_("Name"), required=True) + description = forms.CharField( + widget=forms.Textarea, + label=_("Description"), + required=False) + + def __init__(self, request, *args, **kwargs): + super(self.__class__, self).__init__(request, *args, **kwargs) + # populate share_group_id + sg_id = kwargs.get('initial', {}).get('share_group_id', []) + self.fields['share_group_id'] = forms.CharField( + widget=forms.HiddenInput(), initial=sg_id) + + def handle(self, request, data): + try: + snapshot = manila.share_group_snapshot_create( + request, + data['share_group_id'], data['name'], data['description']) + message = _('Creating share group snapshot "%s".') % data['name'] + messages.success(request, message) + return snapshot + except Exception: + redirect = reverse("horizon:project:share_group_snapshots:index") + exceptions.handle( + request, + _('Unable to create share group snapshot.'), + redirect=redirect) + return False + + +class UpdateShareGroupSnapshotForm(forms.SelfHandlingForm): + name = forms.CharField( + max_length="255", label=_("Name")) + description = forms.CharField( + widget=forms.Textarea, label=_("Description"), required=False) + + def handle(self, request, data): + sgs_id = self.initial['share_group_snapshot_id'] + try: + manila.share_group_snapshot_update( + request, sgs_id, data['name'], data['description']) + message = _('Updating share group snapshot "%s"') % data['name'] + messages.success(request, message) + return True + except Exception: + redirect = reverse("horizon:project:share_group_snapshots:index") + exceptions.handle( + request, _('Unable to update share group snapshot.'), + redirect=redirect) + return False diff --git a/manila_ui/dashboards/project/share_group_snapshots/panel.py b/manila_ui/dashboards/project/share_group_snapshots/panel.py new file mode 100644 index 00000000..8be79791 --- /dev/null +++ b/manila_ui/dashboards/project/share_group_snapshots/panel.py @@ -0,0 +1,29 @@ +# Copyright 2017 Mirantis, Inc. +# 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 +from openstack_dashboard.dashboards.project import dashboard + + +class ShareGroupSnapshots(horizon.Panel): + name = _("Share Group Snapshots") + slug = 'share_group_snapshots' + permissions = ( + 'openstack.services.share', + ) + + +dashboard.Project.register(ShareGroupSnapshots) diff --git a/manila_ui/dashboards/project/share_group_snapshots/tables.py b/manila_ui/dashboards/project/share_group_snapshots/tables.py new file mode 100644 index 00000000..4cab8344 --- /dev/null +++ b/manila_ui/dashboards/project/share_group_snapshots/tables.py @@ -0,0 +1,173 @@ +# Copyright 2017 Mirantis, Inc. +# 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.template.defaultfilters import title # noqa +from django.utils.http import urlencode # noqa +from django.utils.translation import pgettext_lazy +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext_lazy +from horizon import exceptions +from horizon import tables +from horizon.utils import filters + +from manila_ui.api import manila + + +class UpdateShareGroupSnapshot(tables.LinkAction): + name = "update_share_group_snapshot" + verbose_name = _("Update") + url = "horizon:project:share_group_snapshots:update" + classes = ("ajax-modal", "btn-camera") + + def allowed(self, request, share_group_snapshot=None): + return share_group_snapshot.status in ("available", "error") + + +class CreateShareGroupSnapshot(tables.LinkAction): + name = "create_share_group_snapshot" + verbose_name = _("Create Share Group Snapshot") + url = "horizon:project:share_group_snapshots:create" + classes = ("ajax-modal", "btn-camera") + + def allowed(self, request, share_group=None): + self.verbose_name = _("Create Share Group Snapshot") + classes = [c for c in self.classes if c != "disabled"] + self.classes = classes + return share_group.status == "available" + + +class CreateShareGroupFromSnapshot(tables.LinkAction): + name = "create_share_group_from_snapshot" + verbose_name = _("Create Share Group") + url = "horizon:project:share_groups:create" + classes = ("ajax-modal", "btn-camera") + + def get_link_url(self, datum): + base_url = reverse(self.url) + params = urlencode({ + "snapshot_id": self.table.get_object_id(datum)}) + return "?".join([base_url, params]) + + def allowed(self, request, share_group=None): + return share_group.status == "available" + + +class ShareGroupSnapshotShareGroupNameColumn(tables.Column): + def get_link_url(self, snapshot): + return reverse(self.link, args=(snapshot.share_group_id,)) + + +class DeleteShareGroupSnapshot(tables.DeleteAction): + + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete Share Group Snapshot", + u"Delete Share Group Snapshots", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Deleted Share Group Snapshot", + u"Deleted Share Group Snapshots", + count + ) + + def delete(self, request, obj_id): + obj = self.table.get_object_by_id(obj_id) + name = self.table.get_object_display(obj) + try: + manila.share_group_snapshot_delete(request, obj_id) + except Exception: + msg = _('Unable to delete share group snapshot "%s". ' + 'One or more share groups depend on it.') + exceptions.check_message(["snapshots", "dependent"], msg % name) + raise + + def allowed(self, request, snapshot=None): + if snapshot: + return snapshot.status.lower() in ('available', 'error') + return True + + +class UpdateShareGroupSnapshotRow(tables.Row): + ajax = True + + def get_data(self, request, share_group_snapshot_id): + snapshot = manila.share_group_snapshot_get( + request, share_group_snapshot_id) + if not snapshot.name: + snapshot.name = share_group_snapshot_id + return snapshot + + +class ShareGroupSnapshotsTable(tables.DataTable): + STATUS_CHOICES = ( + ("available", True), + ("creating", None), + ("error", False), + ) + STATUS_DISPLAY_CHOICES = ( + ("available", + pgettext_lazy("Current status of snapshot", u"Available")), + ("creating", pgettext_lazy("Current status of snapshot", u"Creating")), + ("error", pgettext_lazy("Current status of snapshot", u"Error")), + ) + name = tables.WrappingColumn( + "name", verbose_name=_("Name"), + link="horizon:project:share_group_snapshots:detail") + description = tables.Column( + "description", + verbose_name=_("Description"), + truncate=40) + created_at = tables.Column( + "created_at", + verbose_name=_("Created at"), + filters=( + filters.parse_isotime, + )) + status = tables.Column( + "status", + filters=(title,), + verbose_name=_("Status"), + status=True, + status_choices=STATUS_CHOICES, + display_choices=STATUS_DISPLAY_CHOICES) + source = ShareGroupSnapshotShareGroupNameColumn( + "share_group", + verbose_name=_("Source"), + link="horizon:project:share_groups:detail") + + def get_object_display(self, obj): + return obj.name + + class Meta(object): + name = "share_group_snapshots" + verbose_name = _("Share Group Snapshots") + status_columns = ["status"] + row_class = UpdateShareGroupSnapshotRow + table_actions = ( + tables.NameFilterAction, + DeleteShareGroupSnapshot, + ) + row_actions = ( + CreateShareGroupFromSnapshot, + UpdateShareGroupSnapshot, + DeleteShareGroupSnapshot, + ) diff --git a/manila_ui/dashboards/project/share_group_snapshots/tabs.py b/manila_ui/dashboards/project/share_group_snapshots/tabs.py new file mode 100644 index 00000000..cc674465 --- /dev/null +++ b/manila_ui/dashboards/project/share_group_snapshots/tabs.py @@ -0,0 +1,34 @@ +# Copyright 2017 Mirantis, Inc. +# 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 tabs + + +class ShareGroupSnapshotOverviewTab(tabs.Tab): + name = _("Share Group Snapshot Overview") + slug = "share_group_snapshot_overview_tab" + template_name = "project/share_group_snapshots/_detail.html" + + def get_context_data(self, request): + return {"share_group_snapshot": self.tab_group.kwargs[ + "share_group_snapshot"]} + + +class ShareGroupSnapshotDetailTabs(tabs.TabGroup): + slug = "share_group_snapshot_details" + tabs = ( + ShareGroupSnapshotOverviewTab, + ) diff --git a/manila_ui/dashboards/project/share_group_snapshots/templates/share_group_snapshots/_create.html b/manila_ui/dashboards/project/share_group_snapshots/templates/share_group_snapshots/_create.html new file mode 100644 index 00000000..0c9d6415 --- /dev/null +++ b/manila_ui/dashboards/project/share_group_snapshots/templates/share_group_snapshots/_create.html @@ -0,0 +1,8 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% block modal-body-right %} +
+ Please, choose a name for the new share group snapshot, + that will be created from '{{ share_group_name }}' share group. +
+{% endblock %} diff --git a/manila_ui/dashboards/project/share_group_snapshots/templates/share_group_snapshots/_detail.html b/manila_ui/dashboards/project/share_group_snapshots/templates/share_group_snapshots/_detail.html new file mode 100644 index 00000000..91dbc9e7 --- /dev/null +++ b/manila_ui/dashboards/project/share_group_snapshots/templates/share_group_snapshots/_detail.html @@ -0,0 +1,23 @@ +{% load i18n sizeformat parse_date %} + +

{% trans "Share Group Snapshot Overview" %}

+
+
+
+
{% trans "Name" %}
+
{{ share_group_snapshot.name }}
+
{% trans "ID" %}
+
{{ share_group_snapshot.id }}
+ {% url 'horizon:project:share_groups:detail' share_group_snapshot.share_group_id as sg_url %} +
{% trans "Source Share Group" %}
+
{{ share_group_snapshot.sg_name_or_id }}
+ {% if share_group_snapshot.description %} +
{% trans "Description" %}
+
{{ share_group_snapshot.description }}
+ {% endif %} +
{% trans "Status" %}
+
{{ share_group_snapshot.status|capfirst }}
+
{% trans "Created" %}
+
{{ share_group_snapshot.created_at|parse_date }}
+
+
diff --git a/manila_ui/dashboards/project/share_group_snapshots/templates/share_group_snapshots/_update.html b/manila_ui/dashboards/project/share_group_snapshots/templates/share_group_snapshots/_update.html new file mode 100644 index 00000000..dab70eab --- /dev/null +++ b/manila_ui/dashboards/project/share_group_snapshots/templates/share_group_snapshots/_update.html @@ -0,0 +1,6 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% block modal-body-right %} +

{% trans "Description" %}:

+

{% trans "From here you can modify name and description of a share group snapshot." %}

+{% endblock %} diff --git a/manila_ui/dashboards/project/share_group_snapshots/templates/share_group_snapshots/create.html b/manila_ui/dashboards/project/share_group_snapshots/templates/share_group_snapshots/create.html new file mode 100644 index 00000000..1a514f01 --- /dev/null +++ b/manila_ui/dashboards/project/share_group_snapshots/templates/share_group_snapshots/create.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Share Group Snapshot" %}{% endblock %} + +{% block main %} + {% include 'project/share_group_snapshots/_create.html' %} +{% endblock %} diff --git a/manila_ui/dashboards/project/share_group_snapshots/templates/share_group_snapshots/detail.html b/manila_ui/dashboards/project/share_group_snapshots/templates/share_group_snapshots/detail.html new file mode 100644 index 00000000..577d367c --- /dev/null +++ b/manila_ui/dashboards/project/share_group_snapshots/templates/share_group_snapshots/detail.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Share Group Snapshot Details" %}{% endblock %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} diff --git a/manila_ui/dashboards/project/share_group_snapshots/templates/share_group_snapshots/index.html b/manila_ui/dashboards/project/share_group_snapshots/templates/share_group_snapshots/index.html new file mode 100644 index 00000000..840225b0 --- /dev/null +++ b/manila_ui/dashboards/project/share_group_snapshots/templates/share_group_snapshots/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Share Group Snapshots" %}{% endblock %} + +{% block main %} +
+
+ {{ share_group_snapshots_table.render }} +
+
+{% endblock %} diff --git a/manila_ui/dashboards/project/share_group_snapshots/templates/share_group_snapshots/update.html b/manila_ui/dashboards/project/share_group_snapshots/templates/share_group_snapshots/update.html new file mode 100644 index 00000000..61fdceb8 --- /dev/null +++ b/manila_ui/dashboards/project/share_group_snapshots/templates/share_group_snapshots/update.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update Share Group Snapshot" %}{% endblock %} + +{% block main %} + {% include 'project/share_group_snapshots/_update.html' %} +{% endblock %} diff --git a/manila_ui/dashboards/project/share_group_snapshots/urls.py b/manila_ui/dashboards/project/share_group_snapshots/urls.py new file mode 100644 index 00000000..d0ff0376 --- /dev/null +++ b/manila_ui/dashboards/project/share_group_snapshots/urls.py @@ -0,0 +1,40 @@ +# Copyright 2017 Mirantis, Inc. +# 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 import urls + +from manila_ui.dashboards.project.share_group_snapshots import views +from manila_ui import features + + +if features.is_share_groups_enabled(): + urlpatterns = [ + urls.url( + r'^$', + views.ShareGroupSnapshotsView.as_view(), + name='index'), + urls.url( + r'^(?P[^/]+)/snapshot_create/$', + views.CreateShareGroupSnapshotView.as_view(), + name='create'), + urls.url( + r'^(?P[^/]+)/$', + views.ShareGroupSnapshotDetailView.as_view(), + name='detail'), + urls.url( + r'^(?P[^/]+)/update/$', + views.UpdateShareGroupSnapshotView.as_view(), + name='update'), + ] diff --git a/manila_ui/dashboards/project/share_group_snapshots/views.py b/manila_ui/dashboards/project/share_group_snapshots/views.py new file mode 100644 index 00000000..c11729a4 --- /dev/null +++ b/manila_ui/dashboards/project/share_group_snapshots/views.py @@ -0,0 +1,164 @@ +# Copyright 2017 Mirantis, Inc. +# 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. + +""" +Project views for managing share group snapshots. +""" + +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 import tabs +from horizon.utils import memoized + +from manila_ui.api import manila +import manila_ui.dashboards.project.share_group_snapshots.forms as sgs_forms +import manila_ui.dashboards.project.share_group_snapshots.tables as sgs_tables +import manila_ui.dashboards.project.share_group_snapshots.tabs as sgs_tabs + + +class ShareGroupSnapshotsView(tables.MultiTableView): + table_classes = ( + sgs_tables.ShareGroupSnapshotsTable, + ) + template_name = "project/share_group_snapshots/index.html" + page_title = _("Share Group Snapshots") + + @memoized.memoized_method + def get_share_group_snapshots_data(self): + share_group_snapshots = [] + try: + share_group_snapshots = manila.share_group_snapshot_list( + self.request, search_opts={'all_tenants': True}) + share_groups = manila.share_group_list(self.request) + sg_names = dict([(sg.id, sg.name or sg.id) for sg in share_groups]) + for snapshot in share_group_snapshots: + snapshot.share_group = sg_names.get(snapshot.share_group_id) + except Exception: + msg = _("Unable to retrieve share group snapshot list.") + exceptions.handle(self.request, msg) + + return share_group_snapshots + + +class ShareGroupSnapshotDetailView(tabs.TabView): + tab_group_class = sgs_tabs.ShareGroupSnapshotDetailTabs + template_name = "project/share_group_snapshots/detail.html" + redirect_url = reverse_lazy("horizon:project:share_group_snapshots:index") + + def get_context_data(self, **kwargs): + context = super(ShareGroupSnapshotDetailView, self).get_context_data( + **kwargs) + snapshot = self.get_data() + snapshot_display_name = snapshot.name or snapshot.id + context["snapshot"] = snapshot + context["snapshot_display_name"] = snapshot_display_name + context["page_title"] = _( + "Share Group Snapshot Details: %(sgs_display_name)s") % ( + {'sgs_display_name': snapshot_display_name}) + return context + + @memoized.memoized_method + def get_data(self): + try: + share_group_snapshot = manila.share_group_snapshot_get( + self.request, self.kwargs['share_group_snapshot_id']) + sg = manila.share_group_get( + self.request, share_group_snapshot.share_group_id) + share_group_snapshot.sg_name_or_id = sg.name or sg.id + except Exception: + exceptions.handle( + self.request, + _('Unable to retrieve share group snapshot details.'), + redirect=self.redirect_url) + return share_group_snapshot + + def get_tabs(self, request, *args, **kwargs): + return self.tab_group_class( + request, share_group_snapshot=self.get_data(), **kwargs) + + +class CreateShareGroupSnapshotView(forms.ModalFormView): + form_class = sgs_forms.CreateShareGroupSnapshotForm + form_id = "create_share_group_snapshot" + template_name = 'project/share_group_snapshots/create.html' + modal_header = _("Create Share Group Snapshot") + modal_id = "create_share_group_snapshot_modal" + submit_label = _("Create Share Group Snapshot") + submit_url = "horizon:project:share_group_snapshots:create" + success_url = reverse_lazy('horizon:project:share_group_snapshots:index') + page_title = _('Create Share Group Snapshot') + + def get_context_data(self, **kwargs): + context = super(self.__class__, self).get_context_data(**kwargs) + sg_id = self.kwargs['share_group_id'] + try: + sg = manila.share_group_get(self.request, sg_id) + context['share_group_name'] = sg.name or sg_id + except Exception: + exceptions.handle( + self.request, + _("Unable to get the specified share group '%s' for " + "snapshot creation.") % sg_id) + context['share_group_name'] = sg_id + context['share_group_id'] = sg_id + # TODO(vponomaryov): add support of quotas when it is implemented + # for share group snapshots on server side. + return context + + def get_initial(self): + self.submit_url = reverse(self.submit_url, kwargs=self.kwargs) + return {'share_group_id': self.kwargs["share_group_id"]} + + +class UpdateShareGroupSnapshotView(forms.ModalFormView): + form_class = sgs_forms.UpdateShareGroupSnapshotForm + form_id = "update_share_group_snapshot" + template_name = 'project/share_group_snapshots/update.html' + modal_header = _("Update Share Group Snapshot") + modal_id = "update_share_group_snapshot_modal" + submit_label = _("Update") + submit_url = "horizon:project:share_group_snapshots:update" + success_url = reverse_lazy('horizon:project:share_group_snapshots:index') + page_title = _('Update Share Group') + + @memoized.memoized_method + def get_object(self): + if not hasattr(self, "_object"): + try: + self._object = manila.share_group_snapshot_get( + self.request, self.kwargs['share_group_snapshot_id']) + except Exception: + msg = _('Unable to retrieve share group snapshot.') + url = reverse('horizon:project:share_group_snapshots:index') + exceptions.handle(self.request, msg, redirect=url) + return self._object + + def get_context_data(self, **kwargs): + context = super(self.__class__, 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 { + 'share_group_snapshot_id': self.kwargs["share_group_snapshot_id"], + 'name': snapshot.name, + 'description': snapshot.description, + } diff --git a/manila_ui/dashboards/project/share_groups/__init__.py b/manila_ui/dashboards/project/share_groups/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/manila_ui/dashboards/project/share_groups/forms.py b/manila_ui/dashboards/project/share_groups/forms.py new file mode 100644 index 00000000..833507db --- /dev/null +++ b/manila_ui/dashboards/project/share_groups/forms.py @@ -0,0 +1,265 @@ +# Copyright 2017 Mirantis, Inc. +# 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.forms import ValidationError # noqa +from django.utils.translation import ugettext_lazy as _ +from horizon import exceptions +from horizon import forms +from horizon import messages +from horizon.utils import memoized +import six + +from manila_ui.api import manila + + +class CreateShareGroupForm(forms.SelfHandlingForm): + name = forms.CharField(label=_("Name"), max_length="255", required=True) + description = forms.CharField( + label=_("Description"), + max_length="255", + widget=forms.Textarea(attrs={"rows": 3}), + required=False) + + def __init__(self, request, *args, **kwargs): + super(CreateShareGroupForm, self).__init__(request, *args, **kwargs) + self.st_field_name_prefix = "share-type-choices-" + self.fields["source_type"] = forms.ChoiceField( + label=_("Source Type"), + widget=forms.Select(attrs={ + "class": "switchable", + "data-slug": "source", + }), + required=False) + + self.fields["snapshot"] = forms.ChoiceField( + label=_("Use share group snapshot as a source"), + widget=forms.SelectWidget(attrs={ + "class": "switched", + "data-switch-on": "source", + "data-source-snapshot": _("Share Group Snapshot"), + }), + required=True) + + if ("snapshot_id" in request.GET or + kwargs.get("data", {}).get("snapshot")): + try: + snapshot = self.get_share_group_snapshot( + request, + request.GET.get( + "snapshot_id", + kwargs.get("data", {}).get("snapshot"))) + self.fields["name"].initial = snapshot.name + self.fields["snapshot"].choices = ( + (snapshot.id, snapshot.name or snapshot.id), + ) + try: + # Set the share group type from the original share group + orig_sg = manila.share_group_get( + request, snapshot.share_group_id) + self.fields["sgt"].initial = orig_sg.share_group_type_id + except Exception: + pass + del self.fields["source_type"] + except Exception: + exceptions.handle( + request, + _("Unable to load the specified share group snapshot.")) + else: + source_type_choices = [] + try: + snapshot_list = manila.share_group_snapshot_list(request) + snapshots = [s for s in snapshot_list + if s.status == "available"] + if snapshots: + source_type_choices.append(("snapshot", _("Snapshot"))) + self.fields["snapshot"].choices = ( + [("", _("Choose a snapshot"))] + + [(s.id, s.name or s.id) for s in snapshots] + ) + else: + del self.fields["snapshot"] + except Exception: + exceptions.handle( + request, + _("Unable to retrieve share group snapshots.")) + + if source_type_choices: + choices = ([('none', _("No source, empty share group"))] + + source_type_choices) + self.fields["source_type"].choices = choices + else: + del self.fields["source_type"] + + self.fields["az"] = forms.ChoiceField( + label=_("Availability Zone"), + widget=forms.SelectWidget(attrs={ + "class": "switched", + "data-switch-on": "source", + "data-source-none": _("Availability Zone"), + }), + required=False) + + availability_zones = manila.availability_zone_list(request) + self.fields["az"].choices = ( + [("", "")] + [(az.name, az.name) for az in availability_zones]) + + share_group_types = manila.share_group_type_list(request) + self.fields["sgt"] = forms.ChoiceField( + label=_("Share Group Type"), + widget=forms.fields.SelectWidget(attrs={ + "class": "switched switchable", + "data-switch-on": "source", + "data-source-none": _("Share Group Type"), + "data-slug": "sgt", + }), + required=True) + self.fields["sgt"].choices = ( + [("", "")] + [(sgt.id, sgt.name) for sgt in share_group_types]) + + # NOTE(vponomaryov): create separate set of available share types + # for each of share group types. + share_types = manila.share_type_list(request) + for sgt in share_group_types: + st_choices = ( + [(st.id, st.name) + for st in share_types if st.id in sgt.share_types]) + amount_of_choices = len(st_choices) + st_field_name = self.st_field_name_prefix + sgt.id + if amount_of_choices < 2: + st_field = forms.ChoiceField( + label=_("Share Types"), + choices=st_choices, + widget=forms.fields.SelectWidget(attrs={ + "class": "switched", + "data-switch-on": "sgt", + "data-sgt-%s" % sgt.id: _( + "Share Types (one available)"), + }), + required=True) + else: + height = min(30 * amount_of_choices, 155) + st_field = forms.MultipleChoiceField( + label=_("Share Types"), + choices=st_choices, + widget=forms.fields.widgets.SelectMultiple(attrs={ + "style": "max-height: %spx;" % height, + "class": "switched", + "data-switch-on": "sgt", + "data-sgt-%s" % sgt.id: _( + "Share Types (multiple available)"), + }), + required=False) + st_field.initial = st_choices[0] + self.fields[st_field_name] = st_field + + self.fields["share_network"] = forms.ChoiceField( + label=_("Share Network"), + widget=forms.fields.SelectWidget(attrs={ + "class": "switched", + "data-switch-on": "source", + "data-source-none": _("Share Network"), + }), + required=False) + share_networks = manila.share_network_list(request) + self.fields["share_network"].choices = ( + [("", "")] + + [(sn.id, sn.name or sn.id) for sn in share_networks]) + + def clean(self): + cleaned_data = super(CreateShareGroupForm, self).clean() + errors = [k for k in self.errors.viewkeys()] + + for k in errors: + sgt_name = k.split(self.st_field_name_prefix)[-1] + chosen_sgt = cleaned_data.get("sgt") + if (k.startswith(self.st_field_name_prefix) and + sgt_name != chosen_sgt): + cleaned_data[k] = "Not set" + self.errors.pop(k, None) + + source_type = cleaned_data.get("source_type") + if source_type != "snapshot": + self.errors.pop("snapshot", None) + share_group_type = cleaned_data.get("sgt") + if share_group_type: + cleaned_data["share_types"] = cleaned_data.get( + self.st_field_name_prefix + share_group_type) + if isinstance(cleaned_data["share_types"], six.string_types): + cleaned_data["share_types"] = [cleaned_data["share_types"]] + else: + self.errors.pop("sgt", None) + + return cleaned_data + + def handle(self, request, data): + try: + source_type = data.get('source_type') + if (data.get("snapshot") and source_type in (None, 'snapshot')): + snapshot = self.get_share_group_snapshot( + request, data["snapshot"]) + snapshot_id = snapshot.id + source_sg = manila.share_group_get( + request, snapshot.share_group_id) + data['sgt'] = source_sg.share_group_type_id + else: + snapshot_id = None + + share_group = manila.share_group_create( + request, + name=data['name'], + description=data['description'], + share_group_type=data['sgt'], + share_types=None if snapshot_id else data.get('share_types'), + share_network=( + None if snapshot_id else data.get('share_network')), + source_share_group_snapshot=snapshot_id, + availability_zone=None if snapshot_id else data['az']) + + message = _('Creating share group "%s"') % data['name'] + messages.success(request, message) + return share_group + except ValidationError as e: + self.api_error(e.messages[0]) + return False + except Exception: + exceptions.handle(request, ignore=True) + self.api_error(_("Unable to create share group.")) + return False + + @memoized.memoized + def get_share_group_snapshot(self, request, sg_snapshot_id): + return manila.share_group_snapshot_get(request, sg_snapshot_id) + + +class UpdateShareGroupForm(forms.SelfHandlingForm): + name = forms.CharField( + max_length="255", label=_("Share Group Name")) + description = forms.CharField( + widget=forms.Textarea, label=_("Description"), required=False) + + def handle(self, request, data): + sg_id = self.initial['share_group_id'] + try: + manila.share_group_update( + request, sg_id, data['name'], data['description']) + message = _('Updating share group "%s"') % data['name'] + messages.success(request, message) + return True + except Exception: + redirect = reverse("horizon:project:share_groups:index") + exceptions.handle( + request, _('Unable to update share group.'), redirect=redirect) + return False diff --git a/manila_ui/dashboards/project/share_groups/panel.py b/manila_ui/dashboards/project/share_groups/panel.py new file mode 100644 index 00000000..23f0667d --- /dev/null +++ b/manila_ui/dashboards/project/share_groups/panel.py @@ -0,0 +1,29 @@ +# Copyright 2017 Mirantis, Inc. +# 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 +from openstack_dashboard.dashboards.project import dashboard + + +class ShareGroups(horizon.Panel): + name = _("Share Groups") + slug = 'share_groups' + permissions = ( + 'openstack.services.share', + ) + + +dashboard.Project.register(ShareGroups) diff --git a/manila_ui/dashboards/project/share_groups/tables.py b/manila_ui/dashboards/project/share_groups/tables.py new file mode 100644 index 00000000..dac174ed --- /dev/null +++ b/manila_ui/dashboards/project/share_groups/tables.py @@ -0,0 +1,161 @@ +# Copyright 2017 Mirantis, Inc. +# 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.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext_lazy +from horizon import tables +import six + +from manila_ui.api import manila +import manila_ui.dashboards.project.share_group_snapshots.tables as sgs_tables + + +class UpdateShareGroup(tables.LinkAction): + name = "update" + verbose_name = _("Update") + url = "horizon:project:share_groups:update" + classes = ("ajax-modal", "btn-create") + + def allowed(self, request, share_group=None): + return share_group.status in ("available", "error") + + +class DeleteShareGroup(tables.DeleteAction): + + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete Share Group", + u"Delete Share Groups", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Deleted Share Group", + u"Deleted Share Groups", + count + ) + + def delete(self, request, obj_id): + manila.share_group_delete(request, obj_id) + + def allowed(self, request, share_group=None): + if share_group: + # Row button + return (share_group.status.lower() in ('available', 'error')) + # Table button. Always return 'True' + return True + + +class CreateShareGroup(tables.LinkAction): + name = "create" + verbose_name = _("Create Share Group") + url = "horizon:project:share_groups:create" + classes = ("ajax-modal", "btn-create") + icon = "plus" + policy_rules = (("share", "share_group:create"),) + + def allowed(self, request, share=None): + # TODO(vponomaryov): implement quota restriction when quota support + # is implemented for share groups. + return True + + +class UpdateShareGroupRow(tables.Row): + ajax = True + + def get_data(self, request, sg_id): + sg = manila.share_group_get(request, sg_id) + return sg + + +class ShareGroupsTable(tables.DataTable): + def get_share_network_link(share_group): + if getattr(share_group, 'share_network_id', None): + return reverse( + "horizon:project:share_networks:share_network_detail", + args=(share_group.share_network_id,)) + else: + return None + + def get_source_share_group_snapshot_link(share_group): + if getattr(share_group, 'source_share_group_snapshot_id', None): + return reverse( + "horizon:project:share_group_snapshots:detail", + args=(share_group.source_share_group_snapshot_id,)) + else: + return None + + STATUS_CHOICES = ( + ("available", True), + ("creating", None), + ("deleting", None), + ("error", False), + ("error_deleting", False), + ) + STATUS_DISPLAY_CHOICES = ( + ("available", u"Available"), + ("creating", u"Creating"), + ("deleting", u"Deleting"), + ("error", u"Error"), + ("error_deleting", u"Error deleting"), + ) + name = tables.Column( + "name", + verbose_name=_("Name"), + link="horizon:project:share_groups:detail") + status = tables.Column( + "status", + verbose_name=_("Status"), + status=True, + status_choices=STATUS_CHOICES, + display_choices=STATUS_DISPLAY_CHOICES, + ) + availability_zone = tables.Column( + "availability_zone", + verbose_name=_("Availability Zone")) + share_network_id = tables.Column( + "share_network_id", + verbose_name=_("Share Network"), + link=get_share_network_link) + source_share_group_snapshot_id = tables.Column( + "source_share_group_snapshot_id", + verbose_name=_("Source Share Group Snapshot"), + link=get_source_share_group_snapshot_link) + + def get_object_display(self, share_group): + return six.text_type(share_group.id) + + def get_object_id(self, share_group): + return six.text_type(share_group.id) + + class Meta(object): + name = "share_groups" + verbose_name = _("Share Groups") + status_columns = ("status", ) + row_class = UpdateShareGroupRow + table_actions = ( + CreateShareGroup, + tables.NameFilterAction, + DeleteShareGroup, + ) + row_actions = ( + sgs_tables.CreateShareGroupSnapshot, + UpdateShareGroup, + DeleteShareGroup, + ) diff --git a/manila_ui/dashboards/project/share_groups/tabs.py b/manila_ui/dashboards/project/share_groups/tabs.py new file mode 100644 index 00000000..bc5b1eda --- /dev/null +++ b/manila_ui/dashboards/project/share_groups/tabs.py @@ -0,0 +1,33 @@ +# Copyright 2017 Mirantis, Inc. +# 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 tabs + + +class ShareGroupOverviewTab(tabs.Tab): + name = _("Share Group Overview") + slug = "share_group_overview_tab" + template_name = "project/share_groups/_detail.html" + + def get_context_data(self, request): + return {"share_group": self.tab_group.kwargs["share_group"]} + + +class ShareGroupDetailTabs(tabs.TabGroup): + slug = "share_group_details" + tabs = ( + ShareGroupOverviewTab, + ) diff --git a/manila_ui/dashboards/project/share_groups/templates/share_groups/_create.html b/manila_ui/dashboards/project/share_groups/templates/share_groups/_create.html new file mode 100644 index 00000000..9e2d3241 --- /dev/null +++ b/manila_ui/dashboards/project/share_groups/templates/share_groups/_create.html @@ -0,0 +1,6 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% block modal-body-right %} +

{% trans "Description" %}:

+

{% trans "Select parameters of share group you want to create. " %}

+{% endblock %} diff --git a/manila_ui/dashboards/project/share_groups/templates/share_groups/_detail.html b/manila_ui/dashboards/project/share_groups/templates/share_groups/_detail.html new file mode 100644 index 00000000..69df06c4 --- /dev/null +++ b/manila_ui/dashboards/project/share_groups/templates/share_groups/_detail.html @@ -0,0 +1,55 @@ +{% load i18n sizeformat parse_date %} + +

{% trans "Share Group Overview" %}

+
+
+
+
{% trans "ID" %}
+
{{ share_group.id }}
+
{% trans "Name" %}
+
{{ share_group.name }}
+
{% trans "Description" %}
+
{{ share_group.description }}
+
{% trans "Status" %}
+
{{ share_group.status|capfirst }}
+
{% trans "Created" %}
+
{{ share_group.created_at|parse_date }}
+ {% if share_group.source_share_group_snapshot_id %} +
{% trans "Source SG Snapshot" %}
+ {% url 'horizon:project:share_group_snapshots:detail' share_group.source_share_group_snapshot_id as ssgs_url%} +
{{ share_group.source_share_group_snapshot_id }}
+ {% endif %} +
{% trans "Share Group Type" %}
+
{{ share_group.share_group_type_id}}
+ {% if share_group.availability_zone %} +
{% trans "Availability Zone" %}
+
{{ share_group.availability_zone }}
+ {% endif %} + {% if share_group.share_network_id %} +
{% trans "Share Network" %}
+ {% url 'horizon:project:share_networks:share_network_detail' share_group.share_network_id as sn_url%} +
{{ share_group.share_network_id }}
+ {% endif %} + {% if share_group.share_types %} +
{% trans "Share Types" %}
+ {% for st in share_group.share_types %} +

+

Share Type Name: {{ st.name }}
+
Share Type Visibility: {% if st.is_public %}Public{% else %}Private{% endif %}
+
Network handling enabled: {{ st.dhss }}
+

+ {% endfor %} + {% endif %} + {% if share_group.members %} +
{% trans "Shares" %}
+ {% for m in share_group.members %} + {% url 'horizon:project:shares:detail' m.id as share_url%} +
+ +
+ {% endfor %} + {% endif %} +
+
diff --git a/manila_ui/dashboards/project/share_groups/templates/share_groups/_update.html b/manila_ui/dashboards/project/share_groups/templates/share_groups/_update.html new file mode 100644 index 00000000..c8826477 --- /dev/null +++ b/manila_ui/dashboards/project/share_groups/templates/share_groups/_update.html @@ -0,0 +1,6 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% block modal-body-right %} +

{% trans "Description" %}:

+

{% trans "From here you can modify name, description and visibility of a share group." %}

+{% endblock %} diff --git a/manila_ui/dashboards/project/share_groups/templates/share_groups/create.html b/manila_ui/dashboards/project/share_groups/templates/share_groups/create.html new file mode 100644 index 00000000..d8e54768 --- /dev/null +++ b/manila_ui/dashboards/project/share_groups/templates/share_groups/create.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Share Group" %}{% endblock %} + +{% block main %} + {% include 'project/share_groups/_create.html' %} +{% endblock %} diff --git a/manila_ui/dashboards/project/share_groups/templates/share_groups/detail.html b/manila_ui/dashboards/project/share_groups/templates/share_groups/detail.html new file mode 100644 index 00000000..95114d91 --- /dev/null +++ b/manila_ui/dashboards/project/share_groups/templates/share_groups/detail.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Share Group Details" %}{% endblock %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} diff --git a/manila_ui/dashboards/project/share_groups/templates/share_groups/index.html b/manila_ui/dashboards/project/share_groups/templates/share_groups/index.html new file mode 100644 index 00000000..93a16df5 --- /dev/null +++ b/manila_ui/dashboards/project/share_groups/templates/share_groups/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Share Groups" %}{% endblock %} + +{% block main %} +
+
+ {{ share_groups_table.render }} +
+
+{% endblock %} diff --git a/manila_ui/dashboards/project/share_groups/templates/share_groups/update.html b/manila_ui/dashboards/project/share_groups/templates/share_groups/update.html new file mode 100644 index 00000000..6977288c --- /dev/null +++ b/manila_ui/dashboards/project/share_groups/templates/share_groups/update.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update Share Group" %}{% endblock %} + +{% block main %} + {% include 'project/share_groups/_update.html' %} +{% endblock %} diff --git a/manila_ui/dashboards/project/share_groups/urls.py b/manila_ui/dashboards/project/share_groups/urls.py new file mode 100644 index 00000000..41e7e742 --- /dev/null +++ b/manila_ui/dashboards/project/share_groups/urls.py @@ -0,0 +1,40 @@ +# Copyright 2017 Mirantis Inc. +# 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 import urls + +from manila_ui.dashboards.project.share_groups import views +from manila_ui import features + + +if features.is_share_groups_enabled(): + urlpatterns = [ + urls.url( + r'^$', + views.ShareGroupsView.as_view(), + name='index'), + urls.url( + r'^create/$', + views.ShareGroupCreateView.as_view(), + name='create'), + urls.url( + r'^(?P[^/]+)/$', + views.ShareGroupDetailView.as_view(), + name='detail'), + urls.url( + r'^(?P[^/]+)/update/$', + views.ShareGroupUpdateView.as_view(), + name='update'), + ] diff --git a/manila_ui/dashboards/project/share_groups/views.py b/manila_ui/dashboards/project/share_groups/views.py new file mode 100644 index 00000000..3f179fbd --- /dev/null +++ b/manila_ui/dashboards/project/share_groups/views.py @@ -0,0 +1,150 @@ +# Copyright 2017 Mirantis, Inc. +# 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. + +""" +Project views for managing share groups. +""" + +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 import tabs +from horizon.utils import memoized + +from manila_ui.api import manila +from manila_ui.dashboards.project.share_groups import forms as sg_forms +from manila_ui.dashboards.project.share_groups import tables as sg_tables +from manila_ui.dashboards.project.share_groups import tabs as sg_tabs + + +class ShareGroupsView(tables.MultiTableView): + table_classes = ( + sg_tables.ShareGroupsTable, + ) + template_name = "project/share_groups/index.html" + page_title = _("Share Groups") + + @memoized.memoized_method + def get_share_groups_data(self): + try: + share_groups = manila.share_group_list( + self.request, detailed=True) + except Exception: + share_groups = [] + exceptions.handle( + self.request, _("Unable to retrieve share groups.")) + return share_groups + + +class ShareGroupDetailView(tabs.TabView): + tab_group_class = sg_tabs.ShareGroupDetailTabs + template_name = 'project/share_groups/detail.html' + + def get_context_data(self, **kwargs): + context = super(self.__class__, self).get_context_data(**kwargs) + share_group = self.get_data() + context["share_group"] = share_group + context["page_title"] = ( + _("Share Group Details: %s") % share_group.id) + return context + + @memoized.memoized_method + def get_data(self): + try: + share_group_id = self.kwargs['share_group_id'] + share_group = manila.share_group_get(self.request, share_group_id) + members = manila.share_list( + self.request, search_opts={"share_group_id": share_group_id}) + share_group.members = members + share_types = manila.share_type_list(self.request) + share_group.share_types = [ + {"id": st.id, + "name": st.name, + "is_public": getattr(st, 'share_type_access:is_public'), + "dhss": st.extra_specs.get('driver_handles_share_servers')} + for st in share_types if st.id in share_group.share_types + ] + return share_group + except Exception: + redirect = reverse('horizon:project:share_groups:index') + exceptions.handle( + self.request, + _('Unable to retrieve share group details.'), + redirect=redirect) + + def get_tabs(self, request, *args, **kwargs): + share_group = self.get_data() + return self.tab_group_class(request, share_group=share_group, **kwargs) + + +class ShareGroupCreateView(forms.ModalFormView): + form_class = sg_forms.CreateShareGroupForm + form_id = "create_share_group" + template_name = 'project/share_groups/create.html' + modal_header = _("Create Share Group") + modal_id = "create_share_group_modal" + submit_label = _("Create") + submit_url = reverse_lazy("horizon:project:share_groups:create") + success_url = reverse_lazy("horizon:project:share_groups:index") + page_title = _('Create a Share') + + def get_context_data(self, **kwargs): + context = super(ShareGroupCreateView, self).get_context_data(**kwargs) + try: + # TODO(vponomaryov): add quota logic here when quotas are + # implemented for share groups. + pass + except Exception: + exceptions.handle(self.request) + return context + + +class ShareGroupUpdateView(forms.ModalFormView): + form_class = sg_forms.UpdateShareGroupForm + form_id = "update_share_group" + template_name = 'project/share_groups/update.html' + modal_header = _("Update") + modal_id = "update_share_group_modal" + submit_label = _("Update") + submit_url = "horizon:project:share_groups:update" + success_url = reverse_lazy("horizon:project:share_groups:index") + page_title = _("Update Share Group") + + def get_object(self): + if not hasattr(self, "_object"): + sg_id = self.kwargs['share_group_id'] + try: + self._object = manila.share_group_get(self.request, sg_id) + except Exception: + msg = _('Unable to retrieve share group.') + url = reverse('horizon:project:share_groups:index') + exceptions.handle(self.request, msg, redirect=url) + return self._object + + def get_context_data(self, **kwargs): + context = super(ShareGroupUpdateView, self).get_context_data(**kwargs) + return context + + def get_initial(self): + self.submit_url = reverse(self.submit_url, kwargs=self.kwargs) + sg = self.get_object() + return { + 'share_group_id': self.kwargs["share_group_id"], + 'name': sg.name, + 'description': sg.description, + } diff --git a/manila_ui/dashboards/project/shares/forms.py b/manila_ui/dashboards/project/shares/forms.py index 6d0074a1..99f22144 100644 --- a/manila_ui/dashboards/project/shares/forms.py +++ b/manila_ui/dashboards/project/shares/forms.py @@ -18,16 +18,17 @@ from django.core.urlresolvers import reverse from django.forms import ValidationError # noqa from django.template.defaultfilters import filesizeformat # noqa from django.utils.translation import ugettext_lazy as _ -import six - from horizon import exceptions from horizon import forms from horizon import messages from horizon.utils.memoized import memoized # noqa +from openstack_dashboard.usage import quotas +import six from manila_ui.api import manila from manila_ui.dashboards import utils -from openstack_dashboard.usage import quotas +from manila_ui import features +from manilaclient.common.apiclient import exceptions as m_exceptions class CreateForm(forms.SelfHandlingForm): @@ -65,6 +66,14 @@ class CreateForm(forms.SelfHandlingForm): self.fields['availability_zone'].choices = ( [("", "")] + [(az.name, az.name) for az in availability_zones]) + if features.is_share_groups_enabled(): + share_groups = manila.share_group_list(request) + self.fields['sg'] = forms.ChoiceField( + label=_("Share Group"), + choices=[("", "")] + [(sg.id, sg.name or sg.id) + for sg in share_groups], + required=False) + self.sn_field_name_prefix = 'share-network-choices-' for st in share_types: extra_specs = st.get_keys() @@ -226,17 +235,20 @@ class CreateForm(forms.SelfHandlingForm): share_type=data['share_type'], is_public=is_public, metadata=metadata, - availability_zone=data['availability_zone']) + availability_zone=data['availability_zone'], + share_group_id=data.get('sg') or None, + ) message = _('Creating share "%s"') % data['name'] messages.success(request, message) return share except ValidationError as e: self.api_error(e.messages[0]) - return False + except m_exceptions.BadRequest as e: + self.api_error(_("Unable to create share. %s") % e.message) except Exception: exceptions.handle(request, ignore=True) self.api_error(_("Unable to create share.")) - return False + return False @memoized def get_snapshot(self, request, id): diff --git a/manila_ui/dashboards/project/shares/tables.py b/manila_ui/dashboards/project/shares/tables.py index 946564c3..b3593375 100644 --- a/manila_ui/dashboards/project/shares/tables.py +++ b/manila_ui/dashboards/project/shares/tables.py @@ -22,10 +22,12 @@ from django.utils.translation import ungettext_lazy from horizon import exceptions from horizon import messages from horizon import tables +from openstack_dashboard.usage import quotas + from manila_ui.api import manila from manila_ui.dashboards.project.share_snapshots import tables as ss_tables from manila_ui.dashboards import utils -from openstack_dashboard.usage import quotas +from manila_ui import features DELETABLE_STATES = ( @@ -60,8 +62,10 @@ class DeleteShare(tables.DeleteAction): return {"project_id": project_id} def delete(self, request, obj_id): + share = manila.share_get(request, obj_id) try: - manila.share_delete(request, obj_id) + manila.share_delete( + request, share.id, share_group_id=share.share_group_id) except Exception: msg = _('Unable to delete share "%s". ') % obj_id messages.error(request, msg) @@ -250,6 +254,21 @@ class SharesTableBase(tables.DataTable): def get_object_display(self, obj): return obj.name or obj.id + def get_share_group_link(share): + if features.is_share_groups_enabled() and share.share_group_id: + return reverse( + "horizon:project:share_groups:detail", + args=(share.share_group_id,)) + else: + return None + + share_group_id = tables.Column( + "share_group_id", + verbose_name=_("Share Group"), + empty_value="-", + link=get_share_group_link, + ) + class ManageRules(tables.LinkAction): name = "manage_rules" @@ -268,7 +287,7 @@ class ManageReplicas(tables.LinkAction): def allowed(self, request, share): share_replication_enabled = share.replication_type is not None - return manila.is_replication_enabled() and share_replication_enabled + return features.is_replication_enabled() and share_replication_enabled class AddRule(tables.LinkAction): @@ -387,3 +406,10 @@ class SharesTable(SharesTableBase): ManageReplicas, EditShareMetadata, DeleteShare) + + columns = [ + 'name', 'description', 'metadata', 'size', 'status', + 'proto', 'visibility', 'share_network', + ] + if features.is_share_groups_enabled(): + columns.append('share_group_id') diff --git a/manila_ui/dashboards/project/shares/templates/shares/_detail.html b/manila_ui/dashboards/project/shares/templates/shares/_detail.html index 32f3018f..fa41ce32 100644 --- a/manila_ui/dashboards/project/shares/templates/shares/_detail.html +++ b/manila_ui/dashboards/project/shares/templates/shares/_detail.html @@ -57,10 +57,15 @@

{% endif %} {% if share.share_network_id %} -
{% trans "Share network" %}
+
{% trans "Share Network" %}
{% url 'horizon:project:share_networks:share_network_detail' share.share_network_id as sn_url%}
{{ share.share_network_id }}
{% endif %} + {% if share.share_group_id %} +
{% trans "Share Group" %}
+ {% url 'horizon:project:share_groups:detail' share.share_group_id as sg_url%} +
{{ share.share_group_id }}
+ {% endif %}
{% trans "Mount snapshot support" %}
{{ share.mount_snapshot_support }}
{% trans "Created" %}
diff --git a/manila_ui/dashboards/project/shares/urls.py b/manila_ui/dashboards/project/shares/urls.py index 37675922..4508e5a3 100644 --- a/manila_ui/dashboards/project/shares/urls.py +++ b/manila_ui/dashboards/project/shares/urls.py @@ -14,9 +14,9 @@ from django.conf import urls -from manila_ui.api import manila from manila_ui.dashboards.project.shares.replicas import views as replica_views from manila_ui.dashboards.project.shares import views as shares_views +from manila_ui import features urlpatterns = [ @@ -58,7 +58,7 @@ urlpatterns = [ name='revert'), ] -if manila.is_replication_enabled(): +if features.is_replication_enabled(): urlpatterns.extend([ urls.url( r'^(?P[^/]+)/create_replica/$', diff --git a/manila_ui/features.py b/manila_ui/features.py new file mode 100644 index 00000000..b72de5c5 --- /dev/null +++ b/manila_ui/features.py @@ -0,0 +1,50 @@ +# Copyright 2017 Mirantis Inc. +# +# 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. + +""" +This module stores functions that return boolean values and say +which manila features are enabled and which are disabled among those +that are optional for manila UI. +These functions are required mostly for complex features, which consist +of more than one logical part in manila UI and requires appropriate logic +change in more than 1 place. For example, to disable share groups feature +we need to do following: +- Remove 'share_groups' panel from 'share groups' panel group in each + dashboard. +- Disable or do not register URLs for disabled features, so, no one + will be able to request disabled features knowing direct URL. +- Add/remove buttons for other (not disabled) features that are related + to it somehow. +""" + +from django.conf import settings +from horizon.utils import memoized + + +@memoized.memoized +def is_share_groups_enabled(): + manila_config = getattr(settings, 'OPENSTACK_MANILA_FEATURES', {}) + return manila_config.get('enable_share_groups', True) + + +@memoized.memoized +def is_replication_enabled(): + manila_config = getattr(settings, 'OPENSTACK_MANILA_FEATURES', {}) + return manila_config.get('enable_replication', True) + + +@memoized.memoized +def is_migration_enabled(): + manila_config = getattr(settings, 'OPENSTACK_MANILA_FEATURES', {}) + return manila_config.get('enable_migration', True) diff --git a/manila_ui/local/enabled/_9080_manila_admin_add_share_groups_panel_to_share_panel_group.py b/manila_ui/local/enabled/_9080_manila_admin_add_share_groups_panel_to_share_panel_group.py new file mode 100644 index 00000000..c0cefb12 --- /dev/null +++ b/manila_ui/local/enabled/_9080_manila_admin_add_share_groups_panel_to_share_panel_group.py @@ -0,0 +1,25 @@ +# Copyright 2017 Mirantis Inc. +# +# 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 manila_ui import features + + +PANEL_DASHBOARD = 'admin' +PANEL_GROUP = 'share' +PANEL = 'share_groups' + +if features.is_share_groups_enabled(): + ADD_PANEL = 'manila_ui.dashboards.admin.share_groups.panel.ShareGroups' +else: + REMOVE_PANEL = not features.is_share_groups_enabled() diff --git a/manila_ui/local/enabled/_9080_manila_project_add_share_groups_panel_to_share_panel_group.py b/manila_ui/local/enabled/_9080_manila_project_add_share_groups_panel_to_share_panel_group.py new file mode 100644 index 00000000..22979deb --- /dev/null +++ b/manila_ui/local/enabled/_9080_manila_project_add_share_groups_panel_to_share_panel_group.py @@ -0,0 +1,25 @@ +# Copyright 2017 Mirantis Inc. +# +# 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 manila_ui import features + + +PANEL_DASHBOARD = 'project' +PANEL_GROUP = 'share' +PANEL = 'share_groups' + +if features.is_share_groups_enabled(): + ADD_PANEL = 'manila_ui.dashboards.project.share_groups.panel.ShareGroups' +else: + REMOVE_PANEL = not features.is_share_groups_enabled() diff --git a/manila_ui/local/enabled/_9085_manila_admin_add_share_group_snapshots_panel_to_share_panel_group.py b/manila_ui/local/enabled/_9085_manila_admin_add_share_group_snapshots_panel_to_share_panel_group.py new file mode 100644 index 00000000..9967ba51 --- /dev/null +++ b/manila_ui/local/enabled/_9085_manila_admin_add_share_group_snapshots_panel_to_share_panel_group.py @@ -0,0 +1,26 @@ +# Copyright 2017 Mirantis Inc. +# +# 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 manila_ui import features + + +PANEL_DASHBOARD = 'admin' +PANEL_GROUP = 'share' +PANEL = 'share_group_snapshots' + +if features.is_share_groups_enabled(): + ADD_PANEL = ('manila_ui.dashboards.admin.share_group_snapshots.panel.' + 'ShareGroupSnapshots') +else: + REMOVE_PANEL = not features.is_share_groups_enabled() diff --git a/manila_ui/local/enabled/_9085_manila_project_add_share_group_snapshots_panel_to_share_panel_group.py b/manila_ui/local/enabled/_9085_manila_project_add_share_group_snapshots_panel_to_share_panel_group.py new file mode 100644 index 00000000..152e8ae8 --- /dev/null +++ b/manila_ui/local/enabled/_9085_manila_project_add_share_group_snapshots_panel_to_share_panel_group.py @@ -0,0 +1,26 @@ +# Copyright 2017 Mirantis Inc. +# +# 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 manila_ui import features + + +PANEL_DASHBOARD = 'project' +PANEL_GROUP = 'share' +PANEL = 'share_group_snapshots' + +if features.is_share_groups_enabled(): + ADD_PANEL = ('manila_ui.dashboards.project.share_group_snapshots.panel.' + 'ShareGroupSnapshots') +else: + REMOVE_PANEL = not features.is_share_groups_enabled() diff --git a/manila_ui/local/enabled/_9090_manila_admin_add_share_group_types_panel_to_share_panel_group.py b/manila_ui/local/enabled/_9090_manila_admin_add_share_group_types_panel_to_share_panel_group.py new file mode 100644 index 00000000..2de37e55 --- /dev/null +++ b/manila_ui/local/enabled/_9090_manila_admin_add_share_group_types_panel_to_share_panel_group.py @@ -0,0 +1,26 @@ +# Copyright 2017 Mirantis Inc. +# +# 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 manila_ui import features + + +PANEL_DASHBOARD = 'admin' +PANEL_GROUP = 'share' +PANEL = 'share_group_types' + +if features.is_share_groups_enabled(): + ADD_PANEL = ( + 'manila_ui.dashboards.admin.share_group_types.panel.ShareGroupTypes') +else: + REMOVE_PANEL = not features.is_share_groups_enabled() diff --git a/manila_ui/local/local_settings.d/_90_manila_shares.py b/manila_ui/local/local_settings.d/_90_manila_shares.py index 8e187f0a..95a6a45a 100644 --- a/manila_ui/local/local_settings.d/_90_manila_shares.py +++ b/manila_ui/local/local_settings.d/_90_manila_shares.py @@ -15,9 +15,11 @@ # The OPENSTACK_MANILA_FEATURES settings can be used to enable or disable # the UI for the various services provided by Manila. OPENSTACK_MANILA_FEATURES = { + 'enable_share_groups': True, 'enable_replication': True, 'enable_migration': True, 'enable_public_share_type_creation': True, + 'enable_public_share_group_type_creation': True, 'enable_public_shares': True, 'enabled_share_protocols': ['NFS', 'CIFS', 'GlusterFS', 'HDFS', 'CephFS', 'MapRFS'], diff --git a/manila_ui/tests/api/test_manila.py b/manila_ui/tests/api/test_manila.py index dbe94337..a5cef1d0 100644 --- a/manila_ui/tests/api/test_manila.py +++ b/manila_ui/tests/api/test_manila.py @@ -26,6 +26,37 @@ class ManilaApiTests(base.APITestCase): super(self.__class__, self).setUp() self.id = "fake_id" + @ddt.data((None, True), ("some_fake_sg_id", False)) + @ddt.unpack + def test_share_create(self, sg_id, is_public): + kwargs = { + "share_network": "fake_sn", + "snapshot_id": "fake_snapshot_id", + "metadata": {"k1": "v1", "k2": "v2"}, + "share_type": "fake_st", + "is_public": is_public, + "availability_zone": "fake_az", + "share_group_id": sg_id, + } + size = 5 + name = "fake_name" + desc = "fake_description" + proto = "fake_share_protocol" + + api.share_create(self.request, size, name, desc, proto, **kwargs) + + self.manilaclient.shares.create.assert_called_once_with( + proto, size, name=name, description=desc, **kwargs) + + @ddt.data(None, "some_fake_sg_id") + def test_share_delete(self, sg_id): + s_id = "fake_share_id" + + api.share_delete(self.request, s_id, sg_id) + + self.manilaclient.shares.delete.assert_called_once_with( + s_id, share_group_id=sg_id) + def test_list_share_export_locations(self): api.share_export_location_list(self.request, self.id) @@ -346,3 +377,294 @@ class ManilaApiTests(base.APITestCase): mock_sn_create = self.manilaclient.share_networks.create mock_sn_create.assert_called_once_with(**expected_kwargs) + + # Share groups tests + + def test_share_group_create(self): + name = "fake_sg_name" + kwargs = { + "description": "fake_desc", + "share_group_type": "fake_sg_type", + "share_types": ["fake", "list", "of", "fake", "share", "types"], + "share_network": "fake_sn", + "source_share_group_snapshot": "fake_source_share_group_snapshot", + "availability_zone": "fake_az", + } + + result = api.share_group_create(self.request, name, **kwargs) + + self.assertEqual( + self.manilaclient.share_groups.create.return_value, result) + self.manilaclient.share_groups.create.assert_called_once_with( + name=name, **kwargs) + + def test_share_group_get(self): + sg = "fake_share_group" + + result = api.share_group_get(self.request, sg) + + self.assertEqual( + self.manilaclient.share_groups.get.return_value, result) + self.manilaclient.share_groups.get.assert_called_once_with(sg) + + def test_share_group_update(self): + sg = "fake_share_group" + name = "fake_name" + desc = "fake_desc" + + result = api.share_group_update(self.request, sg, name, desc) + + self.assertEqual( + self.manilaclient.share_groups.update.return_value, result) + self.manilaclient.share_groups.update.assert_called_once_with( + sg, name=name, description=desc) + + @ddt.data({}, {"force": True}, {"force": False}) + def test_share_group_delete(self, kwargs): + sg = 'fake_share_group' + + api.share_group_delete(self.request, sg, **kwargs) + + self.manilaclient.share_groups.delete.assert_called_once_with( + sg, force=kwargs.get("force", False)) + + def test_share_group_reset_state(self): + sg = 'fake_share_group' + state = 'fake_state' + + result = api.share_group_reset_state(self.request, sg, state) + + self.assertIsNotNone(result) + self.assertEqual( + self.manilaclient.share_groups.reset_state.return_value, + result) + self.manilaclient.share_groups.reset_state.assert_called_once_with( + sg, state) + + @ddt.data( + {}, + {"detailed": True}, + {"detailed": False}, + {"search_opts": {"foo": "bar"}}, + {"sort_key": "id", "sort_dir": "asc"}, + ) + def test_share_group_list(self, kwargs): + result = api.share_group_list(self.request, **kwargs) + + self.assertEqual( + self.manilaclient.share_groups.list.return_value, result) + self.manilaclient.share_groups.list.assert_called_once_with( + detailed=kwargs.get("detailed", True), + search_opts=kwargs.get("search_opts"), + sort_key=kwargs.get("sort_key"), + sort_dir=kwargs.get("sort_dir"), + ) + + # Share Group Snapshots tests + + def test_share_group_snapshot_create(self): + sg = 'fake_share_group' + name = 'fake_name' + desc = 'fake_description' + + result = api.share_group_snapshot_create(self.request, sg, name, desc) + + self.assertIsNotNone(result) + self.assertEqual( + self.manilaclient.share_group_snapshots.create.return_value, + result) + self.manilaclient.share_group_snapshots.create.assert_called_once_with( + share_group=sg, name=name, description=desc) + + def test_share_group_snapshot_get(self): + sgs = 'fake_share_group_snapshot' + + result = api.share_group_snapshot_get(self.request, sgs) + + self.assertIsNotNone(result) + self.assertEqual( + self.manilaclient.share_group_snapshots.get.return_value, result) + self.manilaclient.share_group_snapshots.get.assert_called_once_with( + sgs) + + def test_share_group_snapshot_update(self): + sgs = 'fake_share_group_snapshot' + name = 'fake_name' + desc = 'fake_description' + + result = api.share_group_snapshot_update(self.request, sgs, name, desc) + + self.assertIsNotNone(result) + self.assertEqual( + self.manilaclient.share_group_snapshots.update.return_value, + result) + self.manilaclient.share_group_snapshots.update.assert_called_once_with( + sgs, name=name, description=desc) + + @ddt.data(True, False) + def test_share_group_snapshot_delete(self, force): + sgs = 'fake_share_group_snapshot' + + result = api.share_group_snapshot_delete(self.request, sgs, force) + + self.assertIsNotNone(result) + self.assertEqual( + self.manilaclient.share_group_snapshots.delete.return_value, + result) + self.manilaclient.share_group_snapshots.delete.assert_called_once_with( + sgs, force=force) + + def test_share_group_snapshot_reset_state(self): + sgs = 'fake_share_group_snapshot' + state = 'fake_state' + + result = api.share_group_snapshot_reset_state(self.request, sgs, state) + + rs_method = self.manilaclient.share_group_snapshots.reset_state + self.assertIsNotNone(result) + self.assertEqual(rs_method.return_value, result) + rs_method.assert_called_once_with(sgs, state) + + @ddt.data( + {}, + {'detailed': False}, + {'detailed': True, 'search_opts': 'foo', + 'sort_key': 'k', 'sort_dir': 'v'}, + ) + def test_share_group_snapshot_list(self, kwargs): + result = api.share_group_snapshot_list(self.request, **kwargs) + + self.assertIsNotNone(result) + self.assertEqual( + self.manilaclient.share_group_snapshots.list.return_value, + result) + self.manilaclient.share_group_snapshots.list.assert_called_once_with( + detailed=kwargs.get('detailed', True), + search_opts=kwargs.get('search_opts'), + sort_key=kwargs.get('sort_key'), + sort_dir=kwargs.get('sort_dir')) + + # Share Group Types tests + + @ddt.data( + {'is_public': True}, + {'is_public': False, 'group_specs': {'foo': 'bar'}}, + {'group_specs': {}}, + ) + def test_share_group_type_create(self, kwargs): + name = 'fake_sgt_name' + sts = ['fake', 'list', 'of', 'share', 'types'] + + result = api.share_group_type_create(self.request, name, sts, **kwargs) + + self.assertIsNotNone(result) + self.assertEqual( + self.manilaclient.share_group_types.create.return_value, + result) + self.manilaclient.share_group_types.create.assert_called_once_with( + name=name, share_types=sts, + is_public=kwargs.get('is_public', False), + group_specs=kwargs.get('group_specs')) + + def test_share_group_type_get(self): + sgt = "fake_sgt" + + result = api.share_group_type_get(self.request, sgt) + + self.assertIsNotNone(result) + self.assertEqual( + self.manilaclient.share_group_types.get.return_value, result) + self.manilaclient.share_group_types.get.assert_called_once_with(sgt) + + @ddt.data(True, False) + def test_share_group_type_list(self, show_all): + result = api.share_group_type_list(self.request, show_all) + + self.assertIsNotNone(result) + self.assertEqual( + self.manilaclient.share_group_types.list.return_value, result) + self.manilaclient.share_group_types.list.assert_called_once_with( + show_all=show_all) + + def test_share_group_type_delete(self): + sgt = 'fake_share_group_type' + + result = api.share_group_type_delete(self.request, sgt) + + self.assertIsNotNone(result) + self.assertEqual( + self.manilaclient.share_group_types.delete.return_value, result) + self.manilaclient.share_group_types.delete.assert_called_once_with(sgt) + + def test_share_group_type_access_list(self): + sgt = 'fake_share_group_type' + + result = api.share_group_type_access_list(self.request, sgt) + + self.assertIsNotNone(result) + self.assertEqual( + self.manilaclient.share_group_type_access.list.return_value, + result) + self.manilaclient.share_group_type_access.list.assert_called_once_with( + sgt) + + def test_share_group_type_access_add(self): + sgt = 'fake_share_group_type' + project = 'fake_project' + + result = api.share_group_type_access_add(self.request, sgt, project) + + sgt_access = self.manilaclient.share_group_type_access + self.assertIsNotNone(result) + self.assertEqual( + sgt_access.add_project_access.return_value, result) + sgt_access.add_project_access.assert_called_once_with(sgt, project) + + def test_share_group_type_access_remove(self): + sgt = 'fake_share_group_type' + project = 'fake_project' + + result = api.share_group_type_access_remove(self.request, sgt, project) + + sgt_access = self.manilaclient.share_group_type_access + self.assertIsNotNone(result) + self.assertEqual( + sgt_access.remove_project_access.return_value, result) + sgt_access.remove_project_access.assert_called_once_with(sgt, project) + + def test_share_group_type_set_specs(self): + sgt = 'fake_share_group_type' + group_specs = 'fake_specs' + + result = api.share_group_type_set_specs(self.request, sgt, group_specs) + + get_method = self.manilaclient.share_group_types.get + self.assertIsNotNone(result) + self.assertEqual(get_method.return_value.set_keys.return_value, result) + get_method.assert_called_once_with(sgt) + get_method.return_value.set_keys.assert_called_once_with(group_specs) + + def test_share_group_type_unset_specs(self): + sgt = 'fake_share_group_type' + keys = ['fake', 'list', 'of', 'keys', 'for', 'deletion'] + + result = api.share_group_type_unset_specs(self.request, sgt, keys) + + get_method = self.manilaclient.share_group_types.get + self.assertIsNotNone(result) + self.assertEqual( + get_method.return_value.unset_keys.return_value, result) + get_method.assert_called_once_with(sgt) + get_method.return_value.unset_keys.assert_called_once_with(keys) + + def test_share_group_type_get_specs(self): + sgt = 'fake_share_group_type' + + result = api.share_group_type_get_specs(self.request, sgt) + + get_method = self.manilaclient.share_group_types.get + self.assertIsNotNone(result) + self.assertEqual( + get_method.return_value.get_keys.return_value, result) + get_method.assert_called_once_with(sgt) + get_method.return_value.get_keys.assert_called_once_with() diff --git a/manila_ui/tests/dashboards/admin/share_group_snapshots/__init__.py b/manila_ui/tests/dashboards/admin/share_group_snapshots/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/manila_ui/tests/dashboards/admin/share_group_snapshots/tests.py b/manila_ui/tests/dashboards/admin/share_group_snapshots/tests.py new file mode 100644 index 00000000..fe5f5f1a --- /dev/null +++ b/manila_ui/tests/dashboards/admin/share_group_snapshots/tests.py @@ -0,0 +1,200 @@ +# Copyright (c) 2017 Mirantis, Inc. +# 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 ddt +from django.core.urlresolvers import reverse +import mock + +from manila_ui.api import manila as api_manila +from manila_ui.tests.dashboards.project import test_data +from manila_ui.tests import helpers as test + +INDEX_URL = reverse('horizon:admin:share_group_snapshots:index') + + +@ddt.ddt +class ShareGroupSnapshotTests(test.BaseAdminViewTests): + + def setUp(self): + super(self.__class__, self).setUp() + self.sg = test_data.share_group + self.sg_nl = test_data.share_group_nameless + self.sg_dhss_true = test_data.share_group_dhss_true + self.sgs = test_data.share_group_snapshot + self.sgs_nl = test_data.share_group_snapshot_nameless + + def test_share_group_snapshots_list_get(self): + share_groups = [self.sg, self.sg_nl, self.sg_dhss_true] + sgss = [self.sgs, self.sgs_nl] + self.mock_object( + api_manila, 'share_group_snapshot_list', + mock.Mock(return_value=sgss)) + self.mock_object( + api_manila, 'share_group_list', + mock.Mock(return_value=share_groups)) + + res = self.client.get(INDEX_URL) + + self.assertStatusCode(res, 200) + self.assertMessageCount(error=0) + self.assertNoMessages() + api_manila.share_group_snapshot_list.assert_called_once_with( + mock.ANY, search_opts={'all_tenants': True}) + api_manila.share_group_list.assert_called_once_with(mock.ANY) + self.assertTemplateUsed(res, 'admin/share_group_snapshots/index.html') + self.assertContains(res, "

Share Group Snapshots

") + self.assertContains( + res, 'Delete Share Group Snapshot', len(sgss)) + self.assertContains(res, 'Delete Share Group Snapshots', 1) + for sgs in sgss: + self.assertContains( + res, + 'href="/admin/share_group_snapshots/%s/reset_status"> ' + 'Reset status' % sgs.id, 1) + self.assertContains( + res, 'value="share_group_snapshots__delete__%s' % sgs.id, 1) + self.assertContains( + res, + '%(name)s' % { + 'id': self.sgs.id, 'name': self.sgs.name}, 1) + self.assertContains( + res, + '-' % self.sgs_nl.id, 1) + + def test_share_group_snapshots_list_error_get(self): + self.mock_object( + api_manila, 'share_group_snapshot_list', + mock.Mock(side_effect=type('CustomExc', (Exception, ), {}))) + + res = self.client.get(INDEX_URL) + + self.assertStatusCode(res, 200) + self.assertTemplateUsed(res, 'admin/share_group_snapshots/index.html') + self.assertContains(res, "

Share Group Snapshots

") + self.assertContains(res, 'Delete Share Group Snapshot', 0) + self.assertContains(res, 'Delete Share Group Snapshots', 0) + self.assertNoMessages() + + @ddt.data( + test_data.share_group_snapshot, + test_data.share_group_snapshot_nameless, + ) + def test_share_group_snapshot_detailed_page_get(self, sgs): + sg = test_data.share_group_nameless + url = reverse( + 'horizon:admin:share_group_snapshots:detail', args=[sgs.id]) + self.mock_object( + api_manila, 'share_group_snapshot_get', + mock.Mock(return_value=sgs)) + self.mock_object( + api_manila, 'share_group_get', mock.Mock(return_value=sg)) + + res = self.client.get(url) + + self.assertTemplateUsed(res, 'admin/share_group_snapshots/detail.html') + self.assertStatusCode(res, 200) + api_manila.share_group_snapshot_get.assert_called_once_with( + mock.ANY, sgs.id) + api_manila.share_group_get.assert_called_once_with( + mock.ANY, sgs.share_group_id) + + def test_share_group_snapshot_detailed_page_error_get(self): + url = reverse( + 'horizon:admin:share_group_snapshots:detail', args=[self.sgs.id]) + self.mock_object( + api_manila, 'share_group_snapshot_get', + mock.Mock(side_effect=type('CustomExc', (Exception, ), {}))) + + res = self.client.get(url) + + self.assertTemplateNotUsed( + res, 'admin/share_group_snapshots/detail.html') + self.assertStatusCode(res, 302) + self.assertMessageCount(error=1) + self.assertMessageCount(success=0) + + def test_share_group_snapshot_reset_status_get(self): + url = reverse( + 'horizon:admin:share_group_snapshots:reset_status', + args=[self.sgs.id]) + self.mock_object( + api_manila, 'share_group_snapshot_get', + mock.Mock(return_value=self.sgs)) + + res = self.client.get(url) + + self.assertTemplateUsed( + res, 'admin/share_group_snapshots/reset_status.html') + self.assertStatusCode(res, 200) + self.assertMessageCount(error=0) + api_manila.share_group_snapshot_get.assert_called_once_with( + mock.ANY, self.sgs.id) + + def test_share_group_snapshot_reset_status_error_get(self): + url = reverse( + 'horizon:admin:share_group_snapshots:reset_status', + args=[self.sgs.id]) + self.mock_object( + api_manila, 'share_group_snapshot_get', + mock.Mock(side_effect=type('CustomExc', (Exception, ), {}))) + + res = self.client.get(url) + + self.assertTemplateNotUsed( + res, 'admin/share_group_snapshots/reset_status.html') + self.assertStatusCode(res, 302) + self.assertMessageCount(error=1) + api_manila.share_group_snapshot_get.assert_called_once_with( + mock.ANY, self.sgs.id) + + def test_share_group_snapshot_reset_status_post(self): + url = reverse( + 'horizon:admin:share_group_snapshots:reset_status', + args=[self.sgs.id]) + self.mock_object( + api_manila, 'share_group_snapshot_get', + mock.Mock(return_value=self.sgs)) + self.mock_object(api_manila, 'share_group_snapshot_reset_state') + form_data = {'status': 'error'} + + res = self.client.post(url, form_data) + + self.assertTemplateNotUsed( + res, 'admin/share_group_snapshots/reset_status.html') + self.assertStatusCode(res, 302) + self.assertMessageCount(error=0) + api_manila.share_group_snapshot_get.assert_called_once_with( + mock.ANY, self.sgs.id) + api_manila.share_group_snapshot_reset_state.assert_called_once_with( + mock.ANY, self.sgs.id, form_data['status']) + self.assertRedirectsNoFollow(res, INDEX_URL) + + def test_share_group_snapshot_delete_post(self): + data = {'action': 'share_group_snapshots__delete__%s' % self.sgs.id} + self.mock_object(api_manila, "share_group_snapshot_delete") + self.mock_object( + api_manila, "share_group_snapshot_list", + mock.Mock(return_value=[self.sgs])) + + res = self.client.post(INDEX_URL, data) + + self.assertStatusCode(res, 302) + self.assertMessageCount(success=1) + api_manila.share_group_snapshot_delete.assert_called_once_with( + mock.ANY, self.sgs.id) + api_manila.share_group_snapshot_list.assert_called_once_with( + mock.ANY, search_opts={'all_tenants': True}) + self.assertRedirectsNoFollow(res, INDEX_URL) diff --git a/manila_ui/tests/dashboards/admin/share_group_types/__init__.py b/manila_ui/tests/dashboards/admin/share_group_types/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/manila_ui/tests/dashboards/admin/share_group_types/tests.py b/manila_ui/tests/dashboards/admin/share_group_types/tests.py new file mode 100644 index 00000000..927bd21d --- /dev/null +++ b/manila_ui/tests/dashboards/admin/share_group_types/tests.py @@ -0,0 +1,142 @@ +# Copyright (c) 2017 Mirantis, Inc. +# 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 copy + +from django.core.urlresolvers import reverse +import mock + +from manila_ui.api import manila as api_manila +from manila_ui.tests.dashboards.project import test_data +from manila_ui.tests import helpers as test + +INDEX_URL = reverse('horizon:admin:share_group_types:index') + + +class ShareGroupTypeTests(test.BaseAdminViewTests): + + def setUp(self): + super(self.__class__, self).setUp() + self.sgt = copy.copy(test_data.share_group_type) + self.sgt_p = copy.copy(test_data.share_group_type_private) + self.url = reverse( + 'horizon:admin:share_group_types:update', args=[self.sgt.id]) + self.mock_object( + api_manila, "share_group_type_get", + mock.Mock(return_value=self.sgt)) + + def test_list_share_group_types_get(self): + sgts = [self.sgt, self.sgt_p] + self.mock_object( + api_manila, "share_group_type_list", mock.Mock(return_value=sgts)) + self.mock_object( + api_manila, "share_type_list", + mock.Mock(return_value=[ + test_data.share_type, test_data.share_type_private])) + self.mock_object( + api_manila, "share_group_type_get", + mock.Mock(side_effect=lambda req, sgt_id: ( + self.sgt + if sgt_id in (self.sgt, self.sgt.id, self.sgt.name) else + self.sgt_p))) + + res = self.client.get(INDEX_URL) + + api_manila.share_group_type_list.assert_called_once_with(mock.ANY) + api_manila.share_type_list.assert_called_once_with(mock.ANY) + self.assertEqual(len(sgts), api_manila.share_group_type_get.call_count) + self.assertContains(res, "

Share Group Types

") + self.assertContains(res, 'href="/admin/share_group_types/create"') + self.assertContains(res, 'Delete Share Group Type', len(sgts)) + self.assertContains(res, 'Delete Share Group Types', 1) + for sgt in (self.sgt, self.sgt_p): + for s in ( + 'href="/admin/share_group_types/%s/update">' % sgt.id, + 'value="share_group_types__delete__%s"' % sgt.id): + self.assertContains(res, s, 1, 200) + self.assertContains( + res, + 'href="/admin/share_group_types/%s/manage_access"' % sgt.id, + 0 if sgt.is_public else 1) + + def test_create_share_group_type_post(self): + url = reverse('horizon:admin:share_group_types:create') + data = { + 'method': u'CreateShareGroupTypeForm', + 'is_public': True, + 'name': 'my_share_group_type', + 'share_types': ['foo'], + 'group_specs': 'key=value', + } + self.mock_object( + api_manila, "share_group_type_create", + mock.Mock(return_value=type( + 'SGT', (object, ), {'id': 'sgt_id', 'name': 'sgt_name'}))) + self.mock_object(api_manila, "share_group_type_set_specs") + self.mock_object( + api_manila, "share_type_list", + mock.Mock(return_value=[ + type('ST', (object, ), {'id': s_id, 'name': s_id + '_name'}) + for s_id in ('foo', 'bar') + ])) + + res = self.client.post(url, data) + + api_manila.share_group_type_create.assert_called_once_with( + mock.ANY, + data['name'], + is_public=data['is_public'], + share_types=['foo']) + api_manila.share_type_list.assert_called_once_with(mock.ANY) + api_manila.share_group_type_set_specs.assert_called_once_with( + mock.ANY, 'sgt_id', {'key': 'value'}) + self.assertRedirectsNoFollow(res, INDEX_URL) + + def test_update_share_group_type_get(self): + res = self.client.get(self.url) + + api_manila.share_group_type_get.assert_called_once_with( + mock.ANY, self.sgt.id) + self.assertNoMessages() + self.assertTemplateUsed(res, 'admin/share_group_types/update.html') + + def test_update_share_group_type_post(self): + form_data = {'group_specs': 'foo=bar'} + data = {'group_specs': {'foo': 'bar'}} + self.mock_object(api_manila, "share_group_type_set_specs") + + res = self.client.post(self.url, form_data) + + api_manila.share_group_type_set_specs.assert_called_once_with( + mock.ANY, self.sgt.id, data['group_specs']) + self.assertRedirectsNoFollow(res, INDEX_URL) + + def test_delete_share_group_type(self): + data = {'action': 'share_group_types__delete__%s' % self.sgt.id} + self.mock_object(api_manila, "share_group_type_delete") + self.mock_object( + api_manila, "share_group_type_list", + mock.Mock(return_value=[self.sgt])) + self.mock_object( + api_manila, "share_type_list", + mock.Mock(return_value=[test_data.share_type])) + + res = self.client.post(INDEX_URL, data) + + api_manila.share_group_type_delete.assert_called_once_with( + mock.ANY, self.sgt.id) + api_manila.share_group_type_list.assert_called_once_with( + mock.ANY) + self.assertRedirectsNoFollow(res, INDEX_URL) diff --git a/manila_ui/tests/dashboards/admin/share_groups/__init__.py b/manila_ui/tests/dashboards/admin/share_groups/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/manila_ui/tests/dashboards/admin/share_groups/tests.py b/manila_ui/tests/dashboards/admin/share_groups/tests.py new file mode 100644 index 00000000..af0417a7 --- /dev/null +++ b/manila_ui/tests/dashboards/admin/share_groups/tests.py @@ -0,0 +1,186 @@ +# Copyright (c) 2017 Mirantis, Inc. +# 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 ddt +from django.core.urlresolvers import reverse +import mock + +from manila_ui.api import manila as api_manila +from manila_ui.tests.dashboards.project import test_data +from manila_ui.tests import helpers as test + +INDEX_URL = reverse('horizon:admin:share_groups:index') + + +@ddt.ddt +class ShareGroupTests(test.BaseAdminViewTests): + + def setUp(self): + super(self.__class__, self).setUp() + self.sg = test_data.share_group + self.sg_nl = test_data.share_group_nameless + self.sg_dhss_true = test_data.share_group_dhss_true + + def test_share_groups_list_get(self): + sgs = [self.sg, self.sg_nl, self.sg_dhss_true] + self.mock_object( + api_manila, 'share_group_list', mock.Mock(return_value=sgs)) + + res = self.client.get(INDEX_URL) + + self.assertStatusCode(res, 200) + api_manila.share_group_list.assert_called_once_with( + mock.ANY, detailed=True) + self.assertTemplateUsed(res, 'admin/share_groups/index.html') + self.assertContains(res, "

Share Groups

") + self.assertContains(res, 'Delete Share Group', len(sgs)) + self.assertContains(res, 'Delete Share Groups', 1) + for sg in sgs: + self.assertContains( + res, + 'href="/admin/share_groups/%s/reset_status"> ' + 'Reset status' % sg.id, 1) + self.assertContains( + res, 'value="share_groups__delete__%s' % sg.id, 1) + self.assertContains( + res, + '%(name)s' % { + 'id': sg.id, 'name': sg.name}, 1) + self.assertContains( + res, + '-' % self.sg_nl.id, 1) + self.assertNoMessages() + + def test_share_group_list_error_get(self): + self.mock_object( + api_manila, 'share_group_list', + mock.Mock(side_effect=type('CustomExc', (Exception, ), {}))) + + res = self.client.get(INDEX_URL) + + self.assertStatusCode(res, 200) + self.assertTemplateUsed(res, 'admin/share_groups/index.html') + self.assertContains(res, "

Share Groups

") + self.assertContains(res, 'Delete Share Group', 0) + self.assertContains(res, 'Delete Share Groups', 0) + self.assertNoMessages() + + @ddt.data( + test_data.share_group_nameless, + test_data.share_group_dhss_true, + ) + def test_share_group_detailed_page_get(self, sg): + url = reverse('horizon:admin:share_groups:detail', args=[sg.id]) + shares = [test_data.share, test_data.nameless_share] + self.mock_object( + api_manila, 'share_group_get', mock.Mock(return_value=sg)) + self.mock_object( + api_manila, 'share_list', mock.Mock(return_value=shares)) + self.mock_object( + api_manila, 'share_type_list', mock.Mock(return_value=[ + test_data.share_type, test_data.share_type_dhss_true])) + + res = self.client.get(url) + + self.assertTemplateUsed(res, 'admin/share_groups/detail.html') + self.assertStatusCode(res, 200) + for share in shares: + data = {'id': share.id, 'name': share.name or share.id} + self.assertContains( + res, '%(name)s' % data) + + def test_share_group_detailed_page_error_get(self): + sg = test_data.share_group_dhss_true + url = reverse('horizon:admin:share_groups:detail', args=[sg.id]) + self.mock_object( + api_manila, 'share_group_get', mock.Mock(side_effect=type( + 'CustomExc', (Exception, ), {}))) + + res = self.client.get(url) + + self.assertTemplateNotUsed(res, 'admin/share_groups/detail.html') + self.assertStatusCode(res, 302) + self.assertMessageCount(error=1) + self.assertMessageCount(success=0) + + def test_share_group_reset_status_get(self): + sg = test_data.share_group_dhss_true + url = reverse('horizon:admin:share_groups:reset_status', args=[sg.id]) + self.mock_object( + api_manila, 'share_group_get', mock.Mock(return_value=sg)) + + res = self.client.get(url) + + self.assertTemplateUsed(res, 'admin/share_groups/reset_status.html') + self.assertStatusCode(res, 200) + self.assertMessageCount(error=0) + api_manila.share_group_get.assert_called_once_with(mock.ANY, sg.id) + + def test_share_group_reset_status_error_get(self): + sg = test_data.share_group_dhss_true + url = reverse('horizon:admin:share_groups:reset_status', args=[sg.id]) + self.mock_object( + api_manila, 'share_group_get', + mock.Mock(side_effect=type('CustomExc', (Exception, ), {}))) + + res = self.client.get(url) + + self.assertTemplateNotUsed(res, 'admin/share_groups/reset_status.html') + self.assertStatusCode(res, 302) + self.assertMessageCount(error=1) + api_manila.share_group_get.assert_called_once_with(mock.ANY, sg.id) + + def test_share_group_reset_status_post(self): + sg = test_data.share_group_dhss_true + url = reverse('horizon:admin:share_groups:reset_status', args=[sg.id]) + self.mock_object( + api_manila, 'share_group_get', mock.Mock(return_value=sg)) + self.mock_object(api_manila, 'share_group_reset_state') + form_data = {'status': 'error'} + + res = self.client.post(url, form_data) + + self.assertTemplateNotUsed(res, 'admin/share_groups/reset_status.html') + self.assertStatusCode(res, 302) + self.assertMessageCount(error=0) + api_manila.share_group_get.assert_called_once_with(mock.ANY, sg.id) + api_manila.share_group_reset_state.assert_called_once_with( + mock.ANY, sg.id, form_data['status']) + self.assertRedirectsNoFollow(res, INDEX_URL) + + def test_share_group_delete_post(self): + sg = test_data.share_group_dhss_true + data = {'action': 'share_groups__delete__%s' % sg.id} + self.mock_object(api_manila, "share_group_delete") + self.mock_object( + api_manila, "share_group_list", mock.Mock(return_value=[sg])) + + res = self.client.post(INDEX_URL, data) + + self.assertStatusCode(res, 302) + self.assertMessageCount(success=1) + api_manila.share_group_delete.assert_called_once_with(mock.ANY, sg.id) + api_manila.share_group_list.assert_called_once_with( + mock.ANY, detailed=True) + self.assertRedirectsNoFollow(res, INDEX_URL) diff --git a/manila_ui/tests/dashboards/admin/shares/tests.py b/manila_ui/tests/dashboards/admin/shares/tests.py index 8a7dbcfe..487cf129 100644 --- a/manila_ui/tests/dashboards/admin/shares/tests.py +++ b/manila_ui/tests/dashboards/admin/shares/tests.py @@ -85,7 +85,8 @@ class SharesTests(test.BaseAdminViewTests): res = self.client.post(url, formData) api_keystone.tenant_list.assert_called_once_with(mock.ANY) - api_manila.share_delete.assert_called_once_with(mock.ANY, share.id) + api_manila.share_delete.assert_called_once_with( + mock.ANY, share.id, share_group_id=share.share_group_id) api_manila.share_list.assert_called_once_with( mock.ANY, search_opts={'all_tenants': True}) api_manila.share_snapshot_list.assert_called_once_with( diff --git a/manila_ui/tests/dashboards/project/share_group_snapshots/__init__.py b/manila_ui/tests/dashboards/project/share_group_snapshots/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/manila_ui/tests/dashboards/project/share_group_snapshots/tests.py b/manila_ui/tests/dashboards/project/share_group_snapshots/tests.py new file mode 100644 index 00000000..a012cb71 --- /dev/null +++ b/manila_ui/tests/dashboards/project/share_group_snapshots/tests.py @@ -0,0 +1,295 @@ +# Copyright (c) 2017 Mirantis, Inc. +# 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 ddt +from django.core.urlresolvers import reverse +import mock + +from manila_ui.api import manila as api_manila +from manila_ui.tests.dashboards.project import test_data +from manila_ui.tests import helpers as test + +INDEX_URL = reverse('horizon:project:share_group_snapshots:index') + + +@ddt.ddt +class ShareGroupSnapshotTests(test.TestCase): + + def setUp(self): + super(self.__class__, self).setUp() + self.sg = test_data.share_group + self.sg_nl = test_data.share_group_nameless + self.sg_dhss_true = test_data.share_group_dhss_true + self.sgs = test_data.share_group_snapshot + self.sgs_nl = test_data.share_group_snapshot_nameless + + def test_share_group_snapshots_list_get(self): + share_groups = [self.sg, self.sg_nl, self.sg_dhss_true] + sgss = [self.sgs, self.sgs_nl] + self.mock_object( + api_manila, 'share_group_snapshot_list', + mock.Mock(return_value=sgss)) + self.mock_object( + api_manila, 'share_group_list', + mock.Mock(return_value=share_groups)) + + res = self.client.get(INDEX_URL) + + self.assertStatusCode(res, 200) + self.assertMessageCount(error=0) + self.assertNoMessages() + api_manila.share_group_snapshot_list.assert_called_once_with( + mock.ANY, search_opts={'all_tenants': True}) + api_manila.share_group_list.assert_called_once_with(mock.ANY) + self.assertTemplateUsed( + res, 'project/share_group_snapshots/index.html') + self.assertContains(res, "

Share Group Snapshots

") + self.assertContains( + res, 'Delete Share Group Snapshot', len(sgss)) + self.assertContains(res, 'Delete Share Group Snapshots', 1) + for sgs in sgss: + self.assertContains( + res, 'value="share_group_snapshots__delete__%s' % sgs.id, 1) + self.assertContains( + res, + '%(name)s-' % self.sgs_nl.id, 1) + + def test_share_group_snapshots_list_error_get(self): + self.mock_object( + api_manila, 'share_group_snapshot_list', + mock.Mock(side_effect=type('CustomExc', (Exception, ), {}))) + + res = self.client.get(INDEX_URL) + + self.assertStatusCode(res, 200) + self.assertTemplateUsed( + res, 'project/share_group_snapshots/index.html') + self.assertContains(res, "

Share Group Snapshots

") + self.assertContains(res, 'Delete Share Group Snapshot', 0) + self.assertContains(res, 'Delete Share Group Snapshots', 0) + self.assertNoMessages() + + @ddt.data( + test_data.share_group_snapshot, + test_data.share_group_snapshot_nameless, + ) + def test_share_group_snapshot_detailed_page_get(self, sgs): + sg = test_data.share_group_nameless + url = reverse( + 'horizon:project:share_group_snapshots:detail', args=[sgs.id]) + self.mock_object( + api_manila, 'share_group_snapshot_get', + mock.Mock(return_value=sgs)) + self.mock_object( + api_manila, 'share_group_get', mock.Mock(return_value=sg)) + + res = self.client.get(url) + + self.assertTemplateUsed( + res, 'project/share_group_snapshots/detail.html') + self.assertStatusCode(res, 200) + api_manila.share_group_snapshot_get.assert_called_once_with( + mock.ANY, sgs.id) + api_manila.share_group_get.assert_called_once_with( + mock.ANY, sgs.share_group_id) + + def test_share_group_snapshot_detailed_page_error_get(self): + url = reverse( + 'horizon:project:share_group_snapshots:detail', args=[self.sgs.id]) + self.mock_object( + api_manila, 'share_group_snapshot_get', + mock.Mock(side_effect=type('CustomExc', (Exception, ), {}))) + + res = self.client.get(url) + + self.assertTemplateNotUsed( + res, 'admin/share_group_snapshots/detail.html') + self.assertStatusCode(res, 302) + self.assertMessageCount(error=1) + self.assertMessageCount(success=0) + + @ddt.data( + lambda *args, **kwargs: test_data.share_group, + type('CustomExc', (Exception, ), {}), + ) + def test_share_group_snapshot_create_get(self, sg_get_side_effect): + url = reverse( + 'horizon:project:share_group_snapshots:create', args=[self.sg.id]) + self.mock_object( + api_manila, 'share_group_get', + mock.Mock(side_effect=sg_get_side_effect)) + + res = self.client.get(url) + + self.assertTemplateUsed( + res, 'project/share_group_snapshots/create.html') + self.assertStatusCode(res, 200) + self.assertNoMessages() + api_manila.share_group_get.assert_called_once_with( + mock.ANY, self.sg.id) + self.assertMessageCount(error=0) + + def test_share_group_snapshot_create_post(self): + url = reverse( + 'horizon:project:share_group_snapshots:create', args=[self.sg.id]) + form_data = { + 'method': 'CreateShareGroupSnapshotForm', + 'name': 'fake_SG_snapshot_name', + 'description': 'fake SG snapshot description', + 'share_group_id': self.sg.id, + } + self.mock_object( + api_manila, "share_group_snapshot_create", + mock.Mock(return_value=self.sgs)) + + res = self.client.post(url, form_data) + + self.assertTemplateNotUsed( + res, 'project/share_group_snapshots/create.html') + self.assertStatusCode(res, 302) + self.assertRedirectsNoFollow(res, INDEX_URL) + self.assertMessageCount(error=0) + self.assertMessageCount(success=1) + api_manila.share_group_snapshot_create.assert_called_once_with( + mock.ANY, self.sg.id, form_data['name'], form_data['description']) + + def test_share_group_snapshot_create_error_post(self): + url = reverse( + 'horizon:project:share_group_snapshots:create', args=[self.sg.id]) + form_data = { + 'method': 'CreateShareGroupSnapshotForm', + 'name': 'fake_SG_snapshot_name', + 'description': 'fake SG snapshot description', + 'share_group_id': self.sg.id, + } + self.mock_object( + api_manila, "share_group_snapshot_create", + mock.Mock(side_effect=type('CustomExc', (Exception, ), {}))) + + res = self.client.post(url, form_data) + + self.assertTemplateNotUsed( + res, 'project/share_group_snapshots/create.html') + self.assertStatusCode(res, 302) + self.assertRedirectsNoFollow(res, INDEX_URL) + self.assertMessageCount(error=1) + self.assertMessageCount(success=0) + api_manila.share_group_snapshot_create.assert_called_once_with( + mock.ANY, self.sg.id, form_data['name'], form_data['description']) + + def test_share_group_snapshot_update_get(self): + url = reverse( + 'horizon:project:share_group_snapshots:update', args=[self.sgs.id]) + self.mock_object( + api_manila, 'share_group_snapshot_get', + mock.Mock(return_value=self.sgs)) + + res = self.client.get(url) + + self.assertTemplateUsed( + res, 'project/share_group_snapshots/update.html') + self.assertStatusCode(res, 200) + self.assertNoMessages() + api_manila.share_group_snapshot_get.assert_called_once_with( + mock.ANY, self.sgs.id) + + def test_share_group_snapshot_update_error_get(self): + url = reverse( + 'horizon:project:share_group_snapshots:update', args=[self.sgs.id]) + self.mock_object( + api_manila, 'share_group_snapshot_get', + mock.Mock(side_effect=type('CustomExc', (Exception, ), {}))) + + res = self.client.get(url) + + self.assertTemplateNotUsed( + res, 'project/share_group_snapshots/update.html') + self.assertStatusCode(res, 302) + self.assertRedirectsNoFollow(res, INDEX_URL) + self.assertMessageCount(error=1) + api_manila.share_group_snapshot_get.assert_called_once_with( + mock.ANY, self.sgs.id) + + def test_share_group_snapshot_update_post(self): + self.mock_object(api_manila, "share_group_snapshot_update") + self.mock_object( + api_manila, 'share_group_snapshot_get', + mock.Mock(return_value=self.sgs)) + form_data = { + 'method': 'UpdateShareGroupSnapshotForm', + 'name': self.sgs.name, + 'description': self.sgs.description, + } + url = reverse( + 'horizon:project:share_group_snapshots:update', args=[self.sgs.id]) + + res = self.client.post(url, form_data) + + self.assertStatusCode(res, 302) + self.assertRedirectsNoFollow(res, INDEX_URL) + api_manila.share_group_snapshot_update.assert_called_once_with( + mock.ANY, self.sgs.id, form_data['name'], form_data['description']) + api_manila.share_group_snapshot_get.assert_called_once_with( + mock.ANY, self.sgs.id) + self.assertMessageCount(error=0) + self.assertMessageCount(success=1) + + def test_share_group_snapshot_update_error_post(self): + self.mock_object( + api_manila, "share_group_snapshot_update", + mock.Mock(side_effect=type('CustomExc', (Exception, ), {}))) + self.mock_object( + api_manila, 'share_group_snapshot_get', + mock.Mock(return_value=self.sgs)) + form_data = { + 'method': 'UpdateShareGroupSnapshotForm', + 'name': self.sgs.name, + 'description': self.sgs.description, + } + url = reverse( + 'horizon:project:share_group_snapshots:update', args=[self.sgs.id]) + + res = self.client.post(url, form_data) + + self.assertStatusCode(res, 302) + self.assertRedirectsNoFollow(res, INDEX_URL) + api_manila.share_group_snapshot_update.assert_called_once_with( + mock.ANY, self.sgs.id, form_data['name'], form_data['description']) + api_manila.share_group_snapshot_get.assert_called_once_with( + mock.ANY, self.sgs.id) + self.assertMessageCount(error=1) + self.assertMessageCount(success=0) + + def test_share_group_snapshot_delete_post(self): + data = {'action': 'share_group_snapshots__delete__%s' % self.sgs.id} + self.mock_object(api_manila, "share_group_snapshot_delete") + self.mock_object( + api_manila, "share_group_snapshot_list", + mock.Mock(return_value=[self.sgs])) + + res = self.client.post(INDEX_URL, data) + + self.assertStatusCode(res, 302) + self.assertMessageCount(success=1) + api_manila.share_group_snapshot_delete.assert_called_once_with( + mock.ANY, self.sgs.id) + api_manila.share_group_snapshot_list.assert_called_once_with( + mock.ANY, search_opts={'all_tenants': True}) + self.assertRedirectsNoFollow(res, INDEX_URL) diff --git a/manila_ui/tests/dashboards/project/share_groups/__init__.py b/manila_ui/tests/dashboards/project/share_groups/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/manila_ui/tests/dashboards/project/share_groups/tests.py b/manila_ui/tests/dashboards/project/share_groups/tests.py new file mode 100644 index 00000000..d1e9b4f6 --- /dev/null +++ b/manila_ui/tests/dashboards/project/share_groups/tests.py @@ -0,0 +1,383 @@ +# Copyright (c) 2017 Mirantis, Inc. +# 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 ddt +from django.core.urlresolvers import reverse +import mock + +from manila_ui.api import manila as api_manila +from manila_ui.tests.dashboards.project import test_data +from manila_ui.tests import helpers as test + +INDEX_URL = reverse('horizon:project:share_groups:index') + + +@ddt.ddt +class ShareGroupTests(test.TestCase): + + def setUp(self): + super(self.__class__, self).setUp() + self.sg = test_data.share_group + self.sg_nl = test_data.share_group_nameless + self.sg_dhss_true = test_data.share_group_dhss_true + + def test_share_groups_list_get(self): + sgs = [self.sg, self.sg_nl, self.sg_dhss_true] + self.mock_object( + api_manila, 'share_group_list', mock.Mock(return_value=sgs)) + + res = self.client.get(INDEX_URL) + + self.assertStatusCode(res, 200) + api_manila.share_group_list.assert_called_once_with( + mock.ANY, detailed=True) + self.assertTemplateUsed(res, 'project/share_groups/index.html') + self.assertContains(res, "

Share Groups

") + self.assertContains(res, 'Delete Share Group', len(sgs)) + self.assertContains(res, 'Delete Share Groups', 1) + for sg in sgs: + self.assertContains( + res, 'value="share_groups__delete__%s' % sg.id, 1) + self.assertContains( + res, + '%(name)s' % { + 'id': sg.id, 'name': sg.name}, 1) + self.assertContains( + res, + '-' % self.sg_nl.id, 1) + self.assertNoMessages() + + def test_share_group_list_error_get(self): + self.mock_object( + api_manila, 'share_group_list', + mock.Mock(side_effect=type('CustomExc', (Exception, ), {}))) + + res = self.client.get(INDEX_URL) + + self.assertStatusCode(res, 200) + self.assertTemplateUsed(res, 'project/share_groups/index.html') + self.assertContains(res, "

Share Groups

") + self.assertContains(res, 'Delete Share Group', 0) + self.assertContains(res, 'Delete Share Groups', 0) + self.assertNoMessages() + + @ddt.data( + test_data.share_group_nameless, + test_data.share_group_dhss_true, + ) + def test_share_group_detailed_page_get(self, sg): + url = reverse('horizon:project:share_groups:detail', args=[sg.id]) + shares = [test_data.share, test_data.nameless_share] + self.mock_object( + api_manila, 'share_group_get', mock.Mock(return_value=sg)) + self.mock_object( + api_manila, 'share_list', mock.Mock(return_value=shares)) + self.mock_object( + api_manila, 'share_type_list', mock.Mock(return_value=[ + test_data.share_type, test_data.share_type_dhss_true])) + + res = self.client.get(url) + + self.assertTemplateUsed(res, 'project/share_groups/detail.html') + self.assertStatusCode(res, 200) + for share in shares: + data = {'id': share.id, 'name': share.name or share.id} + self.assertContains( + res, '%(name)s' % data) + + def test_share_group_detailed_page_error_get(self): + url = reverse('horizon:project:share_groups:detail', args=[self.sg.id]) + self.mock_object( + api_manila, 'share_group_get', + mock.Mock(side_effect=type('CustomExc', (Exception, ), {}))) + + res = self.client.get(url) + + self.assertTemplateNotUsed(res, 'project/share_groups/detail.html') + self.assertStatusCode(res, 302) + self.assertMessageCount(error=1) + self.assertMessageCount(success=0) + + def test_share_group_update_get(self): + url = reverse('horizon:project:share_groups:update', args=[self.sg.id]) + self.mock_object( + api_manila, 'share_group_get', mock.Mock(return_value=self.sg)) + + res = self.client.get(url) + + self.assertTemplateUsed(res, 'project/share_groups/update.html') + self.assertStatusCode(res, 200) + self.assertNoMessages() + api_manila.share_group_get.assert_called_once_with( + mock.ANY, self.sg.id) + + def test_share_group_update_error_get(self): + url = reverse('horizon:project:share_groups:update', args=[self.sg.id]) + self.mock_object( + api_manila, 'share_group_get', + mock.Mock(side_effect=type('CustomExc', (Exception, ), {}))) + + res = self.client.get(url) + + self.assertTemplateNotUsed(res, 'project/share_groups/update.html') + self.assertStatusCode(res, 302) + self.assertRedirectsNoFollow(res, INDEX_URL) + self.assertMessageCount(error=1) + api_manila.share_group_get.assert_called_once_with( + mock.ANY, self.sg.id) + + def test_share_group_update_post(self): + self.mock_object(api_manila, "share_group_update") + self.mock_object( + api_manila, 'share_group_get', mock.Mock(return_value=self.sg)) + form_data = { + 'method': 'UpdateShareGroupForm', + 'name': self.sg.name, + 'description': self.sg.description, + } + url = reverse('horizon:project:share_groups:update', args=[self.sg.id]) + + res = self.client.post(url, form_data) + + self.assertStatusCode(res, 302) + self.assertRedirectsNoFollow(res, INDEX_URL) + api_manila.share_group_update.assert_called_once_with( + mock.ANY, self.sg.id, form_data['name'], form_data['description']) + api_manila.share_group_get.assert_called_once_with( + mock.ANY, self.sg.id) + self.assertMessageCount(error=0) + self.assertMessageCount(success=1) + + def test_share_group_update_error_post(self): + self.mock_object( + api_manila, 'share_group_update', + mock.Mock(side_effect=type('CustomExc', (Exception, ), {}))) + self.mock_object( + api_manila, 'share_group_get', mock.Mock(return_value=self.sg)) + form_data = { + 'method': 'UpdateShareGroupForm', + 'name': self.sg.name, + 'description': self.sg.description, + } + url = reverse('horizon:project:share_groups:update', args=[self.sg.id]) + + res = self.client.post(url, form_data) + + self.assertStatusCode(res, 302) + self.assertRedirectsNoFollow(res, INDEX_URL) + api_manila.share_group_update.assert_called_once_with( + mock.ANY, self.sg.id, form_data['name'], form_data['description']) + api_manila.share_group_get.assert_called_once_with( + mock.ANY, self.sg.id) + self.assertMessageCount(error=1) + self.assertMessageCount(success=0) + + def test_share_group_delete_post(self): + sg = test_data.share_group_dhss_true + data = {'action': 'share_groups__delete__%s' % sg.id} + self.mock_object(api_manila, "share_group_delete") + self.mock_object( + api_manila, "share_group_list", mock.Mock(return_value=[sg])) + + res = self.client.post(INDEX_URL, data) + + self.assertStatusCode(res, 302) + self.assertMessageCount(success=1) + api_manila.share_group_delete.assert_called_once_with(mock.ANY, sg.id) + api_manila.share_group_list.assert_called_once_with( + mock.ANY, detailed=True) + self.assertRedirectsNoFollow(res, INDEX_URL) + + def test_share_group_create_get(self): + url = reverse('horizon:project:share_groups:create') + self.mock_object( + api_manila, 'share_group_type_list', + mock.Mock(return_value=[ + test_data.share_group_type, + test_data.share_group_type_private, + test_data.share_group_type_dhss_true, + ])) + self.mock_object( + api_manila, 'share_group_snapshot_list', + mock.Mock(return_value=[ + test_data.share_group_snapshot, + test_data.share_group_snapshot_nameless, + ])) + self.mock_object( + api_manila, 'share_network_list', + mock.Mock(return_value=[ + test_data.inactive_share_network, + test_data.active_share_network, + ])) + self.mock_object( + api_manila, 'share_type_list', + mock.Mock(return_value=[ + test_data.share_type, + test_data.share_type_private, + test_data.share_type_dhss_true, + ])) + self.mock_object( + api_manila, 'availability_zone_list', + mock.Mock(return_value=[])) + + res = self.client.get(url) + + self.assertTemplateUsed(res, 'project/share_groups/create.html') + self.assertStatusCode(res, 200) + self.assertNoMessages() + self.assertMessageCount(error=0) + api_manila.share_group_type_list.assert_called_once_with(mock.ANY) + api_manila.share_group_snapshot_list.assert_called_once_with(mock.ANY) + api_manila.share_network_list.assert_called_once_with(mock.ANY) + api_manila.share_type_list.assert_called_once_with(mock.ANY) + api_manila.availability_zone_list.assert_called_once_with(mock.ANY) + + def test_share_group_create_post(self): + url = reverse('horizon:project:share_groups:create') + form_data = { + 'method': 'CreateShareGroupForm', + 'name': 'fake_sg_name', + 'description': 'fake SG description', + 'sgt': test_data.share_group_type.id, + 'share-type-choices-%s' % test_data.share_group_type.id: ( + test_data.share_group_type.share_types[0]), + 'availability_zone': '', + 'share_network': test_data.inactive_share_network.id, + 'snapshot': '', + } + self.mock_object( + api_manila, "share_group_create", mock.Mock(return_value=self.sg)) + self.mock_object( + api_manila, 'share_group_type_list', + mock.Mock(return_value=[ + test_data.share_group_type, + test_data.share_group_type_private, + test_data.share_group_type_dhss_true, + ])) + self.mock_object( + api_manila, 'share_group_snapshot_list', + mock.Mock(return_value=[ + test_data.share_group_snapshot, + test_data.share_group_snapshot_nameless, + ])) + self.mock_object( + api_manila, 'share_network_list', + mock.Mock(return_value=[ + test_data.inactive_share_network, + test_data.active_share_network, + ])) + self.mock_object( + api_manila, 'share_type_list', + mock.Mock(return_value=[ + test_data.share_type, + test_data.share_type_private, + test_data.share_type_dhss_true, + ])) + self.mock_object( + api_manila, 'availability_zone_list', + mock.Mock(return_value=[])) + + res = self.client.post(url, form_data) + + self.assertTemplateNotUsed(res, 'project/share_groups/create.html') + self.assertStatusCode(res, 302) + self.assertRedirectsNoFollow(res, INDEX_URL) + api_manila.share_group_type_list.assert_called_once_with(mock.ANY) + api_manila.share_group_snapshot_list.assert_called_once_with(mock.ANY) + api_manila.share_network_list.assert_called_once_with(mock.ANY) + api_manila.share_type_list.assert_called_once_with(mock.ANY) + api_manila.availability_zone_list.assert_called_once_with(mock.ANY) + api_manila.share_group_create.assert_called_once_with( + mock.ANY, + name=form_data['name'], + description=form_data['description'], + share_group_type=test_data.share_group_type.id, + share_types=[test_data.share_type.id], + share_network=form_data['share_network'], + source_share_group_snapshot=None, + availability_zone=form_data['availability_zone']) + + def test_share_group_create_from_snapshot_post(self): + url = reverse('horizon:project:share_groups:create') + form_data = { + 'method': 'CreateShareGroupForm', + 'name': 'fake_sg_name', + 'description': 'fake SG description', + 'sgt': '', + 'snapshot': test_data.share_group_snapshot.id, + } + self.mock_object( + api_manila, "share_group_snapshot_get", + mock.Mock(return_value=test_data.share_group_snapshot)) + self.mock_object( + api_manila, "share_group_get", mock.Mock(return_value=self.sg_nl)) + self.mock_object( + api_manila, "share_group_create", mock.Mock(return_value=self.sg)) + self.mock_object( + api_manila, 'share_group_type_list', + mock.Mock(return_value=[ + test_data.share_group_type, + test_data.share_group_type_private, + test_data.share_group_type_dhss_true, + ])) + self.mock_object( + api_manila, 'share_group_snapshot_list', + mock.Mock(return_value=[ + test_data.share_group_snapshot, + test_data.share_group_snapshot_nameless, + ])) + self.mock_object( + api_manila, 'share_network_list', + mock.Mock(return_value=[ + test_data.inactive_share_network, + test_data.active_share_network, + ])) + self.mock_object( + api_manila, 'share_type_list', + mock.Mock(return_value=[ + test_data.share_type, + test_data.share_type_private, + test_data.share_type_dhss_true, + ])) + self.mock_object( + api_manila, 'availability_zone_list', + mock.Mock(return_value=[])) + + res = self.client.post(url, form_data) + + self.assertTemplateNotUsed(res, 'project/share_groups/create.html') + self.assertStatusCode(res, 302) + self.assertRedirectsNoFollow(res, INDEX_URL) + api_manila.share_group_snapshot_list.assert_not_called() + api_manila.share_group_type_list.assert_not_called() + api_manila.share_network_list.assert_not_called() + api_manila.share_type_list.assert_not_called() + api_manila.availability_zone_list.assert_not_called() + api_manila.share_group_snapshot_get.assert_called_once_with( + mock.ANY, test_data.share_group_snapshot.id) + api_manila.share_group_create.assert_called_once_with( + mock.ANY, + name=form_data['name'], + description=form_data['description'], + share_group_type=test_data.share_group_type.id, + share_types=None, + share_network=None, + source_share_group_snapshot=test_data.share_group_snapshot.id, + availability_zone=None) diff --git a/manila_ui/tests/dashboards/project/shares/tests.py b/manila_ui/tests/dashboards/project/shares/tests.py index 99bf3648..4ba728c5 100644 --- a/manila_ui/tests/dashboards/project/shares/tests.py +++ b/manila_ui/tests/dashboards/project/shares/tests.py @@ -45,6 +45,9 @@ class ShareViewTests(test.APITestCase): self.fake_share_type.get_keys = mock.Mock( return_value={'driver_handles_share_servers': 'True'}) self.share = test_data.share + self.mock_object( + api_manila, "share_group_list", + mock.Mock(return_value=[])) self.mock_object( api_manila, "share_get", mock.Mock(return_value=self.share)) self.mock_object( @@ -114,12 +117,14 @@ class ShareViewTests(test.APITestCase): api_manila, "share_type_list", mock.Mock(return_value=[self.fake_share_type, ])) - self.client.post(url, formData) + res = self.client.post(url, formData) + self.assertRedirectsNoFollow(res, INDEX_URL) api_manila.share_create.assert_called_once_with( mock.ANY, size=formData['size'], name=formData['name'], description=formData['description'], proto=formData['share_proto'], - snapshot_id=None, is_public=False, share_network=share_net.id, + snapshot_id=None, is_public=False, + share_group_id=None, share_network=share_net.id, metadata={}, share_type=formData['share_type'], availability_zone=formData['availability_zone']) api_manila.share_snapshot_list.assert_called_once_with(mock.ANY) @@ -173,7 +178,7 @@ class ShareViewTests(test.APITestCase): mock.ANY, size=formData['size'], name=formData['name'], description=formData['description'], proto=formData['share_proto'], snapshot_id=snapshot.id, is_public=False, - share_network=share_net.id, metadata={}, + share_group_id=None, share_network=share_net.id, metadata={}, share_type=formData['share_type'], availability_zone=formData['availability_zone']) self.assertRedirectsNoFollow(res, INDEX_URL) @@ -185,6 +190,8 @@ class ShareViewTests(test.APITestCase): self.mock_object( api_manila, "share_network_list", mock.Mock(return_value=[])) self.mock_object(api_manila, "share_delete") + self.mock_object( + api_manila, "share_get", mock.Mock(return_value=self.share)) self.mock_object( api_manila, "share_list", mock.Mock(return_value=[self.share])) @@ -194,8 +201,9 @@ class ShareViewTests(test.APITestCase): api_manila.share_snapshot_list.assert_called_once_with( mock.ANY, detailed=True) api_manila.share_list.assert_called_with(mock.ANY) + api_manila.share_get.assert_called_with(mock.ANY, self.share.id) api_manila.share_delete.assert_called_with( - mock.ANY, self.share.id) + mock.ANY, self.share.id, share_group_id=self.share.share_group_id) self.assertRedirectsNoFollow(res, INDEX_URL) def test_detail_view(self): @@ -669,6 +677,7 @@ class ShareViewTests(test.APITestCase): metadata=utils.parse_str_meta(data['metadata'])[0], name=data['name'], proto=data['share_proto'], + share_group_id=None, share_network=test_data.active_share_network.id, share_type=data['share_type'], size=data['size'], diff --git a/manila_ui/tests/dashboards/project/test_data.py b/manila_ui/tests/dashboards/project/test_data.py index 1476545a..50a103b0 100644 --- a/manila_ui/tests/dashboards/project/test_data.py +++ b/manila_ui/tests/dashboards/project/test_data.py @@ -32,6 +32,9 @@ except ImportError: from manilaclient.v1 import shares from manilaclient.v2 import share_export_locations +from manilaclient.v2 import share_group_snapshots +from manilaclient.v2 import share_group_types +from manilaclient.v2 import share_groups from manilaclient.v2 import share_instances from manilaclient.v2 import share_replicas from manilaclient.v2 import share_servers @@ -59,6 +62,7 @@ share = shares.Share( 'share_network_id': '7f3d1c33-8d00-4511-29df-a2def31f3b5d', 'availability_zone': 'Test AZ', 'replication_type': 'readable', + 'share_group_id': 'fake_share_group_id', 'mount_snapshot_support': False}) nameless_share = shares.Share( @@ -332,13 +336,136 @@ share_server_errored = share_servers.ShareServer( share_type = share_types.ShareType( share_types.ShareTypeManager(FakeAPIClient), {'id': 'share-type-id1', - 'name': 'test-share-type', + 'name': 'test-share-type1', + 'share_type_access:is_public': True, 'extra_specs': { 'snapshot_support': True, 'driver_handles_share_servers': False} } ) +share_type_private = share_types.ShareType( + share_types.ShareTypeManager(FakeAPIClient), + {'id': 'share-type-id2', + 'name': 'test-share-type2', + 'share_type_access:is_public': False, + 'extra_specs': {'driver_handles_share_servers': False}} +) + +share_type_dhss_true = share_types.ShareType( + share_types.ShareTypeManager(FakeAPIClient), + {'id': 'share-type-id3', + 'name': 'test-share-type3', + 'share_type_access:is_public': True, + 'extra_specs': {'driver_handles_share_servers': True}} +) + +share_group_type = share_group_types.ShareGroupType( + share_group_types.ShareGroupTypeManager(FakeAPIClient), + {'id': 'fake_share_group_type_id1', + 'name': 'fake_share_group_type_name', + 'share_types': [share_type.id], + 'group_specs': {'k1': 'v1', 'k2': 'v2'}, + 'is_public': True} +) + +share_group_type_private = share_group_types.ShareGroupType( + share_group_types.ShareGroupTypeManager(FakeAPIClient), + {'id': 'fake_private_share_group_type_id2', + 'name': 'fake_private_share_group_type_name', + 'share_types': [share_type.id, share_type_private.id], + 'group_specs': {'k1': 'v1', 'k2': 'v2'}, + 'is_public': False} +) + +share_group_type_dhss_true = share_group_types.ShareGroupType( + share_group_types.ShareGroupTypeManager(FakeAPIClient), + {'id': 'fake_share_group_type_id3', + 'name': 'fake_share_group_type_name', + 'share_types': [share_type_dhss_true.id], + 'group_specs': {'k3': 'v3', 'k4': 'v4'}, + 'is_public': True} +) + +share_group = share_groups.ShareGroup( + share_groups.ShareGroupManager(FakeAPIClient), + {'id': 'fake_share_group_id', + 'name': 'fake_share_group_name', + 'description': 'fake sg description', + 'status': 'available', + 'share_types': [share_type.id], + 'share_group_type_id': share_group_type.id, + 'source_share_group_snapshot_id': None, + 'share_network_id': None, + 'share_server_id': None, + 'availability_zone': None, + 'host': 'fake_host_987654321', + 'consistent_snapshot_support': None, + 'created_at': '2017-05-31T13:36:15.000000', + 'project_id': 'fake_project_id_987654321'} +) + +share_group_nameless = share_groups.ShareGroup( + share_groups.ShareGroupManager(FakeAPIClient), + {'id': 'fake_nameless_share_group_id', + 'name': None, + 'status': 'available', + 'share_types': [share_type.id], + 'share_group_type_id': share_group_type.id, + 'source_share_group_snapshot_id': None, + 'share_network_id': None, + 'share_server_id': None, + 'availability_zone': None, + 'host': 'fake_host_987654321', + 'consistent_snapshot_support': None, + 'created_at': '2017-05-31T13:36:15.000000', + 'project_id': 'fake_project_id_987654321'} +) + +share_group_dhss_true = share_groups.ShareGroup( + share_groups.ShareGroupManager(FakeAPIClient), + {'id': 'fake_dhss_true_share_group_id', + 'name': 'fake_dhss_true_share_group_name', + 'status': 'available', + 'share_types': [share_type_dhss_true.id], + 'share_group_type_id': share_group_type_dhss_true.id, + 'source_share_group_snapshot_id': None, + 'share_network_id': 'fake_share_network_id', + 'share_server_id': 'fake_share_server_id', + 'availability_zone': None, + 'host': 'fake_host_987654321', + 'consistent_snapshot_support': None, + 'created_at': '2017-05-31T23:59:59.000000', + 'project_id': 'fake_project_id_987654321'} +) + +share_group_snapshot = share_group_snapshots.ShareGroupSnapshot( + share_group_snapshots.ShareGroupSnapshotManager(FakeAPIClient), + {'id': 'fake_share_group_snapshot_id_1', + 'name': 'fake_share_group_snapshot_name', + 'status': 'available', + 'share_group_id': share_group.id, + 'description': 'fake sgs description', + 'created_at': '2017-06-01T13:13:13.000000', + 'project_id': 'fake_project_id_987654321', + 'members': [ + {'share_id': 'fake_share_id_1', 'id': 'fake_ssi_id_1', 'size': 1}, + {'share_id': 'fake_share_id_2', 'id': 'fake_ssi_id_2', 'size': 2}, + ]} +) + +share_group_snapshot_nameless = share_group_snapshots.ShareGroupSnapshot( + share_group_snapshots.ShareGroupSnapshotManager(FakeAPIClient), + {'id': 'fake_share_group_snapshot_id_2_nameless', + 'name': None, + 'status': 'available', + 'share_group_id': share_group_nameless.id, + 'description': 'fake nameless sgs description', + 'created_at': '2017-06-02T14:14:14.000000', + 'project_id': 'fake_project_id_987654321', + 'members': []} +) + # Quota Sets quota_data = dict(shares='1', share_snapshots='1',