Я хочу иметь возможность отображать список запрещенных IP-адресов, сгруппированных по ip, отсортированных по убыванию по количеству и последнему времени бана, в виде списка изменений администратора.
fail2ban генерирует sqlite-db и debian-8 / fail2ban-debian-0.9.6 генерирует эту таблицу:
"CREATE TABLE bans(" \
"jail TEXT NOT NULL, " \
"ip TEXT, " \
"timeofban INTEGER NOT NULL, " \
"data JSON, " \
"FOREIGN KEY(jail) REFERENCES jails(name) " \
");" \
"CREATE INDEX bans_jail_timeofban_ip ON bans(jail, timeofban);" \
"CREATE INDEX bans_jail_ip ON bans(jail, ip);" \
"CREATE INDEX bans_ip ON bans(ip);"
SQL, который я бы хотел создать django, должен возвращать те же результаты, что и этот SQL:
SELECT
ip,
strftime('%Y-%m-%d %H:%M:%S', timeofban, 'unixepoch', 'localtime') as latest,
COUNT(ip) as ipcount
FROM
bans
GROUP BY
ip
ORDER BY
ipcount DESC,
timeofban DESC
Итак, я начал настраивать дополнительную базу данных в настройках:
DATABASES = {
...
'fail2ban': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': '/var/lib/fail2ban/fail2ban.sqlite3',
}
}
DATABASE_ROUTERS = [
'fail2ban.dbrouter.Fail2BanRouter'
]
Создал модель:
from django.db import models
class Ban(models.Model):
jail = models.CharField('jail', max_length=250)
ip = models.CharField('ip', max_length=100)
timeofban = models.IntegerField('timeofban', primary_key=True)
data = models.TextField('data', blank=True, null=True)
class Meta:
db_table = 'bans'
ordering = ["-timeofban"]
managed = False
def save(self, **kwargs):
raise NotImplementedError()
Установите администратора:
import datetime
from django.contrib import admin
from django.db.models import Count, Max, OuterRef, Subquery
from .models import Ban
@admin.register(Ban)
class BanAdmin(admin.ModelAdmin):
list_display = ['ip', 'jail', 'get_timeofban']
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
def has_change_permission(self, request, obj=None):
return False
def has_view_permission(self, request, obj=None):
return True
def get_timeofban(self, instance):
return datetime.datetime.fromtimestamp(instance.timeofban).strftime('%Y-%m-%d %H:%M:%S')
get_timeofban.short_description = 'timeofban'
get_timeofban.admin_order_field = 'timeofban'
def get_queryset(self, request):
qs = super().get_queryset(request)
duplicates = qs.values('ip') \
.annotate(ipcount=Count('ip'), latest=Max('timeofban')) \
.order_by('-ipcount') \
.filter(ipcount__gt=10) \
.values_list('latest', flat=True)
qs = qs.filter(timeofban__in=duplicates)
subquery = Ban.objects.values('ip') \
.annotate(ipcount=Count('ip')) \
.filter(ip=OuterRef('ip')) \
.order_by('-ipcount') \
.values('ipcount')
return qs.annotate(
ipcount=Subquery(subquery)
).order_by('-ipcount')
Это единственный способ создать QuerySet, который мне нужен для администратора. БД имеет примерно 80000 строк, поэтому без предварительной фильтрации невозможно достичь этого с помощью подзапроса. И я не могу поверить, что в django этот sql - единственный способ получить желаемые результаты:
SELECT
"bans"."jail", "bans"."ip", "bans"."timeofban", "bans"."data",
(
SELECT COUNT(U0."ip") AS "ipcount" FROM "bans" U0
WHERE U0."ip" = ("bans"."ip")
GROUP BY U0."ip" ORDER BY "ipcount" DESC
) AS "ipcount"
FROM "bans"
WHERE "bans"."timeofban" IN (SELECT MAX(U0."timeofban") AS "latest"
FROM "bans" U0
GROUP BY U0."ip" HAVING COUNT(U0."ip") > 10)
ORDER BY "ipcount" DESC
LIMIT 21
Я знаю, что могу получить ValuesQuerySet, создав запрос, который выглядит почти так, как мне нужно:
from django import db
from .models import Ban
Ban.objects.order_by('-ipcount').values('ip').annotate(ipcount=Count('ip'))
db.connections['fail2ban'].queries[-1]['sql']
# 'SELECT "bans"."ip", COUNT("bans"."ip") AS "ipcount" FROM "bans" GROUP BY "bans"."ip" ORDER BY "ipcount" DESC LIMIT 21'
, но даже здесь я озадачен тем, как я мог бы извлечь из этого дополнительное поле 'timeofban'.
Поэтому мой вопрос, есть ли что-то, что я упустил в достижении SQLя хотел бы, чтобы django создавал как QuerySet (не ValueQuerySet) и лучшее решение, чем Subquery-Overkill, который я придумал.