Improved resource monitoring

This commit is contained in:
Marc 2014-07-09 16:17:43 +00:00
parent 1cd3092673
commit 9b9abc3c91
39 changed files with 420 additions and 451 deletions

View File

@ -47,3 +47,6 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
* passlib; nano /usr/local/lib/python2.7/dist-packages/passlib/ext/django/utils.py SortedDict -> collections.OrderedDict * passlib; nano /usr/local/lib/python2.7/dist-packages/passlib/ext/django/utils.py SortedDict -> collections.OrderedDict
* pip install pyinotify * pip install pyinotify
* Backend.operations dynamically generated based on defined methods

View File

@ -8,12 +8,13 @@ from django.utils.encoding import force_text
def action_with_confirmation(action_name, extra_context={}, def action_with_confirmation(action_name, extra_context={},
template='admin/controller/generic_confirmation.html'): template='admin/orchestra/generic_confirmation.html'):
""" """
Generic pattern for actions that needs confirmation step Generic pattern for actions that needs confirmation step
If custom template is provided the form must contain: If custom template is provided the form must contain:
<input type="hidden" name="post" value="generic_confirmation" /> <input type="hidden" name="post" value="generic_confirmation" />
""" """
def decorator(func, extra_context=extra_context, template=template): def decorator(func, extra_context=extra_context, template=template):
@wraps(func, assigned=available_attrs(func)) @wraps(func, assigned=available_attrs(func))
def inner(modeladmin, request, queryset): def inner(modeladmin, request, queryset):

View File

@ -1,18 +1,25 @@
from orchestra.apps.orchestration import ServiceBackend from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceController
from orchestra.apps.resources import ServiceMonitor
from . import settings from . import settings
class MySQLDBBackend(ServiceBackend): class MySQLDBBackend(ServiceController):
verbose_name = "MySQL database" verbose_name = "MySQL database"
model = 'databases.Database' model = 'databases.Database'
def save(self, database): def save(self, database):
if database.type == database.MYSQL: if database.type == database.MYSQL:
context = self.get_context(database) context = self.get_context(database)
self.append("mysql -e 'CREATE DATABASE `%(database)s`;'" % context) self.append(
self.append("mysql -e 'GRANT ALL PRIVILEGES ON `%(database)s`.* " "mysql -e 'CREATE DATABASE `%(database)s`;'" % context
" TO \"%(owner)s\"@\"%(host)s\" WITH GRANT OPTION;'" % context) )
self.append(
"mysql -e 'GRANT ALL PRIVILEGES ON `%(database)s`.* "
" TO \"%(owner)s\"@\"%(host)s\" WITH GRANT OPTION;'" % context
)
def delete(self, database): def delete(self, database):
if database.type == database.MYSQL: if database.type == database.MYSQL:
@ -30,21 +37,27 @@ class MySQLDBBackend(ServiceBackend):
} }
class MySQLUserBackend(ServiceBackend): class MySQLUserBackend(ServiceController):
verbose_name = "MySQL user" verbose_name = "MySQL user"
model = 'databases.DatabaseUser' model = 'databases.DatabaseUser'
def save(self, database): def save(self, database):
if database.type == database.MYSQL: if database.type == database.MYSQL:
context = self.get_context(database) context = self.get_context(database)
self.append("mysql -e 'CREATE USER \"%(username)s\"@\"%(host)s\";'" % context) self.append(
self.append("mysql -e 'UPDATE mysql.user SET Password=\"%(password)s\" " "mysql -e 'CREATE USER \"%(username)s\"@\"%(host)s\";'" % context
" WHERE User=\"%(username)s\";'" % context) )
self.append(
"mysql -e 'UPDATE mysql.user SET Password=\"%(password)s\" "
" WHERE User=\"%(username)s\";'" % context
)
def delete(self, database): def delete(self, database):
if database.type == database.MYSQL: if database.type == database.MYSQL:
context = self.get_context(database) context = self.get_context(database)
self.append("mysql -e 'DROP USER \"%(username)s\"@\"%(host)s\";'" % context) self.append(
"mysql -e 'DROP USER \"%(username)s\"@\"%(host)s\";'" % context
)
def get_context(self, database): def get_context(self, database):
return { return {
@ -54,7 +67,12 @@ class MySQLUserBackend(ServiceBackend):
} }
class MySQLPermissionBackend(ServiceBackend): class MySQLPermissionBackend(ServiceController):
model = 'databases.UserDatabaseRelation' model = 'databases.UserDatabaseRelation'
verbose_name = "MySQL permission" verbose_name = "MySQL permission"
class MysqlDisk(ServiceMonitor):
model = 'database.Database'
resource = ServiceMonitor.DISK
verbose_name = _("MySQL disk")

View File

