Skip to content

Commit

Permalink
Add GPU scheduling app with models, forms, views, and URLs
Browse files Browse the repository at this point in the history
  • Loading branch information
SimoneBendazzoli93 committed Feb 9, 2025
1 parent b415308 commit 21739ca
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 1 deletion.
5 changes: 5 additions & 0 deletions dashboard/apps/gpu_scheduler/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.contrib import admin

# Register your models here.

from .models import GPUBooking
72 changes: 72 additions & 0 deletions dashboard/apps/gpu_scheduler/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from django import forms

from .models import GPUBooking
from django.conf import settings
from MAIA.dashboard_utils import get_groups_in_keycloak, get_pending_projects


class GPUBookingForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(GPUBookingForm, self).__init__(*args, **kwargs)
maia_groups = get_groups_in_keycloak(settings= settings)
pending_projects = get_pending_projects(settings=settings)

for pending_project in pending_projects:
maia_groups[pending_project] = pending_project + " (Pending)"

self.fields['namespace'] = forms.ChoiceField(
choices=[(maia_group,maia_group) for maia_group in maia_groups.values()],
widget=forms.Select(attrs={
'class': "form-select text-center fw-bold",
'style': 'max-width: auto;',
}
)
)
self.fields['user_email'] = forms.EmailField(
widget=forms.EmailInput(
attrs={
"placeholder": "Your Email.",
"class": "form-control"
}
))

self.fields['gpu'] = forms.ChoiceField(
choices=[(gpu,gpu) for gpu in settings.GPU_LIST],
widget=forms.Select(attrs={
'class': "form-select text-center fw-bold",
'style': 'max-width: auto;',
})
)

self.fields['start_date'] = forms.DateField(widget=forms.TextInput(attrs={'class': 'form-control', 'type': 'date'}))

self.fields['end_date'] = forms.DateField(widget=forms.TextInput(attrs={'class': 'form-control', 'type': 'date'}))

def clean(self):
cleaned_data = super().clean()
start_date = cleaned_data.get("start_date")
end_date = cleaned_data.get("end_date")
user_email = cleaned_data.get("user_email")

if start_date and end_date:
if end_date <= start_date:
self.add_error('end_date', "End date must be after start date.")
elif (end_date - start_date).days > 60:
self.add_error('end_date', "The maximum booking duration is 60 days.")
else:
# Check existing bookings for the same user
existing_bookings = GPUBooking.objects.filter(user_email=user_email)
total_days = 0
for booking in existing_bookings:
total_days += (booking.end_date - booking.start_date).days

if total_days > 60:
self.add_error('end_date', "The total booking duration for this user exceeds 60 days.")

return cleaned_data



class Meta:
model = GPUBooking
fields = ('namespace','gpu', 'user_email','start_date', 'end_date')
10 changes: 10 additions & 0 deletions dashboard/apps/gpu_scheduler/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.db import models

class GPUBooking(models.Model):
class Meta:
app_label = 'gpu_scheduler'
user_email = models.CharField(max_length=255, )
start_date = models.DateTimeField()
end_date = models.DateTimeField()
gpu = models.CharField(max_length=255)
namespace = models.CharField(max_length=255)
3 changes: 3 additions & 0 deletions dashboard/apps/gpu_scheduler/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.test import TestCase

# Create your tests here.
16 changes: 16 additions & 0 deletions dashboard/apps/gpu_scheduler/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# -*- encoding: utf-8 -*-
"""
Copyright (c) 2019 - present AppSeed.us
"""

from django.urls import path


from django.urls import path
from .views import GPUSchedulabilityAPIView, book_gpu, gpu_booking_info # Import your API view

urlpatterns = [
path('gpu-schedulability/', GPUSchedulabilityAPIView.as_view(), name='gpu_schedulability'),
path('', book_gpu, name='gpu_booking_form'),
path('my-bookings/', gpu_booking_info, name='book_gpu'),
]
127 changes: 127 additions & 0 deletions dashboard/apps/gpu_scheduler/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
from django.shortcuts import render
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from .models import GPUBooking
from django.conf import settings
from datetime import datetime, timezone
from .forms import GPUBookingForm
from MAIA.dashboard_utils import get_namespaces


