Skip to content

Commit

Permalink
[#48] Finalizing sync job config & run the job
Browse files Browse the repository at this point in the history
  • Loading branch information
ifirmawan committed Feb 18, 2025
1 parent cf14bd2 commit 05852f4
Show file tree
Hide file tree
Showing 3 changed files with 266 additions and 2 deletions.
55 changes: 55 additions & 0 deletions backend/api/v1/v1_rundeck/serializers.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from datetime import datetime
from rest_framework import serializers
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from api.v1.v1_rundeck.models import Settings
from utils.custom_serializer_fields import (
CustomJSONField,
CustomCharField,
CustomIntegerField,
)


Expand Down Expand Up @@ -62,8 +66,59 @@ class Meta:
fields = ["id", "name", "permalink"]


class RundeckExecutionJobSerializer(serializers.Serializer):
id = CustomIntegerField()
permalink = CustomCharField()
status = CustomCharField()
date_started = serializers.SerializerMethodField()
date_ended = serializers.SerializerMethodField()
year_month = serializers.SerializerMethodField()

@extend_schema_field(OpenApiTypes.STR)
def get_date_started(self, instance):
return instance["date-started"]["date"]

@extend_schema_field(OpenApiTypes.STR)
def get_date_ended(self, instance):
if instance.get("date-ended"):
return instance["date-ended"]["date"]
return None

@extend_schema_field(OpenApiTypes.STR)
def get_year_month(self, instance):
if instance["job"].get("options").get("year_month"):
return instance["job"]["options"]["year_month"]
return None

class Meta:
fields = [
"id",
"permalink",
"status",
"date_started",
"date_ended",
"year_month",
]


class ContactsSerializer(serializers.Serializer):
contacts = CustomJSONField()

class Meta:
fields = ["contacts"]


class RundeckJobOptionsSerializer(serializers.Serializer):
year_month = serializers.CharField()

def validate_year_month(self, value):
try:
parsed_date = datetime.strptime(value, "%Y-%m")
return parsed_date.strftime("%Y-%m")
except ValueError:
raise serializers.ValidationError(
"Invalid date format. Use YYYY-MM."
)

class Meta:
fields = ["year_month"]
6 changes: 6 additions & 0 deletions backend/api/v1/v1_rundeck/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
SettingsViewSet,
RundeckProjectsAPI,
RundeckJobsAPI,
RundeckExecutionsAPI,
AdminContactView,
)

Expand Down Expand Up @@ -34,6 +35,11 @@
RundeckJobsAPI.as_view(),
name="rundeck-jobs"
),
re_path(
r"^(?P<version>(v1))/rundeck/job/(?P<job_id>[0-9a-zA-Z_-]+)/execs",
RundeckExecutionsAPI.as_view(),
name="rundeck-executions"
),
re_path(
r"^(?P<version>(v1))/admin/contacts",
AdminContactView.as_view(),
Expand Down
207 changes: 205 additions & 2 deletions backend/api/v1/v1_rundeck/views.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import requests
from rest_framework import viewsets, status
from rest_framework import viewsets, status, serializers
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
from rest_framework.response import Response
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import (
extend_schema,
inline_serializer,
OpenApiParameter,
)
from django.conf import settings
from utils.custom_permissions import IsAdmin
Expand All @@ -14,9 +17,22 @@
UpdateSettingsSerializer,
RundeckProjectSerializer,
RundeckJobSerializer,
RundeckExecutionJobSerializer,
RundeckJobOptionsSerializer,
ContactsSerializer,
)
from api.v1.v1_rundeck.constants import RundeckJobStatus
from api.v1.v1_users.models import SystemUser, UserRoleTypes
from utils.custom_serializer_fields import validate_serializers_message
from utils.rundeck_config import rundeck_set_notification


class RundeckAPIError(Exception):
"""Custom exception for third-party API failures."""
def __init__(self, message, status_code=None):
self.message = message
self.status_code = status_code
super().__init__(message)


