Skip to content

Commit

Permalink
Merge pull request #30 from kthcloud/23-gpu-booking
Browse files Browse the repository at this point in the history
23-gpu-booking
  • Loading branch information
SimoneBendazzoli93 authored Feb 9, 2025
2 parents b415308 + b56c932 commit fcce02c
Show file tree
Hide file tree
Showing 14 changed files with 529 additions and 2 deletions.
2 changes: 1 addition & 1 deletion MAIA_scripts/MAIA_install_core_toolkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def install_maia_core_toolkit(maia_config_file, cluster_config, config_folder):

if "argocd_destination_cluster_address" in cluster_config_dict and not cluster_config_dict["argocd_destination_cluster_address"].endswith("/k8s/clusters/local"):
cluster_address = cluster_config_dict["argocd_destination_cluster_address"]
if cluster_config_dict["argocd_destination_cluster_address"] is not "https://kubernetes.default.svc":
if cluster_config_dict["argocd_destination_cluster_address"] != "https://kubernetes.default.svc":
project_id += f"-{cluster_config_dict['cluster_name']}"
else:
cluster_address = "https://kubernetes.default.svc"
Expand Down
Empty file.
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})
120 changes: 120 additions & 0 deletions dashboard/apps/templates/accounts/gpu_booking.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
{% extends 'layouts/base-fullscreen.html' %}

{% block title %} Sign UP {% endblock title %}

<!-- Specific CSS goes HERE -->
{% block stylesheets %}{% endblock stylesheets %}

{% block content %}

<div class="container position-sticky z-index-sticky top-1">
<div class="row">
<div class="col-12">

{% include "includes/navigation-fullscreen.html" %}

</div>
</div>
</div>

<main class="main-content mt-0"></main>

<section class="min-vh-100 mb-8">
<div class="page-header align-items-start min-vh-50 pt-5 pb-11 m-3 border-radius-lg" style="background-image: url('{{ ASSETS_ROOT }}/img/shutterstock/shutterstock_1941321493.jpg');">
<span class="mask bg-gradient-dark opacity-6"></span>
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-5 text-center mx-auto">
<h1 class="text-white mb-2 mt-5">GPU Booking System</h1>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row mt-lg-n10 mt-md-n11 mt-n10">
<div class="col-xl-4 col-lg-5 col-md-7 mx-auto">
<div class="card z-index-0">

<div class="card-header text-center pt-4">
<h5>Register a New GPU Booking</h5>
</div>

<div class="row px-xl-5 px-sm-4 px-3">

<div class="mt-2 position-relative text-center">
</div>

</div>

<div class="card-body">

{% if success %}

<p class="text-sm mt-3 mb-0 text-center">
<div class="text-center">
<a href="{% url 'login' %}" class="btn bg-gradient-dark w-100 my-4 mb-2">Sign IN</a>
</div>
</p>

{% else %}

<form role="form text-left" method="post" action="" enctype="multipart/form-data">

{% csrf_token %}

<div class="mb-3">
<label>Project Name</label>
{{ form.namespace }}
<span class="text-danger">{{ form.namespace.errors }}</span>
</div>

<div class="mb-3">
<label>Email</label>
{{ form.user_email }}
<span class="text-danger">{{ form.email.errors }}</span>
</div>

<div class="mb-3">
<label>GPU Request</label>
{{ form.gpu }}
<span class="text-danger">{{ form.gpu.errors }}</span>
</div>

<div class="mb-3">
<label>Booking GPU from</label>
{{ form.start_date }}
<span class="text-danger">{{ form.start_date.errors }}</span>
</div>





<div class="mb-3">
<label>Booking GPU until</label>
{{ form.end_date }}
<span class="text-danger">{{ form.end_date.errors }}</span>

<div class="text-center">
<button type="submit" name="register" class="btn bg-gradient-dark w-100 my-4 mb-2">Request GPU Booking</button>
</div>

</form>

{% endif %}

</div>
</div>
</div>
</div>
</div>
</section>

{% include "includes/footer-fullscreen.html" %}

</main>

{% endblock content %}

<!-- Specific JS goes HERE -->
{% block javascripts %}{% endblock javascripts %}
Loading

0 comments on commit fcce02c

Please sign in to comment.