@method_decorator(csrf_exempt, name='dispatch') # 🚀 This disables CSRF for this API
class GPUSchedulabilityAPIView(APIView):
permission_classes = [AllowAny] # 🚀 Allow requests without authentication or CSRF

def post(self, request, *args, **kwargs):
try:
user_email = request.data.get("user_email")
if not user_email:
return Response({"error": "Missing user_email"}, status=400)

secret_token = request.data.get("token")
if not secret_token or secret_token != settings.SECRET_KEY:
return Response({"error": "Invalid or missing secret token"}, status=403)

if "booking" in request.data:
booking_data = request.data["booking"]

# Calculate the total number of days for existing bookings
existing_bookings = GPUBooking.objects.filter(user_email=user_email)
total_days = sum(
(booking.end_date - booking.start_date).days
for booking in existing_bookings
)

# Calculate the number of days for the new booking
ending_time = datetime.strptime(booking_data["ending_time"], "%Y-%m-%d %H:%M:%S")
starting_time = datetime.strptime(booking_data["starting_time"], "%Y-%m-%d %H:%M:%S")
new_booking_days = (ending_time - starting_time).days

# Verify that the sum of existing bookings and the new booking does not exceed 60 days
if total_days + new_booking_days > 60:
return Response({"error": "Total booking days exceed the limit of 60 days"}, status=400)

# Create the new booking
GPUBooking.objects.create(
user_email=user_email,
start_date=booking_data["starting_time"],
end_date=booking_data["ending_time"]
)
return Response({"message": "Booking created successfully"})

try:
user_statuses = GPUBooking.objects.filter(user_email=user_email)

is_schedulable = False

current_time = datetime.now(timezone.utc)
is_schedulable = any(
status.start_date <= current_time and status.end_date >= current_time
for status in user_statuses
)
if is_schedulable:
status = next(status for status in user_statuses if status.start_date <= current_time and status.end_date >= current_time)
return Response({"schedulable": is_schedulable, "until": status.end_date})
else:
return Response({"schedulable": is_schedulable, "until": None})
except GPUBooking.DoesNotExist:
return Response({"schedulable": False, "until": None}) # Default to not schedulable if not found

except Exception as e:
return JsonResponse({"error": str(e)}, status=400)


@login_required(login_url="/maia/login/")
def book_gpu(request):
msg = None
success = False

email = request.user.email

id_token = request.session.get('oidc_id_token')
groups = request.user.groups.all()

namespaces = []

if request.user.is_superuser:
namespaces = get_namespaces(id_token, api_urls=settings.API_URL, private_clusters=settings.PRIVATE_CLUSTERS)

if namespaces is None or len(namespaces) == 0:
namespaces = []
for group in groups:
if str(group) != "MAIA:users":
namespaces.append(str(group).split(":")[-1].lower().replace("_", "-"))

initial_data = {'user_email': email, 'namespace': namespaces[0] if namespaces else None}

if request.method == "POST":
form = GPUBookingForm(request.POST, request.FILES)
if form.is_valid():
form.save()
msg = 'Request for GPU Booking submitted successfully.'
success = True
return redirect("/maia/gpu-booking/my-bookings/")
else:
print(form.errors)
msg = 'Form is not valid'
else:
form = GPUBookingForm(request.POST or None, request.FILES or None, initial=initial_data)
form.fields['namespace'].choices = [(ns, ns) for ns in namespaces]

return render(request, "accounts/gpu_booking.html", {"dashboard_version": settings.DASHBOARD_VERSION, "form": form, "msg": msg, "success": success})


@login_required(login_url="/maia/login/")
def gpu_booking_info(request):
bookings = GPUBooking.objects.filter(user_email=request.user.email)

total_days = 0
for booking in bookings:
total_days += (booking.end_date - booking.start_date).days
return render(request, "accounts/gpu_booking_info.html", {"dashboard_version": settings.DASHBOARD_VERSION, "bookings": bookings, "total_days": total_days})
16 changes: 16 additions & 0 deletions dashboard/apps/templates/includes/sidebar.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,22 @@

