Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
CIRCLE
/
cloud
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
94
Merge Requests
10
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
6f4187b1
authored
Apr 13, 2021
by
Szeberényi Imre
Browse files
Options
Browse Files
Download
Plain Diff
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
Changes
11
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
304 additions
and
40 deletions
+304
-40
circle/dashboard/forms.py
+41
-1
circle/dashboard/models.py
+64
-11
circle/dashboard/static/dashboard/dashboard.js
+1
-1
circle/dashboard/store_api.py
+8
-6
circle/dashboard/templates/dashboard/group-detail.html
+4
-1
circle/dashboard/templates/dashboard/group-export.html
+11
-0
circle/dashboard/templates/dashboard/group-import.html
+11
-0
circle/dashboard/templates/dashboard/group-list/column-actions.html
+3
-3
circle/dashboard/templates/dashboard/index-groups.html
+2
-1
circle/dashboard/urls.py
+8
-3
circle/dashboard/views/group.py
+151
-13
No files found.
circle/dashboard/forms.py
View file @
6f4187b1
...
...
@@ -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
)
...
...
circle/dashboard/models.py
View file @
6f4187b1
...
...
@@ -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
)
...
...
circle/dashboard/static/dashboard/dashboard.js
View file @
6f4187b1
...
...
@@ -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'
),
...
...
circle/dashboard/store_api.py
View file @
6f4187b1
...
...
@@ -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
ex
port_forma
ts
:
imag
es
.
append
(
os
.
path
.
join
(
path
,
item
[
'NAME'
]))
return
imag
es
if
os
.
path
.
splitext
(
item
[
'NAME'
])[
1
]
.
strip
(
'.'
)
in
exts
:
matching_fil
es
.
append
(
os
.
path
.
join
(
path
,
item
[
'NAME'
]))
return
matching_fil
es
def
request_download
(
self
,
path
):
r
=
self
.
_request_cmd
(
"DOWNLOAD"
,
PATH
=
path
,
timeout
=
10
)
...
...
circle/dashboard/templates/dashboard/group-detail.html
View file @
6f4187b1
...
...
@@ -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 }}
...
...
circle/dashboard/templates/dashboard/group-export.html
0 → 100644
View file @
6f4187b1
{% 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>
circle/dashboard/templates/dashboard/group-import.html
0 → 100644
View file @
6f4187b1
{% 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>
circle/dashboard/templates/dashboard/group-list/column-actions.html
View file @
6f4187b1
<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>
circle/dashboard/templates/dashboard/index-groups.html
View file @
6f4187b1
...
...
@@ -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>
...
...
circle/dashboard/urls.py
View file @
6f4187b1
...
...
@@ -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"
),
...
...
circle/dashboard/views/group.py
View file @
6f4187b1
...
...
@@ -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
,
Group
ProfileUpdate
Form
,
GroupCreateForm
,
Group
ImportForm
,
GroupProfileUpdateForm
,
GroupExport
Form
,
)
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.'
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment