Attempting to resolve unfold form inconsistencies.
This commit is contained in:
@@ -1,11 +1,22 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from guardian.admin import GuardedModelAdmin
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.html import format_html
|
||||||
|
try:
|
||||||
|
from unfold.contrib.guardian.admin import GuardedModelAdmin
|
||||||
|
except ImportError: # Fallback for older Unfold builds without guardian admin shim.
|
||||||
|
from guardian.admin import GuardedModelAdmin as GuardianGuardedModelAdmin
|
||||||
|
from unfold.admin import ModelAdmin as UnfoldModelAdmin
|
||||||
|
|
||||||
|
class GuardedModelAdmin(GuardianGuardedModelAdmin, UnfoldModelAdmin):
|
||||||
|
pass
|
||||||
|
|
||||||
from .models import AccessRequest
|
from .models import AccessRequest
|
||||||
|
|
||||||
|
|
||||||
@admin.register(AccessRequest)
|
@admin.register(AccessRequest)
|
||||||
class AccessRequestAdmin(GuardedModelAdmin):
|
class AccessRequestAdmin(GuardedModelAdmin):
|
||||||
|
autocomplete_fields = ("requester", "server", "decided_by")
|
||||||
list_display = (
|
list_display = (
|
||||||
"id",
|
"id",
|
||||||
"requester",
|
"requester",
|
||||||
@@ -14,7 +25,75 @@ class AccessRequestAdmin(GuardedModelAdmin):
|
|||||||
"requested_at",
|
"requested_at",
|
||||||
"expires_at",
|
"expires_at",
|
||||||
"decided_by",
|
"decided_by",
|
||||||
|
"delete_link",
|
||||||
)
|
)
|
||||||
list_filter = ("status", "server")
|
list_filter = ("status", "server")
|
||||||
search_fields = ("requester__username", "requester__email", "server__display_name")
|
search_fields = ("requester__username", "requester__email", "server__display_name")
|
||||||
ordering = ("-requested_at",)
|
ordering = ("-requested_at",)
|
||||||
|
compressed_fields = True
|
||||||
|
actions_on_top = True
|
||||||
|
actions_on_bottom = True
|
||||||
|
def get_readonly_fields(self, request, obj=None):
|
||||||
|
readonly = ["requested_at"]
|
||||||
|
if obj:
|
||||||
|
readonly.extend(["decided_at", "decided_by"])
|
||||||
|
return readonly
|
||||||
|
|
||||||
|
def get_fieldsets(self, request, obj=None):
|
||||||
|
if obj is None:
|
||||||
|
return (
|
||||||
|
(
|
||||||
|
"Request",
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"requester",
|
||||||
|
"server",
|
||||||
|
"status",
|
||||||
|
"reason",
|
||||||
|
"expires_at",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
(
|
||||||
|
"Request",
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"requester",
|
||||||
|
"server",
|
||||||
|
"status",
|
||||||
|
"reason",
|
||||||
|
"expires_at",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Decision",
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"decided_at",
|
||||||
|
"decided_by",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change) -> None:
|
||||||
|
if obj.status in {
|
||||||
|
AccessRequest.Status.APPROVED,
|
||||||
|
AccessRequest.Status.DENIED,
|
||||||
|
AccessRequest.Status.REVOKED,
|
||||||
|
AccessRequest.Status.CANCELLED,
|
||||||
|
}:
|
||||||
|
if not obj.decided_at:
|
||||||
|
obj.decided_at = timezone.now()
|
||||||
|
if not obj.decided_by_id and request.user and request.user.is_authenticated:
|
||||||
|
obj.decided_by = request.user
|
||||||
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
|
def delete_link(self, obj: AccessRequest):
|
||||||
|
url = reverse("admin:access_accessrequest_delete", args=[obj.pk])
|
||||||
|
return format_html('<a class="text-red-600" href="{}">Delete</a>', url)
|
||||||
|
|
||||||
|
delete_link.short_description = "Delete"
|
||||||
|
|||||||
26
app/apps/access/permissions.py
Normal file
26
app/apps/access/permissions.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.utils import timezone
|
||||||
|
from guardian.shortcuts import assign_perm, remove_perm
|
||||||
|
|
||||||
|
from .models import AccessRequest
|
||||||
|
|
||||||
|
|
||||||
|
def sync_server_view_perm(access_request: AccessRequest) -> None:
|
||||||
|
if not access_request or not access_request.requester_id or not access_request.server_id:
|
||||||
|
return
|
||||||
|
now = timezone.now()
|
||||||
|
has_valid_access = (
|
||||||
|
AccessRequest.objects.filter(
|
||||||
|
requester_id=access_request.requester_id,
|
||||||
|
server_id=access_request.server_id,
|
||||||
|
status=AccessRequest.Status.APPROVED,
|
||||||
|
)
|
||||||
|
.filter(Q(expires_at__isnull=True) | Q(expires_at__gt=now))
|
||||||
|
.exists()
|
||||||
|
)
|
||||||
|
if has_valid_access:
|
||||||
|
assign_perm("servers.view_server", access_request.requester, access_request.server)
|
||||||
|
return
|
||||||
|
remove_perm("servers.view_server", access_request.requester, access_request.server)
|
||||||
@@ -6,11 +6,13 @@ from guardian.shortcuts import assign_perm
|
|||||||
|
|
||||||
from apps.core.rbac import assign_default_object_permissions
|
from apps.core.rbac import assign_default_object_permissions
|
||||||
from .models import AccessRequest
|
from .models import AccessRequest
|
||||||
|
from .permissions import sync_server_view_perm
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=AccessRequest)
|
@receiver(post_save, sender=AccessRequest)
|
||||||
def assign_access_request_perms(sender, instance: AccessRequest, created: bool, **kwargs) -> None:
|
def assign_access_request_perms(sender, instance: AccessRequest, created: bool, **kwargs) -> None:
|
||||||
if not created:
|
if not created:
|
||||||
|
sync_server_view_perm(instance)
|
||||||
return
|
return
|
||||||
if instance.requester_id:
|
if instance.requester_id:
|
||||||
user = instance.requester
|
user = instance.requester
|
||||||
@@ -21,3 +23,4 @@ def assign_access_request_perms(sender, instance: AccessRequest, created: bool,
|
|||||||
):
|
):
|
||||||
assign_perm(perm, user, instance)
|
assign_perm(perm, user, instance)
|
||||||
assign_default_object_permissions(instance)
|
assign_default_object_permissions(instance)
|
||||||
|
sync_server_view_perm(instance)
|
||||||
|
|||||||
27
app/apps/access/tasks.py
Normal file
27
app/apps/access/tasks.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from celery import shared_task
|
||||||
|
from django.db import transaction
|
||||||
|
from django.utils import timezone
|
||||||
|
from .models import AccessRequest
|
||||||
|
from .permissions import sync_server_view_perm
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def expire_access_requests() -> int:
|
||||||
|
now = timezone.now()
|
||||||
|
expired_qs = AccessRequest.objects.select_related("server", "requester").filter(
|
||||||
|
status=AccessRequest.Status.APPROVED,
|
||||||
|
expires_at__isnull=False,
|
||||||
|
expires_at__lte=now,
|
||||||
|
)
|
||||||
|
count = 0
|
||||||
|
for access_request in expired_qs:
|
||||||
|
with transaction.atomic():
|
||||||
|
access_request.status = AccessRequest.Status.EXPIRED
|
||||||
|
access_request.decided_at = now
|
||||||
|
access_request.decided_by = None
|
||||||
|
access_request.save(update_fields=["status", "decided_at", "decided_by"])
|
||||||
|
sync_server_view_perm(access_request)
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from guardian.admin import GuardedModelAdmin
|
try:
|
||||||
|
from unfold.contrib.guardian.admin import GuardedModelAdmin
|
||||||
|
except ImportError: # Fallback for older Unfold builds without guardian admin shim.
|
||||||
|
from guardian.admin import GuardedModelAdmin as GuardianGuardedModelAdmin
|
||||||
|
from unfold.admin import ModelAdmin as UnfoldModelAdmin
|
||||||
|
|
||||||
|
class GuardedModelAdmin(GuardianGuardedModelAdmin, UnfoldModelAdmin):
|
||||||
|
pass
|
||||||
|
|
||||||
from .models import SSHKey
|
from .models import SSHKey
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from guardian.admin import GuardedModelAdmin
|
try:
|
||||||
|
from unfold.contrib.guardian.admin import GuardedModelAdmin
|
||||||
|
except ImportError: # Fallback for older Unfold builds without guardian admin shim.
|
||||||
|
from guardian.admin import GuardedModelAdmin as GuardianGuardedModelAdmin
|
||||||
|
from unfold.admin import ModelAdmin as UnfoldModelAdmin
|
||||||
|
|
||||||
|
class GuardedModelAdmin(GuardianGuardedModelAdmin, UnfoldModelAdmin):
|
||||||
|
pass
|
||||||
|
|
||||||
from .models import AgentCertificateAuthority, EnrollmentToken, Server
|
from .models import AgentCertificateAuthority, EnrollmentToken, Server
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,25 @@ from django.db.models import Q
|
|||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from guardian.shortcuts import get_objects_for_user
|
||||||
|
|
||||||
from apps.access.models import AccessRequest
|
from apps.access.models import AccessRequest
|
||||||
|
from apps.servers.models import Server
|
||||||
|
|
||||||
|
|
||||||
@login_required(login_url="/accounts/login/")
|
@login_required(login_url="/accounts/login/")
|
||||||
def dashboard(request):
|
def dashboard(request):
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
|
if request.user.has_perm("servers.view_server"):
|
||||||
|
server_qs = Server.objects.all()
|
||||||
|
else:
|
||||||
|
server_qs = get_objects_for_user(
|
||||||
|
request.user,
|
||||||
|
"servers.view_server",
|
||||||
|
klass=Server,
|
||||||
|
accept_global_perms=False,
|
||||||
|
)
|
||||||
|
|
||||||
access_qs = (
|
access_qs = (
|
||||||
AccessRequest.objects.select_related("server")
|
AccessRequest.objects.select_related("server")
|
||||||
.filter(
|
.filter(
|
||||||
@@ -19,22 +31,24 @@ def dashboard(request):
|
|||||||
status=AccessRequest.Status.APPROVED,
|
status=AccessRequest.Status.APPROVED,
|
||||||
)
|
)
|
||||||
.filter(Q(expires_at__isnull=True) | Q(expires_at__gt=now))
|
.filter(Q(expires_at__isnull=True) | Q(expires_at__gt=now))
|
||||||
.order_by("-requested_at")
|
|
||||||
)
|
)
|
||||||
|
expires_map = {}
|
||||||
seen = set()
|
|
||||||
servers = []
|
|
||||||
for access in access_qs:
|
for access in access_qs:
|
||||||
if access.server_id in seen:
|
expires_at = access.expires_at
|
||||||
continue
|
current = expires_map.get(access.server_id)
|
||||||
seen.add(access.server_id)
|
if current is None or expires_at is None:
|
||||||
servers.append(
|
expires_map[access.server_id] = None
|
||||||
|
elif current and expires_at and expires_at > current:
|
||||||
|
expires_map[access.server_id] = expires_at
|
||||||
|
|
||||||
|
servers = [
|
||||||
{
|
{
|
||||||
"server": access.server,
|
"server": server,
|
||||||
"expires_at": access.expires_at,
|
"expires_at": expires_map.get(server.id),
|
||||||
"last_accessed": None,
|
"last_accessed": None,
|
||||||
}
|
}
|
||||||
)
|
for server in server_qs
|
||||||
|
]
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"servers": servers,
|
"servers": servers,
|
||||||
@@ -45,9 +59,17 @@ def dashboard(request):
|
|||||||
@login_required(login_url="/accounts/login/")
|
@login_required(login_url="/accounts/login/")
|
||||||
def detail(request, server_id: int):
|
def detail(request, server_id: int):
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
|
try:
|
||||||
|
server = Server.objects.get(id=server_id)
|
||||||
|
except Server.DoesNotExist:
|
||||||
|
raise Http404("Server not found")
|
||||||
|
if not request.user.has_perm("servers.view_server", server) and not request.user.has_perm(
|
||||||
|
"servers.view_server"
|
||||||
|
):
|
||||||
|
raise Http404("Server not found")
|
||||||
|
|
||||||
access = (
|
access = (
|
||||||
AccessRequest.objects.select_related("server")
|
AccessRequest.objects.filter(
|
||||||
.filter(
|
|
||||||
requester=request.user,
|
requester=request.user,
|
||||||
server_id=server_id,
|
server_id=server_id,
|
||||||
status=AccessRequest.Status.APPROVED,
|
status=AccessRequest.Status.APPROVED,
|
||||||
@@ -56,12 +78,10 @@ def detail(request, server_id: int):
|
|||||||
.order_by("-requested_at")
|
.order_by("-requested_at")
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
if not access:
|
|
||||||
raise Http404("Server not found")
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"server": access.server,
|
"server": server,
|
||||||
"expires_at": access.expires_at,
|
"expires_at": access.expires_at if access else None,
|
||||||
"last_accessed": None,
|
"last_accessed": None,
|
||||||
}
|
}
|
||||||
return render(request, "servers/detail.html", context)
|
return render(request, "servers/detail.html", context)
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .celery import app as celery_app
|
||||||
|
|
||||||
|
__all__ = ("celery_app",)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from pydantic import Field
|
|||||||
from apps.access.models import AccessRequest
|
from apps.access.models import AccessRequest
|
||||||
from apps.core.rbac import require_authenticated
|
from apps.core.rbac import require_authenticated
|
||||||
from apps.servers.models import Server
|
from apps.servers.models import Server
|
||||||
|
from apps.access.permissions import sync_server_view_perm
|
||||||
|
|
||||||
|
|
||||||
class AccessRequestCreateIn(Schema):
|
class AccessRequestCreateIn(Schema):
|
||||||
@@ -191,6 +192,7 @@ def build_router() -> Router:
|
|||||||
else:
|
else:
|
||||||
access_request.decided_by = None
|
access_request.decided_by = None
|
||||||
access_request.save()
|
access_request.save()
|
||||||
|
sync_server_view_perm(access_request)
|
||||||
return _request_to_out(access_request)
|
return _request_to_out(access_request)
|
||||||
|
|
||||||
@router.delete("/{request_id}", response={204: None})
|
@router.delete("/{request_id}", response={204: None})
|
||||||
@@ -208,6 +210,7 @@ def build_router() -> Router:
|
|||||||
if not request.user.has_perm("access.delete_accessrequest", access_request):
|
if not request.user.has_perm("access.delete_accessrequest", access_request):
|
||||||
raise HttpError(403, "Forbidden")
|
raise HttpError(403, "Forbidden")
|
||||||
access_request.delete()
|
access_request.delete()
|
||||||
|
sync_server_view_perm(access_request)
|
||||||
return 204, None
|
return 204, None
|
||||||
|
|
||||||
return router
|
return router
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from django.http import HttpRequest
|
|||||||
from ninja import File, Form, Router, Schema
|
from ninja import File, Form, Router, Schema
|
||||||
from ninja.files import UploadedFile
|
from ninja.files import UploadedFile
|
||||||
from ninja.errors import HttpError
|
from ninja.errors import HttpError
|
||||||
|
from guardian.shortcuts import get_objects_for_user
|
||||||
from apps.core.rbac import require_perms
|
from apps.core.rbac import require_perms
|
||||||
from apps.servers.models import Server
|
from apps.servers.models import Server
|
||||||
|
|
||||||
@@ -42,7 +43,15 @@ def build_router() -> Router:
|
|||||||
def list_servers(request: HttpRequest):
|
def list_servers(request: HttpRequest):
|
||||||
"""List servers visible to authenticated users."""
|
"""List servers visible to authenticated users."""
|
||||||
require_perms(request, "servers.view_server")
|
require_perms(request, "servers.view_server")
|
||||||
|
if request.user.has_perm("servers.view_server"):
|
||||||
servers = Server.objects.all()
|
servers = Server.objects.all()
|
||||||
|
else:
|
||||||
|
servers = get_objects_for_user(
|
||||||
|
request.user,
|
||||||
|
"servers.view_server",
|
||||||
|
klass=Server,
|
||||||
|
accept_global_perms=False,
|
||||||
|
)
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"id": s.id,
|
"id": s.id,
|
||||||
@@ -64,6 +73,10 @@ def build_router() -> Router:
|
|||||||
server = Server.objects.get(id=server_id)
|
server = Server.objects.get(id=server_id)
|
||||||
except Server.DoesNotExist:
|
except Server.DoesNotExist:
|
||||||
raise HttpError(404, "Not Found")
|
raise HttpError(404, "Not Found")
|
||||||
|
if not request.user.has_perm("servers.view_server", server) and not request.user.has_perm(
|
||||||
|
"servers.view_server"
|
||||||
|
):
|
||||||
|
raise HttpError(403, "Forbidden")
|
||||||
return {
|
return {
|
||||||
"id": server.id,
|
"id": server.id,
|
||||||
"display_name": server.display_name,
|
"display_name": server.display_name,
|
||||||
|
|||||||
9
app/keywarden/celery.py
Normal file
9
app/keywarden/celery.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from celery import Celery
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "keywarden.settings.dev")
|
||||||
|
|
||||||
|
app = Celery("keywarden")
|
||||||
|
app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||||
|
app.autodiscover_tasks()
|
||||||
@@ -96,6 +96,19 @@ SESSION_CACHE_ALIAS = "default"
|
|||||||
|
|
||||||
KEYWARDEN_AGENT_CERT_VALIDITY_DAYS = int(os.getenv("KEYWARDEN_AGENT_CERT_VALIDITY_DAYS", "90"))
|
KEYWARDEN_AGENT_CERT_VALIDITY_DAYS = int(os.getenv("KEYWARDEN_AGENT_CERT_VALIDITY_DAYS", "90"))
|
||||||
|
|
||||||
|
CELERY_BROKER_URL = os.getenv("KEYWARDEN_CELERY_BROKER_URL", REDIS_URL)
|
||||||
|
CELERY_RESULT_BACKEND = os.getenv("KEYWARDEN_CELERY_RESULT_BACKEND", REDIS_URL)
|
||||||
|
CELERY_ACCEPT_CONTENT = ["json"]
|
||||||
|
CELERY_TASK_SERIALIZER = "json"
|
||||||
|
CELERY_RESULT_SERIALIZER = "json"
|
||||||
|
CELERY_TIMEZONE = "UTC"
|
||||||
|
CELERY_BEAT_SCHEDULE = {
|
||||||
|
"expire-access-requests": {
|
||||||
|
"task": "apps.access.tasks.expire_access_requests",
|
||||||
|
"schedule": 60.0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
PASSWORD_HASHERS = [
|
PASSWORD_HASHERS = [
|
||||||
"django.contrib.auth.hashers.Argon2PasswordHasher",
|
"django.contrib.auth.hashers.Argon2PasswordHasher",
|
||||||
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
|
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
|
||||||
@@ -159,9 +172,6 @@ UNFOLD = {
|
|||||||
"/static/unfold/css/simplebar.css",
|
"/static/unfold/css/simplebar.css",
|
||||||
(lambda request: "/static/unfold/css/keywarden.css"),
|
(lambda request: "/static/unfold/css/keywarden.css"),
|
||||||
],
|
],
|
||||||
"SCRIPTS": [
|
|
||||||
"/static/unfold/js/simplebar.js",
|
|
||||||
],
|
|
||||||
"TABS": [
|
"TABS": [
|
||||||
{
|
{
|
||||||
"models": [
|
"models": [
|
||||||
|
|||||||
@@ -83,4 +83,17 @@ html:not(.dark) .kw-pill {
|
|||||||
.kw-warn { color: #fdba74; } /* orange-300 */
|
.kw-warn { color: #fdba74; } /* orange-300 */
|
||||||
.kw-bad { color: #fca5a5; } /* red-300 */
|
.kw-bad { color: #fca5a5; } /* red-300 */
|
||||||
|
|
||||||
|
/* Align related-object action icons with their fields in admin forms. */
|
||||||
|
.related-widget-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-widget-wrapper select,
|
||||||
|
.related-widget-wrapper .select2-container {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ stderr_logfile=/dev/stderr
|
|||||||
stderr_logfile_maxbytes=0
|
stderr_logfile_maxbytes=0
|
||||||
stopsignal=QUIT
|
stopsignal=QUIT
|
||||||
|
|
||||||
|
# REMOVE FOR PROD, should be using seperate worker instances for scalability.
|
||||||
[program:valkey]
|
[program:valkey]
|
||||||
command=/usr/bin/valkey-server --bind 127.0.0.1 --port 6379 --save "" --appendonly no
|
command=/usr/bin/valkey-server --bind 127.0.0.1 --port 6379 --save "" --appendonly no
|
||||||
autostart=true
|
autostart=true
|
||||||
@@ -36,3 +37,31 @@ stdout_logfile_maxbytes=0
|
|||||||
stderr_logfile=/dev/stderr
|
stderr_logfile=/dev/stderr
|
||||||
stderr_logfile_maxbytes=0
|
stderr_logfile_maxbytes=0
|
||||||
stopsignal=TERM
|
stopsignal=TERM
|
||||||
|
|
||||||
|
[program:celery-worker]
|
||||||
|
command=/usr/local/bin/celery -A keywarden worker -l info
|
||||||
|
directory=/app
|
||||||
|
user=djangouser
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
stopsignal=TERM
|
||||||
|
stopasgroup=true
|
||||||
|
killasgroup=true
|
||||||
|
|
||||||
|
[program:celery-beat]
|
||||||
|
command=/usr/local/bin/celery -A keywarden beat -l info
|
||||||
|
directory=/app
|
||||||
|
user=djangouser
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
stopsignal=TERM
|
||||||
|
stopasgroup=true
|
||||||
|
killasgroup=true
|
||||||
|
|||||||
Reference in New Issue
Block a user