@extend_schema(
Expand All @@ -38,6 +54,82 @@ def get_serializer_class(self):
return UpdateSettingsSerializer
return SettingsSerializer

def update(self, request, *args, **kwargs):
partial = kwargs.pop("partial", False)
instance = self.get_object()
serializer = self.get_serializer(
instance,
data=request.data,
partial=partial
)

if not serializer.is_valid():
return Response(
serializer.errors,
status=status.HTTP_400_BAD_REQUEST
)

try:
self.perform_update(serializer)
except RundeckAPIError as e:
return Response(
{"message": e.message},
status=e.status_code or status.HTTP_502_BAD_GATEWAY
)
return Response(serializer.data, status=status.HTTP_200_OK)

def perform_update(self, serializer):
try:
instance = serializer.save()
api_url = (
f"{settings.RUNDECK_API_URL}/project/"
f"{instance.project_name}/jobs/export"
)
response = requests.get(
api_url,
headers={
"X-Rundeck-Auth-Token": settings.RUNDECK_API_TOKEN
}
)
response.raise_for_status()
if response.status_code == 200:
configs = response.json()
configs = list(filter(
lambda x: x["id"] == f"{instance.job_id}",
configs
))
config = configs[0] if len(configs) > 0 else None
if config:
config = rundeck_set_notification(
config=config,
on_success_emails=instance.on_success_emails,
on_failure_emails=instance.on_failure_emails,
on_exceeded_emails=instance.on_exceeded_emails,
)
requests.delete(
f"{settings.RUNDECK_API_URL}/job/{instance.job_id}",
headers={
"X-Rundeck-Auth-Token": settings.RUNDECK_API_TOKEN
}
)
import_api_url = (
f"{settings.RUNDECK_API_URL}/project/"
f"{instance.project_name}/jobs/import"
)
response = requests.post(
import_api_url,
json=[config],
headers={
"X-Rundeck-Auth-Token": settings.RUNDECK_API_TOKEN
}
)
response.raise_for_status()
except requests.RequestException as e:
raise RundeckAPIError(
str(e),
status.HTTP_502_BAD_GATEWAY
)


class RundeckProjectsAPI(APIView):
permission_classes = [IsAuthenticated, IsAdmin]
Expand Down Expand Up @@ -87,7 +179,7 @@ class RundeckJobsAPI(APIView):
def get(self, request, version, project):
try:
response = requests.get(
f"{settings.RUNDECK_API_URL}/project/{project}/jobs",
f"{settings.RUNDECK_API_URL}/project/{[project]}/jobs",
headers={
"X-Rundeck-Auth-Token": settings.RUNDECK_API_TOKEN
}
Expand Down Expand Up @@ -135,3 +227,114 @@ def get(self, request, version):
},
status=status.HTTP_200_OK
)


class RundeckExecutionsAPI(APIView):
permission_classes = [IsAuthenticated, IsAdmin]

@extend_schema(
summary="Fetch Job Executions",
tags=["Rundeck"],
parameters=[
OpenApiParameter(
name="page",
default=1,
required=False,
type=OpenApiTypes.NUMBER,
location=OpenApiParameter.QUERY,
),
OpenApiParameter(
name="status",
default=None,
required=False,
enum=RundeckJobStatus.FieldStr.keys(),
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
),
],
responses={
(200, "application/json"): inline_serializer(
"DataList",
fields={
"current": serializers.IntegerField(),
"total": serializers.IntegerField(),
"total_page": serializers.IntegerField(),
"data": RundeckExecutionJobSerializer(many=True),
},
)
},
)
def get(self, request, version, job_id):
try:
page = int(request.GET.get("page", "1"))
page = page - 1
api_url = (
f"{settings.RUNDECK_API_URL}/job/{job_id}/executions"
f"?offset={page}"
)
job_status = request.GET.get("status", None)
if job_status:
api_url = f"{api_url}&status={job_status}"
response = requests.get(
api_url,
headers={
"X-Rundeck-Auth-Token": settings.RUNDECK_API_TOKEN
}
)
data = response.json()
return Response(
{
"current": page,
"total": data["paging"]["total"],
"total_page": data["paging"]["max"],
"data": RundeckExecutionJobSerializer(
instance=data["executions"],
many=True
).data
},
status=response.status_code,
)
except Exception as e:
return Response(
{"message": str(e)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)

@extend_schema(
summary="Run Job",
tags=["Rundeck"],
request=RundeckJobOptionsSerializer,
responses=RundeckExecutionJobSerializer,
)
def post(self, request, version, job_id):
try:
serializer = RundeckJobOptionsSerializer(
data=request.data
)
if not serializer.is_valid():
return Response(
{
"message": validate_serializers_message(
serializer.errors
)
},
status=status.HTTP_400_BAD_REQUEST,
)
response = requests.post(
f"{settings.RUNDECK_API_URL}/job/{job_id}/run",
{
"year_month": serializer.validated_data["year_month"]
},
headers={
"X-Rundeck-Auth-Token": settings.RUNDECK_API_TOKEN
}
)
return Response(
response.json(),
status=response.status_code,
)
except Exception as e:
return Response(
{"message": str(e)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)

0 comments on commit 05852f4

Please # to comment.