Commit 7a501d0a by Őry Máté

Update to master

Merge remote-tracking branch 'origin/master' into feature-node-traits

Conflicts:
	circle/dashboard/forms.py
	circle/dashboard/urls.py
	circle/dashboard/views.py
parents 8a1af341 66a9ff8a
============= ============
cirecle-cloud circle-cloud
============= ============
This is the Django based controller and web portal of the CIRCLE Cloud. This is the Django based controller and web portal of the CIRCLE Cloud.
\ No newline at end of file
...@@ -151,9 +151,9 @@ class AclBase(Model): ...@@ -151,9 +151,9 @@ class AclBase(Model):
return True return True
return False return False
def get_users_with_level(self): def get_users_with_level(self, **kwargs):
logger.debug('%s.get_users_with_level() called', unicode(self)) logger.debug('%s.get_users_with_level() called', unicode(self))
object_levels = (self.object_level_set.select_related( object_levels = (self.object_level_set.filter(**kwargs).select_related(
'users', 'level').all()) 'users', 'level').all())
users = [] users = []
for object_level in object_levels: for object_level in object_levels:
......
...@@ -339,7 +339,6 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE': ...@@ -339,7 +339,6 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
'djangosaml2.backends.Saml2Backend', 'djangosaml2.backends.Saml2Backend',
) )
LOGIN_URL = '/saml2/login/'
remote_metadata = join(SITE_ROOT, 'remote_metadata.xml') remote_metadata = join(SITE_ROOT, 'remote_metadata.xml')
if not isfile(remote_metadata): if not isfile(remote_metadata):
...@@ -388,6 +387,8 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE': ...@@ -388,6 +387,8 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
'DJANGO_SAML_GROUP_OWNER_ATTRIBUTES', '').split(',') 'DJANGO_SAML_GROUP_OWNER_ATTRIBUTES', '').split(',')
SAML_CREATE_UNKNOWN_USER = True SAML_CREATE_UNKNOWN_USER = True
if get_env_variable('DJANGO_SAML_ORG_ID_ATTRIBUTE', False) != False: if get_env_variable('DJANGO_SAML_ORG_ID_ATTRIBUTE', False) is not False:
SAML_ORG_ID_ATTRIBUTE = get_env_variable( SAML_ORG_ID_ATTRIBUTE = get_env_variable(
'DJANGO_SAML_ORG_ID_ATTRIBUTE') 'DJANGO_SAML_ORG_ID_ATTRIBUTE')
LOGIN_REDIRECT_URL = "/"
...@@ -29,3 +29,12 @@ CACHES = { ...@@ -29,3 +29,12 @@ CACHES = {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache' 'BACKEND': 'django.core.cache.backends.dummy.DummyCache'
} }
} }
LOGGING['loggers']['djangosaml2'] = {'handlers': ['console'],
'level': 'CRITICAL'}
LOGGING['handlers']['console'] = {'level': 'WARNING',
'class': 'logging.StreamHandler',
'formatter': 'simple'}
for i in LOCAL_APPS:
LOGGING['loggers'][i] = {'handlers': ['console'], 'level': 'CRITICAL'}
...@@ -6,6 +6,8 @@ from django.shortcuts import redirect ...@@ -6,6 +6,8 @@ from django.shortcuts import redirect
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from circle.settings.base import get_env_variable from circle.settings.base import get_env_variable
from dashboard.views import circle_login
from dashboard.forms import CirclePasswordResetForm, CircleSetPasswordForm
admin.autodiscover() admin.autodiscover()
...@@ -23,6 +25,19 @@ urlpatterns = patterns( ...@@ -23,6 +25,19 @@ urlpatterns = patterns(
url(r'^admin/', include(admin.site.urls)), url(r'^admin/', include(admin.site.urls)),
url(r'^network/', include('network.urls')), url(r'^network/', include('network.urls')),
url(r'^dashboard/', include('dashboard.urls')), url(r'^dashboard/', include('dashboard.urls')),
url((r'^accounts/reset/(?P<uidb36>[0-9A-Za-z]{1,13})-'
'(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$'),
'django.contrib.auth.views.password_reset_confirm',
{'set_password_form': CircleSetPasswordForm},
name='accounts.password_reset_confirm'
),
url(r'^accounts/password/reset/$', ("django.contrib.auth.views."
"password_reset"),
{'password_reset_form': CirclePasswordResetForm},
name="accounts.password-reset",
),
url(r'^accounts/login/?$', circle_login, name="accounts.login"),
url(r'^accounts/', include('django.contrib.auth.urls')), url(r'^accounts/', include('django.contrib.auth.urls')),
) )
......
...@@ -14,6 +14,10 @@ from model_utils.models import TimeStampedModel ...@@ -14,6 +14,10 @@ from model_utils.models import TimeStampedModel
logger = getLogger(__name__) logger = getLogger(__name__)
class WorkerNotFound(Exception):
pass
def activitycontextimpl(act, on_abort=None, on_commit=None): def activitycontextimpl(act, on_abort=None, on_commit=None):
try: try:
yield act yield act
...@@ -60,7 +64,8 @@ class ActivityModel(TimeStampedModel): ...@@ -60,7 +64,8 @@ class ActivityModel(TimeStampedModel):
if not self.finished: if not self.finished:
self.finished = timezone.now() self.finished = timezone.now()
self.succeeded = succeeded self.succeeded = succeeded
self.result = result if result is not None:
self.result = result
if event_handler is not None: if event_handler is not None:
event_handler(self) event_handler(self)
self.save() self.save()
......
[
{
"pk": 1,
"model": "firewall.vlan",
"fields": {
"comment": "",
"ipv6_template": "2001:7:2:4031:%(b)d:%(c)d:%(d)d:0",
"domain": 1,
"dhcp_pool": "",
"managed": true,
"name": "pub",
"vid": 3066,
"created_at": "2014-02-19T17:00:17.358Z",
"modified_at": "2014-02-19T17:00:17.358Z",
"owner": null,
"snat_ip": null,
"snat_to": [],
"network6": null,
"network4": "10.7.0.93/16",
"reverse_domain": "%(d)d.%(c)d.%(b)d.%(a)d.in-addr.arpa",
"network_type": "public",
"description": ""
}
},
{
"pk": 1,
"model": "firewall.host",
"fields": {
"comment": "",
"vlan": 1,
"reverse": "",
"created_at": "2014-02-19T17:03:45.365Z",
"hostname": "devenv",
"modified_at": "2014-02-24T15:55:01.412Z",
"location": "",
"pub_ipv4": null,
"mac": "11:22:33:44:55:66",
"shared_ip": false,
"ipv4": "10.7.0.96",
"groups": [],
"ipv6": null,
"owner": 1,
"description": ""
}
},
{
"pk": 1,
"model": "firewall.domain",
"fields": {
"description": "",
"created_at": "2014-02-19T17:00:08.819Z",
"modified_at": "2014-02-19T17:00:08.819Z",
"ttl": 600,
"owner": 1,
"name": "test.ik.bme.hu"
}
},
{
"pk": 1,
"model": "vm.node",
"fields": {
"name": "devenv",
"created": "2014-02-19T17:03:45.322Z",
"overcommit": 1.0,
"enabled": true,
"modified": "2014-02-19T21:11:34.671Z",
"priority": 1,
"traits": [],
"host": 1
}
}
]
...@@ -1372,6 +1372,35 @@ ...@@ -1372,6 +1372,35 @@
} }
}, },
{ {
"pk": 12,
"model": "vm.instance",
"fields": {
"destroyed": null,
"disks": [],
"boot_menu": false,
"owner": 1,
"time_of_delete": null,
"max_ram_size": 200,
"pw": "ads",
"time_of_suspend": null,
"ram_size": 200,
"priority": 4,
"active_since": null,
"template": null,
"access_method": "nx",
"lease": 1,
"node": null,
"description": "",
"arch": "x86_64",
"name": "vanneve",
"created": "2013-09-16T09:05:59.991Z",
"raw_data": "",
"vnc_port": 1235,
"num_cores": 2,
"modified": "2013-10-14T07:27:38.192Z"
}
},
{
"pk": 1, "pk": 1,
"model": "firewall.domain", "model": "firewall.domain",
"fields": { "fields": {
...@@ -1462,7 +1491,6 @@ ...@@ -1462,7 +1491,6 @@
"modified": "2014-01-24T00:58:19.654Z", "modified": "2014-01-24T00:58:19.654Z",
"system": "", "system": "",
"priority": 20, "priority": 20,
"state": "READY",
"access_method": "ssh", "access_method": "ssh",
"raw_data": "", "raw_data": "",
"arch": "x86_64", "arch": "x86_64",
......
from datetime import timedelta from datetime import timedelta
import uuid
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.forms import (
AuthenticationForm, PasswordResetForm, SetPasswordForm,
)
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import ( from crispy_forms.layout import (
...@@ -18,55 +20,60 @@ from sizefield.widgets import FileSizeWidget ...@@ -18,55 +20,60 @@ from sizefield.widgets import FileSizeWidget
from firewall.models import Vlan, Host from firewall.models import Vlan, Host
from storage.models import Disk, DataStore from storage.models import Disk, DataStore
from vm.models import ( from vm.models import (
InstanceTemplate, Lease, InterfaceTemplate, Node, Trait, InstanceTemplate, Lease, InterfaceTemplate, Node, Trait, Instance
) )
VLANS = Vlan.objects.all() VLANS = Vlan.objects.all()
DISKS = Disk.objects.exclude(type="qcow2-snap") DISKS = Disk.objects.exclude(type="qcow2-snap")
class VmCreateForm(forms.Form): class VmCustomizeForm(forms.Form):
template = forms.ModelChoiceField(queryset=InstanceTemplate.objects.all(), name = forms.CharField()
empty_label="Select pls")
cpu_priority = forms.IntegerField() cpu_priority = forms.IntegerField()
cpu_count = forms.IntegerField() cpu_count = forms.IntegerField()
ram_size = forms.IntegerField() ram_size = forms.IntegerField()
disks = forms.ModelMultipleChoiceField( disks = forms.ModelMultipleChoiceField(
queryset=DISKS, queryset=None, required=True)
required=False
)
networks = forms.ModelMultipleChoiceField( networks = forms.ModelMultipleChoiceField(
queryset=VLANS, required=False) queryset=None, required=False)
template = forms.CharField()
customized = forms.CharField() # dummy flag field
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(VmCreateForm, self).__init__(*args, **kwargs) self.user = kwargs.pop("user", None)
self.template = kwargs.pop("template", None)
super(VmCustomizeForm, self).__init__(*args, **kwargs)
# set displayed disk and network list
self.fields['disks'].queryset = Disk.get_objects_with_level(
'user', self.user).exclude(type="qcow2-snap")
self.fields['networks'].queryset = Vlan.get_objects_with_level(
'user', self.user)
# set initial for disk and network list
self.initial['disks'] = self.template.disks.all()
self.initial['networks'] = InterfaceTemplate.objects.filter(
template=self.template).values_list("vlan", flat=True)
# set initial for resources
self.initial['cpu_priority'] = self.template.priority
self.initial['cpu_count'] = self.template.num_cores
self.initial['ram_size'] = self.template.ram_size
# initial name and template pk
self.initial['name'] = self.template.name
self.initial['template'] = self.template.pk
self.initial['customized'] = self.template.pk
self.helper = FormHelper(self) self.helper = FormHelper(self)
self.helper.form_show_labels = False self.helper.form_show_labels = False
self.helper.layout = Layout( self.helper.layout = Layout(
Div( Field("template", type="hidden"),
Div( Field("customized", type="hidden"),
Field('template', id="vm-create-template-select",
css_class="select form-control"),
css_class="col-sm-10",
),
css_class="row",
),
Div( # buttons Div( # buttons
Div( Div(
AnyTag(
"a",
HTML("%s " % _("Advanced")),
AnyTag(
"i",
css_class="vm-create-advanced-icon icon-caret-down"
),
css_class="btn btn-info vm-create-advanced-btn",
),
css_class="col-sm-5",
),
Div(
AnyTag( # tip: don't try to use Button class AnyTag( # tip: don't try to use Button class
"button", "button",
AnyTag( AnyTag(
...@@ -74,193 +81,200 @@ class VmCreateForm(forms.Form): ...@@ -74,193 +81,200 @@ class VmCreateForm(forms.Form):
css_class="icon-play" css_class="icon-play"
), ),
HTML(" Start"), HTML(" Start"),
css_id="vm-create-submit", css_id="vm-create-customized-start",
css_class="btn btn-success", css_class="btn btn-success",
), ),
css_class="col-sm-5 text-right", css_class="col-sm-11 text-right",
), ),
css_class="row", css_class="row",
), ),
Div( # vm-create-advanced Div(
Div( Div(
Div( Field("name"),
AnyTag( css_class="col-sm-5",
'h2',
HTML(_("Resources")),
),
css_class="col-sm-12",
),
css_class="row",
), ),
Div( # cpu priority css_class="row",
Div( ),
HTML('<label for="vm-cpu-priority-slider">' Div(
'<i class="icon-trophy"></i> CPU priority' Div(
'</label>'), AnyTag(
css_class="col-sm-3" 'h2',
), HTML(_("Resources")),
Div(
Field('cpu_priority', id="vm-cpu-priority-slider",
css_class="vm-slider",
data_slider_min="0", data_slider_max="100",
data_slider_step="1", data_slider_value="20",
data_slider_handle="square",
data_slider_tooltip="hide"),
css_class="col-sm-9"
), ),
css_class="row" css_class="col-sm-12",
), ),
Div( # cpu count css_class="row",
Div( ),
HTML('<label for="cpu-count-slider">' Div( # cpu priority
'<i class="icon-cogs"></i> CPU count' Div(
'</label>'), HTML('<label for="vm-cpu-priority-slider">'
css_class="col-sm-3" '<i class="icon-trophy"></i> CPU priority'
), '</label>'),
Div( css_class="col-sm-3"
Field('cpu_count', id="vm-cpu-count-slider",
css_class="vm-slider",
data_slider_min="1", data_slider_max="8",
data_slider_step="1", data_slider_value="2",
data_slider_handle="square",
data_slider_tooltip="hide"),
css_class="col-sm-9"
),
css_class="row"
), ),
Div( # ram size Div(
Div( Field('cpu_priority', id="vm-cpu-priority-slider",
HTML('<label for="ram-slider">' css_class="vm-slider",
'<i class="icon-ticket"></i> RAM amount' data_slider_min="0", data_slider_max="100",
'</label>'), data_slider_step="1",
css_class="col-sm-3" data_slider_value=self.template.priority,
), data_slider_handle="square",
Div( data_slider_tooltip="hide"),
Field('ram_size', id="vm-ram-size-slider", css_class="col-sm-9"
css_class="vm-slider", ),
data_slider_min="128", data_slider_max="4096", css_class="row"
data_slider_step="128", data_slider_value="512", ),
data_slider_handle="square", Div( # cpu count
data_slider_tooltip="hide"), Div(
css_class="col-sm-9" HTML('<label for="cpu-count-slider">'
'<i class="icon-cogs"></i> CPU count'
'</label>'),
css_class="col-sm-3"
),
Div(
Field('cpu_count', id="vm-cpu-count-slider",
css_class="vm-slider",
data_slider_min="1", data_slider_max="8",
data_slider_step="1",
data_slider_value=self.template.num_cores,
data_slider_handle="square",
data_slider_tooltip="hide"),
css_class="col-sm-9"
),
css_class="row"
),
Div( # ram size
Div(
HTML('<label for="ram-slider">'
'<i class="icon-ticket"></i> RAM amount'
'</label>'),
css_class="col-sm-3"
),
Div(
Field('ram_size', id="vm-ram-size-slider",
css_class="vm-slider",
data_slider_min="128", data_slider_max="4096",
data_slider_step="128",
data_slider_value=self.template.ram_size,
data_slider_handle="square",
data_slider_tooltip="hide"),
css_class="col-sm-9"
),
css_class="row"
),
Div( # disks
Div(
AnyTag(
"h2",
HTML("Disks")
), ),
css_class="row" css_class="col-sm-4",
), ),
Div( # disks Div(
Div( Div(
AnyTag( Field("disks", css_class="form-control",
"h2", id="vm-create-disk-add-form"),
HTML("Disks") css_class="js-hidden",
), style="padding-top: 15px; max-width: 450px;",
css_class="col-sm-4",
), ),
Div( Div(
Div( AnyTag(
Field("disks", css_class="form-control", "h3",
id="vm-create-disk-add-form"), HTML(_("No disks are added!")),
css_class="js-hidden", css_id="vm-create-disk-list",
style="padding-top: 15px; max-width: 450px;",
), ),
Div( AnyTag(
AnyTag( "h3",
"h3", Div(
HTML(_("No disks are added!")), AnyTag(
css_id="vm-create-disk-list", "select",
), css_class="form-control",
AnyTag( css_id="vm-create-disk-add-select",
"h3", ),
Div( Div(
AnyTag( AnyTag(
"select", "a",
css_class="form-control",
css_id="vm-create-disk-add-select",
),
Div(
AnyTag( AnyTag(
"a", "i",
AnyTag( css_class="icon-plus-sign",
"i",
css_class="icon-plus-sign",
),
href="#",
css_id="vm-create-disk-add-button",
css_class="btn btn-success",
), ),
css_class="input-group-btn" href="#",
css_id="vm-create-disk-add-button",
css_class="btn btn-success",
), ),
css_class="input-group", css_class="input-group-btn"
style="max-width: 330px;",
), ),
css_id="vm-create-disk-add", css_class="input-group",
style="max-width: 330px;",
), ),
css_class="no-js-hidden", css_id="vm-create-disk-add",
), ),
css_class="col-sm-8", css_class="no-js-hidden",
style="padding-top: 3px;",
), ),
css_class="row", css_class="col-sm-8",
), # end of disks style="padding-top: 3px;",
Div( # network ),
Div( css_class="row",
AnyTag( ), # end of disks
"h2", Div( # network
HTML(_("Network")), Div(
AnyTag(
"h2",
HTML(_("Network")),
),
css_class="col-sm-4",
),
Div(
Div( # js-hidden
Field(
"networks",
css_class="form-control",
id="vm-create-network-add-vlan",
), ),
css_class="col-sm-4", css_class="js-hidden",
style="padding-top: 15px; max-width: 450px;",
), ),
Div( Div( # no-js-hidden
Div( # js-hidden AnyTag(
Field( "h3",
"networks", HTML(_("Not added to any network!")),
css_class="form-control", css_id="vm-create-network-list",
id="vm-create-network-add-vlan",
),
css_class="js-hidden",
style="padding-top: 15px; max-width: 450px;",
), ),
Div( # no-js-hidden AnyTag(
AnyTag( "h3",
"h3", Div(
HTML(_("Not added to any network!")), AnyTag(
css_id="vm-create-network-list", "select",
), css_class=("form-control "
AnyTag( "font-awesome-font"),
"h3", css_id="vm-create-network-add-select",
),
Div( Div(
AnyTag( AnyTag(
"select", "a",
css_class=("form-control "
"font-awesome-font"),
css_id="vm-create-network-add-select",
),
Div(
AnyTag( AnyTag(
"a", "i",
AnyTag( css_class="icon-plus-sign",
"i",
css_class="icon-plus-sign",
),
css_id=("vm-create-network-add"
"-button"),
css_class="btn btn-success",
), ),
css_class="input-group-btn", css_id=("vm-create-network-add"
"-button"),
css_class="btn btn-success",
), ),
css_class="input-group", css_class="input-group-btn",
style="max-width: 330px;",
), ),
css_class="vm-create-network-add" css_class="input-group",
style="max-width: 330px;",
), ),
css_class="no-js-hidden", css_class="vm-create-network-add"
), ),
css_class="col-sm-8", css_class="no-js-hidden",
style="padding-top: 3px;",
), ),
css_class="row" css_class="col-sm-8",
), # end of network style="padding-top: 3px;",
css_class="vm-create-advanced" ),
), css_class="row"
), # end of network
) )
...@@ -448,7 +462,7 @@ class TemplateForm(forms.ModelForm): ...@@ -448,7 +462,7 @@ class TemplateForm(forms.ModelForm):
template = InstanceTemplate.objects.get(pk=parent) template = InstanceTemplate.objects.get(pk=parent)
parent = template.__dict__ parent = template.__dict__
fields = ["system", "name", "num_cores", "boot_menu", "ram_size", fields = ["system", "name", "num_cores", "boot_menu", "ram_size",
"priority", "state", "access_method", "raw_data", "priority", "access_method", "raw_data",
"arch", "description"] "arch", "description"]
for f in fields: for f in fields:
self.initial[f] = parent[f] self.initial[f] = parent[f]
...@@ -474,6 +488,15 @@ class TemplateForm(forms.ModelForm): ...@@ -474,6 +488,15 @@ class TemplateForm(forms.ModelForm):
return User.objects.get(pk=self.instance.owner.pk) return User.objects.get(pk=self.instance.owner.pk)
return self.user return self.user
def clean_raw_data(self):
# if raw_data has changed and the user is not superuser
if "raw_data" in self.changed_data and not self.user.is_superuser:
old_raw_data = InstanceTemplate.objects.get(
pk=self.instance.pk).raw_data
return old_raw_data
else:
return self.cleaned_data['raw_data']
def save(self, commit=True): def save(self, commit=True):
data = self.cleaned_data data = self.cleaned_data
self.instance.max_ram_size = data.get('ram_size') self.instance.max_ram_size = data.get('ram_size')
...@@ -499,6 +522,9 @@ class TemplateForm(forms.ModelForm): ...@@ -499,6 +522,9 @@ class TemplateForm(forms.ModelForm):
@property @property
def helper(self): def helper(self):
kwargs_raw_data = {}
if not self.user.is_superuser:
kwargs_raw_data['readonly'] = None
helper = FormHelper() helper = FormHelper()
helper.layout = Layout( helper.layout = Layout(
Field("name"), Field("name"),
...@@ -550,7 +576,7 @@ class TemplateForm(forms.ModelForm): ...@@ -550,7 +576,7 @@ class TemplateForm(forms.ModelForm):
"stuff", "stuff",
Field('access_method'), Field('access_method'),
Field('boot_menu'), Field('boot_menu'),
Field('raw_data'), Field('raw_data', **kwargs_raw_data),
Field('req_traits'), Field('req_traits'),
Field('description'), Field('description'),
Field("parent", type="hidden"), Field("parent", type="hidden"),
...@@ -715,27 +741,61 @@ class LeaseForm(forms.ModelForm): ...@@ -715,27 +741,61 @@ class LeaseForm(forms.ModelForm):
class DiskAddForm(forms.Form): class DiskAddForm(forms.Form):
name = forms.CharField() name = forms.CharField()
size = forms.CharField(widget=FileSizeWidget) size = forms.CharField(widget=FileSizeWidget, required=False)
url = forms.CharField(required=False)
is_template = forms.CharField()
object_pk = forms.CharField()
def __init__(self, *args, **kwargs):
self.is_template = kwargs.pop("is_template")
self.object_pk = kwargs.pop("object_pk")
self.user = kwargs.pop("user")
super(DiskAddForm, self).__init__(*args, **kwargs)
self.initial['is_template'] = 1 if self.is_template is True else 0
self.initial['object_pk'] = self.object_pk
def clean_size(self): def clean_size(self):
size_in_bytes = self.cleaned_data.get("size") size_in_bytes = self.cleaned_data.get("size")
if not size_in_bytes.isdigit(): if not size_in_bytes.isdigit() and len(size_in_bytes) > 0:
raise forms.ValidationError(_("Invalid format, you can use " raise forms.ValidationError(_("Invalid format, you can use "
" GB or MB!")) " GB or MB!"))
return size_in_bytes return size_in_bytes
def save(self, vm, commit=True): def clean(self):
cleaned_data = self.cleaned_data
size = cleaned_data.get("size")
url = cleaned_data.get("url")
if not size and not url:
msg = _("You have to either specify size or URL")
self._errors[_("Global")] = self.error_class([msg])
return cleaned_data
def save(self, commit=True):
data = self.cleaned_data data = self.cleaned_data
d = Disk(
name=data['name'], if self.is_template:
filename=str(uuid.uuid4()), inst = InstanceTemplate.objects.get(pk=self.object_pk)
datastore=DataStore.objects.all()[0], else:
type="qcow2-norm", inst = Instance.objects.get(pk=self.object_pk)
size=data['size'],
dev_num="a", if data['size']:
) kwargs = {
d.save() 'name': data['name'],
vm.disks.add(d) 'type': "qcow2-norm",
'datastore': DataStore.objects.all()[0],
'size': data['size'],
}
d = Disk.create_empty(instance=inst, user=self.user, **kwargs)
else:
kwargs = {
'name': data['name'],
'url': data['url'],
}
Disk.create_from_url_async(instance=inst, user=self.user,
**kwargs)
d = None
return d return d
@property @property
...@@ -743,15 +803,114 @@ class DiskAddForm(forms.Form): ...@@ -743,15 +803,114 @@ class DiskAddForm(forms.Form):
helper = FormHelper() helper = FormHelper()
helper.form_show_labels = False helper.form_show_labels = False
helper.layout = Layout( helper.layout = Layout(
Field("is_template", type="hidden"),
Field("object_pk", type="hidden"),
Field("name", placeholder=_("Name")), Field("name", placeholder=_("Name")),
Field("size", placeholder=_("Disk size (for example: 20GB, " Field("size", placeholder=_("Disk size (for example: 20GB, "
"1500MB)")), "1500MB)")),
Field("url", placeholder=_("URL to an ISO image")),
AnyTag(
"div",
HTML(
_("Either specify the size for an empty disk or a URL "
"to an ISO image!")
),
css_class="alert alert-info",
style="padding: 5px; text-align: justify;",
),
) )
helper.add_input(Submit("submit", "Create new disk", helper.add_input(Submit("submit", _("Add"),
css_class="btn btn-success")) css_class="btn btn-success"))
return helper return helper
class CircleAuthenticationForm(AuthenticationForm):
# fields: username, password
@property
def helper(self):
helper = FormHelper()
helper.form_show_labels = False
helper.layout = Layout(
AnyTag(
"div",
AnyTag(
"span",
AnyTag(
"i",
css_class="icon-user",
),
css_class="input-group-addon",
),
Field("username", placeholder=_("Username"),
css_class="form-control"),
css_class="input-group",
),
AnyTag(
"div",
AnyTag(
"span",
AnyTag(
"i",
css_class="icon-lock",
),
css_class="input-group-addon",
),
Field("password", placeholder=_("Password"),
css_class="form-control"),
css_class="input-group",
),
)
helper.add_input(Submit("submit", _("Sign in"),
css_class="btn btn-success"))
return helper
class CirclePasswordResetForm(PasswordResetForm):
# fields: email
@property
def helper(self):
helper = FormHelper()
helper.form_show_labels = False
helper.layout = Layout(
AnyTag(
"div",
AnyTag(
"span",
AnyTag(
"i",
css_class="icon-envelope",
),
css_class="input-group-addon",
),
Field("email", placeholder=_("Email address"),
css_class="form-control"),
Div(
AnyTag(
"button",
HTML(_("Reset password")),
css_class="btn btn-success",
),
css_class="input-group-btn",
),
css_class="input-group",
),
)
return helper
class CircleSetPasswordForm(SetPasswordForm):
@property
def helper(self):
helper = FormHelper()
helper.add_input(Submit("submit", _("Change password"),
css_class="btn btn-success change-password",
css_id="submit-password-button"))
return helper
class LinkButton(BaseInput): class LinkButton(BaseInput):
""" """
......
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'Notification'
db.create_table(u'dashboard_notification', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)),
('modified', self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now)),
('status', self.gf('model_utils.fields.StatusField')(default='new', max_length=100, no_check_for_status=True)),
('to', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
('subject', self.gf('django.db.models.fields.CharField')(max_length=128)),
('message', self.gf('django.db.models.fields.TextField')()),
))
db.send_create_signal(u'dashboard', ['Notification'])
def backwards(self, orm):
# Deleting model 'Notification'
db.delete_table(u'dashboard_notification')
models = {
u'acl.level': {
'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Level'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'weight': ('django.db.models.fields.IntegerField', [], {'null': 'True'})
},
u'acl.objectlevel': {
'Meta': {'unique_together': "(('content_type', 'object_id', 'level'),)", 'object_name': 'ObjectLevel'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'level': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['acl.Level']"}),
'object_id': ('django.db.models.fields.IntegerField', [], {}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'symmetrical': 'False'})
},
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'dashboard.favourite': {
'Meta': {'object_name': 'Favourite'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Instance']"}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
},
u'dashboard.groupprofile': {
'Meta': {'object_name': 'GroupProfile'},
'description': ('django.db.models.fields.TextField', [], {}),
'group': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.Group']", 'unique': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'org_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
},
u'dashboard.notification': {
'Meta': {'ordering': "['-created']", 'object_name': 'Notification'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'message': ('django.db.models.fields.TextField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'status': ('model_utils.fields.StatusField', [], {'default': "'new'", 'max_length': '100', u'no_check_for_status': 'True'}),
'subject': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'to': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
},
u'dashboard.profile': {
'Meta': {'object_name': 'Profile'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance_limit': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
'org_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'preferred_language': ('django.db.models.fields.CharField', [], {'default': "'en-us'", 'max_length': '32'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
},
u'firewall.domain': {
'Meta': {'object_name': 'Domain'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'ttl': ('django.db.models.fields.IntegerField', [], {'default': '600'})
},
u'firewall.group': {
'Meta': {'object_name': 'Group'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'firewall.host': {
'Meta': {'unique_together': "(('hostname', 'vlan'),)", 'object_name': 'Host'},
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['firewall.Group']", 'null': 'True', 'blank': 'True'}),
'hostname': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ipv4': ('firewall.fields.IPAddressField', [], {'unique': 'True', 'max_length': '100'}),
'ipv6': ('firewall.fields.IPAddressField', [], {'max_length': '100', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'location': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'mac': ('firewall.fields.MACAddressField', [], {'unique': 'True', 'max_length': '17'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'pub_ipv4': ('firewall.fields.IPAddressField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
'reverse': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
'shared_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'vlan': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Vlan']"})
},
u'firewall.vlan': {
'Meta': {'object_name': 'Vlan'},
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'dhcp_pool': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Domain']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ipv6_template': ('django.db.models.fields.TextField', [], {'default': "'2001:738:2001:4031:%(b)d:%(c)d:%(d)d:0'"}),
'managed': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'network4': ('firewall.fields.IPNetworkField', [], {'max_length': '100'}),
'network6': ('firewall.fields.IPNetworkField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
'network_type': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'reverse_domain': ('django.db.models.fields.TextField', [], {'default': "'%(d)d.%(c)d.%(b)d.%(a)d.in-addr.arpa'"}),
'snat_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
'snat_to': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['firewall.Vlan']", 'null': 'True', 'blank': 'True'}),
'vid': ('django.db.models.fields.IntegerField', [], {'unique': 'True'})
},
u'storage.datastore': {
'Meta': {'ordering': "['name']", 'object_name': 'DataStore'},
'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'})
},
u'storage.disk': {
'Meta': {'ordering': "['name']", 'object_name': 'Disk'},
'base': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'derivatives'", 'null': 'True', 'to': u"orm['storage.Disk']"}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'datastore': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['storage.DataStore']"}),
'destroyed': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'dev_num': ('django.db.models.fields.CharField', [], {'default': "'a'", 'max_length': '1'}),
'filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'ready': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'size': ('sizefield.models.FileSizeField', [], {}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '10'})
},
u'taggit.tag': {
'Meta': {'object_name': 'Tag'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'})
},
u'taggit.taggeditem': {
'Meta': {'object_name': 'TaggedItem'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_tagged_items'", 'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_items'", 'to': u"orm['taggit.Tag']"})
},
u'vm.instance': {
'Meta': {'ordering': "[u'pk']", 'object_name': 'Instance'},
'access_method': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'active_since': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'boot_menu': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'destroyed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'disks': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'instance_set'", 'symmetrical': 'False', 'to': u"orm['storage.Disk']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lease': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Lease']"}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'instance_set'", 'null': 'True', 'to': u"orm['vm.Node']"}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'pw': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
'ram_size': ('django.db.models.fields.IntegerField', [], {}),
'raw_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'req_traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'}),
'template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'instance_set'", 'null': 'True', 'to': u"orm['vm.InstanceTemplate']"}),
'time_of_delete': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'time_of_suspend': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'vnc_port': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'})
},
u'vm.instancetemplate': {
'Meta': {'ordering': "[u'name']", 'object_name': 'InstanceTemplate'},
'access_method': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'boot_menu': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'disks': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'template_set'", 'symmetrical': 'False', 'to': u"orm['storage.Disk']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lease': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Lease']"}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.InstanceTemplate']", 'null': 'True', 'blank': 'True'}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'ram_size': ('django.db.models.fields.IntegerField', [], {}),
'raw_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'req_traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'default': "u'NEW'", 'max_length': '10'}),
'system': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
u'vm.lease': {
'Meta': {'ordering': "[u'name']", 'object_name': 'Lease'},
'delete_interval_seconds': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'suspend_interval_seconds': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'})
},
u'vm.node': {
'Meta': {'object_name': 'Node'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'host': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Host']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}),
'overcommit': ('django.db.models.fields.FloatField', [], {'default': '1.0'}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'})
},
u'vm.trait': {
'Meta': {'object_name': 'Trait'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
}
}
complete_apps = ['dashboard']
\ No newline at end of file
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'Notification.valid_until'
db.add_column(u'dashboard_notification', 'valid_until',
self.gf('django.db.models.fields.DateTimeField')(default=None, null=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'Notification.valid_until'
db.delete_column(u'dashboard_notification', 'valid_until')
models = {
u'acl.level': {
'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Level'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'weight': ('django.db.models.fields.IntegerField', [], {'null': 'True'})
},
u'acl.objectlevel': {
'Meta': {'unique_together': "(('content_type', 'object_id', 'level'),)", 'object_name': 'ObjectLevel'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'level': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['acl.Level']"}),
'object_id': ('django.db.models.fields.IntegerField', [], {}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'symmetrical': 'False'})
},
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'dashboard.favourite': {
'Meta': {'object_name': 'Favourite'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Instance']"}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
},
u'dashboard.groupprofile': {
'Meta': {'object_name': 'GroupProfile'},
'description': ('django.db.models.fields.TextField', [], {}),
'group': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.Group']", 'unique': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'org_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
},
u'dashboard.notification': {
'Meta': {'ordering': "['-created']", 'object_name': 'Notification'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'message': ('django.db.models.fields.TextField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'status': ('model_utils.fields.StatusField', [], {'default': "'new'", 'max_length': '100', u'no_check_for_status': 'True'}),
'subject': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'to': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'valid_until': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'})
},
u'dashboard.profile': {
'Meta': {'object_name': 'Profile'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance_limit': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
'org_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'preferred_language': ('django.db.models.fields.CharField', [], {'default': "'en-us'", 'max_length': '32'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
},
u'firewall.domain': {
'Meta': {'object_name': 'Domain'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'ttl': ('django.db.models.fields.IntegerField', [], {'default': '600'})
},
u'firewall.group': {
'Meta': {'object_name': 'Group'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'firewall.host': {
'Meta': {'unique_together': "(('hostname', 'vlan'),)", 'object_name': 'Host'},
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['firewall.Group']", 'null': 'True', 'blank': 'True'}),
'hostname': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ipv4': ('firewall.fields.IPAddressField', [], {'unique': 'True', 'max_length': '100'}),
'ipv6': ('firewall.fields.IPAddressField', [], {'max_length': '100', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'location': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'mac': ('firewall.fields.MACAddressField', [], {'unique': 'True', 'max_length': '17'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'pub_ipv4': ('firewall.fields.IPAddressField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
'reverse': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
'shared_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'vlan': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Vlan']"})
},
u'firewall.vlan': {
'Meta': {'object_name': 'Vlan'},
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'dhcp_pool': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Domain']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ipv6_template': ('django.db.models.fields.TextField', [], {'default': "'2001:738:2001:4031:%(b)d:%(c)d:%(d)d:0'"}),
'managed': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'network4': ('firewall.fields.IPNetworkField', [], {'max_length': '100'}),
'network6': ('firewall.fields.IPNetworkField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
'network_type': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'reverse_domain': ('django.db.models.fields.TextField', [], {'default': "'%(d)d.%(c)d.%(b)d.%(a)d.in-addr.arpa'"}),
'snat_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
'snat_to': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['firewall.Vlan']", 'null': 'True', 'blank': 'True'}),
'vid': ('django.db.models.fields.IntegerField', [], {'unique': 'True'})
},
u'storage.datastore': {
'Meta': {'ordering': "['name']", 'object_name': 'DataStore'},
'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'})
},
u'storage.disk': {
'Meta': {'ordering': "['name']", 'object_name': 'Disk'},
'base': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'derivatives'", 'null': 'True', 'to': u"orm['storage.Disk']"}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'datastore': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['storage.DataStore']"}),
'destroyed': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'dev_num': ('django.db.models.fields.CharField', [], {'default': "'a'", 'max_length': '1'}),
'filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'ready': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'size': ('sizefield.models.FileSizeField', [], {}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '10'})
},
u'taggit.tag': {
'Meta': {'object_name': 'Tag'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'})
},
u'taggit.taggeditem': {
'Meta': {'object_name': 'TaggedItem'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_tagged_items'", 'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_items'", 'to': u"orm['taggit.Tag']"})
},
u'vm.instance': {
'Meta': {'ordering': "[u'pk']", 'object_name': 'Instance'},
'access_method': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'active_since': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'boot_menu': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'destroyed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'disks': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'instance_set'", 'symmetrical': 'False', 'to': u"orm['storage.Disk']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lease': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Lease']"}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'instance_set'", 'null': 'True', 'to': u"orm['vm.Node']"}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'pw': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
'ram_size': ('django.db.models.fields.IntegerField', [], {}),
'raw_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'req_traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'}),
'system': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'instance_set'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['vm.InstanceTemplate']"}),
'time_of_delete': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'time_of_suspend': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'vnc_port': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'})
},
u'vm.instancetemplate': {
'Meta': {'ordering': "[u'name']", 'object_name': 'InstanceTemplate'},
'access_method': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'boot_menu': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'disks': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'template_set'", 'symmetrical': 'False', 'to': u"orm['storage.Disk']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lease': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Lease']"}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.InstanceTemplate']", 'null': 'True', 'blank': 'True'}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'ram_size': ('django.db.models.fields.IntegerField', [], {}),
'raw_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'req_traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'}),
'system': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
u'vm.lease': {
'Meta': {'ordering': "[u'name']", 'object_name': 'Lease'},
'delete_interval_seconds': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'suspend_interval_seconds': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'})
},
u'vm.node': {
'Meta': {'object_name': 'Node'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'host': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Host']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}),
'overcommit': ('django.db.models.fields.FloatField', [], {'default': '1.0'}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'})
},
u'vm.trait': {
'Meta': {'object_name': 'Trait'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
}
}
complete_apps = ['dashboard']
\ No newline at end of file
...@@ -5,9 +5,15 @@ from django.conf import settings ...@@ -5,9 +5,15 @@ from django.conf import settings
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.contrib.auth.signals import user_logged_in from django.contrib.auth.signals import user_logged_in
from django.db.models import ( from django.db.models import (
Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField,
DateTimeField,
) )
from django.utils.translation import ugettext_lazy as _ from django.template.loader import render_to_string
from django.utils.translation import ugettext_lazy as _, override
from model_utils.models import TimeStampedModel
from model_utils.fields import StatusField
from model_utils import Choices
from vm.models import Instance from vm.models import Instance
from acl.models import AclBase from acl.models import AclBase
...@@ -20,6 +26,34 @@ class Favourite(Model): ...@@ -20,6 +26,34 @@ class Favourite(Model):
user = ForeignKey(User) user = ForeignKey(User)
class Notification(TimeStampedModel):
STATUS = Choices(('new', _('new')),
('delivered', _('delivered')),
('read', _('read')))
status = StatusField()
to = ForeignKey(User)
subject = CharField(max_length=128)
message = TextField()
valid_until = DateTimeField(null=True, default=None)
class Meta:
ordering = ['-created']
@classmethod
def send(cls, user, subject, template, context={}, valid_until=None):
try:
language = user.profile.preferred_language
except:
language = None
with override(language):
context['user'] = user
rendered = render_to_string(template, context)
subject = unicode(subject)
return cls.objects.create(to=user, subject=subject, message=rendered,
valid_until=valid_until)
class Profile(Model): class Profile(Model):
user = OneToOneField(User) user = OneToOneField(User)
preferred_language = CharField(verbose_name=_('preferred language'), preferred_language = CharField(verbose_name=_('preferred language'),
...@@ -31,6 +65,10 @@ class Profile(Model): ...@@ -31,6 +65,10 @@ class Profile(Model):
help_text=_('Unique identifier of the person, e.g. a student number.')) help_text=_('Unique identifier of the person, e.g. a student number.'))
instance_limit = IntegerField(default=5) instance_limit = IntegerField(default=5)
def notify(self, subject, template, context={}, valid_until=None):
return Notification.send(self.user, subject, template, context,
valid_until)
class GroupProfile(AclBase): class GroupProfile(AclBase):
ACL_LEVELS = ( ACL_LEVELS = (
......
...@@ -331,3 +331,61 @@ a.hover-black { ...@@ -331,3 +331,61 @@ a.hover-black {
display: block; display: block;
} }
.notification-messages {
padding: 10px 8px;
width: 350px;
}
.notification-message {
margin-bottom: 10px;
padding: 0 0 4px 0;
border-bottom: 1px dotted #D3D3D3;
}
.notification-messages .notification-message:last-child {
margin-bottom: 0px;
padding: 0px;
border-bottom: none;
}
.notification-message-text {
padding: 8px 15px;
display: none;
}
.notification-message .notification-message-subject {
cursor: pointer;
}
#vm-migrate-node-list {
list-style: none;
}
#vm-migrate-node-list li {
padding-bottom: 10px;
}
.vm-migrate-node-property {
display: block;
padding-left: 15px;
}
.vm-details-help {
max-width: 700px;
padding: 10px 10px 0px 10px;
margin-left: 50px;
/* fancy stuff
border: 1px solid #ccc;
box-shadow: 0 0 10px rgba(0,0,0,0.2);
border-radius: 8px;
*/
}
.vm-details-help li {
padding-bottom: 5px;
}
.vm-details-help ul {
padding-left: 0px;
}
...@@ -66,6 +66,8 @@ $(function () { ...@@ -66,6 +66,8 @@ $(function () {
if (window.location.hash) { if (window.location.hash) {
if(window.location.hash.substring(1,4) == "ipv") if(window.location.hash.substring(1,4) == "ipv")
$("a[href=#network]").tab('show'); $("a[href=#network]").tab('show');
if(window.location.hash == "activity")
checkNewActivity(false, 1);
$("a[href=" + window.location.hash +"]").tab('show'); $("a[href=" + window.location.hash +"]").tab('show');
} }
...@@ -205,7 +207,16 @@ $(function () { ...@@ -205,7 +207,16 @@ $(function () {
window.location.href = "/dashboard/vm/list/?s=" + input; window.location.href = "/dashboard/vm/list/?s=" + input;
} }
}); });
/* notification message toggle */
$(document).on('click', ".notification-message-subject", function() {
$(".notification-message-text", $(this).parent()).slideToggle();
return false;
});
$("#notification-button a").click(function() {
$('.notification-messages').load("/dashboard/notifications/");
});
}); });
function generateVmHTML(pk, name, fav) { function generateVmHTML(pk, name, fav) {
......
/* for functions in both vm list and vm detail */
$(function() {
/* vm migrate */
$('.vm-migrate').click(function(e) {
var icon = $(this).children("i");
var vm = $(this).data("vm-pk");
icon.removeClass("icon-truck").addClass("icon-spinner icon-spin");
$.ajax({
type: 'GET',
url: '/dashboard/vm/' + vm + '/migrate/',
success: function(data) {
icon.addClass("icon-truck").removeClass("icon-spinner icon-spin");
$('body').append(data);
$('#create-modal').modal('show');
$('#create-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove();
});
$('#vm-migrate-node-list li').click(function(e) {
var li = $(this).closest('li');
if (li.find('input').attr('disabled'))
return true;
$('#vm-migrate-node-list li').removeClass('panel-primary');
li.addClass('panel-primary').find('input').attr('checked', true);
return false;
});
$('#vm-migrate-node-list li input:checked').closest('li').addClass('panel-primary');
}
});
return false;
});
});
$(function() {
"use strict";
Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js",
"input.js", "display.js", "jsunzip.js", "rfb.js"]);
var rfb;
function updateState(rfb, state, oldstate, msg) {
$('#_console .btn-toolbar button').attr('disabled', !(state === "normal"));
rfb.sendKey(0xffe3); // press and release ctrl to kill screensaver
if (typeof(msg) !== 'undefined') {
$('#noVNC_status').html(msg);
}
}
$('a[data-toggle$="pill"][href!="#console"]').click(function() {
if (rfb) {
rfb.disconnect();
rfb = 0;
}
$("#vm-info-pane").fadeIn();
$("#vm-detail-pane").removeClass("col-md-12");
});
$('#sendCtrlAltDelButton').click(function() {
rfb.sendCtrlAltDel(); return false;});
$('#sendPasswordButton').click(function() {
var pw = $("#vm-details-pw-input").val();
for (var i=0; i < pw.length; i++) {
rfb.sendKey(pw.charCodeAt(i));
} return false;});
$("body").on("click", 'a[href$="console"]', function() {
var host, port, password, path;
$("#vm-info-pane").hide();
$("#vm-detail-pane").addClass("col-md-12");
WebUtil.init_logging('warn');
host = window.location.hostname;
if (window.location.port == 8080) {
port = 9999;
} else {
port = window.location.port == "" ? "443" : window.location.port;
}
password = '';
$('#_console .btn-toolbar button').attr('disabled', true);
$('#noVNC_status').html('Retreiving authorization token.');
$.get(VNC_URL, function(data) {
if (data.indexOf('vnc') != 0) {
$('#noVNC_status').html('No authorization token received.');
}
else {
rfb = new RFB({'target': $D('noVNC_canvas'),
'encrypt': (window.location.protocol === "https:"),
'true_color': true,
'local_cursor': true,
'shared': true,
'view_only': false,
'updateState': updateState});
rfb.connect(host, port, password, data);
}
}).fail(function(){
$('#noVNC_status').html("Can't connect to console.");
});
});
if (window.location.hash == "#console")
window.onscriptsload = function(){$('a[href$="console"]').click();};
});
...@@ -2,24 +2,72 @@ var vlans = []; ...@@ -2,24 +2,72 @@ var vlans = [];
var disks = []; var disks = [];
$(function() { $(function() {
vmCreateLoaded(); vmCustomizeLoaded();
}); });
function vmCreateLoaded() { function vmCreateLoaded() {
$('.vm-create-advanced').hide(); $(".vm-create-template-details").hide();
$('.vm-create-advanced-btn').click(function() {
$('.vm-create-advanced').stop().slideToggle(); $(".vm-create-template-summary").click(function() {
if ($('.vm-create-advanced-icon').hasClass('icon-caret-down')) { $(this).next(".vm-create-template-details").slideToggle();
$('.vm-create-advanced-icon').removeClass('icon-caret-down').addClass('icon-caret-up');
} else {
$('.vm-create-advanced-icon').removeClass('icon-caret-up').addClass('icon-caret-down');
}
}); });
$(".customize-vm").click(function() {
var template = $(this).data("template-pk");
console.log(template);
$('#vm-create-template-select').change(function() { $.get("/dashboard/vm/create/?template=" + template, function(data) {
vmCreateTemplateChange(this); var r = $('#create-modal'); r.next('div').remove(); r.remove();
$('body').append(data);
vmCreateLoaded();
addSliderMiscs();
$('#create-modal').modal('show');
$('#create-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove();
});
});
return false;
}); });
/* start vm button clicks */
$('.vm-create-start').click(function() {
template = $(this).data("template-pk");
$.ajax({
url: '/dashboard/vm/create/',
headers: {"X-CSRFToken": getCookie('csrftoken')},
type: 'POST',
data: {'template': template},
success: function(data, textStatus, xhr) {
if(data.redirect) {
window.location.replace(data.redirect + '#activity');
}
else {
var r = $('#create-modal'); r.next('div').remove(); r.remove();
$('body').append(data);
vmCreateLoaded();
addSliderMiscs();
$('#create-modal').modal('show');
$('#create-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove();
});
}
},
error: function(xhr, textStatus, error) {
var r = $('#create-modal'); r.next('div').remove(); r.remove();
if (xhr.status == 500) {
addMessage("500 Internal Server Error", "danger");
} else {
addMessage(xhr.status + " Unknown Error", "danger");
}
}
});
return false;
});
}
function vmCustomizeLoaded() {
/* network thingies */ /* network thingies */
/* add network */ /* add network */
...@@ -86,15 +134,24 @@ function vmCreateLoaded() { ...@@ -86,15 +134,24 @@ function vmCreateLoaded() {
/* copy networks from hidden select */ /* copy networks from hidden select */
$('#vm-create-network-add-vlan option').each(function() { $('#vm-create-network-add-vlan option').each(function() {
var managed = $(this).text().indexOf("mana") == 0; var managed = $(this).text().indexOf("mana") == 0;
var text = $(this).text(); var raw_text = $(this).text();
var pk = $(this).val(); var pk = $(this).val();
if(managed) { if(managed) {
text = text.replace("managed -", "&#xf0ac;"); text = raw_text.replace("managed -", "&#xf0ac;");
} else { } else {
text = text.replace("unmanaged -", "&#xf0c1;"); text = raw_text.replace("unmanaged -", "&#xf0c1;");
} }
var html = '<option data-managed="' + (managed ? 1 : 0) + '" value="' + pk + '">' + text + '</option>'; var html = '<option data-managed="' + (managed ? 1 : 0) + '" value="' + pk + '">' + text + '</option>';
$('#vm-create-network-add-select').append(html);
if($('#vm-create-network-list span').length < 1) {
$("#vm-create-network-list").html("");
}
if($(this).is(":selected")) {
$("#vm-create-network-list").append(vmCreateNetworkLabel(pk, raw_text.replace("unmanaged -", "").replace("managed -", ""), managed));
} else {
$('#vm-create-network-add-select').append(html);
}
}); });
...@@ -168,8 +225,20 @@ function vmCreateLoaded() { ...@@ -168,8 +225,20 @@ function vmCreateLoaded() {
}); });
/* copy disks from hidden select */ /* copy disks from hidden select */
$('#vm-create-disk-add-select').html($('#vm-create-disk-add-form').html()); $('#vm-create-disk-add-form option').each(function() {
var text = $(this).text();
var pk = $(this).val();
var html = '<option value="' + pk + '">' + text + '</option>';
if($('#vm-create-disk-list span').length < 1) {
$("#vm-create-disk-list").html("");
}
if($(this).is(":selected")) {
$("#vm-create-disk-list").append(vmCreateDiskLabel(pk, text));
} else {
$('#vm-create-disk-add-select').append(html);
}
});
/* build up disk list */ /* build up disk list */
$('#vm-create-disk-add-select option').each(function() { $('#vm-create-disk-add-select option').each(function() {
...@@ -179,8 +248,8 @@ function vmCreateLoaded() { ...@@ -179,8 +248,8 @@ function vmCreateLoaded() {
}); });
}); });
/* add button */ /* start vm button clicks */
$('#vm-create-submit').click(function() { $('#vm-create-customized-start').click(function() {
$.ajax({ $.ajax({
url: '/dashboard/vm/create/', url: '/dashboard/vm/create/',
headers: {"X-CSRFToken": getCookie('csrftoken')}, headers: {"X-CSRFToken": getCookie('csrftoken')},
...@@ -219,97 +288,6 @@ function vmCreateLoaded() { ...@@ -219,97 +288,6 @@ function vmCreateLoaded() {
$('.js-hidden').hide(); $('.js-hidden').hide();
} }
function vmCreateTemplateChange(new_this) {
this.value = new_this.value;
if(this.value < 0) return;
$.ajax({
url: '/dashboard/template/' + this.value,
type: 'GET',
success: function(data, textStatus, xhr) {
if(xhr.status == 200) {
// set sliders
$('#vm-cpu-priority-slider').slider("setValue", data['priority']);
$('#vm-cpu-count-slider').slider("setValue", data['num_cores']);
$('#vm-ram-size-slider').slider("setValue", data['ram_size']);
/* slider doesn't have change event ........................ */
refreshSliders();
/* clear selections */
$("#vm-create-network-add-vlan").find('option').prop('selected', false);
$('#vm-create-disk-add-form').find('option').prop('selected', false);
/* clear the network select */
$("#vm-create-network-add-select").html('');
/* append vlans from InterfaceTemplates */
$('#vm-create-network-list').html("");
var added_vlans = []
for(var n = 0; n<data['network'].length; n++) {
nn = data['network'][n]
$('#vm-create-network-list').append(
vmCreateNetworkLabel(nn.vlan_pk, nn.vlan, nn.managed)
);
$('#vm-create-network-add-vlan option[value="' + nn.vlan_pk + '"]').prop('selected', true);
added_vlans.push(nn.vlan_pk);
}
/* remove already added vlans from dropdown or add new ones */
$('#vm-create-network-add-select').html('');
// this is working because the vlans array already has the icon's hex code
for(var i=0; i < vlans.length; i++)
if(added_vlans.indexOf(vlans[i].pk) == -1) {
var html = '<option data-managed="' + (vlans[i].managed ? 1 : 0) + '" value="' + vlans[i].pk + '">' + vlans[i].name + '</option>';
$('#vm-create-network-add-select').append(html);
}
/* enable the network add button if there are not added vlans */
if(added_vlans.length != vlans.length) {
$('#vm-create-network-add-button').attr('disabled', false);
} else {
$('#vm-create-network-add-select').html('<option value="-1">No more networks!</option>');
$('#vm-create-network-add-button').attr('disabled', true);
}
/* if there are no added vlans print it out */
if(added_vlans.length < 1) {
$('#vm-create-network-list').html("Not added to any network!");
}
/* append disks */
$('#vm-create-disk-list').html('');
var added_disks = []
for(var d = 0; d<data['disks'].length; d++) {
dd = data['disks'][d]
$('#vm-create-disk-list').append(
vmCreateDiskLabel(dd.pk, dd.name)
);
$('#vm-create-disk-add-form option[value="' + dd.pk + '"]').prop('selected', true);
added_disks.push(dd.pk);
}
/* remove already added disks from dropdown or add new ones */
$('#vm-create-disk-add-select').html('');
for(var i=0; i < disks.length; i++)
if(added_disks.indexOf(disks[i].pk) == -1)
$('#vm-create-disk-add-select').append($('<option>', {
value: disks[i].pk,
text: disks[i].name
}));
/* enable the disk add button if there are not added disks */
if(added_disks.length != disks.length) {
$('#vm-create-disk-add-button').attr('disabled', false);
} else {
$('#vm-create-disk-add-select').html('<option value="-1">We are out of &lt;options&gt; hehe</option>');
$('#vm-create-disk-add-button').attr('disabled', true);
}
}
}
});
}
function vmCreateNetworkLabel(pk, name, managed) { function vmCreateNetworkLabel(pk, name, managed) {
return '<span id="vlan-' + pk + '" class="label label-' + (managed ? 'primary' : 'default') + '"><i class="icon-' + (managed ? 'globe' : 'link') + '"></i> ' + name + ' <a href="#" class="hover-black vm-create-remove-network"><i class="icon-remove-sign"></i></a></span> '; return '<span id="vlan-' + pk + '" class="label label-' + (managed ? 'primary' : 'default') + '"><i class="icon-' + (managed ? 'globe' : 'link') + '"></i> ' + name + ' <a href="#" class="hover-black vm-create-remove-network"><i class="icon-remove-sign"></i></a></span> ';
......
...@@ -3,6 +3,10 @@ $(function() { ...@@ -3,6 +3,10 @@ $(function() {
if(decideActivityRefresh()) { if(decideActivityRefresh()) {
checkNewActivity(false, 1); checkNewActivity(false, 1);
} }
$('a[href="#activity"]').click(function(){
$('a[href="#activity"] i').addClass('icon-spin');
checkNewActivity(false,0);
});
/* save resources */ /* save resources */
$('#vm-details-resources-save').click(function() { $('#vm-details-resources-save').click(function() {
...@@ -131,7 +135,11 @@ $(function() { ...@@ -131,7 +135,11 @@ $(function() {
location.reload(); location.reload();
}, },
error: function(xhr, textStatus, error) { error: function(xhr, textStatus, error) {
if (xhr.status == 500) {
addMessage("Internal Server Error", "danger");
} else {
addMessage(xhr.status + " Unknown Error", "danger");
}
} }
}); });
} else { } else {
...@@ -152,6 +160,11 @@ $(function() { ...@@ -152,6 +160,11 @@ $(function() {
$("#vm-details-disk-add-for-form").html($("#vm-details-disk-add-form").html()); $("#vm-details-disk-add-for-form").html($("#vm-details-disk-add-form").html());
return false; return false;
}); });
/* show help */
$(".vm-details-help-button").click(function() {
$(".vm-details-help").stop().slideToggle();
});
}); });
...@@ -198,17 +211,23 @@ function checkNewActivity(only_state, runs) { ...@@ -198,17 +211,23 @@ function checkNewActivity(only_state, runs) {
success: function(data) { success: function(data) {
if(!only_state) { if(!only_state) {
$("#activity-timeline").html(data['activities']); $("#activity-timeline").html(data['activities']);
$("[title]").tooltip();
} }
$("#vm-details-state").html(data['state']); $("#vm-details-state").html(data['state']);
if(data['state'] == "RUNNING") {
$("[data-target=#_console]").attr("data-toggle", "pill").attr("href", "#console").parent("li").removeClass("disabled");
} else {
$("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled");
}
if(decideActivityRefresh()) { if(runs > 0 && decideActivityRefresh()) {
console.log("szia");
setTimeout( setTimeout(
function() {checkNewActivity(only_state, runs + 1)}, function() {checkNewActivity(only_state, runs + 1)},
1000 + runs * 250 1000 + Math.exp(runs * 0.05)
); );
} }
$('a[href="#activity"] i').removeClass('icon-spin');
}, },
error: function() { error: function() {
......
{% load i18n %}
{% load sizefieldtags %}
<i class="{% if d.is_downloading %}icon-refresh icon-spin{% else %}icon-file{% endif %}"></i>
{{ d.name }} (#{{ d.id }}) -
{% if not d.is_downloading %}
{% if d.ready %}
{{ d.size|filesize }}
{% else %}
<div class="label label-danger">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>
{% load i18n %}
{% for n in notifications %}
<li class="notification-message">
<span class="notification-message-subject">
{% if n.status == "new" %}<i class="icon-envelope-alt"></i> {% endif %}
{{ n.subject }}
</span>
<span class="notification-message-date pull-right">
{{ n.created|timesince }}
</span>
<div style="clear: both;"></div>
<div class="notification-message-text">
{{ n.message|safe }}
</div>
</li>
{% empty %}
<li class="notification-message">
{% trans "You have no notifications." %}
</li>
{% endfor %}
{% load sizefieldtags %}
{% load i18n %}
<div class="vm-create-template-list">
{% for t in templates %}
<div class="vm-create-template">
<div class="vm-create-template-summary">
{{ t.name }}
<span class="pull-right"><i class="icon-{{ t.os_type }}"></i> {{ t.system }}</span>
</div>
<div class="vm-create-template-details">
<ul>
<li>
<i class="icon-gears"></i> {% trans "CPU" %}
<div class="progress pull-right">
<div class="progress-bar progress-bar-success" role="progressbar"
aria-valuenow="{{ t.num_cores }}" aria-valuemin="0" aria-valuemax="8" style="width: 80%">
<span class="progress-bar-text">{{ t.num_cores }} cores</span>
</div>
</div>
</li>
<li>
<i class="icon-ticket"></i> {% trans "Memory" %}
<div class="progress pull-right">
<div class="progress-bar progress-bar-info" role="progressbar"
aria-valuenow="{{ t.ram_size }}" aria-valuemin="0" aria-valuemax="4096"
style="width: 80%">
<span class="progress-bar-text">{{ t.ram_size }} MB</span>
</div>
</div>
</li>
<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 %}
</span>
<div style="clear: both;"></div>
</li>
<li>
<i class="icon-globe"></i> {% trans "Network" %}
<span style="float: right;">
{% for i in t.interface_set.all %}{{ i.vlan.name }}{% if not forloop.last %}, {% endif %}{% endfor %}
</span>
</li>
<li>
<i class="icon-tag"></i> {% trans "Type" %}: {{ t.lease.name }}
<span style="float: right;">
<i class="icon-pause"></i> {{ t.lease.get_readable_suspend_time }}
<i class="icon-remove"></i> {{ t.lease.get_readable_delete_time }}
</span>
</li>
<li>
<i class="icon-hand-right"></i> Description:
<span style="float: right; max-width: 350px;">
{{ t.description }}
</span>
<div class="clearfix"></div>
</li>
</ul>
<div style="margin-top: 20px; padding: 0 15px; width: 100%">
<a class="btn btn-primary btn-xs customize-vm" data-template-pk="{{ t.pk }}" href="{% url "dashboard.views.vm-create" %}?template={{ t.pk }}"><i class="icon-wrench"></i> Customize</a>
<form class="pull-right text-right" method="POST" action="{% url "dashboard.views.vm-create" %}">
{% csrf_token %}
<input type="hidden" name="template" value="{{ t.pk }}"/>
<button class="btn btn-success btn-xs vm-create-start" data-template-pk="{{ t.pk }}" type="submit"><i class="icon-play"></i> Start</button>
</form>
</div>
</div>
</div>
{% endfor %}
</div>
<style>
.row {
margin-bottom: 15px;
}
.vm-create-template {
max-width: 800px;
border: 1px solid black;
border-bottom: none;
}
.vm-create-template-list .vm-create-template:last-child {
border-bottom: 1px solid black;
}
.vm-create-template-summary {
padding: 15px;
cursor: pointer;
}
.vm-create-template:nth-child(odd) .vm-create-template-summary {
background: #F5F5F5;
}
.vm-create-template-list .vm-create-template-summary:hover {
background: #D2D2D2;
}
.vm-create-template-details {
border-top: 1px dashed #D3D3D3;
padding: 15px;
}
.vm-create-template-details ul {
list-style: none;
padding: 0 15px;
}
.vm-create-template-details li {
border-bottom: 1px dotted #aaa;
padding: 5px 0px;
}
.progress {
position: relative;
width: 200px;
height: 12px;
margin-bottom: 0px;
margin-top: 5px;
}
.progress-bar-text {
position: absolute;
display: block;
width: 100%;
color: white;
/* outline */
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000;
font-size: 10px;
}
</style>
{% block "extra-js" %}
<script>
$('.progress-bar').each(function() {
var min = $(this).attr('aria-valuemin');
var max = $(this).attr('aria-valuemax');
var now = $(this).attr('aria-valuenow');
var siz = (now-min)*100/(max-min);
$(this).css('width', siz+'%');
});
</script>
{% endblock %}
{% load crispy_forms_tags %}
{% load sizefieldtags %}
{% crispy vm_create_form %}
<script src="/static/dashboard/vm-create.js"></script>
{% load i18n %}
{% load sizefieldtags %}
<form method="POST" action="{% url "dashboard.views.vm-migrate" pk=vm.pk %}">
{% csrf_token %}
<ul id="vm-migrate-node-list">
{% with current=vm.node.pk selected=vm.select_node.pk %}
{% for n in nodes %}
<li class="panel panel-default"><div class="panel-body">
<label for="migrate-to-{{n.pk}}">
<strong>{{ n }}</strong>
{% if current == n.pk %}<div class="label label-info">{% trans "current" %}</div>{% endif %}
{% if selected == n.pk %}<div class="label label-success">{% trans "recommended" %}</div>{% endif %}
</label>
<input id="migrate-to-{{n.pk}}" type="radio" name="node" value="{{ n.pk }}" style="float: right;"
{% if current == n.pk %}disabled="disabled"{% endif %}
{% if selected == n.pk %}checked="checked"{% endif %} />
<span class="vm-migrate-node-property">{% trans "CPU load" %}: {{ n.cpu_usage }}</span>
<span class="vm-migrate-node-property">{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span>
<div style="clear: both;"></div>
</div></li>
{% endfor %}
{% endwith %}
</ul>
<button type="submit" class="btn btn-primary btn-sm"><i class="icon-truck"></i> Migrate</button>
</form>
...@@ -23,16 +23,28 @@ ...@@ -23,16 +23,28 @@
<body> <body>
<div class="navbar navbar-inverse navbar-fixed-top"> <div class="navbar navbar-inverse navbar-fixed-top">
<a class="navbar-brand" href="/dashboard/">{% block header-site %}CIRCLE{% endblock %}</a> <div class="navbar-header">
<!-- temporarily --> <a class="navbar-brand" href="{% url "dashboard.index" %}">CIRCLE</a>
<a class="navbar-brand pull-right" href="/network/" style="color: white; font-size: 10px;">Network</a> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<a class="navbar-brand pull-right" href="/admin/" style="color: white; font-size: 10px;">Admin</a> <span class="icon-bar"></span>
{% if user.is_authenticated %} <span class="icon-bar"></span>
<a class="navbar-brand pull-right" href="{% url "logout" %}?next={% url "login" %}" style="color: white; font-size: 10px;">Log out {{user}}</a> <span class="icon-bar"></span>
{% else %} </button>
<a class="navbar-brand pull-right" href="{% url "login" %}?next={% url "dashboard.index" %}" style="color: white; font-size: 10px;">Login</a> </div><!-- .navbar-header -->
{% endif %} <div class="collapse navbar-collapse">
</div> <ul class="nav navbar-nav pull-right">
{% block navbar-ul %}
{% endblock %}
</ul>
<a class="navbar-brand pull-right" href="/network/" style="color: white; font-size: 10px;">Network</a>
<a class="navbar-brand pull-right" href="/admin/" style="color: white; font-size: 10px;">Admin</a>
{% if user.is_authenticated %}
<a class="navbar-brand pull-right" href="{% url "logout" %}?next={% url "login" %}" style="color: white; font-size: 10px;">Log out {{user}}</a>
{% else %}
<a class="navbar-brand pull-right" href="{% url "login" %}?next={% url "dashboard.index" %}" style="color: white; font-size: 10px;">Login</a>
{% endif %}
</div><!-- .collapse .navbar-collapse -->
</div><!-- navbar navbar-inverse navbar-fixed-top -->
<div class="container"> <div class="container">
{% block messages %} {% block messages %}
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="body-content">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
{%blocktrans with instance=instance.name%}
Renewing <em>{{instance}}</em>
{%endblocktrans%}
</h3>
</div>
<div class="panel-body">
{%blocktrans with object=instance.name%}
Do you want to renew <strong>{{ object }}</strong>?
{%endblocktrans%}
{%blocktrans with suspend=time_of_suspend delete=time_of_delete|default:"n/a" %}
The instance will be suspended at <em>{{suspend}}</em>
and removed at <em>{{delete}}</em> if you renew it now.
{%endblocktrans%}
<div class="pull-right">
<form action="" method="POST">
{% csrf_token %}
<a class="btn btn-default"
href="{{instance.get_absolute_path}}">{% trans "Back" %}</a>
<button class="btn btn-danger">{% trans "Renew" %}</button>
</form>
</div>
</div>
</div>
{% endblock %}
...@@ -3,6 +3,17 @@ ...@@ -3,6 +3,17 @@
{% block title-page %}{% trans "Dashboard" %}{% endblock %} {% block title-page %}{% trans "Dashboard" %}{% endblock %}
{% block navbar-ul %}
<li class="dropdown" id="notification-button">
<a href="{% url "dashboard.views.notifications" %}" style="color: white; font-size: 12px;" class="dropdown-toggle" data-toggle="dropdown">
Notifications{% if new_notifications %} <span class="badge">{{ new_notifications }}</span>{% endif %}
</a>
<ul class="dropdown-menu notification-messages">
<li>{% trans "Loading..." %}</li>
</ul>
</li>
{% endblock %}
{% block content %} {% block content %}
<div class="body-content dashboard-index"> <div class="body-content dashboard-index">
<div class="row"> <div class="row">
...@@ -17,9 +28,11 @@ ...@@ -17,9 +28,11 @@
{% include "dashboard/index-groups.html" %} {% include "dashboard/index-groups.html" %}
</div> </div>
{% comment %}
<div class="col-lg-4 col-sm-6"> <div class="col-lg-4 col-sm-6">
{% include "dashboard/index-files.html" %} {% include "dashboard/index-files.html" %}
</div> </div>
{% endcomment %}
{% if perms.vm.create_template %} {% if perms.vm.create_template %}
<div class="col-lg-4 col-sm-6"> <div class="col-lg-4 col-sm-6">
......
<div class="modal fade" id="create-modal" tabindex="-1" role="dialog"> <div class="modal fade" id="create-modal" tabindex="-1" role="dialog">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
{% if box_title and ajax_title %}
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">{{ box_title }}</h4> <h4 class="modal-title">{{ box_title }}</h4>
</div> </div>
{% endif %}
<div class="modal-body"> <div class="modal-body">
{% include template %} {% include template %}
</div> </div>
......
...@@ -8,10 +8,10 @@ ...@@ -8,10 +8,10 @@
<div class="row"> <div class="row">
<div class="col-md-4" id="node-info-pane"> <div class="col-md-4" id="node-info-pane">
<div id="node-info-data" class="big"> <div id="node-info-data" class="big">
<span id="node-status-label" class="label {% if node.state == 'Online' %}label-success <span id="node-details-state" class="label {% if node.state == 'ONLINE' %}label-success
{% elif node.state == 'Missing' %}label-danger {% elif node.state == 'MISSING' %}label-danger
{% elif node.state == 'Disabled' %}label-warning {% elif node.state == 'DISABLED' %}label-warning
{% elif node.state == 'Offline' %}label-warning {% elif node.state == 'OFFLINE' %}label-warning
{% endif %}">{{ node.state }}</span> {% endif %}">{{ node.state }}</span>
<div class="btn-group"> <div class="btn-group">
{% with node as record %} {% with node as record %}
...@@ -42,10 +42,10 @@ ...@@ -42,10 +42,10 @@
</ul> </ul>
<div id="panel-body" class="tab-content panel-body"> <div id="panel-body" class="tab-content panel-body">
<div class="tab-pane active" id="home">{% include "dashboard/node-detail-home.html" %}</div> <div class="tab-pane active" id="home">{% include "dashboard/node-detail/home.html" %}</div>
<div class="tab-pane" id="resources">{% include "dashboard/node-detail-resources.html" %}</div> <div class="tab-pane" id="resources">{% include "dashboard/node-detail/resources.html" %}</div>
<div class="tab-pane" id="activity">{% include "dashboard/node-detail-activity.html" %}</div> <div class="tab-pane" id="activity">{% include "dashboard/node-detail/activity.html" %}</div>
<div class="tab-pane" id="virtualmachines">{% include "dashboard/node-detail-vm.html" %}</div> <div class="tab-pane" id="virtualmachines">{% include "dashboard/node-detail/vm.html" %}</div>
</div> </div>
</div> </div>
</div> </div>
......
{% load i18n %} {% load i18n %}
<h3>{% trans "Activity" %}</h3> {% for a in activities %}
<style> <div class="activity" data-activity-id="{{ a.pk }}">
.sub-timeline { <span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}">
border-left: 3px solid green; <i class="{% if not a.finished %} icon-refresh icon-spin {% else %}icon-plus{% endif %}"></i>
margin-left: 30px; </span>
padding-left: 10px; <strong>{{ a.get_readable_name }}</strong>
} {{ a.started|date:"Y-m-d H:i" }}, {{ a.user }}
</style> {% if a.children.count > 0 %}
<div class="sub-timeline">
<div class="timeline"> {% for s in a.children.all %}
{% for a in activity %} <div data-activity-id="{{ s.pk }}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}">
<div class="activity" data-activity-id="{{ a.pk }}"> {{ s.get_readable_name }} -
<span class="timeline-icon"> {% if s.finished %}
<i class="{% if not a.finished %} icon-refresh icon-spin {% else %}icon-plus{% endif %}"></i> {{ s.finished|time:"H:i:s" }}
</span> {% else %}
<strong>{{ a.get_readable_name }}</strong> <i class="icon-refresh icon-spin" class="sub-activity-loading-icon"></i>
{{ a.started|date:"Y-m-d. H:i" }}, {{ a.user }} {% endif %}
{% if a.children.count > 0 %} {% if s.has_failed %}
<div class="sub-timeline"> <div class="label label-danger">{% trans "failed" %}</div>
{% for s in a.children.all %} {% endif %}
<div data-activity-id="{{ s.pk }}" class="sub-activity">
{{ s.get_readable_name }} -
{% if s.finished %}
{{ s.finished|time:"H:i:s" }}
{% else %}
<i class="icon-refresh icon-spin" class="sub-activity-loading-icon"></i>
{% endif %}
</div>
{% endfor %}
</div> </div>
{% endif %} {% endfor %}
</div> </div>
{% endfor %} {% endif %}
<div><span class="timeline-icon timeline-warning"><i class="icon-remove"></i></span> <strong>Removing</strong> 2013-11-21 15:32</div>
<div><span class="timeline-icon timeline-warning"><i class="icon-pause"></i></span> <strong>Suspending</strong> 2013-09-21 15:32</div>
<div><span class="timeline-icon"><i class="icon-ellipsis-vertical" ></i></span> <strong>(now)</strong></div>
<div><span class="timeline-icon"><i class="icon-truck"></i></span> <strong>Migrated to mega5</strong> 2013-04-21 15:32, ABC123</div>
<div><span class="timeline-icon"><i class="icon-refresh"></i></span> <strong>Forced reboot</strong> 2013-04-21 15:32, ABC123</div>
<div><span class="timeline-icon"><i class="icon-plus"></i></span> <strong>Created</strong> 2013-04-21 15:32, ABC123</div>
</div> </div>
{% endfor %}
{% block extra_js %}
<script src="{{ STATIC_URL }}dashboard/vm-details.js"></script>
{% endblock %}
{% load i18n %}
<h3>{% trans "Activity" %}</h3>
<div id="activity-timeline" class="timeline">
{% include "dashboard/node-detail/_activity-timeline.html" %}
</div>
...@@ -35,10 +35,11 @@ ...@@ -35,10 +35,11 @@
</div><!-- id:node-details-traits --> </div><!-- id:node-details-traits -->
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
<img src="/static/grafikon.png" style="width:45%"/> {% if graphite_enabled %}
<img src="/static/grafikon.png" style="width:45%"/> <img src="{% url "dashboard.views.node-graph" node.pk "cpu" "6h" %}" style="width:100%"/>
<img src="/static/grafikon.png" style="width:45%"/> <img src="{% url "dashboard.views.node-graph" node.pk "memory" "6h" %}" style="width:100%"/>
<img src="/static/grafikon.png" style="width:45%"/> <img src="{% url "dashboard.views.node-graph" node.pk "network" "6h" %}" style="width:100%"/>
{% endif %}
</div> </div>
</div> </div>
<style> <style>
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
{% if template == "dashboard/vm-create.html" %} {% if template == "dashboard/_vm-create-1.html" %}
<script src="{{ STATIC_URL }}dashboard/vm-create.js"></script> <script src="{{ STATIC_URL }}dashboard/vm-create.js"></script>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin"><i class="icon-desktop"></i> {% trans "Notifications" %}</h3>
</div>
<div class="panel-body">
<ul style="list-style: none;">
{% include "dashboard/_notifications-timeline.html" %}
</ul>
</div>
</div>
</div>
</div>
{% endblock %}
{%load i18n%}
{%blocktrans with instance=instance.name user=user.name%}
Your ownership offer of {{instance}} has been accepted by {{user}}.
{%endblocktrans%}
{%load i18n%}
{%blocktrans with instance=instance.name user=user.name%}
{{user}} offered you to take the ownership of his/her virtual machine
called {{instance}}.{%endblocktrans%}
<a href="{{token}}" class="btn btn-success btn-small">{%trans "Accept"%}</a>
{%load i18n%}
{%blocktrans with instance=instance.name url=instance.get_absolute_url %}
Your instance <a href="{{url}}">{{instance}}</a> has been destroyed due to expiration.
{%endblocktrans%}
{%load i18n%}
{%blocktrans with instance=instance.name url=instance.get_absolute_url suspend=instance.time_of_suspend delete=instance.time_of_delete %}
Your instance <a href="{{url}}">{{instance}}</a> is going to expire.
It will be suspended at {{suspend}} and destroyed at {{delete}}.
{%endblocktrans%}
{%blocktrans with token=token url=instance.get_absolute_url %}
Please, either <a href="{{token}}">renew</a> or <a href="{{url}}">destroy</a>
it now.
{%endblocktrans%}
{%load i18n%}
{%blocktrans with instance=instance.name url=instance.get_absolute_url %}
Your instance <a href="{{url}}">{{instance}}</a> has been suspended due to expiration.
{%endblocktrans%}
{% extends "dashboard/base.html" %} {% extends "dashboard/base.html" %}
{% load i18n %} {% load i18n %}
{% load sizefieldtags %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block content %} {% block content %}
...@@ -22,7 +23,7 @@ ...@@ -22,7 +23,7 @@
<div class="col-md-4"> <div class="col-md-4">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="no-margin"><i class="icon-group"></i> {% trans "Manage access" %}</h3> <h4 class="no-margin"><i class="icon-group"></i> {% trans "Manage access" %}</h4>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<form action="{% url "dashboard.views.template-acl" pk=object.pk %}" method="post">{% csrf_token %} <form action="{% url "dashboard.views.template-acl" pk=object.pk %}" method="post">{% csrf_token %}
...@@ -64,8 +65,35 @@ ...@@ -64,8 +65,35 @@
</form> </form>
</div> </div>
</div> </div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<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;">
{% for d in disks %}
<li>
{% include "dashboard/_disk-list-element.html" %}
</li>
{% endfor %}
</ul>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="no-margin"><i class="icon-folder-open"></i> {% trans "Create new disk" %}</h4>
</div>
<div class="panel-body">
<form action="{% url "dashboard.views.disk-add" %}" method="POST">
{% crispy disk_add_form %}
</form>
</div>
</div>
</div><!-- .col-md-4 -->
</div><!-- .row -->
<style> <style>
......
This is a test for {{user.username}}.
Var: {{var}}.
{% load crispy_forms_tags %}
<style>
.row {
margin-bottom: 15px;
}
</style>
<form method="POST" action="/dashboard/vm/create/">
{% csrf_token %}
{% crispy vm_create_form %}
</form>
...@@ -5,35 +5,53 @@ ...@@ -5,35 +5,53 @@
<div class="body-content"> <div class="body-content">
<div class="page-header"> <div class="page-header">
<div class="pull-right" style="padding-top: 15px;"> <div class="pull-right" style="padding-top: 15px;">
<a title="Rename" href="#" class="btn btn-default btn-xs vm-details-rename-button"><i class="icon-pencil"></i></a>
<a title="Pause == sleep?" href="#" class="btn btn-default btn-xs"><i class="icon-pause"></i></a>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}"> <form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="sleep" value="dummy"/> <input type="hidden" name="sleep" />
<button title="{% trans "Sleep" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-moon"></i></button> <button title="{% trans "Sleep" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-moon"></i></button>
</form> </form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}"> <form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="deploy" value="dummy"/> <input type="hidden" name="deploy" />
<button title="{% trans "Deploy" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-play"></i></button> <button title="{% trans "Deploy" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-play"></i></button>
</form> </form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}"> <form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="wake_up" value="dummy"/> <input type="hidden" name="wake_up" />
<button title="{% trans "Wake up" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-sun"></i></button> <button title="{% trans "Wake up" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-sun"></i></button>
</form> </form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}"> <form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="shut_down" value="dummy"/> <input type="hidden" name="shut_down" />
<button title="{% trans "Shut down" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-off"></i></button> <button title="{% trans "Shut down" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-off"></i></button>
</form> </form>
<a title="Migrate" href="#" class="btn btn-default btn-xs"><i class="icon-truck"></i></a>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}"> <form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="save_as" value="dummy"/> <input type="hidden" name="reboot" />
<button title="{% trans "Reboot (ctrl + alt + del)" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-refresh"></i></button>
</form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %}
<input type="hidden" name="reset" />
<button title="{% trans "Reset (power cycle)" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-bolt"></i></button>
</form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %}
<input type="hidden" name="shut_off"/>
<button title="{% trans "Shut off" %}" class="btn btn-default btn-xs" type="submit">
<i class="icon-ban-circle"></i>
</button>
</form>
<a title="Migrate" data-vm-pk="{{ instance.pk }}" href="{% url "dashboard.views.vm-migrate" pk=instance.pk %}" class="btn btn-default btn-xs vm-migrate">
<i class="icon-truck"></i>
</a>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %}
<input type="hidden" name="save_as" />
<button title="{% trans "Save as template" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-save"></i></button> <button title="{% trans "Save as template" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-save"></i></button>
</form> </form>
<a title="{% trans "Destroy" %}" href="{% url "dashboard.views.delete-vm" pk=instance.pk %}" class="btn btn-default btn-xs vm-delete" data-vm-pk="{{ instance.pk }}"><i class="icon-remove"></i></a> <a title="{% trans "Destroy" %}" href="{% url "dashboard.views.delete-vm" pk=instance.pk %}" class="btn btn-default btn-xs vm-delete" data-vm-pk="{{ instance.pk }}"><i class="icon-remove"></i></a>
<a title="{% trans "Help" %}" href="#" class="btn btn-default btn-xs vm-details-help-button"><i class="icon-question"></i></a>
</div> </div>
<h1> <h1>
<div id="vm-details-rename"> <div id="vm-details-rename">
...@@ -46,7 +64,49 @@ ...@@ -46,7 +64,49 @@
<div id="vm-details-h1-name"> <div id="vm-details-h1-name">
{{ instance.name }} {{ instance.name }}
</div> </div>
<small>{{ instance.primary_host.get_fqdn }}</small></h1> <small>{{ instance.primary_host.get_fqdn }}</small>
</h1>
<div class="vm-details-help js-hidden">
<ul style="list-style: none;">
<li>
<strong>{% trans "Sleep" %}:</strong>
{% trans "Suspend virtual machine with memory dump." %}
</li>
<li>
<strong>{% trans "Wake up" %}:</strong>
{% trans "?" %}
</li>
<li>
<strong>{% trans "Shutdown" %}:</strong>
{% trans "Shutdown virtual machine with ACPI signal." %}
</li>
<li>
<strong>{% trans "Reboot (ctrl + alt + del)" %}:</strong>
{% trans "Reboot virtual machine with Ctrl+Alt+Del signal." %}
</li>
<li>
<strong>{% trans "Reset (power cycle)" %}:</strong>
{% trans "Reset virtual machine (reset button)" %}
</li>
<li>
<strong>{% trans "Shut off" %}:</strong>
{% trans "Shut off VM. (plug-out)" %}
</li>
<li>
<strong>{% trans "Migrate" %}:</strong>
{% trans "Live migrate running vm to another node." %}
</li>
<li>
<strong>{% trans "Save as template" %}:</strong>
{% trans "?" %}
</li>
<li>
<strong>{% trans "Destroy" %}:</strong>
{% trans "Remove virtual machine and its networks." %}
</li>
</ul>
</div>
<div style="clear: both;"></div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-4" id="vm-info-pane"> <div class="col-md-4" id="vm-info-pane">
...@@ -69,7 +129,7 @@ ...@@ -69,7 +129,7 @@
<dt>Password:</dt> <dt>Password:</dt>
<dd> <dd>
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control input-sm input-tags" value="{{ instance.pw }}"/> <input type="text" id="vm-details-pw-input" class="form-control input-sm input-tags" value="{{ instance.pw }}"/>
<span class="input-group-addon input-tags" id="vm-details-pw-show"> <span class="input-group-addon input-tags" id="vm-details-pw-show">
<i class="icon-eye-open" id="vm-details-pw-eye" title="Show password"></i> <i class="icon-eye-open" id="vm-details-pw-eye" title="Show password"></i>
</span> </span>
...@@ -102,8 +162,11 @@ ...@@ -102,8 +162,11 @@
<i class="icon-tasks icon-2x"></i><br> <i class="icon-tasks icon-2x"></i><br>
{% trans "Resources" %}</a> {% trans "Resources" %}</a>
</li> </li>
<li {% if instance.state != "RUNNING" %}class="disabled"{% endif %}> <li {% if not instance.is_console_available %}class="disabled">
<a href="#{% if instance.state == "RUNNING" %}console" data-toggle="pill{% endif %}" data-target="#_console" class="text-center"> <a href="#" data-toggle="pill_" data-target="#_console" class="text-center">
{% else %}>
<a href="#console" data-toggle="pill" data-target="#_console" class="text-center">
{% endif %}
<i class="icon-desktop icon-2x"></i><br> <i class="icon-desktop icon-2x"></i><br>
{% trans "Console" %}</a></li> {% trans "Console" %}</a></li>
<li> <li>
...@@ -137,3 +200,9 @@ ...@@ -137,3 +200,9 @@
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %}
<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>
{% endblock %}
...@@ -4,13 +4,16 @@ ...@@ -4,13 +4,16 @@
<span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}"> <span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}">
<i class="{% if not a.finished %} icon-refresh icon-spin {% else %}icon-plus{% endif %}"></i> <i class="{% if not a.finished %} icon-refresh icon-spin {% else %}icon-plus{% endif %}"></i>
</span> </span>
<strong>{{ a.get_readable_name }}</strong> <strong{% if user.is_superuser and a.result %} title="{{ a.result }}"{% endif %}>
{{ a.started|date:"Y-m-d H:i" }}, {{ a.user }} {{ a.get_readable_name }}
</strong>
{{ a.started|date:"Y-m-d H:i" }}{% if a.user %}, {{ a.user }}{% endif %}
{% if a.children.count > 0 %} {% if a.children.count > 0 %}
<div class="sub-timeline"> <div class="sub-timeline">
{% for s in a.children.all %} {% for s in a.children.all %}
<div data-activity-id="{{ s.pk }}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}"> <div data-activity-id="{{ s.pk }}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}">
{{ s.get_readable_name }} - <span{% if user.is_superuser and s.result %} title="{{ s.result }}"{% endif %}>
{{ s.get_readable_name }}</span> &ndash;
{% if s.finished %} {% if s.finished %}
{{ s.finished|time:"H:i:s" }} {{ s.finished|time:"H:i:s" }}
{% else %} {% else %}
......
{% load i18n %} {% load i18n %}
<h3>{% trans "Owner" %}</h3> <h3>{% trans "Owner" %}</h3>
<p> <p>
{% if user == instance.owner %}
{% blocktrans %}You are the current owner of this instance.{% endblocktrans %} {% blocktrans %}You are the current owner of this instance.{% endblocktrans %}
<a href="#" class="btn btn-link">{% trans "Transfer ownership..." %}</a> {% else %}
{% blocktrans with owner=instance.owner %}
The current owner of this instance is {{owner}}.
{% endblocktrans %}
{% endif %}
{% if user == instance.owner or user.is_superuser %}
<a href="{% url "dashboard.views.vm-transfer-ownership" instance.pk %}"
class="btn btn-link">{% trans "Transfer ownership..." %}</a>
{% endif %}
</p> </p>
<h3>{% trans "Permissions"|capfirst %}</h3> <h3>{% trans "Permissions"|capfirst %}</h3>
<form action="{{acl.url}}" method="post">{% csrf_token %} <form action="{{acl.url}}" method="post">{% csrf_token %}
......
...@@ -5,7 +5,3 @@ ...@@ -5,7 +5,3 @@
<div id="activity-timeline" class="timeline"> <div id="activity-timeline" class="timeline">
{% include "dashboard/vm-detail/_activity-timeline.html" %} {% include "dashboard/vm-detail/_activity-timeline.html" %}
</div> </div>
{% block extra_js %}
<script src="{{ STATIC_URL }}dashboard/vm-details.js"></script>
{% endblock %}
<div class="btn-toolbar"> <div class="btn-toolbar">
<button id="sendCtrlAltDelButton" class="btn btn-danger small" href="#">Send CtrlAltDel</button> <button id="sendCtrlAltDelButton" class="btn btn-danger small">Send CtrlAltDel</button>
<button id="sendPasswordButton" class="btn btn-default small" href="#">Type password</button> <button id="sendPasswordButton" class="btn btn-default small">Type password</button>
</div> </div>
<div class="alert alert-info" id="noVNC_status"> <div class="alert alert-info" id="noVNC_status">
</div> </div>
...@@ -10,68 +10,6 @@ ...@@ -10,68 +10,6 @@
<script src="{{ STATIC_URL }}dashboard/novnc/util.js"></script> <script src="{{ STATIC_URL }}dashboard/novnc/util.js"></script>
<script> <script>
"use strict"; var INCLUDE_URI = '{{ STATIC_URL }}dashboard/novnc/';
var VNC_URL = "{{ vnc_url }}";
var INCLUDE_URI = '{{ STATIC_URL }}dashboard/novnc/'; </script>
Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js",
"input.js", "display.js", "jsunzip.js", "rfb.js"]);
var rfb;
function updateState(rfb, state, oldstate, msg) {
var s, sb, cad
s = $('#noVNC_status')[0];
cad = $('#sendCtrlAltDelButton')[0];
if (state === "normal") { cad.disabled = false; }
else { cad.disabled = true; }
if (typeof(msg) !== 'undefined') {
s.innerHTML = msg;
}
}
$('a[data-toggle$="pill"][href!="#console"]').click(function() {
if (rfb) {
rfb.disconnect();
rfb = 0;
}
$("#vm-info-pane").fadeIn();
$("#vm-detail-pane").removeClass("col-md-12");
});
$('#sendCtrlAltDelButton').click(function() {
rfb.sendCtrlAltDel(); return false;});
$('#sendPasswordButton').click(function() {
var pw = '{{instance.pw}}';
for (var i=0; i < pw.length; i++) {
rfb.sendKey(pw.charCodeAt(i));
} return false;});
$('a[href$="console"]').click(function() {
var host, port, password, path;
$("#vm-info-pane").hide();
$("#vm-detail-pane").addClass("col-md-12");
WebUtil.init_logging('warn');
host = window.location.hostname;
if (window.location.port == 8080) {
port = 9999;
} else {
port = window.location.port == "" ? "443" : window.location.port;
}
password = '';
path = 'vnc/?d={{ vnc_url }}';
rfb = new RFB({'target': $D('noVNC_canvas'),
'encrypt': (window.location.protocol === "https:"),
'true_color': true,
'local_cursor': true,
'shared': true,
'view_only': false,
'updateState': updateState});
rfb.connect(host, port, password, path);
});
</script>
...@@ -8,6 +8,16 @@ ...@@ -8,6 +8,16 @@
<dd><small>{{ instance.description }}</small></dd> <dd><small>{{ instance.description }}</small></dd>
</dl> </dl>
<h4>{% trans "Expiration" %} {% if instance.is_expiring %}<i class="icon-warning-sign text-danger"></i>{% endif %}
<a href="{% url "dashboard.views.vm-renew" instance.pk "" %}" class="btn btn-success btn-xs pull-right">{% trans "renew" %}</a>
</h4>
<dl>
<dt>{% trans "Suspended at:" %}</dt>
<dd><i class="icon-moon"></i> {{ instance.time_of_suspend|timeuntil }}</dd>
<dt>{% trans "Destroyed at:" %}</dt>
<dd><i class="icon-remove"></i> {{ instance.time_of_delete|timeuntil }}</dd>
</dl>
<div style="font-weight: bold;">{% trans "Tags" %}</div> <div style="font-weight: bold;">{% trans "Tags" %}</div>
<div id="vm-details-tags" style="margin-bottom: 20px;"> <div id="vm-details-tags" style="margin-bottom: 20px;">
<div id="vm-details-tags-list"> <div id="vm-details-tags-list">
...@@ -37,8 +47,10 @@ ...@@ -37,8 +47,10 @@
</div><!-- id:vm-details-tags --> </div><!-- id:vm-details-tags -->
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{% if graphite_enabled %}
<img src="{% url "dashboard.views.vm-graph" instance.pk "cpu" "6h" %}" style="width:100%"/> <img src="{% url "dashboard.views.vm-graph" instance.pk "cpu" "6h" %}" style="width:100%"/>
<img src="{% url "dashboard.views.vm-graph" instance.pk "memory" "6h" %}" style="width:100%"/> <img src="{% url "dashboard.views.vm-graph" instance.pk "memory" "6h" %}" style="width:100%"/>
<img src="{% url "dashboard.views.vm-graph" instance.pk "network" "6h" %}" style="width:100%"/> <img src="{% url "dashboard.views.vm-graph" instance.pk "network" "6h" %}" style="width:100%"/>
{% endif %}
</div> </div>
</div> </div>
...@@ -52,12 +52,15 @@ ...@@ -52,12 +52,15 @@
</a> </a>
</div> </div>
</h3> </h3>
<div class="row" id="vm-details-disk-add-for-form">
</div> <div class="row" id="vm-details-disk-add-for-form"></div>
{% if not instance.disks.all %}
{% trans "No disks are added!" %}
{% endif %}
{% for d in instance.disks.all %} {% for d in instance.disks.all %}
<h4 class="list-group-item-heading dashboard-vm-details-network-h3"> <h4 class="list-group-item-heading dashboard-vm-details-network-h3">
<i class="icon-file"></i> {{ d.name }} (#{{ d.id }}) - {{ d.size|filesize }} {% include "dashboard/_disk-list-element.html" %}
</h4> </h4>
{% endfor %} {% endfor %}
</div> </div>
...@@ -67,7 +70,7 @@ ...@@ -67,7 +70,7 @@
<div class="col-md-12"> <div class="col-md-12">
<div> <div>
<hr /> <hr />
<form method="POST" action="" style="max-width: 300px;"> <form method="POST" action="{% url "dashboard.views.disk-add" %}" style="max-width: 350px;">
{% crispy forms.disk_add_form %} {% crispy forms.disk_add_form %}
</form> </form>
<hr /> <hr />
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
<div class="pull-right"> <div class="pull-right">
<form action="" method="POST"> <form action="" method="POST">
{% csrf_token %} {% csrf_token %}
E-mail address or identifier of user:
<input name="name"> <input name="name">
<input type="submit"> <input type="submit">
</form> </form>
......
...@@ -72,5 +72,6 @@ ...@@ -72,5 +72,6 @@
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
<script src="{{ STATIC_URL}}dashboard/vm-list.js"></script> <script src="{{ STATIC_URL}}dashboard/vm-list.js"></script>
<script src="{{ STATIC_URL}}dashboard/vm-common.js"></script>
{% endblock %} {% endblock %}
<a href="{% url "dashboard.views.vm-migrate" pk=record.pk %}" class="btn btn-default btn-xs vm-migrate" data-vm-pk="{{ record.pk }}" title data-original-title="Migrate">
<a class="btn btn-default btn-xs" title data-original-title="Migrate">
<i class="icon-truck"></i> <i class="icon-truck"></i>
</a> </a>
<a id="vm-list-rename-button" class="btn btn-default btn-xs" title data-original-title="Rename"> <a id="vm-list-rename-button" class="btn btn-default btn-xs" title data-original-title="Rename">
......
from django.contrib.auth.models import User
from django.test import TestCase
from ..models import Profile
from ..views import search_user
class NotificationTestCase(TestCase):
def setUp(self):
self.u1 = User.objects.create(username='user1')
Profile.objects.get_or_create(user=self.u1)
self.u2 = User.objects.create(username='user2')
Profile.objects.get_or_create(user=self.u2)
def test_notification_send(self):
c1 = self.u1.notification_set.count()
c2 = self.u2.notification_set.count()
profile = self.u1.profile
msg = profile.notify('subj',
'dashboard/test_message.txt',
{'var': 'testme'})
assert self.u1.notification_set.count() == c1 + 1
assert self.u2.notification_set.count() == c2
assert 'user1' in msg.message
assert 'testme' in msg.message
assert msg in self.u1.notification_set.all()
class ProfileTestCase(TestCase):
def setUp(self):
self.u1 = User.objects.create(username='user1')
Profile.objects.get_or_create(user=self.u1)
self.u2 = User.objects.create(username='user2',
email='john@example.org')
Profile.objects.get_or_create(user=self.u2, org_id='apple')
def test_search_user_by_name(self):
self.assertEqual(search_user('user1'), self.u1)
self.assertEqual(search_user('user2'), self.u2)
def test_search_user_by_mail(self):
self.assertEqual(search_user('john@example.org'), self.u2)
def test_search_user_by_orgid(self):
self.assertEqual(search_user('apple'), self.u2)
def test_search_user_nonexist(self):
with self.assertRaises(User.DoesNotExist):
search_user('no-such-user')
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.core.exceptions import SuspiciousOperation
from django.core.urlresolvers import reverse
from vm.models import Instance, InstanceTemplate, Lease from vm.models import Instance, InstanceTemplate, Lease, Node
from ..models import Profile
from ..views import VmRenewView
from storage.models import Disk from storage.models import Disk
from firewall.models import Vlan from firewall.models import Vlan
from mock import Mock
class VmDetailTest(TestCase): class LoginMixin(object):
fixtures = ['test-vm-fixture.json'] def login(self, client, username, password='password'):
response = client.post('/accounts/login/', {'username': username,
'password': password})
self.assertNotEqual(response.status_code, 403)
class VmDetailTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json', 'node.json']
def setUp(self): def setUp(self):
Instance.get_remote_queue_name = Mock(return_value='test')
self.u1 = User.objects.create(username='user1') self.u1 = User.objects.create(username='user1')
self.u1.set_password('password') self.u1.set_password('password')
self.u1.save() self.u1.save()
...@@ -32,11 +45,6 @@ class VmDetailTest(TestCase): ...@@ -32,11 +45,6 @@ class VmDetailTest(TestCase):
self.us.delete() self.us.delete()
self.g1.delete() self.g1.delete()
def login(self, client, username, password='password'):
response = client.post('/accounts/login/', {'username': username,
'password': password})
self.assertNotEqual(response.status_code, 403)
def test_404_vm_page(self): def test_404_vm_page(self):
c = Client() c = Client()
self.login(c, 'user1') self.login(c, 'user1')
...@@ -107,6 +115,19 @@ class VmDetailTest(TestCase): ...@@ -107,6 +115,19 @@ class VmDetailTest(TestCase):
response = c.post('/dashboard/vm/mass-delete/', {'vms': [1]}) response = c.post('/dashboard/vm/mass-delete/', {'vms': [1]})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
def test_permitted_password_change(self):
c = Client()
self.login(c, "user2")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner')
inst.node = Node.objects.all()[0]
inst.save()
password = inst.pw
response = c.post("/dashboard/vm/1/", {'change_password': True})
self.assertTrue(Instance.get_remote_queue_name.called)
self.assertEqual(response.status_code, 302)
self.assertNotEqual(password, Instance.objects.get(pk=1).pw)
def test_unpermitted_password_change(self): def test_unpermitted_password_change(self):
c = Client() c = Client()
self.login(c, "user2") self.login(c, "user2")
...@@ -186,6 +207,28 @@ class VmDetailTest(TestCase): ...@@ -186,6 +207,28 @@ class VmDetailTest(TestCase):
response = c.post('/dashboard/template/1/', {}) response = c.post('/dashboard/template/1/', {})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_edit_unpermitted_template_raw_data(self):
c = Client()
self.login(c, 'user1')
tmpl = InstanceTemplate.objects.get(id=1)
tmpl.set_level(self.u1, 'owner')
tmpl.disks.get().set_level(self.u1, 'owner')
kwargs = tmpl.__dict__.copy()
kwargs.update(name='t1', lease=1, disks=1, raw_data='tst1')
response = c.post('/dashboard/template/1/', kwargs)
self.assertEqual(response.status_code, 302)
self.assertEqual(InstanceTemplate.objects.get(id=1).raw_data,
tmpl.raw_data)
def test_edit_permitted_template_raw_data(self):
c = Client()
self.login(c, 'superuser')
kwargs = InstanceTemplate.objects.get(id=1).__dict__.copy()
kwargs.update(name='t2', lease=1, disks=1, raw_data='tst2')
response = c.post('/dashboard/template/1/', kwargs)
self.assertEqual(response.status_code, 302)
self.assertEqual(InstanceTemplate.objects.get(id=1).raw_data, 'tst2')
def test_permitted_lease_delete(self): def test_permitted_lease_delete(self):
c = Client() c = Client()
self.login(c, 'superuser') self.login(c, 'superuser')
...@@ -209,8 +252,12 @@ class VmDetailTest(TestCase): ...@@ -209,8 +252,12 @@ class VmDetailTest(TestCase):
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner') inst.set_level(self.u1, 'owner')
disks = inst.disks.count() disks = inst.disks.count()
response = c.post("/dashboard/vm/1/", {'disk-name': "a", response = c.post("/dashboard/disk/add/", {
'disk-size': 1}) 'disk-name': "a",
'disk-size': 1,
'disk-is_template': 0,
'disk-object_pk': 1,
})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
self.assertEqual(disks, inst.disks.count()) self.assertEqual(disks, inst.disks.count())
...@@ -220,7 +267,229 @@ class VmDetailTest(TestCase): ...@@ -220,7 +267,229 @@ class VmDetailTest(TestCase):
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner') inst.set_level(self.u1, 'owner')
disks = inst.disks.count() disks = inst.disks.count()
response = c.post("/dashboard/vm/1/", {'disk-name': "a", response = c.post("/dashboard/disk/add/", {
'disk-size': 1}) 'disk-name': "a",
'disk-size': 1,
'disk-is_template': 0,
'disk-object_pk': 1,
})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(disks + 1, inst.disks.count()) self.assertEqual(disks + 1, inst.disks.count())
def test_notification_read(self):
c = Client()
self.login(c, "user1")
self.u1.profile.notify('subj', 'dashboard/test_message.txt',
{'var': 'testme'})
assert self.u1.notification_set.get().status == 'new'
response = c.get("/dashboard/notifications/")
self.assertEqual(response.status_code, 200)
assert self.u1.notification_set.get().status == 'read'
def test_unpermitted_activity_get(self):
c = Client()
self.login(c, "user2")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner')
response = c.get("/dashboard/vm/1/activity/")
self.assertEqual(response.status_code, 403)
def test_permitted_activity_get(self):
c = Client()
self.login(c, "user1")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner')
response = c.get("/dashboard/vm/1/activity/")
self.assertEqual(response.status_code, 200)
class VmDetailVncTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json', 'node.json']
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u1.set_password('password')
self.u1.save()
def test_permitted_vm_console(self):
c = Client()
self.login(c, 'user1')
inst = Instance.objects.get(pk=1)
inst.node = Node.objects.all()[0]
inst.save()
inst.set_level(self.u1, 'operator')
response = c.get('/dashboard/vm/1/vnctoken/')
self.assertEqual(response.status_code, 200)
def test_not_permitted_vm_console(self):
c = Client()
self.login(c, 'user1')
inst = Instance.objects.get(pk=1)
inst.node = Node.objects.all()[0]
inst.save()
inst.set_level(self.u1, 'user')
response = c.get('/dashboard/vm/1/vnctoken/')
self.assertEqual(response.status_code, 403)
class TransferOwnershipViewTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json']
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u1.set_password('password')
self.u1.save()
Profile.objects.create(user=self.u1)
self.u2 = User.objects.create(username='user2', is_staff=True)
self.u2.set_password('password')
self.u2.save()
Profile.objects.create(user=self.u2)
self.us = User.objects.create(username='superuser', is_superuser=True)
self.us.set_password('password')
self.us.save()
Profile.objects.create(user=self.us)
inst = Instance.objects.get(pk=1)
inst.owner = self.u1
inst.save()
def test_non_owner_offer(self):
c2 = self.u2.notification_set.count()
c = Client()
self.login(c, 'user2')
with self.assertRaises(SuspiciousOperation):
c.post('/dashboard/vm/1/tx/')
self.assertEqual(self.u2.notification_set.count(), c2)
def test_owned_offer(self):
c2 = self.u2.notification_set.count()
c = Client()
self.login(c, 'user1')
response = c.get('/dashboard/vm/1/tx/')
assert response.status_code == 200
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
self.assertEqual(self.u2.notification_set.count(), c2 + 1)
def test_transfer(self):
c = Client()
self.login(c, 'user1')
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
url = response.context['token']
c = Client()
self.login(c, 'user2')
response = c.post(url)
self.assertEquals(Instance.objects.get(pk=1).owner.pk, self.u2.pk)
def test_transfer_token_used_by_others(self):
c = Client()
self.login(c, 'user1')
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
url = response.context['token']
response = c.post(url) # token is for user2
assert response.status_code == 403
self.assertEquals(Instance.objects.get(pk=1).owner.pk, self.u1.pk)
def test_transfer_by_superuser(self):
c = Client()
self.login(c, 'superuser')
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
url = response.context['token']
c = Client()
self.login(c, 'user2')
response = c.post(url)
self.assertEquals(Instance.objects.get(pk=1).owner.pk, self.u2.pk)
class RenewViewTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json']
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u1.set_password('password')
self.u1.save()
Profile.objects.create(user=self.u1)
self.u2 = User.objects.create(username='user2', is_staff=True)
self.u2.set_password('password')
self.u2.save()
Profile.objects.create(user=self.u2)
self.us = User.objects.create(username='superuser', is_superuser=True)
self.us.set_password('password')
self.us.save()
Profile.objects.create(user=self.us)
inst = Instance.objects.get(pk=1)
inst.owner = self.u1
inst.save()
def test_renew_by_owner(self):
c = Client()
ct = Instance.objects.get(pk=1).activity_log.\
filter(activity_code__endswith='renew').count()
self.login(c, 'user1')
response = c.get('/dashboard/vm/1/renew/')
self.assertEquals(response.status_code, 200)
response = c.post('/dashboard/vm/1/renew/')
self.assertEquals(response.status_code, 302)
ct2 = Instance.objects.get(pk=1).activity_log.\
filter(activity_code__endswith='renew').count()
self.assertEquals(ct + 1, ct2)
def test_renew_get_by_nonowner_wo_key(self):
c = Client()
self.login(c, 'user2')
response = c.get('/dashboard/vm/1/renew/')
self.assertEquals(response.status_code, 403)
def test_renew_post_by_nonowner_wo_key(self):
c = Client()
self.login(c, 'user2')
response = c.post('/dashboard/vm/1/renew/')
self.assertEquals(response.status_code, 403)
def test_renew_get_by_nonowner_w_key(self):
key = VmRenewView.get_token_url(Instance.objects.get(pk=1), self.u2)
c = Client()
response = c.get(key)
self.assertEquals(response.status_code, 200)
def test_renew_post_by_anon_w_key(self):
key = VmRenewView.get_token_url(Instance.objects.get(pk=1), self.u2)
ct = Instance.objects.get(pk=1).activity_log.\
filter(activity_code__endswith='renew').count()
c = Client()
response = c.post(key)
self.assertEquals(response.status_code, 302)
ct2 = Instance.objects.get(pk=1).activity_log.\
filter(activity_code__endswith='renew').count()
self.assertEquals(ct + 1, ct2)
def test_renew_post_by_anon_w_invalid_key(self):
class Mockinst(object):
pk = 2
key = VmRenewView.get_token_url(Mockinst(), self.u2)
ct = Instance.objects.get(pk=1).activity_log.\
filter(activity_code__endswith='renew').count()
c = Client()
self.login(c, 'user2')
response = c.get(key)
self.assertEquals(response.status_code, 404)
response = c.post(key)
self.assertEquals(response.status_code, 404)
ct2 = Instance.objects.get(pk=1).activity_log.\
filter(activity_code__endswith='renew').count()
self.assertEquals(ct, ct2)
def test_renew_post_by_anon_w_expired_key(self):
key = reverse(VmRenewView.url_name, args=(
12, 'WzEyLDFd:1WLbSi:2zIb8SUNAIRIOMTmSmKSSit2gpY'))
ct = Instance.objects.get(pk=12).activity_log.\
filter(activity_code__endswith='renew').count()
c = Client()
self.login(c, 'user2')
response = c.get(key)
self.assertEquals(response.status_code, 302)
response = c.post(key)
self.assertEquals(response.status_code, 403)
ct2 = Instance.objects.get(pk=12).activity_log.\
filter(activity_code__endswith='renew').count()
self.assertEquals(ct, ct2)
...@@ -2,13 +2,15 @@ from django.conf.urls import patterns, url ...@@ -2,13 +2,15 @@ from django.conf.urls import patterns, url
from vm.models import Instance from vm.models import Instance
from .views import ( from .views import (
IndexView, VmDetailView, VmList, VmCreate, TemplateDetail, AclUpdateView, AclUpdateView, DiskAddView, FavouriteView, GroupAclUpdateView, GroupDelete,
VmDelete, VmMassDelete, vm_activity, NodeList, NodeDetailView, PortDelete, GroupDetailView, GroupList, GroupUserDelete, IndexView, LeaseCreate,
TransferOwnershipView, TransferOwnershipConfirmView, NodeDelete, LeaseDelete, LeaseDetail, NodeAddTraitView, NodeCreate, NodeDelete,
TemplateList, LeaseDetail, NodeCreate, LeaseCreate, TemplateCreate, NodeDetailView, NodeGraphView, NodeList, NodeStatus, NotificationView,
FavouriteView, NodeStatus, GroupList, TemplateDelete, LeaseDelete, PortDelete, TemplateAclUpdateView, TemplateCreate, TemplateDelete,
VmGraphView, TemplateAclUpdateView, GroupDetailView, GroupDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
GroupAclUpdateView, GroupUserDelete, NodeAddTraitView, TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmMigrateView,
VmRenewView,
) )
urlpatterns = patterns( urlpatterns = patterns(
...@@ -36,6 +38,8 @@ urlpatterns = patterns( ...@@ -36,6 +38,8 @@ urlpatterns = patterns(
name='dashboard.views.remove-port'), name='dashboard.views.remove-port'),
url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(), url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(),
name='dashboard.views.detail'), name='dashboard.views.detail'),
url(r'^vm/(?P<pk>\d+)/vnctoken/$', VmDetailVncTokenView.as_view(),
name='dashboard.views.detail-vnc'),
url(r'^vm/(?P<pk>\d+)/acl/$', AclUpdateView.as_view(model=Instance), url(r'^vm/(?P<pk>\d+)/acl/$', AclUpdateView.as_view(model=Instance),
name='dashboard.views.vm-acl'), name='dashboard.views.vm-acl'),
url(r'^vm/(?P<pk>\d+)/tx/$', TransferOwnershipView.as_view(), url(r'^vm/(?P<pk>\d+)/tx/$', TransferOwnershipView.as_view(),
...@@ -48,13 +52,17 @@ urlpatterns = patterns( ...@@ -48,13 +52,17 @@ urlpatterns = patterns(
url(r'^vm/mass-delete/', VmMassDelete.as_view(), url(r'^vm/mass-delete/', VmMassDelete.as_view(),
name='dashboard.view.mass-delete-vm'), name='dashboard.view.mass-delete-vm'),
url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity), url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity),
url(r'^vm/(?P<pk>\d+)/migrate/$', VmMigrateView.as_view(),
name='dashboard.views.vm-migrate'),
url(r'^vm/(?P<pk>\d+)/renew/((?P<key>.*)/?)$', VmRenewView.as_view(),
name='dashboard.views.vm-renew'),
url(r'^node/list/$', NodeList.as_view(), name='dashboard.views.node-list'), url(r'^node/list/$', NodeList.as_view(), name='dashboard.views.node-list'),
url(r'^node/(?P<pk>\d+)/$', NodeDetailView.as_view(), url(r'^node/(?P<pk>\d+)/$', NodeDetailView.as_view(),
name='dashboard.views.node-detail'), name='dashboard.views.node-detail'),
url(r'^node/(?P<pk>\d+)/add-trait/$', NodeAddTraitView.as_view(), url(r'^node/(?P<pk>\d+)/add-trait/$', NodeAddTraitView.as_view(),
name='dashboard.views.node-addtrait'), name='dashboard.views.node-addtrait'),
url(r'^tx/$', TransferOwnershipConfirmView.as_view(), url(r'^tx/(?P<key>.*)/?$', TransferOwnershipConfirmView.as_view(),
name='dashboard.views.vm-transfer-ownership-confirm'), name='dashboard.views.vm-transfer-ownership-confirm'),
url(r'^node/delete/(?P<pk>\d+)/$', NodeDelete.as_view(), url(r'^node/delete/(?P<pk>\d+)/$', NodeDelete.as_view(),
name="dashboard.views.delete-node"), name="dashboard.views.delete-node"),
...@@ -73,10 +81,20 @@ urlpatterns = patterns( ...@@ -73,10 +81,20 @@ urlpatterns = patterns(
r'(?P<time>[0-9]{1,2}[hdwy])$'), r'(?P<time>[0-9]{1,2}[hdwy])$'),
VmGraphView.as_view(), VmGraphView.as_view(),
name='dashboard.views.vm-graph'), name='dashboard.views.vm-graph'),
url((r'^node/(?P<pk>\d+)/graph/(?P<metric>cpu|memory|network)/'
r'(?P<time>[0-9]{1,2}[hdwy])$'),
NodeGraphView.as_view(),
name='dashboard.views.node-graph'),
url(r'^group/(?P<pk>\d+)/$', GroupDetailView.as_view(), url(r'^group/(?P<pk>\d+)/$', GroupDetailView.as_view(),
name='dashboard.views.group-detail'), name='dashboard.views.group-detail'),
url(r'^group/(?P<pk>\d+)/acl/$', GroupAclUpdateView.as_view(), url(r'^group/(?P<pk>\d+)/acl/$', GroupAclUpdateView.as_view(),
name='dashboard.views.group-acl'), name='dashboard.views.group-acl'),
url(r'^groupuser/delete/(?P<pk>\d+)/$', GroupUserDelete.as_view(), url(r'^groupuser/delete/(?P<pk>\d+)/$', GroupUserDelete.as_view(),
name="dashboard.views.delete-groupuser"), name="dashboard.views.delete-groupuser"),
url(r'^notifications/$', NotificationView.as_view(),
name="dashboard.views.notifications"),
url(r'^disk/add/$', DiskAddView.as_view(),
name="dashboard.views.disk-add"),
) )
...@@ -5,7 +5,9 @@ import re ...@@ -5,7 +5,9 @@ import re
from datetime import datetime from datetime import datetime
import requests import requests
from django.conf import settings
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.contrib.auth.views import login, redirect_to_login
from django.contrib.messages import warning from django.contrib.messages import warning
from django.core.exceptions import ( from django.core.exceptions import (
PermissionDenied, SuspiciousOperation, PermissionDenied, SuspiciousOperation,
...@@ -13,36 +15,49 @@ from django.core.exceptions import ( ...@@ -13,36 +15,49 @@ from django.core.exceptions import (
from django.core import signing from django.core import signing
from django.core.urlresolvers import reverse, reverse_lazy from django.core.urlresolvers import reverse, reverse_lazy
from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.shortcuts import redirect, render from django.shortcuts import redirect, render, get_object_or_404
from django.views.decorators.http import require_GET from django.views.decorators.http import require_GET
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from django.views.generic import (TemplateView, DetailView, View, DeleteView, from django.views.generic import (TemplateView, DetailView, View, DeleteView,
UpdateView, CreateView) UpdateView, CreateView)
from django.contrib import messages from django.contrib import messages
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.template.defaultfilters import title from django.template.defaultfilters import title as title_filter
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.template import RequestContext
from django.forms.models import inlineformset_factory from django.forms.models import inlineformset_factory
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from braces.views import LoginRequiredMixin, SuperuserRequiredMixin from braces.views import (
LoginRequiredMixin, SuperuserRequiredMixin, AccessMixin
)
from .forms import ( from .forms import (
VmCreateForm, TemplateForm, LeaseForm, NodeForm, HostForm, DiskAddForm, CircleAuthenticationForm, DiskAddForm, HostForm, LeaseForm, NodeForm,
TraitForm, TemplateForm, TraitForm, VmCustomizeForm,
) )
from .tables import (VmListTable, NodeListTable, NodeVmListTable, from .tables import (VmListTable, NodeListTable, NodeVmListTable,
TemplateListTable, LeaseListTable, GroupListTable,) TemplateListTable, LeaseListTable, GroupListTable,)
from vm.models import (Instance, InstanceTemplate, InterfaceTemplate, from vm.models import (
InstanceActivity, Node, Trait, instance_activity, Lease, Instance, instance_activity, InstanceActivity, InstanceTemplate, Interface,
Interface, NodeActivity, ) InterfaceTemplate, Lease, Node, NodeActivity, Trait,
)
from firewall.models import Vlan, Host, Rule from firewall.models import Vlan, Host, Rule
from storage.models import Disk from dashboard.models import Favourite, Profile
from dashboard.models import Favourite
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def search_user(keyword):
try:
return User.objects.get(username=keyword)
except User.DoesNotExist:
try:
return User.objects.get(profile__org_id=keyword)
except User.DoesNotExist:
return User.objects.get(email=keyword)
# github.com/django/django/blob/stable/1.6.x/django/contrib/messages/views.py # github.com/django/django/blob/stable/1.6.x/django/contrib/messages/views.py
class SuccessMessageMixin(object): class SuccessMessageMixin(object):
""" """
...@@ -82,6 +97,10 @@ class IndexView(LoginRequiredMixin, TemplateView): ...@@ -82,6 +97,10 @@ class IndexView(LoginRequiredMixin, TemplateView):
'more_instances': instances.count() - len(instances[:5]) 'more_instances': instances.count() - len(instances[:5])
}) })
if user is not None:
context['new_notifications'] = user.notification_set.filter(
status="new").count()
nodes = Node.objects.all() nodes = Node.objects.all()
groups = Group.objects.all() groups = Group.objects.all()
context.update({ context.update({
...@@ -145,6 +164,25 @@ class CheckedDetailView(LoginRequiredMixin, DetailView): ...@@ -145,6 +164,25 @@ class CheckedDetailView(LoginRequiredMixin, DetailView):
return context return context
class VmDetailVncTokenView(CheckedDetailView):
template_name = "dashboard/vm-detail.html"
model = Instance
def get(self, request, **kwargs):
self.object = self.get_object()
if not self.object.has_level(request.user, 'operator'):
raise PermissionDenied()
if self.object.node:
port = self.object.vnc_port
host = str(self.object.node.host.ipv4)
value = signing.dumps({'host': host,
'port': port},
key=getenv("PROXY_SECRET", 'asdasd')),
return HttpResponse('vnc/?d=%s' % value)
else:
raise Http404()
class VmDetailView(CheckedDetailView): class VmDetailView(CheckedDetailView):
template_name = "dashboard/vm-detail.html" template_name = "dashboard/vm-detail.html"
model = Instance model = Instance
...@@ -152,15 +190,11 @@ class VmDetailView(CheckedDetailView): ...@@ -152,15 +190,11 @@ class VmDetailView(CheckedDetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(VmDetailView, self).get_context_data(**kwargs) context = super(VmDetailView, self).get_context_data(**kwargs)
instance = context['instance'] instance = context['instance']
if instance.node: context.update({
port = instance.vnc_port 'graphite_enabled': VmGraphView.get_graphite_url() is not None,
host = str(instance.node.host.ipv4) 'vnc_url': reverse_lazy("dashboard.views.detail-vnc",
value = signing.dumps({'host': host, kwargs={'pk': self.object.pk})
'port': port}, })
key=getenv("PROXY_SECRET", 'asdasd')),
context.update({
'vnc_url': '%s' % value
})
# activity data # activity data
ia = InstanceActivity.objects.filter( ia = InstanceActivity.objects.filter(
...@@ -176,7 +210,10 @@ class VmDetailView(CheckedDetailView): ...@@ -176,7 +210,10 @@ class VmDetailView(CheckedDetailView):
).all() ).all()
context['acl'] = get_vm_acl_data(instance) context['acl'] = get_vm_acl_data(instance)
context['forms'] = { context['forms'] = {
'disk_add_form': DiskAddForm(prefix="disk"), 'disk_add_form': DiskAddForm(
user=self.request.user,
is_template=False, object_pk=self.get_object().pk,
prefix="disk"),
} }
context['os_type_icon'] = instance.os_type.replace("unknown", context['os_type_icon'] = instance.os_type.replace("unknown",
"question") "question")
...@@ -195,13 +232,13 @@ class VmDetailView(CheckedDetailView): ...@@ -195,13 +232,13 @@ class VmDetailView(CheckedDetailView):
'port': self.__add_port, 'port': self.__add_port,
'new_network_vlan': self.__new_network, 'new_network_vlan': self.__new_network,
'save_as': self.__save_as, 'save_as': self.__save_as,
'disk-name': self.__add_disk,
'shut_down': self.__shut_down, 'shut_down': self.__shut_down,
'sleep': self.__sleep, 'sleep': self.__sleep,
'wake_up': self.__wake_up, 'wake_up': self.__wake_up,
'deploy': self.__deploy, 'deploy': self.__deploy,
'reset': self.__reset, 'reset': self.__reset,
'reboot': self.__reboot, 'reboot': self.__reboot,
'shut_off': self.__shut_off,
} }
for k, v in options.iteritems(): for k, v in options.iteritems():
if request.POST.get(k) is not None: if request.POST.get(k) is not None:
...@@ -370,24 +407,6 @@ class VmDetailView(CheckedDetailView): ...@@ -370,24 +407,6 @@ class VmDetailView(CheckedDetailView):
return redirect(reverse_lazy("dashboard.views.template-detail", return redirect(reverse_lazy("dashboard.views.template-detail",
kwargs={'pk': template.pk})) kwargs={'pk': template.pk}))
def __add_disk(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
form = DiskAddForm(request.POST, prefix="disk")
if form.is_valid():
messages.success(request, _("New disk successfully created!"))
form.save(self.object)
else:
error = "<br /> ".join(["<strong>%s</strong>: %s" %
(title(i[0]), i[1][0])
for i in form.errors.items()])
messages.error(request, error)
return redirect("%s#resources" % reverse_lazy(
"dashboard.views.detail", kwargs={'pk': self.object.pk}))
def __shut_down(self, request): def __shut_down(self, request):
self.object = self.get_object() self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'): if not self.object.has_level(request.user, 'owner'):
...@@ -436,6 +455,14 @@ class VmDetailView(CheckedDetailView): ...@@ -436,6 +455,14 @@ class VmDetailView(CheckedDetailView):
self.object.reboot_async(request.user) self.object.reboot_async(request.user)
return redirect("%s#activity" % self.object.get_absolute_url()) return redirect("%s#activity" % self.object.get_absolute_url())
def __shut_off(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
self.object.shut_off_async(request.user)
return redirect("%s#activity" % self.object.get_absolute_url())
class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView): class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
template_name = "dashboard/node-detail.html" template_name = "dashboard/node-detail.html"
...@@ -449,11 +476,13 @@ class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView): ...@@ -449,11 +476,13 @@ class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
context = super(NodeDetailView, self).get_context_data(**kwargs) context = super(NodeDetailView, self).get_context_data(**kwargs)
instances = Instance.active.filter(node=self.object) instances = Instance.active.filter(node=self.object)
context['table'] = NodeVmListTable(instances) context['table'] = NodeVmListTable(instances)
ia = NodeActivity.objects.filter( na = NodeActivity.objects.filter(
node=self.object, parent=None node=self.object, parent=None
).order_by('-started').select_related() ).order_by('-started').select_related()
context['activities'] = ia context['activities'] = na
context['trait_form'] = form context['trait_form'] = form
context['graphite_enabled'] = (
NodeGraphView.get_graphite_url() is not None)
return context return context
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
...@@ -765,8 +794,16 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView): ...@@ -765,8 +794,16 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
return super(TemplateDetail, self).get(request, *args, **kwargs) return super(TemplateDetail, self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
obj = self.get_object()
context = super(TemplateDetail, self).get_context_data(**kwargs) context = super(TemplateDetail, self).get_context_data(**kwargs)
context['acl'] = get_vm_acl_data(self.get_object()) context['acl'] = get_vm_acl_data(obj)
context['disks'] = obj.disks.all()
context['disk_add_form'] = DiskAddForm(
user=self.request.user,
is_template=True,
object_pk=obj.pk,
prefix="disk",
)
return context return context
def get_success_url(self): def get_success_url(self):
...@@ -981,7 +1018,7 @@ class GroupDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView): ...@@ -981,7 +1018,7 @@ class GroupDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView):
class VmCreate(LoginRequiredMixin, TemplateView): class VmCreate(LoginRequiredMixin, TemplateView):
form_class = VmCreateForm form_class = VmCustomizeForm
form = None form = None
def get_template_names(self): def get_template_names(self):
...@@ -991,51 +1028,66 @@ class VmCreate(LoginRequiredMixin, TemplateView): ...@@ -991,51 +1028,66 @@ class VmCreate(LoginRequiredMixin, TemplateView):
return ['dashboard/nojs-wrapper.html'] return ['dashboard/nojs-wrapper.html']
def get(self, request, form=None, *args, **kwargs): def get(self, request, form=None, *args, **kwargs):
if form is None: form_error = form is not None
form = self.form_class() template = (form.template.pk if form_error
form.fields['disks'].queryset = Disk.get_objects_with_level( else request.GET.get("template"))
'user', request.user).exclude(type="qcow2-snap")
form.fields['networks'].queryset = Vlan.get_objects_with_level(
'user', request.user)
templates = InstanceTemplate.get_objects_with_level('user', templates = InstanceTemplate.get_objects_with_level('user',
request.user) request.user)
form.fields['template'].queryset = templates if form is None and template:
form = self.form_class(user=request.user,
template=templates.get(pk=template))
context = self.get_context_data(**kwargs) context = self.get_context_data(**kwargs)
context.update({ if template:
'template': 'dashboard/vm-create.html', context.update({
'box_title': 'Create a VM', 'template': 'dashboard/_vm-create-2.html',
'vm_create_form': form, 'box_title': _('Customize VM'),
}) 'ajax_title': False,
'vm_create_form': form,
'template_o': templates.get(pk=template),
})
else:
context.update({
'template': 'dashboard/_vm-create-1.html',
'box_title': _('Create a VM'),
'ajax_title': False,
'templates': templates.all(),
})
return self.render_to_response(context) return self.render_to_response(context)
def post(self, request, *args, **kwargs): def __create_normal(self, request, *args, **kwargs):
form = self.form_class(request.POST) user = request.user
template = InstanceTemplate.objects.get(
pk=request.POST.get("template"))
# permission check
if not template.has_level(request.user, 'user'):
raise PermissionDenied()
inst = Instance.create_from_template(
template=template, owner=user)
return self.__deploy(request, inst)
def __create_customized(self, request, *args, **kwargs):
user = request.user
form = self.form_class(
request.POST, user=request.user,
template=InstanceTemplate.objects.get(
pk=request.POST.get("template")
)
)
if not form.is_valid(): if not form.is_valid():
return self.get(request, form, *args, **kwargs) return self.get(request, form, *args, **kwargs)
post = form.cleaned_data post = form.cleaned_data
user = request.user
try: template = InstanceTemplate.objects.get(pk=post['template'])
limit = user.profile.instance_limit # permission check
except Exception as e: if not template.has_level(user, 'user'):
logger.debug('No profile or instance limit: %s', e)
else:
current = Instance.active.filter(owner=user).count()
logger.debug('current use: %d, limit: %d', current, limit)
if limit < current:
messages.error(request,
_('Instance limit (%d) exceeded.') % limit)
if request.is_ajax():
return HttpResponse(json.dumps({'redirect': '/'}),
content_type="application/json")
else:
return redirect('/')
template = post['template']
if not template.has_level(request.user, 'user'):
raise PermissionDenied() raise PermissionDenied()
if request.user.has_perm('vm.set_resources'): if request.user.has_perm('vm.set_resources'):
ikwargs = { ikwargs = {
'name': post['name'],
'num_cores': post['cpu_count'], 'num_cores': post['cpu_count'],
'ram_size': post['ram_size'], 'ram_size': post['ram_size'],
'priority': post['cpu_priority'], 'priority': post['cpu_priority'],
...@@ -1046,17 +1098,45 @@ class VmCreate(LoginRequiredMixin, TemplateView): ...@@ -1046,17 +1098,45 @@ class VmCreate(LoginRequiredMixin, TemplateView):
inst = Instance.create_from_template( inst = Instance.create_from_template(
template=template, owner=user, networks=networks, template=template, owner=user, networks=networks,
disks=disks, **ikwargs) disks=disks, **ikwargs)
return self.__deploy(request, inst)
else: else:
inst = Instance.create_from_template( raise PermissionDenied()
template=template, owner=user)
inst.deploy_async(user=request.user) def __deploy(self, request, instance, *args, **kwargs):
instance.deploy_async(user=request.user)
messages.success(request, _('VM successfully created!')) messages.success(request, _('VM successfully created!'))
path = inst.get_absolute_url() path = instance.get_absolute_url()
if request.is_ajax(): if request.is_ajax():
return HttpResponse(json.dumps({'redirect': path}), return HttpResponse(json.dumps({'redirect': path}),
content_type="application/json") content_type="application/json")
else: else:
return redirect(path) return redirect("%s#activity" % path)
def post(self, request, *args, **kwargs):
user = request.user
# limit chekcs
try:
limit = user.profile.instance_limit
except Exception as e:
logger.debug('No profile or instance limit: %s', e)
else:
current = Instance.active.filter(owner=user).count()
logger.debug('current use: %d, limit: %d', current, limit)
if limit < current:
messages.error(request,
_('Instance limit (%d) exceeded.') % limit)
if request.is_ajax():
return HttpResponse(json.dumps({'redirect': '/'}),
content_type="application/json")
else:
return redirect('/')
create_func = (self.__create_normal if
request.POST.get("customized") is None else
self.__create_customized)
return create_func(request, *args, **kwargs)
class NodeCreate(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView): class NodeCreate(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView):
...@@ -1388,7 +1468,8 @@ class VmMassDelete(LoginRequiredMixin, View): ...@@ -1388,7 +1468,8 @@ class VmMassDelete(LoginRequiredMixin, View):
return redirect(next if next else reverse_lazy('dashboard.index')) return redirect(next if next else reverse_lazy('dashboard.index'))
class LeaseCreate(SuccessMessageMixin, CreateView): class LeaseCreate(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, CreateView):
model = Lease model = Lease
form_class = LeaseForm form_class = LeaseForm
template_name = "dashboard/lease-create.html" template_name = "dashboard/lease-create.html"
...@@ -1398,7 +1479,8 @@ class LeaseCreate(SuccessMessageMixin, CreateView): ...@@ -1398,7 +1479,8 @@ class LeaseCreate(SuccessMessageMixin, CreateView):
return reverse_lazy("dashboard.views.template-list") return reverse_lazy("dashboard.views.template-list")
class LeaseDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView): class LeaseDetail(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, UpdateView):
model = Lease model = Lease
form_class = LeaseForm form_class = LeaseForm
template_name = "dashboard/lease-edit.html" template_name = "dashboard/lease-edit.html"
...@@ -1448,12 +1530,15 @@ def vm_activity(request, pk): ...@@ -1448,12 +1530,15 @@ def vm_activity(request, pk):
response['state'] = instance.state response['state'] = instance.state
if only_state is not None and only_state == "false": # instance activity if only_state is not None and only_state == "false": # instance activity
print "Sdsa" context = {
'activities': InstanceActivity.objects.filter(
instance=instance, parent=None
).order_by('-started').select_related()
}
activities = render_to_string( activities = render_to_string(
"dashboard/vm-detail/_activity-timeline.html", "dashboard/vm-detail/_activity-timeline.html",
{'activities': InstanceActivity.objects.filter( RequestContext(request, context),
instance=instance, parent=None
).order_by('-started').select_related()}
) )
response['activities'] = activities response['activities'] = activities
...@@ -1482,9 +1567,10 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView): ...@@ -1482,9 +1567,10 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
try: try:
new_owner = User.objects.get(username=request.POST['name']) new_owner = search_user(request.POST['name'])
except User.DoesNotExist: except User.DoesNotExist:
raise Http404() messages.error(request, _('Can not find specified user.'))
return self.get(request, *args, **kwargs)
except KeyError: except KeyError:
raise SuspiciousOperation() raise SuspiciousOperation()
...@@ -1495,29 +1581,198 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView): ...@@ -1495,29 +1581,198 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView):
token = signing.dumps((obj.pk, new_owner.pk), token = signing.dumps((obj.pk, new_owner.pk),
salt=TransferOwnershipConfirmView.get_salt()) salt=TransferOwnershipConfirmView.get_salt())
return HttpResponse("%s?key=%s" % ( token_path = reverse(
reverse('dashboard.views.vm-transfer-ownership-confirm'), token), 'dashboard.views.vm-transfer-ownership-confirm', args=[token])
content_type="text/plain") try:
new_owner.profile.notify(
_('Ownership offer'),
'dashboard/notifications/ownership-offer.html',
{'instance': obj, 'token': token_path})
except Profile.DoesNotExist:
messages.error(request, _('Can not notify selected user.'))
else:
messages.success(request,
_('User %s is notified about the offer.') % (
unicode(new_owner), ))
return redirect(reverse_lazy("dashboard.views.detail",
kwargs={'pk': obj.pk}))
class AbstractVmFunctionView(AccessMixin, View):
"""Abstract instance-action view.
User can do the action with a valid token or if has at least required_level
ACL level for the instance.
Children should at least implement/add template_name, success_message,
url_name, and do_action().
"""
token_max_age = 3 * 24 * 3600
required_level = 'owner'
success_message = _("Failed to perform requested action.")
@classmethod
def check_acl(cls, instance, user):
if not instance.has_level(user, cls.required_level):
raise PermissionDenied()
@classmethod
def get_salt(cls):
return unicode(cls)
@classmethod
def get_token(cls, instance, user, *args):
t = tuple([getattr(i, 'pk', i) for i in [instance, user] + list(args)])
return signing.dumps(t, salt=cls.get_salt())
@classmethod
def get_token_url(cls, instance, user, *args):
key = cls.get_token(instance, user, *args)
args = (instance.pk, key) + args
return reverse(cls.url_name, args=args)
# this wont work, CBVs suck: reverse(cls.as_view(), args=args)
def get_template_names(self):
return [self.template_name]
def get(self, request, pk, key=None, *args, **kwargs):
class LoginNeeded(Exception):
pass
pk = int(pk)
instance = get_object_or_404(Instance, pk=pk)
try:
if key:
logger.debug('Confirm dialog for token %s.', key)
try:
self.validate_key(pk, key)
except signing.SignatureExpired:
messages.error(request, _(
'The token has expired, please log in.'))
raise LoginNeeded()
self.key = key
else:
if not request.user.is_authenticated():
raise LoginNeeded()
self.check_acl(instance, request.user)
except LoginNeeded:
return redirect_to_login(request.get_full_path(),
self.get_login_url(),
self.get_redirect_field_name())
except SuspiciousOperation as e:
messages.error(request, _('This token is invalid.'))
logger.warning('This token %s is invalid. %s', key, unicode(e))
raise PermissionDenied()
return render(request, self.get_template_names(),
self.get_context(instance))
def post(self, request, pk, key=None, *args, **kwargs):
class LoginNeeded(Exception):
pass
pk = int(pk)
instance = get_object_or_404(Instance, pk=pk)
try:
if not request.user.is_authenticated() and key:
try:
user = self.validate_key(pk, key)
except signing.SignatureExpired:
messages.error(request, _(
'The token has expired, please log in.'))
raise LoginNeeded()
self.key = key
else:
user = request.user
self.check_acl(instance, request.user)
except LoginNeeded:
return redirect_to_login(request.get_full_path(),
self.get_login_url(),
self.get_redirect_field_name())
except SuspiciousOperation as e:
messages.error(request, _('This token is invalid.'))
logger.warning('This token %s is invalid. %s', key, unicode(e))
raise PermissionDenied()
if self.do_action(instance, user):
messages.success(request, self.success_message)
else:
messages.error(request, self.fail_message)
return HttpResponseRedirect(instance.get_absolute_url())
def validate_key(self, pk, key):
"""Get object based on signed token.
"""
try:
data = signing.loads(key, salt=self.get_salt())
logger.debug('Token data: %s', unicode(data))
instance, user = data
logger.debug('Extracted token data: instance: %s, user: %s',
unicode(instance), unicode(user))
except (signing.BadSignature, ValueError, TypeError) as e:
logger.warning('Tried invalid token. Token: %s, user: %s. %s',
key, unicode(self.request.user), unicode(e))
raise SuspiciousOperation()
try:
instance, user = signing.loads(key, max_age=self.token_max_age,
salt=self.get_salt())
logger.debug('Extracted non-expired token data: %s, %s',
unicode(instance), unicode(user))
except signing.BadSignature as e:
raise signing.SignatureExpired()
if pk != instance:
logger.debug('pk (%d) != instance (%d)', pk, instance)
raise SuspiciousOperation()
user = User.objects.get(pk=user)
return user
def do_action(self, instance, user): # noqa
raise NotImplementedError('Please override do_action(instance, user)')
def get_context(self, instance):
context = {'instance': instance}
if getattr(self, 'key', None) is not None:
context['key'] = self.key
return context
class VmRenewView(AbstractVmFunctionView):
"""User can renew an instance."""
template_name = 'dashboard/confirm/base-renew.html'
success_message = _("Virtual machine is successfully renewed.")
url_name = 'dashboard.views.vm-renew'
def get_context(self, instance):
context = super(VmRenewView, self).get_context(instance)
(context['time_of_suspend'],
context['time_of_delete']) = instance.get_renew_times()
return context
def do_action(self, instance, user):
instance.renew(user=user)
logger.info('Instance %s renewed by %s.', unicode(instance),
unicode(user))
return True
class TransferOwnershipConfirmView(LoginRequiredMixin, View): class TransferOwnershipConfirmView(LoginRequiredMixin, View):
"""User can accept an ownership offer."""
max_age = 3 * 24 * 3600 max_age = 3 * 24 * 3600
success_message = _("Ownership successfully transferred.") success_message = _("Ownership successfully transferred to you.")
@classmethod @classmethod
def get_salt(cls): def get_salt(cls):
return unicode(cls) return unicode(cls)
def get(self, request, *args, **kwargs): def get(self, request, key, *args, **kwargs):
"""Confirm ownership transfer based on token. """Confirm ownership transfer based on token.
""" """
logger.debug('Confirm dialog for token %s.', key)
try: try:
key = request.GET['key']
logger.debug('Confirm dialog for token %s.', key)
instance, new_owner = self.get_instance(key, request.user) instance, new_owner = self.get_instance(key, request.user)
except KeyError: except PermissionDenied:
raise Http404()
except PermissionDenied():
messages.error(request, _('This token is for an other user.')) messages.error(request, _('This token is for an other user.'))
raise raise
except SuspiciousOperation: except SuspiciousOperation:
...@@ -1527,16 +1782,10 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View): ...@@ -1527,16 +1782,10 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View):
"dashboard/confirm/base-transfer-ownership.html", "dashboard/confirm/base-transfer-ownership.html",
dictionary={'instance': instance, 'key': key}) dictionary={'instance': instance, 'key': key})
def post(self, request, *args, **kwargs): def post(self, request, key, *args, **kwargs):
"""Really transfer ownership based on token. """Really transfer ownership based on token.
""" """
try: instance, owner = self.get_instance(key, request.user)
key = request.POST['key']
instance, owner = self.get_instance(key, request.user)
except KeyError:
logger.debug('Posted to %s without key field.',
unicode(self.__class__))
raise SuspiciousOperation()
old = instance.owner old = instance.owner
with instance_activity(code_suffix='ownership-transferred', with instance_activity(code_suffix='ownership-transferred',
...@@ -1547,6 +1796,11 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View): ...@@ -1547,6 +1796,11 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View):
messages.success(request, self.success_message) messages.success(request, self.success_message)
logger.info('Ownership of %s transferred from %s to %s.', logger.info('Ownership of %s transferred from %s to %s.',
unicode(instance), unicode(old), unicode(request.user)) unicode(instance), unicode(old), unicode(request.user))
if old.profile:
old.profile.notify(
_('Ownership accepted'),
'dashboard/notifications/ownership-accepted.html',
{'instance': instance})
return HttpResponseRedirect(instance.get_absolute_url()) return HttpResponseRedirect(instance.get_absolute_url())
def get_instance(self, key, user): def get_instance(self, key, user):
...@@ -1556,15 +1810,7 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View): ...@@ -1556,15 +1810,7 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View):
instance, new_owner = ( instance, new_owner = (
signing.loads(key, max_age=self.max_age, signing.loads(key, max_age=self.max_age,
salt=self.get_salt())) salt=self.get_salt()))
except signing.BadSignature as e: except (signing.BadSignature, ValueError, TypeError) as e:
logger.error('Tried invalid token. Token: %s, user: %s. %s',
key, unicode(user), unicode(e))
raise SuspiciousOperation()
except ValueError as e:
logger.error('Tried invalid token. Token: %s, user: %s. %s',
key, unicode(user), unicode(e))
raise SuspiciousOperation()
except TypeError as e:
logger.error('Tried invalid token. Token: %s, user: %s. %s', logger.error('Tried invalid token. Token: %s, user: %s. %s',
key, unicode(user), unicode(e)) key, unicode(user), unicode(e))
raise SuspiciousOperation() raise SuspiciousOperation()
...@@ -1584,45 +1830,205 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View): ...@@ -1584,45 +1830,205 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View):
return (instance, new_owner) return (instance, new_owner)
class VmGraphView(LoginRequiredMixin, View): class GraphViewBase(LoginRequiredMixin, View):
def get(self, request, pk, metric, time, *args, **kwargs): @staticmethod
def get_graphite_url():
graphite_host = getenv("GRAPHITE_HOST", None) graphite_host = getenv("GRAPHITE_HOST", None)
graphite_port = getenv("GRAPHITE_PORT", None) graphite_port = getenv("GRAPHITE_PORT", None)
if (graphite_host in ['', None] or graphite_port in ['', None]): if (graphite_host in ['', None] or graphite_port in ['', None]):
logger.debug('GRAPHITE_HOST is empty.') logger.debug('GRAPHITE_HOST is empty.')
return None
return 'http://%s:%s' % (graphite_host, graphite_port)
def get(self, request, pk, metric, time, *args, **kwargs):
graphite_url = GraphViewBase.get_graphite_url()
if graphite_url is None:
raise Http404() raise Http404()
if metric not in ['cpu', 'memory', 'network']: if metric not in self.metrics.keys():
raise SuspiciousOperation() raise SuspiciousOperation()
try: try:
instance = Instance.objects.get(id=pk) instance = self.get_object(request, pk)
except Instance.DoesNotExist: except self.model.DoesNotExist:
raise Http404() raise Http404()
if not instance.has_level(request.user, 'user'):
raise PermissionDenied()
targets = {
'cpu': ('cactiStyle(alias(derivative(%s.cpu.usage),'
'"cpu usage (%%)"))'),
'memory': ('cactiStyle(alias(%s.memory.usage,'
'"memory usage (%%)"))'),
'network': ('cactiStyle(aliasByMetric('
'derivative(%s.network.bytes_*)))'),
}
if metric not in targets.keys():
raise SuspiciousOperation()
prefix = 'vm.%s' % instance.vm_name prefix = self.get_prefix(instance)
target = targets[metric] % prefix target = self.metrics[metric] % {'prefix': prefix}
title = '%s (%s) - %s' % (instance.name, instance.vm_name, metric) title = self.get_title(instance, metric)
params = {'target': target, params = {'target': target,
'from': '-%s' % time, 'from': '-%s' % time,
'title': title.encode('UTF-8'), 'title': title.encode('UTF-8'),
'width': '500', 'width': '500',
'height': '200'} 'height': '200'}
url = ('http://%s:%s/render/?%s' % (graphite_host, graphite_port, response = requests.post('%s/render/' % graphite_url, data=params)
params))
response = requests.post(url, data=params)
return HttpResponse(response.content, mimetype="image/png") return HttpResponse(response.content, mimetype="image/png")
def get_prefix(self, instance):
raise NotImplementedError("Subclass must implement abstract method")
def get_title(self, instance, metric):
raise NotImplementedError("Subclass must implement abstract method")
def get_object(self, request, pk):
instance = self.model.objects.get(id=pk)
if not instance.has_level(request.user, 'user'):
raise PermissionDenied()
return instance
class VmGraphView(GraphViewBase):
metrics = {
'cpu': ('cactiStyle(alias(nonNegativeDerivative(%(prefix)s.cpu.usage),'
'"cpu usage (%%)"))'),
'memory': ('cactiStyle(alias(%(prefix)s.memory.usage,'
'"memory usage (%%)"))'),
'network': (
'group('
'aliasSub(nonNegativeDerivative(%(prefix)s.network.bytes_recv*),'
' ".*-(\d+)\\)", "out (vlan \\1)"),'
'aliasSub(nonNegativeDerivative(%(prefix)s.network.bytes_sent*),'
' ".*-(\d+)\\)", "in (vlan \\1)"))'),
}
model = Instance
def get_prefix(self, instance):
return 'vm.%s' % instance.vm_name
def get_title(self, instance, metric):
return '%s (%s) - %s' % (instance.name, instance.vm_name, metric)
class NodeGraphView(SuperuserRequiredMixin, GraphViewBase):
metrics = {
'cpu': ('cactiStyle(alias(nonNegativeDerivative(%(prefix)s.cpu.times),'
'"cpu usage (%%)"))'),
'memory': ('cactiStyle(alias(%(prefix)s.memory.usage,'
'"memory usage (%%)"))'),
'network': ('cactiStyle(aliasByMetric('
'nonNegativeDerivative(%(prefix)s.network.bytes_*)))'),
}
model = Node
def get_prefix(self, instance):
return 'circle.%s' % instance.name
def get_title(self, instance, metric):
return '%s - %s' % (instance.name, metric)
def get_object(self, request, pk):
return self.model.objects.get(id=pk)
class NotificationView(LoginRequiredMixin, TemplateView):
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/_notifications-timeline.html']
else:
return ['dashboard/notifications.html']
def get_context_data(self, *args, **kwargs):
context = super(NotificationView, self).get_context_data(
*args, **kwargs)
# we need to convert it to list, otherwise it's gonna be
# similar to a QuerySet and update everything to
# read status after get
n = 10 if self.request.is_ajax() else 1000
context['notifications'] = list(
self.request.user.notification_set.values()[:n])
return context
def get(self, *args, **kwargs):
response = super(NotificationView, self).get(*args, **kwargs)
un = self.request.user.notification_set.filter(status="new")
for u in un:
u.status = "read"
u.save()
return response
class VmMigrateView(SuperuserRequiredMixin, TemplateView):
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/modal-wrapper.html']
else:
return ['dashboard/nojs-wrapper.html']
def get(self, request, form=None, *args, **kwargs):
context = self.get_context_data(**kwargs)
vm = Instance.objects.get(pk=kwargs['pk'])
context.update({
'template': 'dashboard/_vm-migrate.html',
'box_title': _('Migrate %(name)s' % {'name': vm.name}),
'ajax_title': True,
'vm': vm,
'nodes': [n for n in Node.objects.filter(enabled=True)
if n.state == "ONLINE"]
})
return self.render_to_response(context)
def post(self, *args, **kwargs):
node = self.request.POST.get("node")
vm = Instance.objects.get(pk=kwargs['pk'])
if node:
node = Node.objects.get(pk=node)
vm.migrate_async(to_node=node, user=self.request.user)
else:
messages.error(self.request, _("You didn't select a node!"))
return redirect("%s#activity" % vm.get_absolute_url())
def circle_login(request):
authentication_form = CircleAuthenticationForm
extra_context = {
'saml2': hasattr(settings, "SAML_CONFIG")
}
return login(request, authentication_form=authentication_form,
extra_context=extra_context)
class DiskAddView(TemplateView):
def post(self, *args, **kwargs):
is_template = self.request.POST.get("disk-is_template")
object_pk = self.request.POST.get("disk-object_pk")
is_template = int(is_template) == 1
if is_template:
obj = InstanceTemplate.objects.get(pk=object_pk)
else:
obj = Instance.objects.get(pk=object_pk)
if not obj.has_level(self.request.user, 'owner'):
raise PermissionDenied()
form = DiskAddForm(
self.request.POST,
user=self.request.user,
is_template=is_template, object_pk=object_pk,
prefix="disk"
)
if form.is_valid():
if form.cleaned_data.get("size"):
messages.success(self.request, _("Disk successfully added!"))
else:
messages.success(self.request, _("Disk download started!"))
form.save()
else:
error = "<br /> ".join(["<strong>%s</strong>: %s" %
(title_filter(i[0]), i[1][0])
for i in form.errors.items()])
messages.error(self.request, error)
if is_template:
r = obj.get_absolute_url()
else:
r = obj.get_absolute_url()
r = "%s#resources" % r
return redirect(r)
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from itertools import islice from itertools import islice, chain
import logging import logging
from netaddr import IPSet from netaddr import IPSet, EUI
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models from django.db import models
...@@ -298,13 +298,28 @@ class Vlan(AclBase, models.Model): ...@@ -298,13 +298,28 @@ class Vlan(AclBase, models.Model):
def prefix6(self): def prefix6(self):
return self.network6.prefixlen return self.network6.prefixlen
def get_next_address(self, used_v4):
try:
last_address = list(used_v4)[-1]
except IndexError:
return []
next_address = last_address + 1
if next_address in self.network4.iter_hosts():
logger.debug("Found unused IPv4 address %s after %s.",
next_address, last_address)
return [next_address]
else:
return []
def get_new_address(self): def get_new_address(self):
hosts = Host.objects.filter(vlan=self) hosts = self.host_set
used_v4 = IPSet(hosts.values_list('ipv4', flat=True)) used_v4 = IPSet(hosts.values_list('ipv4', flat=True))
used_v6 = IPSet(hosts.exclude(ipv6__isnull=True) used_v6 = IPSet(hosts.exclude(ipv6__isnull=True)
.values_list('ipv6', flat=True)) .values_list('ipv6', flat=True))
for ipv4 in islice(self.network4.iter_hosts(), 10000): for ipv4 in chain(self.get_next_address(used_v4),
islice(self.network4.iter_hosts(), 10000)):
ipv4 = str(ipv4) ipv4 = str(ipv4)
if ipv4 not in used_v4: if ipv4 not in used_v4:
logger.debug("Found unused IPv4 address %s.", ipv4) logger.debug("Found unused IPv4 address %s.", ipv4)
...@@ -616,6 +631,8 @@ class Host(models.Model): ...@@ -616,6 +631,8 @@ class Host(models.Model):
if self.shared_ip and public: if self.shared_ip and public:
res = Record.objects.filter(type='A', res = Record.objects.filter(type='A',
address=self.pub_ipv4) address=self.pub_ipv4)
if res.count() < 1:
return unicode(self.pub_ipv4)
else: else:
res = self.record_set.filter(type='A', res = self.record_set.filter(type='A',
address=self.ipv4) address=self.ipv4)
...@@ -691,6 +708,17 @@ class Host(models.Model): ...@@ -691,6 +708,17 @@ class Host(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return ('network.host', None, {'pk': self.pk}) return ('network.host', None, {'pk': self.pk})
@property
def eui(self):
return EUI(self.mac)
@property
def hw_vendor(self):
try:
return self.eui.oui.registration().org
except:
return None
class Firewall(models.Model): class Firewall(models.Model):
name = models.CharField(max_length=20, unique=True, name = models.CharField(max_length=20, unique=True,
......
from django.test import TestCase
from admin import HostAdmin
class MockInstance:
def __init__(self, groups):
self.groups = MockGroups(groups)
class MockGroup:
def __init__(self, name):
self.name = name
class MockGroups:
def __init__(self, groups):
self.groups = groups
def all(self):
return self.groups
class HostAdminTestCase(TestCase):
def test_no_groups(self):
instance = MockInstance([])
l = HostAdmin.list_groups(instance)
self.assertEqual(l, "")
def test_sigle_group(self):
instance = MockInstance([MockGroup("alma")])
l = HostAdmin.list_groups(instance)
self.assertEqual(l, "alma")
def test_multiple_groups(self):
instance = MockInstance([MockGroup("alma"),
MockGroup("korte"), MockGroup("szilva")])
l = HostAdmin.list_groups(instance)
self.assertEqual(l, "alma, korte, szilva")
from django.test import TestCase
from django.contrib.auth.models import User
from ..admin import HostAdmin
from firewall.models import Vlan, Domain, Record, Host
from django.forms import ValidationError
class MockInstance:
def __init__(self, groups):
self.groups = MockGroups(groups)
class MockGroup:
def __init__(self, name):
self.name = name
class MockGroups:
def __init__(self, groups):
self.groups = groups
def all(self):
return self.groups
class HostAdminTestCase(TestCase):
def test_no_groups(self):
instance = MockInstance([])
l = HostAdmin.list_groups(instance)
self.assertEqual(l, "")
def test_sigle_group(self):
instance = MockInstance([MockGroup("alma")])
l = HostAdmin.list_groups(instance)
self.assertEqual(l, "alma")
def test_multiple_groups(self):
instance = MockInstance([MockGroup("alma"),
MockGroup("korte"), MockGroup("szilva")])
l = HostAdmin.list_groups(instance)
self.assertEqual(l, "alma, korte, szilva")
class GetNewAddressTestCase(TestCase):
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u1.save()
d = Domain(name='example.org', owner=self.u1)
d.save()
# /29 = .1-.6 = 6 hosts/subnet + broadcast + network id
self.vlan = Vlan(vid=1, name='test', network4='10.0.0.0/29',
network6='2001:738:2001:4031::/80', domain=d,
owner=self.u1)
self.vlan.save()
self.vlan.host_set.all().delete()
for i in [1] + range(3, 6):
Host(hostname='h-%d' % i, mac='01:02:03:04:05:%02d' % i,
ipv4='10.0.0.%d' % i, vlan=self.vlan,
owner=self.u1).save()
def test_new_addr_w_empty_vlan(self):
self.vlan.host_set.all().delete()
self.vlan.get_new_address()
def test_all_addr_in_use(self):
for i in (2, 6):
Host(hostname='h-%d' % i, mac='01:02:03:04:05:%02d' % i,
ipv4='10.0.0.%d' % i, vlan=self.vlan,
owner=self.u1).save()
self.assertRaises(ValidationError, self.vlan.get_new_address)
def test_all_addr_in_use_w_ipv6(self):
Host(hostname='h-x', mac='01:02:03:04:05:06',
ipv4='10.0.0.6', ipv6='2001:738:2001:4031:0:0:2:0',
vlan=self.vlan, owner=self.u1).save()
self.assertRaises(ValidationError, self.vlan.get_new_address)
def test_new_addr_last(self):
self.assertEqual(self.vlan.get_new_address()['ipv4'], '10.0.0.6')
def test_new_addr_w_overflow(self):
Host(hostname='h-6', mac='01:02:03:04:05:06',
ipv4='10.0.0.6', vlan=self.vlan, owner=self.u1).save()
self.assertEqual(self.vlan.get_new_address()['ipv4'], '10.0.0.2')
class HostGetHostnameTestCase(TestCase):
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u1.save()
self.d = Domain(name='example.org', owner=self.u1)
self.d.save()
Record.objects.all().delete()
self.vlan = Vlan(vid=1, name='test', network4='10.0.0.0/24',
network6='2001:738:2001:4031::/80', domain=self.d,
owner=self.u1, network_type='portforward',
snat_ip='10.1.1.1')
self.vlan.save()
self.h = Host(hostname='h', mac='01:02:03:04:05:00', ipv4='10.0.0.1',
vlan=self.vlan, owner=self.u1, shared_ip=True,
pub_ipv4=self.vlan.snat_ip)
self.h.save()
def test_issue_93_wo_record(self):
self.assertEqual(self.h.get_hostname(proto='ipv4', public=True),
unicode(self.h.pub_ipv4))
def test_issue_93_w_record(self):
self.r = Record(name='vm', type='A', domain=self.d, owner=self.u1,
address=self.vlan.snat_ip)
self.r.save()
self.assertEqual(self.h.get_hostname(proto='ipv4', public=True),
self.r.fqdn)
...@@ -11,7 +11,8 @@ celery = Celery('manager', backend='amqp', ...@@ -11,7 +11,8 @@ celery = Celery('manager', backend='amqp',
'vm.tasks.local_periodic_tasks', 'vm.tasks.local_periodic_tasks',
'vm.tasks.local_agent_tasks', 'vm.tasks.local_agent_tasks',
'storage.tasks.local_tasks', 'storage.tasks.local_tasks',
'firewall.tasks.local_tasks']) 'storage.tasks.periodic_tasks',
'firewall.tasks.local_tasks', ])
celery.conf.update( celery.conf.update(
CELERY_TASK_RESULT_EXPIRES=300, CELERY_TASK_RESULT_EXPIRES=300,
...@@ -27,11 +28,21 @@ celery.conf.update( ...@@ -27,11 +28,21 @@ celery.conf.update(
'schedule': timedelta(seconds=5), 'schedule': timedelta(seconds=5),
'options': {'queue': 'localhost.man'} 'options': {'queue': 'localhost.man'}
}, },
'vm.periodic_tasks': { 'vm.update_domain_states': {
'task': 'vm.tasks.local_periodic_tasks.update_domain_states', 'task': 'vm.tasks.local_periodic_tasks.update_domain_states',
'schedule': timedelta(seconds=10), 'schedule': timedelta(seconds=10),
'options': {'queue': 'localhost.man'} 'options': {'queue': 'localhost.man'}
}, },
'vm.garbage_collector': {
'task': 'vm.tasks.local_periodic_tasks.garbage_collector',
'schedule': timedelta(minutes=10),
'options': {'queue': 'localhost.man'}
},
'storage.periodic_tasks': {
'task': 'storage.tasks.periodic_tasks.garbage_collector',
'schedule': timedelta(hours=1),
'options': {'queue': 'localhost.man'}
},
} }
) )
...@@ -23,7 +23,8 @@ def select_node(instance, nodes): ...@@ -23,7 +23,8 @@ def select_node(instance, nodes):
''' Select a node for hosting an instance based on its requirements. ''' Select a node for hosting an instance based on its requirements.
''' '''
# check required traits # check required traits
nodes = [n for n in nodes if has_traits(instance.req_traits.all(), n)] nodes = [n for n in nodes
if n.enabled and has_traits(instance.req_traits.all(), n)]
if not nodes: if not nodes:
raise TraitsUnsatisfiableException() raise TraitsUnsatisfiableException()
...@@ -51,7 +52,7 @@ def has_enough_ram(ram_size, node): ...@@ -51,7 +52,7 @@ def has_enough_ram(ram_size, node):
ram_size mebibytes of memory; otherwise, false. ram_size mebibytes of memory; otherwise, false.
""" """
total = node.ram_size total = node.ram_size
used = (node.ram_usage() / 100) * total used = (node.ram_usage / 100) * total
unused = total - used unused = total - used
overcommit = node.ram_size_with_overcommit overcommit = node.ram_size_with_overcommit
...@@ -66,7 +67,7 @@ def free_cpu_time(node): ...@@ -66,7 +67,7 @@ def free_cpu_time(node):
Higher values indicate more idle time. Higher values indicate more idle time.
""" """
activity = node.cpu_usage() / 100 activity = node.cpu_usage / 100
inactivity = 1 - activity inactivity = 1 - activity
cores = node.num_cores cores = node.num_cores
return cores * inactivity return cores * inactivity
...@@ -36,6 +36,9 @@ class GroupTable(Table): ...@@ -36,6 +36,9 @@ class GroupTable(Table):
class HostTable(Table): class HostTable(Table):
hostname = LinkColumn('network.host', args=[A('pk')]) hostname = LinkColumn('network.host', args=[A('pk')])
mac = TemplateColumn(
template_name="network/columns/mac.html"
)
class Meta: class Meta:
model = Host model = Host
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<title>{% block title %}Firewall GUI{% endblock %}</title> <title>{% block title %}Firewall GUI{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href='http://fonts.googleapis.com/css?family=Source+Sans+Pro:200,400&amp;subset=latin,latin-ext' rel='stylesheet' type='text/css'> <link href='//fonts.googleapis.com/css?family=Source+Sans+Pro:200,400&amp;subset=latin,latin-ext' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css"> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-theme.min.css"> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-theme.min.css">
<link href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css" rel="stylesheet" /> <link href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css" rel="stylesheet" />
...@@ -79,7 +79,7 @@ ...@@ -79,7 +79,7 @@
</footer> </footer>
</div><!-- .footer-container .container --> </div><!-- .footer-container .container -->
<script src="http://code.jquery.com/jquery-latest.js"></script> <script src="//code.jquery.com/jquery-latest.js"></script>
<script src="{% url "network.js_catalog" %}"></script> <script src="{% url "network.js_catalog" %}"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script> <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<script src="{% static "js/bootbox.min.js" %}"></script> <script src="{% static "js/bootbox.min.js" %}"></script>
......
{% load i18n %}
<span title="{% blocktrans with vendor=record.hw_vendor|default:"n/a" %}Vendor: {{vendor}}{% endblocktrans %}">{{ record.mac }}</span>
from django import contrib from django import contrib
# from django.utils.translation import ugettext_lazy as _ # from django.utils.translation import ugettext_lazy as _
from .models import Disk, DataStore from .models import Disk, DataStore, DiskActivity
class DiskAdmin(contrib.admin.ModelAdmin): class DiskAdmin(contrib.admin.ModelAdmin):
...@@ -13,4 +13,5 @@ class DataStoreAdmin(contrib.admin.ModelAdmin): ...@@ -13,4 +13,5 @@ class DataStoreAdmin(contrib.admin.ModelAdmin):
contrib.admin.site.register(Disk, DiskAdmin) contrib.admin.site.register(Disk, DiskAdmin)
contrib.admin.site.register(DiskActivity)
contrib.admin.site.register(DataStore, DataStoreAdmin) contrib.admin.site.register(DataStore, DataStoreAdmin)
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding unique constraint on 'Disk', fields ['filename']
db.create_unique(u'storage_disk', ['filename'])
def backwards(self, orm):
# Removing unique constraint on 'Disk', fields ['filename']
db.delete_unique(u'storage_disk', ['filename'])
models = {
u'acl.level': {
'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Level'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'weight': ('django.db.models.fields.IntegerField', [], {'null': 'True'})
},
u'acl.objectlevel': {
'Meta': {'unique_together': "(('content_type', 'object_id', 'level'),)", 'object_name': 'ObjectLevel'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'level': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['acl.Level']"}),
'object_id': ('django.db.models.fields.IntegerField', [], {}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'symmetrical': 'False'})
},
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'storage.datastore': {
'Meta': {'ordering': "['name']", 'object_name': 'DataStore'},
'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'})
},
u'storage.disk': {
'Meta': {'ordering': "['name']", 'object_name': 'Disk'},
'base': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'derivatives'", 'null': 'True', 'to': u"orm['storage.Disk']"}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'datastore': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['storage.DataStore']"}),
'destroyed': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'dev_num': ('django.db.models.fields.CharField', [], {'default': "'a'", 'max_length': '1'}),
'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'ready': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'size': ('sizefield.models.FileSizeField', [], {}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '10'})
},
u'storage.diskactivity': {
'Meta': {'object_name': 'DiskActivity'},
'activity_code': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'disk': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_log'", 'to': u"orm['storage.Disk']"}),
'finished': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': u"orm['storage.DiskActivity']"}),
'result': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'started': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'succeeded': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'task_uuid': ('django.db.models.fields.CharField', [], {'max_length': '50', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
}
}
complete_apps = ['storage']
\ No newline at end of file
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
from contextlib import contextmanager from contextlib import contextmanager
import logging import logging
from os.path import join
import uuid import uuid
from django.db.models import (Model, BooleanField, CharField, DateTimeField, from django.db.models import (Model, BooleanField, CharField, DateTimeField,
...@@ -10,10 +11,13 @@ from django.utils import timezone ...@@ -10,10 +11,13 @@ from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
from sizefield.models import FileSizeField from sizefield.models import FileSizeField
from datetime import timedelta
from acl.models import AclBase from acl.models import AclBase
from .tasks import local_tasks, remote_tasks from .tasks import local_tasks, remote_tasks
from common.models import ActivityModel, activitycontextimpl from celery.exceptions import TimeoutError
from manager.mancelery import celery
from common.models import ActivityModel, activitycontextimpl, WorkerNotFound
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -35,8 +39,19 @@ class DataStore(Model): ...@@ -35,8 +39,19 @@ class DataStore(Model):
def __unicode__(self): def __unicode__(self):
return u'%s (%s)' % (self.name, self.path) return u'%s (%s)' % (self.name, self.path)
def get_remote_queue_name(self, queue_id): def get_remote_queue_name(self, queue_id, check_worker=True):
return self.hostname + '.' + queue_id logger.debug("Checking for storage queue %s.%s",
self.hostname, queue_id)
if not check_worker or local_tasks.check_queue(self.hostname,
queue_id):
return self.hostname + '.' + queue_id
else:
raise WorkerNotFound()
def get_deletable_disks(self):
return [disk.filename for disk in
self.disk_set.filter(
destroyed__isnull=False) if disk.is_deletable]
class Disk(AclBase, TimeStampedModel): class Disk(AclBase, TimeStampedModel):
...@@ -51,7 +66,8 @@ class Disk(AclBase, TimeStampedModel): ...@@ -51,7 +66,8 @@ class Disk(AclBase, TimeStampedModel):
TYPES = [('qcow2-norm', 'qcow2 normal'), ('qcow2-snap', 'qcow2 snapshot'), TYPES = [('qcow2-norm', 'qcow2 normal'), ('qcow2-snap', 'qcow2 snapshot'),
('iso', 'iso'), ('raw-ro', 'raw read-only'), ('raw-rw', 'raw')] ('iso', 'iso'), ('raw-ro', 'raw read-only'), ('raw-rw', 'raw')]
name = CharField(blank=True, max_length=100, verbose_name=_("name")) name = CharField(blank=True, max_length=100, verbose_name=_("name"))
filename = CharField(max_length=256, verbose_name=_("filename")) filename = CharField(max_length=256, unique=True,
verbose_name=_("filename"))
datastore = ForeignKey(DataStore, verbose_name=_("datastore"), datastore = ForeignKey(DataStore, verbose_name=_("datastore"),
help_text=_("The datastore that holds the disk.")) help_text=_("The datastore that holds the disk."))
type = CharField(max_length=10, choices=TYPES) type = CharField(max_length=10, choices=TYPES)
...@@ -94,10 +110,14 @@ class Disk(AclBase, TimeStampedModel): ...@@ -94,10 +110,14 @@ class Disk(AclBase, TimeStampedModel):
@property @property
def path(self): def path(self):
return self.datastore.path + '/' + self.filename """The path where the files are stored.
"""
return join(self.datastore.path, self.filename)
@property @property
def format(self): def vm_format(self):
"""Returns the proper file format for different type of images.
"""
return { return {
'qcow2-norm': 'qcow2', 'qcow2-norm': 'qcow2',
'qcow2-snap': 'qcow2', 'qcow2-snap': 'qcow2',
...@@ -107,7 +127,21 @@ class Disk(AclBase, TimeStampedModel): ...@@ -107,7 +127,21 @@ class Disk(AclBase, TimeStampedModel):
}[self.type] }[self.type]
@property @property
def format(self):
"""Returns the proper file format for different types of images.
"""
return {
'qcow2-norm': 'qcow2',
'qcow2-snap': 'qcow2',
'iso': 'iso',
'raw-ro': 'raw',
'raw-rw': 'raw',
}[self.type]
@property
def device_type(self): def device_type(self):
"""Returns the proper device prefix for different types of images.
"""
return { return {
'qcow2-norm': 'vd', 'qcow2-norm': 'vd',
'qcow2-snap': 'vd', 'qcow2-snap': 'vd',
...@@ -116,8 +150,42 @@ class Disk(AclBase, TimeStampedModel): ...@@ -116,8 +150,42 @@ class Disk(AclBase, TimeStampedModel):
'raw-rw': 'vd', 'raw-rw': 'vd',
}[self.type] }[self.type]
def is_downloading(self):
da = DiskActivity.objects.filter(disk=self).latest("created")
return (da.activity_code == "storage.Disk.download"
and da.succeeded is None)
def get_download_percentage(self):
if not self.is_downloading():
return None
task = DiskActivity.objects.latest("created").task_uuid
result = celery.AsyncResult(id=task)
return result.info.get("percent")
@property
def is_deletable(self):
"""True if the associated file can be deleted.
"""
# Check if all children and the disk itself is destroyed.
yesterday = timezone.now() - timedelta(days=1)
return (self.destroyed is not None
and self.destroyed < yesterday) and self.children_deletable
@property
def children_deletable(self):
"""True if all children of the disk are deletable.
"""
return all(i.is_deletable for i in self.derivatives.all())
@property
def is_in_use(self): def is_in_use(self):
return any([i.state != 'STOPPED' for i in self.instance_set.all()]) """True if disk is attached to an active VM.
'In use' means the disk is attached to a VM which is not STOPPED, as
any other VMs leave the disk in an inconsistent state.
"""
return any(i.state != 'STOPPED' for i in self.instance_set.all())
def get_exclusive(self): def get_exclusive(self):
"""Get an instance of the disk for exclusive usage. """Get an instance of the disk for exclusive usage.
...@@ -133,47 +201,52 @@ class Disk(AclBase, TimeStampedModel): ...@@ -133,47 +201,52 @@ class Disk(AclBase, TimeStampedModel):
if self.type not in type_mapping.keys(): if self.type not in type_mapping.keys():
raise self.WrongDiskTypeError(self.type) raise self.WrongDiskTypeError(self.type)
filename = self.filename if self.type == 'iso' else str(uuid.uuid4())
new_type = type_mapping[self.type] new_type = type_mapping[self.type]
return Disk.objects.create(base=self, datastore=self.datastore, return Disk.create(base=self, datastore=self.datastore,
filename=filename, name=self.name, name=self.name, size=self.size,
size=self.size, type=new_type) type=new_type)
def get_vmdisk_desc(self): def get_vmdisk_desc(self):
"""Serialize disk object to the vmdriver.
"""
return { return {
'source': self.path, 'source': self.path,
'driver_type': self.format, 'driver_type': self.vm_format,
'driver_cache': 'default', 'driver_cache': 'none',
'target_device': self.device_type + self.dev_num, 'target_device': self.device_type + self.dev_num,
'disk_device': 'cdrom' if self.type == 'iso' else 'disk' 'disk_device': 'cdrom' if self.type == 'iso' else 'disk'
} }
def get_disk_desc(self): def get_disk_desc(self):
"""Serialize disk object to the storage driver.
"""
return { return {
'name': self.filename, 'name': self.filename,
'dir': self.datastore.path, 'dir': self.datastore.path,
'format': self.format, 'format': self.format,
'size': self.size, 'size': self.size,
'base_name': self.base.filename if self.base else None, 'base_name': self.base.filename if self.base else None,
'type': 'snapshot' if self.type == 'qcow2-snap' else 'normal' 'type': 'snapshot' if self.base else 'normal'
} }
def get_remote_queue_name(self, queue_id): def get_remote_queue_name(self, queue_id='storage', check_worker=True):
"""Returns the proper queue name based on the datastore.
"""
if self.datastore: if self.datastore:
return self.datastore.get_remote_queue_name(queue_id) return self.datastore.get_remote_queue_name(queue_id, check_worker)
else: else:
return None return None
def __unicode__(self): def __unicode__(self):
return u"%s (#%d)" % (self.name, self.id) return u"%s (#%d)" % (self.name, self.id or 0)
def clean(self, *args, **kwargs): def clean(self, *args, **kwargs):
if self.size == "" and self.base: if self.size == "" and self.base:
self.size = self.base.size self.size = self.base.size
super(Disk, self).clean(*args, **kwargs) super(Disk, self).clean(*args, **kwargs)
def deploy(self, user=None, task_uuid=None): def deploy(self, user=None, task_uuid=None, timeout=15):
"""Reify the disk model on the associated data store. """Reify the disk model on the associated data store.
:param self: the disk model to reify :param self: the disk model to reify
...@@ -203,14 +276,16 @@ class Disk(AclBase, TimeStampedModel): ...@@ -203,14 +276,16 @@ class Disk(AclBase, TimeStampedModel):
# Delegate create / snapshot jobs # Delegate create / snapshot jobs
queue_name = self.get_remote_queue_name('storage') queue_name = self.get_remote_queue_name('storage')
disk_desc = self.get_disk_desc() disk_desc = self.get_disk_desc()
if self.type == 'qcow2-snap': if self.base is not None:
with act.sub_activity('creating_snapshot'): with act.sub_activity('creating_snapshot'):
remote_tasks.snapshot.apply_async(args=[disk_desc], remote_tasks.snapshot.apply_async(args=[disk_desc],
queue=queue_name).get() queue=queue_name
).get(timeout=timeout)
else: else:
with act.sub_activity('creating_disk'): with act.sub_activity('creating_disk'):
remote_tasks.create.apply_async(args=[disk_desc], remote_tasks.create.apply_async(args=[disk_desc],
queue=queue_name).get() queue=queue_name
).get(timeout=timeout)
self.ready = True self.ready = True
self.save() self.save()
...@@ -223,6 +298,104 @@ class Disk(AclBase, TimeStampedModel): ...@@ -223,6 +298,104 @@ class Disk(AclBase, TimeStampedModel):
return local_tasks.deploy.apply_async(args=[self, user], return local_tasks.deploy.apply_async(args=[self, user],
queue="localhost.man") queue="localhost.man")
@classmethod
def create(cls, **params):
datastore = params.pop('datastore', DataStore.objects.get())
disk = cls(filename=str(uuid.uuid4()), datastore=datastore, **params)
disk.save()
return disk
@classmethod
def create_empty(cls, instance=None, user=None, **kwargs):
"""Create empty Disk object.
:param instance: Instance or template attach the Disk to.
:type instance: vm.models.Instance or InstanceTemplate or NoneType
:param user: Creator of the disk.
:type user: django.contrib.auth.User
:return: Disk object without a real image, to be .deploy()ed later.
"""
disk = cls.create(**kwargs)
with disk_activity(code_suffix="create", user=user, disk=disk):
if instance:
instance.disks.add(disk)
return disk
@classmethod
def create_from_url_async(cls, url, instance=None, user=None, **kwargs):
"""Create disk object and download data from url asynchrnously.
:param url: URL of image to download.
:type url: string
:param instance: Instance or template attach the Disk to.
:type instance: vm.models.Instance or InstanceTemplate or NoneType
:param user: owner of the disk
:type user: django.contrib.auth.User
:return: Task
:rtype: AsyncResult
"""
kwargs.update({'cls': cls, 'url': url,
'instance': instance, 'user': user})
return local_tasks.create_from_url.apply_async(
kwargs=kwargs, queue='localhost.man')
@classmethod
def create_from_url(cls, url, instance=None, user=None,
task_uuid=None, abortable_task=None, **kwargs):
"""Create disk object and download data from url synchronusly.
:param url: image url to download.
:type url: url
:param instance: Instance or template attach the Disk to.
:type instance: vm.models.Instance or InstanceTemplate or NoneType
:param user: owner of the disk
:type user: django.contrib.auth.User
:param task_uuid: TODO
:param abortable_task: TODO
:return: The created Disk object
:rtype: Disk
"""
kwargs.setdefault('name', url.split('/')[-1])
disk = Disk.create(type="iso", size=1, **kwargs)
# TODO get proper datastore
disk.datastore = DataStore.objects.get()
if instance:
instance.disks.add(disk)
queue_name = disk.get_remote_queue_name('storage')
def __on_abort(activity, error):
activity.disk.destroyed = timezone.now()
activity.disk.save()
if abortable_task:
from celery.contrib.abortable import AbortableAsyncResult
class AbortException(Exception):
pass
with disk_activity(code_suffix='download', disk=disk,
task_uuid=task_uuid, user=user,
on_abort=__on_abort):
result = remote_tasks.download.apply_async(
kwargs={'url': url, 'parent_id': task_uuid,
'disk': disk.get_disk_desc()},
queue=queue_name)
while True:
try:
size = result.get(timeout=5)
break
except TimeoutError:
if abortable_task and abortable_task.is_aborted():
AbortableAsyncResult(result.id).abort()
raise AbortException("Download aborted by user.")
disk.size = size
disk.ready = True
disk.save()
return disk
def destroy(self, user=None, task_uuid=None): def destroy(self, user=None, task_uuid=None):
if self.destroyed: if self.destroyed:
return False return False
...@@ -241,7 +414,7 @@ class Disk(AclBase, TimeStampedModel): ...@@ -241,7 +414,7 @@ class Disk(AclBase, TimeStampedModel):
queue='localhost.man') queue='localhost.man')
def restore(self, user=None, task_uuid=None): def restore(self, user=None, task_uuid=None):
"""Restore destroyed disk. """Recover destroyed disk from trash if possible.
""" """
# TODO # TODO
pass pass
...@@ -250,38 +423,46 @@ class Disk(AclBase, TimeStampedModel): ...@@ -250,38 +423,46 @@ class Disk(AclBase, TimeStampedModel):
local_tasks.restore.apply_async(args=[self, user], local_tasks.restore.apply_async(args=[self, user],
queue='localhost.man') queue='localhost.man')
def save_as(self, user=None, task_uuid=None): def save_as_async(self, disk, task_uuid=None, timeout=300, user=None):
return local_tasks.save_as.apply_async(args=[disk, timeout, user],
queue="localhost.man")
def save_as(self, user=None, task_uuid=None, timeout=300):
"""Save VM as template.
VM must be in STOPPED state to perform this action.
The timeout parameter is not used now.
"""
mapping = { mapping = {
'qcow2-snap': ('qcow2-norm', self.base), 'qcow2-snap': ('qcow2-norm', self.base),
} }
if self.type not in mapping.keys(): if self.type not in mapping.keys():
raise self.WrongDiskTypeError(self.type) raise self.WrongDiskTypeError(self.type)
if self.is_in_use(): if self.is_in_use:
raise self.DiskInUseError(self) raise self.DiskInUseError(self)
# from this point on, the caller has to guarantee that the disk is not # from this point on, the caller has to guarantee that the disk is not
# going to be used until the operation is complete # going to be used until the operation is complete
with disk_activity(code_suffix='save_as', disk=self, new_type, new_base = mapping[self.type]
task_uuid=task_uuid, user=user):
filename = str(uuid.uuid4()) disk = Disk.create(base=new_base, datastore=self.datastore,
new_type, new_base = mapping[self.type] name=self.name, size=self.size,
type=new_type)
disk = Disk.objects.create(base=new_base, datastore=self.datastore,
filename=filename, name=self.name,
size=self.size, type=new_type)
disk.save()
with disk_activity(code_suffix="save_as", disk=self,
user=user, task_uuid=None):
queue_name = self.get_remote_queue_name('storage') queue_name = self.get_remote_queue_name('storage')
remote_tasks.merge.apply_async(args=[self.get_disk_desc(), remote_tasks.merge.apply_async(args=[self.get_disk_desc(),
disk.get_disk_desc()], disk.get_disk_desc()],
queue=queue_name).get() queue=queue_name
).get() # Timeout
disk.ready = True disk.ready = True
disk.save() disk.save()
return disk return disk
class DiskActivity(ActivityModel): class DiskActivity(ActivityModel):
...@@ -312,6 +493,7 @@ class DiskActivity(ActivityModel): ...@@ -312,6 +493,7 @@ class DiskActivity(ActivityModel):
@contextmanager @contextmanager
def disk_activity(code_suffix, disk, task_uuid=None, user=None): def disk_activity(code_suffix, disk, task_uuid=None, user=None,
on_abort=None, on_commit=None):
act = DiskActivity.create(code_suffix, disk, task_uuid, user) act = DiskActivity.create(code_suffix, disk, task_uuid, user)
return activitycontextimpl(act) return activitycontextimpl(act, on_abort=on_abort, on_commit=on_commit)
from manager.mancelery import celery from manager.mancelery import celery
from celery.contrib.abortable import AbortableTask
@celery.task
def check_queue(storage, queue_id):
''' Celery inspect job to check for active workers at queue_id
return True/False
'''
drivers = ['storage', 'download']
worker_list = [storage + "." + d for d in drivers]
queue_name = storage + "." + queue_id
# v is List of List of queues dict
active_queues = celery.control.inspect(worker_list).active_queues()
if active_queues is not None:
node_workers = [v for k, v in active_queues.iteritems()]
for worker in node_workers:
for queue in worker:
if queue['name'] == queue_name:
return True
return False
@celery.task
def save_as(disk, timeout, user):
disk.save_disk_as(task_uuid=save_as.request.id, user=user,
disk=disk, timeout=timeout)
@celery.task @celery.task
...@@ -14,3 +40,23 @@ def destroy(disk, user): ...@@ -14,3 +40,23 @@ def destroy(disk, user):
@celery.task @celery.task
def restore(disk, user): def restore(disk, user):
disk.restore(task_uuid=restore.request.id, user=user) disk.restore(task_uuid=restore.request.id, user=user)
class CreateFromURLTask(AbortableTask):
def __init__(self):
self.bind(celery)
def run(self, **kwargs):
Disk = kwargs.pop('cls')
Disk.create_from_url(url=kwargs.pop('url'),
task_uuid=create_from_url.request.id,
abortable_task=self,
**kwargs)
create_from_url = CreateFromURLTask()
@celery.task
def create_empty(Disk, instance, params, user):
Disk.create_empty(instance, params, user,
task_uuid=create_empty.request.id)
from storage.models import DataStore
import os
from manager.mancelery import celery
import logging
from storage.tasks import remote_tasks
logger = logging.getLogger(__name__)
@celery.task
def garbage_collector(timeout=15):
""" Garbage collector for disk images.
Moves 1 day old deleted images to trash folder.
If there is not enough free space on datastore (default 10%)
deletes oldest images from trash.
:param timeout: Seconds before TimeOut exception
:type timeoit: 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):
logger.info("Image: %s at Datastore: %s moved to trash folder." %
(i, ds.path))
remote_tasks.move_to_trash.apply_async(
args=[ds.path, i], queue=queue_name).get(timeout=timeout)
try:
remote_tasks.make_free_space.apply_async(
args=[ds.path], queue=queue_name).get(timeout=timeout)
except Exception as e:
logger.warning(str(e))
@celery.task
def list_orphan_disks(timeout=15):
"""List disk image files without Disk object in the database.
Exclude cloud-xxxxxxxx.dump format images.
:param timeout: Seconds before TimeOut exception
:type timeoit: int
"""
import re
for ds in DataStore.objects.all():
queue_name = ds.get_remote_queue_name('storage')
files = set(remote_tasks.list_files.apply_async(
args=[ds.path], queue=queue_name).get(timeout=timeout))
disks = set([disk.filename for disk in ds.disk_set.all()])
for i in files - disks:
if not re.match('cloud-[0-9]*\.dump', i):
logging.warning("Orphan disk: %s" % i)
@celery.task
def list_missing_disks(timeout=15):
"""List Disk objects without disk image files.
:param timeout: Seconds before TimeOut exception
:type timeoit: int
"""
for ds in DataStore.objects.all():
queue_name = ds.get_remote_queue_name('storage')
files = set(remote_tasks.list_files.apply_async(
args=[ds.path], queue=queue_name).get(timeout=timeout))
disks = set([disk.filename for disk in
ds.disk_set.filter(destroyed__isnull=True)])
for i in disks - files:
logging.critical("Image: %s is missing from %s datastore."
% (i, ds.path))
...@@ -6,11 +6,21 @@ def list(dir): ...@@ -6,11 +6,21 @@ def list(dir):
pass pass
@celery.task(name='storagedriver.list_files')
def list_files(dir):
pass
@celery.task(name='storagedriver.create') @celery.task(name='storagedriver.create')
def create(disk_desc): def create(disk_desc):
pass pass
@celery.task(name='storagedriver.download')
def download(disk_desc, url):
pass
@celery.task(name='storagedriver.delete') @celery.task(name='storagedriver.delete')
def delete(path): def delete(path):
pass pass
...@@ -34,3 +44,18 @@ def get(path): ...@@ -34,3 +44,18 @@ def get(path):
@celery.task(name='storagedriver.merge') @celery.task(name='storagedriver.merge')
def merge(src_disk_desc, dst_disk_desc): def merge(src_disk_desc, dst_disk_desc):
pass pass
@celery.task(name='storagedriver.make_free_space')
def make_free_space(datastore, percent):
pass
@celery.task(name='storagedriver.move_to_trash')
def move_to_trash(datastore, disk_path):
pass
@celery.task(name='storagedriver.get_storage_stat')
def get_storage_stat(path):
pass
"""
This file demonstrates writing tests using the unittest module. These will pass
when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
from django.test import TestCase
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.assertEqual(1 + 1, 2)
from datetime import timedelta
from django.test import TestCase
from django.utils import timezone
from ..models import Disk, DataStore
old = timezone.now() - timedelta(days=2)
new = timezone.now() - timedelta(hours=2)
class DiskTestCase(TestCase):
n = 0
def setUp(self):
self.ds = DataStore.objects.create(path="/datastore",
hostname="devenv", name="default")
def _disk(self, destroyed=None, base=None):
self.n += 1
n = "d%d" % self.n
return Disk.objects.create(name=n, filename=n, base=base, size=1,
destroyed=destroyed, datastore=self.ds)
def test_deletable_not_destroyed(self):
d = self._disk()
assert not d.is_deletable
def test_deletable_newly_destroyed(self):
d = self._disk(destroyed=new)
assert not d.is_deletable
def test_deletable_no_child(self):
d = self._disk(destroyed=old)
assert d.is_deletable
def test_deletable_child_not_destroyed(self):
d = self._disk()
self._disk(base=d, destroyed=old)
self._disk(base=d)
assert not d.is_deletable
def test_deletable_child_newly_destroyed(self):
d = self._disk(destroyed=old)
self._disk(base=d, destroyed=new)
self._disk(base=d)
assert not d.is_deletable
{% load i18n %}
{% load staticfiles %}
{% get_current_language as LANGUAGE_CODE %}
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}{% endblock %} | CIRCLE</title>
<meta name="description" content="">
<meta name="author" content="">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
<link href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css" rel="stylesheet">
<style type="text/css">
html, body {
background-color: #eee;
}
body {
margin-top: 40px;
}
.container {
width: 600px;
}
ul, li {
list-style: none;
margin: 0;
padding: 0;
}
.container > .content {
background-color: #fff;
padding: 20px;
-webkit-border-radius: 10px 10px 10px 10px;
-moz-border-radius: 10px 10px 10px 10px;
border-radius: 10px 10px 10px 10px;
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,.15);
-moz-box-shadow: 0 1px 2px rgba(0,0,0,.15);
box-shadow: 0 1px 2px rgba(0,0,0,.15);
}
.login-form-errors .alert {
margin-right: 30px;
margin-left: 30px;
}
.login-form {
margin-top: 40px;
padding: 0 10px;
}
.login-form form {
padding: 0 20px;
}
.input-group {
margin-bottom: 10px;
}
.input-group-addon {
width: 38px;
}
.form-group label {
margin-top: 20px;
}
#submit-password-button {
margin-top: 15px;
}
/* fix for crispy-forms' html */
.form-group {
margin-bottom: 0px;
}
.help-block {
display: none;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
{% block content %}{% endblock %}
</div>
</div> <!-- /container -->
</body>
</html>
{% extends "base.html" %} {% extends "registration/base.html" %}
{% load i18n %} {% load i18n %}
{% load staticfiles %} {% load crispy_forms_tags %}
{% get_current_language as LANGUAGE_CODE %} {% get_current_language as LANGUAGE_CODE %}
{% block title %}{% trans "Login" %}{% endblock %}
{% block content %} {% block content %}
<form action="" method="POST"> <div class="row">
{% csrf_token %} {% if form.password.errors or form.username.errors %}
{{ form }} <div class="login-form-errors">
<input type="submit" value="LOGIN" /> {% include "display-form-errors.html" %}
</form> </div>
{% endif %}
<div class="col-sm-{% if saml2 %}6{% else %}12{% endif %}">
<div class="login-form">
<form action="" method="POST">
{% csrf_token %}
{% crispy form %}
</form>
</div>
</div>
{% if saml2 %}
<div class="col-sm-6">
<h4 style="padding-top: 0; margin-top: 0;">{% trans "Login with SSO" %}</h4>
<a href="{% url "saml2_login" %}">{% trans "Click here!" %}</a>
</div>
{% endif %}
</div>
<div class="row">
<div class="col-sm-12">
<a class="pull-right" href="{% url "accounts.password-reset" %}">{% trans "Forgot your password?" %}</a>
</div>
</div>
{% endblock %} {% endblock %}
{% extends "registration/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% get_current_language as LANGUAGE_CODE %}
{% block title %}{% trans "Password reset complete" %}{% endblock %}
{% block content %}
<div class="row">
<div class="login-form-errors">
{% include "display-form-errors.html" %}
</div>
<div class="col-sm-12">
<div class="alert alert-success">
{% trans "Password change successful!" %}
<a href="{% url "accounts.login" %}">{% trans "Click here to login" %}</a>
</div>
</div>
</div>
{% endblock %}
{% extends "registration/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% get_current_language as LANGUAGE_CODE %}
{% block title %}{% trans "Password reset confirm" %}{% endblock %}
{% block content %}
<div class="row">
<div class="login-form-errors">
{% include "display-form-errors.html" %}
</div>
<div class="col-sm-12">
<div style="margin: 0 0 25px 0;">
{% blocktrans %}Please enter your new password twice so we can verify you typed it in correctly!{% endblocktrans %}
</div>
{% if form %}
{% crispy form %}
{% else %}
<div class="alert alert-warning">
{% url "accounts.password-reset" as url %}
{% blocktrans with url=url %}This token is expired, please <a href="{{ url }}">request</a> a new password reset link again!{% endblocktrans %}
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% extends "registration/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% get_current_language as LANGUAGE_CODE %}
{% block title %}{% trans "Password reset done" %}{% endblock %}
{% block content %}
<div class="row">
<div class="login-form-errors">
{% include "display-form-errors.html" %}
</div>
<div class="col-sm-12">
<div class="pull-right"><a href="{% url "accounts.login" %}">{% trans "Back to login" %}</a></div>
{% trans "We have sent you an email about your next steps!" %}
</div>
</div>
{% endblock %}
{% extends "registration/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% get_current_language as LANGUAGE_CODE %}
{% block title %}{% trans "Password reset" %}{% endblock %}
{% block content %}
<div class="row">
<div class="login-form-errors">
{% include "display-form-errors.html" %}
</div>
<div class="col-sm-12">
<div class="pull-right"><a href="{% url "accounts.login" %}">{% trans "Back to login" %}</a></div>
<h4 style="margin: 0 0 25px 0;">{% blocktrans %}Enter your email address to reset your password!{% endblocktrans %}</h4>
<form action="" method="POST">
{% csrf_token %}
{% crispy form %}
</form>
</div>
</div>
{% endblock %}
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'Instance.system'
db.add_column(u'vm_instance', 'system',
self.gf('django.db.models.fields.TextField')(default='', blank=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'Instance.system'
db.delete_column(u'vm_instance', 'system')
models = {
u'acl.level': {
'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Level'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'weight': ('django.db.models.fields.IntegerField', [], {'null': 'True'})
},
u'acl.objectlevel': {
'Meta': {'unique_together': "(('content_type', 'object_id', 'level'),)", 'object_name': 'ObjectLevel'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'level': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['acl.Level']"}),
'object_id': ('django.db.models.fields.IntegerField', [], {}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'symmetrical': 'False'})
},
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'firewall.domain': {
'Meta': {'object_name': 'Domain'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'ttl': ('django.db.models.fields.IntegerField', [], {'default': '600'})
},
u'firewall.group': {
'Meta': {'object_name': 'Group'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'firewall.host': {
'Meta': {'unique_together': "(('hostname', 'vlan'),)", 'object_name': 'Host'},
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['firewall.Group']", 'null': 'True', 'blank': 'True'}),
'hostname': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ipv4': ('firewall.fields.IPAddressField', [], {'unique': 'True', 'max_length': '100'}),
'ipv6': ('firewall.fields.IPAddressField', [], {'max_length': '100', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'location': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'mac': ('firewall.fields.MACAddressField', [], {'unique': 'True', 'max_length': '17'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'pub_ipv4': ('firewall.fields.IPAddressField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
'reverse': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
'shared_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'vlan': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Vlan']"})
},
u'firewall.vlan': {
'Meta': {'object_name': 'Vlan'},
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'dhcp_pool': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Domain']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ipv6_template': ('django.db.models.fields.TextField', [], {'default': "'2001:738:2001:4031:%(b)d:%(c)d:%(d)d:0'"}),
'managed': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'network4': ('firewall.fields.IPNetworkField', [], {'max_length': '100'}),
'network6': ('firewall.fields.IPNetworkField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
'network_type': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'reverse_domain': ('django.db.models.fields.TextField', [], {'default': "'%(d)d.%(c)d.%(b)d.%(a)d.in-addr.arpa'"}),
'snat_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
'snat_to': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['firewall.Vlan']", 'null': 'True', 'blank': 'True'}),
'vid': ('django.db.models.fields.IntegerField', [], {'unique': 'True'})
},
u'storage.datastore': {
'Meta': {'ordering': "['name']", 'object_name': 'DataStore'},
'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'})
},
u'storage.disk': {
'Meta': {'ordering': "['name']", 'object_name': 'Disk'},
'base': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'derivatives'", 'null': 'True', 'to': u"orm['storage.Disk']"}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'datastore': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['storage.DataStore']"}),
'destroyed': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'dev_num': ('django.db.models.fields.CharField', [], {'default': "'a'", 'max_length': '1'}),
'filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'ready': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'size': ('sizefield.models.FileSizeField', [], {}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '10'})
},
u'taggit.tag': {
'Meta': {'object_name': 'Tag'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'})
},
u'taggit.taggeditem': {
'Meta': {'object_name': 'TaggedItem'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_tagged_items'", 'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_items'", 'to': u"orm['taggit.Tag']"})
},
u'vm.instance': {
'Meta': {'ordering': "[u'pk']", 'object_name': 'Instance'},
'access_method': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'active_since': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'boot_menu': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'destroyed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'disks': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'instance_set'", 'symmetrical': 'False', 'to': u"orm['storage.Disk']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lease': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Lease']"}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'instance_set'", 'null': 'True', 'to': u"orm['vm.Node']"}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'pw': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
'ram_size': ('django.db.models.fields.IntegerField', [], {}),
'raw_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'req_traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'}),
'system': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'instance_set'", 'null': 'True', 'to': u"orm['vm.InstanceTemplate']"}),
'time_of_delete': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'time_of_suspend': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'vnc_port': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'})
},
u'vm.instanceactivity': {
'Meta': {'ordering': "[u'-finished', u'-started', u'instance', u'-id']", 'object_name': 'InstanceActivity'},
'activity_code': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'finished': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'activity_log'", 'to': u"orm['vm.Instance']"}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': u"orm['vm.InstanceActivity']"}),
'result': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'resultant_state': ('django.db.models.fields.CharField', [], {'max_length': '20', 'null': 'True', 'blank': 'True'}),
'started': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'succeeded': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'task_uuid': ('django.db.models.fields.CharField', [], {'max_length': '50', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'vm.instancetemplate': {
'Meta': {'ordering': "[u'name']", 'object_name': 'InstanceTemplate'},
'access_method': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'boot_menu': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'disks': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'template_set'", 'symmetrical': 'False', 'to': u"orm['storage.Disk']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lease': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Lease']"}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.InstanceTemplate']", 'null': 'True', 'blank': 'True'}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'ram_size': ('django.db.models.fields.IntegerField', [], {}),
'raw_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'req_traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'default': "u'NEW'", 'max_length': '10'}),
'system': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
u'vm.interface': {
'Meta': {'object_name': 'Interface'},
'host': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Host']", 'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'interface_set'", 'to': u"orm['vm.Instance']"}),
'vlan': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'vm_interface'", 'to': u"orm['firewall.Vlan']"})
},
u'vm.interfacetemplate': {
'Meta': {'object_name': 'InterfaceTemplate'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'managed': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'interface_set'", 'to': u"orm['vm.InstanceTemplate']"}),
'vlan': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Vlan']"})
},
u'vm.lease': {
'Meta': {'ordering': "[u'name']", 'object_name': 'Lease'},
'delete_interval_seconds': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'suspend_interval_seconds': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'})
},
u'vm.namedbaseresourceconfig': {
'Meta': {'object_name': 'NamedBaseResourceConfig'},
'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'ram_size': ('django.db.models.fields.IntegerField', [], {})
},
u'vm.node': {
'Meta': {'object_name': 'Node'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'host': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Host']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}),
'overcommit': ('django.db.models.fields.FloatField', [], {'default': '1.0'}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'})
},
u'vm.nodeactivity': {
'Meta': {'object_name': 'NodeActivity'},
'activity_code': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'finished': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'node': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'activity_log'", 'to': u"orm['vm.Node']"}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': u"orm['vm.NodeActivity']"}),
'result': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'started': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'succeeded': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'task_uuid': ('django.db.models.fields.CharField', [], {'max_length': '50', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'vm.trait': {
'Meta': {'object_name': 'Trait'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
}
}
complete_apps = ['vm']
\ No newline at end of file
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Deleting field 'InstanceTemplate.state'
db.delete_column(u'vm_instancetemplate', 'state')
def backwards(self, orm):
# Adding field 'InstanceTemplate.state'
db.add_column(u'vm_instancetemplate', 'state',
self.gf('django.db.models.fields.CharField')(default=u'NEW', max_length=10),
keep_default=False)
models = {
u'acl.level': {
'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Level'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'weight': ('django.db.models.fields.IntegerField', [], {'null': 'True'})
},
u'acl.objectlevel': {
'Meta': {'unique_together': "(('content_type', 'object_id', 'level'),)", 'object_name': 'ObjectLevel'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'level': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['acl.Level']"}),
'object_id': ('django.db.models.fields.IntegerField', [], {}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'symmetrical': 'False'})
},
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'firewall.domain': {
'Meta': {'object_name': 'Domain'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'ttl': ('django.db.models.fields.IntegerField', [], {'default': '600'})
},
u'firewall.group': {
'Meta': {'object_name': 'Group'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'firewall.host': {
'Meta': {'unique_together': "(('hostname', 'vlan'),)", 'object_name': 'Host'},
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['firewall.Group']", 'null': 'True', 'blank': 'True'}),
'hostname': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ipv4': ('firewall.fields.IPAddressField', [], {'unique': 'True', 'max_length': '100'}),
'ipv6': ('firewall.fields.IPAddressField', [], {'max_length': '100', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'location': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'mac': ('firewall.fields.MACAddressField', [], {'unique': 'True', 'max_length': '17'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'pub_ipv4': ('firewall.fields.IPAddressField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
'reverse': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
'shared_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'vlan': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Vlan']"})
},
u'firewall.vlan': {
'Meta': {'object_name': 'Vlan'},
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'dhcp_pool': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Domain']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ipv6_template': ('django.db.models.fields.TextField', [], {'default': "'2001:738:2001:4031:%(b)d:%(c)d:%(d)d:0'"}),
'managed': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'network4': ('firewall.fields.IPNetworkField', [], {'max_length': '100'}),
'network6': ('firewall.fields.IPNetworkField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
'network_type': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'reverse_domain': ('django.db.models.fields.TextField', [], {'default': "'%(d)d.%(c)d.%(b)d.%(a)d.in-addr.arpa'"}),
'snat_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
'snat_to': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['firewall.Vlan']", 'null': 'True', 'blank': 'True'}),
'vid': ('django.db.models.fields.IntegerField', [], {'unique': 'True'})
},
u'storage.datastore': {
'Meta': {'ordering': "['name']", 'object_name': 'DataStore'},
'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'})
},
u'storage.disk': {
'Meta': {'ordering': "['name']", 'object_name': 'Disk'},
'base': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'derivatives'", 'null': 'True', 'to': u"orm['storage.Disk']"}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'datastore': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['storage.DataStore']"}),
'destroyed': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'dev_num': ('django.db.models.fields.CharField', [], {'default': "'a'", 'max_length': '1'}),
'filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'ready': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'size': ('sizefield.models.FileSizeField', [], {}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '10'})
},
u'taggit.tag': {
'Meta': {'object_name': 'Tag'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'})
},
u'taggit.taggeditem': {
'Meta': {'object_name': 'TaggedItem'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_tagged_items'", 'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_items'", 'to': u"orm['taggit.Tag']"})
},
u'vm.instance': {
'Meta': {'ordering': "[u'pk']", 'object_name': 'Instance'},
'access_method': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'active_since': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'boot_menu': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'destroyed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'disks': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'instance_set'", 'symmetrical': 'False', 'to': u"orm['storage.Disk']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lease': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Lease']"}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'instance_set'", 'null': 'True', 'to': u"orm['vm.Node']"}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'pw': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
'ram_size': ('django.db.models.fields.IntegerField', [], {}),
'raw_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'req_traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'}),
'system': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'instance_set'", 'null': 'True', 'to': u"orm['vm.InstanceTemplate']"}),
'time_of_delete': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'time_of_suspend': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'vnc_port': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'})
},
u'vm.instanceactivity': {
'Meta': {'ordering': "[u'-finished', u'-started', u'instance', u'-id']", 'object_name': 'InstanceActivity'},
'activity_code': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'finished': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'activity_log'", 'to': u"orm['vm.Instance']"}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': u"orm['vm.InstanceActivity']"}),
'result': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'resultant_state': ('django.db.models.fields.CharField', [], {'max_length': '20', 'null': 'True', 'blank': 'True'}),
'started': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'succeeded': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'task_uuid': ('django.db.models.fields.CharField', [], {'max_length': '50', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'vm.instancetemplate': {
'Meta': {'ordering': "[u'name']", 'object_name': 'InstanceTemplate'},
'access_method': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'boot_menu': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'disks': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'template_set'", 'symmetrical': 'False', 'to': u"orm['storage.Disk']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lease': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Lease']"}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.InstanceTemplate']", 'null': 'True', 'blank': 'True'}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'ram_size': ('django.db.models.fields.IntegerField', [], {}),
'raw_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'req_traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'}),
'system': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
u'vm.interface': {
'Meta': {'object_name': 'Interface'},
'host': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Host']", 'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'interface_set'", 'to': u"orm['vm.Instance']"}),
'vlan': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'vm_interface'", 'to': u"orm['firewall.Vlan']"})
},
u'vm.interfacetemplate': {
'Meta': {'object_name': 'InterfaceTemplate'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'managed': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'interface_set'", 'to': u"orm['vm.InstanceTemplate']"}),
'vlan': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Vlan']"})
},
u'vm.lease': {
'Meta': {'ordering': "[u'name']", 'object_name': 'Lease'},
'delete_interval_seconds': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'suspend_interval_seconds': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'})
},
u'vm.namedbaseresourceconfig': {
'Meta': {'object_name': 'NamedBaseResourceConfig'},
'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'ram_size': ('django.db.models.fields.IntegerField', [], {})
},
u'vm.node': {
'Meta': {'object_name': 'Node'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'host': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Host']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}),
'overcommit': ('django.db.models.fields.FloatField', [], {'default': '1.0'}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'})
},
u'vm.nodeactivity': {
'Meta': {'object_name': 'NodeActivity'},
'activity_code': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'finished': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'node': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'activity_log'", 'to': u"orm['vm.Node']"}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': u"orm['vm.NodeActivity']"}),
'result': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'started': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'succeeded': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'task_uuid': ('django.db.models.fields.CharField', [], {'max_length': '50', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'vm.trait': {
'Meta': {'object_name': 'Trait'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
}
}
complete_apps = ['vm']
\ No newline at end of file
...@@ -98,6 +98,18 @@ class NodeActivity(ActivityModel): ...@@ -98,6 +98,18 @@ class NodeActivity(ActivityModel):
app_label = 'vm' app_label = 'vm'
db_table = 'vm_nodeactivity' db_table = 'vm_nodeactivity'
def __unicode__(self):
if self.parent:
return '{}({})->{}'.format(self.parent.activity_code,
self.node,
self.activity_code)
else:
return '{}({})'.format(self.activity_code,
self.node)
def get_readable_name(self):
return self.activity_code.split('.')[-1].replace('_', ' ').capitalize()
@classmethod @classmethod
def create(cls, code_suffix, node, task_uuid=None, user=None): def create(cls, code_suffix, node, task_uuid=None, user=None):
act = cls(activity_code='vm.Node.' + code_suffix, act = cls(activity_code='vm.Node.' + code_suffix,
......
...@@ -5,12 +5,12 @@ from importlib import import_module ...@@ -5,12 +5,12 @@ from importlib import import_module
import string import string
import django.conf import django.conf
from django.db.models import (BooleanField, CharField, DateTimeField,
IntegerField, ForeignKey, Manager,
ManyToManyField, permalink, TextField)
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core import signing from django.core import signing
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db.models import (BooleanField, CharField, DateTimeField,
IntegerField, ForeignKey, Manager,
ManyToManyField, permalink, SET_NULL, TextField)
from django.dispatch import Signal from django.dispatch import Signal
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
...@@ -40,12 +40,26 @@ ACCESS_METHODS = [(key, name) for key, (name, port, transport) ...@@ -40,12 +40,26 @@ ACCESS_METHODS = [(key, name) for key, (name, port, transport)
VNC_PORT_RANGE = (2000, 65536) # inclusive start, exclusive end VNC_PORT_RANGE = (2000, 65536) # inclusive start, exclusive end
def find_unused_port(port_range, used_ports=[]):
"""Find an unused port in the specified range.
The list of used ports can be specified optionally.
:param port_range: a tuple representing a port range (w/ exclusive end)
e.g. (6000, 7000) represents ports 6000 through 6999
"""
ports = xrange(*port_range)
used = set(used_ports)
unused = (port for port in ports if port not in used)
return next(unused, None) # first or None
def find_unused_vnc_port(): def find_unused_vnc_port():
used = set(Instance.objects.values_list('vnc_port', flat=True)) port = find_unused_port(
for p in xrange(*VNC_PORT_RANGE): port_range=VNC_PORT_RANGE,
if p not in used: used_ports=Instance.objects.values_list('vnc_port', flat=True))
return p
else: if port is None:
raise Exception("No unused port could be found for VNC.") raise Exception("No unused port could be found for VNC.")
...@@ -74,6 +88,11 @@ class VirtualMachineDescModel(BaseResourceConfigModel): ...@@ -74,6 +88,11 @@ class VirtualMachineDescModel(BaseResourceConfigModel):
"node to declare to be suitable " "node to declare to be suitable "
"for hosting the VM."), "for hosting the VM."),
verbose_name=_("required traits")) 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'))
tags = TaggableManager(blank=True, verbose_name=_("tags")) tags = TaggableManager(blank=True, verbose_name=_("tags"))
class Meta: class Meta:
...@@ -83,27 +102,12 @@ class VirtualMachineDescModel(BaseResourceConfigModel): ...@@ -83,27 +102,12 @@ class VirtualMachineDescModel(BaseResourceConfigModel):
class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel): class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel):
"""Virtual machine template. """Virtual machine template.
Every template has:
* a name and a description
* an optional parent template
* state of the template
* an OS name/description
* a method of access to the system
* default values of base resource configuration
* list of attached images
* set of interfaces
* lease times (suspension & deletion)
* time of creation and last modification
""" """
ACL_LEVELS = ( ACL_LEVELS = (
('user', _('user')), # see all details ('user', _('user')), # see all details
('operator', _('operator')), ('operator', _('operator')),
('owner', _('owner')), # superuser, can delete, delegate perms ('owner', _('owner')), # superuser, can delete, delegate perms
) )
STATES = [('NEW', _('new')), # template has just been created
('SAVING', _('saving')), # changes are being saved
('READY', _('ready'))] # template is ready for instantiation
name = CharField(max_length=100, unique=True, name = CharField(max_length=100, unique=True,
verbose_name=_('name'), verbose_name=_('name'),
help_text=_('Human readable name of template.')) help_text=_('Human readable name of template.'))
...@@ -111,12 +115,6 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -111,12 +115,6 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel):
parent = ForeignKey('self', null=True, blank=True, parent = ForeignKey('self', null=True, blank=True,
verbose_name=_('parent template'), verbose_name=_('parent template'),
help_text=_('Template which this one is derived of.')) help_text=_('Template which this one is derived of.'))
system = TextField(verbose_name=_('operating system'),
blank=True,
help_text=(_('Name of operating system in '
'format like "%s".') %
'Ubuntu 12.04 LTS Desktop amd64'))
state = CharField(max_length=10, choices=STATES, default='NEW')
disks = ManyToManyField(Disk, verbose_name=_('disks'), disks = ManyToManyField(Disk, verbose_name=_('disks'),
related_name='template_set', related_name='template_set',
help_text=_('Disks which are to be mounted.')) help_text=_('Disks which are to be mounted.'))
...@@ -125,7 +123,7 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -125,7 +123,7 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel):
class Meta: class Meta:
app_label = 'vm' app_label = 'vm'
db_table = 'vm_instancetemplate' db_table = 'vm_instancetemplate'
ordering = ['name', ] ordering = ('name', )
permissions = ( permissions = (
('create_template', _('Can create an instance template.')), ('create_template', _('Can create an instance template.')),
) )
...@@ -135,15 +133,15 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -135,15 +133,15 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel):
def __unicode__(self): def __unicode__(self):
return self.name return self.name
@property
def running_instances(self): def running_instances(self):
"""Returns the number of running instances of the template. """The number of running instances of the template.
""" """
return len([i for i in self.instance_set.all() return sum(1 for i in self.instance_set.all() if i.is_running)
if i.state == 'RUNNING'])
@property @property
def os_type(self): def os_type(self):
"""Get the type of the template's operating system. """The type of the template's operating system.
""" """
if self.access_method == 'rdp': if self.access_method == 'rdp':
return 'windows' return 'windows'
...@@ -156,24 +154,14 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -156,24 +154,14 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel):
if is_new: if is_new:
self.set_level(self.owner, 'owner') self.set_level(self.owner, 'owner')
@permalink
def get_absolute_url(self):
return ('dashboard.views.template-detail', None, {'pk': self.pk})
class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
"""Virtual machine instance. """Virtual machine instance.
Every instance has:
* a name and a description
* an optional parent template
* associated share
* a generated password for login authentication
* time of deletion and time of suspension
* lease times (suspension & deletion)
* last boot timestamp
* host node
* current state (libvirt domain state)
* time of creation and last modification
* base resource configuration values
* owner and privilege information
""" """
ACL_LEVELS = ( ACL_LEVELS = (
('user', _('user')), # see all details ('user', _('user')), # see all details
...@@ -184,7 +172,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -184,7 +172,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
help_text=_("Human readable name of instance.")) help_text=_("Human readable name of instance."))
description = TextField(blank=True, verbose_name=_('description')) description = TextField(blank=True, verbose_name=_('description'))
template = ForeignKey(InstanceTemplate, blank=True, null=True, template = ForeignKey(InstanceTemplate, blank=True, null=True,
related_name='instance_set', related_name='instance_set', on_delete=SET_NULL,
help_text=_("Template the instance derives from."), help_text=_("Template the instance derives from."),
verbose_name=_('template')) verbose_name=_('template'))
pw = CharField(help_text=_("Original password of the instance."), pw = CharField(help_text=_("Original password of the instance."),
...@@ -221,7 +209,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -221,7 +209,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
class Meta: class Meta:
app_label = 'vm' app_label = 'vm'
db_table = 'vm_instance' db_table = 'vm_instance'
ordering = ['pk', ] ordering = ('pk', )
permissions = ( permissions = (
('access_console', _('Can access the graphical console of a VM.')), ('access_console', _('Can access the graphical console of a VM.')),
('change_resources', _('Can change resources of a running VM.')), ('change_resources', _('Can change resources of a running VM.')),
...@@ -255,51 +243,66 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -255,51 +243,66 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
self.instance = instance self.instance = instance
def __unicode__(self): def __unicode__(self):
parts = [self.name, "(" + str(self.id) + ")"] parts = (self.name, "(" + str(self.id) + ")")
return " ".join([s for s in parts if s != ""]) return " ".join(s for s in parts if s != "")
@property
def is_console_available(self):
return self.is_running
@property
def is_running(self):
return self.state == 'RUNNING'
@property @property
def state(self): def state(self):
"""State of the virtual machine instance. """State of the virtual machine instance.
""" """
# check special cases
if self.activity_log.filter(activity_code__endswith='migrate', if self.activity_log.filter(activity_code__endswith='migrate',
finished__isnull=True).exists(): finished__isnull=True).exists():
return 'MIGRATING' return 'MIGRATING'
# <<< add checks for special cases before this
# default case
acts = self.activity_log.filter(finished__isnull=False,
resultant_state__isnull=False
).order_by('-finished')[:1]
try: try:
act = self.activity_log.filter(finished__isnull=False, act = acts[0]
resultant_state__isnull=False
).order_by('-finished').all()[0]
except IndexError: except IndexError:
act = None return 'NOSTATE'
return 'NOSTATE' if act is None else act.resultant_state else:
return act.resultant_state
def manual_state_change(self, new_state, reason=None, user=None): @classmethod
# TODO cancel concurrent activity (if exists) def create(cls, params, disks, networks, req_traits, tags):
act = InstanceActivity.create(code_suffix='manual_state_change', # create instance and do additional setup
instance=self, user=user) inst = cls(**params)
act.finished = act.started
act.result = reason
act.resultant_state = new_state
act.succeeded = True
act.save()
def vm_state_changed(self, new_state): # save instance
try: inst.full_clean()
act = InstanceActivity.create(code_suffix='vm_state_changed', inst.save()
instance=self) inst.set_level(inst.owner, 'owner')
except ActivityInProgressError:
pass # discard state change if another activity is in progress.
else:
act.finished = act.started
act.resultant_state = new_state
act.succeeded = True
act.save()
def clean(self, *args, **kwargs): def __on_commit(activity):
if self.time_of_delete is None: activity.resultant_state = 'PENDING'
self.renew(which='delete')
super(Instance, self).clean(*args, **kwargs) with instance_activity(code_suffix='create', instance=inst,
on_commit=__on_commit, user=inst.owner) as act:
# create related entities
inst.disks.add(*[disk.get_exclusive() for disk in disks])
for net in networks:
Interface.create(instance=inst, vlan=net.vlan,
owner=inst.owner, managed=net.managed,
base_activity=act)
inst.req_traits.add(*req_traits)
inst.tags.add(*tags)
return inst
@classmethod @classmethod
def create_from_template(cls, template, owner, disks=None, networks=None, def create_from_template(cls, template, owner, disks=None, networks=None,
...@@ -352,35 +355,47 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -352,35 +355,47 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
params.update([(f, getattr(template, f)) for f in common_fields]) params.update([(f, getattr(template, f)) for f in common_fields])
params.update(kwargs) # override defaults w/ user supplied values params.update(kwargs) # override defaults w/ user supplied values
return [cls.__create_instance(params, disks, networks, req_traits, if amount > 1 and '%d' not in params['name']:
tags) for i in xrange(amount)] params['name'] += ' %d'
@classmethod
def __create_instance(cls, params, disks, networks, req_traits, tags):
# create instance and do additional setup
inst = cls(**params)
# save instance
inst.clean()
inst.save()
inst.set_level(inst.owner, 'owner')
def __on_commit(activity): customized_params = (dict(params,
activity.resultant_state = 'PENDING' name=params['name'].replace('%d', str(i)))
for i in xrange(amount))
return [cls.create(cps, disks, networks, req_traits, tags)
for cps in customized_params]
with instance_activity(code_suffix='create', instance=inst, def clean(self, *args, **kwargs):
on_commit=__on_commit, user=inst.owner): if self.time_of_delete is None:
# create related entities self._do_renew(which='delete')
inst.disks.add(*[disk.get_exclusive() for disk in disks]) super(Instance, self).clean(*args, **kwargs)
for net in networks: def manual_state_change(self, new_state, reason=None, user=None):
Interface.create(instance=inst, vlan=net.vlan, # TODO cancel concurrent activity (if exists)
owner=inst.owner, managed=net.managed) act = InstanceActivity.create(code_suffix='manual_state_change',
instance=self, user=user)
act.finished = act.started
act.result = reason
act.resultant_state = new_state
act.succeeded = True
act.save()
inst.req_traits.add(*req_traits) def vm_state_changed(self, new_state):
inst.tags.add(*tags) # log state change
try:
act = InstanceActivity.create(code_suffix='vm_state_changed',
instance=self)
except ActivityInProgressError:
pass # discard state change if another activity is in progress.
else:
act.finished = act.started
act.resultant_state = new_state
act.succeeded = True
act.save()
return inst if new_state == 'STOPPED':
self.vnc_port = None
self.node = None
self.save()
@permalink @permalink
def get_absolute_url(self): def get_absolute_url(self):
...@@ -401,9 +416,13 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -401,9 +416,13 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
It is always on the first hard drive storage named cloud-<id>.dump It is always on the first hard drive storage named cloud-<id>.dump
""" """
datastore = self.disks.all()[0].datastore try:
path = datastore.path + '/' + self.vm_name + '.dump' datastore = self.disks.all()[0].datastore
return {'datastore': datastore, 'path': path} except:
return None
else:
path = datastore.path + '/' + self.vm_name + '.dump'
return {'datastore': datastore, 'path': path}
@property @property
def primary_host(self): def primary_host(self):
...@@ -455,15 +474,6 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -455,15 +474,6 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
else: else:
return self.template.os_type return self.template.os_type
@property
def system(self):
"""Get the instance's operating system.
"""
if self.template is None:
return _("Unknown")
else:
return self.template.system
def get_age(self): def get_age(self):
"""Deprecated. Use uptime instead. """Deprecated. Use uptime instead.
...@@ -507,7 +517,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -507,7 +517,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
'port': port, 'proto': proto, 'pw': self.pw, 'port': port, 'proto': proto, 'pw': self.pw,
'host': host} 'host': host}
elif proto == 'ssh': elif proto == 'ssh':
return ('sshpass -p %(pw)s ssh -o StrictHostKeyChecking=n ' return ('sshpass -p %(pw)s ssh -o StrictHostKeyChecking=no '
'cloud@%(host)s -p %(port)d') % { 'cloud@%(host)s -p %(port)d') % {
'port': port, 'proto': proto, 'pw': self.pw, 'port': port, 'proto': proto, 'pw': self.pw,
'host': host} 'host': host}
...@@ -560,16 +570,98 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -560,16 +570,98 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
else: else:
raise Node.DoesNotExist() raise Node.DoesNotExist()
def renew(self, which='both'): def _is_notified_about_expiration(self):
renews = self.activity_log.filter(activity_code__endswith='renew')
cond = {'activity_code__endswith': 'notification_about_expiration'}
if len(renews) > 0:
cond['finished__gt'] = renews[0].started
return self.activity_log.filter(**cond).exists()
def notify_owners_about_expiration(self, again=False):
"""Notify owners about vm expiring soon if they aren't already.
:param again: Notify already notified owners.
"""
if not again and self._is_notified_about_expiration():
return False
success, failed = [], []
def on_commit(act):
act.result = {'failed': failed, 'success': success}
with instance_activity('notification_about_expiration', instance=self,
on_commit=on_commit):
from dashboard.views import VmRenewView
level = self.get_level_object("owner")
for u, ulevel in self.get_users_with_level(level__pk=level.pk):
try:
token = VmRenewView.get_token_url(self, u)
u.profile.notify(
_('%s expiring soon') % unicode(self),
'dashboard/notifications/vm-expiring.html',
{'instance': self, 'token': token}, valid_until=min(
self.time_of_delete, self.time_of_suspend))
except Exception as e:
failed.append((u, e))
else:
success.append(u)
return True
def is_expiring(self, threshold=0.1):
"""Returns if an instance will expire soon.
Soon means that the time of suspend or delete comes in 10% of the
interval what the Lease allows. This rate is configurable with the
only parameter, threshold (0.1 = 10% by default).
"""
return (self._is_suspend_expiring(self, threshold) or
self._is_delete_expiring(self, threshold))
def _is_suspend_expiring(self, threshold=0.1):
interval = self.lease.suspend_interval
if interval is not None:
limit = timezone.now() + threshold * self.lease.suspend_interval
return limit > self.time_of_suspend
else:
return False
def _is_delete_expiring(self, threshold=0.1):
interval = self.lease.delete_interval
if interval is not None:
limit = timezone.now() + threshold * self.lease.delete_interval
return limit > self.time_of_delete
else:
return False
def get_renew_times(self):
"""Returns new suspend and delete times if renew would be called.
"""
return (
timezone.now() + self.lease.suspend_interval,
timezone.now() + self.lease.delete_interval)
def _do_renew(self, which='both'):
"""Set expiration times to renewed values.
"""
time_of_suspend, time_of_delete = self.get_renew_times()
if which in ('suspend', 'both'):
self.time_of_suspend = time_of_suspend
if which in ('delete', 'both'):
self.time_of_delete = time_of_delete
def renew(self, which='both', base_activity=None, user=None):
"""Renew virtual machine instance leases. """Renew virtual machine instance leases.
""" """
if which not in ['suspend', 'delete', 'both']: if base_activity is None:
raise ValueError('No such expiration type.') act = instance_activity(code_suffix='renew', instance=self,
if which in ['suspend', 'both']: user=user)
self.time_of_suspend = timezone.now() + self.lease.suspend_interval else:
if which in ['delete', 'both']: act = base_activity.sub_activity('renew')
self.time_of_delete = timezone.now() + self.lease.delete_interval with act:
self.save() if which not in ('suspend', 'delete', 'both'):
raise ValueError('No such expiration type.')
self._do_renew(which)
self.save()
def change_password(self, user=None): def change_password(self, user=None):
"""Generate new password for the vm """Generate new password for the vm
...@@ -588,10 +680,13 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -588,10 +680,13 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
self.pw)) self.pw))
self.save() self.save()
def __schedule_vm(self, act): def select_node(self):
"""Schedule the virtual machine. """Returns the node the VM should be deployed or migrated to.
"""
return scheduler.select_node(self, Node.objects.all())
:param self: The virtual machine. def __schedule_vm(self, act):
"""Schedule the virtual machine as part of a higher level activity.
:param act: Parent activity. :param act: Parent activity.
""" """
...@@ -601,11 +696,11 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -601,11 +696,11 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
# Schedule # Schedule
if self.node is None: if self.node is None:
self.node = scheduler.select_node(self, Node.objects.all()) self.node = self.select_node()
self.save() self.save()
def __deploy_vm(self, act): def __deploy_vm(self, act, timeout=15):
"""Deploy the virtual machine. """Deploy the virtual machine.
:param self: The virtual machine. :param self: The virtual machine.
...@@ -615,9 +710,10 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -615,9 +710,10 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
queue_name = self.get_remote_queue_name('vm') queue_name = self.get_remote_queue_name('vm')
# Deploy VM on remote machine # Deploy VM on remote machine
with act.sub_activity('deploying_vm'): with act.sub_activity('deploying_vm') as deploy_act:
vm_tasks.deploy.apply_async(args=[self.get_vm_desc()], deploy_act.result = vm_tasks.deploy.apply_async(
queue=queue_name).get() args=[self.get_vm_desc()],
queue=queue_name).get(timeout=timeout)
# Estabilish network connection (vmdriver) # Estabilish network connection (vmdriver)
with act.sub_activity('deploying_net'): with act.sub_activity('deploying_net'):
...@@ -627,9 +723,9 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -627,9 +723,9 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
# Resume vm # Resume vm
with act.sub_activity('booting'): with act.sub_activity('booting'):
vm_tasks.resume.apply_async(args=[self.vm_name], vm_tasks.resume.apply_async(args=[self.vm_name],
queue=queue_name).get() queue=queue_name).get(timeout=timeout)
self.renew('suspend') self.renew('suspend', act)
def deploy(self, user=None, task_uuid=None): def deploy(self, user=None, task_uuid=None):
"""Deploy new virtual machine with network """Deploy new virtual machine with network
...@@ -679,7 +775,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -679,7 +775,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
return local_tasks.deploy.apply_async(args=[self, user], return local_tasks.deploy.apply_async(args=[self, user],
queue="localhost.man") queue="localhost.man")
def __destroy_vm(self, act): def __destroy_vm(self, act, timeout=15):
"""Destroy the virtual machine and its associated networks. """Destroy the virtual machine and its associated networks.
:param self: The virtual machine. :param self: The virtual machine.
...@@ -696,13 +792,17 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -696,13 +792,17 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
queue_name = self.get_remote_queue_name('vm') queue_name = self.get_remote_queue_name('vm')
try: try:
vm_tasks.destroy.apply_async(args=[self.vm_name], vm_tasks.destroy.apply_async(args=[self.vm_name],
queue=queue_name).get() queue=queue_name
).get(timeout=timeout)
except Exception as e: except Exception as e:
if e.libvirtError is True: if e.libvirtError is True and "Domain not found" in str(e):
if "Domain not found" in str(e): logger.debug("Domain %s was not found at %s"
pass % (self.vm_name, queue_name))
pass
def __cleanup_after_destroy_vm(self, act): else:
raise
def __cleanup_after_destroy_vm(self, act, timeout=15):
"""Clean up the virtual machine's data after destroy. """Clean up the virtual machine's data after destroy.
:param self: The virtual machine. :param self: The virtual machine.
...@@ -710,12 +810,12 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -710,12 +810,12 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
:param act: Parent activity. :param act: Parent activity.
""" """
# Delete mem. dump if exists # Delete mem. dump if exists
queue_name = self.mem_dump['datastore'].get_remote_queue_name(
'storage')
try: try:
queue_name = self.mem_dump['datastore'].get_remote_queue_name(
'storage')
from storage.tasks.remote_tasks import delete_dump from storage.tasks.remote_tasks import delete_dump
delete_dump.apply_async(args=[self.mem_dump['path']], delete_dump.apply_async(args=[self.mem_dump['path']],
queue=queue_name).get() queue=queue_name).get(timeout=timeout)
except: except:
pass pass
...@@ -754,6 +854,28 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -754,6 +854,28 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
return local_tasks.redeploy.apply_async(args=[self, user], return local_tasks.redeploy.apply_async(args=[self, user],
queue="localhost.man") queue="localhost.man")
def shut_off(self, user=None, task_uuid=None):
"""Shut off VM. (plug-out)
"""
def __on_commit(activity):
activity.resultant_state = 'STOPPED'
with instance_activity(code_suffix='shut_off', instance=self,
task_uuid=task_uuid, user=user,
on_commit=__on_commit) as act:
# Destroy VM
if self.node:
self.__destroy_vm(act)
self.__cleanup_after_destroy_vm(act)
self.save()
def shut_off_async(self, user=None):
"""Shut off VM. (plug-out)
"""
return local_tasks.shut_off.apply_async(args=[self, user],
queue="localhost.man")
def destroy(self, user=None, task_uuid=None): def destroy(self, user=None, task_uuid=None):
"""Remove virtual machine and its networks. """Remove virtual machine and its networks.
...@@ -796,7 +918,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -796,7 +918,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
return local_tasks.destroy.apply_async(args=[self, user], return local_tasks.destroy.apply_async(args=[self, user],
queue="localhost.man") queue="localhost.man")
def sleep(self, user=None, task_uuid=None): def sleep(self, user=None, task_uuid=None, timeout=60):
"""Suspend virtual machine with memory dump. """Suspend virtual machine with memory dump.
""" """
if self.state not in ['RUNNING']: if self.state not in ['RUNNING']:
...@@ -813,12 +935,22 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -813,12 +935,22 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
with instance_activity(code_suffix='sleep', instance=self, with instance_activity(code_suffix='sleep', instance=self,
on_abort=__on_abort, on_commit=__on_commit, on_abort=__on_abort, on_commit=__on_commit,
task_uuid=task_uuid, user=user): task_uuid=task_uuid, user=user) as act:
queue_name = self.get_remote_queue_name('vm') # Destroy networks
vm_tasks.sleep.apply_async(args=[self.vm_name, with act.sub_activity('destroying_net'):
self.mem_dump['path']], for net in self.interface_set.all():
queue=queue_name).get() net.destroy(delete_host=False)
# Suspend vm
with act.sub_activity('suspending'):
queue_name = self.get_remote_queue_name('vm')
vm_tasks.sleep.apply_async(args=[self.vm_name,
self.mem_dump['path']],
queue=queue_name
).get(timeout=timeout)
self.node = None
self.save()
def sleep_async(self, user=None): def sleep_async(self, user=None):
"""Execute sleep asynchronously. """Execute sleep asynchronously.
...@@ -826,7 +958,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -826,7 +958,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
return local_tasks.sleep.apply_async(args=[self, user], return local_tasks.sleep.apply_async(args=[self, user],
queue="localhost.man") queue="localhost.man")
def wake_up(self, user=None, task_uuid=None): def wake_up(self, user=None, task_uuid=None, timeout=60):
if self.state not in ['SUSPENDED']: if self.state not in ['SUSPENDED']:
raise self.WrongStateError(self) raise self.WrongStateError(self)
...@@ -838,12 +970,23 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -838,12 +970,23 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
with instance_activity(code_suffix='wake_up', instance=self, with instance_activity(code_suffix='wake_up', instance=self,
on_abort=__on_abort, on_commit=__on_commit, on_abort=__on_abort, on_commit=__on_commit,
task_uuid=task_uuid, user=user): task_uuid=task_uuid, user=user) as act:
# Schedule vm
self.__schedule_vm(act)
queue_name = self.get_remote_queue_name('vm') queue_name = self.get_remote_queue_name('vm')
vm_tasks.wake_up.apply_async(args=[self.vm_name,
self.mem_dump['path']], # Resume vm
queue=queue_name).get() with act.sub_activity('resuming'):
vm_tasks.wake_up.apply_async(args=[self.vm_name,
self.mem_dump['path']],
queue=queue_name
).get(timeout=timeout)
# Estabilish network connection (vmdriver)
with act.sub_activity('deploying_net'):
for net in self.interface_set.all():
net.deploy()
def wake_up_async(self, user=None): def wake_up_async(self, user=None):
"""Execute wake_up asynchronously. """Execute wake_up asynchronously.
...@@ -851,7 +994,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -851,7 +994,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
return local_tasks.wake_up.apply_async(args=[self, user], return local_tasks.wake_up.apply_async(args=[self, user],
queue="localhost.man") queue="localhost.man")
def shutdown(self, user=None, task_uuid=None): def shutdown(self, user=None, task_uuid=None, timeout=120):
"""Shutdown virtual machine with ACPI signal. """Shutdown virtual machine with ACPI signal.
""" """
def __on_abort(activity, error): def __on_abort(activity, error):
...@@ -870,7 +1013,8 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -870,7 +1013,8 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
logger.debug("RPC Shutdown at queue: %s, for vm: %s.", logger.debug("RPC Shutdown at queue: %s, for vm: %s.",
self.vm_name, queue_name) self.vm_name, queue_name)
vm_tasks.shutdown.apply_async(kwargs={'name': self.vm_name}, vm_tasks.shutdown.apply_async(kwargs={'name': self.vm_name},
queue=queue_name).get() queue=queue_name
).get(timeout=timeout)
self.node = None self.node = None
self.vnc_port = None self.vnc_port = None
self.save() self.save()
...@@ -881,23 +1025,24 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -881,23 +1025,24 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
return local_tasks.shutdown.apply_async(args=[self, user], return local_tasks.shutdown.apply_async(args=[self, user],
queue="localhost.man") queue="localhost.man")
def reset(self, user=None, task_uuid=None): def reset(self, user=None, task_uuid=None, timeout=5):
"""Reset virtual machine (reset button) """Reset virtual machine (reset button)
""" """
with instance_activity(code_suffix='reset', instance=self, with instance_activity(code_suffix='reset', instance=self,
task_uuid=task_uuid, user=user): task_uuid=task_uuid, user=user):
queue_name = self.get_remote_queue_name('vm') queue_name = self.get_remote_queue_name('vm')
vm_tasks.restart.apply_async(args=[self.vm_name], vm_tasks.reset.apply_async(args=[self.vm_name],
queue=queue_name).get() queue=queue_name
).get(timeout=timeout)
def reset_async(self, user=None): def reset_async(self, user=None):
"""Execute reset asynchronously. """Execute reset asynchronously.
""" """
return local_tasks.restart.apply_async(args=[self, user], return local_tasks.reset.apply_async(args=[self, user],
queue="localhost.man") queue="localhost.man")
def reboot(self, user=None, task_uuid=None): def reboot(self, user=None, task_uuid=None, timeout=5):
"""Reboot virtual machine with Ctrl+Alt+Del signal. """Reboot virtual machine with Ctrl+Alt+Del signal.
""" """
with instance_activity(code_suffix='reboot', instance=self, with instance_activity(code_suffix='reboot', instance=self,
...@@ -905,7 +1050,8 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -905,7 +1050,8 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
queue_name = self.get_remote_queue_name('vm') queue_name = self.get_remote_queue_name('vm')
vm_tasks.reboot.apply_async(args=[self.vm_name], vm_tasks.reboot.apply_async(args=[self.vm_name],
queue=queue_name).get() queue=queue_name
).get(timeout=timeout)
def reboot_async(self, user=None): def reboot_async(self, user=None):
"""Execute reboot asynchronously. """ """Execute reboot asynchronously. """
...@@ -917,7 +1063,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -917,7 +1063,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
return local_tasks.migrate.apply_async(args=[self, to_node, user], return local_tasks.migrate.apply_async(args=[self, to_node, user],
queue="localhost.man") queue="localhost.man")
def migrate(self, to_node, user=None, task_uuid=None): def migrate(self, to_node, user=None, task_uuid=None, timeout=120):
"""Live migrate running vm to another node. """ """Live migrate running vm to another node. """
with instance_activity(code_suffix='migrate', instance=self, with instance_activity(code_suffix='migrate', instance=self,
task_uuid=task_uuid, user=user) as act: task_uuid=task_uuid, user=user) as act:
...@@ -930,7 +1076,8 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -930,7 +1076,8 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
queue_name = self.get_remote_queue_name('vm') queue_name = self.get_remote_queue_name('vm')
vm_tasks.migrate.apply_async(args=[self.vm_name, vm_tasks.migrate.apply_async(args=[self.vm_name,
to_node.host.hostname], to_node.host.hostname],
queue=queue_name).get() queue=queue_name
).get(timeout=timeout)
# Refresh node information # Refresh node information
self.node = to_node self.node = to_node
self.save() self.save()
...@@ -939,47 +1086,54 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -939,47 +1086,54 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
for net in self.interface_set.all(): for net in self.interface_set.all():
net.deploy() net.deploy()
def save_as_template(self, name, **kwargs): def save_as_template_async(self, name, user=None, **kwargs):
# prepare parameters return local_tasks.save_as_template.apply_async(
kwargs.setdefault('name', name) args=[self, name, user, kwargs], queue="localhost.man")
kwargs.setdefault('description', self.description)
kwargs.setdefault('parent', self.template)
kwargs.setdefault('num_cores', self.num_cores)
kwargs.setdefault('ram_size', self.ram_size)
kwargs.setdefault('max_ram_size', self.max_ram_size)
kwargs.setdefault('arch', self.arch)
kwargs.setdefault('priority', self.priority)
kwargs.setdefault('boot_menu', self.boot_menu)
kwargs.setdefault('raw_data', self.raw_data)
kwargs.setdefault('lease', self.lease)
kwargs.setdefault('access_method', self.access_method)
kwargs.setdefault('system', self.template.system
if self.template else None)
def __try_save_disk(disk):
try:
return disk.save_as()
except Disk.WrongDiskTypeError:
return disk
# copy disks
disks = [__try_save_disk(disk) for disk in self.disks.all()]
kwargs.setdefault('disks', disks)
# create template and do additional setup
tmpl = InstanceTemplate(**kwargs)
# save template def save_as_template(self, name, task_uuid=None, user=None,
tmpl.save() timeout=300, **kwargs):
try: with instance_activity(code_suffix="save_as_template", instance=self,
# create interface templates task_uuid=task_uuid, user=user) as act:
for i in self.interface_set.all(): # prepare parameters
i.save_as_template(tmpl) kwargs.setdefault('name', name)
except: kwargs.setdefault('description', self.description)
tmpl.delete() kwargs.setdefault('parent', self.template)
raise kwargs.setdefault('num_cores', self.num_cores)
else: kwargs.setdefault('ram_size', self.ram_size)
return tmpl kwargs.setdefault('max_ram_size', self.max_ram_size)
kwargs.setdefault('arch', self.arch)
kwargs.setdefault('priority', self.priority)
kwargs.setdefault('boot_menu', self.boot_menu)
kwargs.setdefault('raw_data', self.raw_data)
kwargs.setdefault('lease', self.lease)
kwargs.setdefault('access_method', self.access_method)
kwargs.setdefault('system', self.template.system
if self.template else None)
def __try_save_disk(disk):
try:
return disk.save_as() # can do in parallel
except Disk.WrongDiskTypeError:
return disk
# create template and do additional setup
tmpl = InstanceTemplate(**kwargs)
tmpl.full_clean() # Avoiding database errors.
tmpl.save()
with act.sub_activity('saving_disks'):
tmpl.disks.add(*[__try_save_disk(disk)
for disk in self.disks.all()])
# save template
tmpl.save()
try:
# create interface templates
for i in self.interface_set.all():
i.save_as_template(tmpl)
except:
tmpl.delete()
raise
else:
return tmpl
def shutdown_and_save_as_template(self, name, user=None, task_uuid=None, def shutdown_and_save_as_template(self, name, user=None, task_uuid=None,
**kwargs): **kwargs):
......
...@@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy as _ ...@@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy as _
from firewall.models import Vlan, Host from firewall.models import Vlan, Host
from ..tasks import net_tasks from ..tasks import net_tasks
from .activity import instance_activity
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -105,7 +106,7 @@ class Interface(Model): ...@@ -105,7 +106,7 @@ class Interface(Model):
self.host.delete() self.host.delete()
@classmethod @classmethod
def create(cls, instance, vlan, managed, owner=None): def create(cls, instance, vlan, managed, owner=None, base_activity=None):
"""Create a new interface for a VM instance to the specified VLAN. """Create a new interface for a VM instance to the specified VLAN.
""" """
if managed: if managed:
...@@ -115,9 +116,19 @@ class Interface(Model): ...@@ -115,9 +116,19 @@ class Interface(Model):
host.mac = str(cls.generate_mac(instance, vlan)) host.mac = str(cls.generate_mac(instance, vlan))
host.hostname = instance.vm_name host.hostname = instance.vm_name
# Get adresses from firewall # Get adresses from firewall
addresses = vlan.get_new_address() if base_activity is None:
host.ipv4 = addresses['ipv4'] act = instance_activity(code_suffix='allocating_ip',
host.ipv6 = addresses['ipv6'] instance=instance, user=owner)
else:
act = base_activity.sub_activity('allocating_ip')
with act as act:
addresses = vlan.get_new_address()
host.ipv4 = addresses['ipv4']
host.ipv6 = addresses['ipv6']
act.result = ('new addresses: ipv4: %(ip4)s, ipv6: %(ip6)s, '
'vlan: %(vlan)s' % {'ip4': host.ipv4,
'ip6': host.ipv6,
'vlan': vlan.name})
host.owner = owner host.owner = owner
if vlan.network_type == 'public': if vlan.network_type == 'public':
host.shared_ip = False host.shared_ip = False
......
...@@ -11,15 +11,16 @@ from celery.exceptions import TimeoutError ...@@ -11,15 +11,16 @@ from celery.exceptions import TimeoutError
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from common.models import method_cache from common.models import method_cache, WorkerNotFound
from firewall.models import Host from firewall.models import Host
from ..tasks import vm_tasks from ..tasks import vm_tasks
from .common import Trait from .common import Trait
from .activity import node_activity from .activity import node_activity, NodeActivity
from monitor.calvin.calvin import Query from monitor.calvin.calvin import Query
from monitor.calvin.calvin import GraphiteHandler from monitor.calvin.calvin import GraphiteHandler
from django.utils import timezone
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -54,53 +55,64 @@ class Node(TimeStampedModel): ...@@ -54,53 +55,64 @@ class Node(TimeStampedModel):
def __unicode__(self): def __unicode__(self):
return self.name return self.name
@property
@method_cache(10, 5) @method_cache(10, 5)
def online(self): def get_online(self):
"""Check if the node is online.
Runs a remote ping task if the worker is running.
"""
try:
return self.remote_query(vm_tasks.ping, timeout=1, default=False)
except WorkerNotFound:
return False
return self.remote_query(vm_tasks.ping, timeout=1, default=False) online = property(get_online)
@property
@method_cache(300) @method_cache(300)
def num_cores(self): def get_num_cores(self):
"""Number of CPU threads available to the virtual machines. """Number of CPU threads available to the virtual machines.
""" """
return self.remote_query(vm_tasks.get_core_num) return self.remote_query(vm_tasks.get_core_num, default=0)
num_cores = property(get_num_cores)
@property @property
def state(self): def state(self):
"""Node state. """The state combined of online and enabled attributes.
""" """
if self.enabled and self.online: if self.enabled and self.online:
return 'Online' return 'ONLINE'
elif self.enabled and not self.online: elif self.enabled and not self.online:
return 'Missing' return 'MISSING'
elif not self.enabled and self.online: elif not self.enabled and self.online:
return 'Disabled' return 'DISABLED'
else: else:
return 'Offline' return 'OFFLINE'
def disable(self, user=None): def disable(self, user=None):
''' Disable the node.''' ''' Disable the node.'''
with node_activity(code_suffix='disable', node=self, user=user): if self.enabled is True:
self.enabled = False with node_activity(code_suffix='disable', node=self, user=user):
self.save() self.enabled = False
self.save()
def enable(self, user=None): def enable(self, user=None):
''' Enable the node. ''' ''' Enable the node. '''
with node_activity(code_suffix='enable', node=self, user=user): if self.enabled is not True:
self.enabled = True with node_activity(code_suffix='enable', node=self, user=user):
self.save() self.enabled = True
self.save()
self.get_num_cores(invalidate_cache=True)
self.get_ram_size(invalidate_cache=True)
@property
@method_cache(300) @method_cache(300)
def ram_size(self): def get_ram_size(self):
"""Bytes of total memory in the node. """Bytes of total memory in the node.
""" """
return self.remote_query(vm_tasks.get_ram_size, default=0)
return self.remote_query(vm_tasks.get_ram_size) ram_size = property(get_ram_size)
@property @property
def ram_size_with_overcommit(self): def ram_size_with_overcommit(self):
...@@ -110,25 +122,77 @@ class Node(TimeStampedModel): ...@@ -110,25 +122,77 @@ class Node(TimeStampedModel):
@method_cache(30) @method_cache(30)
def get_remote_queue_name(self, queue_id): def get_remote_queue_name(self, queue_id):
""" Return the remote queue name """Return the name of the remote celery queue for this node.
throws Exception if there is no worker on the queue. throws Exception if there is no worker on the queue.
Until the cache provide reult there can be dead quques. Until the cache provide reult there can be dead queues.
""" """
if vm_tasks.check_queue(self.host.hostname, queue_id): if vm_tasks.check_queue(self.host.hostname, queue_id):
self.node_online()
return self.host.hostname + "." + queue_id return self.host.hostname + "." + queue_id
else: else:
raise Exception("Worker not found.") if self.enabled is True:
self.node_offline()
raise WorkerNotFound()
def node_online(self):
"""Create activity and log entry when node reappears.
"""
try:
act = self.activity_log.order_by('-pk')[0]
except IndexError:
pass # no monitoring activity at all
else:
logger.debug("The last activity was %s" % act)
if act.activity_code.endswith("offline"):
act = NodeActivity.create(code_suffix='monitor_succes_online',
node=self, user=None)
act.started = timezone.now()
act.finished = timezone.now()
act.succeeded = True
act.save()
logger.info("Node %s is ONLINE." % self.name)
self.get_num_cores(invalidate_cache=True)
self.get_ram_size(invalidate_cache=True)
def node_offline(self):
"""Called when a node disappears.
If the node is not already offline, record an activity and a log entry.
"""
try:
act = self.activity_log.order_by('-pk')[0]
except IndexError:
pass # no activity at all
else:
logger.debug("The last activity was %s" % act)
if act.activity_code.endswith("offline"):
return
act = NodeActivity.create(code_suffix='monitor_failed_offline',
node=self, user=None)
act.started = timezone.now()
act.finished = timezone.now()
act.succeeded = False
act.save()
logger.critical("Node %s is OFFLINE%s.", self.name,
", but enabled" if self.enabled else "")
# TODO: check if we should reschedule any VMs?
def remote_query(self, task, timeout=30, raise_=False, default=None): def remote_query(self, task, timeout=30, raise_=False, default=None):
"""Query the given task, and get the result. """Query the given task, and get the result.
If the result is not ready in timeout secs, return default value or If the result is not ready or worker not reachable
raise a TimeoutError.""" in timeout secs, return default value or raise a
r = task.apply_async( TimeoutError or WorkerNotFound exception.
queue=self.get_remote_queue_name('vm'), expires=timeout + 60) """
try: try:
r = task.apply_async(
queue=self.get_remote_queue_name('vm'), expires=timeout + 60)
return r.get(timeout=timeout) return r.get(timeout=timeout)
except TimeoutError: except (TimeoutError, WorkerNotFound):
if raise_: if raise_:
raise raise
else: else:
...@@ -171,7 +235,16 @@ class Node(TimeStampedModel): ...@@ -171,7 +235,16 @@ class Node(TimeStampedModel):
def ram_usage(self): def ram_usage(self):
return float(self.get_monitor_info()["memory.usage"]) / 100 return float(self.get_monitor_info()["memory.usage"]) / 100
@property
def byte_ram_usage(self):
return self.ram_usage * self.ram_size
def update_vm_states(self): def update_vm_states(self):
"""Update state of Instances running on this Node.
Query state of all libvirt domains, and notify Instances by their
vm_state_changed hook.
"""
domains = {} domains = {}
domain_list = self.remote_query(vm_tasks.list_domains_info, timeout=5) domain_list = self.remote_query(vm_tasks.list_domains_info, timeout=5)
if domain_list is None: if domain_list is None:
...@@ -194,6 +267,8 @@ class Node(TimeStampedModel): ...@@ -194,6 +267,8 @@ class Node(TimeStampedModel):
except KeyError: except KeyError:
logger.info('Node %s update: instance %s missing from ' logger.info('Node %s update: instance %s missing from '
'libvirt', self, i['id']) 'libvirt', self, i['id'])
# Set state to STOPPED when instance is missing
self.instance_set.get(id=i['id']).vm_state_changed('STOPPED')
else: else:
if d != i['state']: if d != i['state']:
logger.info('Node %s update: instance %s state changed ' logger.info('Node %s update: instance %s state changed '
......
import logging
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from manager.mancelery import celery from manager.mancelery import celery
from vm.models import Node from vm.models import Node, Instance
logger = logging.getLogger(__name__)
@celery.task(ignore_result=True) @celery.task(ignore_result=True)
...@@ -7,3 +13,44 @@ def update_domain_states(): ...@@ -7,3 +13,44 @@ def update_domain_states():
nodes = Node.objects.filter(enabled=True).all() nodes = Node.objects.filter(enabled=True).all()
for node in nodes: for node in nodes:
node.update_vm_states() node.update_vm_states()
@celery.task(ignore_result=True)
def garbage_collector(timeout=15):
"""Garbage collector for instances.
Suspends and destroys expired instances.
:param timeout: Seconds before TimeOut exception
:type timeout: int
"""
now = timezone.now()
for i in Instance.objects.filter(destroyed=None).all():
if i.time_of_delete and now < i.time_of_delete:
i.destroy_async()
logger.info("Expired instance %d destroyed.", i.pk)
try:
i.owner.profile.notify(
_('%s destroyed') % unicode(i),
'dashboard/notifications/vm-destroyed.html',
{'instance': i})
except Exception as e:
logger.debug('Could not notify owner of instance %d .%s',
i.pk, unicode(e))
elif (i.time_of_suspend and now < i.time_of_suspend and
i.state == 'RUNNING'):
i.sleep_async()
logger.info("Expired instance %d suspended." % i.pk)
try:
i.owner.profile.notify(
_('%s suspended') % unicode(i),
'dashboard/notifications/vm-suspended.html',
{'instance': i})
except Exception as e:
logger.debug('Could not notify owner of instance %d .%s',
i.pk, unicode(e))
elif i.is_expiring():
logger.debug("Instance %d expires soon." % i.pk)
i.notify_owners_about_expiration()
else:
logger.debug("Instance %d didn't expire." % i.pk)
...@@ -14,11 +14,22 @@ def redeploy(instance, user): ...@@ -14,11 +14,22 @@ def redeploy(instance, user):
@celery.task @celery.task
def shut_off(instance, user):
instance.shut_off(task_uuid=shut_off.request.id, user=user)
@celery.task
def destroy(instance, user): def destroy(instance, user):
instance.destroy(task_uuid=destroy.request.id, user=user) instance.destroy(task_uuid=destroy.request.id, user=user)
@celery.task @celery.task
def save_as_template(instance, name, user, params):
instance.save_as_template(name, task_uuid=save_as_template.request.id,
user=user, **params)
@celery.task
def sleep(instance, user): def sleep(instance, user):
instance.sleep(task_uuid=sleep.request.id, user=user) instance.sleep(task_uuid=sleep.request.id, user=user)
......
...@@ -2,12 +2,15 @@ from manager.mancelery import celery ...@@ -2,12 +2,15 @@ from manager.mancelery import celery
def check_queue(node_hostname, queue_id): def check_queue(node_hostname, queue_id):
drivers = ['vmdriver', 'netdriver'] drivers = ['vmdriver', 'netdriver', 'agentdriver']
worker_list = [node_hostname + "." + d for d in drivers] worker_list = [node_hostname + "." + d for d in drivers]
queue_name = node_hostname + "." + queue_id queue_name = node_hostname + "." + queue_id
inspect = celery.control.inspect(worker_list) inspect = celery.control.inspect(worker_list)
active_queues = inspect.active_queues()
if active_queues is None:
return False
# v is List of List of queues dict # v is List of List of queues dict
node_workers = [v for k, v in inspect.active_queues().iteritems()] node_workers = [v for k, v in active_queues.iteritems()]
for worker in node_workers: for worker in node_workers:
for queue in worker: for queue in worker:
if queue['name'] == queue_name: if queue['name'] == queue_name:
......
from django.test import TestCase from django.test import TestCase
from mock import Mock
from ..models.common import (
Lease
)
from ..models.instance import ( from ..models.instance import (
InstanceTemplate, Instance find_unused_port, InstanceTemplate, Instance
) )
from ..models.network import ( from ..models.network import (
Interface Interface
) )
from ..models.common import (
Lease
) class PortFinderTestCase(TestCase):
def test_find_unused_port_without_used_ports(self):
port = find_unused_port(port_range=(1000, 2000))
assert port is not None
def test_find_unused_port_with_fully_saturated_range(self):
r = (10, 20)
port = find_unused_port(port_range=r, used_ports=range(*r))
assert port is None
class TemplateTestCase(TestCase): class TemplateTestCase(TestCase):
...@@ -20,6 +33,13 @@ class TemplateTestCase(TestCase): ...@@ -20,6 +33,13 @@ class TemplateTestCase(TestCase):
# TODO add images & net # TODO add images & net
class InstanceTestCase(TestCase):
def test_is_running(self):
inst = Mock(state='RUNNING')
assert Instance.is_running.getter(inst)
class InterfaceTestCase(TestCase): class InterfaceTestCase(TestCase):
def test_interface_create(self): def test_interface_create(self):
......
description "IK Cloud Django Development Server" description "CIRCLE mancelery"
start on runlevel [2345] start on runlevel [2345]
stop on runlevel [!2345] stop on runlevel [!2345]
respawn respawn
respawn limit 30 30 respawn limit 30 30
setgid cloud
setuid cloud setuid cloud
chdir /home/cloud/circle/circle
script script
. /home/cloud/.virtualenvs/circle/local/bin/postactivate cd /home/cloud/circle/circle
exec /home/cloud/.virtualenvs/circle/bin/python manage.py celery --app=manager.mancelery worker --autoreload --loglevel=info --hostname=mancelery -B -c 1 --logfile /tmp/mancelery.log . /home/cloud/.virtualenvs/circle/bin/activate
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py celery --app=manager.mancelery worker --autoreload --loglevel=info --hostname=mancelery -B -c 1
end script end script
description "CIRCLE django dev server"
start on runlevel [2345]
stop on runlevel [!2345]
respawn
respawn limit 30 30
setgid cloud
setuid cloud
script
cd /home/cloud/circle/circle
. /home/cloud/.virtualenvs/circle/bin/activate
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py runserver '[::]:8080'
end script
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment