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
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