From fc358590523de0f58d65e0b0bf69f273895dc9ae Mon Sep 17 00:00:00 2001 From: Marc Date: Tue, 8 Jul 2014 15:19:15 +0000 Subject: [PATCH] Random work --- TODO.md | 4 + orchestra/admin/filters.py | 18 + orchestra/admin/menu.py | 10 +- orchestra/admin/utils.py | 7 +- orchestra/apps/accounts/admin.py | 10 +- orchestra/apps/accounts/models.py | 4 +- orchestra/apps/bills/models.py | 13 + orchestra/apps/databases/admin.py.save | 115 +++ orchestra/apps/databases/forms.py | 1 + orchestra/apps/domains/admin.py | 4 +- orchestra/apps/issues/admin.py | 12 +- orchestra/apps/issues/forms.py | 9 +- orchestra/apps/issues/models.py | 10 +- orchestra/apps/orchestration/admin.py | 4 +- orchestra/apps/orchestration/backends.py | 12 +- orchestra/apps/resources/__init__.py | 0 orchestra/apps/resources/admin.py | 92 ++ orchestra/apps/resources/forms.py | 30 + orchestra/apps/resources/models.py | 112 ++ orchestra/apps/users/admin.py | 8 +- orchestra/apps/users/roles/mail/admin.py | 4 +- orchestra/apps/webapps/models.py | 2 +- orchestra/apps/websites/admin.py | 4 +- orchestra/apps/websites/models.py | 2 +- orchestra/apps/websites/resources.py | 291 ++++++ orchestra/bin/orchestra-admin | 7 +- orchestra/conf/base_settings.py | 6 +- orchestra/core/__init__.py | 1 - orchestra/forms/widgets.py | 27 + .../icons/Utilities-system-monitor.png | Bin 0 -> 3389 bytes .../icons/Utilities-system-monitor.svg | 368 +++++++ orchestra/static/orchestra/icons/basket.png | Bin 4312 -> 4316 bytes orchestra/static/orchestra/icons/basket.svg | 66 +- orchestra/static/orchestra/icons/bill.png | Bin 0 -> 2812 bytes orchestra/static/orchestra/icons/bill.svg | 677 ++++++++++--- orchestra/static/orchestra/icons/gauge.png | Bin 0 -> 2780 bytes orchestra/static/orchestra/icons/gauge.svg | 957 ++++++++++++++++++ orchestra/static/orchestra/icons/price.svg | 22 +- scripts/services/postfix.md | 17 +- 39 files changed, 2668 insertions(+), 258 deletions(-) create mode 100644 orchestra/admin/filters.py create mode 100644 orchestra/apps/bills/models.py create mode 100644 orchestra/apps/databases/admin.py.save create mode 100644 orchestra/apps/resources/__init__.py create mode 100644 orchestra/apps/resources/admin.py create mode 100644 orchestra/apps/resources/forms.py create mode 100644 orchestra/apps/resources/models.py create mode 100644 orchestra/apps/websites/resources.py create mode 100644 orchestra/static/orchestra/icons/Utilities-system-monitor.png create mode 100644 orchestra/static/orchestra/icons/Utilities-system-monitor.svg create mode 100644 orchestra/static/orchestra/icons/bill.png create mode 100644 orchestra/static/orchestra/icons/gauge.png create mode 100644 orchestra/static/orchestra/icons/gauge.svg 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 0000000000000000000000000000000000000000..c755c141f4af081d94c82e8fee55db46619cd5b3 GIT binary patch literal 3389 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 TD{Uch00000NkvXXu0mjfF-dj& literal 0 HcmV?d00001 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 1732bcbd5d492df45da3b26f58c43e2121b48103..f1508c02d355ddbfc84e74bbd6d62a7c6017dfad 100644 GIT binary patch delta 4171 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 R8Yut(002ovPDHLkV1gw2&*%UE 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)" /> AOji{MK4KMj^%+ z){g6+)#nO5FFWV%-Me@1!q83UNK6Uf<(FT6YuBz_S0_nw*2=owuDti&d$ZpAZ|~i^ z_ubFR$WuvL8@G}qAC$CaHp-rwx<}o_I4&Gr|nf&U3KZZ@4oxuE3drrOXu8^ z*I$2q_p>*9AQgZrMQj2|ia}3bOOk@I0JK^ycI?=(rPJyBZgFw(*KfS>hNZzyb^x7^$LsQ9@^8__tg8gYq`CDY_) zqDX}@5-~~@umYqObBI=v)1XKQG>#pmvHWrIcM3XifI+$dsfsMA;*7N^)($*h8gESm zW9&!(n9`U~1Vovi{t{cha1GX;sx28_D6O&B#NvYxyl3vnA@aALr}frz81V(bG96h2 zNUi9yN(k7-plnTHsEK)RjD@6#OgX0-Jz*79DOPSl5dhXrPT{>j1%P2O#vl^Y=J?7A zTefazzI7YRGgqMJ0i?OI-0&i$=~~)b(PQ*5 zK`|;N0Tsm>3YL*-Twa$T&Qwl(@B!LxlQf(3`+YWT+Qih<6ad{lKft9Er2lvZpJg8d z*i`(Y_)r;N@ovQ)1+@i-dWSlPxEysk>Rg-;zF-+20qAPa|LwZ#01)RQ*b$;dx^?&O zCp&yN-X8+(cAN3>aV91vm^g1E;o4j2U;jM~D<@(EmdExiZ4B?kDi0ovL9GD=BXO){ z;o9nP@kdL~7mJS+{9Z0jMFf1e%i?RV@%laYkhI%;@&5anedr-B+P9DHfdh2^{cldp z&T{d-eT06WcCW|bhaYC@rkmLC{qG}L2F}qM8^cdd(cAfT($LrmJZg8}d#?*wE*gCF zaOvr_WABhWM_q>IE}mVE6cG`h$9uf1jIV=`JZIyLH*)^XH{*^Sqj%&8A3pXNo4@%@ zj_u#iwNE_3p(mb*kq}|wx#wu?-p%t5KFFhc_VDc8ck}8akFc6BO3;MQyFDS})?c?kx$zrs)RXxnUasufILdbVIo${jf^Cva#!Dj$ zHjg(d8b=3>-LvinLVFX$> zbvih!)N)^yejfPXXxs`e9CKu2O>}(Qp!ih)rEhaq(mq+*hD56kP1&)Az7#44^%ZL% zuq2tomK{aS_@-efsvx9|EWROAx<|XrJB4A|L`zOo)3IGFYEsD4-73 z?1$Bh-^u37F_bAJVJ_r3X)}OoZ1>k1U$xY0EU$0wYrC|%F}=+3!I4EUuyAaeZ0-m; zc3{ZBEbDQ+yF{M#hUNf#(WaCDDjIc;uFpcAs5C(+N#~R)RCkirfed@FVc??aF~HEt zQzkcT=Ax}v3>o-IDfIfA@6;Yyi$E5%+ss3j;G2}T)+12LNokZ?3ayEN#5A(S`y3-u z_`?pVGR9(UGIDOM;i~{R)#G^JloO$%Gs#onjm zE$cVVuxA)RMPqg4d{K9?>0;lImw-EzVBU8zx<2W?WfyBu=KUTBVNCE^Dipks_l zIrX~xiPC5^`lW|dlvI`JLSq|fH``234);5abPN$0ITsO_RS^gwFhsbj>Q&W!nuFEL z94dlw9^-s`S$mc{wNbS(I#!=j>MTT(a)^!mF^^u&1nistAwBT^uz@0o1d{r0ttLs* zI7u^i4y4>UHCp4``Qx8`W_m{!@n + 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 0000000000000000000000000000000000000000..d5d2e57eb227d169ef68004360f704bd5c8a1db5 GIT binary patch literal 2780 zcmV<23M2K2P)gupd>0K6cB_o zw5)BQL{)>J3!+s8Eg#wx648Z9ZCTW+MQsV9p=%`ClCrc3&{8$!Lur7J1QOzkq>3HK z_Bfu!<2Rm}H*dN9@ZOl0v7Ok?BK1mF^Ok$h`Tx&7=iKwIMo207e{P=hA+)`{-K8i> zou+9XDW!0^Tt+w?)>5gIF3+*W#JP}{NF?f1RZV(4p7qgav^$whMuiZI%jI&1VHiGO zysob9y>vP~lF4Mo%H{GdP16okDwW*X5}G%G#fula^7(vMBoeu=v$ONYP$<+HjYcsH z1Jg9obsfVnCcCcdlU-F+(&;p+s=k;?rFK>-l|N{jrd$vLLI@Fy#TLio@yA!LTzPXO z62UahDWI#@wZFB`bX{k3bTpq%ryn#7<7rh@%jbFL>$NFF(yZhx8D^@I(QchZ2*L4O41~`8F_^y*DPd=|{+Dl%qcPx=esC|8X-5!tU zGF{i7({;V*@pytBkEg}!_1@Fi*my%jLqnI#<(hOm!!Rh7O62qTZx;%MN2HY6SrZUK zh-fsre$ARSzepq!@v7BzU8k?FFWuYQyS-2-JhpV{($wC)druJ-3Wa{KeEIS%hYugV zL{XG|HFE2GKHv4xX!I_h&$r&=@k|miO_N+Mx4T>}Z_qR?J8uF)2+`Wwdgm2aT=AP| zG&*UeR4T>$@4x@#=;-K{v9YnS>iEXS#;|FctG!-sKT*WcE4y&$DT2;r)$tGhNF4nGzQ23J;B*ECIv#p1gY6B8fRG%YhP0?}yn z`n7AJ$wt-R zVzEf6RQgXopMRxXEZ{*wZf>3gzkmP!{R0C7w`MY#et?FCh6RyGO)nZKXT+qZ*On!x@8 z$s7?v5R1h&^z`&R5ekKxs%}yRbtbDTTuu_sJ~1&t zKA#trO66JL5_=DL6nN1AEdl&k2q9)9AcUZ!qvKoc?d^WUFwiuOW511FL~D@CI-d z@O9ui;AVT=_iG4j6hZ{15Lml*t?26Nx~U22tH6`M z79oTmNCCff5QqR*P9gBtTWo5#s2Y{5)kW#)0+yLZ&=YhWg1z@X`(zNIP$R>KF{k^cYW)*In zLZGg$uA`}`NoblzrBWf4O8skSXvnbNZSs1(Gy1!tD5L3g`dU?0hn!l<<#Jg{IUuF1 zEs;+EUjsf2_<_#=bwY@+5F!kufrG#Xd;GhAv#?ttgz$pnlhQQJn}A7Bg+gIBKr)$h z2LgesoVmU>9gdBSJ*BGZFtcx$13$N~+iZZf%?B7l2p3QS{tDb|zxyZ<0&rUq3zD4;Nsf<~|G#7YYW0KBr&1 zT&^jF(;NW{EH`jtxxj6}AMMw>flYG&4^WGz85}`un&v5+&)GB~LcUEoVAVI(Aa!nV^Ks1}AM61DJF!<_l0cYSxKfmJg(^`5bF_B1=IyyRJTU(oqL?RMcG}l$IL3*$@n^vHHo-zbh zY3;QBPfr&tX_#Mo0l|B)zLA~!4+G&TV`mUh2kfm8@E)-6 z0)Te|cTZ!$jnj@lji45+4XBpH+Vka3w&5O`2K@biYfc1gf!%;Hoi)yNU;_=nYt!g@ z4)Dx4Zx(`Y!72%A1@8pfYlCW@R=)$Q?yA-CCJ;RB{8=jL!77EP+g-QW?lvbPHt^f9 z4z{L~yB!FeQO+C`wgLNQP*z=$hi$TPtkVXM-Mv`laEA?gD{yoMIR$A*4H0KAVjYoGW;uShO>)+&IUk=Dwvw&01T~v}2>1fvpEI7b2tI%qR*HLN9-y)~x!pMM4v@hL+8w}%oO)t6circLz_fxbQq^)@ i0_OS9b>0YEsP;d{;`y-sOUr`*0000 + + + + + + + + + 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