From 912ee9744eba75c1655de061dc48d99c9089dd01 Mon Sep 17 00:00:00 2001 From: Marc Aymerich Date: Fri, 18 Sep 2015 11:29:52 +0000 Subject: [PATCH] Fixed account delete/disable --- orchestra/contrib/accounts/actions.py | 110 +++++++++++++++--- orchestra/contrib/accounts/admin.py | 16 ++- orchestra/contrib/accounts/models.py | 11 +- .../delete_related_services_confirmation.html | 39 +++++++ .../disable_selected_confirmation.html | 49 ++++++++ orchestra/contrib/resources/models.py | 8 +- orchestra/contrib/resources/signals.py | 8 +- orchestra/contrib/saas/backends/__init__.py | 6 +- orchestra/contrib/websites/backends/apache.py | 14 +-- orchestra/getips.py | 15 +++ 10 files changed, 239 insertions(+), 37 deletions(-) create mode 100644 orchestra/contrib/accounts/templates/admin/accounts/account/delete_related_services_confirmation.html create mode 100644 orchestra/contrib/accounts/templates/admin/accounts/account/disable_selected_confirmation.html create mode 100644 orchestra/getips.py diff --git a/orchestra/contrib/accounts/actions.py b/orchestra/contrib/accounts/actions.py index 9c93ff07..032af361 100644 --- a/orchestra/contrib/accounts/actions.py +++ b/orchestra/contrib/accounts/actions.py @@ -10,7 +10,7 @@ from django.utils import timezone from django.utils.encoding import force_text from django.utils.html import format_html from django.utils.text import capfirst -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext, ugettext_lazy as _ from orchestra.core import services @@ -146,16 +146,24 @@ def delete_related_services(modeladmin, request, queryset): # The user has already confirmed the deletion. # Do the deletion and return a None to display the change list view again. if request.POST.get('post'): - n = queryset.count() - if n: + accounts = len(queryset) + msg = _("Related services deleted and account disabled.") + for account in queryset: + account.is_active = False + account.save(update_fields=('is_active',)) + modeladmin.log_change(request, account, msg) + if accounts: + relateds = len(to_delete) for obj in to_delete: obj_display = force_text(obj) modeladmin.log_deletion(request, obj, obj_display) - # TODO This probably will fail in certain conditions, just capture exception obj.delete() - modeladmin.message_user(request, _("Successfully deleted %(count)d %(items)s.") % { - "count": n, "items": model_ngettext(modeladmin.opts, n) - }, messages.SUCCESS) + context = { + 'accounts': accounts, + 'relateds': relateds, + } + msg = _("Successfully disabled %(accounts)d account and deleted %(relateds)d related services.") % context + modeladmin.message_user(request, msg, messages.SUCCESS) # Return None to display the change list page again. return None @@ -165,7 +173,7 @@ def delete_related_services(modeladmin, request, queryset): objects_name = force_text(opts.verbose_name_plural) context = dict( - modeladmin.admin_site.each_context(request), + admin_site.each_context(request), title=_("Are you sure?"), objects_name=objects_name, deletable_objects=[related_services], @@ -174,11 +182,85 @@ def delete_related_services(modeladmin, request, queryset): opts=opts, action_checkbox_name=helpers.ACTION_CHECKBOX_NAME, ) - request.current_app = modeladmin.admin_site.name + request.current_app = admin_site.name # Display the confirmation page - return TemplateResponse(request, modeladmin.delete_selected_confirmation_template or [ - "admin/%s/%s/delete_selected_confirmation.html" % (app_label, opts.model_name), - "admin/%s/delete_selected_confirmation.html" % app_label, - "admin/delete_selected_confirmation.html" - ], context) + template = 'admin/%s/%s/delete_related_services_confirmation.html' % (app_label, opts.model_name) + return TemplateResponse(request, template, context) delete_related_services.short_description = _("Delete related services") + + +def disable_selected(modeladmin, request, queryset): + opts = modeladmin.model._meta + app_label = opts.app_label + + # The user has already confirmed the deletion. + # Do the disable and return a None to display the change list view again. + if request.POST.get('post'): + n = 0 + for account in queryset: + account.disable() + modeladmin.log_change(request, account, _("Disabled")) + n += 1 + modeladmin.message_user(request, ungettext( + _("One account has been successfully disabled."), + _("%i accounts have been successfully disabled.") % n, + n) + ) + return None + + user = request.user + admin_site = modeladmin.admin_site + + def format(obj): + has_admin = obj.__class__ in admin_site._registry + opts = obj._meta + no_edit_link = '%s: %s' % (capfirst(opts.verbose_name), force_text(obj)) + if has_admin: + try: + admin_url = reverse( + 'admin:%s_%s_change' % (opts.app_label, opts.model_name), + None, + (quote(obj._get_pk_val()),) + ) + except NoReverseMatch: + # Change url doesn't exist -- don't display link to edit + return no_edit_link + + p = '%s.%s' % (opts.app_label, get_permission_codename('delete', opts)) + if not user.has_perm(p): + perms_needed.add(opts.verbose_name) + # Display a link to the admin page. + context = (capfirst(opts.verbose_name), admin_url, obj) + return format_html('{}: {}', *context) + else: + # Don't display link to edit, because it either has no + # admin or is edited inline. + return no_edit_link + + display = [] + for account in queryset: + current = [] + for related in account.get_services_to_disable(): + current.append(format(related)) + display.append([format(account), current]) + + if len(queryset) == 1: + objects_name = force_text(opts.verbose_name) + else: + objects_name = force_text(opts.verbose_name_plural) + + context = dict( + admin_site.each_context(request), + title=_("Are you sure?"), + objects_name=objects_name, + deletable_objects=display, + queryset=queryset, + opts=opts, + action_checkbox_name=helpers.ACTION_CHECKBOX_NAME, + ) + request.current_app = admin_site.name + template = 'admin/%s/%s/disable_selected_confirmation.html' % (app_label, opts.model_name) + return TemplateResponse(request, template, context) +disable_selected.short_description = _("Disable selected accounts") +disable_selected.url = 'disable' +disable_selected.tool_description = _("Disable") diff --git a/orchestra/contrib/accounts/admin.py b/orchestra/contrib/accounts/admin.py index 4584e880..4cadc51b 100644 --- a/orchestra/contrib/accounts/admin.py +++ b/orchestra/contrib/accounts/admin.py @@ -13,12 +13,12 @@ from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin -from orchestra.admin.actions import SendEmail, disable +from orchestra.admin.actions import SendEmail from orchestra.admin.utils import wrap_admin_view, admin_link, set_url_query, change_url from orchestra.core import services, accounts from orchestra.forms import UserChangeForm -from .actions import list_contacts, service_report, delete_related_services +from .actions import list_contacts, service_report, delete_related_services, disable_selected from .filters import HasMainUserListFilter from .forms import AccountCreationForm from .models import Account @@ -61,8 +61,10 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin) filter_horizontal = () change_readonly_fields = ('username', 'main_systemuser_link', 'is_active') change_form_template = 'admin/accounts/account/change_form.html' - actions = [disable, list_contacts, service_report, SendEmail(), delete_related_services] - change_view_actions = [disable, service_report] + actions = ( + disable_selected, delete_related_services, list_contacts, service_report, SendEmail() + ) + change_view_actions = (disable_selected, service_report) ordering = () main_systemuser_link = admin_link('main_systemuser') @@ -108,6 +110,12 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin) form.save_related(obj) else: super(AccountAdmin, self).save_model(request, obj, form, change) + + def get_actions(self, request): + actions = super(AccountAdmin, self).get_actions(request) + if 'delete_selected' in actions: + del actions['delete_selected'] + return actions admin.site.register(Account, AccountAdmin) diff --git a/orchestra/contrib/accounts/models.py b/orchestra/contrib/accounts/models.py index 84e0b74f..4977427d 100644 --- a/orchestra/contrib/accounts/models.py +++ b/orchestra/contrib/accounts/models.py @@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy as _ from orchestra.contrib.orchestration.middlewares import OperationsMiddleware from orchestra.contrib.orchestration import Operation +from orchestra.core import services from orchestra.utils.mail import send_email_template from . import settings @@ -82,13 +83,17 @@ class Account(auth.AbstractBaseUser): self.save(update_fields=['is_active']) self.notify_related() - def notify_related(self): - # Trigger save() on related objects that depend on this account + def get_services_to_disable(self): for rel in self._meta.get_all_related_objects(): source = getattr(rel, 'related_model', rel.model) if source in services and hasattr(source, 'active'): for obj in getattr(self, rel.get_accessor_name()).all(): - OperationsMiddleware.collect(Operation.SAVE, instance=obj, update_fields=[]) + yield obj + + def notify_related(self): + """ Trigger save() on related objects that depend on this account """ + for obj in self.get_services_to_disable(): + OperationsMiddleware.collect(Operation.SAVE, instance=obj, update_fields=[]) def send_email(self, template, context, email_from=None, contacts=[], attachments=[], html=None): contacts = self.contacts.filter(email_usages=contacts) diff --git a/orchestra/contrib/accounts/templates/admin/accounts/account/delete_related_services_confirmation.html b/orchestra/contrib/accounts/templates/admin/accounts/account/delete_related_services_confirmation.html new file mode 100644 index 00000000..b25aee0b --- /dev/null +++ b/orchestra/contrib/accounts/templates/admin/accounts/account/delete_related_services_confirmation.html @@ -0,0 +1,39 @@ +{% extends "admin/delete_selected_confirmation.html" %} +{% load i18n l10n admin_urls %} + +{% block content %} +{% if perms_lacking %} +

