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
3b66d6d6
authored
Mar 20, 2014
by
Guba Sándor
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'feature-base_template'
Conflicts: circle/dashboard/static/dashboard/dashboard.css circle/dashboard/urls.py
parents
4d5d7287
f4067fef
Hide whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
358 additions
and
90 deletions
+358
-90
circle/dashboard/fixtures/test-vm-fixture.json
+18
-1
circle/dashboard/forms.py
+3
-9
circle/dashboard/static/dashboard/dashboard.css
+7
-0
circle/dashboard/static/dashboard/dashboard.js
+21
-3
circle/dashboard/static/dashboard/disk-list.js
+23
-0
circle/dashboard/templates/dashboard/_disk-list-element.html
+9
-6
circle/dashboard/templates/dashboard/_vm-create-1.html
+1
-1
circle/dashboard/templates/dashboard/confirm/ajax-delete.html
+1
-1
circle/dashboard/templates/dashboard/confirm/base-delete.html
+2
-2
circle/dashboard/templates/dashboard/template-create.html
+1
-1
circle/dashboard/templates/dashboard/template-edit.html
+13
-6
circle/dashboard/templates/dashboard/template-list.html
+2
-2
circle/dashboard/templates/dashboard/vm-detail.html
+1
-0
circle/dashboard/templates/dashboard/vm-detail/resources.html
+3
-1
circle/dashboard/tests/test_views.py
+3
-0
circle/dashboard/urls.py
+7
-1
circle/dashboard/views.py
+89
-10
circle/storage/models.py
+58
-14
circle/storage/tasks/periodic_tasks.py
+6
-5
circle/storage/tests/test_models.py
+40
-0
circle/vm/models/instance.py
+50
-27
No files found.
circle/dashboard/fixtures/test-vm-fixture.json
View file @
3b66d6d6
...
...
@@ -42,6 +42,23 @@
}
},
{
"pk"
:
1
,
"model"
:
"storage.diskactivity"
,
"fields"
:{
"activity_code"
:
"storage.Disk.create"
,
"succeeded"
:
true
,
"parent"
:
null
,
"created"
:
"2014-03-18T15:44:37.671Z"
,
"started"
:
"2014-03-18T15:44:37.671Z"
,
"finished"
:
"2014-03-18T15:44:37.677Z"
,
"modified"
:
"2014-03-18T15:44:37.679Z"
,
"task_uuid"
:
null
,
"user"
:
1
,
"disk"
:
1
,
"result"
:
null
}
},
{
"pk"
:
1
,
"model"
:
"auth.permission"
,
"fields"
:
{
...
...
@@ -1497,7 +1514,7 @@
"boot_menu"
:
false
,
"ram_size"
:
1024
,
"modified"
:
"2014-01-24T00:58:19.654Z"
,
"system"
:
""
,
"system"
:
"
bubuntu
"
,
"priority"
:
20
,
"access_method"
:
"ssh"
,
"raw_data"
:
""
,
...
...
circle/dashboard/forms.py
View file @
3b66d6d6
...
...
@@ -445,15 +445,12 @@ class NodeForm(forms.ModelForm):
class
TemplateForm
(
forms
.
ModelForm
):
networks
=
forms
.
ModelMultipleChoiceField
(
queryset
=
VLANS
,
required
=
False
)
system
=
forms
.
CharField
(
widget
=
forms
.
TextInput
)
def
__init__
(
self
,
*
args
,
**
kwargs
):
parent
=
kwargs
.
pop
(
"parent"
,
None
)
self
.
user
=
kwargs
.
pop
(
"user"
,
None
)
super
(
TemplateForm
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
self
.
fields
[
'disks'
]
=
forms
.
ModelMultipleChoiceField
(
queryset
=
Disk
.
get_objects_with_level
(
'user'
,
self
.
user
)
.
exclude
(
type
=
"qcow2-snap"
)
)
data
=
self
.
data
.
copy
()
data
[
'owner'
]
=
self
.
user
.
pk
...
...
@@ -468,7 +465,6 @@ class TemplateForm(forms.ModelForm):
for
f
in
fields
:
self
.
initial
[
f
]
=
parent
[
f
]
self
.
initial
[
'lease'
]
=
parent
[
'lease_id'
]
self
.
initial
[
'disks'
]
=
template
.
disks
.
all
()
self
.
initial
[
'parent'
]
=
template
self
.
initial
[
'name'
]
=
"Clone of
%
s"
%
self
.
initial
[
'name'
]
self
.
for_networks
=
template
...
...
@@ -506,8 +502,6 @@ class TemplateForm(forms.ModelForm):
if
commit
:
instance
.
save
()
self
.
instance
.
disks
=
data
[
'disks'
]
# TODO why do I need this
# create and/or delete InterfaceTemplates
networks
=
InterfaceTemplate
.
objects
.
filter
(
template
=
self
.
instance
)
.
values_list
(
"vlan"
,
flat
=
True
)
...
...
@@ -526,6 +520,7 @@ class TemplateForm(forms.ModelForm):
kwargs_raw_data
=
{}
if
not
self
.
user
.
is_superuser
:
kwargs_raw_data
[
'readonly'
]
=
None
helper
=
FormHelper
()
helper
.
layout
=
Layout
(
Field
(
"name"
),
...
...
@@ -585,7 +580,6 @@ class TemplateForm(forms.ModelForm):
),
Fieldset
(
_
(
"External"
),
Field
(
"disks"
),
Field
(
"networks"
),
Field
(
"lease"
),
Field
(
"tags"
),
...
...
@@ -596,7 +590,7 @@ class TemplateForm(forms.ModelForm):
class
Meta
:
model
=
InstanceTemplate
exclude
=
(
'state'
,
)
exclude
=
(
'state'
,
'disks'
,
)
class
LeaseForm
(
forms
.
ModelForm
):
...
...
circle/dashboard/static/dashboard/dashboard.css
View file @
3b66d6d6
...
...
@@ -409,4 +409,11 @@ footer {
footer
a
,
footer
a
:hover
,
footer
a
:visited
{
color
:
white
;
text-decoration
:
underline
;
.template-disk-list
{
list-style
:
none
;
padding-left
:
0
;
}
.template-disk-list
li
{
padding-bottom
:
5px
;
}
circle/dashboard/static/dashboard/dashboard.js
View file @
3b66d6d6
...
...
@@ -122,6 +122,18 @@ $(function () {
'redirect'
:
dir
});
return
false
;
});
/* for disk remove buttons */
$
(
'.disk-remove'
).
click
(
function
()
{
var
disk_pk
=
$
(
this
).
data
(
'disk-pk'
);
addModalConfirmation
(
deleteObject
,
{
'url'
:
'/dashboard/disk/'
+
disk_pk
+
'/remove/'
,
'data'
:
[],
'pk'
:
disk_pk
,
'type'
:
"disk"
,
});
return
false
;
});
/* for Node removes buttons */
$
(
'.node-delete'
).
click
(
function
()
{
...
...
@@ -273,9 +285,15 @@ function deleteObject(data) {
if
(
!
data
[
'redirect'
])
{
selected
=
[];
addMessage
(
re
[
'message'
],
'success'
);
$
(
'a[data-'
+
data
[
'type'
]
+
'-pk="'
+
data
[
'pk'
]
+
'"]'
).
closest
(
'tr'
).
fadeOut
(
function
()
{
$
(
this
).
remove
();
});
if
(
data
.
type
===
"disk"
)
{
// no need to remove them from DOM
$
(
'a[data-disk-pk="'
+
data
.
pk
+
'"]'
).
parent
(
"li"
).
fadeOut
();
$
(
'a[data-disk-pk="'
+
data
.
pk
+
'"]'
).
parent
(
"h4"
).
fadeOut
();
}
else
{
$
(
'a[data-'
+
data
[
'type'
]
+
'-pk="'
+
data
[
'pk'
]
+
'"]'
).
closest
(
'tr'
).
fadeOut
(
function
()
{
$
(
this
).
remove
();
});
}
}
else
{
window
.
location
.
replace
(
'/dashboard'
);
}
...
...
circle/dashboard/static/dashboard/disk-list.js
0 → 100644
View file @
3b66d6d6
$
(
function
()
{
$
(
".disk-list-disk-percentage"
).
each
(
function
()
{
var
disk
=
$
(
this
).
data
(
"disk-pk"
);
var
element
=
$
(
this
);
refreshDisk
(
disk
,
element
);
});
});
function
refreshDisk
(
disk
,
element
)
{
$
.
get
(
"/dashboard/disk/"
+
disk
+
"/status/"
,
function
(
result
)
{
if
(
result
.
percentage
==
null
||
result
.
failed
==
"True"
)
{
location
.
reload
();
}
else
{
var
diff
=
result
.
percentage
-
parseInt
(
element
.
html
());
var
refresh
=
5
-
diff
;
refresh
=
refresh
<
1
?
1
:
(
result
.
percentage
==
0
?
1
:
refresh
);
if
(
isNaN
(
refresh
))
refresh
=
2
;
// this should not happen
element
.
html
(
result
.
percentage
);
setTimeout
(
function
()
{
refreshDisk
(
disk
,
element
)},
refresh
*
1000
);
}
});
}
circle/dashboard/templates/dashboard/_disk-list-element.html
View file @
3b66d6d6
{% load i18n %}
{% load sizefieldtags %}
<i
class=
"{% if d.is_downloading %}icon-refresh icon-spin{% else %}icon-file{% endif %}"
></i>
<i
class=
"{% if d.is_downloading %}icon-refresh icon-spin{% else %}icon-file{%
if d.failed %}"
style=
"color: #d9534f;{% endif %}{%
endif %}"
></i>
{{ d.name }} (#{{ d.id }}) -
{% if not d.is_downloading %}
{% if
d.ready
%}
{
{ d.size|filesize }
}
{% if
not d.failed
%}
{
% if d.size %}{{ d.size|filesize }}{% endif %
}
{% else %}
<div
class=
"label label-danger"
>
failed
</div>
<div
class=
"label label-danger"
{%
if
user
.
is_superuser
%}
title=
"{{ d.get_latest_activity_result }}"
{%
endif
%}
>
failed
</div>
{% endif %}
{% else %}
<span
class=
"disk-list-disk-percentage"
data-disk-pk=
"{{ d.pk }}"
>
{{ d.get_download_percentage }}
</span>
%{% endif %}
<div
class=
"btn btn-xs btn-danger pull-right"
><i
class=
"icon-remove"
></i>
Remove
</div>
<a
href=
"{% url "
dashboard
.
views
.
disk-remove
"
pk=
d.pk
%}?
next=
{{
request
.
path
}}"
data-disk-pk=
"{{ d.pk }}"
class=
"btn btn-xs btn-danger pull-right disk-remove"
>
<i
class=
"icon-remove"
></i>
{% if long_remove %} Remove{% endif %}
</a>
<div
style=
"clear: both;"
></div>
circle/dashboard/templates/dashboard/_vm-create-1.html
View file @
3b66d6d6
...
...
@@ -32,7 +32,7 @@
<li>
<i
class=
"icon-file"
></i>
{% trans "Disks" %}
<span
style=
"float: right; text-align: right;"
>
{% for d in t.disks.all %}{{ d.name }} ({
{ d.size|filesize }
}){% if not forloop.last %}, {% endif %}{% endfor %}
{% for d in t.disks.all %}{{ d.name }} ({
% if d.size %}{{ d.size|filesize }}{% endif %
}){% if not forloop.last %}, {% endif %}{% endfor %}
</span>
<div
style=
"clear: both;"
></div>
</li>
...
...
circle/dashboard/templates/dashboard/confirm/ajax-delete.html
View file @
3b66d6d6
...
...
@@ -4,7 +4,7 @@
<div
class=
"modal-content"
>
<div
class=
"modal-body"
>
{% if text %}
{{ text }}
{{ text
|safe
}}
{% else %}
{%blocktrans with object=object%}
Are you sure you want to delete
<strong>
{{ object }}
</strong>
?
...
...
circle/dashboard/templates/dashboard/confirm/base-delete.html
View file @
3b66d6d6
...
...
@@ -15,7 +15,7 @@
</div>
<div
class=
"panel-body"
>
{% if text %}
{{ text }}
{{ text
|safe
}}
{% else %}
{%blocktrans with object=object%}
Are you sure you want to delete
<strong>
{{ object }}
</strong>
?
...
...
@@ -26,7 +26,7 @@
{% csrf_token %}
<a
class=
"btn btn-default"
>
Back
</a>
<input
type=
"hidden"
name=
"next"
value=
"{{ request.GET.next }}"
/>
<button
class=
"btn btn-danger"
>
Yes
, delete
</button>
<button
class=
"btn btn-danger"
>
Yes
</button>
</form>
</div>
</div>
...
...
circle/dashboard/templates/dashboard/template-create.html
View file @
3b66d6d6
...
...
@@ -8,7 +8,7 @@
<div
class=
"panel panel-default"
>
<div
class=
"panel-heading"
>
<a
class=
"pull-right btn btn-default btn-xs"
href=
"{% url "
dashboard
.
views
.
template-list
"
%}"
>
{% trans "Back" %}
</a>
<h3
class=
"no-margin"
><i
class=
"icon-desktop"
></i>
{% trans "Create
template
" %}
</h3>
<h3
class=
"no-margin"
><i
class=
"icon-desktop"
></i>
{% trans "Create
base VM
" %}
</h3>
</div>
<div
class=
"panel-body"
>
{% with form=form %}
...
...
circle/dashboard/templates/dashboard/template-edit.html
View file @
3b66d6d6
...
...
@@ -72,7 +72,10 @@
<h4
class=
"no-margin"
><i
class=
"icon-file"
></i>
{% trans "Disk list" %}
</h4>
</div>
<div
class=
"panel-body"
>
<ul
style=
"list-style: none; padding-left: 0;"
>
<ul
class=
"template-disk-list"
>
{% if not disks %}
{% trans "No disks are added!" %}
{% endif %}
{% for d in disks %}
<li>
{% include "dashboard/_disk-list-element.html" %}
...
...
@@ -104,10 +107,14 @@
font-weight
:
bold
;
}
</style>
<script>
$
(
function
()
{
$
(
"#hint_id_num_cores, #hint_id_priority, #hint_id_ram_size"
).
hide
();
});
</script>
{% endblock %}
{% block extra_js %}
<script>
$
(
function
()
{
$
(
"#hint_id_num_cores, #hint_id_priority, #hint_id_ram_size"
).
hide
();
});
</script>
<script
src=
"{{ STATIC_URL }}dashboard/disk-list.js"
></script>
{% endblock %}
circle/dashboard/templates/dashboard/template-list.html
View file @
3b66d6d6
...
...
@@ -8,7 +8,7 @@
<div
class=
"panel panel-default"
>
<div
class=
"panel-heading"
>
<a
href=
"{% url "
dashboard
.
views
.
template-create
"
%}"
class=
"pull-right btn btn-success btn-xs"
>
<i
class=
"icon-plus"
></i>
new template
<i
class=
"icon-plus"
></i>
{% trans "new base vm" %}
</a>
<h3
class=
"no-margin"
><i
class=
"icon-puzzle-piece"
></i>
{% trans "Templates" %}
</h3>
</div>
...
...
@@ -24,7 +24,7 @@
<div
class=
"panel panel-default"
>
<div
class=
"panel-heading"
>
<a
href=
"{% url "
dashboard
.
views
.
lease-create
"
%}"
class=
"pull-right btn btn-success btn-xs"
style=
"margin-right: 10px;"
>
<i
class=
"icon-plus"
></i>
new lease
<i
class=
"icon-plus"
></i>
{% trans "new lease" %}
</a>
<h3
class=
"no-margin"
><i
class=
"icon-time"
></i>
{% trans "Leases" %}
</h3>
</div>
...
...
circle/dashboard/templates/dashboard/vm-detail.html
View file @
3b66d6d6
...
...
@@ -205,4 +205,5 @@
<script
src=
"{{ STATIC_URL }}dashboard/vm-details.js"
></script>
<script
src=
"{{ STATIC_URL }}dashboard/vm-common.js"
></script>
<script
src=
"{{ STATIC_URL }}dashboard/vm-console.js"
></script>
<script
src=
"{{ STATIC_URL }}dashboard/disk-list.js"
></script>
{% endblock %}
circle/dashboard/templates/dashboard/vm-detail/resources.html
View file @
3b66d6d6
...
...
@@ -60,7 +60,9 @@
{% endif %}
{% for d in instance.disks.all %}
<h4
class=
"list-group-item-heading dashboard-vm-details-network-h3"
>
{% include "dashboard/_disk-list-element.html" %}
{% with long_remove=True %}
{% include "dashboard/_disk-list-element.html" %}
{% endwith %}
</h4>
{% endfor %}
</div>
...
...
circle/dashboard/tests/test_views.py
View file @
3b66d6d6
...
...
@@ -187,6 +187,7 @@ class VmDetailTest(LoginMixin, TestCase):
Vlan
.
objects
.
get
(
id
=
1
)
.
set_level
(
self
.
u1
,
'user'
)
response
=
c
.
post
(
'/dashboard/vm/create/'
,
{
'template'
:
1
,
'system'
:
"bubi"
,
'cpu_priority'
:
1
,
'cpu_count'
:
1
,
'ram_size'
:
1000
})
self
.
assertEqual
(
response
.
status_code
,
403
)
...
...
@@ -199,6 +200,7 @@ class VmDetailTest(LoginMixin, TestCase):
Vlan
.
objects
.
get
(
id
=
1
)
.
set_level
(
self
.
u1
,
'user'
)
response
=
c
.
post
(
'/dashboard/vm/create/'
,
{
'template'
:
1
,
'system'
:
"bubi"
,
'cpu_priority'
:
1
,
'cpu_count'
:
1
,
'ram_size'
:
1000
})
self
.
assertEqual
(
response
.
status_code
,
302
)
...
...
@@ -208,6 +210,7 @@ class VmDetailTest(LoginMixin, TestCase):
self
.
login
(
c
,
'superuser'
)
response
=
c
.
post
(
'/dashboard/vm/create/'
,
{
'template'
:
1
,
'system'
:
"bubi"
,
'cpu_priority'
:
1
,
'cpu_count'
:
1
,
'ram_size'
:
1000
})
self
.
assertEqual
(
response
.
status_code
,
302
)
...
...
circle/dashboard/urls.py
View file @
3b66d6d6
...
...
@@ -10,7 +10,8 @@ from .views import (
TemplateCreate
,
TemplateDelete
,
TemplateDetail
,
TemplateList
,
TransferOwnershipConfirmView
,
TransferOwnershipView
,
vm_activity
,
VmCreate
,
VmDelete
,
VmDetailView
,
VmDetailVncTokenView
,
VmGraphView
,
VmList
,
VmMassDelete
,
VmMigrateView
,
VmRenewView
,
VmMassDelete
,
VmMigrateView
,
VmRenewView
,
DiskRemoveView
,
get_disk_download_status
,
)
urlpatterns
=
patterns
(
...
...
@@ -99,6 +100,11 @@ urlpatterns = patterns(
url
(
r'^disk/add/$'
,
DiskAddView
.
as_view
(),
name
=
"dashboard.views.disk-add"
),
url
(
r'^disk/(?P<pk>\d+)/remove/$'
,
DiskRemoveView
.
as_view
(),
name
=
"dashboard.views.disk-remove"
),
url
(
r'^disk/(?P<pk>\d+)/status/$'
,
get_disk_download_status
,
name
=
"dashboard.views.disk-status"
),
url
(
r'^profile/$'
,
MyPreferencesView
.
as_view
(),
name
=
"dashboard.views.profile"
),
)
circle/dashboard/views.py
View file @
3b66d6d6
from
__future__
import
unicode_literals
from
os
import
getenv
import
json
import
logging
...
...
@@ -43,6 +45,7 @@ from vm.models import (
Instance
,
instance_activity
,
InstanceActivity
,
InstanceTemplate
,
Interface
,
InterfaceTemplate
,
Lease
,
Node
,
NodeActivity
,
Trait
,
)
from
storage.models
import
Disk
from
firewall.models
import
Vlan
,
Host
,
Rule
from
dashboard.models
import
Favourite
,
Profile
...
...
@@ -275,6 +278,7 @@ class VmDetailView(CheckedDetailView):
resources
=
{
'num_cores'
:
request
.
POST
.
get
(
'cpu-count'
),
'ram_size'
:
request
.
POST
.
get
(
'ram-size'
),
'max_ram_size'
:
request
.
POST
.
get
(
'ram-size'
),
# TODO: max_ram
'priority'
:
request
.
POST
.
get
(
'cpu-priority'
)
}
Instance
.
objects
.
filter
(
pk
=
self
.
object
.
pk
)
.
update
(
**
resources
)
...
...
@@ -419,12 +423,10 @@ class VmDetailView(CheckedDetailView):
new_name
=
"Saved from
%
s (#
%
d) at
%
s"
%
(
self
.
object
.
name
,
self
.
object
.
pk
,
date
)
template
=
self
.
object
.
save_as_template
(
name
=
new_name
,
owner
=
request
.
user
)
messages
.
success
(
request
,
_
(
"Instance successfully saved as template, "
"please rename it!"
))
return
redirect
(
reverse_lazy
(
"dashboard.views.template-detail"
,
kwargs
=
{
'pk'
:
template
.
pk
}))
self
.
object
.
save_as_template_async
(
name
=
new_name
,
user
=
request
.
user
)
messages
.
success
(
request
,
_
(
"Saving instance as template!"
))
return
redirect
(
"
%
s#activity"
%
self
.
object
.
get_absolute_url
())
def
__shut_down
(
self
,
request
):
self
.
object
=
self
.
get_object
()
...
...
@@ -765,13 +767,29 @@ class TemplateCreate(SuccessMessageMixin, CreateView):
form
=
self
.
form_class
(
request
.
POST
,
user
=
request
.
user
)
if
not
form
.
is_valid
():
return
self
.
get
(
request
,
form
,
*
args
,
**
kwargs
)
post
=
form
.
cleaned_data
for
disk
in
post
[
'disks'
]:
if
not
disk
.
has_level
(
request
.
user
,
'user'
):
raise
PermissionDenied
()
else
:
post
=
form
.
cleaned_data
networks
=
self
.
__create_networks
(
post
.
pop
(
"networks"
))
req_traits
=
post
.
pop
(
"req_traits"
)
tags
=
post
.
pop
(
"tags"
)
post
[
'pw'
]
=
User
.
objects
.
make_random_password
()
post
.
pop
(
"parent"
)
post
[
'max_ram_size'
]
=
post
[
'ram_size'
]
inst
=
Instance
.
create
(
params
=
post
,
disks
=
[],
networks
=
networks
,
tags
=
tags
,
req_traits
=
req_traits
)
messages
.
success
(
request
,
_
(
"Your disk has been created, "
"you can now add disks to it!"
))
return
redirect
(
"
%
s#resources"
%
inst
.
get_absolute_url
())
return
super
(
TemplateCreate
,
self
)
.
post
(
self
,
request
,
args
,
kwargs
)
def
__create_networks
(
self
,
vlans
):
networks
=
[]
for
v
in
vlans
:
networks
.
append
(
InterfaceTemplate
(
vlan
=
v
,
managed
=
v
.
managed
))
return
networks
def
get_success_url
(
self
):
return
reverse_lazy
(
"dashboard.views.template-list"
)
...
...
@@ -2115,3 +2133,64 @@ def set_language_cookie(request, response, lang=None):
cname
=
getattr
(
settings
,
'LANGUAGE_COOKIE_NAME'
,
'django_language'
)
response
.
set_cookie
(
cname
,
lang
,
365
*
86400
)
class
DiskRemoveView
(
DeleteView
):
model
=
Disk
def
get_template_names
(
self
):
if
self
.
request
.
is_ajax
():
return
[
'dashboard/confirm/ajax-delete.html'
]
else
:
return
[
'dashboard/confirm/base-delete.html'
]
def
get_context_data
(
self
,
**
kwargs
):
context
=
super
(
DiskRemoveView
,
self
)
.
get_context_data
(
**
kwargs
)
disk
=
self
.
get_object
()
app
=
disk
.
get_appliance
()
context
[
'title'
]
=
_
(
"Disk remove confirmation"
)
context
[
'text'
]
=
_
(
"Are you sure you want to remove "
"<strong>
%(disk)
s</strong> from "
"<strong>
%(app)
s</strong>?"
%
{
'disk'
:
disk
,
'app'
:
app
}
)
return
context
def
delete
(
self
,
request
,
*
args
,
**
kwargs
):
disk
=
self
.
get_object
()
if
not
disk
.
has_level
(
request
.
user
,
'owner'
):
raise
PermissionDenied
()
disk
=
self
.
get_object
()
app
=
disk
.
get_appliance
()
app
.
disks
.
remove
(
disk
)
disk
.
destroy
()
next_url
=
request
.
POST
.
get
(
"next"
)
success_url
=
next_url
if
next_url
else
app
.
get_absolute_url
()
success_message
=
_
(
"Disk successfully removed!"
)
if
request
.
is_ajax
():
return
HttpResponse
(
json
.
dumps
({
'message'
:
success_message
}),
content_type
=
"application/json"
,
)
else
:
messages
.
success
(
request
,
success_message
)
return
HttpResponseRedirect
(
"
%
s#resources"
%
success_url
)
@require_GET
def
get_disk_download_status
(
request
,
pk
):
disk
=
Disk
.
objects
.
get
(
pk
=
pk
)
if
not
disk
.
has_level
(
request
.
user
,
'owner'
):
raise
PermissionDenied
()
return
HttpResponse
(
json
.
dumps
({
'percentage'
:
disk
.
get_download_percentage
(),
'failed'
:
disk
.
failed
}),
content_type
=
"application/json"
,
)
circle/storage/models.py
View file @
3b66d6d6
# coding=utf-8
# -*- coding: utf-8 -*-
from
__future__
import
unicode_literals
from
contextlib
import
contextmanager
import
logging
...
...
@@ -107,10 +108,35 @@ class Disk(AclBase, TimeStampedModel):
self
.
disk
=
disk
class
DiskIsNotReady
(
Exception
):
""" Exception for operations that need a deployed disk.
"""
def
__init__
(
self
,
disk
,
message
=
None
):
if
message
is
None
:
message
=
(
"The requested operation can't be performed on "
"disk '
%
s (
%
s)' because it has never been"
"deployed."
%
(
disk
.
name
,
disk
.
filename
))
Exception
.
__init__
(
self
,
message
)
self
.
disk
=
disk
@property
def
ready
(
self
):
""" Returns True if the disk is physically ready on the storage.
It needs at least 1 successfull deploy action.
"""
return
self
.
activity_log
.
filter
(
activity_code__endswith
=
"deploy"
,
succeeded__isnull
=
False
)
succeeded
=
True
)
@property
def
failed
(
self
):
""" Returns True if the last activity on the disk is failed.
"""
result
=
self
.
activity_log
.
all
()
.
order_by
(
'-id'
)[
0
]
.
succeeded
return
not
(
result
is
None
)
and
not
result
@property
def
path
(
self
):
...
...
@@ -155,18 +181,22 @@ class Disk(AclBase, TimeStampedModel):
}[
self
.
type
]
def
is_downloading
(
self
):
return
self
.
activity_log
.
filter
(
activity_code__endswith
=
"downloading_disk"
,
succeeded__isnull
=
True
)
return
self
.
size
is
None
and
not
self
.
failed
def
get_download_percentage
(
self
):
if
not
self
.
is_downloading
():
return
None
task
=
self
.
activity_log
.
filter
(
activity_code__endswith
=
"deploy"
,
succeeded__isnull
=
True
)[
0
]
.
task_uuid
result
=
celery
.
AsyncResult
(
id
=
task
)
return
result
.
info
.
get
(
"percent"
)
try
:
task
=
self
.
activity_log
.
filter
(
activity_code__endswith
=
"deploy"
,
succeeded__isnull
=
True
)[
0
]
.
task_uuid
result
=
celery
.
AsyncResult
(
id
=
task
)
return
result
.
info
.
get
(
"percent"
)
except
:
return
0
def
get_latest_activity_result
(
self
):
return
self
.
activity_log
.
latest
(
"pk"
)
.
result
@property
def
is_deletable
(
self
):
...
...
@@ -192,6 +222,17 @@ class Disk(AclBase, TimeStampedModel):
"""
return
any
(
i
.
state
!=
'STOPPED'
for
i
in
self
.
instance_set
.
all
())
def
get_appliance
(
self
):
"""Return an Instance or InstanceTemplate object where the disk is used
"""
instance
=
self
.
instance_set
.
all
()
template
=
self
.
template_set
.
all
()
app
=
list
(
instance
)
+
list
(
template
)
if
len
(
app
)
>
0
:
return
app
[
0
]
else
:
return
None
def
get_exclusive
(
self
):
"""Get an instance of the disk for exclusive usage.
...
...
@@ -247,7 +288,7 @@ class Disk(AclBase, TimeStampedModel):
return
u"
%
s (#
%
d)"
%
(
self
.
name
,
self
.
id
or
0
)
def
clean
(
self
,
*
args
,
**
kwargs
):
if
self
.
size
==
""
and
self
.
base
:
if
(
self
.
size
is
None
or
""
)
and
self
.
base
:
self
.
size
=
self
.
base
.
size
super
(
Disk
,
self
)
.
clean
(
*
args
,
**
kwargs
)
...
...
@@ -305,7 +346,9 @@ class Disk(AclBase, TimeStampedModel):
"""
datastore
=
params
.
pop
(
'datastore'
,
DataStore
.
objects
.
get
())
disk
=
cls
(
filename
=
str
(
uuid
.
uuid4
()),
datastore
=
datastore
,
**
params
)
disk
.
clean
()
disk
.
save
()
logger
.
debug
(
"Disk created:
%
s"
,
params
)
with
disk_activity
(
code_suffix
=
"create"
,
user
=
user
,
disk
=
disk
):
...
...
@@ -366,8 +409,6 @@ class Disk(AclBase, TimeStampedModel):
kwargs
.
setdefault
(
'name'
,
url
.
split
(
'/'
)[
-
1
])
disk
=
Disk
.
create
(
type
=
"iso"
,
instance
=
instance
,
user
=
user
,
size
=
None
,
**
kwargs
)
# TODO get proper datastore
disk
.
datastore
=
DataStore
.
objects
.
get
()
queue_name
=
disk
.
get_remote_queue_name
(
'storage'
)
def
__on_abort
(
activity
,
error
):
...
...
@@ -439,6 +480,7 @@ class Disk(AclBase, TimeStampedModel):
"""
mapping
=
{
'qcow2-snap'
:
(
'qcow2-norm'
,
self
.
base
),
'qcow2-norm'
:
(
'qcow2-norm'
,
self
),
}
if
self
.
type
not
in
mapping
.
keys
():
raise
self
.
WrongDiskTypeError
(
self
.
type
)
...
...
@@ -446,6 +488,9 @@ class Disk(AclBase, TimeStampedModel):
if
self
.
is_in_use
:
raise
self
.
DiskInUseError
(
self
)
if
not
self
.
ready
:
raise
self
.
DiskIsNotReady
(
self
)
# from this point on, the caller has to guarantee that the disk is not
# going to be used until the operation is complete
...
...
@@ -455,7 +500,6 @@ class Disk(AclBase, TimeStampedModel):
name
=
self
.
name
,
size
=
self
.
size
,
type
=
new_type
)
disk
.
save
()
with
disk_activity
(
code_suffix
=
"save_as"
,
disk
=
self
,
user
=
user
,
task_uuid
=
task_uuid
):
with
disk_activity
(
code_suffix
=
"deploy"
,
disk
=
disk
,
...
...
circle/storage/tasks/periodic_tasks.py
View file @
3b66d6d6
from
storage.models
import
DataStore
import
os
from
manager.mancelery
import
celery
import
logging
from
storage.tasks
import
remote_tasks
...
...
@@ -16,13 +15,15 @@ def garbage_collector(timeout=15):
deletes oldest images from trash.
:param timeout: Seconds before TimeOut exception
:type timeo
i
t: int
:type timeo
u
t: int
"""
for
ds
in
DataStore
.
objects
.
all
():
file_list
=
os
.
listdir
(
ds
.
path
)
disk_list
=
ds
.
get_deletable_disks
()
queue_name
=
ds
.
get_remote_queue_name
(
'storage'
)
for
i
in
set
(
file_list
)
.
intersection
(
disk_list
):
files
=
set
(
remote_tasks
.
list_files
.
apply_async
(
args
=
[
ds
.
path
],
queue
=
queue_name
)
.
get
(
timeout
=
timeout
))
disks
=
set
(
ds
.
get_deletable_disks
())
queue_name
=
ds
.
get_remote_queue_name
(
'storage'
)
for
i
in
disks
&
files
:
logger
.
info
(
"Image:
%
s at Datastore:
%
s moved to trash folder."
%
(
i
,
ds
.
path
))
remote_tasks
.
move_to_trash
.
apply_async
(
...
...
circle/storage/tests/test_models.py
View file @
3b66d6d6
...
...
@@ -2,9 +2,11 @@ from datetime import timedelta
from
django.test
import
TestCase
from
django.utils
import
timezone
from
mock
import
MagicMock
,
Mock
from
..models
import
Disk
,
DataStore
old
=
timezone
.
now
()
-
timedelta
(
days
=
2
)
new
=
timezone
.
now
()
-
timedelta
(
hours
=
2
)
...
...
@@ -46,3 +48,41 @@ class DiskTestCase(TestCase):
self
.
_disk
(
base
=
d
,
destroyed
=
new
)
self
.
_disk
(
base
=
d
)
assert
not
d
.
is_deletable
def
test_save_as_disk_in_use_error
(
self
):
class
MockException
(
Exception
):
pass
d
=
MagicMock
(
spec
=
Disk
)
d
.
DiskInUseError
=
MockException
d
.
type
=
"qcow2-norm"
d
.
is_in_use
=
True
with
self
.
assertRaises
(
MockException
):
Disk
.
save_as
(
d
)
def
test_save_as_wrong_type
(
self
):
class
MockException
(
Exception
):
pass
d
=
MagicMock
(
spec
=
Disk
)
d
.
WrongDiskTypeError
=
MockException
d
.
type
=
"wrong"
with
self
.
assertRaises
(
MockException
):
Disk
.
save_as
(
d
)
def
test_save_as_disk_not_ready
(
self
):
class
MockException
(
Exception
):
pass
d
=
MagicMock
(
spec
=
Disk
)
d
.
DiskIsNotReady
=
MockException
d
.
type
=
"qcow2-norm"
d
.
is_in_use
=
False
d
.
ready
=
False
with
self
.
assertRaises
(
MockException
):
Disk
.
save_as
(
d
)
def
test_download_percentage_no_download
(
self
):
d
=
MagicMock
(
spec
=
Disk
)
d
.
is_downloading
=
Mock
(
return_value
=
False
)
assert
Disk
.
get_download_percentage
(
d
)
is
None
circle/vm/models/instance.py
View file @
3b66d6d6
...
...
@@ -93,7 +93,6 @@ class VirtualMachineDescModel(BaseResourceConfigModel):
"for hosting the VM."
),
verbose_name
=
_
(
"required traits"
))
system
=
TextField
(
verbose_name
=
_
(
'operating system'
),
blank
=
True
,
help_text
=
(
_
(
'Name of operating system in '
'format like "
%
s".'
)
%
'Ubuntu 12.04 LTS Desktop amd64'
))
...
...
@@ -250,7 +249,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel,
if
message
is
None
:
message
=
(
"The instance's current state (
%
s) is "
"inappropriate for the invoked operation."
%
instance
.
stat
e
)
%
instance
.
stat
us
)
Exception
.
__init__
(
self
,
message
)
...
...
@@ -266,7 +265,9 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel,
@property
def
is_running
(
self
):
return
self
.
state
==
'RUNNING'
"""Check if VM is in running state.
"""
return
self
.
status
==
'RUNNI