Commit 5009e403 by Szeberényi Imre

Revert "Revert "Merge branch 'master' of https://git.ik.bme.hu/circle/cloud""

This reverts commit edd5e74d
parent edd5e74d
......@@ -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, .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'),
......
......@@ -806,6 +806,10 @@ textarea[name="new_members"] {
margin-top: -6px;
}
.group-remove-all-btn {
margin-right: 5px;
}
.store-action-button {
margin-left: 5px;
}
......
......@@ -47,7 +47,7 @@ class NoStoreException(StoreApiException):
class Store(object):
def __init__(self, user, default_timeout=0.5):
def __init__(self, user, default_timeout=5):
self.store_url = settings.STORE_URL
if not self.store_url:
raise NoStoreException
......@@ -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)
......@@ -127,6 +129,21 @@ class Store(object):
r = self._request_cmd("UPLOAD", PATH=path)
return r.json()['LINK']
def request_ssh_download(self, path):
r = self._request_cmd("SSH_DOWNLOAD", PATH=path)
return r.json()['LINK'], r.json()['PORT']
def request_ssh_upload(self):
r = self._request_cmd("SSH_UPLOAD")
return r.json()['LINK'], r.json()['PORT']
def ssh_upload_finished(self, uploaded_name, path):
self._request_cmd(
"SSH_UPLOAD_FINISHED",
FILENAME=uploaded_name,
PATH=path,
)
def remove(self, path):
self._request_cmd("REMOVE", PATH=path)
......
......@@ -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>
......@@ -82,6 +85,9 @@
{% trans "Create user" %}
</a>
{% endif %}
<a data-group_pk="{{ group.pk }}" href="{% url "dashboard.views.remove-all-users" group_pk=group.pk %}" class="btn btn-danger group-remove-all-btn pull-right">
{% trans "Remove all users" %}
</a>
</h3>
<form action="" method="post">{% csrf_token %}
<table class="table table-striped table-with-form-fields table-bordered" id="group-detail-user-table">
......@@ -138,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
......@@ -32,6 +33,7 @@ from .views import (
DiskRemoveView, get_disk_download_status,
GroupRemoveUserView,
GroupRemoveFutureUserView,
GroupRemoveAllUsersView,
GroupCreate, GroupProfileUpdate,
TemplateChoose,
UserCreationView,
......@@ -56,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"),
......@@ -192,8 +193,16 @@ urlpatterns = [
url(r'^group/(?P<group_pk>\d+)/remove/futureuser/(?P<member_org_id>.+)/$',
GroupRemoveFutureUserView.as_view(),
name="dashboard.views.remove-future-user"),
url(r'^group/(?P<group_pk>\d+)/remove/user/all/$',
GroupRemoveAllUsersView.as_view(),
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__)
......@@ -285,6 +287,33 @@ class GroupRemoveFutureUserView(GroupRemoveUserView):
group=self.get_object()).delete()
class GroupRemoveAllUsersView(DeleteViewBase):
model = Group
level = 'operator'
slug_field = 'pk'
slug_url_kwarg = 'group_pk'
success_message = _("All users successfully removed from group.")
def check_auth(self):
if not self.get_object().profile.has_level(
self.request.user, self.level):
raise PermissionDenied()
def get_context_data(self, **kwargs):
context = super(GroupRemoveAllUsersView, self).get_context_data(**kwargs)
context['member'] = _("all users")
return context
def get_success_url(self):
return reverse_lazy("dashboard.views.group-detail",
kwargs={'pk': self.get_object().pk})
def delete_obj(self, request, *args, **kwargs):
container = self.get_object()
container.user_set.clear()
FutureMember.objects.filter(group=container).delete()
class GroupDelete(DeleteViewBase):
model = Group
success_message = _("Group successfully deleted.")
......@@ -298,7 +327,6 @@ class GroupDelete(DeleteViewBase):
class GroupCreate(GroupCodeMixin, LoginRequiredMixin, TemplateView):
form_class = GroupCreateForm
def get_template_names(self):
......@@ -334,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.')
......
......@@ -477,7 +477,7 @@ class Disk(TimeStampedModel):
return disk
@classmethod
def import_disk(cls, user, name, download_link, task):
def import_disk(cls, user, name, download_link, port, task):
params = {'name': name,
'type': 'qcow2-norm'}
disk = cls.__create(user=user, params=params)
......@@ -486,7 +486,7 @@ class Disk(TimeStampedModel):
kwargs={
"disk_desc": disk.get_disk_desc(),
"url": download_link,
"task": task.request.id
"port": port
},
queue=queue_name
)
......@@ -497,18 +497,17 @@ class Disk(TimeStampedModel):
disk.save()
return disk
def export(self, exported_name, disk_format, upload_link, task):
def export(self, disk_format, upload_link, port, task):
queue_name = self.get_remote_queue_name('storage', priority='slow')
remote = storage_tasks.export_disk.apply_async(
kwargs={
"disk_desc": self.get_disk_desc(),
"disk_format": disk_format,
"exported_name": exported_name,
"upload_link": upload_link,
"task": task.request.id
"port": port
},
queue=queue_name)
self._run_abortable_task(remote, task)
return self._run_abortable_task(remote, task)
def destroy(self, user=None, task_uuid=None):
if self.destroyed:
......
......@@ -39,12 +39,12 @@ def download(disk_desc, url):
@celery.task(name='storagedriver.import_disk')
def import_disk(disk_desc, url):
def import_disk(disk_desc, url, port):
pass
@celery.task(name='storagedriver.export_disk')
def export_disk(disk_desc, format):
def export_disk(disk_desc, disk_format, url, port):
pass
......
......@@ -253,9 +253,10 @@
<p>
{% blocktrans %}
Press <strong>n</strong> to create new partition.
Type <strong>l</strong> to choose logical type.
Set partition number - the same as the Linux LVM (vda5) has above: <strong>5</strong>.
If not selected automatically, type <strong>l</strong> to choose logical type, and
set partition number - the same as the Linux LVM (vda5) has above: <strong>5</strong>.
You can use the default starting and ending sector.
Do <strong>NOT</strong> remove the LVM2_member signature.
{% endblocktrans %}
</p>
</li>
......
......@@ -352,7 +352,6 @@ class ImportDiskOperation(InstanceOperation):
'from the user store. The disk image has to be in the '
'root directory of the store.')
abortable = True
has_percentage = True
required_perms = ('storage.import_disk',)
accept_states = ('STOPPED', 'PENDING', 'RUNNING')
async_queue = 'localhost.man.slow'
......@@ -366,8 +365,8 @@ class ImportDiskOperation(InstanceOperation):
def _operation(self, user, name, disk_path, task):
store = Store(user)
download_link = store.request_download(disk_path)
disk = Disk.import_disk(user, name, download_link, task)
download_link, port = store.request_ssh_download(disk_path)
disk = Disk.import_disk(user, name, download_link, port, task)
self.instance.disks.add(disk)
......@@ -377,7 +376,6 @@ class ExportDiskOperation(InstanceOperation):
name = _('export disk')
description = _('Export disk to the selected format.')
abortable = True
has_percentage = True
required_perms = ('storage.export_disk',)
accept_states = ('STOPPED',)
async_queue = 'localhost.man.slow'
......@@ -391,8 +389,9 @@ class ExportDiskOperation(InstanceOperation):
def _operation(self, user, disk, exported_name, disk_format, task):
store = Store(user)
upload_link = store.request_upload('/')
disk.export(exported_name, disk_format, upload_link, task)
upload_link, port = store.request_ssh_upload()
file_name = disk.export(disk_format, upload_link, port, task)
store.ssh_upload_finished(file_name, exported_name + '.' + disk_format)
@register_operation
......
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