@ -4,10 +4,10 @@ from django.utils.translation import ugettext_lazy as _
from . import settings from . import settings
from orchestra.apps.orchestration import ServiceBackend from orchestra.apps.orchestration import ServiceController
class Bind9MasterDomainBackend(ServiceBackend): class Bind9MasterDomainBackend(ServiceController):
verbose_name = _("Bind9 master domain") verbose_name = _("Bind9 master domain")
model = 'domains.Domain' model = 'domains.Domain'
related_models = ( related_models = (

View File

@ -1,11 +1,41 @@
from django.template import Template, Context from django.template import Template, Context
from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceBackend from orchestra.apps.orchestration import ServiceController
from orchestra.apps.resources import ServiceMonitor
class MailmanBackend(ServiceBackend): class MailmanBackend(ServiceController):
verbose_name = "Mailman" verbose_name = "Mailman"
model = 'lists.List' model = 'lists.List'
def save(self, mailinglist):
pass class MailmanTraffic(ServiceMonitor):
model = 'lists.List'
resource = ServiceMonitor.TRAFFIC
def process(self, output):
for line in output.readlines():
listname, value = line.strip().slpit()
def monitor(self, mailinglist):
self.append(
"LISTS=$(grep -v 'post to mailman' /var/log/mailman/post"
" | grep size | cut -d'<' -f2 | cut -d'>' -f1 | sort | uniq"
" | while read line; do \n"
" grep \"$line\" post | head -n1 | awk {'print $8\" \"$11'}"
" | sed 's/size=//' | sed 's/,//'\n"
"done)"
)
self.append(
'SUBS=""\n'
'while read LIST; do\n'
' NAME=$(echo "$LIST" | awk {\'print $1\'})\n'
' SIZE=$(echo "$LIST" | awk {\'print $2\'})\n'
' if [[ ! $(echo -e "$SUBS" | grep "$NAME") ]]; then\n'
' SUBS="${SUBS}${NAME} $(list_members "$NAME" | wc -l)\n"\n'
' fi\n'
' SUBSCRIBERS=$(echo -e "$SUBS" | grep "$NAME" | awk {\'print $2\'})\n'
' echo "$NAME $(($SUBSCRIBERS*$SIZE))"\n'
'done <<< "$LISTS"'
)

View File

@ -1 +1 @@
from .backends import ServiceBackend from .backends import ServiceBackend, ServiceController

View File

@ -22,10 +22,11 @@ STATE_COLORS = {
class RouteAdmin(admin.ModelAdmin): class RouteAdmin(admin.ModelAdmin):
list_display = [ list_display = [
'id', 'backend', 'host', 'match', 'display_model', 'is_active' 'id', 'backend', 'host', 'match', 'display_model', 'display_actions',
'is_active'
] ]
list_editable = ['backend', 'host', 'match', 'is_active'] list_editable = ['backend', 'host', 'match', 'is_active']
list_filter = ['backend', 'host', 'is_active'] list_filter = ['host', 'is_active', 'backend']
def display_model(self, route): def display_model(self, route):
try: try:
@ -35,6 +36,14 @@ class RouteAdmin(admin.ModelAdmin):
display_model.short_description = _("model") display_model.short_description = _("model")
display_model.allow_tags = True display_model.allow_tags = True
def display_actions(self, route):
try:
return '<br>'.join(route.get_backend().get_actions())
except KeyError:
return "<span style='color: red;'>NOT AVAILABLE</span>"
display_actions.short_description = _("actions")
display_actions.allow_tags = True
class BackendOperationInline(admin.TabularInline): class BackendOperationInline(admin.TabularInline):
model = BackendOperation model = BackendOperation

View File

@ -23,6 +23,7 @@ class ServiceBackend(object):
function_method = methods.Python function_method = methods.Python
type = 'task' # 'sync' type = 'task' # 'sync'
ignore_fields = [] ignore_fields = []
actions = []
# TODO type: 'script', execution:'task' # TODO type: 'script', execution:'task'
@ -37,6 +38,10 @@ class ServiceBackend(object):
def __init__(self): def __init__(self):
self.cmds = [] self.cmds = []
@classmethod
def get_actions(cls):
return [ action for action in cls.actions if action in dir(cls) ]
@classmethod @classmethod
def get_name(cls): def get_name(cls):
return cls.__name__ return cls.__name__
@ -68,7 +73,7 @@ class ServiceBackend(object):
choices = [] choices = []
for b in backends: for b in backends:
# don't evaluate b.verbose_name ugettext_lazy # don't evaluate b.verbose_name ugettext_lazy
verbose = getattr(b.verbose_name, '_proxy____args', [None]) verbose = getattr(b.verbose_name, '_proxy____args', [b.verbose_name])
if verbose[0]: if verbose[0]:
verbose = b.verbose_name verbose = b.verbose_name
else: else:
@ -110,3 +115,12 @@ class ServiceBackend(object):
the service once in bulk operations the service once in bulk operations
""" """
pass pass
class ServiceController(ServiceBackend):
actions = ('save', 'delete')
@classmethod
def get_backends(cls):
""" filter controller classes """
return [ plugin for plugin in cls.plugins if ServiceController in plugin.__mro__ ]

View File

@ -76,16 +76,14 @@ class BackendOperation(models.Model):
""" """
Encapsulates an operation, storing its related object, the action and the backend. Encapsulates an operation, storing its related object, the action and the backend.
""" """
SAVE = 'save'
DELETE = 'delete' DELETE = 'delete'
ACTIONS = ( SAVE = 'save'
(SAVE, _("save")), MONITOR = 'monitor'
(DELETE, _("delete")),
)
log = models.ForeignKey('orchestration.BackendLog', related_name='operations') log = models.ForeignKey('orchestration.BackendLog', related_name='operations')
# TODO backend and backend_class() (like content_type)
backend_class = models.CharField(_("backend"), max_length=256) backend_class = models.CharField(_("backend"), max_length=256)
action = models.CharField(_("action"), max_length=64, choices=ACTIONS) action = models.CharField(_("action"), max_length=64)
content_type = models.ForeignKey(ContentType) content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField() object_id = models.PositiveIntegerField()
instance = generic.GenericForeignKey('content_type', 'object_id') instance = generic.GenericForeignKey('content_type', 'object_id')
@ -149,14 +147,21 @@ class Route(models.Model):
@classmethod @classmethod
def get_servers(cls, operation): def get_servers(cls, operation):
backend_name = operation.backend.get_name() # TODO use cached data sctructure and refactor
backend = operation.backend
servers = []
try: try:
routes = cls.objects.filter(is_active=True, backend=backend_name) routes = cls.objects.filter(is_active=True, backend=backend.get_name())
except cls.DoesNotExist: except cls.DoesNotExist:
return [] return servers
safe_locals = { 'instance': operation.instance } safe_locals = {
pks = [ route.pk for route in routes.all() if eval(route.match, safe_locals) ] 'instance': operation.instance
return [ route.host for route in routes.filter(pk__in=pks) ] }
actions = backend.get_actions()
for route in routes:
if operation.action in actions and eval(route.match, safe_locals):
servers.append(route.host)
return servers
def get_backend(self): def get_backend(self):
for backend in ServiceBackend.get_backends(): for backend in ServiceBackend.get_backends():

View File

@ -1 +1,4 @@
from .backends import ServiceMonitor
default_app_config = 'orchestra.apps.resources.apps.ResourcesConfig' default_app_config = 'orchestra.apps.resources.apps.ResourcesConfig'

View File

@ -1,5 +1,3 @@
import sys
from django.contrib import admin from django.contrib import admin
from django.contrib.contenttypes import generic from django.contrib.contenttypes import generic
from django.utils.functional import cached_property from django.utils.functional import cached_property
@ -7,9 +5,10 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.admin.filters import UsedContentTypeFilter from orchestra.admin.filters import UsedContentTypeFilter
from orchestra.admin.utils import insertattr, get_modeladmin from orchestra.admin.utils import insertattr, get_modeladmin
from orchestra.utils import running_syncdb
from .forms import ResourceForm from .forms import ResourceForm
from .models import Resource, ResourceAllocation, Monitor, MonitorData from .models import Resource, ResourceData, MonitorData
class ResourceAdmin(admin.ModelAdmin): class ResourceAdmin(admin.ModelAdmin):
@ -26,30 +25,24 @@ class ResourceAdmin(admin.ModelAdmin):
resources = obj.content_type.resource_set.filter(is_active=True) resources = obj.content_type.resource_set.filter(is_active=True)
inlines = [] inlines = []
for inline in modeladmin.inlines: for inline in modeladmin.inlines:
if inline.model is ResourceAllocation: if inline.model is ResourceData:
inline = resource_inline_factory(resources) inline = resource_inline_factory(resources)
inlines.append(inline) inlines.append(inline)
modeladmin.inlines = inlines modeladmin.inlines = inlines
class ResourceAllocationAdmin(admin.ModelAdmin): class ResourceDataAdmin(admin.ModelAdmin):
list_display = ('id', 'resource', 'content_object', 'value') list_display = ('id', 'resource', 'used', 'allocated', 'last_update',) # TODO content_object
list_filter = ('resource',) list_filter = ('resource',)
class MonitorAdmin(admin.ModelAdmin):
list_display = ('backend', 'resource', 'crontab')
list_filter = ('backend', 'resource')
class MonitorDataAdmin(admin.ModelAdmin): class MonitorDataAdmin(admin.ModelAdmin):
list_display = ('id', 'monitor', 'content_object', 'date', 'value') list_display = ('id', 'monitor', 'date', 'value') # TODO content_object
list_filter = ('monitor',) list_filter = ('monitor',)
admin.site.register(Resource, ResourceAdmin) admin.site.register(Resource, ResourceAdmin)
admin.site.register(ResourceAllocation, ResourceAllocationAdmin) admin.site.register(ResourceData, ResourceDataAdmin)
admin.site.register(Monitor, MonitorAdmin)
admin.site.register(MonitorData, MonitorDataAdmin) admin.site.register(MonitorData, MonitorDataAdmin)
@ -68,7 +61,7 @@ def resource_inline_factory(resources):
return forms return forms
class ResourceInline(generic.GenericTabularInline): class ResourceInline(generic.GenericTabularInline):
model = ResourceAllocation model = ResourceData
verbose_name_plural = _("resources") verbose_name_plural = _("resources")
form = ResourceForm form = ResourceForm
formset = ResourceInlineFormSet formset = ResourceInlineFormSet
@ -84,7 +77,7 @@ def resource_inline_factory(resources):
return ResourceInline return ResourceInline
if not 'migrate' in sys.argv and not 'syncdb' in sys.argv: if not running_syncdb():
# not run during syncdb # not run during syncdb
for resources in Resource.group_by_content_type(): for resources in Resource.group_by_content_type():
inline = resource_inline_factory(resources) inline = resource_inline_factory(resources)

View File

@ -1,6 +1,8 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.contrib.contenttypes import generic from django.contrib.contenttypes import generic
from orchestra.utils import running_syncdb
class ResourcesConfig(AppConfig): class ResourcesConfig(AppConfig):
name = 'orchestra.apps.resources' name = 'orchestra.apps.resources'
@ -9,7 +11,8 @@ class ResourcesConfig(AppConfig):
def ready(self): def ready(self):
from .models import Resource from .models import Resource
# TODO execute on Resource.save() # TODO execute on Resource.save()
relation = generic.GenericRelation('resources.ResourceAllocation') if not running_syncdb():
for resources in Resource.group_by_content_type(): relation = generic.GenericRelation('resources.ResourceData')
model = resources[0].content_type.model_class() for resources in Resource.group_by_content_type():
model.add_to_class('allocations', relation) model = resources[0].content_type.model_class()
model.add_to_class('resources', relation)

View File

@ -0,0 +1,27 @@
from orchestra.apps.orchestration import ServiceBackend
class ServiceMonitor(ServiceBackend):
TRAFFIC = 'traffic'
DISK = 'disk'
MEMORY = 'memory'
CPU = 'cpu'
actions = ('monitor', 'resource_exceeded', 'resource_recovery')
@classmethod
def get_backends(cls):
""" filter monitor classes """
return [plugin for plugin in cls.plugins if ServiceMonitor in plugin.__mro__]
def store(self, stdout):
""" object_id value """
for line in stdout.readlines():
line = line.strip()
object_id, value = line.split()
# TODO date
MonitorHistory.store(self.model, object_id, value, date)
def execute(self, server):
log = super(MonitorBackend, self).execute(server)
return log

View File

@ -7,23 +7,33 @@ from orchestra.forms.widgets import ShowTextWidget, ReadOnlyWidget
class ResourceForm(forms.ModelForm): class ResourceForm(forms.ModelForm):
verbose_name = forms.CharField(label=_("Name"), widget=ShowTextWidget(bold=True), verbose_name = forms.CharField(label=_("Name"), widget=ShowTextWidget(bold=True),
required=False) required=False)
current = forms.CharField(label=_("Current"), widget=ShowTextWidget(), used = forms.IntegerField(label=_("Used"), widget=ShowTextWidget(),
required=False) required=False)
value = forms.CharField(label=_("Allocation")) last_update = forms.CharField(label=_("Last update"), widget=ShowTextWidget(),
required=False)
allocated = forms.IntegerField(label=_("Allocated"))
class Meta: class Meta:
fields = ('verbose_name', 'current', 'value',) fields = ('verbose_name', 'used', 'last_update', 'allocated',)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.resource = kwargs.pop('resource', None) self.resource = kwargs.pop('resource', None)
super(ResourceForm, self).__init__(*args, **kwargs) super(ResourceForm, self).__init__(*args, **kwargs)
if self.resource: if self.resource:
self.fields['verbose_name'].initial = self.resource.verbose_name self.fields['verbose_name'].initial = self.resource.verbose_name
self.fields['current'].initial = self.resource.get_current() self.fields['used'].initial = self.resource.get_current()
if self.resource.ondemand: if self.resource.ondemand:
self.fields['value'].widget = ReadOnlyWidget('') self.fields['allocated'].required = False
self.fields['allocated'].widget = ReadOnlyWidget(None, '')
else: else:
self.fields['value'].initial = self.resource.default_allocation self.fields['allocated'].required = True
self.fields['allocated'].initial = self.resource.default_allocation
def has_changed(self):
""" Make sure resourcedata objects are created for all resources """
if not self.instance.pk:
return True
return super(ResourceForm, self).has_changed()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.instance.resource_id = self.resource.pk self.instance.resource_id = self.resource.pk

View File

@ -7,13 +7,25 @@ from django.core import validators
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from djcelery.models import PeriodicTask, CrontabSchedule from djcelery.models import PeriodicTask, CrontabSchedule
from orchestra.models.fields import MultiSelectField
from orchestra.utils.apps import autodiscover from orchestra.utils.apps import autodiscover
from .backends import ServiceMonitor
class Resource(models.Model): class Resource(models.Model):
MONTHLY = 'MONTHLY' """
Defines a resource, a resource is basically an interpretation of data
gathered by a Monitor
"""
LAST = 'LAST'
MONTHLY_SUM = 'MONTHLY_SUM'
MONTHLY_AVG = 'MONTHLY_AVG'
PERIODS = ( PERIODS = (
(MONTHLY, _('Monthly')), (LAST, _("Last")),
(MONTHLY_SUM, _("Monthly Sum")),
(MONTHLY_AVG, _("Monthly Average")),
) )
name = models.CharField(_("name"), max_length=32, unique=True, name = models.CharField(_("name"), max_length=32, unique=True,
@ -24,11 +36,14 @@ class Resource(models.Model):
verbose_name = models.CharField(_("verbose name"), max_length=256, unique=True) verbose_name = models.CharField(_("verbose name"), max_length=256, unique=True)
content_type = models.ForeignKey(ContentType) # TODO filter by servicE? content_type = models.ForeignKey(ContentType) # TODO filter by servicE?
period = models.CharField(_("period"), max_length=16, choices=PERIODS, period = models.CharField(_("period"), max_length=16, choices=PERIODS,
default=MONTHLY) default=LAST)
ondemand = models.BooleanField(default=False) ondemand = models.BooleanField(_("on demand"), default=False)
default_allocation = models.PositiveIntegerField(null=True, blank=True) default_allocation = models.PositiveIntegerField(_("default allocation"),
is_active = models.BooleanField(default=True) null=True, blank=True)
disable_trigger = models.BooleanField(default=False) is_active = models.BooleanField(_("is active"), default=True)
disable_trigger = models.BooleanField(_("disable trigger"), default=False)
monitors = MultiSelectField(_("monitors"), max_length=256,
choices=ServiceMonitor.get_choices())
def __unicode__(self): def __unicode__(self):
return self.name return self.name
@ -53,47 +68,58 @@ class Resource(models.Model):
today = datetime.date.today() today = datetime.date.today()
result = 0 result = 0
has_result = False has_result = False
for monitor in self.monitors.all(): for monitor in self.monitors:
has_result = True dataset = MonitorData.objects.filter(monitor=monitor)
if self.period == self.MONTHLY: if self.period == self.MONTHLY_AVG:
data = monitor.dataset.filter(date__year=today.year, try:
date__month=today.month) last = dataset.latest()
result += data.aggregate(models.Sum('value'))['value__sum'] except MonitorData.DoesNotExist:
continue
has_result = True
epoch = datetime(year=today.year, month=today.month, day=1)
total = (epoch-last.date).total_seconds()
dataset = dataset.filter(date__year=today.year,
date__month=today.month)
for data in dataset:
slot = (previous-data.date).total_seconds()
result += data.value * slot/total
elif self.period == self.MONTHLY_SUM:
data = dataset.filter(date__year=today.year,
date__month=today.month)
value = data.aggregate(models.Sum('value'))['value__sum']
if value:
has_result = True
result += value
elif self.period == self.LAST:
try:
result += dataset.latest().value
except MonitorData.DoesNotExist:
continue
has_result = True
else: else:
raise NotImplementedError("%s support not implemented" % self.period) raise NotImplementedError("%s support not implemented" % self.period)
return result if has_result else None return result if has_result else None
class ResourceAllocation(models.Model): class ResourceData(models.Model):
""" Stores computed resource usage and allocation """
resource = models.ForeignKey(Resource) resource = models.ForeignKey(Resource)
content_type = models.ForeignKey(ContentType) content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField() object_id = models.PositiveIntegerField()
value = models.PositiveIntegerField() used = models.PositiveIntegerField(null=True)
last_update = models.DateTimeField(null=True)
allocated = models.PositiveIntegerField(null=True)
content_object = generic.GenericForeignKey() content_object = generic.GenericForeignKey()
class Meta: class Meta:
unique_together = ('resource', 'content_type', 'object_id') unique_together = ('resource', 'content_type', 'object_id')
verbose_name_plural = _("resource data")
autodiscover('monitors')
class Monitor(models.Model):
backend = models.CharField(_("backend"), max_length=256,)
# choices=MonitorBackend.get_choices())
resource = models.ForeignKey(Resource, related_name='monitors')
crontab = models.ForeignKey(CrontabSchedule)
class Meta:
unique_together=('backend', 'resource')
def __unicode__(self):
return self.backend
class MonitorData(models.Model): class MonitorData(models.Model):
monitor = models.ForeignKey(Monitor, related_name='dataset') """ Stores monitored data """
monitor = models.CharField(_("monitor"), max_length=256,
choices=ServiceMonitor.get_choices())
content_type = models.ForeignKey(ContentType) content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField() object_id = models.PositiveIntegerField()
date = models.DateTimeField(auto_now_add=True) date = models.DateTimeField(auto_now_add=True)
@ -101,5 +127,9 @@ class MonitorData(models.Model):
content_object = generic.GenericForeignKey() content_object = generic.GenericForeignKey()
class Meta:
get_latest_by = 'date'
verbose_name_plural = _("monitor data")
def __unicode__(self): def __unicode__(self):
return str(self.monitor) return str(self.monitor)

View File

@ -1,27 +1,24 @@
from rest_framework import serializers from rest_framework import serializers
from orchestra.api import router from orchestra.api import router
from orchestra.utils import running_syncdb
from .models import Resource, ResourceAllocation from .models import Resource, ResourceData
class ResourceSerializer(serializers.ModelSerializer): class ResourceSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField('get_name') name = serializers.SerializerMethodField('get_name')
current = serializers.SerializerMethodField('get_current')
allocation = serializers.IntegerField(source='value')
class Meta: class Meta:
model = ResourceAllocation model = ResourceData
fields = ('name', 'current', 'allocation') fields = ('name', 'used', 'allocated')
read_only_fields = ('used',)
def get_name(self, instance): def get_name(self, instance):
return instance.resource.name return instance.resource.name
def get_current(self, instance):
return instance.resource.get_current()
if not running_syncdb():
for resources in Resource.group_by_content_type(): for resources in Resource.group_by_content_type():
model = resources[0].content_type.model_class() model = resources[0].content_type.model_class()
router.insert(model, 'resources', ResourceSerializer, required=False, router.insert(model, 'resources', ResourceSerializer, required=False)
source='allocations')

View File

@ -0,0 +1,14 @@
from celery import shared_task
from .backends import ServiceMonitor
@shared_task
def monitor(backend_name):
routes = Route.objects.filter(is_active=True, backend=backend_name)
for route in routes:
pass
for backend in ServiceMonitor.get_backends():
if backend.get_name() == backend_name:
# TODO execute monitor BackendOperation
pass

View File

@ -15,7 +15,7 @@ from .roles.filters import role_list_filter_factory
class UserAdmin(AccountAdminMixin, auth.UserAdmin, ExtendedModelAdmin): class UserAdmin(AccountAdminMixin, auth.UserAdmin, ExtendedModelAdmin):
list_display = ('username', 'is_main') list_display = ('username', 'display_is_main')
list_filter = ('is_staff', 'is_superuser', 'is_active') list_filter = ('is_staff', 'is_superuser', 'is_active')
fieldsets = ( fieldsets = (
(None, { (None, {
@ -25,7 +25,7 @@ class UserAdmin(AccountAdminMixin, auth.UserAdmin, ExtendedModelAdmin):
'fields': ('first_name', 'last_name', 'email') 'fields': ('first_name', 'last_name', 'email')
}), }),
(_("Permissions"), { (_("Permissions"), {
'fields': ('is_active', 'is_staff', 'is_superuser', 'is_admin', 'is_main') 'fields': ('is_active', 'is_staff', 'is_superuser', 'display_is_main')
}), }),
(_("Important dates"), { (_("Important dates"), {
'fields': ('last_login', 'date_joined') 'fields': ('last_login', 'date_joined')
@ -38,7 +38,7 @@ class UserAdmin(AccountAdminMixin, auth.UserAdmin, ExtendedModelAdmin):
}), }),
) )
search_fields = ['username', 'account__user__username'] search_fields = ['username', 'account__user__username']
readonly_fields = ('is_main', 'account_link') readonly_fields = ('display_is_main', 'account_link')
change_readonly_fields = ('username',) change_readonly_fields = ('username',)
filter_horizontal = () filter_horizontal = ()
add_form = UserCreationForm add_form = UserCreationForm
@ -46,10 +46,10 @@ class UserAdmin(AccountAdminMixin, auth.UserAdmin, ExtendedModelAdmin):
roles = [] roles = []
ordering = ('-id',) ordering = ('-id',)
def display_is_main(self, instance):
def is_main(self, user): return instance.is_main
return user.account.user == user display_is_main.short_description = _("is main")
is_main.boolean = True display_is_main.boolean = True
def get_urls(self): def get_urls(self):
""" Returns the additional urls for the change view links """ """ Returns the additional urls for the change view links """

View File

@ -1,11 +1,12 @@
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceBackend from orchestra.apps.orchestration import ServiceController
from orchestra.apps.resources import ServiceMonitor
from . import settings from . import settings
class SystemUserBackend(ServiceBackend): class SystemUserBackend(ServiceController):
verbose_name = _("System User") verbose_name = _("System User")
model = 'users.User' model = 'users.User'
ignore_fields = ['last_login'] ignore_fields = ['last_login']
@ -39,3 +40,22 @@ class SystemUserBackend(ServiceBackend):
} }
context['home'] = settings.USERS_SYSTEMUSER_HOME % context context['home'] = settings.USERS_SYSTEMUSER_HOME % context
return context return context
class SystemUserDisk(ServiceMonitor):
model = 'users.User'
resource = ServiceMonitor.DISK
verbose_name = _('System user disk')
def monitor(self, user):
context = self.get_context(user)
self.append("du -s %(home)s | {\n"
" read value\n"
" echo '%(username)s' $value\n"
"}" % context)
def process(self, output):
# TODO transaction
for line in output.readlines():
username, value = line.strip().slpit()
History.store(object_id=user_id, value=value)

View File

@ -36,6 +36,10 @@ class User(auth.AbstractBaseUser):
USERNAME_FIELD = 'username' USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['email'] REQUIRED_FIELDS = ['email']
@property
def is_main(self):
return self.account.user == self
def get_full_name(self): def get_full_name(self):
full_name = '%s %s' % (self.first_name, self.last_name) full_name = '%s %s' % (self.first_name, self.last_name)
return full_name.strip() or self.username return full_name.strip() or self.username

View File

@ -3,12 +3,13 @@ import os
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 _
from orchestra.apps.orchestration import ServiceBackend from orchestra.apps.orchestration import ServiceController
from orchestra.apps.resources import ServiceMonitor
from . import settings from . import settings
class MailSystemUserBackend(ServiceBackend): class MailSystemUserBackend(ServiceController):
verbose_name = _("Mail system user") verbose_name = _("Mail system user")
model = 'mail.Mailbox' model = 'mail.Mailbox'
@ -62,7 +63,7 @@ class MailSystemUserBackend(ServiceBackend):
return context return context
class PostfixAddressBackend(ServiceBackend): class PostfixAddressBackend(ServiceController):
verbose_name = _("Postfix address") verbose_name = _("Postfix address")
model = 'mail.Address' model = 'mail.Address'
@ -132,12 +133,12 @@ class PostfixAddressBackend(ServiceBackend):
return context return context
class AutoresponseBackend(ServiceBackend): class AutoresponseBackend(ServiceController):
verbose_name = _("Mail autoresponse") verbose_name = _("Mail autoresponse")
model = 'mail.Autoresponse' model = 'mail.Autoresponse'
def save(self, autoresponse):
pass
def delete(self, autoresponse): class MailDisk(ServiceMonitor):
pass model = 'email.Mailbox'
resource = ServiceMonitor.DISK
verbose_name = _("Mail disk")

View File

@ -0,0 +1,20 @@
from django.utils.translation import ugettext_lazy as _
from orchestra.apps.resources import ServiceMonitor
class OpenVZDisk(ServiceMonitor):
model = 'vps.VPS'
resource = ServiceMonitor.DISK
class OpenVZMemory(ServiceMonitor):
model = 'vps.VPS'
resource = ServiceMonitor.MEMORY
class OpenVZTraffic(ServiceMonitor):
model = 'vps.VPS'
resource = ServiceMonitor.TRAFFIC

View File

@ -1,12 +1,9 @@
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceBackend from orchestra.apps.orchestration import ServiceController
from . import WebAppServiceMixin from . import WebAppServiceMixin
class AwstatsBackend(WebAppServiceMixin, ServiceBackend): class AwstatsBackend(WebAppServiceMixin, ServiceController):
verbose_name = _("Awstats") verbose_name = _("Awstats")
def save(self, webapp):
pass

View File

@ -1,11 +1,12 @@
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceBackend from orchestra.apps.orchestration import ServiceController
from . import WebAppServiceMixin from . import WebAppServiceMixin
from .. import settings from .. import settings
class DokuWikiMuBackend(WebAppServiceMixin, ServiceBackend):
class DokuWikiMuBackend(WebAppServiceMixin, ServiceController):
verbose_name = _("DokuWiki multisite") verbose_name = _("DokuWiki multisite")
def save(self, webapp): def save(self, webapp):

View File

@ -2,13 +2,13 @@ import os
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceBackend from orchestra.apps.orchestration import ServiceController
from . import WebAppServiceMixin from . import WebAppServiceMixin
from .. import settings from .. import settings
class DrupalMuBackend(WebAppServiceMixin, ServiceBackend): class DrupalMuBackend(WebAppServiceMixin, ServiceController):
verbose_name = _("Drupal multisite") verbose_name = _("Drupal multisite")
def save(self, webapp): def save(self, webapp):

View File

@ -2,13 +2,13 @@ import os
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceBackend from orchestra.apps.orchestration import ServiceController
from . import WebAppServiceMixin from . import WebAppServiceMixin
from .. import settings from .. import settings
class PHPFcgidBackend(WebAppServiceMixin, ServiceBackend): class PHPFcgidBackend(WebAppServiceMixin, ServiceController):
verbose_name = _("PHP-Fcgid") verbose_name = _("PHP-Fcgid")
def save(self, webapp): def save(self, webapp):

View File

@ -3,13 +3,13 @@ import os
from django.template import Template, Context from django.template import Template, Context
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceBackend from orchestra.apps.orchestration import ServiceController
from . import WebAppServiceMixin from . import WebAppServiceMixin
from .. import settings from .. import settings
class PHPFPMBackend(WebAppServiceMixin, ServiceBackend): class PHPFPMBackend(WebAppServiceMixin, ServiceController):
verbose_name = _("PHP-FPM") verbose_name = _("PHP-FPM")
def save(self, webapp): def save(self, webapp):

View File

@ -1,11 +1,11 @@
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceBackend from orchestra.apps.orchestration import ServiceController
from . import WebAppServiceMixin from . import WebAppServiceMixin
class StaticBackend(WebAppServiceMixin, ServiceBackend): class StaticBackend(WebAppServiceMixin, ServiceController):
verbose_name = _("Static") verbose_name = _("Static")
def save(self, webapp): def save(self, webapp):

View File

@ -4,13 +4,13 @@ import sys
import requests import requests
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceBackend from orchestra.apps.orchestration import ServiceController
from . import WebAppServiceMixin from . import WebAppServiceMixin
from .. import settings from .. import settings
class WordpressMuBackend(WebAppServiceMixin, ServiceBackend): class WordpressMuBackend(WebAppServiceMixin, ServiceController):
verbose_name = _("Wordpress multisite") verbose_name = _("Wordpress multisite")
@property @property

View File

@ -3,12 +3,13 @@ import os
from django.template import Template, Context from django.template import Template, Context
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceBackend from orchestra.apps.orchestration import ServiceController
from orchestra.apps.resources import ServiceMonitor
from .. import settings from .. import settings
class Apache2Backend(ServiceBackend): class Apache2Backend(ServiceController):
model = 'websites.Website' model = 'websites.Website'
related_models = (('websites.Content', 'website'),) related_models = (('websites.Content', 'website'),)
verbose_name = _("Apache 2") verbose_name = _("Apache 2")
@ -173,3 +174,58 @@ class Apache2Backend(ServiceBackend):
'fpm_port': content.webapp.get_fpm_port(), 'fpm_port': content.webapp.get_fpm_port(),
}) })
return context return context
class Apache2Traffic(ServiceMonitor):
model = 'websites.Website'
resource = ServiceMonitor.TRAFFIC
verbose_name = _("Apache 2 Traffic")
def monitor(self, site):
context = self.get_context(site)
self.append("""
awk 'BEGIN {
ini = "%(start_date)s";
end = "%(end_date)s";
months["Jan"]="01";
months["Feb"]="02";
months["Mar"]="03";
months["Apr"]="04";
months["May"]="05";
months["Jun"]="06";
months["Jul"]="07";
months["Aug"]="08";
months["Sep"]="09";
months["Oct"]="10";
months["Nov"]="11";
months["Dec"]="12";
} {
date = substr($4,2)
year = substr(date,8,4)
month = months[substr(date,4,3)];
day = substr(date,1,2)
hour = substr(date,13,2)
minute = substr(date,16,2)
second = substr(date,19,2);
line_date = year month day hour minute second
if ( line_date > ini && line_date < end)
if ( $10 == "" )
sum+=$9
else
sum+=$10;
} END {
print sum;
}' %(log_file)s | {
read value
echo %(site_name)s $value
}
""" % context)
def get_context(self, site):
return {
'log_file': os.path.join(settings.WEBSITES_BASE_APACHE_LOGS, site.unique_name),
'start_date': '',
'end_date': '',
'site_name': '',
}

