Commit 207f7413 by Kálmán Viktor

Merge branch 'master' into feature-mass-ops

Conflicts:
	circle/dashboard/templates/dashboard/vm-list.html
	circle/dashboard/views.py
parents 3d55c446 da2b020d
...@@ -161,7 +161,7 @@ STATICFILES_FINDERS = ( ...@@ -161,7 +161,7 @@ STATICFILES_FINDERS = (
) )
########## END STATIC FILE CONFIGURATION ########## END STATIC FILE CONFIGURATION
p = join(dirname(SITE_ROOT), 'site-circle/static') p = normpath(join(SITE_ROOT, '../../site-circle/static'))
if exists(p): if exists(p):
STATICFILES_DIRS = (p, ) STATICFILES_DIRS = (p, )
...@@ -211,8 +211,8 @@ TEMPLATE_LOADERS = ( ...@@ -211,8 +211,8 @@ TEMPLATE_LOADERS = (
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs
TEMPLATE_DIRS = ( TEMPLATE_DIRS = (
normpath(join(SITE_ROOT, '../../site-circle/templates')),
normpath(join(SITE_ROOT, 'templates')), normpath(join(SITE_ROOT, 'templates')),
join(dirname(SITE_ROOT), 'site-circle/templates'),
) )
########## END TEMPLATE CONFIGURATION ########## END TEMPLATE CONFIGURATION
......
...@@ -24,18 +24,10 @@ from os import environ ...@@ -24,18 +24,10 @@ from os import environ
from base import * # noqa from base import * # noqa
def get_env_setting(setting):
""" Get the environment setting or return exception """
try:
return environ[setting]
except KeyError:
error_msg = "Set the %s env variable" % setting
raise ImproperlyConfigured(error_msg)
########## HOST CONFIGURATION ########## HOST CONFIGURATION
# See: https://docs.djangoproject.com/en/1.5/releases/1.5/ # See: https://docs.djangoproject.com/en/1.5/releases/1.5/
# #allowed-hosts-required-in-production # #allowed-hosts-required-in-production
ALLOWED_HOSTS = get_env_setting('DJANGO_ALLOWED_HOSTS').split(',') ALLOWED_HOSTS = get_env_variable('DJANGO_ALLOWED_HOSTS').split(',')
########## END HOST CONFIGURATION ########## END HOST CONFIGURATION
########## EMAIL CONFIGURATION ########## EMAIL CONFIGURATION
...@@ -44,18 +36,18 @@ EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' ...@@ -44,18 +36,18 @@ EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
try: try:
# See: https://docs.djangoproject.com/en/dev/ref/settings/#email-host # See: https://docs.djangoproject.com/en/dev/ref/settings/#email-host
EMAIL_HOST = environ.get('EMAIL_HOST') EMAIL_HOST = get_env_variable('EMAIL_HOST')
except ImproperlyConfigured: except ImproperlyConfigured:
pass EMAIL_HOST = 'localhost'
else: else:
# https://docs.djangoproject.com/en/dev/ref/settings/#email-host-password # https://docs.djangoproject.com/en/dev/ref/settings/#email-host-password
EMAIL_HOST_PASSWORD = environ.get('EMAIL_HOST_PASSWORD', '') EMAIL_HOST_PASSWORD = get_env_variable('EMAIL_HOST_PASSWORD', '')
# See: https://docs.djangoproject.com/en/dev/ref/settings/#email-host-user # See: https://docs.djangoproject.com/en/dev/ref/settings/#email-host-user
EMAIL_HOST_USER = environ.get('EMAIL_HOST_USER', 'your_email@example.com') EMAIL_HOST_USER = get_env_variable('EMAIL_HOST_USER', 'your_email@example.com')
# See: https://docs.djangoproject.com/en/dev/ref/settings/#email-port # See: https://docs.djangoproject.com/en/dev/ref/settings/#email-port
EMAIL_PORT = environ.get('EMAIL_PORT', 587) EMAIL_PORT = get_env_variable('EMAIL_PORT', 587)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#email-use-tls # See: https://docs.djangoproject.com/en/dev/ref/settings/#email-use-tls
EMAIL_USE_TLS = True EMAIL_USE_TLS = True
...@@ -64,7 +56,8 @@ else: ...@@ -64,7 +56,8 @@ else:
EMAIL_SUBJECT_PREFIX = '[%s] ' % SITE_NAME EMAIL_SUBJECT_PREFIX = '[%s] ' % SITE_NAME
# See: https://docs.djangoproject.com/en/dev/ref/settings/#server-email # See: https://docs.djangoproject.com/en/dev/ref/settings/#server-email
SERVER_EMAIL = EMAIL_HOST_USER DEFAULT_FROM_EMAIL = get_env_variable('DEFAULT_FROM_EMAIL')
SERVER_EMAIL = get_env_variable('SERVER_EMAIL', DEFAULT_FROM_EMAIL)
########## END EMAIL CONFIGURATION ########## END EMAIL CONFIGURATION
...@@ -83,5 +76,12 @@ CACHES = { ...@@ -83,5 +76,12 @@ CACHES = {
########## SECRET CONFIGURATION ########## SECRET CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key # See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
SECRET_KEY = get_env_setting('SECRET_KEY') SECRET_KEY = get_env_variable('SECRET_KEY')
########## END SECRET CONFIGURATION ########## END SECRET CONFIGURATION
level = environ.get('LOGLEVEL', 'INFO')
LOGGING['handlers']['syslog']['level'] = level
for i in LOCAL_APPS:
LOGGING['loggers'][i] = {'handlers': ['syslog'], 'level': level}
LOGGING['loggers']['djangosaml2'] = {'handlers': ['syslog'], 'level': level}
LOGGING['loggers']['django'] = {'handlers': ['syslog'], 'level': level}
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
from collections import deque from collections import deque
from contextlib import contextmanager from contextlib import contextmanager
from functools import update_wrapper
from hashlib import sha224 from hashlib import sha224
from itertools import chain, imap from itertools import chain, imap
from logging import getLogger from logging import getLogger
...@@ -36,6 +37,7 @@ from django.utils.functional import Promise ...@@ -36,6 +37,7 @@ from django.utils.functional import Promise
from django.utils.translation import ugettext_lazy as _, ugettext_noop from django.utils.translation import ugettext_lazy as _, ugettext_noop
from jsonfield import JSONField from jsonfield import JSONField
from manager.mancelery import celery
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -212,6 +214,38 @@ class ActivityModel(TimeStampedModel): ...@@ -212,6 +214,38 @@ class ActivityModel(TimeStampedModel):
self.result_data = None if value is None else value.to_dict() self.result_data = None if value is None else value.to_dict()
@celery.task()
def compute_cached(method, instance, memcached_seconds,
key, start, *args, **kwargs):
"""Compute and store actual value of cached method."""
if isinstance(method, basestring):
model, id = instance
instance = model.objects.get(id=id)
try:
method = getattr(model, method)
while hasattr(method, '_original') or hasattr(method, 'fget'):
try:
method = method._original
except AttributeError:
method = method.fget
except AttributeError:
logger.exception("Couldnt get original method of %s",
unicode(method))
raise
# call the actual method
result = method(instance, *args, **kwargs)
# save to memcache
cache.set(key, result, memcached_seconds)
elapsed = time() - start
cache.set("%s.cached" % key, 2, max(memcached_seconds * 0.5,
memcached_seconds * 0.75 - elapsed))
logger.debug('Value of <%s>.%s(%s)=<%s> saved to cache (%s elapsed).',
unicode(instance), method.__name__, unicode(args),
unicode(result), elapsed)
return result
def method_cache(memcached_seconds=60, instance_seconds=5): # noqa def method_cache(memcached_seconds=60, instance_seconds=5): # noqa
"""Cache return value of decorated method to memcached and memory. """Cache return value of decorated method to memcached and memory.
...@@ -233,9 +267,11 @@ def method_cache(memcached_seconds=60, instance_seconds=5): # noqa ...@@ -233,9 +267,11 @@ def method_cache(memcached_seconds=60, instance_seconds=5): # noqa
def inner_cache(method): def inner_cache(method):
method_name = method.__name__
def get_key(instance, *args, **kwargs): def get_key(instance, *args, **kwargs):
return sha224(unicode(method.__module__) + return sha224(unicode(method.__module__) +
unicode(method.__name__) + method_name +
unicode(instance.id) + unicode(instance.id) +
unicode(args) + unicode(args) +
unicode(kwargs)).hexdigest() unicode(kwargs)).hexdigest()
...@@ -254,21 +290,31 @@ def method_cache(memcached_seconds=60, instance_seconds=5): # noqa ...@@ -254,21 +290,31 @@ def method_cache(memcached_seconds=60, instance_seconds=5): # noqa
if vals['time'] + instance_seconds > now: if vals['time'] + instance_seconds > now:
# has valid on class cache, return that # has valid on class cache, return that
result = vals['value'] result = vals['value']
setattr(instance, key, {'time': now, 'value': result})
if result is None: if result is None:
result = cache.get(key) result = cache.get(key)
if invalidate or (result is None): if invalidate or (result is None):
# all caches failed, call the actual method logger.debug("all caches failed, compute now")
result = method(instance, *args, **kwargs) result = compute_cached(method, instance, memcached_seconds,
# save to memcache and class attr key, time(), *args, **kwargs)
cache.set(key, result, memcached_seconds)
setattr(instance, key, {'time': now, 'value': result}) setattr(instance, key, {'time': now, 'value': result})
logger.debug('Value of <%s>.%s(%s)=<%s> saved to cache.', elif not cache.get("%s.cached" % key):
unicode(instance), method.__name__, logger.debug("caches expiring, compute async")
unicode(args), unicode(result)) cache.set("%s.cached" % key, 1, memcached_seconds * 0.5)
try:
compute_cached.apply_async(
queue='localhost.man', kwargs=kwargs, args=[
method_name, (instance.__class__, instance.id),
memcached_seconds, key, time()] + list(args))
except:
logger.exception("Couldnt compute async %s", method_name)
return result return result
update_wrapper(x, method)
x._original = method
return x return x
return inner_cache return inner_cache
......
...@@ -39,7 +39,7 @@ from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm ...@@ -39,7 +39,7 @@ from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm
from django.forms.widgets import TextInput, HiddenInput from django.forms.widgets import TextInput, HiddenInput
from django.template import Context from django.template import Context
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy as _
from sizefield.widgets import FileSizeWidget from sizefield.widgets import FileSizeWidget
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
...@@ -1138,3 +1138,29 @@ class VmResourcesForm(forms.ModelForm): ...@@ -1138,3 +1138,29 @@ class VmResourcesForm(forms.ModelForm):
class Meta: class Meta:
model = Instance model = Instance
fields = ('num_cores', 'priority', 'ram_size', ) fields = ('num_cores', 'priority', 'ram_size', )
vm_search_choices = (
(0, _("owned")),
(1, _("shared")),
(2, _("all")),
)
class VmListSearchForm(forms.Form):
s = forms.CharField(widget=forms.TextInput(attrs={
'class': "form-control input-tags",
'placeholder': _("Search...")
}))
stype = forms.ChoiceField(vm_search_choices, widget=forms.Select(attrs={
'class': "btn btn-default input-tags",
}))
def __init__(self, *args, **kwargs):
super(VmListSearchForm, self).__init__(*args, **kwargs)
# set initial value, otherwise it would be overwritten by request.GET
if not self.data.get("stype"):
data = self.data.copy()
data['stype'] = 2
self.data = data
...@@ -258,14 +258,15 @@ $(function () { ...@@ -258,14 +258,15 @@ $(function () {
html += '<div class="list-group-item list-group-item-last">' + gettext("No result") + '</div>'; html += '<div class="list-group-item list-group-item-last">' + gettext("No result") + '</div>';
$("#dashboard-vm-list").html(html); $("#dashboard-vm-list").html(html);
$('.title-favourite').tooltip({'placement': 'right'}); $('.title-favourite').tooltip({'placement': 'right'});
});
// if there is only one result and ENTER is pressed redirect $("#dashboard-vm-search-form").submit(function() {
if(e.keyCode == 13 && search_result.length == 1) { var vm_list_items = $("#dashboard-vm-list .list-group-item");
window.location.href = "/dashboard/vm/" + search_result[0].pk + "/"; if(vm_list_items.length == 1) {
} window.location.href = vm_list_items.first().prop("href");
if(e.keyCode == 13 && search_result.length > 1 && input.length > 0) { return false;
window.location.href = "/dashboard/vm/list/?s=" + input;
} }
return true;
}); });
/* search for nodes */ /* search for nodes */
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
$(function() { $(function() {
/* vm operations */ /* vm operations */
$('#ops, #vm-details-resources-disk, #vm-details-renew-op, #vm-details-pw-reset, #vm-details-add-interface').on('click', '.operation', function(e) { $('#ops, #vm-details-resources-disk, #vm-details-renew-op, #vm-details-pw-reset, #vm-details-add-interface, .operation-wrapper').on('click', '.operation', function(e) {
var icon = $(this).children("i").addClass('fa-spinner fa-spin'); var icon = $(this).children("i").addClass('fa-spinner fa-spin');
$.ajax({ $.ajax({
......
...@@ -186,18 +186,7 @@ $(function() { ...@@ -186,18 +186,7 @@ $(function() {
success: function(re, textStatus, xhr) { success: function(re, textStatus, xhr) {
/* remove the html element */ /* remove the html element */
$('a[data-interface-pk="' + data.pk + '"]').closest("div").fadeOut(); $('a[data-interface-pk="' + data.pk + '"]').closest("div").fadeOut();
location.reload();
/* add the removed element to the list */
network_select = $('select[name="new_network_vlan"]');
name_html = (re.removed_network.managed ? "&#xf0ac;": "&#xf0c1;") + " " + re.removed_network.vlan;
option_html = '<option value="' + re.removed_network.vlan_pk + '">' + name_html + '</option>';
// if it's -1 then it's a dummy placeholder so we can use .html
if($("option", network_select)[0].value === "-1") {
network_select.html(option_html);
network_select.next("div").children("button").prop("disabled", false);
} else {
network_select.append(option_html);
}
}, },
error: function(xhr, textStatus, error) { error: function(xhr, textStatus, error) {
addMessage('Uh oh :(', 'danger') addMessage('Uh oh :(', 'danger')
......
...@@ -41,6 +41,7 @@ def send_email_notifications(): ...@@ -41,6 +41,7 @@ def send_email_notifications():
for i in Notification.objects.filter(q): for i in Notification.objects.filter(q):
recipients.setdefault(i.to, []) recipients.setdefault(i.to, [])
recipients[i.to].append(i) recipients[i.to].append(i)
logger.info("Delivering notifications to %d users", len(recipients))
for user, msgs in recipients.iteritems(): for user, msgs in recipients.iteritems():
if (not user.profile or not user.email or not if (not user.profile or not user.email or not
......
...@@ -46,12 +46,15 @@ ...@@ -46,12 +46,15 @@
</style> </style>
<div href="#" class="list-group-item list-group-footer"> <div href="#" class="list-group-item list-group-footer">
<div class="row"> <div class="row">
<form action="{% url "dashboard.views.vm-list" %}" method="GET" id="dashboard-vm-search-form">
<div class="col-sm-6 col-xs-6 input-group input-group-sm"> <div class="col-sm-6 col-xs-6 input-group input-group-sm">
<input id="dashboard-vm-search-input" type="text" class="form-control" placeholder="{% trans "Search..." %}" /> <input id="dashboard-vm-search-input" type="text" class="form-control" name="s"
placeholder="{% trans "Search..." %}" />
<div class="input-group-btn"> <div class="input-group-btn">
<button type="submit" class="form-control btn btn-primary"><i class="fa fa-search"></i></button> <button type="submit" class="form-control btn btn-primary"><i class="fa fa-search"></i></button>
</div> </div>
</div> </div>
</form>
<div class="col-sm-6 text-right"> <div class="col-sm-6 text-right">
<a class="btn btn-primary btn-xs" href="{% url "dashboard.views.vm-list" %}"> <a class="btn btn-primary btn-xs" href="{% url "dashboard.views.vm-list" %}">
<i class="fa fa-chevron-circle-right"></i> <i class="fa fa-chevron-circle-right"></i>
......
...@@ -100,6 +100,20 @@ ...@@ -100,6 +100,20 @@
{% endif %} {% endif %}
</dd> </dd>
</dl> </dl>
{% if op.mount_store %}
<strong>{% trans "Store" %}</strong>
<p>
{{ op.mount_store.description }}
</p>
<div class="operation-wrapper">
<a href="{{ op.mount_store.get_url }}" class="btn btn-info btn-xs operation"
{% if op.mount_store.disabled %}disabled{% endif %}>
<i class="fa fa-{{op.mount_store.icon}}"></i>
{{ op.mount_store.name }}
</a>
</div>
{% endif %}
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{% if graphite_enabled %} {% if graphite_enabled %}
......
...@@ -15,11 +15,16 @@ ...@@ -15,11 +15,16 @@
</div> </div>
<h3 class="no-margin"><i class="fa fa-desktop"></i> {% trans "Virtual machines" %}</h3> <h3 class="no-margin"><i class="fa fa-desktop"></i> {% trans "Virtual machines" %}</h3>
</div> </div>
<div class="pull-right" style="max-width: 250px; margin-top: 15px; margin-right: 15px;"> <div class="pull-right" style="max-width: 300px; margin-top: 15px; margin-right: 15px;">
<form action="" method="GET" class="input-group"> <form action="" method="GET">
<input type="text" name="s"{% if request.GET.s %} value="{{ request.GET.s }}"{% endif %} class="form-control input-tags" placeholder="{% trans "Search..."%}" /> <div class="input-group">
{{ search_form.s }}
<div class="input-group-btn"> <div class="input-group-btn">
<button type="submit" class="form-control btn btn-primary input-tags" title="search"><i class="fa fa-search"></i></button> {{ search_form.stype }}
<button type="submit" class="btn btn-primary input-tags">
<i class="fa fa-search"></i>
</button>
</div>
</div> </div>
</form> </form>
</div> </div>
...@@ -33,9 +38,8 @@ ...@@ -33,9 +38,8 @@
<i class="fa fa-{{ o.icon }}"></i> <i class="fa fa-{{ o.icon }}"></i>
</a> </a>
{% endfor %} {% endfor %}
</div> </div><!-- #vm-mass-ops -->
</div><!-- .panel-body .vm-list-group-control -->
</div>
<div class="panel-body"> <div class="panel-body">
<table class="table table-bordered table-striped table-hover vm-list-table" <table class="table table-bordered table-striped table-hover vm-list-table"
id="vm-list-table"> id="vm-list-table">
...@@ -100,6 +104,9 @@ ...@@ -100,6 +104,9 @@
</div> </div>
</div> </div>
<div class="alert alert-info">
You can filter the list by certain attributes (owner, name, status, tags) in the following way: "owner:John Doe name:my little server". If you don't specify any attribute names the filtering will be done by name.
</div>
<div class="alert alert-info"> <div class="alert alert-info">
{% trans "You can select multiple vm instances while holding down the <strong>CTRL</strong> key." %} {% trans "You can select multiple vm instances while holding down the <strong>CTRL</strong> key." %}
......
...@@ -70,7 +70,7 @@ from .forms import ( ...@@ -70,7 +70,7 @@ from .forms import (
VmSaveForm, UserKeyForm, VmRenewForm, VmSaveForm, UserKeyForm, VmRenewForm,
CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm, CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm,
TraitsForm, RawDataForm, GroupPermissionForm, AclUserAddForm, TraitsForm, RawDataForm, GroupPermissionForm, AclUserAddForm,
VmResourcesForm, VmAddInterfaceForm, VmResourcesForm, VmAddInterfaceForm, VmListSearchForm
) )
from .tables import ( from .tables import (
...@@ -225,7 +225,7 @@ class IndexView(LoginRequiredMixin, TemplateView): ...@@ -225,7 +225,7 @@ class IndexView(LoginRequiredMixin, TemplateView):
# template # template
if user.has_perm('vm.create_template'): if user.has_perm('vm.create_template'):
context['templates'] = InstanceTemplate.get_objects_with_level( context['templates'] = InstanceTemplate.get_objects_with_level(
'operator', user).all()[:5] 'operator', user, disregard_superuser=True).all()[:5]
# toplist # toplist
if settings.STORE_URL: if settings.STORE_URL:
...@@ -518,6 +518,7 @@ class OperationView(RedirectToLoginMixin, DetailView): ...@@ -518,6 +518,7 @@ class OperationView(RedirectToLoginMixin, DetailView):
show_in_toolbar = True show_in_toolbar = True
effect = None effect = None
wait_for_result = None wait_for_result = None
with_reload = False
@property @property
def name(self): def name(self):
...@@ -660,11 +661,14 @@ class AjaxOperationMixin(object): ...@@ -660,11 +661,14 @@ class AjaxOperationMixin(object):
resp = super(AjaxOperationMixin, self).post( resp = super(AjaxOperationMixin, self).post(
request, extra, *args, **kwargs) request, extra, *args, **kwargs)
if request.is_ajax(): if request.is_ajax():
if not self.with_reload:
store = messages.get_messages(request) store = messages.get_messages(request)
store.used = True store.used = True
else:
store = []
return HttpResponse( return HttpResponse(
json.dumps({'success': True, json.dumps({'success': True,
'with_reload': getattr(self, 'with_reload', False), 'with_reload': self.with_reload,
'messages': [unicode(m) for m in store]}), 'messages': [unicode(m) for m in store]}),
content_type="application=json" content_type="application=json"
) )
...@@ -706,9 +710,8 @@ class FormOperationMixin(object): ...@@ -706,9 +710,8 @@ class FormOperationMixin(object):
return HttpResponse( return HttpResponse(
json.dumps({ json.dumps({
'success': True, 'success': True,
'with_reload': getattr(self, 'with_reload', False)}), 'with_reload': self.with_reload}),
content_type="application=json" content_type="application=json")
)
else: else:
return resp return resp
else: else:
...@@ -718,7 +721,7 @@ class FormOperationMixin(object): ...@@ -718,7 +721,7 @@ class FormOperationMixin(object):
class RequestFormOperationMixin(FormOperationMixin): class RequestFormOperationMixin(FormOperationMixin):
def get_form_kwargs(self): def get_form_kwargs(self):
val = super(FormOperationMixin, self).get_form_kwargs() val = super(RequestFormOperationMixin, self).get_form_kwargs()
val.update({'request': self.request}) val.update({'request': self.request})
return val return val
...@@ -966,6 +969,10 @@ vm_ops = OrderedDict([ ...@@ -966,6 +969,10 @@ vm_ops = OrderedDict([
('password_reset', VmOperationView.factory( ('password_reset', VmOperationView.factory(
op='password_reset', icon='unlock', effect='warning', op='password_reset', icon='unlock', effect='warning',
show_in_toolbar=False, wait_for_result=0.5, with_reload=True)), show_in_toolbar=False, wait_for_result=0.5, with_reload=True)),
('mount_store', VmOperationView.factory(
op='mount_store', icon='briefcase', effect='info',
show_in_toolbar=False,
)),
]) ])
...@@ -1681,12 +1688,15 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView): ...@@ -1681,12 +1688,15 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView):
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
context = super(VmList, self).get_context_data(*args, **kwargs) context = super(VmList, self).get_context_data(*args, **kwargs)
context['ops'] = [v for k, v in vm_mass_ops.iteritems()] context['ops'] = [v for k, v in vm_mass_ops.iteritems()]
context['search_form'] = self.search_form
return context return context
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
if self.request.is_ajax(): if self.request.is_ajax():
return self._create_ajax_request() return self._create_ajax_request()
else: else:
self.search_form = VmListSearchForm(self.request.GET)
self.search_form.full_clean()
return super(VmList, self).get(*args, **kwargs) return super(VmList, self).get(*args, **kwargs)
def _create_ajax_request(self): def _create_ajax_request(self):
...@@ -1726,8 +1736,7 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView): ...@@ -1726,8 +1736,7 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView):
def get_queryset(self): def get_queryset(self):
logger.debug('VmList.get_queryset() called. User: %s', logger.debug('VmList.get_queryset() called. User: %s',
unicode(self.request.user)) unicode(self.request.user))
queryset = Instance.get_objects_with_level( queryset = self.create_default_queryset()
'user', self.request.user).filter(destroyed_at=None)
self.create_fake_get() self.create_fake_get()
sort = self.request.GET.get("sort") sort = self.request.GET.get("sort")
...@@ -1742,6 +1751,18 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView): ...@@ -1742,6 +1751,18 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView):
**self.get_queryset_filters()).select_related('owner', 'node' **self.get_queryset_filters()).select_related('owner', 'node'
).distinct() ).distinct()
def create_default_queryset(self):
cleaned_data = self.search_form.cleaned_data
stype = cleaned_data.get('stype', 2)
superuser = stype == 2
shared = stype == 1
level = "owner" if stype == 0 else "user"
queryset = Instance.get_objects_with_level(
level, self.request.user,
group_also=shared, disregard_superuser=not superuser,
).filter(destroyed_at=None)
return queryset
def create_fake_get(self): def create_fake_get(self):
""" """
Updates the request's GET dict to filter the vm list Updates the request's GET dict to filter the vm list
...@@ -1983,8 +2004,8 @@ class VmCreate(LoginRequiredMixin, TemplateView): ...@@ -1983,8 +2004,8 @@ class VmCreate(LoginRequiredMixin, TemplateView):
form_error = form is not None form_error = form is not None
template = (form.template.pk if form_error template = (form.template.pk if form_error
else request.GET.get("template")) else request.GET.get("template"))
templates = InstanceTemplate.get_objects_with_level('user', templates = InstanceTemplate.get_objects_with_level(
request.user) 'user', request.user, disregard_superuser=True)
if form is None and template: if form is None and template:
form = self.form_class(user=request.user, form = self.form_class(user=request.user,
template=templates.get(pk=template)) template=templates.get(pk=template))
......
...@@ -62,6 +62,15 @@ class BuildFirewall: ...@@ -62,6 +62,15 @@ class BuildFirewall:
extra='-j DNAT --to-destination %s:%s' % (rule.host.ipv4, extra='-j DNAT --to-destination %s:%s' % (rule.host.ipv4,
rule.dport))) rule.dport)))
# SNAT rules for machines with public IPv4
for host in Host.objects.exclude(external_ipv4=None).select_related(
'vlan').prefetch_related('vlan__snat_to'):
for vl_out in host.vlan.snat_to.all():
self.add_rules(POSTROUTING=IptRule(
priority=1500, src=(host.ipv4, None),
extra='-o %s -j SNAT --to-source %s' % (
vl_out.name, host.external_ipv4)))
# default outbound NAT rules for VLANs # default outbound NAT rules for VLANs
for vl_in in Vlan.objects.exclude( for vl_in in Vlan.objects.exclude(
snat_ip=None).prefetch_related('snat_to'): snat_ip=None).prefetch_related('snat_to'):
...@@ -183,9 +192,12 @@ def generate_ptr_records(): ...@@ -183,9 +192,12 @@ def generate_ptr_records():
for host in Host.objects.order_by('vlan').all(): for host in Host.objects.order_by('vlan').all():
template = host.vlan.reverse_domain template = host.vlan.reverse_domain
i = host.get_external_ipv4().words if not host.shared_ip and host.external_ipv4: # DMZ
reverse = (host.reverse if host.reverse not in [None, ''] i = host.external_ipv4.words
else host.get_fqdn()) reverse = host.get_hostname('ipv4', public=True)
else:
i = host.ipv4.words
reverse = host.get_hostname('ipv4', public=False)
# ipv4 # ipv4
if host.ipv4: if host.ipv4:
...@@ -194,7 +206,7 @@ def generate_ptr_records(): ...@@ -194,7 +206,7 @@ def generate_ptr_records():
# ipv6 # ipv6
if host.ipv6: if host.ipv6:
DNS.append("^%s:%s:%s" % (host.ipv6.reverse_dns, DNS.append("^%s:%s:%s" % (host.ipv6.reverse_dns.rstrip('.'),
reverse, settings['dns_ttl'])) reverse, settings['dns_ttl']))
return DNS return DNS
...@@ -211,14 +223,14 @@ def generate_records(): ...@@ -211,14 +223,14 @@ def generate_records():
'CNAME': 'C%(fqdn)s:%(address)s:%(ttl)s', 'CNAME': 'C%(fqdn)s:%(address)s:%(ttl)s',
'MX': '@%(fqdn)s::%(address)s:%(dist)s:%(ttl)s', 'MX': '@%(fqdn)s::%(address)s:%(dist)s:%(ttl)s',
'PTR': '^%(fqdn)s:%(address)s:%(ttl)s', 'PTR': '^%(fqdn)s:%(address)s:%(ttl)s',
'TXT': '%(fqdn)s:%(octal)s:%(ttl)s'} 'TXT': "'%(fqdn)s:%(octal)s:%(ttl)s"}
retval = [] retval = []
for r in Record.objects.all(): for r in Record.objects.all():
params = {'fqdn': r.fqdn, 'address': r.address, 'ttl': r.ttl} params = {'fqdn': r.fqdn, 'address': r.address, 'ttl': r.ttl}
if r.type == 'MX': if r.type == 'MX':
params['address'], params['dist'] = r.address.split(':', 2) params['dist'], params['address'] = r.address.split(':', 2)
if r.type == 'AAAA': if r.type == 'AAAA':
try: try:
params['octal'] = ipv6_to_octal(r.address) params['octal'] = ipv6_to_octal(r.address)
......
...@@ -22,7 +22,7 @@ from collections import OrderedDict ...@@ -22,7 +22,7 @@ from collections import OrderedDict
logger = logging.getLogger() logger = logging.getLogger()
ipv4_re = re.compile( ipv4_re = re.compile(
r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}') r'(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}')
class InvalidRuleExcepion(Exception): class InvalidRuleExcepion(Exception):
......
...@@ -575,10 +575,14 @@ class Host(models.Model): ...@@ -575,10 +575,14 @@ class Host(models.Model):
# IPv4 # IPv4
if self.ipv4 is not None: if self.ipv4 is not None:
if not self.shared_ip and self.external_ipv4: # DMZ
ipv4 = self.external_ipv4
else:
ipv4 = self.ipv4
# update existing records # update existing records
affected_records = Record.objects.filter( affected_records = Record.objects.filter(
host=self, name=self.hostname, host=self, name=self.hostname,
type='A').update(address=self.ipv4) type='A').update(address=ipv4)
# create new record # create new record
if affected_records == 0: if affected_records == 0:
Record(host=self, Record(host=self,
...@@ -714,6 +718,8 @@ class Host(models.Model): ...@@ -714,6 +718,8 @@ class Host(models.Model):
:type proto: str. :type proto: str.
""" """
assert proto in ('ipv6', 'ipv4', ) assert proto in ('ipv6', 'ipv4', )
if self.reverse:
return self.reverse
try: try:
if proto == 'ipv6': if proto == 'ipv6':
res = self.record_set.filter(type='AAAA', res = self.record_set.filter(type='AAAA',
...@@ -736,7 +742,7 @@ class Host(models.Model): ...@@ -736,7 +742,7 @@ class Host(models.Model):
Return a list of ports with forwarding rules set. Return a list of ports with forwarding rules set.
""" """
retval = [] retval = []
for rule in self.rules.all(): for rule in self.rules.filter(dport__isnull=False, direction='in'):
forward = { forward = {
'proto': rule.proto, 'proto': rule.proto,
'private': rule.dport, 'private': rule.dport,
......
...@@ -35,7 +35,7 @@ COMMIT ...@@ -35,7 +35,7 @@ COMMIT
{% if proto == "ipv4" %} {% if proto == "ipv4" %}
-A FORWARD -p icmp --icmp-type echo-request -g LOG_ACC -A FORWARD -p icmp --icmp-type echo-request -g LOG_ACC
{% else %} {% else %}
-A FORWARD -p icmpv6 --icmpv6-type echo-request -g LOG_ACC -A FORWARD -p icmpv6 -g LOG_ACC
{% endif %} {% endif %}
# initialize INPUT chain # initialize INPUT chain
...@@ -45,6 +45,11 @@ COMMIT ...@@ -45,6 +45,11 @@ COMMIT
-A INPUT -m state --state INVALID -g LOG_DROP -A INPUT -m state --state INVALID -g LOG_DROP
-A INPUT -i lo -j ACCEPT -A INPUT -i lo -j ACCEPT
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
{% if proto == "ipv4" %}
-A INPUT -p icmp --icmp-type echo-request -g LOG_ACC
{% else %}
-A INPUT -p icmpv6 -g LOG_ACC
{% endif %}
# initialize OUTPUT chain # initialize OUTPUT chain
-A OUTPUT -m state --state INVALID -g LOG_DROP -A OUTPUT -m state --state INVALID -g LOG_DROP
......
...@@ -32,6 +32,7 @@ celery = Celery('manager', ...@@ -32,6 +32,7 @@ celery = Celery('manager',
'storage.tasks.periodic_tasks', 'storage.tasks.periodic_tasks',
'firewall.tasks.local_tasks', 'firewall.tasks.local_tasks',
'monitor.tasks.local_periodic_tasks', 'monitor.tasks.local_periodic_tasks',
'dashboard.tasks.local_periodic_tasks',
]) ])
celery.conf.update( celery.conf.update(
...@@ -60,7 +61,7 @@ celery.conf.update( ...@@ -60,7 +61,7 @@ celery.conf.update(
'schedule': timedelta(hours=1), 'schedule': timedelta(hours=1),
'options': {'queue': 'localhost.man'} 'options': {'queue': 'localhost.man'}
}, },
'dashboard.local_periodic_tasks': { 'dashboard.send_email_notifications': {
'task': 'dashboard.tasks.local_periodic_tasks.' 'task': 'dashboard.tasks.local_periodic_tasks.'
'send_email_notifications', 'send_email_notifications',
'schedule': timedelta(hours=24), 'schedule': timedelta(hours=24),
......
...@@ -28,7 +28,7 @@ def check_queue(storage, queue_id, priority): ...@@ -28,7 +28,7 @@ def check_queue(storage, queue_id, priority):
if priority is not None: if priority is not None:
queue_name = queue_name + "." + priority queue_name = queue_name + "." + priority
inspect = celery.control.inspect() inspect = celery.control.inspect()
inspect.timeout = 0.1 inspect.timeout = 0.5
active_queues = inspect.active_queues() active_queues = inspect.active_queues()
if active_queues is None: if active_queues is None:
return False return False
......
...@@ -293,6 +293,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -293,6 +293,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
message = ugettext_noop( message = ugettext_noop(
"Instance %(instance)s has already been destroyed.") "Instance %(instance)s has already been destroyed.")
class NoAgentError(InstanceError):
message = ugettext_noop(
"No agent software is running on instance %(instance)s.")
class WrongStateError(InstanceError): class WrongStateError(InstanceError):
message = ugettext_noop( message = ugettext_noop(
"Current state (%(state)s) of instance %(instance)s is " "Current state (%(state)s) of instance %(instance)s is "
...@@ -483,9 +487,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -483,9 +487,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
""" """
try: try:
datastore = self.disks.all()[0].datastore datastore = self.disks.all()[0].datastore
except: except IndexError:
return None from storage.models import DataStore
else: datastore = DataStore.objects.get()
path = datastore.path + '/' + self.vm_name + '.dump' path = datastore.path + '/' + self.vm_name + '.dump'
return {'datastore': datastore, 'path': path} return {'datastore': datastore, 'path': path}
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from functools import update_wrapper
from logging import getLogger from logging import getLogger
from warnings import warn from warnings import warn
import requests import requests
...@@ -51,6 +52,8 @@ def node_available(function): ...@@ -51,6 +52,8 @@ def node_available(function):
return function(self, *args, **kwargs) return function(self, *args, **kwargs)
else: else:
return None return None
update_wrapper(decorate, function)
decorate._original = function
return decorate return decorate
......
...@@ -19,10 +19,12 @@ from __future__ import absolute_import, unicode_literals ...@@ -19,10 +19,12 @@ from __future__ import absolute_import, unicode_literals
from logging import getLogger from logging import getLogger
from re import search from re import search
from string import ascii_lowercase from string import ascii_lowercase
from urlparse import urlsplit
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, ugettext_noop from django.utils.translation import ugettext_lazy as _, ugettext_noop
from django.conf import settings
from sizefield.utils import filesizeformat from sizefield.utils import filesizeformat
...@@ -41,6 +43,8 @@ from .models import ( ...@@ -41,6 +43,8 @@ from .models import (
) )
from .tasks import agent_tasks from .tasks import agent_tasks
from dashboard.store_api import Store, NoStoreException
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -923,8 +927,27 @@ class ResourcesOperation(InstanceOperation): ...@@ -923,8 +927,27 @@ class ResourcesOperation(InstanceOperation):
register_operation(ResourcesOperation) register_operation(ResourcesOperation)
class PasswordResetOperation(InstanceOperation): class EnsureAgentMixin(object):
activity_code_suffix = 'Password reset' accept_states = ('RUNNING', )
def check_precond(self):
super(EnsureAgentMixin, self).check_precond()
last_boot_time = self.instance.activity_log.filter(
succeeded=True, activity_code__in=(
"vm.Instance.deploy", "vm.Instance.reset",
"vm.Instance.reboot")).latest("finished").finished
try:
InstanceActivity.objects.filter(
activity_code="vm.Instance.agent.starting",
started__gt=last_boot_time).latest("started")
except InstanceActivity.DoesNotExist: # no agent since last boot
raise self.instance.NoAgentError(self.instance)
class PasswordResetOperation(EnsureAgentMixin, InstanceOperation):
activity_code_suffix = 'password_reset'
id = 'password_reset' id = 'password_reset'
name = _("password reset") name = _("password reset")
description = _("Generate and set a new login password on the virtual " description = _("Generate and set a new login password on the virtual "
...@@ -934,7 +957,6 @@ class PasswordResetOperation(InstanceOperation): ...@@ -934,7 +957,6 @@ class PasswordResetOperation(InstanceOperation):
"it.") "it.")
acl_level = "owner" acl_level = "owner"
required_perms = () required_perms = ()
accept_states = ('RUNNING', )
def _operation(self): def _operation(self):
self.instance.pw = pwgen() self.instance.pw = pwgen()
...@@ -945,3 +967,34 @@ class PasswordResetOperation(InstanceOperation): ...@@ -945,3 +967,34 @@ class PasswordResetOperation(InstanceOperation):
register_operation(PasswordResetOperation) register_operation(PasswordResetOperation)
class MountStoreOperation(EnsureAgentMixin, InstanceOperation):
activity_code_suffix = 'mount_store'
id = 'mount_store'
name = _("mount store")
description = _(
"This operation attaches your personal file store. Other users who "
"have access to this machine can see these files as well."
)
acl_level = "owner"
required_perms = ()
def check_auth(self, user):
super(MountStoreOperation, self).check_auth(user)
try:
Store(user)
except NoStoreException:
raise PermissionDenied # not show the button at all
def _operation(self):
inst = self.instance
queue = self.instance.get_remote_queue_name("agent")
host = urlsplit(settings.STORE_URL).hostname
username = Store(inst.owner).username
password = inst.owner.profile.smb_password
agent_tasks.mount_store.apply_async(
queue=queue, args=(inst.vm_name, host, username, password))
register_operation(MountStoreOperation)
...@@ -86,18 +86,11 @@ def agent_started(vm, version=None): ...@@ -86,18 +86,11 @@ def agent_started(vm, version=None):
if version and version != settings.AGENT_VERSION: if version and version != settings.AGENT_VERSION:
try: try:
with act.sub_activity( update_agent(vm, instance, act)
'update',
readable_name=create_readable(
ugettext_noop('update to %(version)s'),
version=settings.AGENT_VERSION)
):
update.apply_async(
queue=queue,
args=(vm, create_agent_tar())).get(timeout=10)
return
except TimeoutError: except TimeoutError:
pass pass
else:
return # agent is going to restart
if not initialized: if not initialized:
measure_boot_time(instance) measure_boot_time(instance)
...@@ -139,3 +132,23 @@ def agent_stopped(vm): ...@@ -139,3 +132,23 @@ def agent_stopped(vm):
act = qs.latest('id') act = qs.latest('id')
with act.sub_activity('stopping', readable_name=ugettext_noop('stopping')): with act.sub_activity('stopping', readable_name=ugettext_noop('stopping')):
pass pass
def update_agent(instance, vm, act=None):
if act:
act.sub_activity(
'update',
readable_name=create_readable(
ugettext_noop('update to %(version)s'),
version=settings.AGENT_VERSION))
else:
from vm.models import instance_activity
act = instance_activity(
code_suffix='agent.update', instance=instance,
readable_name=create_readable(
ugettext_noop('update agent to %(version)s'),
version=settings.AGENT_VERSION))
with act:
queue = instance.get_remote_queue_name("agent")
update.apply_async(queue=queue,
args=(vm, create_agent_tar())).get(timeout=10)
...@@ -55,7 +55,7 @@ def get_queues(): ...@@ -55,7 +55,7 @@ def get_queues():
result = cache.get(key) result = cache.get(key)
if result is None: if result is None:
inspect = celery.control.inspect() inspect = celery.control.inspect()
inspect.timeout = 0.1 inspect.timeout = 0.5
result = inspect.active_queues() result = inspect.active_queues()
logger.debug('Queue list of length %d cached.', len(result)) logger.debug('Queue list of length %d cached.', len(result))
cache.set(key, result, 10) cache.set(key, result, 10)
......
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