{% blocktrans %}Deleting the selected {{ objects_name }} would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:{% endblocktrans %}

+ +{% elif protected %} +

{% blocktrans %}Deleting the selected {{ objects_name }} would require deleting the following protected related objects:{% endblocktrans %}

+ +{% else %} +

{% blocktrans %}Are you sure you want to delete the selected {{ objects_name }}? All of the following objects and their related items will be deleted:{% endblocktrans %}

+ {% include "admin/includes/object_delete_summary.html" %} +

{% trans "Objects" %}

+ {% for deletable_object in deletable_objects %} + + {% endfor %} +
{% csrf_token %} +
+ {% for obj in queryset %} + + {% endfor %} + + + + {% trans "No, take me back" %} +
+
+{% endif %} +{% endblock %} + diff --git a/orchestra/contrib/accounts/templates/admin/accounts/account/disable_selected_confirmation.html b/orchestra/contrib/accounts/templates/admin/accounts/account/disable_selected_confirmation.html new file mode 100644 index 00000000..0479bc20 --- /dev/null +++ b/orchestra/contrib/accounts/templates/admin/accounts/account/disable_selected_confirmation.html @@ -0,0 +1,49 @@ +{% extends "admin/base_site.html" %} +{% load i18n l10n admin_urls %} + +{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} delete-confirmation delete-selected-confirmation{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +{% if perms_lacking %} +

{% blocktrans %}Disabling the selected {{ objects_name }} would result in disabling related objects, but your account doesn't have permission to disable the following types of objects:{% endblocktrans %}

+ +{% elif protected %} +

{% blocktrans %}Deleting the selected {{ objects_name }} would require deleting the following protected related objects:{% endblocktrans %}

+ +{% else %} +

{% blocktrans %}Are you sure you want to disable the selected {{ objects_name }}? All of the following objects and their related items will be disabled:{% endblocktrans %}

+

{% trans "Objects" %}

+ {% for deletable_object in deletable_objects %} + + {% endfor %} +
{% csrf_token %} +
+ {% for obj in queryset %} + + {% endfor %} + + + + {% trans "No, take me back" %} +
+
+{% endif %} +{% endblock %} + diff --git a/orchestra/contrib/resources/models.py b/orchestra/contrib/resources/models.py index 13ee177f..e2c2e4fb 100644 --- a/orchestra/contrib/resources/models.py +++ b/orchestra/contrib/resources/models.py @@ -120,10 +120,12 @@ class Resource(models.Model): # This only works on tests (multiprocessing used on real deployments) apps.get_app_config('resources').reload_relations() - def sync_periodic_task(self): + def sync_periodic_task(self, delete=False): """ sync periodic task on save/delete resource operations """ name = 'monitor.%s' % str(self) - if self.pk and self.crontab and self.is_active: + if delete or not self.crontab or not self.is_active: + PeriodicTask.objects.filter(name=name).delete() + elif self.pk: try: task = PeriodicTask.objects.get(name=name) except PeriodicTask.DoesNotExist: @@ -138,8 +140,6 @@ class Resource(models.Model): if task.crontab != self.crontab: task.crontab = self.crontab task.save(update_fields=['crontab']) - else: - PeriodicTask.objects.filter(name=name).delete() def get_model_path(self, monitor): """ returns a model path between self.content_type and monitor.model """ diff --git a/orchestra/contrib/resources/signals.py b/orchestra/contrib/resources/signals.py index 918dbbf2..6ce7376a 100644 --- a/orchestra/contrib/resources/signals.py +++ b/orchestra/contrib/resources/signals.py @@ -5,8 +5,14 @@ from .models import Resource @receiver(post_save, sender=Resource, dispatch_uid="resources.sync_periodic_task") -@receiver(post_delete, sender=Resource, dispatch_uid="resources.sync_periodic_task") def sync_periodic_task(sender, **kwargs): """ useing signals instead of Model.delete() override beucause of admin bulk delete() """ instance = kwargs['instance'] instance.sync_periodic_task() + + +@receiver(post_delete, sender=Resource, dispatch_uid="resources.delete_periodic_task") +def delete_periodic_task(sender, **kwargs): + """ useing signals instead of Model.delete() override beucause of admin bulk delete() """ + instance = kwargs['instance'] + instance.sync_periodic_task(delete=True) diff --git a/orchestra/contrib/saas/backends/__init__.py b/orchestra/contrib/saas/backends/__init__.py index 57f256fc..17c4fe15 100644 --- a/orchestra/contrib/saas/backends/__init__.py +++ b/orchestra/contrib/saas/backends/__init__.py @@ -69,8 +69,10 @@ class SaaSWebTraffic(ServiceMonitor): try: with open(access_log, 'r') as handler: for line in handler.readlines(): - meta, request, response, hostname, __ = line.split('"') + line = line.split() + meta = line[:4] host, __, __, date, tz = meta.split() + size, hostname = line[-2:] if host in {ignore_hosts}: continue try: @@ -78,7 +80,7 @@ class SaaSWebTraffic(ServiceMonitor): except KeyError: continue else: - # [16/Sep/2015:11:40:38 +0200] + # [16/Sep/2015:11:40:38 day, month, date = date[1:].split('/') year, hour, min, sec = date.split(':') date = year + months[month] + day + hour + min + sec diff --git a/orchestra/contrib/websites/backends/apache.py b/orchestra/contrib/websites/backends/apache.py index 2143a613..e91a1973 100644 --- a/orchestra/contrib/websites/backends/apache.py +++ b/orchestra/contrib/websites/backends/apache.py @@ -116,25 +116,21 @@ class Apache2Backend(ServiceController): if context['server_name'] and site.active: self.append(textwrap.dedent(""" # Enable site %(site_name)s - if [[ ! -f %(sites_enabled)s ]]; then - a2ensite %(site_unique_name)s.conf - UPDATED_APACHE=1 - fi""") % context + [[ $(a2ensite %(site_unique_name)s) =~ "already enabled" ]] || UPDATED_APACHE=1\ + """) % context ) else: self.append(textwrap.dedent(""" # Disable site %(site_name)s - if [[ -f %(sites_enabled)s ]]; then - a2dissite %(site_unique_name)s.conf; - UPDATED_APACHE=1 - fi""") % context + [[ $(a2dissite %(site_unique_name)s) =~ "already disabled" ]] || UPDATED_APACHE=1\ + """) % context ) def delete(self, site): context = self.get_context(site) self.append(textwrap.dedent(""" # Remove site configuration for %(site_name)s - a2dissite %(site_unique_name)s.conf && UPDATED_APACHE=1 + [[ $(a2dissite %(site_unique_name)s) =~ "already disabled" ]] || UPDATED_APACHE=1 rm -f %(sites_available)s\ """) % context ) diff --git a/orchestra/getips.py b/orchestra/getips.py new file mode 100644 index 00000000..839f24fd --- /dev/null +++ b/orchestra/getips.py @@ -0,0 +1,15 @@ +#! /usr/bin/env python + +import requests +import json + + +SLICE_ID=1513 + + +slice_req = requests.get('https://controller.community-lab.net/api/slices/%i' % SLICE_ID) +slice = json.loads(slice_req.content) +for sliver in slice['slivers']: + sliver_req = requests.get(sliver['uri']) + sliver = json.loads(sliver_req.content) + print sliver['mgmt_net']['address']