community/front-end/ofe/website/ghpcfe/views/applications.py (359 lines of code) (raw):
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
""" applications.py """
import logging
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.urls import reverse_lazy
from django.views import generic
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from ..cluster_manager import c2
from ..cluster_manager import spack
from ..cluster_manager import utils
from ..cluster_manager.clusterinfo import ClusterInfo
from ..forms import ApplicationEditForm
from ..forms import ApplicationForm
from ..forms import CustomInstallationApplicationForm
from ..forms import SpackApplicationForm
from ..models import Application
from ..models import Cluster
from ..models import CustomInstallationApplication
from ..models import SpackApplication
from ..serializers import ApplicationSerializer
from .view_utils import GCSFile
from .view_utils import StreamingFileView
logger = logging.getLogger(__name__)
class ApplicationListView(LoginRequiredMixin, generic.ListView):
"""Custom ListView for Application model"""
model = Application
template_name = "application/list.html"
def get_queryset(self):
queryset = super().get_queryset()
if self.request.user.has_admin_role():
pass
else:
wanted_items = set()
for application in queryset:
cluster = application.cluster
if (
self.request.user in cluster.authorised_users.all()
and cluster.status == "r"
and application.status == "r"
):
wanted_items.add(application.pk)
queryset = queryset.filter(pk__in=wanted_items)
for item in queryset:
if hasattr(item, "spackapplication"):
item.type = "spack"
elif hasattr(item, "custominstallationapplication"):
item.type = "custom"
else:
item.type = "pre-installed"
return queryset
def get_context_data(self, *args, **kwargs):
loading = 0
for application in Application.objects.all():
if application.status in ["p", "q", "i"]:
loading = 1
break
context = super().get_context_data(*args, **kwargs)
context["loading"] = loading
context["navtab"] = "application"
short_status_messages = {
"n": "Newly configured",
"p": "Being prepared",
"q": "Queueing",
"i": "Being installed",
"r": "Installed and ready",
"e": "Installation failed",
"x": "Cluster destroyed",
}
context["status_messages"] = short_status_messages
return context
class ApplicationDetailView(generic.DetailView):
"""Custom DetailView for Application model"""
model = Application
template_name = "application/detail.html"
def get_template_names(self):
logger.debug(
"ApplicationDetailView: Object type: %s", type(self.get_object())
)
if hasattr(self.get_object(), "spackapplication"):
return ["application/spack_detail.html"]
return super().get_template_names()
def get_context_data(self, **kwargs):
admin_view = 0
if self.request.user.has_admin_role():
admin_view = 1
context = super().get_context_data(**kwargs)
if hasattr(self.get_object(), "spackapplication"):
spack_application = SpackApplication.objects.get(
pk=context["application"].id
)
context["application"].spack_spec = spack_application.spack_spec
load = context["application"].load_command
if load and load.startswith("spack load /"):
context["application"].spack_hash = load.split("/", 1)[1]
context["navtab"] = "application"
context["admin_view"] = admin_view
return context
class ApplicationCreateSelectView(LoginRequiredMixin, generic.ListView):
"""Custom view to select application install types"""
model = Cluster
template_name = "application/select_form.html"
def get_queryset(self):
queryset = super().get_queryset()
return queryset.filter(status="r")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["navtab"] = "application"
return context
def post(self, request):
"""Custom post handler to redirect based on application type"""
if request.POST["application-type"] == "spack":
itemtype = "application-create-spack-cluster"
elif request.POST["application-type"] == "custom":
itemtype = "application-create-install"
elif request.POST["application-type"] == "installed":
itemtype = "application-create"
return HttpResponseRedirect(
reverse(itemtype, kwargs={"cluster": request.POST["cluster"]})
)
class ApplicationCreateView(LoginRequiredMixin, generic.CreateView):
"""Custom CreateView for Application model"""
success_url = reverse_lazy("applications")
template_name = "application/create_form.html"
form_class = ApplicationForm
def get_initial(self):
return {"cluster": Cluster.objects.get(pk=self.kwargs["cluster"])}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["cluster"] = Cluster.objects.get(pk=self.kwargs["cluster"])
context["navtab"] = "application"
return context
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.status = "r"
cluster = ClusterInfo(self.object.cluster)
self.object.install_loc = cluster.get_app_install_loc(
form.cleaned_data["installation_path"]
)
self.object.save()
return HttpResponseRedirect(self.get_success_url())
class CustomInstallationApplicationCreateView(LoginRequiredMixin, generic.CreateView): # pylint: disable=line-too-long
"""CreateView for Custom Installation of Application"""
template_name = "application/custom_install_create_form.html"
form_class = CustomInstallationApplicationForm
def get_initial(self):
return {"cluster": Cluster.objects.get(pk=self.kwargs["cluster"])}
def get_context_data(self, **kwargs):
"""Perform extra query to populate instance types data"""
context = super().get_context_data(**kwargs)
context["cluster"] = Cluster.objects.get(pk=self.kwargs["cluster"])
context["navtab"] = "application"
return context
def get_success_url(self):
return reverse("application-detail", kwargs={"pk": self.object.pk})
def form_valid(self, form):
self.object = form.save(commit=False)
cluster = ClusterInfo(self.object.cluster)
self.object.install_loc = cluster.get_app_install_loc(
form.cleaned_data["install_loc"]
)
if form.cleaned_data["module_name"]:
self.object.load_command = (
f'module load {form.cleaned_data["module_name"]}'
)
self.object.save()
messages.success(
self.request,
f'Application "{self.object.name}" created in database. Click '
'"Install" button below to actually install it on cluster.',
)
return HttpResponseRedirect(self.get_success_url())
class SpackApplicationCreateView(LoginRequiredMixin, generic.CreateView):
"""Custom CreateView for Application model"""
# success_url = reverse_lazy('applications'})
template_name = "application/spack_create_form.html"
form_class = SpackApplicationForm
def get_initial(self):
return {"cluster": Cluster.objects.get(pk=self.kwargs["cluster"])}
def get_context_data(self, **kwargs):
"""Perform extra query to populate instance types data"""
context = super().get_context_data(**kwargs)
context["cluster"] = Cluster.objects.get(pk=self.kwargs["cluster"])
context["navtab"] = "application"
return context
def get_success_url(self):
return reverse("application-detail", kwargs={"pk": self.object.pk})
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.install_loc = self.object.cluster.spack_install
if self.object.version:
# We need to insert the version immediately following the app name
# and eventually support compiler...
self.object.spack_spec = (
f"@{self.object.version}"
f'{self.object.spack_spec if self.object.spack_spec else ""}'
)
# Check if install_partition is not null
if not self.object.install_partition:
messages.error(
self.request,
'Please select an "Install Partition" before saving the application.'
)
return self.form_invalid(form)
self.object.save()
form.save_m2m()
messages.success(
self.request,
f'Application "{self.object.name}" created in database. Click '
'"Spack install" button below to actually install it on cluster.',
)
return HttpResponseRedirect(self.get_success_url())
class ApplicationUpdateView(LoginRequiredMixin, generic.UpdateView):
"""Custom UpdateView for Application model"""
model = Application
template_name = "application/edit_form.html"
form_class = ApplicationEditForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["navtab"] = "application"
return context
def get_success_url(self):
return reverse_lazy("application-detail", kwargs={"pk": self.object.pk})
class ApplicationDeleteView(LoginRequiredMixin, generic.DeleteView):
"""Custom DeleteView for Application model"""
model = Application
success_url = reverse_lazy("applications")
template_name = "application/check_delete.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["navtab"] = "application"
return context
class ApplicationLogFileView(LoginRequiredMixin, StreamingFileView):
"""View for application installation logs"""
bucket = utils.load_config()["server"]["gcs_bucket"]
valid_logs = [
{
"title": "Installation Output",
"type": GCSFile,
"args": (bucket, "stdout"),
},
{
"title": "Installation Error Log",
"type": GCSFile,
"args": (bucket, "stderr"),
},
]
def _create_file_info_object(self, logfile_info, *args, **kwargs):
return logfile_info["type"](*logfile_info["args"], *args, **kwargs)
def get_file_info(self):
logid = self.kwargs.get("logid", -1)
application_id = self.kwargs.get("pk")
application = get_object_or_404(Application, pk=application_id)
cluster_id = application.cluster.id
bucket_prefix = f"clusters/{cluster_id}/installs/{application_id}"
entry = self.valid_logs[logid]
return self._create_file_info_object(entry, *[bucket_prefix])
class ApplicationLogView(LoginRequiredMixin, generic.DetailView):
"""View to display application log files"""
model = Application
template_name = "application/log.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["log_files"] = [
{"id": n, "title": entry["title"]}
for n, entry in enumerate(ApplicationLogFileView.valid_logs)
]
context["navtab"] = "application"
return context
# For APIs
class ApplicationViewSet(viewsets.ModelViewSet):
"""Custom ModelViewSet for Application model"""
permission_classes = (IsAuthenticated,)
queryset = Application.objects.all().order_by("name")
serializer_class = ApplicationSerializer
class SpackPackageViewSet(LoginRequiredMixin, viewsets.ViewSet):
"""Download a list of Spack packages available"""
def list(self, request):
return Response(spack.get_package_list())
def retrieve(self, request, pk=None):
pkgs = spack.get_package_list()
if pk in pkgs:
return Response(spack.get_package_info([pk]))
return Response("Package Not Found", status=404)
# Other supporting views
class BackendCustomAppInstall(LoginRequiredMixin, generic.View):
"""Backend logic to launch a custom app installation"""
def get(self, request, pk):
app = get_object_or_404(CustomInstallationApplication, pk=pk)
app.status = "p"
app.save()
cluster_id = app.cluster.id
def response(message):
if message.get("cluster_id") != cluster_id:
logger.error(
"Cluster ID mismatch to callback: expected %s, received %s",
pk,
message.get("cluster_id"),
)
if message.get("app_id") != pk:
logger.error(
"Application ID mismatch to callback: expected %s, "
"received %s",
pk,
message.get("app_id"),
)
if "log_message" in message:
logger.info("Install log message: %s", message["log_message"])
app = Application.objects.get(pk=pk)
app.status = message["status"]
if message["status"] == "r":
# TODO App was installed. Should have more attributes to set
pass
app.save()
c2.send_command(
cluster_id,
"INSTALL_APPLICATION",
on_response=response,
data={
"app_id": app.id,
"name": app.name,
"install_script": app.install_script,
"module_name": app.module_name,
"module_script": app.module_script,
"partition": app.install_partition.name,
},
)
return HttpResponseRedirect(
reverse("application-detail", kwargs={"pk": pk})
)
class BackendSpackInstall(LoginRequiredMixin, generic.View):
"""Backend logic to launch app installation via Spack"""
def get(self, request, pk):
app = get_object_or_404(SpackApplication, pk=pk)
app.status = "p"
app.save()
cluster_id = app.cluster.id
def response(message):
if message.get("cluster_id") != cluster_id:
logger.error(
"Cluster ID mismatch versus callback: expected %s, "
"received %s",
pk,
message.get("cluster_id"),
)
if message.get("app_id") != pk:
logger.error(
"Application ID mismatch versus callback: expected %s, "
"received %s",
pk,
message.get("app_id"),
)
if "log_message" in message:
logger.info("Install log message: %s", message["log_message"])
app = Application.objects.get(pk=pk)
app.status = message["status"]
if message["status"] == "r":
# App was installed. Should have more attributes to set
app.spack_hash = message.get("spack_hash", "")
app.load_command = message.get("load_command", "")
app.installed_architecture = message.get("spack_arch", "")
app.compiler = message.get("compiler", "")
app.mpi = message.get("mpi", "")
app.save()
c2.send_command(
cluster_id,
"SPACK_INSTALL",
on_response=response,
data={
"app_id": app.id,
"name": app.spack_name,
"spec": app.spack_spec,
"partition": app.install_partition.name,
"extra_sbatch": [f"--gpus={app.install_partition.GPU_per_node}"]
if app.install_partition.GPU_per_node
else [],
},
)
return HttpResponseRedirect(
reverse("application-detail", kwargs={"pk": pk})
)