<span class="nav-link-text ms-1">Users</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link " href="/maia/gpu-booking/">
<div class="icon icon-shape icon-sm shadow border-radius-md bg-white text-center me-2 d-flex align-items-center justify-content-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M176 24c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 40c-35.3 0-64 28.7-64 64l-40 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l40 0 0 56-40 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l40 0 0 56-40 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l40 0c0 35.3 28.7 64 64 64l0 40c0 13.3 10.7 24 24 24s24-10.7 24-24l0-40 56 0 0 40c0 13.3 10.7 24 24 24s24-10.7 24-24l0-40 56 0 0 40c0 13.3 10.7 24 24 24s24-10.7 24-24l0-40c35.3 0 64-28.7 64-64l40 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-40 0 0-56 40 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-40 0 0-56 40 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-40 0c0-35.3-28.7-64-64-64l0-40c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 40-56 0 0-40c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 40-56 0 0-40zM160 128l192 0c17.7 0 32 14.3 32 32l0 192c0 17.7-14.3 32-32 32l-192 0c-17.7 0-32-14.3-32-32l0-192c0-17.7 14.3-32 32-32zm192 32l-192 0 0 192 192 0 0-192z"/></svg>
</div>
<span class="nav-link-text ms-1">Book a GPU</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link " href="/maia/gpu-booking/my-bookings/">
<div class="icon icon-shape icon-sm shadow border-radius-md bg-white text-center me-2 d-flex align-items-center justify-content-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M96 0C60.7 0 32 28.7 32 64l0 384c0 35.3 28.7 64 64 64l288 0c35.3 0 64-28.7 64-64l0-384c0-35.3-28.7-64-64-64L96 0zM208 288l64 0c44.2 0 80 35.8 80 80c0 8.8-7.2 16-16 16l-192 0c-8.8 0-16-7.2-16-16c0-44.2 35.8-80 80-80zm-32-96a64 64 0 1 1 128 0 64 64 0 1 1 -128 0zM512 80c0-8.8-7.2-16-16-16s-16 7.2-16 16l0 64c0 8.8 7.2 16 16 16s16-7.2 16-16l0-64zM496 192c-8.8 0-16 7.2-16 16l0 64c0 8.8 7.2 16 16 16s16-7.2 16-16l0-64c0-8.8-7.2-16-16-16zm16 144c0-8.8-7.2-16-16-16s-16 7.2-16 16l0 64c0 8.8 7.2 16 16 16s16-7.2 16-16l0-64z"/></svg>
</div>
<span class="nav-link-text ms-1">My GPU Bookings</span>
</a>
</li>
{% endif %}
{% for namespace in namespaces %}
Expand Down
1 change: 1 addition & 0 deletions dashboard/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
'apps.dyn_datatables',
'mozilla_django_oidc', # Load after auth
'bootstrap5',
"apps.gpu_scheduler",
]

MIDDLEWARE = [
Expand Down
6 changes: 5 additions & 1 deletion dashboard/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
path('admin/', admin.site.urls),
path('maia/resources',include("apps.resources.urls")),
path('maia/user-management/',include("apps.user_management.urls")),#path('app',include("apps.deploy_app.urls")),
path("maia/gpu-booking/", include("apps.gpu_scheduler.urls")), # Generic Routing
#path('deploy',include("apps.deploy.urls")),
path("maia/namespaces/", include("apps.namespaces.urls")),
# Django admin route
Expand All @@ -20,5 +21,8 @@
path('', include('apps.dyn_datatables.urls')), # Dynamic DB Routes

# Leave `Home.Urls` as last the last line
path("maia/", include("apps.home.urls")) # Generic Routing
path("maia/", include("apps.home.urls")), # Generic Routing
path("maia-api/", include("apps.gpu_scheduler.urls")), # Generic Routing


]
1 change: 1 addition & 0 deletions dashboard/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ helm repo update
git clone https://github.com/kthcloud/MAIA.git
pip install ./MAIA
python manage.py makemigrations authentication
python manage.py makemigrations gpu_scheduler
python manage.py makemigrations
python manage.py migrate

Expand Down

0 comments on commit 21739ca

Please sign in to comment.