diff --git a/TODO.md b/TODO.md
index 5fc5201a..e1478bfa 100644
--- a/TODO.md
+++ b/TODO.md
@@ -43,3 +43,7 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
* use HTTP OPTIONS instead of configuration endpoint, or rename to settings?
* Log changes from rest api (serialized objects)
+
+
+* passlib; nano /usr/local/lib/python2.7/dist-packages/passlib/ext/django/utils.py SortedDict -> collections.OrderedDict
+* pip install pyinotify
diff --git a/orchestra/admin/filters.py b/orchestra/admin/filters.py
new file mode 100644
index 00000000..e3c4bd34
--- /dev/null
+++ b/orchestra/admin/filters.py
@@ -0,0 +1,18 @@
+from django.contrib.admin import SimpleListFilter
+from django.utils.translation import ugettext as _
+
+
+class UsedContentTypeFilter(SimpleListFilter):
+ title = _('Content type')
+ parameter_name = 'content_type'
+
+ def lookups(self, request, model_admin):
+ qset = model_admin.model._default_manager.all().order_by()
+ result = ()
+ for pk, name in qset.values_list('content_type', 'content_type__name').distinct():
+ result += ((str(pk), name.capitalize()),)
+ return result
+
+ def queryset(self, request, queryset):
+ if self.value():
+ return queryset.filter(content_type=self.value())
diff --git a/orchestra/admin/menu.py b/orchestra/admin/menu.py
index b280796a..4d95731b 100644
--- a/orchestra/admin/menu.py
+++ b/orchestra/admin/menu.py
@@ -1,5 +1,6 @@
from admin_tools.menu import items, Menu
from django.core.urlresolvers import reverse
+from django.utils.text import capfirst
from django.utils.translation import ugettext_lazy as _
from orchestra.core import services
@@ -17,11 +18,11 @@ def api_link(context):
if 'object_id' in context:
object_id = context['object_id']
try:
- return reverse('%s-detail' % opts.module_name, args=[object_id])
+ return reverse('%s-detail' % opts.model_name, args=[object_id])
except:
return reverse('api-root')
try:
- return reverse('%s-list' % opts.module_name)
+ return reverse('%s-list' % opts.model_name)
except:
return reverse('api-root')
@@ -32,7 +33,8 @@ def get_services():
if options.get('menu', True):
opts = model._meta
url = reverse('admin:%s_%s_changelist' % (opts.app_label, opts.model_name))
- result.append(items.MenuItem(options.get('verbose_name_plural'), url))
+ name = capfirst(options.get('verbose_name_plural'))
+ result.append(items.MenuItem(name, url))
return sorted(result, key=lambda i: i.title)
@@ -72,6 +74,8 @@ def get_administration_models():
administration_models.append('djcelery.*')
if isinstalled('orchestra.apps.issues'):
administration_models.append('orchestra.apps.issues.*')
+ if isinstalled('orchestra.apps.resources'):
+ administration_models.append('orchestra.apps.resources.*')
return administration_models
diff --git a/orchestra/admin/utils.py b/orchestra/admin/utils.py
index 8a888973..aee8e478 100644
--- a/orchestra/admin/utils.py
+++ b/orchestra/admin/utils.py
@@ -17,7 +17,7 @@ def get_modeladmin(model, import_module=True):
""" returns the modeladmin registred for model """
for k,v in admin.site._registry.iteritems():
if k is model:
- return v
+ return type(v)
if import_module:
# Sometimes the admin module is not yet imported
app_label = model._meta.app_label
@@ -30,8 +30,9 @@ def get_modeladmin(model, import_module=True):
def insertattr(model, name, value, weight=0):
""" Inserts attribute to a modeladmin """
- is_model = models.Model in model.__mro__
- modeladmin = get_modeladmin(model) if is_model else model
+ modeladmin = model
+ if models.Model in model.__mro__:
+ modeladmin = get_modeladmin(model)
# Avoid inlines defined on parent class be shared between subclasses
# Seems that if we use tuples they are lost in some conditions like changing
# the tuple in modeladmin.__init__
diff --git a/orchestra/apps/accounts/admin.py b/orchestra/apps/accounts/admin.py
index 6076dbb7..cce56fc6 100644
--- a/orchestra/apps/accounts/admin.py
+++ b/orchestra/apps/accounts/admin.py
@@ -77,11 +77,11 @@ class AccountAdmin(ExtendedModelAdmin):
obj.user.account = obj
obj.user.save()
- def queryset(self, request):
+ def get_queryset(self, request):
""" Select related for performance """
# TODO move invoicecontact to contacts
related = ('user', 'invoicecontact')
- return super(AccountAdmin, self).queryset(request).select_related(*related)
+ return super(AccountAdmin, self).get_queryset(request).select_related(*related)
admin.site.register(Account, AccountAdmin)
@@ -131,9 +131,9 @@ class AccountAdminMixin(object):
account_link.allow_tags = True
account_link.admin_order_field = 'account__user__username'
- def queryset(self, request):
+ def get_queryset(self, request):
""" Select related for performance """
- qs = super(AccountAdminMixin, self).queryset(request)
+ qs = super(AccountAdminMixin, self).get_queryset(request)
return qs.select_related('account__user')
def formfield_for_dbfield(self, db_field, **kwargs):
@@ -177,7 +177,7 @@ class SelectAccountAdminMixin(AccountAdminMixin):
urls = super(AccountAdminMixin, self).get_urls()
admin_site = self.admin_site
opts = self.model._meta
- info = opts.app_label, opts.module_name
+ info = opts.app_label, opts.model_name
account_list = AccountListAdmin(Account, admin_site).changelist_view
select_urls = patterns("",
url("/select-account/$",
diff --git a/orchestra/apps/accounts/models.py b/orchestra/apps/accounts/models.py
index 9fa42335..a62a6b6c 100644
--- a/orchestra/apps/accounts/models.py
+++ b/orchestra/apps/accounts/models.py
@@ -1,12 +1,12 @@
+from django.conf import settings as django_settings
from django.db import models
-from django.contrib.auth import get_user_model
from django.utils.translation import ugettext_lazy as _
from . import settings
class Account(models.Model):
- user = models.OneToOneField(get_user_model(), related_name='accounts')
+ user = models.OneToOneField(django_settings.AUTH_USER_MODEL, related_name='accounts')
type = models.CharField(_("type"), max_length=32, choices=settings.ACCOUNTS_TYPES,
default=settings.ACCOUNTS_DEFAULT_TYPE)
language = models.CharField(_("language"), max_length=2,
diff --git a/orchestra/apps/bills/models.py b/orchestra/apps/bills/models.py
new file mode 100644
index 00000000..453565bd
--- /dev/null
+++ b/orchestra/apps/bills/models.py
@@ -0,0 +1,13 @@
+from django.db import models
+
+
+class Bill(models.Model):
+ pass
+
+
+class Invoice(models.Model):
+ pass
+
+
+class Fee(models.Model):
+ pass
diff --git a/orchestra/apps/databases/admin.py.save b/orchestra/apps/databases/admin.py.save
new file mode 100644
index 00000000..2c83365a
--- /dev/null
+++ b/orchestra/apps/databases/admin.py.save
@@ -0,0 +1,115 @@
+./apps/vps/admin.pyfrom django.db import models from django.conf.urls import patterns from django.contrib import admin from django.contrib.auth.admin import
+UserAdmin from django.core.urlresolvers import reverse from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _
+./apps/issues/admin.py from orchestra.admin import ExtendedModelAdmin from orchestra.admin.utils import link from orchestra.apps.accounts.admin import
+AccountAdminMixin, SelectAccountAdminMixin ./apps/contacts/admin.py from .forms import (DatabaseUserChangeForm, DatabaseUserCreationForm, ./apps/webapps/admin.py
+DatabaseCreationForm) from .models import Database, Role, DatabaseUser ./apps/prices/admin.py ./apps/websites/admin.py class UserInline(admin.TabularInline):
+./apps/users/roles/posix/admin.py model = Role ./apps/users/roles/admin.py verbose_name_plural = _("Users") ./apps/users/roles/jabber/admin.py readonly_fields =
+('user_link',) ./apps/users/roles/mail/admin.py extra = 0 ./apps/users/admin.py ./apps/orchestration/admin.py user_link = link('user') ./apps/orders/admin.py
+./apps/domains/admin.py def formfield_for_dbfield(self, db_field, **kwargs): ./apps/accounts/admin.py """ Make value input widget bigger """ ./apps/lists/admin.py if
+db_field.name == 'user': ./apps/resources/admin.py users = db_field.rel.to.objects.filter(type=self.parent_object.type)
+ kwargs['queryset'] = users.filter(account=self.account)
+ return super(UserInline, self).formfield_for_dbfield(db_field, **kwargs)
+
+
+class PermissionInline(AccountAdminMixin, admin.TabularInline):
+ model = Role
+ verbose_name_plural = _("Permissions")
+ readonly_fields = ('database_link',)
+ extra = 0
+ filter_by_account_fields = ['database']
+
+ database_link = link('database', popup=True)
+
+ def formfield_for_dbfield(self, db_field, **kwargs):
+ """ Make value input widget bigger """
+ formfield = super(PermissionInline, self).formfield_for_dbfield(db_field, **kwargs)
+ if db_field.name == 'database':
+ # Hack widget render in order to append ?account=id to the add url
+ db_type = self.parent_object.type
+ old_render = formfield.widget.render
+ def render(*args, **kwargs):
+ output = old_render(*args, **kwargs)
+ output = output.replace('/add/?', '/add/?type=%s&' % db_type)
+ return mark_safe(output)
+ formfield.widget.render = render
+ formfield.queryset = formfield.queryset.filter(type=db_type)
+ return formfield
+
+
+class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
+ list_display = ('name', 'type', 'account_link')
+ list_filter = ('type',)
+ search_fields = ['name', 'account__user__username']
+ inlines = [UserInline]
+ add_inlines = []
+ change_readonly_fields = ('name', 'type')
+ extra = 1
+ fieldsets = (
+ (None, {
+ 'classes': ('extrapretty',),
+ 'fields': ('account_link', 'name', 'type'),
+ }),
+ )
+ add_fieldsets = (
+ (None, {
+ 'classes': ('wide',),
+ 'fields': ('account_link', 'name', 'type')
+ }),
+ (_("Create new user"), {
+ 'classes': ('wide',),
+ 'fields': ('username', 'password1', 'password2'),
+ }),
+ (_("Use existing user"), {
+ 'classes': ('wide',),
+ 'fields': ('user',)
+ }),
+ )
+ add_form = DatabaseCreationForm
+
+ def save_model(self, request, obj, form, change):
+ super(DatabaseAdmin, self).save_model(request, obj, form, change)
+ if not change:
+ user = form.cleaned_data['user']
+ if not user:
+ user = DatabaseUser.objects.create(
+ username=form.cleaned_data['username'],
+ type=obj.type,
+ account_id = obj.account.pk,
+ )
+ user.set_password(form.cleaned_data["password1"])
+ user.save()
+ Role.objects.create(database=obj, user=user, is_owner=True)
+
+
+class DatabaseUserAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
+ list_display = ('username', 'type', 'account_link')
+ list_filter = ('type',)
+ search_fields = ['username', 'account__user__username']
+ form = DatabaseUserChangeForm
+ add_form = DatabaseUserCreationForm
+ change_readonly_fields = ('username', 'type')
+ inlines = [PermissionInline]
+ add_inlines = []
+ fieldsets = (
+ (None, {
+ 'classes': ('extrapretty',),
+ 'fields': ('account_link', 'username', 'password', 'type')
+ }),
+ )
+ add_fieldsets = (
+ (None, {
+ 'classes': ('extrapretty',),
+ 'fields': ('account_link', 'username', 'password1', 'password2', 'type')
+ }),
+ )
+
+ def get_urls(self):
+ useradmin = UserAdmin(DatabaseUser, self.admin_site)
+ return patterns('',
+ (r'^(\d+)/password/$',
+ self.admin_site.admin_view(useradmin.user_change_password))
+ ) + super(DatabaseUserAdmin, self).get_urls()
+
+
+admin.site.register(Database, DatabaseAdmin)
+admin.site.register(DatabaseUser, DatabaseUserAdmin)
diff --git a/orchestra/apps/databases/forms.py b/orchestra/apps/databases/forms.py
index 1688da05..5e5f13c1 100644
--- a/orchestra/apps/databases/forms.py
+++ b/orchestra/apps/databases/forms.py
@@ -130,6 +130,7 @@ class DatabaseUserChangeForm(forms.ModelForm):
class Meta:
model = DatabaseUser
+ fields = ('username', 'password', 'type', 'account')
def clean_password(self):
return self.initial["password"]
diff --git a/orchestra/apps/domains/admin.py b/orchestra/apps/domains/admin.py
index 0bbc4e20..9a460eb3 100644
--- a/orchestra/apps/domains/admin.py
+++ b/orchestra/apps/domains/admin.py
@@ -107,9 +107,9 @@ class DomainAdmin(ChangeListDefaultFilter, AccountAdminMixin, ExtendedModelAdmin
return TemplateResponse(request, 'admin/domains/domain/view_zone.html',
context)
- def queryset(self, request):
+ def get_queryset(self, request):
""" Order by structured name and imporve performance """
- qs = super(DomainAdmin, self).queryset(request)
+ qs = super(DomainAdmin, self).get_queryset(request)
qs = qs.select_related('top', 'account__user')
# qs = qs.select_related('top')
# For some reason if we do this we know for sure that join table will be called T4
diff --git a/orchestra/apps/issues/admin.py b/orchestra/apps/issues/admin.py
index 7b867e85..db5c496c 100644
--- a/orchestra/apps/issues/admin.py
+++ b/orchestra/apps/issues/admin.py
@@ -88,9 +88,9 @@ class MessageInline(admin.TabularInline):
self.form.user = request.user
return super(MessageInline, self).get_formset(request, obj, **kwargs)
- def queryset(self, request):
+ def get_queryset(self, request):
""" Don't show any message """
- qs = super(MessageInline, self).queryset(request)
+ qs = super(MessageInline, self).get_queryset(request)
return qs.none()
@@ -308,9 +308,9 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView
data_formated = markdowt_tn(strip_tags(data))
return HttpResponse(data_formated)
- def queryset(self, request):
+ def get_queryset(self, request):
""" Order by structured name and imporve performance """
- qs = super(TicketAdmin, self).queryset(request)
+ qs = super(TicketAdmin, self).get_queryset(request)
return qs.select_related('queue', 'owner', 'creator')
@@ -345,8 +345,8 @@ class QueueAdmin(admin.ModelAdmin):
list_display.append(display_notify)
return list_display
- def queryset(self, request):
- qs = super(QueueAdmin, self).queryset(request)
+ def get_queryset(self, request):
+ qs = super(QueueAdmin, self).get_queryset(request)
qs = qs.annotate(models.Count('tickets'))
return qs
diff --git a/orchestra/apps/issues/forms.py b/orchestra/apps/issues/forms.py
index a051e7d7..d8770238 100644
--- a/orchestra/apps/issues/forms.py
+++ b/orchestra/apps/issues/forms.py
@@ -36,6 +36,9 @@ class MessageInlineForm(forms.ModelForm):
created_on = forms.CharField(label="Created On", required=False)
content = forms.CharField(widget=MarkDownWidget(), required=False)
+ class Meta:
+ fields = ('author', 'author_name', 'created_on', 'content')
+
def __init__(self, *args, **kwargs):
super(MessageInlineForm, self).__init__(*args, **kwargs)
admin_link = reverse('admin:users_user_change', args=(self.user.pk,))
@@ -56,7 +59,7 @@ class UsersIterator(forms.models.ModelChoiceIterator):
def __init__(self, *args, **kwargs):
self.ticket = kwargs.pop('ticket', False)
super(forms.models.ModelChoiceIterator, self).__init__(*args, **kwargs)
-
+
def __iter__(self):
yield ('', '---------')
users = User.objects.exclude(is_active=False).order_by('name')
@@ -74,6 +77,10 @@ class TicketForm(forms.ModelForm):
class Meta:
model = Ticket
+ fields = (
+ 'creator', 'creator_name', 'owner', 'queue', 'subject', 'description',
+ 'priority', 'state', 'cc', 'display_description'
+ )
def __init__(self, *args, **kwargs):
super(TicketForm, self).__init__(*args, **kwargs)
diff --git a/orchestra/apps/issues/models.py b/orchestra/apps/issues/models.py
index 48ccfd4a..7922b2de 100644
--- a/orchestra/apps/issues/models.py
+++ b/orchestra/apps/issues/models.py
@@ -1,4 +1,4 @@
-from django.contrib.auth import get_user_model
+from django.conf import settings as django_settings
from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
@@ -56,10 +56,10 @@ class Ticket(models.Model):
(CLOSED, 'Closed'),
)
- creator = models.ForeignKey(get_user_model(), verbose_name=_("created by"),
+ creator = models.ForeignKey(django_settings.AUTH_USER_MODEL, verbose_name=_("created by"),
related_name='tickets_created', null=True)
creator_name = models.CharField(_("creator name"), max_length=256, blank=True)
- owner = models.ForeignKey(get_user_model(), null=True, blank=True,
+ owner = models.ForeignKey(django_settings.AUTH_USER_MODEL, null=True, blank=True,
related_name='tickets_owned', verbose_name=_("assigned to"))
queue = models.ForeignKey(Queue, related_name='tickets', null=True, blank=True)
subject = models.CharField(_("subject"), max_length=256)
@@ -153,7 +153,7 @@ class Ticket(models.Model):
class Message(models.Model):
ticket = models.ForeignKey('issues.Ticket', verbose_name=_("ticket"),
related_name='messages')
- author = models.ForeignKey(get_user_model(), verbose_name=_("author"),
+ author = models.ForeignKey(django_settings.AUTH_USER_MODEL, verbose_name=_("author"),
related_name='ticket_messages')
author_name = models.CharField(_("author name"), max_length=256, blank=True)
content = models.TextField(_("content"))
@@ -183,7 +183,7 @@ class TicketTracker(models.Model):
""" Keeps track of user read tickets """
ticket = models.ForeignKey(Ticket, verbose_name=_("ticket"),
related_name='trackers')
- user = models.ForeignKey(get_user_model(), verbose_name=_("user"),
+ user = models.ForeignKey(django_settings.AUTH_USER_MODEL, verbose_name=_("user"),
related_name='ticket_trackers')
class Meta:
diff --git a/orchestra/apps/orchestration/admin.py b/orchestra/apps/orchestration/admin.py
index eb3576db..ffe921fe 100644
--- a/orchestra/apps/orchestration/admin.py
+++ b/orchestra/apps/orchestration/admin.py
@@ -109,9 +109,9 @@ class BackendLogAdmin(admin.ModelAdmin):
return monospace_format(escape(log.traceback))
mono_traceback.short_description = _("traceback")
- def queryset(self, request):
+ def get_queryset(self, request):
""" Order by structured name and imporve performance """
- qs = super(BackendLogAdmin, self).queryset(request)
+ qs = super(BackendLogAdmin, self).get_queryset(request)
return qs.select_related('server')
diff --git a/orchestra/apps/orchestration/backends.py b/orchestra/apps/orchestration/backends.py
index 2f79cabf..a99800d5 100644
--- a/orchestra/apps/orchestration/backends.py
+++ b/orchestra/apps/orchestration/backends.py
@@ -65,8 +65,16 @@ class ServiceBackend(object):
@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])
+ choices = []
+ for b in backends:
+ # don't evaluate b.verbose_name ugettext_lazy
+ verbose = getattr(b.verbose_name, '_proxy____args', [None])
+ if verbose[0]:
+ verbose = b.verbose_name
+ else:
+ verbose = b.get_name()
+ choices.append((b.get_name(), verbose))
+ return sorted(choices, key=lambda e: e[0])
def get_banner(self):
time = datetime.now().strftime("%h %d, %Y %I:%M:%S")
diff --git a/orchestra/apps/resources/__init__.py b/orchestra/apps/resources/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/orchestra/apps/resources/admin.py b/orchestra/apps/resources/admin.py
new file mode 100644
index 00000000..dde23748
--- /dev/null
+++ b/orchestra/apps/resources/admin.py
@@ -0,0 +1,92 @@
+import sys
+
+from django.contrib import admin
+from django.contrib.contenttypes import generic
+from django.utils.functional import cached_property
+from django.utils.translation import ugettext_lazy as _
+
+from orchestra.admin.filters import UsedContentTypeFilter
+from orchestra.admin.utils import insertattr, get_modeladmin
+
+from .forms import ResourceForm
+from .models import Resource, ResourceAllocation, Monitor, MonitorData
+
+
+class ResourceAdmin(admin.ModelAdmin):
+ list_display = (
+ 'name', 'verbose_name', 'content_type', 'period', 'ondemand',
+ 'default_allocation', 'disable_trigger'
+ )
+ list_filter = (UsedContentTypeFilter, 'period', 'ondemand', 'disable_trigger')
+
+ def save_model(self, request, obj, form, change):
+ super(ResourceAdmin, self).save_model(request, obj, form, change)
+ model = obj.content_type.model_class()
+ modeladmin = get_modeladmin(model)
+ resources = obj.content_type.resource_set.filter(is_active=True)
+ inlines = []
+ for inline in modeladmin.inlines:
+ if inline.model is ResourceAllocation:
+ inline = resource_inline_factory(resources)
+ inlines.append(inline)
+ modeladmin.inlines = inlines
+
+
+class ResourceAllocationAdmin(admin.ModelAdmin):
+ list_display = ('id', 'resource', 'content_object', 'value')
+ list_filter = ('resource',)
+
+
+class MonitorAdmin(admin.ModelAdmin):
+ list_display = ('backend', 'resource', 'crontab')
+ list_filter = ('backend', 'resource')
+
+
+class MonitorDataAdmin(admin.ModelAdmin):
+ list_display = ('id', 'monitor', 'content_object', 'date', 'value')
+ list_filter = ('monitor',)
+
+
+admin.site.register(Resource, ResourceAdmin)
+admin.site.register(ResourceAllocation, ResourceAllocationAdmin)
+admin.site.register(Monitor, MonitorAdmin)
+admin.site.register(MonitorData, MonitorDataAdmin)
+
+
+# Mokey-patching
+
+def resource_inline_factory(resources):
+ class ResourceInlineFormSet(generic.BaseGenericInlineFormSet):
+ def total_form_count(self):
+ return len(resources)
+
+ @cached_property
+ def forms(self):
+ forms = []
+ for i, resource in enumerate(resources):
+ forms.append(self._construct_form(i, resource=resource))
+ return forms
+
+ class ResourceInline(generic.GenericTabularInline):
+ model = ResourceAllocation
+ verbose_name_plural = _("resources")
+ form = ResourceForm
+ formset = ResourceInlineFormSet
+
+ class Media:
+ css = {
+ 'all': ('orchestra/css/hide-inline-id.css',)
+ }
+
+ def has_add_permission(self, *args, **kwargs):
+ """ Hidde add another """
+ return False
+
+ return ResourceInline
+
+if not 'migrate' in sys.argv and not 'syncdb' in sys.argv:
+ # not run during syncdb
+ for resources in Resource.group_by_content_type():
+ inline = resource_inline_factory(resources)
+ model = resources[0].content_type.model_class()
+ insertattr(model, 'inlines', inline)
diff --git a/orchestra/apps/resources/forms.py b/orchestra/apps/resources/forms.py
new file mode 100644
index 00000000..eb75b7b0
--- /dev/null
+++ b/orchestra/apps/resources/forms.py
@@ -0,0 +1,30 @@
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+
+from orchestra.forms.widgets import ShowTextWidget, ReadOnlyWidget
+
+
+class ResourceForm(forms.ModelForm):
+ verbose_name = forms.CharField(label=_("Name"), widget=ShowTextWidget(bold=True),
+ required=False)
+ current = forms.CharField(label=_("Current"), widget=ShowTextWidget(),
+ required=False)
+ value = forms.CharField(label=_("Allocation"))
+
+ class Meta:
+ fields = ('verbose_name', 'current', 'value',)
+
+ def __init__(self, *args, **kwargs):
+ self.resource = kwargs.pop('resource', None)
+ super(ResourceForm, self).__init__(*args, **kwargs)
+ if self.resource:
+ self.fields['verbose_name'].initial = self.resource.verbose_name
+ self.fields['current'].initial = self.resource.get_current()
+ if self.resource.ondemand:
+ self.fields['value'].widget = ReadOnlyWidget('')
+ else:
+ self.fields['value'].initial = self.resource.default_allocation
+
+ def save(self, *args, **kwargs):
+ self.instance.resource_id = self.resource.pk
+ return super(ResourceForm, self).save(*args, **kwargs)
diff --git a/orchestra/apps/resources/models.py b/orchestra/apps/resources/models.py
new file mode 100644
index 00000000..132343bb
--- /dev/null
+++ b/orchestra/apps/resources/models.py
@@ -0,0 +1,112 @@
+import datetime
+
+from django.db import models
+from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.models import ContentType
+from django.core import validators
+from django.utils.translation import ugettext_lazy as _
+from djcelery.models import PeriodicTask, CrontabSchedule
+
+from orchestra.utils.apps import autodiscover
+
+
+class Resource(models.Model):
+ MONTHLY = 'MONTHLY'
+ PERIODS = (
+ (MONTHLY, _('Monthly')),
+ )
+
+ name = models.CharField(_("name"), max_length=32, unique=True,
+ help_text=_('Required. 32 characters or fewer. Lowercase letters, '
+ 'digits and hyphen only.'),
+ validators=[validators.RegexValidator(r'^[a-z0-9_\-]+$',
+ _('Enter a valid name.'), 'invalid')])
+ verbose_name = models.CharField(_("verbose name"), max_length=256, unique=True)
+ content_type = models.ForeignKey(ContentType) # TODO filter by servicE?
+ period = models.CharField(_("period"), max_length=16, choices=PERIODS,
+ default=MONTHLY)
+ ondemand = models.BooleanField(default=False)
+ default_allocation = models.PositiveIntegerField(null=True, blank=True)
+ is_active = models.BooleanField(default=True)
+ disable_trigger = models.BooleanField(default=False)
+
+ def __unicode__(self):
+ return self.name
+
+ @classmethod
+ def group_by_content_type(cls):
+ prev = None
+ group = []
+ for resource in cls.objects.filter(is_active=True).order_by('content_type'):
+ ct = resource.content_type
+ if prev != ct:
+ if group:
+ yield group
+ group = [resource]
+ else:
+ group.append(resource)
+ prev = ct
+ if group:
+ yield group
+
+ def get_current(self):
+ today = datetime.date.today()
+ result = 0
+ has_result = False
+ for monitor in self.monitors.all():
+ has_result = True
+ if self.period == self.MONTHLY:
+ data = monitor.dataset.filter(date__year=today.year,
+ date__month=today.month)
+ result += data.aggregate(models.Sum('value'))['value__sum']
+ else:
+ raise NotImplementedError("%s support not implemented" % self.period)
+ return result if has_result else None
+
+
+class ResourceAllocation(models.Model):
+ resource = models.ForeignKey(Resource)
+ content_type = models.ForeignKey(ContentType)
+ object_id = models.PositiveIntegerField()
+ value = models.PositiveIntegerField()
+
+ content_object = generic.GenericForeignKey()
+
+ class Meta:
+ unique_together = ('resource', 'content_type', 'object_id')
+
+
+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):
+ monitor = models.ForeignKey(Monitor, related_name='dataset')
+ content_type = models.ForeignKey(ContentType)
+ object_id = models.PositiveIntegerField()
+ date = models.DateTimeField(auto_now_add=True)
+ value = models.PositiveIntegerField()
+
+ content_object = generic.GenericForeignKey()
+
+ def __unicode__(self):
+ return str(self.monitor)
+
+
+#for resources in Resource.group_by_content_type():
+# model = resources[0].content_type.model_class()
+# print resources[0].content_type.model_class()
+# model.add_to_class('allocations', generic.GenericRelation('resources.ResourceAllocation'))
+
diff --git a/orchestra/apps/users/admin.py b/orchestra/apps/users/admin.py
index 2ab9ed6e..615d3709 100644
--- a/orchestra/apps/users/admin.py
+++ b/orchestra/apps/users/admin.py
@@ -61,10 +61,10 @@ class UserAdmin(AccountAdminMixin, auth.UserAdmin, ExtendedModelAdmin):
new_urls += patterns("",
url('^(\d+)/%s/$' % role.url_name,
wrap_admin_view(self, role().change_view),
- name='%s_%s_%s_change' % (opts.app_label, opts.module_name, role.name)),
+ name='%s_%s_%s_change' % (opts.app_label, opts.model_name, role.name)),
url('^(\d+)/%s/delete/$' % role.url_name,
wrap_admin_view(self, role().delete_view),
- name='%s_%s_%s_delete' % (opts.app_label, opts.module_name, role.name))
+ name='%s_%s_%s_delete' % (opts.app_label, opts.model_name, role.name))
)
return new_urls + urls
@@ -101,10 +101,10 @@ class UserAdmin(AccountAdminMixin, auth.UserAdmin, ExtendedModelAdmin):
kwargs['extra_context'] = extra_context
return super(UserAdmin, self).change_view(request, object_id, **kwargs)
- def queryset(self, request):
+ def get_queryset(self, request):
""" Select related for performance """
related = ['account__user'] + [ role.name for role in self.roles ]
- return super(UserAdmin, self).queryset(request).select_related(*related)
+ return super(UserAdmin, self).get_queryset(request).select_related(*related)
admin.site.register(User, UserAdmin)
diff --git a/orchestra/apps/users/roles/mail/admin.py b/orchestra/apps/users/roles/mail/admin.py
index e36cde21..0124b242 100644
--- a/orchestra/apps/users/roles/mail/admin.py
+++ b/orchestra/apps/users/roles/mail/admin.py
@@ -111,9 +111,9 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
kwargs['queryset'] = mailboxes.filter(user__account=self.account)
return super(AddressAdmin, self).formfield_for_dbfield(db_field, **kwargs)
- def queryset(self, request):
+ def get_queryset(self, request):
""" Select related for performance """
- qs = super(AddressAdmin, self).queryset(request)
+ qs = super(AddressAdmin, self).get_queryset(request)
# TODO django 1.7 account__user is not needed
return qs.select_related('domain', 'account__user')
diff --git a/orchestra/apps/webapps/models.py b/orchestra/apps/webapps/models.py
index 573f5817..30c5eaa0 100644
--- a/orchestra/apps/webapps/models.py
+++ b/orchestra/apps/webapps/models.py
@@ -13,7 +13,7 @@ from . import settings
def settings_to_choices(choices):
return sorted(
[ (name, opt[0]) for name,opt in choices.iteritems() ],
- key=lambda e: e[1]
+ key=lambda e: e[0]
)
diff --git a/orchestra/apps/websites/admin.py b/orchestra/apps/websites/admin.py
index 1e04f7e7..741f863a 100644
--- a/orchestra/apps/websites/admin.py
+++ b/orchestra/apps/websites/admin.py
@@ -84,9 +84,9 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
kwargs['widget'] = forms.TextInput(attrs={'size':'100'})
return super(WebsiteAdmin, self).formfield_for_dbfield(db_field, **kwargs)
- def queryset(self, request):
+ def get_queryset(self, request):
""" Select related for performance """
- qs = super(WebsiteAdmin, self).queryset(request)
+ qs = super(WebsiteAdmin, self).get_queryset(request)
return qs.prefetch_related('domains')
diff --git a/orchestra/apps/websites/models.py b/orchestra/apps/websites/models.py
index 19ecdf2d..c64fcc6d 100644
--- a/orchestra/apps/websites/models.py
+++ b/orchestra/apps/websites/models.py
@@ -13,7 +13,7 @@ from . import settings
def settings_to_choices(choices):
return sorted(
[ (name, opt[0]) for name,opt in choices.iteritems() ],
- key=lambda e: e[1]
+ key=lambda e: e[0]
)
diff --git a/orchestra/apps/websites/resources.py b/orchestra/apps/websites/resources.py
new file mode 100644
index 00000000..1ae1dede
--- /dev/null
+++ b/orchestra/apps/websites/resources.py
@@ -0,0 +1,291 @@
+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
+
diff --git a/orchestra/bin/orchestra-admin b/orchestra/bin/orchestra-admin
index a547ac83..d5d5fa33 100755
--- a/orchestra/bin/orchestra-admin
+++ b/orchestra/bin/orchestra-admin
@@ -131,16 +131,15 @@ function install_requirements () {
django-celery-email==1.0.3 \
django-fluent-dashboard==0.3.5 \
https://bitbucket.org/izi/django-admin-tools/get/a0abfffd76a0.zip \
- South==0.8.1 \
IPy==0.81 \
django-extensions==1.1.1 \
django-transaction-signals==1.0.0 \
- django-celery==3.1.1 \
+ django-celery==3.1.10 \
celery==3.1.7 \
kombu==3.0.8 \
Markdown==2.4 \
- django-debug-toolbar==1.0.1 \
- djangorestframework==2.3.13 \
+ django-debug-toolbar==1.2.1 \
+ djangorestframework==2.3.14 \
paramiko==1.12.1 \
Pygments==1.6 \
django-filter==0.7 \
diff --git a/orchestra/conf/base_settings.py b/orchestra/conf/base_settings.py
index cb6e3688..3300c612 100644
--- a/orchestra/conf/base_settings.py
+++ b/orchestra/conf/base_settings.py
@@ -80,7 +80,6 @@ INSTALLED_APPS = (
'orchestra.apps.orders',
# Third-party apps
- 'south',
'django_extensions',
'djcelery',
'djcelery_email',
@@ -103,6 +102,7 @@ INSTALLED_APPS = (
'orchestra.apps.accounts',
'orchestra.apps.contacts',
+ 'orchestra.apps.resources',
)
@@ -150,6 +150,8 @@ FLUENT_DASHBOARD_APP_GROUPS = (
'orchestra.apps.orchestration.models.BackendLog',
'orchestra.apps.orchestration.models.Server',
'orchestra.apps.issues.models.Ticket',
+ 'orchestra.apps.resources.models.Resource',
+ 'orchestra.apps.resources.models.Monitor',
),
'collapsible': True,
}),
@@ -180,6 +182,8 @@ FLUENT_DASHBOARD_APP_ICONS = {
'orchestration/route': 'hal.png',
'orchestration/backendlog': 'scriptlog.png',
'issues/ticket': "Ticket_star.png",
+ 'resources/resource': "gauge.png",
+ 'resources/monitor': "Utilities-system-monitor.png",
}
# Django-celery
diff --git a/orchestra/core/__init__.py b/orchestra/core/__init__.py
index 1fdb31d0..b926baee 100644
--- a/orchestra/core/__init__.py
+++ b/orchestra/core/__init__.py
@@ -5,7 +5,6 @@ class Service(object):
if model in self._registry:
raise KeyError("%s already registered" % str(model))
plural = kwargs.get('verbose_name_plural', model._meta.verbose_name_plural)
- plural = plural[0].upper() + plural[1:]
self._registry[model] = {
'verbose_name': kwargs.get('verbose_name', model._meta.verbose_name),
'verbose_name_plural': plural,
diff --git a/orchestra/forms/widgets.py b/orchestra/forms/widgets.py
index 6883527a..3bf083e5 100644
--- a/orchestra/forms/widgets.py
+++ b/orchestra/forms/widgets.py
@@ -1,5 +1,32 @@
from django import forms
from django.utils.safestring import mark_safe
+from django.utils.encoding import force_text
+
+
+class ShowTextWidget(forms.Widget):
+ def render(self, name, value, attrs):
+ value = force_text(value)
+ if value is None:
+ return ''
+ if hasattr(self, 'initial'):
+ value = self.initial
+ if self.bold:
+ final_value = u'%s' % (value)
+ else:
+ final_value = '
'.join(value.split('\n'))
+ if self.warning:
+ final_value = u'