From 8e3b33ee212fc1ea018f3c2cecef79b89260a325 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Mon, 15 Jun 2015 16:06:54 -0400 Subject: [PATCH 1/2] Added age_deleted command for support team --- awx/main/management/commands/age_deleted.py | 130 ++++++++++++++++++++ awx/main/tests/commands/__init__.py | 1 + awx/main/tests/commands/age_deleted.py | 42 +++++++ 3 files changed, 173 insertions(+) create mode 100644 awx/main/management/commands/age_deleted.py create mode 100644 awx/main/tests/commands/age_deleted.py diff --git a/awx/main/management/commands/age_deleted.py b/awx/main/management/commands/age_deleted.py new file mode 100644 index 0000000000..3d73d3f54e --- /dev/null +++ b/awx/main/management/commands/age_deleted.py @@ -0,0 +1,130 @@ +# Copyright (c) 2015 Ansible, Inc. +# All Rights Reserved. + +# Python +import datetime +import logging +from optparse import make_option + +# Django +from django.core.management.base import BaseCommand +from django.db import transaction +from django.contrib.auth.models import User +from django.utils.dateparse import parse_datetime +from django.utils.timezone import now, is_aware, make_aware +from django.core.management.base import CommandError + +# AWX +from awx.main.models import * # noqa + +class Command(BaseCommand): + ''' + Management command to age deleted items. + ''' + + help = 'Age deleted items in the database.' + + option_list = BaseCommand.option_list + ( + make_option('--days', dest='days', type='int', default=90, metavar='N', + help='Age deleted items N days (90 if not specified)'), + make_option('--id', dest='id', type='int', default=None, + help='Object primary key'), + make_option('--type', dest='type', default=None, + help='Model to limit aging to'), + ) + + def get_models(self, model): + if not model._meta.abstract: + yield model + for sub in model.__subclasses__(): + for submodel in self.get_models(sub): + yield submodel + + def cleanup_model(self, model, id=None): + ''' + Presume the '_deleted_' string to be in the 'name' field unless considering the User model. + When considering the User model, presume the '_d_' string to be in the 'username' field. + ''' + name_field = 'name' + name_prefix = '_deleted_' + n_aged_items = 0 + if model is User: + name_field = 'username' + name_prefix = '_d_' + active_field = None + for field in model._meta.fields: + if field.name in ('is_active', 'active'): + active_field = field.name + if not active_field: + #print("Skipping model %s, no active field" % model) + print("Returning %s" % n_aged_items) + return n_aged_items + + kv = { + active_field: False, + } + if id: + kv['pk'] = id + else: + kv['%s__startswith' % name_field] = name_prefix + + qs = model.objects.filter(**kv) + #print("Aging model %s" % model) + for instance in qs: + name = getattr(instance, name_field) + name_pieces = name.split('_') + if not name_pieces or len(name_pieces) < 3: + print("Unexpected deleted model name format %s" % name) + return n_aged_items + + if len(name_pieces) <= 3: + name_append = '' + else: + name_append = '_' + name_pieces[3] + + dt = parse_datetime(name_pieces[2]) + if not is_aware(dt): + dt = make_aware(dt, self.cutoff.tzinfo) + if not dt: + print('unable to find deleted timestamp in %s field' % name_field) + else: + aged_date = dt - datetime.timedelta(days=self.days) + instance.name = name_prefix + aged_date.isoformat() + name_append + instance.save() + #print("Aged %s" % instance.name) + n_aged_items += 1 + + + + return n_aged_items + + @transaction.atomic + def handle(self, *args, **options): + self.days = int(options.get('days', 90)) + self.id = options.get('id', None) + self.type = options.get('type', None) + self.cutoff = now() - datetime.timedelta(days=self.days) + + if self.id and not self.type: + raise CommandError('Specifying id requires --type') + + n_aged_items = 0 + if not self.type: + n_aged_items += self.cleanup_model(User) + for model in self.get_models(PrimordialModel): + n_aged_items += self.cleanup_model(model) + else: + model_found = None + if self.type == User.__name__: + model_found = User + else: + for model in self.get_models(PrimordialModel): + if model.__name__ == self.type: + model_found = model + break + if not model_found: + raise RuntimeError("Invalid type %s" % self.type) + n_aged_items += self.cleanup_model(model_found, self.id) + + print("Aged %d items" % n_aged_items) + diff --git a/awx/main/tests/commands/__init__.py b/awx/main/tests/commands/__init__.py index 7626f0f739..9c099516c1 100644 --- a/awx/main/tests/commands/__init__.py +++ b/awx/main/tests/commands/__init__.py @@ -6,3 +6,4 @@ from __future__ import absolute_import from .run_fact_cache_receiver import * # noqa from .commands_monolithic import * # noqa from .cleanup_facts import * # noqa +from .age_deleted import * # noqa diff --git a/awx/main/tests/commands/age_deleted.py b/awx/main/tests/commands/age_deleted.py new file mode 100644 index 0000000000..b8c9a55986 --- /dev/null +++ b/awx/main/tests/commands/age_deleted.py @@ -0,0 +1,42 @@ +# Copyright (c) 2015 Ansible, Inc. +# All Rights Reserved + +# Python +from datetime import datetime +from dateutil.relativedelta import relativedelta +import mock + +#Django +from django.core.management.base import CommandError + +# AWX +from awx.main.tests.base import BaseTest +from awx.main.tests.commands.base import BaseCommandMixin + +__all__ = ['AgeDeletedCommandFunctionalTest'] + +class AgeDeletedCommandFunctionalTest(BaseCommandMixin, BaseTest): + def setUp(self): + super(AgeDeletedCommandFunctionalTest, self).setUp() + self.create_test_license_file() + self.setup_instances() + self.setup_users() + self.organization = self.make_organization(self.super_django_user) + self.credential = self.make_credential() + self.credential2 = self.make_credential() + self.credential.mark_inactive(True) + self.credential2.mark_inactive(True) + self.credential_active = self.make_credential() + self.super_django_user.mark_inactive(True) + + def test_default(self): + result, stdout, stderr = self.run_command('age_deleted') + self.assertEqual(stdout, 'Aged %d items\n' % 3) + + def test_type(self): + result, stdout, stderr = self.run_command('age_deleted', type='Credential') + self.assertEqual(stdout, 'Aged %d items\n' % 2) + + def test_id_type(self): + result, stdout, stderr = self.run_command('age_deleted', type='Credential', id=self.credential.pk) + self.assertEqual(stdout, 'Aged %d items\n' % 1) From c076ba166901a761314aae928ec685bf69653099 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Mon, 15 Jun 2015 16:56:12 -0400 Subject: [PATCH 2/2] flake8 --- awx/main/management/commands/age_deleted.py | 1 - awx/main/tests/commands/age_deleted.py | 8 -------- 2 files changed, 9 deletions(-) diff --git a/awx/main/management/commands/age_deleted.py b/awx/main/management/commands/age_deleted.py index 3d73d3f54e..66a1a69ed6 100644 --- a/awx/main/management/commands/age_deleted.py +++ b/awx/main/management/commands/age_deleted.py @@ -3,7 +3,6 @@ # Python import datetime -import logging from optparse import make_option # Django diff --git a/awx/main/tests/commands/age_deleted.py b/awx/main/tests/commands/age_deleted.py index b8c9a55986..02738c2c1f 100644 --- a/awx/main/tests/commands/age_deleted.py +++ b/awx/main/tests/commands/age_deleted.py @@ -1,14 +1,6 @@ # Copyright (c) 2015 Ansible, Inc. # All Rights Reserved -# Python -from datetime import datetime -from dateutil.relativedelta import relativedelta -import mock - -#Django -from django.core.management.base import CommandError - # AWX from awx.main.tests.base import BaseTest from awx.main.tests.commands.base import BaseCommandMixin