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
1ab77c1f
authored
Jan 27, 2021
by
Szeberényi Imre
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' into 'smallville_fix'
# Conflicts: # circle/vm/tasks/local_periodic_tasks.py
parents
fa743214
76a2a4a6
Pipeline
#1391
failed with stage
in 0 seconds
Changes
51
Pipelines
1
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
51 changed files
with
1098 additions
and
169 deletions
+1098
-169
.gitignore
+2
-0
circle/circle/settings/base.py
+11
-1
circle/dashboard/fixtures/node.json
+4
-1
circle/dashboard/forms.py
+0
-0
circle/dashboard/migrations/0007_groupprofile_disk_quota.py
+21
-0
circle/dashboard/migrations/0008_profile_template_instance_limit.py
+20
-0
circle/dashboard/models.py
+19
-8
circle/dashboard/static/dashboard/activity.js
+2
-1
circle/dashboard/static/dashboard/dashboard.js
+2
-0
circle/dashboard/static/dashboard/dashboard.less
+5
-1
circle/dashboard/static/dashboard/node-list.js
+7
-0
circle/dashboard/store_api.py
+35
-17
circle/dashboard/templates/dashboard/_disk-list-element.html
+19
-5
circle/dashboard/templates/dashboard/node-list.html
+19
-0
circle/dashboard/templates/dashboard/vm-detail/_disk-operations.html
+6
-6
circle/dashboard/templates/dashboard/vm-detail/network.html
+6
-1
circle/dashboard/tests/test_views.py
+2
-2
circle/dashboard/urls.py
+3
-0
circle/dashboard/views/node.py
+44
-2
circle/dashboard/views/store.py
+2
-1
circle/dashboard/views/util.py
+1
-1
circle/dashboard/views/vm.py
+56
-33
circle/manager/mancelery.py
+19
-0
circle/manager/scheduler.py
+102
-15
circle/request/forms.py
+72
-14
circle/request/migrations/0005_requestfield.py
+25
-0
circle/request/migrations/0006_auto_20181115_1548.py
+20
-0
circle/request/migrations/0007_requestfield_request_type.py
+20
-0
circle/request/models.py
+22
-0
circle/request/templates/request/_request-field-form.html
+23
-0
circle/request/templates/request/_request-lease-form.html
+3
-0
circle/request/templates/request/_request-template-form.html
+3
-1
circle/request/templates/request/field-list.html
+57
-0
circle/request/templates/request/list.html
+8
-3
circle/request/templates/request/request-resource.html
+3
-1
circle/request/tests.py
+16
-4
circle/request/urls.py
+11
-0
circle/request/views.py
+59
-5
circle/storage/migrations/0003_auto_20200424_2000.py
+19
-0
circle/storage/models.py
+66
-19
circle/storage/tasks/storage_tasks.py
+10
-0
circle/vm/management/__init__.py
+0
-0
circle/vm/management/commands/__init__.py
+0
-0
circle/vm/management/commands/mass_create_vms.py
+42
-0
circle/vm/migrations/0003_auto_20171213_2018.py
+32
-0
circle/vm/models/instance.py
+75
-1
circle/vm/models/node.py
+52
-15
circle/vm/operations.py
+0
-0
circle/vm/tasks/local_periodic_tasks.py
+47
-5
requirements/base.txt
+5
-5
requirements/production.txt
+1
-1
No files found.
.gitignore
View file @
1ab77c1f
...
...
@@ -8,6 +8,8 @@
*.swp
*.swo
*~
.vscode
.idea
# Sphinx docs:
build
...
...
circle/circle/settings/base.py
View file @
1ab77c1f
...
...
@@ -495,6 +495,7 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
},
'required_attributes'
:
required_attrs
,
'optional_attributes'
:
optional_attrs
,
'want_response_signed'
:
False
,
},
},
'metadata'
:
{
'local'
:
[
remote_metadata
],
},
...
...
@@ -576,7 +577,7 @@ SESSION_COOKIE_NAME = "csessid%x" % (((getnode() // 139) ^
MAX_NODE_RAM
=
get_env_variable
(
"MAX_NODE_RAM"
,
1024
)
MAX_NODE_CPU_CORE
=
get_env_variable
(
"MAX_NODE_CPU_CORE"
,
10
)
SCHEDULER_METHOD
=
get_env_variable
(
"SCHEDULER_METHOD"
,
'
random
'
)
SCHEDULER_METHOD
=
get_env_variable
(
"SCHEDULER_METHOD"
,
'
advanced
'
)
# Url to download the client: (e.g. http://circlecloud.org/client/download/)
CLIENT_DOWNLOAD_URL
=
get_env_variable
(
'CLIENT_DOWNLOAD_URL'
,
'http://circlecloud.org/client/download/'
)
...
...
@@ -590,3 +591,12 @@ REQUEST_HOOK_URL = get_env_variable("REQUEST_HOOK_URL", "")
SSHKEY_EMAIL_ADD_KEY
=
False
TWO_FACTOR_ISSUER
=
get_env_variable
(
"TWO_FACTOR_ISSUER"
,
"CIRCLE"
)
# Default value is every day at midnight
AUTO_MIGRATION_CRONTAB
=
get_env_variable
(
"AUTO_MIGRATION_CRONTAB"
,
"0 0 * * *"
)
AUTO_MIGRATION_TIME_LIMIT_IN_HOURS
=
(
get_env_variable
(
"AUTO_MIGRATION_TIME_LIMIT_IN_HOURS"
,
"2"
))
# Maximum time difference until the monitor's values get valid
SCHEDULER_TIME_SENSITIVITY_IN_SECONDS
=
(
get_env_variable
(
"SCHEDULER_TIME_SENSITIVITY_IN_SECONDS"
,
"60"
))
circle/dashboard/fixtures/node.json
View file @
1ab77c1f
...
...
@@ -65,7 +65,10 @@
"modified"
:
"2014-02-19T21:11:34.671Z"
,
"priority"
:
1
,
"traits"
:
[],
"host"
:
1
"host"
:
1
,
"ram_weight"
:
1.0
,
"cpu_weight"
:
1.0
,
"time_stamp"
:
"2017-12-13T21:08:08.819Z"
}
}
]
circle/dashboard/forms.py
View file @
1ab77c1f
This diff is collapsed.
Click to expand it.
circle/dashboard/migrations/0007_groupprofile_disk_quota.py
0 → 100644
View file @
1ab77c1f
# -*- coding: utf-8 -*-
# Generated by Django 1.11.25 on 2020-04-24 20:00
from
__future__
import
unicode_literals
from
django.db
import
migrations
import
sizefield.models
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'dashboard'
,
'0006_auto_20170707_1909'
),
]
operations
=
[
migrations
.
AddField
(
model_name
=
'groupprofile'
,
name
=
'disk_quota'
,
field
=
sizefield
.
models
.
FileSizeField
(
default
=
2147483648
,
help_text
=
'Disk quota in mebibytes.'
,
verbose_name
=
'disk quota'
),
),
]
circle/dashboard/migrations/0008_profile_template_instance_limit.py
0 → 100644
View file @
1ab77c1f
# -*- coding: utf-8 -*-
# Generated by Django 1.11.25 on 2020-11-06 13:33
from
__future__
import
unicode_literals
from
django.db
import
migrations
,
models
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'dashboard'
,
'0007_groupprofile_disk_quota'
),
]
operations
=
[
migrations
.
AddField
(
model_name
=
'profile'
,
name
=
'template_instance_limit'
,
field
=
models
.
IntegerField
(
default
=
1
),
),
]
circle/dashboard/models.py
View file @
1ab77c1f
...
...
@@ -50,7 +50,7 @@ from common.models import HumanReadableObject, create_readable, Encoder
from
vm.models.instance
import
ACCESS_METHODS
from
.store_api
import
Store
,
NoStoreException
,
NotOkException
,
Timeout
from
.store_api
import
Store
,
NoStoreException
,
NotOkException
from
.validators
import
connect_command_template_validator
logger
=
getLogger
(
__name__
)
...
...
@@ -162,7 +162,7 @@ class ConnectCommand(Model):
validators
=
[
connect_command_template_validator
])
class
Meta
:
ordering
=
(
'id'
,
)
ordering
=
(
'id'
,)
def
__unicode__
(
self
):
return
self
.
template
...
...
@@ -178,6 +178,7 @@ class Profile(Model):
unique
=
True
,
blank
=
True
,
null
=
True
,
max_length
=
64
,
help_text
=
_
(
'Unique identifier of the person, e.g. a student number.'
))
instance_limit
=
IntegerField
(
default
=
5
)
template_instance_limit
=
IntegerField
(
default
=
1
)
use_gravatar
=
BooleanField
(
verbose_name
=
_
(
"Use Gravatar"
),
default
=
True
,
help_text
=
_
(
"Whether to use email address as Gravatar profile image"
))
...
...
@@ -218,7 +219,7 @@ class Profile(Model):
'id'
:
command
.
id
,
'cmd'
:
command
.
template
%
{
'port'
:
instance
.
get_connect_port
(
use_ipv6
=
use_ipv6
),
'host'
:
instance
.
get_connect_host
(
use_ipv6
=
use_ipv6
),
'host'
:
instance
.
get_connect_host
(
use_ipv6
=
use_ipv6
),
'password'
:
instance
.
pw
,
'username'
:
'cloud'
,
}}
for
command
in
commands
]
...
...
@@ -263,7 +264,7 @@ class Profile(Model):
super
(
Profile
,
self
)
.
save
(
*
args
,
**
kwargs
)
class
Meta
:
ordering
=
(
'id'
,
)
ordering
=
(
'id'
,)
permissions
=
(
(
'use_autocomplete'
,
_
(
'Can use autocomplete.'
)),
)
...
...
@@ -275,7 +276,7 @@ class FutureMember(Model):
group
=
ForeignKey
(
Group
)
class
Meta
:
ordering
=
(
'id'
,
)
ordering
=
(
'id'
,)
unique_together
=
(
'org_id'
,
'group'
)
def
__unicode__
(
self
):
...
...
@@ -293,9 +294,13 @@ class GroupProfile(AclBase):
unique
=
True
,
blank
=
True
,
null
=
True
,
max_length
=
64
,
help_text
=
_
(
'Unique identifier of the group at the organization.'
))
description
=
TextField
(
blank
=
True
)
disk_quota
=
FileSizeField
(
verbose_name
=
_
(
'disk quota'
),
default
=
2048
*
1024
*
1024
,
help_text
=
_
(
'Disk quota in mebibytes.'
))
class
Meta
:
ordering
=
(
'id'
,
)
ordering
=
(
'id'
,)
def
__unicode__
(
self
):
return
self
.
group
.
name
...
...
@@ -331,7 +336,11 @@ def create_profile(user):
profile
,
created
=
Profile
.
objects
.
get_or_create
(
user
=
user
)
try
:
Store
(
user
)
.
create_user
(
profile
.
smb_password
,
None
,
profile
.
disk_quota
)
store
=
Store
(
user
)
quotas
=
[
profile
.
disk_quota
]
quotas
+=
[
group
.
profile
.
disk_quota
for
group
in
user
.
groups
.
all
()]
max_quota
=
max
(
quotas
)
store
.
create_user
(
profile
.
smb_password
,
None
,
max_quota
)
except
:
logger
.
exception
(
"Can't create user
%
s"
,
unicode
(
user
))
return
created
...
...
@@ -347,6 +356,7 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
logger
.
debug
(
"Register save_org_id to djangosaml2 pre_user_save"
)
from
djangosaml2.signals
import
pre_user_save
def
save_org_id
(
sender
,
instance
,
attributes
,
**
kwargs
):
logger
.
debug
(
"save_org_id called by
%
s"
,
instance
.
username
)
atr
=
settings
.
SAML_ORG_ID_ATTRIBUTE
...
...
@@ -399,6 +409,7 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
return
False
# User did not change
pre_user_save
.
connect
(
save_org_id
)
...
...
@@ -411,7 +422,7 @@ def update_store_profile(sender, **kwargs):
profile
.
disk_quota
)
except
NoStoreException
:
logger
.
debug
(
"Store is not available."
)
except
(
NotOkException
,
Timeout
)
:
except
NotOkException
:
logger
.
critical
(
"Store is not accepting connections."
)
...
...
circle/dashboard/static/dashboard/activity.js
View file @
1ab77c1f
...
...
@@ -41,12 +41,14 @@ $(function() {
$
(
'#confirmation-modal'
).
on
(
'hidden.bs.modal'
,
function
()
{
$
(
'#confirmation-modal'
).
remove
();
});
$
(
'#vm-migrate-node-list li input:checked'
).
closest
(
'li'
).
addClass
(
'panel-primary'
);
}
});
e
.
preventDefault
();
});
/* if the operation fails show the modal again */
$
(
"body"
).
on
(
"click"
,
"#confirmation-modal #op-form-send"
,
function
()
{
var
url
=
$
(
this
).
closest
(
"form"
).
prop
(
"action"
);
...
...
@@ -237,4 +239,3 @@ String.prototype.hashCode = function() {
}
return
hash
;
};
circle/dashboard/static/dashboard/dashboard.js
View file @
1ab77c1f
...
...
@@ -558,3 +558,5 @@ $(function() {
inputs
.
prop
(
"checked"
,
!
inputs
.
prop
(
"checked"
));
});
});
$
.
fn
.
modal
.
Constructor
.
prototype
.
enforceFocus
=
function
()
{};
circle/dashboard/static/dashboard/dashboard.less
View file @
1ab77c1f
...
...
@@ -1079,6 +1079,10 @@ textarea[name="new_members"] {
max-width: 100%;
}
#node-list-auto-migration-body {
padding: 20px;
}
#vm-list-table td.state,
#vm-list-table td.memory {
white-space: nowrap;
...
...
@@ -1088,7 +1092,7 @@ textarea[name="new_members"] {
vertical-align: middle;
}
.disk-resize-btn {
.disk-resize-btn
, .disk-export-btn
{
margin-right: 5px;
}
...
...
circle/dashboard/static/dashboard/node-list.js
View file @
1ab77c1f
...
...
@@ -3,4 +3,11 @@ $(function() {
// find disabled nodes, set danger (red) on the rows
$
(
'.node-disabled'
).
closest
(
"tr"
).
addClass
(
'danger'
);
});
$
(
'#reschedule-now'
).
click
(
function
()
{
$
.
get
(
$
(
this
).
attr
(
'href'
),
function
(
data
){
highlight
=
data
.
result
===
'ok'
?
'success'
:
'danger'
;
addMessage
(
data
.
message
,
highlight
);
});
return
false
;
});
});
circle/dashboard/store_api.py
View file @
1ab77c1f
...
...
@@ -14,19 +14,20 @@
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from
os.path
import
splitext
import
json
import
logging
from
urlparse
import
urljoin
from
datetime
import
datetime
from
django.http
import
Http404
import
os
from
datetime
import
datetime
from
django.conf
import
settings
from
django.http
import
Http404
from
os.path
import
splitext
from
requests
import
get
,
post
,
codes
from
requests.exceptions
import
Timeout
# noqa
from
sizefield.utils
import
filesizeformat
from
storage.models
import
Disk
logger
=
logging
.
getLogger
(
__name__
)
...
...
@@ -47,6 +48,17 @@ class NoStoreException(StoreApiException):
class
Store
(
object
):
def
__init__
(
self
,
user
,
default_timeout
=
0.5
):
self
.
store_url
=
settings
.
STORE_URL
if
not
self
.
store_url
:
raise
NoStoreException
if
user
.
is_superuser
and
not
user
.
profile
.
org_id
:
self
.
username
=
'u-admin'
elif
not
user
.
profile
.
org_id
:
raise
NoStoreException
else
:
self
.
username
=
'u-
%
s'
%
user
.
profile
.
org_id
self
.
request_args
=
{
'verify'
:
settings
.
STORE_VERIFY_SSL
}
if
settings
.
STORE_SSL_AUTH
:
self
.
request_args
[
'cert'
]
=
(
settings
.
STORE_CLIENT_CERT
,
...
...
@@ -54,18 +66,15 @@ class Store(object):
if
settings
.
STORE_BASIC_AUTH
:
self
.
request_args
[
'auth'
]
=
(
settings
.
STORE_CLIENT_USER
,
settings
.
STORE_CLIENT_PASSWORD
)
self
.
username
=
"u-
%
d"
%
user
.
pk
self
.
default_timeout
=
default_timeout
self
.
store_url
=
settings
.
STORE_URL
if
not
self
.
store_url
:
raise
NoStoreException
def
_request
(
self
,
url
,
method
=
get
,
timeout
=
None
,
raise_status_code
=
True
,
**
kwargs
):
url
=
urljoin
(
self
.
store_url
,
url
)
if
timeout
is
None
:
timeout
=
self
.
default_timeout
payload
=
json
.
dumps
(
kwargs
)
if
kwargs
else
None
kwargs
[
'USER'
]
=
self
.
username
payload
=
json
.
dumps
(
kwargs
)
try
:
headers
=
{
'content-type'
:
'application/json'
}
response
=
method
(
url
,
data
=
payload
,
headers
=
headers
,
...
...
@@ -83,7 +92,7 @@ class Store(object):
return
response
def
_request_cmd
(
self
,
cmd
,
**
kwargs
):
return
self
.
_request
(
self
.
username
,
post
,
CMD
=
cmd
,
**
kwargs
)
return
self
.
_request
(
"/user/"
,
post
,
CMD
=
cmd
,
**
kwargs
)
def
list
(
self
,
path
,
process
=
True
):
r
=
self
.
_request_cmd
(
"LIST"
,
PATH
=
path
)
...
...
@@ -101,13 +110,22 @@ class Store(object):
else
:
return
result
def
get_disk_images
(
self
,
path
=
'/'
):
images
=
[]
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
export_formats
:
images
.
append
(
os
.
path
.
join
(
path
,
item
[
'NAME'
]))
return
images
def
request_download
(
self
,
path
):
r
=
self
.
_request_cmd
(
"DOWNLOAD"
,
PATH
=
path
,
timeout
=
10
)
return
r
.
json
()[
'LINK'
]
def
request_upload
(
self
,
path
):
r
=
self
.
_request_cmd
(
"UPLOAD"
,
PATH
=
path
)
return
r
.
json
()[
'LINK'
]
r
=
self
.
_request_cmd
(
"UPLOAD"
,
PATH
=
path
)
return
r
.
json
()[
'LINK'
]
def
remove
(
self
,
path
):
self
.
_request_cmd
(
"REMOVE"
,
PATH
=
path
)
...
...
@@ -119,7 +137,7 @@ class Store(object):
self
.
_request_cmd
(
"RENAME"
,
PATH
=
old_path
,
NEW_NAME
=
new_name
)
def
get_quota
(
self
):
# no CMD? :o
r
=
self
.
_request
(
self
.
username
)
r
=
self
.
_request
(
"/user/"
)
quota
=
r
.
json
()
quota
.
update
({
'readable_used'
:
filesizeformat
(
float
(
quota
[
'used'
])),
...
...
@@ -129,17 +147,17 @@ class Store(object):
return
quota
def
set_quota
(
self
,
quota
):
self
.
_request
(
"/quota/"
+
self
.
username
,
post
,
QUOTA
=
quota
)
self
.
_request
(
"/quota/"
,
post
,
QUOTA
=
quota
)
def
user_exist
(
self
):
try
:
self
.
_request
(
self
.
username
)
self
.
_request
(
"/user/"
)
return
True
except
NotOkException
:
return
False
def
create_user
(
self
,
password
,
keys
,
quota
):
self
.
_request
(
"/new/"
+
self
.
username
,
method
=
post
,
self
.
_request
(
"/new/"
,
method
=
post
,
SMBPASSWD
=
password
,
KEYS
=
keys
,
QUOTA
=
quota
)
@staticmethod
...
...
circle/dashboard/templates/dashboard/_disk-list-element.html
View file @
1ab77c1f
...
...
@@ -6,15 +6,29 @@
<span
class=
"operation-wrapper pull-right"
>
{% if d.is_exportable %}
{% if op.export_disk %}
<a
href=
"{{ op.export_disk.get_url }}?disk={{ d.pk }}"
class=
"btn btn-xs btn-{{ op.export_disk.effect }} operation disk-export-btn
{% if op.export_disk.disabled %}disabled{% endif %}"
>
<i
class=
"fa fa-{{ op.export_disk.icon }} fa-fw-12"
></i>
{% trans "Export" %}
</a>
{% endif %}
{% else %}
<small
class=
"btn-xs"
>
{% trans "Not exportable" %}
</small>
{% endif %}
{% if d.is_resizable %}
{% if op.resize_disk %}
<a
href=
"{{ op.resize_disk.get_url }}?disk={{
d.pk
}}"
class=
"btn btn-xs btn-{{ op.resize_disk.effect }} operation disk-resize-btn
<a
href=
"{{ op.resize_disk.get_url }}?disk={{
d.pk
}}"
class=
"btn btn-xs btn-{{ op.resize_disk.effect }} operation disk-resize-btn
{% if op.resize_disk.disabled %}disabled{% endif %}"
>
<i
class=
"fa fa-{{ op.resize_disk.icon }} fa-fw-12"
></i>
{% trans "Resize" %}
</a>
{% else %}
<a
href=
"{% url "
request
.
views
.
request-resize
"
vm_pk=
instance.pk
disk_pk=
d.pk
%}"
class=
"btn btn-xs btn-primary operation"
>
<a
href=
"{% url "
request
.
views
.
request-resize
"
vm_pk=
instance.pk
disk_pk=
d.pk
%}"
class=
"btn btn-xs btn-primary operation"
>
<i
class=
"fa fa-arrows-alt fa-fw-12"
></i>
{% trans "Request resize" %}
</a>
{% endif %}
...
...
@@ -24,8 +38,8 @@
</small>
{% endif %}
{% if op.remove_disk %}
<a
href=
"{{ op.remove_disk.get_url }}?disk={{
d.pk
}}"
class=
"btn btn-xs btn-{{ op.remove_disk.effect
}} operation disk-remove-btn
<a
href=
"{{ op.remove_disk.get_url }}?disk={{
d.pk
}}"
class=
"btn btn-xs btn-{{ op.remove_disk.effect
}} operation disk-remove-btn
{% if op.remove_disk.disabled %}disabled{% endif %}"
>
<i
class=
"fa fa-{{ op.remove_disk.icon }} fa-fw-12"
></i>
{% trans "Remove" %}
</a>
...
...
circle/dashboard/templates/dashboard/node-list.html
View file @
1ab77c1f
...
...
@@ -41,4 +41,23 @@
</div>
<!-- -col-md-12 -->
</div>
<!-- .row -->
<div
class=
"row"
>
<div
class=
"col-md-12"
>
<div
class=
"panel panel-default"
>
<div
class=
"panel-heading"
>
<a
id=
"reschedule-now"
class=
"btn btn-danger pull-right"
href=
"{% url "
dashboard
.
views
.
reschedule
"
%}"
>
<i
class=
"fa fa-magic"
></i>
{% trans "Reschedule now" %}
</a>
<h3
class=
"no-margin"
><i
class=
"fa fa-truck"
></i>
{% trans "Virtual machine auto migration" %}
</h3>
</div>
<div
id=
"node-list-auto-migration-body"
>
<h1>
Crontab
</h1>
<form>
{{ auto_migration_form.as_p }}
</form>
</div>
</div>
</div>
<!-- -col-md-12 -->
</div>
<!-- .row -->
{% endblock %}
circle/dashboard/templates/dashboard/vm-detail/_disk-operations.html
View file @
1ab77c1f
{% load i18n %}
{% for op in ops %}
{% if op.is_disk_operation %}
<a
href=
"{{
op.get_url
}}"
class=
"btn btn-success btn-xs
operation operation-{{
op.op
}}"
>
<i
class=
"fa fa-{{
op.icon
}} fa-fw-12"
></i>
{{op.name
}}
</a>
{% endif %}
{% if op.is_disk_operation %}
<a
href=
"{{
op.get_url
}}"
class=
"btn btn-success btn-xs
operation operation-{{
op.op
}}"
>
<i
class=
"fa fa-{{
op.icon
}} fa-fw-12"
></i>
{{ op.name
}}
</a>
{% endif %}
{% endfor %}
circle/dashboard/templates/dashboard/vm-detail/network.html
View file @
1ab77c1f
...
...
@@ -37,6 +37,9 @@
<dl>
<dt>
{% trans "IPv4 address" %}:
</dt>
<dd>
{{ i.host.ipv4 }}
</dd>
<dt>
{% trans "IPv6 address" %}:
</dt>
<dd>
{{ i.host.ipv6 }}
</dd>
{% if request.user.is_superuser %}
<dt>
{% trans "MAC address" %}:
</dt>
<dd>
{{ i.host.mac }}
</dd>
{% endif %}
<dt>
{% trans "DNS name" %}:
</dt>
<dd>
{{ i.host.get_fqdn }}
</dd>
<dt>
{% trans "Groups" %}:
</dt>
<dd>
...
...
@@ -114,7 +117,9 @@
{% if l.ipv6 %}
<tr>
<td>
{% display_portforward6 l %}
{% autoescape off %}
{% display_portforward6 l %}
{% endautoescape %}
</td>
<td><i
class=
"fa fa-long-arrow-right"
></i></td>
<td>
...
...
circle/dashboard/tests/test_views.py
View file @
1ab77c1f
...
...
@@ -534,7 +534,7 @@ class VmDetailTest(LoginMixin, MockCeleryMixin, TestCase):
with
patch
.
object
(
DeployOperation
,
'async'
)
as
async
:
response
=
c
.
post
(
"/dashboard/vm/create/"
,
{
'name'
:
'vm'
,
'amount'
:
2
,
'amount'
:
1
,
'customized'
:
1
,
'template'
:
1
,
'cpu_priority'
:
10
,
'cpu_count'
:
1
,
'ram_size'
:
128
,
...
...
@@ -543,7 +543,7 @@ class VmDetailTest(LoginMixin, MockCeleryMixin, TestCase):
assert
async
.
called
self
.
assertEqual
(
response
.
status_code
,
302
)
self
.
assertEqual
(
instance_count
+
2
,
Instance
.
objects
.
all
()
.
count
())
self
.
assertEqual
(
instance_count
+
1
,
Instance
.
objects
.
all
()
.
count
())
def
test_unpermitted_description_update
(
self
):
c
=
Client
()
...
...
circle/dashboard/urls.py
View file @
1ab77c1f
...
...
@@ -56,6 +56,7 @@ from .views import (
MessageList
,
MessageDetail
,
MessageCreate
,
MessageDelete
,
EnableTwoFactorView
,
DisableTwoFactorView
,
AclUserGroupAutocomplete
,
AclUserAutocomplete
,
RescheduleView
,
)
from
.views.vm
import
vm_ops
,
vm_mass_ops
from
.views.node
import
node_ops
...
...
@@ -153,6 +154,8 @@ urlpatterns = [
r'(?P<time>[0-9]{1,2}[hdwy])$'
),
NodeListGraphView
.
as_view
(),
name
=
'dashboard.views.node-list-graph'
),
url
(
r'^node/reschedule/$'
,
RescheduleView
.
as_view
(),
name
=
"dashboard.views.reschedule"
),
url
((
r'^template/(?P<pk>\d+)/graph/(?P<metric>[a-z]+)/'
r'(?P<time>[0-9]{1,2}[hdwy])$'
),
TemplateGraphView
.
as_view
(),
...
...
circle/dashboard/views/node.py
View file @
1ab77c1f
...
...
@@ -25,7 +25,7 @@ from django.core.exceptions import PermissionDenied
from
django.core.urlresolvers
import
reverse_lazy
from
django.db.models
import
Count
from
django.forms.models
import
inlineformset_factory
from
django.http
import
HttpResponse
from
django.http
import
HttpResponse
,
JsonResponse
from
django.shortcuts
import
redirect
from
django.template.loader
import
render_to_string
from
django.utils.translation
import
ugettext
as
_
...
...
@@ -37,11 +37,14 @@ from django_tables2 import SingleTableView
from
firewall.models
import
Host
from
vm.models
import
Node
,
NodeActivity
,
Trait
from
vm.tasks.vm_tasks
import
check_queue
from
vm.tasks.local_periodic_tasks
import
auto_migrate
from
..forms
import
TraitForm
,
HostForm
,
NodeForm
from
..forms
import
TraitForm
,
HostForm
,
NodeForm
,
AutoMigrationForm
from
..tables
import
NodeListTable
from
.util
import
AjaxOperationMixin
,
OperationView
,
GraphMixin
,
DeleteViewBase
from
manager.mancelery
import
crontab_parser
def
get_operations
(
instance
,
user
):
ops
=
[]
...
...
@@ -190,6 +193,14 @@ class NodeList(LoginRequiredMixin, GraphMixin, SingleTableView):
table_class
=
NodeListTable
table_pagination
=
False
def
get_crontab
(
self
):
return
crontab_parser
(
settings
.
AUTO_MIGRATION_CRONTAB
)
def
get_context_data
(
self
):
context
=
super
(
NodeList
,
self
)
.
get_context_data
()
context
[
"auto_migration_form"
]
=
AutoMigrationForm
(
self
.
get_crontab
())
return
context
def
get
(
self
,
*
args
,
**
kwargs
):
if
not
self
.
request
.
user
.
has_perm
(
'vm.view_statistics'
):
raise
PermissionDenied
()
...
...
@@ -210,9 +221,20 @@ class NodeList(LoginRequiredMixin, GraphMixin, SingleTableView):
return
super
(
NodeList
,
self
)
.
get
(
*
args
,
**
kwargs
)
def
get_queryset
(
self
):
self
.
wrong_nodes_message
()
return
Node
.
objects
.
annotate
(
number_of_VMs
=
Count
(
'instance_set'
))
.
select_related
(
'host'
)
def
wrong_nodes_message
(
self
):
wrong_nodes
=
[]
for
node
in
Node
.
objects
.
all
():
if
node
.
monitor_info
is
None
:
wrong_nodes
.
append
(
node
.
name
)
message
=
', '
.
join
(
wrong_nodes
)
if
wrong_nodes
:
messages
.
error
(
self
.
request
,
"Can't reach "
+
message
+
" monitor info"
)
class
NodeCreate
(
LoginRequiredMixin
,
SuperuserRequiredMixin
,
TemplateView
):
...
...
@@ -356,3 +378,23 @@ class NodeActivityDetail(LoginRequiredMixin, SuperuserRequiredMixin,
)
.
order_by
(
'-started'
)
.
select_related
())
ctx
[
'icon'
]
=
_get_activity_icon
(
self
.
object
)
return
ctx
class
RescheduleView
(
SuperuserRequiredMixin
,
View
):
def
get
(
self
,
*
args
,
**
kwargs
):
try
:
auto_migrate
.
apply_async
(
queue
=
'localhost.man.slow'
)
except
Exception
as
e
:
msg
=
str
(
e
)
result
=
'error'
else
:
result
=
'ok'
msg
=
_
(
'Reschedule has started.'
)
if
self
.
request
.
is_ajax
():
return
JsonResponse
({
'result'
:
result
,
'message'
:
msg
})
else
:
if
result
==
'ok'
:
messages
.
success
(
self
.
request
,
msg
)
else
:
messages
.
error
(
self
.
request
,
msg
)
return
redirect
(
'dashboard.views.node-list'
)
circle/dashboard/views/store.py
View file @
1ab77c1f
...
...
@@ -35,7 +35,8 @@ from django.views.generic import TemplateView
from
braces.views
import
LoginRequiredMixin
from
..store_api
import
Store
,
NoStoreException
,
NotOkException
from
..store_api
import
(
Store
,
NoStoreException
,
NotOkException
)
logger
=
logging
.
getLogger
(
__name__
)
...
...
circle/dashboard/views/util.py
View file @
1ab77c1f
...
...
@@ -682,7 +682,7 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View):
messages
.
error
(
request
,
_
(
'This token is invalid or has expired.'
))
raise
PermissionDenied
()
return
render
(
request
,
self
.
template
,
dictionary
=
{
'instance'
:
instance
,
'key'
:
key
})
{
'instance'
:
instance
,
'key'
:
key
})
def
change_owner
(
self
,
instance
,
new_owner
):
instance
.
owner
=
new_owner
...
...
circle/dashboard/views/vm.py
View file @
1ab77c1f
...
...
@@ -61,9 +61,10 @@ from .util import (
)
from
..forms
import
(
AclUserOrGroupAddForm
,
VmResourcesForm
,
TraitsForm
,
RawDataForm
,
VmAddInterfaceForm
,
VmCreateDiskForm
,
VmDownloadDiskForm
,
VmSaveForm
,
VmAddInterfaceForm
,
VmCreateDiskForm
,
VmDownloadDiskForm
,
VmImportDiskForm
,
VmSaveForm
,
VmRenewForm
,
VmStateChangeForm
,
VmListSearchForm
,
VmCustomizeForm
,
VmDiskResizeForm
,
RedeployForm
,
VmDiskRemoveForm
,
VmDisk
ExportForm
,
VmDisk
ResizeForm
,
RedeployForm
,
VmDiskRemoveForm
,
VmMigrateForm
,
VmDeployForm
,
VmPortRemoveForm
,
VmPortAddForm
,
VmRemoveInterfaceForm
,
...
...
@@ -166,8 +167,8 @@ class VmDetailView(GraphMixin, CheckedDetailView):
# resources forms
can_edit
=
(
instance
.
has_level
(
user
,
"owner"
)
and
self
.
request
.
user
.
has_perm
(
"vm.change_resources"
))
instance
.
has_level
(
user
,
"owner"
)
and
self
.
request
.
user
.
has_perm
(
"vm.change_resources"
))
context
[
'resources_form'
]
=
VmResourcesForm
(
can_edit
=
can_edit
,
instance
=
instance
)
...
...
@@ -269,7 +270,7 @@ class VmDetailView(GraphMixin, CheckedDetailView):
return
JsonResponse
({
'message'
:
message
})
else
:
return
redirect
(
reverse_lazy
(
"dashboard.views.detail"
,
kwargs
=
{
'pk'
:
self
.
object
.
pk
}))
kwargs
=
{
'pk'
:
self
.
object
.
pk
}))
def
__abort_operation
(
self
,
request
):
self
.
object
=
self
.
get_object
()
...
...
@@ -301,7 +302,6 @@ class VmRawDataUpdate(SuperuserRequiredMixin, UpdateView):
class
VmOperationView
(
AjaxOperationMixin
,
OperationView
):
model
=
Instance
context_object_name
=
'instance'
# much simpler to mock object
...
...
@@ -350,7 +350,6 @@ class VmRemoveInterfaceView(FormOperationMixin, VmOperationView):
class
VmAddInterfaceView
(
FormOperationMixin
,
VmOperationView
):
op
=
'add_interface'
form_class
=
VmAddInterfaceForm
show_in_toolbar
=
False
...
...
@@ -391,7 +390,6 @@ class VmDiskModifyView(FormOperationMixin, VmOperationView):
class
VmCreateDiskView
(
FormOperationMixin
,
VmOperationView
):
op
=
'create_disk'
form_class
=
VmCreateDiskForm
show_in_toolbar
=
False
...
...
@@ -408,8 +406,22 @@ class VmCreateDiskView(FormOperationMixin, VmOperationView):
return
val
class
VmDownloadDiskView
(
FormOperationMixin
,
VmOperationView
):
class
VmImportDiskView
(
FormOperationMixin
,
VmOperationView
):
op
=
'import_disk'
form_class
=
VmImportDiskForm
show_in_toolbar
=
False
icon
=
'upload'
effect
=
"success"
is_disk_operation
=
True
with_reload
=
True
def
get_form_kwargs
(
self
):
val
=
super
(
VmImportDiskView
,
self
)
.
get_form_kwargs
()
val
.
update
({
'user'
:
self
.
request
.
user
})
return
val
class
VmDownloadDiskView
(
FormOperationMixin
,
VmOperationView
):
op
=
'download_disk'
form_class
=
VmDownloadDiskForm
show_in_toolbar
=
False
...
...
@@ -420,7 +432,6 @@ class VmDownloadDiskView(FormOperationMixin, VmOperationView):
class
VmMigrateView
(
FormOperationMixin
,
VmOperationView
):
op
=
'migrate'
icon
=
'truck'
effect
=
'info'
...
...
@@ -449,8 +460,7 @@ class VmMigrateView(FormOperationMixin, VmOperationView):
if
isinstance
(
inst
,
Instance
):
nodes_w_traits
=
[
n
.
pk
for
n
in
Node
.
objects
.
filter
(
enabled
=
True
)
if
n
.
online
and
has_traits
(
inst
.
req_traits
.
all
(),
n
)
if
n
.
online
and
has_traits
(
inst
.
req_traits
.
all
(),
n
)
]
ctx
[
'nodes_w_traits'
]
=
nodes_w_traits
...
...
@@ -458,7 +468,6 @@ class VmMigrateView(FormOperationMixin, VmOperationView):
class
VmPortRemoveView
(
FormOperationMixin
,
VmOperationView
):
template_name
=
'dashboard/_vm-remove-port.html'
op
=
'remove_port'
show_in_toolbar
=
False
...
...
@@ -487,7 +496,6 @@ class VmPortRemoveView(FormOperationMixin, VmOperationView):