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
A prog2-höz tartozó friss repo anyagok itt elérhetőek:
https://git.iit.bme.hu/
Commit
744c1885
authored
Jan 08, 2026
by
Your Name
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
storage + UI fix
parent
9b3516eb
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
181 additions
and
189 deletions
+181
-189
circle/dashboard/forms.py
+58
-8
circle/dashboard/static/dashboard/activity.js
+35
-31
circle/dashboard/static/dashboard/dashboard.js
+33
-33
circle/dashboard/templates/base.html
+0
-95
circle/dashboard/templates/dashboard/base.html
+2
-1
circle/dashboard/templates/dashboard/storage/detail.html
+13
-4
circle/dashboard/views/storage.py
+30
-8
circle/manager/scheduler.py
+3
-3
circle/storage/tasks/periodic_tasks.py
+7
-6
No files found.
circle/dashboard/forms.py
View file @
744c1885
...
...
@@ -24,7 +24,7 @@ import pyotp
from
crispy_forms.bootstrap
import
FormActions
from
crispy_forms.helper
import
FormHelper
from
crispy_forms.layout
import
(
Layout
,
Div
,
BaseInput
,
Field
,
HTML
,
Submit
,
TEMPLATE_PACK
,
Fieldset
Layout
,
Div
,
BaseInput
,
Field
,
HTML
,
Submit
,
TEMPLATE_PACK
,
Fieldset
,
)
from
crispy_forms.utils
import
render_field
from
dal
import
autocomplete
...
...
@@ -1735,24 +1735,74 @@ class DataStoreForm(ModelForm):
fields
=
(
"name"
,
"path"
,
"hostname"
)
#class DiskForm(ModelForm):
# created = forms.DateTimeField()
# modified = forms.DateTimeField()
#
# def __init__(self, *args, **kwargs):
# super(DiskForm, self).__init__(*args, **kwargs)
#
# for k, v in self.fields.iteritems():
# v.widget.attrs['readonly'] = True
# self.fields['created'].initial = self.instance.created
# self.fields['modified'].initial = self.instance.modified
#
# class Meta:
# model = Disk
# fields = ("name", "filename", "datastore", "type", "bus", "size",
# "base", "dev_num", "destroyed", "is_ready",)
#
class
DiskForm
(
ModelForm
):
created
=
forms
.
DateTimeField
()
modified
=
forms
.
DateTimeField
()
created
=
forms
.
DateTimeField
(
required
=
False
)
modified
=
forms
.
DateTimeField
(
required
=
False
)
@property
def
helper
(
self
):
helper
=
FormHelper
()
helper
.
form_method
=
"post"
helper
.
layout
=
Layout
(
Fieldset
(
''
,
'name'
,
'filename'
,
'datastore'
,
'type'
,
'bus'
,
'size'
,
'base'
,
'dev_num'
,
'destroyed'
,
'is_ready'
,
'created'
,
'modified'
,
),
FormActions
(
Submit
(
'submit'
,
'Save'
),
)
)
return
helper
def
__init__
(
self
,
*
args
,
**
kwargs
):
super
(
DiskForm
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
for
k
,
v
in
self
.
fields
.
iteritems
():
v
.
widget
.
attrs
[
'readonly'
]
=
True
self
.
fields
[
'created'
]
.
initial
=
self
.
instance
.
created
self
.
fields
[
'modified'
]
.
initial
=
self
.
instance
.
modified
# Make all fields non-editable except 'datastore', and 'filename'.
for
name
,
field
in
self
.
fields
.
iteritems
():
if
name
==
'datastore'
or
name
==
'filename'
:
continue
# field.widget.attrs['disabled'] = 'disabled'
# Show timestamps (read-only display fields)
self
.
fields
[
'created'
]
.
initial
=
getattr
(
self
.
instance
,
'created'
,
None
)
self
.
fields
[
'modified'
]
.
initial
=
getattr
(
self
.
instance
,
'modified'
,
None
)
# self.fields['created'].widget.attrs['disabled'] = 'disabled'
# self.fields['modified'].widget.attrs['disabled'] = 'disabled'
class
Meta
:
model
=
Disk
fields
=
(
"name"
,
"filename"
,
"datastore"
,
"type"
,
"bus"
,
"size"
,
"base"
,
"dev_num"
,
"destroyed"
,
"is_ready"
,)
class
MessageForm
(
ModelForm
):
class
Meta
:
model
=
Message
...
...
circle/dashboard/static/dashboard/activity.js
View file @
744c1885
...
...
@@ -27,39 +27,43 @@ $(function() {
return
false
;
});
function
initAutocompleteSelect2
(
$root
)
{
$root
.
find
(
'select[data-autocomplete-light-function="select2"]'
).
each
(
function
()
{
var
$el
=
$
(
this
);
// már initelve?
if
(
$el
.
hasClass
(
'select2-hidden-accessible'
))
return
;
var
url
=
$el
.
data
(
'autocomplete-light-url'
);
var
placeholder
=
$el
.
data
(
'placeholder'
)
||
''
;
$el
.
select2
({
dropdownParent
:
$root
,
// bootstrap modal fókusz miatt
width
:
'resolve'
,
placeholder
:
placeholder
,
allowClear
:
true
,
ajax
:
{
url
:
url
,
dataType
:
'json'
,
delay
:
250
,
data
:
function
(
params
)
{
return
{
q
:
params
.
term
,
page
:
params
.
page
||
1
};
},
processResults
:
function
(
data
)
{
// DAL autocomplete view tipikusan már {results: [...], pagination: {more: ...}}
return
data
;
}
},
// data-html="true" miatt: hagyjuk az HTML-t renderelődni
escapeMarkup
:
function
(
m
)
{
return
m
;
}
});
});
function
spinDisks
()
{
$
(
'#disks-spinner'
).
show
().
addClass
(
'fa-spin'
);
}
$
(
function
()
{
$
(
'#storage-link'
).
on
(
'click'
,
function
()
{
$
(
'#storage-link-spinner'
)
.
show
()
.
addClass
(
'fa-spin'
);
});
});
// Datastore autosubmit spinner
$
(
'select[data-autosubmit="1"]'
).
on
(
'change'
,
function
()
{
$
(
'#ds-spinner'
)
.
show
()
.
addClass
(
'fa-spin'
);
});
// Filter clicks
$
(
'.storage-filter'
).
on
(
'click'
,
function
()
{
spinDisks
();
});
// Search submit
$
(
'#network-host-list-form'
).
on
(
'submit'
,
function
()
{
spinDisks
();
});
$
(
function
()
{
$
(
'.nav-spinner'
).
on
(
'click'
,
function
()
{
$
(
this
).
find
(
'.fa-spinner'
)
.
show
()
.
addClass
(
'fa-spin'
);
});
});
function
showConfirmationModal
(
data
)
{
// ha valamiért bent maradt egy régi modal, takarítsuk (örökölt kódnál előfordul)
$
(
'#confirmation-modal'
).
remove
();
...
...
circle/dashboard/static/dashboard/dashboard.js
View file @
744c1885
...
...
@@ -29,39 +29,6 @@ $(function () {
return
false
;
});
function
initAutocompleteSelect2
(
$root
)
{
$root
.
find
(
'select[data-autocomplete-light-function="select2"]'
).
each
(
function
()
{
var
$el
=
$
(
this
);
// már initelve?
if
(
$el
.
hasClass
(
'select2-hidden-accessible'
))
return
;
var
url
=
$el
.
data
(
'autocomplete-light-url'
);
var
placeholder
=
$el
.
data
(
'placeholder'
)
||
''
;
$el
.
select2
({
dropdownParent
:
$root
,
// bootstrap modal fókusz miatt
width
:
'resolve'
,
placeholder
:
placeholder
,
allowClear
:
true
,
ajax
:
{
url
:
url
,
dataType
:
'json'
,
delay
:
250
,
data
:
function
(
params
)
{
return
{
q
:
params
.
term
,
page
:
params
.
page
||
1
};
},
processResults
:
function
(
data
)
{
// DAL autocomplete view tipikusan már {results: [...], pagination: {more: ...}}
return
data
;
}
},
// data-html="true" miatt: hagyjuk az HTML-t renderelődni
escapeMarkup
:
function
(
m
)
{
return
m
;
}
});
});
}
$
(
'.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'
,
...
...
@@ -317,6 +284,39 @@ $(function () {
});
});
function
initAutocompleteSelect2
(
$root
)
{
$root
.
find
(
'select[data-autocomplete-light-function="select2"]'
).
each
(
function
()
{
var
$el
=
$
(
this
);
// már initelve?
if
(
$el
.
hasClass
(
'select2-hidden-accessible'
))
return
;
var
url
=
$el
.
data
(
'autocomplete-light-url'
);
var
placeholder
=
$el
.
data
(
'placeholder'
)
||
''
;
$el
.
select2
({
dropdownParent
:
$root
,
// bootstrap modal fókusz miatt
width
:
'resolve'
,
placeholder
:
placeholder
,
allowClear
:
true
,
ajax
:
{
url
:
url
,
dataType
:
'json'
,
delay
:
250
,
data
:
function
(
params
)
{
return
{
q
:
params
.
term
,
page
:
params
.
page
||
1
};
},
processResults
:
function
(
data
)
{
// DAL autocomplete view tipikusan már {results: [...], pagination: {more: ...}}
return
data
;
}
},
// data-html="true" miatt: hagyjuk az HTML-t renderelődni
escapeMarkup
:
function
(
m
)
{
return
m
;
}
});
});
}
function
generateVmHTML
(
data
,
is_last
)
{
return
'<a href="'
+
data
.
url
+
'" class="list-group-item'
+
(
is_last
?
' list-group-item-last'
:
''
)
+
'">'
+
...
...
circle/dashboard/templates/base.html
View file @
744c1885
...
...
@@ -99,100 +99,5 @@
{% block extra_etc %}
{% endblock %}
<style>
/* Full-page loading overlay */
#loading-overlay
{
position
:
fixed
;
top
:
0
;
left
:
0
;
width
:
100%
;
height
:
100%
;
background
:
rgba
(
255
,
255
,
255
,
0.85
);
z-index
:
9999
;
display
:
none
;
}
#loading-overlay
.spinner
{
position
:
absolute
;
top
:
50%
;
left
:
50%
;
transform
:
translate
(
-50%
,
-50%
);
text-align
:
center
;
color
:
#555
;
}
</style>
<div
id=
"loading-overlay"
>
<div
class=
"spinner"
>
<i
class=
"fa fa-spinner fa-spin fa-3x"
></i>
<p>
{% trans "Loading..." %}
</p>
</div>
</div>
//
<script>
// (function () {
// // Robust loader without beforeunload/select-change pitfalls.
// var overlay = document.getElementById("loading-overlay");
// if (!overlay) return;
//
// var timer = null;
//
// function showLoaderDelayed() {
// // Avoid stacking timers.
// if (timer) return;
//
// timer = setTimeout(function () {
// overlay.style.display = "block";
// }, 300); // show only if navigation takes noticeable time
// }
//
// function cancelLoader() {
// if (timer) {
// clearTimeout(timer);
// timer = null;
// }
// // In case it was shown and the browser restored from BFCache.
// overlay.style.display = "none";
// }
//
// // Cancel on load/pageshow (covers bfcache restore too).
// window.addEventListener("load", cancelLoader);
// window.addEventListener("pageshow", cancelLoader);
//
// // Show on form submit (POST or GET).
// var forms = document.getElementsByTagName("form");
// for (var i = 0; i
<
forms
.
length
;
i
++
)
{
// forms[i].addEventListener("submit", function () {
//// showLoaderDelayed();
// });
// }
// // Show on autosubmit select change (programmatic form.submit() does not trigger submit events).
// var autos = document.querySelectorAll('select[data-autosubmit="1"]');
// for (var k = 0; k
<
autos
.
length
;
k
++
)
{
// autos[k].addEventListener("change", function () {
// showLoaderDelayed();
// });
// }
//
// // Show on same-tab link clicks.
// var links = document.getElementsByTagName("a");
// for (var j = 0; j
<
links
.
length
;
j
++
)
{
// (function (a) {
// a.addEventListener("click", function (e) {
// // Ignore modified clicks (new tab/window, etc.)
// if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey) return;
// if (a.target && a.target !== "_self") return;
// if (!a.href) return;
//
// // Ignore hash-only navigation on the same page.
// var href = a.getAttribute("href");
// if (href && href.charAt(0) === "#") return;
//
//// showLoaderDelayed();
// });
// })(links[j]);
// }
// })();
//
</script>
</body>
</html>
circle/dashboard/templates/dashboard/base.html
View file @
744c1885
...
...
@@ -38,9 +38,10 @@
</a>
</li>
<li>
<a
href=
"{% url "
dashboard
.
views
.
storage
"
%}"
>
<a
href=
"{% url "
dashboard
.
views
.
storage
"
%}"
class=
"nav-spinner"
>
<i
class=
"fa fa-database"
></i>
<span
class=
"hidden-sm"
>
{% trans "Storage" %}
</span>
<i
class=
"fa fa-spinner"
style=
"display:none; margin-left:4px;"
></i>
</a>
</li>
<li>
...
...
circle/dashboard/templates/dashboard/storage/detail.html
View file @
744c1885
...
...
@@ -13,7 +13,11 @@
<div
class=
"col-md-5"
>
<div
class=
"panel panel-default"
>
<div
class=
"panel-heading"
>
<h3
class=
"no-margin"
><i
class=
"fa fa-database"
></i>
{% trans "Datastore" %}
</h3>
<h3
class=
"no-margin"
>
<i
class=
"fa fa-database"
></i>
i
{% trans "Datastore" %}
<i
id=
"ds-spinner"
class=
"fa fa-spinner"
style=
"display:none;"
></i>
</h3>
</div>
<div
class=
"panel-body"
>
<form
method=
"get"
action=
""
>
...
...
@@ -111,7 +115,9 @@
{% if stats %}
<div
class=
"panel panel-default"
>
<div
class=
"panel-heading"
>
<h3
class=
"no-margin"
><i
class=
"fa fa-file"
></i>
{% trans "Disks" %}
</h3>
<h3
class=
"no-margin"
><i
class=
"fa fa-file"
></i>
{% trans "Disks" %}
<i
id=
"disks-spinner"
class=
"fa fa-spinner"
style=
"display:none;"
></i>
</h3>
</div>
<div
class=
"panel-body"
>
<div
class=
"row"
>
...
...
@@ -119,11 +125,13 @@
<ul
class=
"nav nav-pills"
style=
"margin: 5px 0 20px 0;"
>
<li
class=
"disabled"
><a
href=
"#"
>
{% trans "Filter by type" %}
</a></li>
<li
{%
if
not
request
.
GET
.
filter
%}
class=
"active"
{%
endif
%}
>
<a
href=
"{{ request.path }}?s={{ request.GET.s }}"
>
{% trans "ALL" %}
</a>
<a
href=
"{{ request.path }}?ds={{ ds_selected }}&s={{ request.GET.s }}"
class=
"storage-filter"
>
{% trans "ALL" %}
</a>
</li>
{% for f in filter_names %}
<li
{%
if
request
.
GET
.
filter =
=
f
.
0
%}
class=
"active"
{%
endif
%}
>
<a
href=
"?
filter={{ f.0 }}&s={{ request.GET.s }}
"
>
{{ f.1|capfirst }}
</a>
<a
href=
"?
ds={{ ds_selected }}&filter={{ f.0 }}&s={{ request.GET.s }}"
class=
"storage-filter
"
>
{{ f.1|capfirst }}
</a>
</li>
{% endfor %}
</ul>
...
...
@@ -134,6 +142,7 @@
<input
type=
"text"
name=
"s"
class=
"form-control"
value=
"{{ request.GET.s }}"
placeholder=
"{% trans "
Search
..."
%}"
/>
<input
type=
"hidden"
name=
"filter"
value=
"{{ request.GET.filter }}"
/>
<input
type=
"hidden"
name=
"ds"
value=
"{{ ds_selected }}"
/>
<span
class=
"input-group-btn"
>
<button
class=
"btn btn-primary"
><i
class=
"fa fa-search"
></i></button>
</span>
...
...
circle/dashboard/views/storage.py
View file @
744c1885
...
...
@@ -17,6 +17,7 @@
from
__future__
import
unicode_literals
,
absolute_import
import
errno
import
logging
from
django.contrib
import
messages
from
django.core.urlresolvers
import
reverse
...
...
@@ -35,6 +36,8 @@ from ..forms import DataStoreForm, DiskForm
from
django.shortcuts
import
get_object_or_404
,
redirect
from
django.db
import
IntegrityError
logger
=
logging
.
getLogger
(
__name__
)
class
StorageDetail
(
SuperuserRequiredMixin
,
TemplateView
):
template_name
=
"dashboard/storage/detail.html"
...
...
@@ -147,7 +150,7 @@ class StorageDetail(SuperuserRequiredMixin, TemplateView):
messages
.
success
(
request
,
_
(
"Datastore updated."
))
return
self
.
_redirect_with_ds
(
ds
.
pk
)
def
get_table_data
(
self
,
ds
):
def
get_table_data
(
self
,
ds
,
missing
):
if
ds
is
None
:
return
Disk
.
objects
.
none
()
...
...
@@ -163,12 +166,18 @@ class StorageDetail(SuperuserRequiredMixin, TemplateView):
}
if
filter_name
:
qs
=
qs
.
filter
(
**
filter_queries
.
get
(
filter_name
,
{}))
if
filter_name
==
'missing'
:
qs
=
missing
else
:
qs
=
qs
.
filter
(
**
filter_queries
.
get
(
filter_name
,
{}))
if
search
:
search
=
search
.
strip
()
qs
=
qs
.
filter
(
Q
(
name__icontains
=
search
)
|
Q
(
filename__icontains
=
search
))
qs
=
qs
.
filter
(
Q
(
name__icontains
=
search
)
|
Q
(
filename__icontains
=
search
)
|
Q
(
instance_set__name__icontains
=
search
)
|
Q
(
template_set__name__icontains
=
search
)
)
.
distinct
()
return
qs
...
...
@@ -235,7 +244,8 @@ class StorageDetail(SuperuserRequiredMixin, TemplateView):
context
[
"orphan_disks"
]
=
None
context
[
"disk_table"
]
=
DiskListTable
(
self
.
get_table_data
(
ds
),
request
=
self
.
request
,
self
.
get_table_data
(
ds
,
missing
=
context
[
"missing_disks"
]),
request
=
self
.
request
,
template
=
"django_tables2/with_pagination.html"
)
...
...
@@ -243,15 +253,27 @@ class StorageDetail(SuperuserRequiredMixin, TemplateView):
(
'vm'
,
_
(
"virtual machine"
)),
(
'template'
,
_
(
"template"
)),
(
'none'
,
_
(
"none"
)),
(
'missing'
,
_
(
"missing"
)),
)
return
context
class
DiskDetail
(
SuperuserRequiredMixin
,
UpdateView
):
model
=
Disk
form_class
=
DiskForm
template_name
=
"dashboard/storage/disk.html"
def
form_valid
(
self
,
form
):
pass
# Save only allowed edits (datastore) and redirect back.
self
.
object
=
form
.
save
()
messages
.
success
(
self
.
request
,
_
(
"Disk updated."
))
return
redirect
(
self
.
request
.
path
)
#class DiskDetail(SuperuserRequiredMixin, UpdateView):
# model = Disk
# form_class = DiskForm
# template_name = "dashboard/storage/disk.html"
#
# def form_valid(self, form):
# pass
#
circle/manager/scheduler.py
View file @
744c1885
...
...
@@ -62,10 +62,10 @@ def common_select(instance, nodes):
nodes
=
[
n
for
n
in
nodes
if
n
.
schedule_enabled
and
n
.
online
and
has_traits
(
instance
.
req_traits
.
all
(),
n
)]
logger
.
error
(
'capab:
%
s 0
selected_nodes:
%
s'
,
instance
.
capability_group
,
nodes
)
logger
.
debug
(
'capab:
%
s
selected_nodes:
%
s'
,
instance
.
capability_group
,
nodes
)
if
instance
.
capability_group
:
nodes
=
[
n
for
n
in
nodes
if
n
.
capability
==
instance
.
capability_group
]
logger
.
error
(
'capab:
%
s selected_nodes:
%
s'
,
instance
.
capability_group
,
nodes
)
nodes
=
[
n
for
n
in
nodes
if
n
ot
n
.
capability
or
n
.
capability
==
instance
.
capability_group
]
logger
.
debug
(
'capab:
%
s selected_nodes:
%
s'
,
instance
.
capability_group
,
nodes
)
if
not
nodes
:
logger
.
warning
(
'select_node: no usable node for
%
s'
,
unicode
(
instance
))
raise
TraitsUnsatisfiableException
()
...
...
circle/storage/tasks/periodic_tasks.py
View file @
744c1885
...
...
@@ -39,16 +39,17 @@ def garbage_collector(timeout=15):
args
=
[
ds
.
path
],
queue
=
queue_name
)
.
get
(
timeout
=
timeout
))
disks
=
set
(
ds
.
get_deletable_disks
())
queue_name
=
ds
.
get_remote_queue_name
(
'storage'
,
priority
=
'slow'
)
for
i
in
disks
&
files
:
logger
.
info
(
"Image:
%
s at Datastore:
%
s moved to trash folder."
%
(
i
,
ds
.
path
))
storage_tasks
.
move_to_trash
.
apply_async
(
args
=
[
ds
.
path
,
i
],
queue
=
queue_name
)
.
get
(
timeout
=
timeout
)
try
:
for
i
in
disks
&
files
:
logger
.
error
(
"Image:
%
s at Datastore:
%
s moved to trash folder."
%
(
i
,
ds
.
path
))
storage_tasks
.
move_to_trash
.
apply_async
(
args
=
[
ds
.
path
,
i
],
queue
=
queue_name
)
.
get
(
timeout
=
timeout
)
storage_tasks
.
make_free_space
.
apply_async
(
args
=
[
ds
.
path
],
queue
=
queue_name
)
.
get
(
timeout
=
timeout
)
except
Exception
as
e
:
logger
.
warning
(
str
(
e
))
logger
.
error
(
str
(
e
))
@celery.task
...
...
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