Commit 6f4187b1 by Szeberényi Imre

Merge branch 'group_export_import' into 'master'

Export and import groups via the user store

See merge request !425
parents 4b62f1c2 2d0244f9
Pipeline #1438 passed with stage
in 0 seconds
......@@ -255,6 +255,44 @@ class GroupCreateForm(NoFormTagMixin, forms.ModelForm):
fields = ('name',)
class GroupImportForm(NoFormTagMixin, forms.Form):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user")
super(GroupImportForm, self).__init__(*args, **kwargs)
exported_group_paths = Store(self.user).get_files_with_exts(["group"])
exported_group_names = [
os.path.basename(item) for item in exported_group_paths
]
self.choices = zip(exported_group_paths, exported_group_names)
self.fields["group_path"] = forms.ChoiceField(
label=_("Group to import"),
choices=self.choices
)
@property
def helper(self):
helper = super(GroupImportForm, self).helper
helper.add_input(Submit("submit", _("Import")))
return helper
class GroupExportForm(NoFormTagMixin, forms.Form):
def __init__(self, *args, **kwargs):
default = kwargs.pop("group_name")
super(GroupExportForm, self).__init__(*args, **kwargs)
self.fields["exported_name"] = forms.CharField(
max_length=100, label=_('Filename'), initial=default
)
@property
def helper(self):
helper = super(GroupExportForm, self).helper
helper.add_input(Submit("submit", _("Export")))
return helper
class GroupProfileUpdateForm(NoFormTagMixin, forms.ModelForm):
def __init__(self, *args, **kwargs):
......@@ -935,7 +973,9 @@ class VmImportDiskForm(OperationForm):
self.user = kwargs.pop('user')
super(VmImportDiskForm, self).__init__(*args, **kwargs)
disk_paths = Store(self.user).get_disk_images()
disk_paths = Store(self.user).get_files_with_exts(
[f[0] for f in Disk.EXPORT_FORMATS]
)
disk_filenames = [os.path.basename(item) for item in disk_paths]
self.choices = zip(disk_paths, disk_filenames)
......
......@@ -17,14 +17,15 @@
from __future__ import absolute_import
from datetime import timedelta
from itertools import chain
import json
from hashlib import md5
from logging import getLogger
from datetime import timedelta
from django.conf import settings
from django.contrib.auth.models import User, Group
from django.contrib.auth.models import User, Group, Permission
from django.contrib.auth.signals import user_logged_in
from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import reverse
from django.db.models import (
Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField,
......@@ -36,20 +37,16 @@ from django.utils import timezone
from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _
from django_sshkey.models import UserKey
from django.core.exceptions import ObjectDoesNotExist
from sizefield.models import FileSizeField
from itertools import chain
from jsonfield import JSONField
from model_utils.models import TimeFramedModel, TimeStampedModel
from model_utils.fields import StatusField
from model_utils import Choices
from model_utils.fields import StatusField
from model_utils.models import TimeFramedModel, TimeStampedModel
from sizefield.models import FileSizeField
from acl.models import AclBase
from common.models import HumanReadableObject, create_readable, Encoder
from vm.models.instance import ACCESS_METHODS
from .store_api import Store, NoStoreException, NotOkException
from .validators import connect_command_template_validator
......@@ -323,6 +320,62 @@ class GroupProfile(AclBase):
return reverse('dashboard.views.group-detail',
kwargs={'pk': self.group.pk})
@classmethod
def create_from_json(cls, owner, json_data):
group = Group()
try:
data = json.loads(json_data)
group.name = data["name"]
group.save()
profile = group.profile
profile.set_user_level(owner, "owner")
profile.description = data["desc"]
profile.org_id = data["org_id"]
profile.instance_limit = int(data["instance_limit"])
profile.template_instance_limit = int(data["template_instance_limit"])
profile.disk_quota = long(data["disk_quota"])
profile.save()
for org_id in data["users"]:
try:
if org_id is not None:
user = Profile.objects.get(org_id=org_id).user
user.groups.add(group)
except ObjectDoesNotExist:
future_member = FutureMember(org_id=org_id, group=group)
future_member.save()
for permission in data["permissions"]:
group.permissions.add(
Permission.objects.get_by_natural_key(*permission)
)
return group.profile
except (KeyError, ValueError, TypeError):
if group.id is not None:
group.delete()
logger.error("Invalid group JSON")
def convert_to_json(self):
json_group = {
"name": self.group.name,
"desc": self.description,
"org_id": self.org_id,
"instance_limit": self.instance_limit,
"template_instance_limit": self.template_instance_limit,
"disk_quota": self.disk_quota,
"users": [
user.profile.org_id for user in self.group.user_set.all()
],
"permissions": [
permission.natural_key()
for permission in self.group.permissions.all()
]
}
return json.dumps(json_group)
def get_or_create_profile(self):
obj, created = GroupProfile.objects.get_or_create(group_id=self.pk)
......
......@@ -29,7 +29,7 @@ $(function () {
return false;
});
$('.group-create, .node-create, .tx-tpl-ownership, .group-delete, .node-delete, .disk-remove, .template-delete, .delete-from-group, .group-remove-all-btn, .lease-delete').click(function(e) {
$('.group-create, .group-import, .group-export, .node-create, .tx-tpl-ownership, .group-delete, .node-delete, .disk-remove, .template-delete, .delete-from-group, .group-remove-all-btn, .lease-delete').click(function(e) {
$.ajax({
type: 'GET',
url: $(this).prop('href'),
......
......@@ -110,14 +110,16 @@ class Store(object):
else:
return result
def get_disk_images(self, path='/'):
images = []
def get_files_with_exts(self, exts, path='/'):
"""
Get list of files from store with the given file extensions.
"""
matching_files = []
file_list = self.list(path, process=False)
export_formats = [item[0] for item in Disk.EXPORT_FORMATS]
for item in file_list:
if os.path.splitext(item['NAME'])[1].strip('.') in export_formats:
images.append(os.path.join(path, item['NAME']))
return images
if os.path.splitext(item['NAME'])[1].strip('.') in exts:
matching_files.append(os.path.join(path, item['NAME']))
return matching_files
def request_download(self, path):
r = self._request_cmd("DOWNLOAD", PATH=path, timeout=10)
......
......@@ -12,6 +12,9 @@
<a title="{% trans "Rename" %}" class="btn btn-default btn-xs group-details-rename-button">
<i class="fa fa-pencil"></i>
</a>
<a title="{% trans "Export" %}" data-group-pk="{{ group.pk }}" class="btn btn-default btn-xs group-export" href="{% url "dashboard.views.group-export" group_pk=group.pk %}">
<i class="fa fa-upload"></i>
</a>
<a title="{% trans "Delete" %}" data-group-pk="{{ group.pk }}" class="btn btn-default btn-xs real-link group-delete" href="{% url "dashboard.views.delete-group" pk=group.pk %}">
<i class="fa fa-trash-o"></i>
</a>
......@@ -141,7 +144,7 @@
<hr />
<script type="text/javascript" src="/static/admin/js/vendor/jquery/jquery.min.js"></script>
<script type="text/javascript" src="/static/admin/js/jquery.init.js"></script>
<script type="text/javascript" src="/static/admin/js/jquery.init.js"></script>
<script type="text/javascript" src="/static/autocomplete_light/jquery.init.js"></script>
<script type="text/javascript" src="/static/autocomplete_light/vendor/select2/dist/js/select2.js"></script>
{{ group_perm_form.media }}
......
{% load crispy_forms_tags %}
{% load i18n %}
<p class="text-muted">
{% trans "Export a group to the user store with the given filename." %}
</p>
<form method="POST" data-group_pk="{{ group.pk }}" action="{% url "dashboard.views.group-export" group_pk=group.pk %}">
{% csrf_token %}
{% crispy form %}
</form>
{% load crispy_forms_tags %}
{% load i18n %}
<p class="text-muted">
{% trans "Import a previously exported group from the user store." %}
</p>
<form method="POST" action="{% url "dashboard.views.group-import" %}">
{% csrf_token %}
{% crispy form %}
</form>
<a data-group-pk="{{ record.pk }}"
class="btn btn-danger btn-xs real-link group-delete"
href="{% url "dashboard.views.delete-group" pk=record.pk %}?next={{ request.path }}">
<i class="fa fa-trash-o"></i>
class="btn btn-danger btn-xs real-link group-delete"
href="{% url "dashboard.views.delete-group" pk=record.pk %}?next={{ request.path }}">
<i class="fa fa-trash-o"></i>
</a>
......@@ -40,7 +40,8 @@
{% trans "list" %}
{% endif %}
</a>
<a class="btn btn-success btn-xs group-create" href="{% url "dashboard.views.group-create" %}"><i class="fa fa-plus-circle"></i> {% trans "new" %} </a>
<a class="btn btn-success btn-xs group-create" href="{% url "dashboard.views.group-create" %}" title="{% trans "new" %}"><i class="fa fa-plus-circle"></i></a>
<a class="btn btn-success btn-xs group-import" href="{% url "dashboard.views.group-import" %}" title="{% trans "import" %}"><i class="fa fa-download"></i></a>
</div>
</div>
</div>
......
......@@ -16,6 +16,7 @@
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
from django.conf.urls import url
from vm.models import Instance
......@@ -57,11 +58,10 @@ from .views import (
MessageList, MessageDetail, MessageCreate, MessageDelete,
EnableTwoFactorView, DisableTwoFactorView,
AclUserGroupAutocomplete, AclUserAutocomplete,
RescheduleView,
RescheduleView, GroupImportView, GroupExportView
)
from .views.vm import vm_ops, vm_mass_ops
from .views.node import node_ops
from .views.vm import vm_ops, vm_mass_ops
urlpatterns = [
url(r'^$', IndexView.as_view(), name="dashboard.index"),
......@@ -198,6 +198,11 @@ urlpatterns = [
name="dashboard.views.remove-all-users"),
url(r'^group/create/$', GroupCreate.as_view(),
name='dashboard.views.group-create'),
url(r'^group/import/$', GroupImportView.as_view(),
name="dashboard.views.group-import"),
url(r'^group/(?P<group_pk>\d+)/export/$',
GroupExportView.as_view(),
name="dashboard.views.group-export"),
url(r'^group/(?P<group_pk>\d+)/permissions/$',
GroupPermissionsView.as_view(),
name="dashboard.views.group-permissions"),
......
......@@ -18,31 +18,33 @@ from __future__ import unicode_literals, absolute_import
import json
import logging
from itertools import chain
import requests
from braces.views import SuperuserRequiredMixin, LoginRequiredMixin
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.models import User, Group
from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import PermissionDenied
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.urlresolvers import reverse, reverse_lazy
from django.http import HttpResponse, Http404
from django.shortcuts import redirect
from django.utils.translation import ugettext as _
from django.views.generic import UpdateView, TemplateView
from braces.views import SuperuserRequiredMixin, LoginRequiredMixin
from django.views.generic.detail import SingleObjectMixin
from django_tables2 import SingleTableView
from itertools import chain
from vm.models import Instance, InstanceTemplate
from .util import (CheckedDetailView, AclUpdateView, search_user,
saml_available, DeleteViewBase)
from ..forms import (
AddGroupMemberForm, AclUserOrGroupAddForm, GroupPermissionForm,
GroupCreateForm, GroupProfileUpdateForm,
GroupCreateForm, GroupImportForm, GroupProfileUpdateForm, GroupExportForm,
)
from ..models import FutureMember, GroupProfile
from vm.models import Instance, InstanceTemplate
from ..store_api import Store, NoStoreException
from ..tables import GroupListTable
from .util import (CheckedDetailView, AclUpdateView, search_user,
saml_available, DeleteViewBase)
logger = logging.getLogger(__name__)
......@@ -325,7 +327,6 @@ class GroupDelete(DeleteViewBase):
class GroupCreate(GroupCodeMixin, LoginRequiredMixin, TemplateView):
form_class = GroupCreateForm
def get_template_names(self):
......@@ -361,16 +362,153 @@ class GroupCreate(GroupCodeMixin, LoginRequiredMixin, TemplateView):
savedform.profile.set_level(request.user, 'owner')
messages.success(request, _('Group successfully created.'))
if request.is_ajax():
return HttpResponse(json.dumps({'redirect':
savedform.profile.get_absolute_url()}),
content_type="application/json")
return HttpResponse(
json.dumps(
{'redirect': savedform.profile.get_absolute_url()}
),
content_type="application/json"
)
else:
return redirect(savedform.profile.get_absolute_url())
class GroupImportView(LoginRequiredMixin, TemplateView):
form_class = GroupImportForm
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/_modal.html']
else:
return ['dashboard/nojs-wrapper.html']
def get(self, request, form=None, *args, **kwargs):
if not request.user.has_module_perms('auth'):
raise PermissionDenied()
try:
Store(request.user)
except NoStoreException:
raise PermissionDenied()
if form is None:
form = self.form_class(user=request.user)
context = self.get_context_data(**kwargs)
context.update({
'template': 'dashboard/group-import.html',
'box_title': _('Import a Group'),
'form': form,
'ajax_title': True,
})
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
if not request.user.has_module_perms('auth'):
raise PermissionDenied()
try:
Store(request.user)
except NoStoreException:
raise PermissionDenied()
form = self.form_class(request.POST, user=request.user)
if form.is_valid():
group_path = form.cleaned_data["group_path"]
url = Store(request.user).request_download(group_path)
json_str = requests.get(url).content
profile = GroupProfile.create_from_json(request.user, json_str)
if profile is None:
raise SuspiciousOperation()
success_message = _("Group successfully imported.")
if request.is_ajax():
response = {
'message': success_message,
'redirect': profile.get_absolute_url()
}
return HttpResponse(
json.dumps(response),
content_type="application/json"
)
else:
messages.success(request, success_message)
return redirect(profile.get_absolute_url())
else:
return self.get(request, form, *args, **kwargs)
class GroupExportView(LoginRequiredMixin, SingleObjectMixin, TemplateView):
form_class = GroupExportForm
model = Group
pk_url_kwarg = "group_pk"
def __init__(self):
super(GroupExportView, self).__init__()
self.object = None
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/_modal.html']
else:
return ['dashboard/nojs-wrapper.html']
def get(self, request, form=None, *args, **kwargs):
self.object = self.get_object()
if not self.object.profile.has_level(request.user, 'operator'):
raise PermissionDenied()
try:
Store(request.user)
except NoStoreException:
raise PermissionDenied()
if form is None:
form = self.form_class(group_name=self.object.name)
context = self.get_context_data(**kwargs)
context.update({
'group': self.object,
'template': 'dashboard/group-export.html',
'box_title': _('Export Group'),
'form': form,
'ajax_title': True,
})
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
group = self.object
if not group.profile.has_level(request.user, 'operator'):
raise PermissionDenied()
try:
Store(request.user)
except NoStoreException:
raise PermissionDenied()
form = self.form_class(request.POST, group_name=self.object.name)
if form.is_valid():
name = form.cleaned_data["exported_name"]
group_json = group.profile.convert_to_json()
store = Store(request.user)
url = store.request_upload("/")
data = {'data': (name + '.group', group_json)}
requests.post(url, files=data)
success_message = _("Group successfully exported.")
if request.is_ajax():
response = {
'message': success_message,
'redirect': group.profile.get_absolute_url()
}
return HttpResponse(
json.dumps(response),
content_type="application/json"
)
else:
messages.success(request, success_message)
return redirect(group.profile.get_absolute_url())
else:
return self.get(request, form, *args, **kwargs)
class GroupProfileUpdate(SuccessMessageMixin, GroupCodeMixin,
LoginRequiredMixin, UpdateView):
form_class = GroupProfileUpdateForm
model = Group
success_message = _('Group is successfully updated.')
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment