Commit 2a3a6018 by Szeberényi Imre

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

parents 6a7bb6c1 6f4187b1
...@@ -255,6 +255,44 @@ class GroupCreateForm(NoFormTagMixin, forms.ModelForm): ...@@ -255,6 +255,44 @@ class GroupCreateForm(NoFormTagMixin, forms.ModelForm):
fields = ('name',) 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): class GroupProfileUpdateForm(NoFormTagMixin, forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
...@@ -935,7 +973,9 @@ class VmImportDiskForm(OperationForm): ...@@ -935,7 +973,9 @@ class VmImportDiskForm(OperationForm):
self.user = kwargs.pop('user') self.user = kwargs.pop('user')
super(VmImportDiskForm, self).__init__(*args, **kwargs) 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] disk_filenames = [os.path.basename(item) for item in disk_paths]
self.choices = zip(disk_paths, disk_filenames) self.choices = zip(disk_paths, disk_filenames)
......
...@@ -17,14 +17,15 @@ ...@@ -17,14 +17,15 @@
from __future__ import absolute_import from __future__ import absolute_import
from datetime import timedelta import json
from itertools import chain
from hashlib import md5 from hashlib import md5
from logging import getLogger from logging import getLogger
from datetime import timedelta
from django.conf import settings 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.contrib.auth.signals import user_logged_in
from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db.models import ( from django.db.models import (
Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField, Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField,
...@@ -36,20 +37,16 @@ from django.utils import timezone ...@@ -36,20 +37,16 @@ from django.utils import timezone
from django.utils.html import escape from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django_sshkey.models import UserKey from django_sshkey.models import UserKey
from django.core.exceptions import ObjectDoesNotExist from itertools import chain
from sizefield.models import FileSizeField
from jsonfield import JSONField 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 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 acl.models import AclBase
from common.models import HumanReadableObject, create_readable, Encoder from common.models import HumanReadableObject, create_readable, Encoder
from vm.models.instance import ACCESS_METHODS from vm.models.instance import ACCESS_METHODS
from .store_api import Store, NoStoreException, NotOkException from .store_api import Store, NoStoreException, NotOkException
from .validators import connect_command_template_validator from .validators import connect_command_template_validator
...@@ -323,6 +320,62 @@ class GroupProfile(AclBase): ...@@ -323,6 +320,62 @@ class GroupProfile(AclBase):
return reverse('dashboard.views.group-detail', return reverse('dashboard.views.group-detail',
kwargs={'pk': self.group.pk}) 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): def get_or_create_profile(self):
obj, created = GroupProfile.objects.get_or_create(group_id=self.pk) obj, created = GroupProfile.objects.get_or_create(group_id=self.pk)
......
...@@ -29,7 +29,7 @@ $(function () { ...@@ -29,7 +29,7 @@ $(function () {
return false; 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({ $.ajax({
type: 'GET', type: 'GET',
url: $(this).prop('href'), url: $(this).prop('href'),
......
...@@ -806,6 +806,10 @@ textarea[name="new_members"] { ...@@ -806,6 +806,10 @@ textarea[name="new_members"] {
margin-top: -6px; margin-top: -6px;
} }
.group-remove-all-btn {
margin-right: 5px;
}
.store-action-button { .store-action-button {
margin-left: 5px; margin-left: 5px;
} }
......
...@@ -47,7 +47,7 @@ class NoStoreException(StoreApiException): ...@@ -47,7 +47,7 @@ class NoStoreException(StoreApiException):
class Store(object): class Store(object):
def __init__(self, user, default_timeout=0.5): def __init__(self, user, default_timeout=5):
self.store_url = settings.STORE_URL self.store_url = settings.STORE_URL
if not self.store_url: if not self.store_url:
raise NoStoreException raise NoStoreException
...@@ -110,14 +110,16 @@ class Store(object): ...@@ -110,14 +110,16 @@ class Store(object):
else: else:
return result return result
def get_disk_images(self, path='/'): def get_files_with_exts(self, exts, path='/'):
images = [] """
Get list of files from store with the given file extensions.
"""
matching_files = []
file_list = self.list(path, process=False) file_list = self.list(path, process=False)
export_formats = [item[0] for item in Disk.EXPORT_FORMATS]
for item in file_list: for item in file_list:
if os.path.splitext(item['NAME'])[1].strip('.') in export_formats: if os.path.splitext(item['NAME'])[1].strip('.') in exts:
images.append(os.path.join(path, item['NAME'])) matching_files.append(os.path.join(path, item['NAME']))
return images return matching_files
def request_download(self, path): def request_download(self, path):
r = self._request_cmd("DOWNLOAD", PATH=path, timeout=10) r = self._request_cmd("DOWNLOAD", PATH=path, timeout=10)
...@@ -127,6 +129,21 @@ class Store(object): ...@@ -127,6 +129,21 @@ class Store(object):
r = self._request_cmd("UPLOAD", PATH=path) r = self._request_cmd("UPLOAD", PATH=path)
return r.json()['LINK'] 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): def remove(self, path):
self._request_cmd("REMOVE", PATH=path) self._request_cmd("REMOVE", PATH=path)
......
...@@ -12,6 +12,9 @@ ...@@ -12,6 +12,9 @@
<a title="{% trans "Rename" %}" class="btn btn-default btn-xs group-details-rename-button"> <a title="{% trans "Rename" %}" class="btn btn-default btn-xs group-details-rename-button">
<i class="fa fa-pencil"></i> <i class="fa fa-pencil"></i>
</a> </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 %}"> <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> <i class="fa fa-trash-o"></i>
</a> </a>
...@@ -82,6 +85,9 @@ ...@@ -82,6 +85,9 @@
{% trans "Create user" %} {% trans "Create user" %}
</a> </a>
{% endif %} {% 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> </h3>
<form action="" method="post">{% csrf_token %} <form action="" method="post">{% csrf_token %}
<table class="table table-striped table-with-form-fields table-bordered" id="group-detail-user-table"> <table class="table table-striped table-with-form-fields table-bordered" id="group-detail-user-table">
......
{% 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>
...@@ -40,7 +40,8 @@ ...@@ -40,7 +40,8 @@
{% trans "list" %} {% trans "list" %}
{% endif %} {% endif %}
</a> </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> </div>
</div> </div>
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import from __future__ import absolute_import
from django.conf.urls import url from django.conf.urls import url
from vm.models import Instance from vm.models import Instance
...@@ -32,6 +33,7 @@ from .views import ( ...@@ -32,6 +33,7 @@ from .views import (
DiskRemoveView, get_disk_download_status, DiskRemoveView, get_disk_download_status,
GroupRemoveUserView, GroupRemoveUserView,
GroupRemoveFutureUserView, GroupRemoveFutureUserView,
GroupRemoveAllUsersView,
GroupCreate, GroupProfileUpdate, GroupCreate, GroupProfileUpdate,
TemplateChoose, TemplateChoose,
UserCreationView, UserCreationView,
...@@ -56,11 +58,10 @@ from .views import ( ...@@ -56,11 +58,10 @@ from .views import (
MessageList, MessageDetail, MessageCreate, MessageDelete, MessageList, MessageDetail, MessageCreate, MessageDelete,
EnableTwoFactorView, DisableTwoFactorView, EnableTwoFactorView, DisableTwoFactorView,
AclUserGroupAutocomplete, AclUserAutocomplete, AclUserGroupAutocomplete, AclUserAutocomplete,
RescheduleView, RescheduleView, GroupImportView, GroupExportView
) )
from .views.vm import vm_ops, vm_mass_ops
from .views.node import node_ops from .views.node import node_ops
from .views.vm import vm_ops, vm_mass_ops
urlpatterns = [ urlpatterns = [
url(r'^$', IndexView.as_view(), name="dashboard.index"), url(r'^$', IndexView.as_view(), name="dashboard.index"),
...@@ -192,8 +193,16 @@ urlpatterns = [ ...@@ -192,8 +193,16 @@ urlpatterns = [
url(r'^group/(?P<group_pk>\d+)/remove/futureuser/(?P<member_org_id>.+)/$', url(r'^group/(?P<group_pk>\d+)/remove/futureuser/(?P<member_org_id>.+)/$',
GroupRemoveFutureUserView.as_view(), GroupRemoveFutureUserView.as_view(),
name="dashboard.views.remove-future-user"), 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(), url(r'^group/create/$', GroupCreate.as_view(),
name='dashboard.views.group-create'), 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/$', url(r'^group/(?P<group_pk>\d+)/permissions/$',
GroupPermissionsView.as_view(), GroupPermissionsView.as_view(),
name="dashboard.views.group-permissions"), name="dashboard.views.group-permissions"),
......
...@@ -18,31 +18,33 @@ from __future__ import unicode_literals, absolute_import ...@@ -18,31 +18,33 @@ from __future__ import unicode_literals, absolute_import
import json import json
import logging import logging
from itertools import chain
import requests
from braces.views import SuperuserRequiredMixin, LoginRequiredMixin
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.contrib.messages.views import SuccessMessageMixin 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.core.urlresolvers import reverse, reverse_lazy
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.generic import UpdateView, TemplateView from django.views.generic import UpdateView, TemplateView
from django.views.generic.detail import SingleObjectMixin
from braces.views import SuperuserRequiredMixin, LoginRequiredMixin
from django_tables2 import SingleTableView 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 ( from ..forms import (
AddGroupMemberForm, AclUserOrGroupAddForm, GroupPermissionForm, AddGroupMemberForm, AclUserOrGroupAddForm, GroupPermissionForm,
GroupCreateForm, GroupProfileUpdateForm, GroupCreateForm, GroupImportForm, GroupProfileUpdateForm, GroupExportForm,
) )
from ..models import FutureMember, GroupProfile from ..models import FutureMember, GroupProfile
from vm.models import Instance, InstanceTemplate from ..store_api import Store, NoStoreException
from ..tables import GroupListTable from ..tables import GroupListTable
from .util import (CheckedDetailView, AclUpdateView, search_user,
saml_available, DeleteViewBase)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -285,6 +287,33 @@ class GroupRemoveFutureUserView(GroupRemoveUserView): ...@@ -285,6 +287,33 @@ class GroupRemoveFutureUserView(GroupRemoveUserView):
group=self.get_object()).delete() 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): class GroupDelete(DeleteViewBase):
model = Group model = Group
success_message = _("Group successfully deleted.") success_message = _("Group successfully deleted.")
...@@ -298,7 +327,6 @@ class GroupDelete(DeleteViewBase): ...@@ -298,7 +327,6 @@ class GroupDelete(DeleteViewBase):
class GroupCreate(GroupCodeMixin, LoginRequiredMixin, TemplateView): class GroupCreate(GroupCodeMixin, LoginRequiredMixin, TemplateView):
form_class = GroupCreateForm form_class = GroupCreateForm
def get_template_names(self): def get_template_names(self):
...@@ -334,16 +362,153 @@ class GroupCreate(GroupCodeMixin, LoginRequiredMixin, TemplateView): ...@@ -334,16 +362,153 @@ class GroupCreate(GroupCodeMixin, LoginRequiredMixin, TemplateView):
savedform.profile.set_level(request.user, 'owner') savedform.profile.set_level(request.user, 'owner')
messages.success(request, _('Group successfully created.')) messages.success(request, _('Group successfully created.'))
if request.is_ajax(): if request.is_ajax():
return HttpResponse(json.dumps({'redirect': return HttpResponse(
savedform.profile.get_absolute_url()}), json.dumps(
content_type="application/json") {'redirect': savedform.profile.get_absolute_url()}
),
content_type="application/json"
)
else: else:
return redirect(savedform.profile.get_absolute_url()) 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, class GroupProfileUpdate(SuccessMessageMixin, GroupCodeMixin,
LoginRequiredMixin, UpdateView): LoginRequiredMixin, UpdateView):
form_class = GroupProfileUpdateForm form_class = GroupProfileUpdateForm
model = Group model = Group
success_message = _('Group is successfully updated.') success_message = _('Group is successfully updated.')
......
...@@ -477,7 +477,7 @@ class Disk(TimeStampedModel): ...@@ -477,7 +477,7 @@ class Disk(TimeStampedModel):
return disk return disk
@classmethod @classmethod
def import_disk(cls, user, name, download_link, task): def import_disk(cls, user, name, download_link, port, task):
params = {'name': name, params = {'name': name,
'type': 'qcow2-norm'} 'type': 'qcow2-norm'}
disk = cls.__create(user=user, params=params) disk = cls.__create(user=user, params=params)
...@@ -486,7 +486,7 @@ class Disk(TimeStampedModel): ...@@ -486,7 +486,7 @@ class Disk(TimeStampedModel):
kwargs={ kwargs={
"disk_desc": disk.get_disk_desc(), "disk_desc": disk.get_disk_desc(),
"url": download_link, "url": download_link,
"task": task.request.id "port": port
}, },
queue=queue_name queue=queue_name
) )
...@@ -497,18 +497,17 @@ class Disk(TimeStampedModel): ...@@ -497,18 +497,17 @@ class Disk(TimeStampedModel):
disk.save() disk.save()
return disk 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') queue_name = self.get_remote_queue_name('storage', priority='slow')
remote = storage_tasks.export_disk.apply_async( remote = storage_tasks.export_disk.apply_async(
kwargs={ kwargs={
"disk_desc": self.get_disk_desc(), "disk_desc": self.get_disk_desc(),
"disk_format": disk_format, "disk_format": disk_format,
"exported_name": exported_name,
"upload_link": upload_link, "upload_link": upload_link,
"task": task.request.id "port": port
}, },
queue=queue_name) queue=queue_name)
self._run_abortable_task(remote, task) return self._run_abortable_task(remote, task)
def destroy(self, user=None, task_uuid=None): def destroy(self, user=None, task_uuid=None):
if self.destroyed: if self.destroyed:
......
...@@ -39,12 +39,12 @@ def download(disk_desc, url): ...@@ -39,12 +39,12 @@ def download(disk_desc, url):
@celery.task(name='storagedriver.import_disk') @celery.task(name='storagedriver.import_disk')
def import_disk(disk_desc, url): def import_disk(disk_desc, url, port):
pass pass
@celery.task(name='storagedriver.export_disk') @celery.task(name='storagedriver.export_disk')
def export_disk(disk_desc, format): def export_disk(disk_desc, disk_format, url, port):
pass pass
......
...@@ -253,9 +253,10 @@ ...@@ -253,9 +253,10 @@
<p> <p>
{% blocktrans %} {% blocktrans %}
Press <strong>n</strong> to create new partition. Press <strong>n</strong> to create new partition.
Type <strong>l</strong> to choose logical type. 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>. set partition number - the same as the Linux LVM (vda5) has above: <strong>5</strong>.
You can use the default starting and ending sector. You can use the default starting and ending sector.
Do <strong>NOT</strong> remove the LVM2_member signature.
{% endblocktrans %} {% endblocktrans %}
</p> </p>
</li> </li>
......
...@@ -352,7 +352,6 @@ class ImportDiskOperation(InstanceOperation): ...@@ -352,7 +352,6 @@ class ImportDiskOperation(InstanceOperation):
'from the user store. The disk image has to be in the ' 'from the user store. The disk image has to be in the '
'root directory of the store.') 'root directory of the store.')
abortable = True abortable = True
has_percentage = True
required_perms = ('storage.import_disk',) required_perms = ('storage.import_disk',)
accept_states = ('STOPPED', 'PENDING', 'RUNNING') accept_states = ('STOPPED', 'PENDING', 'RUNNING')
async_queue = 'localhost.man.slow' async_queue = 'localhost.man.slow'
...@@ -366,8 +365,8 @@ class ImportDiskOperation(InstanceOperation): ...@@ -366,8 +365,8 @@ class ImportDiskOperation(InstanceOperation):
def _operation(self, user, name, disk_path, task): def _operation(self, user, name, disk_path, task):
store = Store(user) store = Store(user)
download_link = store.request_download(disk_path) download_link, port = store.request_ssh_download(disk_path)
disk = Disk.import_disk(user, name, download_link, task) disk = Disk.import_disk(user, name, download_link, port, task)
self.instance.disks.add(disk) self.instance.disks.add(disk)
...@@ -377,7 +376,6 @@ class ExportDiskOperation(InstanceOperation): ...@@ -377,7 +376,6 @@ class ExportDiskOperation(InstanceOperation):
name = _('export disk') name = _('export disk')
description = _('Export disk to the selected format.') description = _('Export disk to the selected format.')
abortable = True abortable = True
has_percentage = True
required_perms = ('storage.export_disk',) required_perms = ('storage.export_disk',)
accept_states = ('STOPPED',) accept_states = ('STOPPED',)
async_queue = 'localhost.man.slow' async_queue = 'localhost.man.slow'
...@@ -391,8 +389,9 @@ class ExportDiskOperation(InstanceOperation): ...@@ -391,8 +389,9 @@ class ExportDiskOperation(InstanceOperation):
def _operation(self, user, disk, exported_name, disk_format, task): def _operation(self, user, disk, exported_name, disk_format, task):
store = Store(user) store = Store(user)
upload_link = store.request_upload('/') upload_link, port = store.request_ssh_upload()
disk.export(exported_name, disk_format, upload_link, task) file_name = disk.export(disk_format, upload_link, port, task)
store.ssh_upload_finished(file_name, exported_name + '.' + disk_format)
@register_operation @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