View File

@ -3,12 +3,12 @@ from functools import partial
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceBackend from orchestra.apps.orchestration import ServiceController
from .. import settings from .. import settings
class WebalizerBackend(ServiceBackend): class WebalizerBackend(ServiceController):
verbose_name = _("Webalizer") verbose_name = _("Webalizer")
model = 'websites.Content' model = 'websites.Content'

View File

@ -1,291 +0,0 @@
from . import settings
class ServiceBackend(object):
"""
Service management backend base class
It uses the _unit of work_ design principle, which allows bulk operations to
be conviniently supported. Each backend generates the configuration for all
the changes of all modified objects, reloading the daemon just once.
"""
verbose_name = None
model = None
related_models = () # ((model, accessor__attribute),)
script_method = methods.BashSSH
function_method = methods.Python
type = 'task' # 'sync'
ignore_fields = []
# TODO type: 'script', execution:'task'
__metaclass__ = plugins.PluginMount
def __unicode__(self):
return type(self).__name__
def __str__(self):
return unicode(self)
def __init__(self):
self.cmds = []
@classmethod
def get_name(cls):
return cls.__name__
@classmethod
def is_main(cls, obj):
opts = obj._meta
return cls.model == '%s.%s' % (opts.app_label, opts.object_name)
@classmethod
def get_related(cls, obj):
opts = obj._meta
model = '%s.%s' % (opts.app_label, opts.object_name)
for rel_model, field in cls.related_models:
if rel_model == model:
related = obj
for attribute in field.split('__'):
related = getattr(related, attribute)
return related
return None
@classmethod
def get_backends(cls):
return cls.plugins
@classmethod
def get_choices(cls):
backends = cls.get_backends()
choices = ( (b.get_name(), b.verbose_name or b.get_name()) for b in backends )
return sorted(choices, key=lambda e: e[1])
def get_banner(self):
time = datetime.now().strftime("%h %d, %Y %I:%M:%S")
return "Generated by Orchestra %s" % time
def append(self, *cmd):
# aggregate commands acording to its execution method
if isinstance(cmd[0], basestring):
method = self.script_method
cmd = cmd[0]
else:
method = self.function_method
cmd = partial(*cmd)
if not self.cmds or self.cmds[-1][0] != method:
self.cmds.append((method, [cmd]))
else:
self.cmds[-1][1].append(cmd)
def execute(self, server):
from .models import BackendLog
state = BackendLog.STARTED if self.cmds else BackendLog.SUCCESS
log = BackendLog.objects.create(backend=self.get_name(), state=state, server=server)
for method, cmds in self.cmds:
method(log, server, cmds)
if log.state != BackendLog.SUCCESS:
break
return log
def ServiceController(ServiceBackend):
def save(self, obj)
raise NotImplementedError
def delete(self, obj):
raise NotImplementedError
def commit(self):
"""
apply the configuration, usually reloading a service
reloading a service is done in a separated method in order to reload
the service once in bulk operations
"""
pass
class ServiceMonitor(ServiceBackend):
TRAFFIC = 'traffic'
DISK = 'disk'
MEMORY = 'memory'
CPU = 'cpu'
def prepare(self):
pass
def store(self, stdout):
""" object_id value """
for line in stdout.readlines():
line = line.strip()
object_id, value = line.split()
# TODO date
MonitorHistory.store(self.model, object_id, value, date)
def monitor(self, obj):
raise NotImplementedError
def trigger(self, obj):
raise NotImplementedError
def execute(self, server):
log = super(MonitorBackend, self).execute(server)
return log
class AccountDisk(MonitorBackend):
model = 'accounts.Account'
resource = MonitorBackend.DISK
verbose_name = 'Disk'
def monitor(self, user):
context = self.get_context(user)
self.append("du -s %(home)s | {\n"
" read value\n"
" echo '%(username)s' $value\n"
"}" % context)
def process(self, output):
# TODO transaction
for line in output.readlines():
username, value = line.strip().slpit()
History.store(object_id=user_id, value=value)
class MailmanTraffic(MonitorBackend):
model = 'lists.List'
resource = MonitorBackend.TRAFFIC
def process(self, output):
for line in output.readlines():
listname, value = line.strip().slpit()
def monitor(self, mailinglist):
self.append("LISTS=$(grep -v 'post to mailman' /var/log/mailman/post"
" | grep size | cut -d'<' -f2 | cut -d'>' -f1 | sort | uniq"
" | while read line; do \n"
" grep \"$line\" post | head -n1 | awk {'print $8\" \"$11'}"
" | sed 's/size=//' | sed 's/,//'\n"
"done)")
self.append('SUBS=""\n'
'while read LIST; do\n'
' NAME=$(echo "$LIST" | awk {\'print $1\'})\n'
' SIZE=$(echo "$LIST" | awk {\'print $2\'})\n'
' if [[ ! $(echo -e "$SUBS" | grep "$NAME") ]]; then\n'
' SUBS="${SUBS}${NAME} $(list_members "$NAME" | wc -l)\n"\n'
' fi\n'
' SUBSCRIBERS=$(echo -e "$SUBS" | grep "$NAME" | awk {\'print $2\'})\n'
' echo "$NAME $(($SUBSCRIBERS*$SIZE))"\n'
'done <<< "$LISTS"')
class MailDisk(MonitorBackend):
model = 'email.Mailbox'
resource = MonitorBackend.DISK
verbose_name = _("Mail disk")
def process(self, output):
pass
def monitor(self, mail):
pass
class MysqlDisk(MonitorBackend):
model = 'database.Database'
resource = MonitorBackend.DISK
verbose_name = _("MySQL disk")
def process(self, output):
pass
def monitor(self, db):
pass
class OpenVZDisk(MonitorBackend):
model = 'vps.VPS'
resource = MonitorBackend.DISK
class OpenVZMemory(MonitorBackend):
model = 'vps.VPS'
resource = MonitorBackend.MEMORY
class OpenVZTraffic(MonitorBackend):
model = 'vps.VPS'
resource = MonitorBackend.TRAFFIC
class Apache2Traffic(MonitorBackend):
model = 'websites.Website'
resource = MonitorBackend.TRAFFIC
verbose_name = _("Apache2 Traffic")
def monitor(self, site):
context = self.get_context(site)
self.append("""
awk 'BEGIN {
ini = "%(start_date)s";
end = "%(end_date)s";
months["Jan"]="01";
months["Feb"]="02";
months["Mar"]="03";
months["Apr"]="04";
months["May"]="05";
months["Jun"]="06";
months["Jul"]="07";
months["Aug"]="08";
months["Sep"]="09";
months["Oct"]="10";
months["Nov"]="11";
months["Dec"]="12";
} {
date = substr($4,2)
year = substr(date,8,4)
month = months[substr(date,4,3)];
day = substr(date,1,2)
hour = substr(date,13,2)
minute = substr(date,16,2)
second = substr(date,19,2);
line_date = year month day hour minute second
if ( line_date > ini && line_date < end)
if ( $10 == "" )
sum+=$9
else
sum+=$10;
} END {
print sum;
}' %(log_file)s | {
read value
echo %(site_name)s $value
}
""" % context)
def trigger(self, site):
pass
def get_context(self, site):
return {
'log_file': os.path.join(settings.WEBSITES_BASE_APACHE_LOGS, site.unique_name)
}
# start_date and end_date expected format: YYYYMMDDhhmmss
function get_traffic(){
RESULT=$(get_traffic)
if [[ $RESULT ]]; then
echo $RESULT
else
echo 0
fi
return 0

View File

@ -4,6 +4,11 @@ from django.utils.encoding import force_text
class ShowTextWidget(forms.Widget): class ShowTextWidget(forms.Widget):
def __init__(self, *args, **kwargs):
for kwarg in ['bold', 'warning', 'hidden']:
setattr(self, kwarg, kwargs.pop(kwarg, False))
super(ShowTextWidget, self).__init__(*args, **kwargs)
def render(self, name, value, attrs): def render(self, name, value, attrs):
value = force_text(value) value = force_text(value)
if value is None: if value is None:
@ -20,11 +25,6 @@ class ShowTextWidget(forms.Widget):
final_value = u'%s<input type="hidden" name="%s" value="%s"/>' % (final_value, name, value) final_value = u'%s<input type="hidden" name="%s" value="%s"/>' % (final_value, name, value)
return mark_safe(final_value) return mark_safe(final_value)
def __init__(self, *args, **kwargs):
for kwarg in ['bold', 'warning', 'hidden']:
setattr(self, kwarg, kwargs.pop(kwarg, False))
super(ShowTextWidget, self).__init__(*args, **kwargs)
def _has_changed(self, initial, data): def _has_changed(self, initial, data):
return False return False

View File

@ -54,4 +54,4 @@ class MultiSelectField(models.CharField):
if isinstalled('south'): if isinstalled('south'):
from south.modelsinspector import add_introspection_rules from south.modelsinspector import add_introspection_rules
add_introspection_rules([], ["^controller\.models\.fields\.MultiSelectField"]) add_introspection_rules([], ["^orchestra\.models\.fields\.MultiSelectField"])

View File

@ -5,18 +5,18 @@ body {
#header #branding h1 { #header #branding h1 {
margin: 0; margin: 0;
padding: 5px 10px; padding: 2px 10px;
background: transparent url(/static/orchestra/images/orchestra-logo.png) 10px 5px no-repeat; background: transparent url(/static/orchestra/images/orchestra-logo.png) 10px 2px no-repeat;
text-indent: 0; text-indent: 0;
height: 31px; height: 31px;
font-size: 18px; font-size: 16px;
font-weight: bold; /* font-weight: bold;*/
padding-left: 50px; padding-left: 50px;
line-height: 30px; line-height: 30px;
} }
#branding h1, #branding h1 a:link, #branding h1 a:visited { #branding h1, #branding h1 a:link, #branding h1 a:visited {
color: #707070; color: #555;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -16,8 +16,8 @@
inkscape:version="0.48.3.1 r9886" inkscape:version="0.48.3.1 r9886"
sodipodi:docname="orchestra-logo.svg" sodipodi:docname="orchestra-logo.svg"
inkscape:export-filename="/home/glic3rinu/orchestra/django-orchestra/orchestra/static/orchestra/images/orchestra-logo.png" inkscape:export-filename="/home/glic3rinu/orchestra/django-orchestra/orchestra/static/orchestra/images/orchestra-logo.png"
inkscape:export-xdpi="90" inkscape:export-xdpi="81.290321"
inkscape:export-ydpi="90"> inkscape:export-ydpi="81.290321">
<defs <defs
id="defs4"> id="defs4">
<linearGradient <linearGradient

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -9,7 +9,7 @@ register = template.Library()
@register.simple_tag(name="version") @register.simple_tag(name="version")
def controller_version(): def orchestra_version():
return get_version() return get_version()

View File

@ -1,3 +1,4 @@
import sys
import urlparse import urlparse
from django.core.mail import EmailMultiAlternatives from django.core.mail import EmailMultiAlternatives
@ -37,3 +38,6 @@ def send_email_template(template, context, to, email_from=None, html=None):
msg.attach_alternative(html_message, "text/html") msg.attach_alternative(html_message, "text/html")
msg.send() msg.send()
def running_syncdb():
return 'migrate' in sys.argv or 'syncdb' in sys.argv