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'' %(final_value) + if self.hidden: + final_value = u'%s' % (final_value, name, 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): + return False class ReadOnlyWidget(forms.Widget): diff --git a/orchestra/static/orchestra/icons/Utilities-system-monitor.png b/orchestra/static/orchestra/icons/Utilities-system-monitor.png new file mode 100644 index 00000000..c755c141 Binary files /dev/null and b/orchestra/static/orchestra/icons/Utilities-system-monitor.png differ diff --git a/orchestra/static/orchestra/icons/Utilities-system-monitor.svg b/orchestra/static/orchestra/icons/Utilities-system-monitor.svg new file mode 100644 index 00000000..e2a2843e --- /dev/null +++ b/orchestra/static/orchestra/icons/Utilities-system-monitor.svg @@ -0,0 +1,368 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + System Monitor + 2005-10-10 + + + Andreas Nilsson + + + + + system + monitor + performance + + + + + + Jakub Steiner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/static/orchestra/icons/basket.png b/orchestra/static/orchestra/icons/basket.png index 1732bcbd..f1508c02 100644 Binary files a/orchestra/static/orchestra/icons/basket.png and b/orchestra/static/orchestra/icons/basket.png differ diff --git a/orchestra/static/orchestra/icons/basket.svg b/orchestra/static/orchestra/icons/basket.svg index d2241ad0..346f1e72 100644 --- a/orchestra/static/orchestra/icons/basket.svg +++ b/orchestra/static/orchestra/icons/basket.svg @@ -17,7 +17,7 @@ inkscape:version="0.48.3.1 r9886" sodipodi:docname="basket.svg" inkscape:output_extension="org.inkscape.output.svg.inkscape" - inkscape:export-filename="/home/glic3rinu/orchestra/django-orchestra/orchestra/static/orchestra/icons/basket.png" + inkscape:export-filename="/home/glic3/orchestra/django-orchestra/orchestra/static/orchestra/icons/basket.png" inkscape:export-xdpi="90" inkscape:export-ydpi="90" version="1.0"> @@ -195,16 +195,6 @@ y1="33.139202" x2="30.587307" y2="37.720802" /> - + inkscape:window-maximized="1"> + d="m 8,37.75 c 0,2.071068 -6.2680135,3.75 -14,3.75 -7.731986,0 -14,-1.678932 -14,-3.75 0,-2.071068 6.268014,-3.75 14,-3.75 7.7319865,0 14,1.678932 14,3.75 z" + transform="matrix(1.4862766,0,0,1.6296267,31.350537,-35.455906)" /> + sodipodi:docname="bill.svg" + inkscape:export-filename="/home/glic3/orchestra/django-orchestra/orchestra/static/orchestra/icons/bill.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90"> - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + inkscape:window-width="1920" + inkscape:window-height="1024" + inkscape:window-x="0" + inkscape:window-y="27" + inkscape:window-maximized="1" /> @@ -1245,104 +1543,175 @@ inkscape:label="Layer 1" inkscape:groupmode="layer"> + id="g10836" + transform="matrix(0.74543284,0,0,0.74543284,-0.15955986,-6.7557163)"> + - - - - - - - - - - + transform="matrix(1.2849052,0,0,0.8866667,0.967458,18.439998)" + id="g2858" + style="opacity:0.4"> - - - - - - - - - + width="40.700279" + height="9" + x="4" + y="39" + id="rect2860" + style="fill:url(#linearGradient7979);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible" /> + + + + + + + + + + + + + + + + + + + + + + + + Invoice + + diff --git a/orchestra/static/orchestra/icons/gauge.png b/orchestra/static/orchestra/icons/gauge.png new file mode 100644 index 00000000..d5d2e57e Binary files /dev/null and b/orchestra/static/orchestra/icons/gauge.png differ diff --git a/orchestra/static/orchestra/icons/gauge.svg b/orchestra/static/orchestra/icons/gauge.svg new file mode 100644 index 00000000..b9fdd512 --- /dev/null +++ b/orchestra/static/orchestra/icons/gauge.svg @@ -0,0 +1,957 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/orchestra/static/orchestra/icons/price.svg b/orchestra/static/orchestra/icons/price.svg index ef0830b8..6e299136 100644 --- a/orchestra/static/orchestra/icons/price.svg +++ b/orchestra/static/orchestra/icons/price.svg @@ -25,16 +25,7 @@ id="metadata11">image/svg+xml + @@ -100,5 +91,4 @@ id="path5" style="fill:#555753" inkscape:connector-curvature="0" /> - - \ No newline at end of file + \ No newline at end of file diff --git a/scripts/services/postfix.md b/scripts/services/postfix.md index 47226145..084167ab 100644 --- a/scripts/services/postfix.md +++ b/scripts/services/postfix.md @@ -3,10 +3,21 @@ apt-get install postfix # http://www.postfix.org/VIRTUAL_README.html#virtual_mailbox # https://help.ubuntu.com/community/PostfixVirtualMailBoxClamSmtpHowto - - # http://wiki2.dovecot.org/HowTo/VirtualUserFlatFilesPostfix +# http://www.mailscanner.info/ubuntu.html -root@web:~# apt-get install dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-sieve +apt-get install dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-sieve + + +cat > /etc/apt/sources.list.d/mailscanner.list << 'EOF' +deb http://apt.baruwa.org/debian wheezy main +deb-src http://apt.baruwa.org/debian wheezy main +EOF + +wget -O - http://apt.baruwa.org/baruwa-apt-keys.gpg | apt-key add - + + +apt-get update +apt-get install mailscanner