community/front-end/ofe/website/ghpcfe/views/workbench.py (295 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.
""" workbench.py """
import json
from collections import defaultdict
from asgiref.sync import sync_to_async
from django.shortcuts import get_object_or_404
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import redirect_to_login
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views import generic
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from django.contrib import messages
from django.db.models import Q
from django.db import transaction
from django.forms import inlineformset_factory
from ..models import (
Cluster,
Credential,
Workbench,
VirtualSubnet,
MountPoint,
WorkbenchMountPoint,
Filesystem,
FilesystemExport,
FilesystemImpl,
)
from ..forms import WorkbenchForm, WorkbenchMountPointForm
from ..cluster_manager import cloud_info
from ..cluster_manager.workbenchinfo import WorkbenchInfo
from .asyncview import BackendAsyncView
class WorkbenchListView(LoginRequiredMixin, generic.ListView):
"""Custom ListView for Cluster model"""
model = Workbench
template_name = "workbench/list.html"
def get_context_data(self, *args, **kwargs):
loading = 0
for cluster in Workbench.objects.all():
if cluster.status in ["c", "i", "t"]:
loading = 1
break
context = super().get_context_data(*args, **kwargs)
context["loading"] = loading
context["navtab"] = "workbench"
return context
class WorkbenchDetailView(LoginRequiredMixin, generic.DetailView):
"""Custom DetailView for Cluster model"""
model = Workbench
template_name = "workbench/detail.html"
def get_context_data(self, **kwargs):
"""Perform extra query to populate instance types data"""
context = super().get_context_data(**kwargs)
context["navtab"] = "workbench"
return context
class WorkbenchCreateView1(LoginRequiredMixin, generic.ListView):
"""Custom view for the first step of cluster creation"""
model = Credential
template_name = "credential/select_form.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["navtab"] = "workbench"
return context
def post(self, request):
return HttpResponseRedirect(
reverse(
"workbench-create2",
kwargs={"credential": request.POST["credential"]},
)
)
class WorkbenchCreateView2(LoginRequiredMixin, CreateView):
"""Custom CreateView for Workbench model"""
template_name = "workbench/create_form.html"
form_class = WorkbenchForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["cloud_credential"] = self.cloud_credential
kwargs["user"] = self.request.user
return kwargs
def get(self, request, *args, **kwargs):
self.cloud_credential = get_object_or_404(
Credential, pk=kwargs["credential"]
)
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.cloud_credential = get_object_or_404(
Credential, pk=request.POST["cloud_credential"]
)
return super().post(request, *args, **kwargs)
def form_valid(self, form):
self.object = form.save(commit=False)
mountpoints = []
if (
hasattr(self.object, "attached_cluster") and
self.object.attached_cluster
):
if (
self.object.subnet.vpc !=
self.object.attached_cluster.subnet.vpc
):
form.add_error(None, "Cluster and workbench must share a vpc")
return self.form_invalid(form)
for cluster_mp in self.object.attached_cluster.mount_points.all():
wb_mp = WorkbenchMountPoint()
wb_mp.export = cluster_mp.export
wb_mp.workbench = self.object
wb_mp.mount_order = cluster_mp.mount_order
wb_mp.mount_path = cluster_mp.mount_path
mountpoints.append(wb_mp)
self.object.owner = self.request.user
self.object.cloud_region = self.object.subnet.cloud_region
self.object.save()
for wb_mp in mountpoints:
wb_mp.save()
form.save_m2m()
messages.success(
self.request,
"A record for this workbench has been created. Please add any "
"desired storage below.",
)
return HttpResponseRedirect(self.get_success_url())
def get_context_data(self, **kwargs):
"""Perform extra query to populate instance types data"""
context = super().get_context_data(**kwargs)
region_info = cloud_info.get_region_zone_info(
"GCP", self.cloud_credential.detail
)
subnet_regions = {
sn.id: sn.cloud_region
for sn in VirtualSubnet.objects.filter(
cloud_credential=self.cloud_credential
)
.filter(Q(cloud_state="i") | Q(cloud_state="m"))
.all()
}
cluster_subnets = defaultdict(dict)
for icluster in Cluster.objects.all():
if icluster.cloud_state == "xm":
continue
cluster_subnets[icluster.subnet.id][icluster.id] = icluster.name
context["cluster_subnets"] = dict(cluster_subnets)
context["subnet_regions"] = json.dumps(subnet_regions)
context["region_info"] = json.dumps(region_info)
context["navtab"] = "workbench"
return context
def get_success_url(self):
# Redirect to backend view that creates cluster files
return reverse(
"backend-create-workbench", kwargs={"pk": self.object.pk}
)
class WorkbenchUpdate(LoginRequiredMixin, UpdateView):
"""Custom DetailView for Cluster model"""
model = Workbench
template_name = "workbench/update.html"
form_class = WorkbenchForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["user"] = self.request.user
return kwargs
def get_context_data(self, **kwargs):
"""Perform extra query to populate instance types data"""
context = super().get_context_data(**kwargs)
context["navtab"] = "workbench"
context["mountpoints_formset"] = self.get_mp_formset()
return context
def get_mp_formset(self, **kwargs):
def formfield_cb(model_field, **kwargs):
field = model_field.formfield(**kwargs)
if model_field.name == "export":
workbench = self.object
fsquery = list(
Filesystem.objects.all()
.exclude(
impl_type=FilesystemImpl.BUILT_IN
)
.filter(cloud_state__in=["m", "i"])
.filter(vpc=workbench.subnet.vpc)
.values_list("pk", flat=True)
)
export_qs = FilesystemExport.objects.filter(
filesystem__in=fsquery
)
if hasattr(self.object, "attached_cluster"):
exp_ids = [x.export.id for x in MountPoint.objects.filter(
cluster=self.object.attached_cluster
)]
export_qs = export_qs | FilesystemExport.objects.filter(
id__in=exp_ids
)
field.queryset = export_qs
return field
# This creates a new class on the fly
FormClass = inlineformset_factory( # pylint: disable=invalid-name
Workbench,
WorkbenchMountPoint,
form=WorkbenchMountPointForm,
formfield_callback=formfield_cb,
can_delete=True,
extra=1,
)
if self.request.POST:
kwargs["data"] = self.request.POST
return FormClass(instance=self.object, **kwargs)
def get_success_url(self):
# Update the Terraform
return reverse(
"backend-update-workbench", kwargs={"pk": self.object.pk}
)
def form_valid(self, form):
context = self.get_context_data()
workbenchmountpoints = context["mountpoints_formset"]
# Verify formset validity (surprised there's no method to do this)
for formset in workbenchmountpoints:
if not formset.is_valid():
for error in formset.errors:
form.add_error(None, error)
return self.form_invalid(form)
with transaction.atomic():
self.object = form.save()
workbenchmountpoints.instance = self.object
workbenchmountpoints.save()
msg = (
"Workbench configuration updated. Click 'create' to provision "
"the workbench"
)
messages.success(self.request, msg)
return super().form_valid(form)
class WorkbenchDeleteView(LoginRequiredMixin, DeleteView):
"""Custom DeleteView for Workbench model"""
model = Workbench
template_name = "workbench/check_delete.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["navtab"] = "workbench"
return context
def get_success_url(self):
workbench = Workbench.objects.get(pk=self.kwargs["pk"])
messages.success(self.request, f"workbench {workbench.name} deleted.")
return reverse("workbench")
class WorkbenchDestroyView(LoginRequiredMixin, generic.DetailView):
"""Custom View to confirm Workbench destroy"""
model = Workbench
template_name = "workbench/check_destroy.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["navtab"] = "workbench"
return context
class BackendCreateWorkbench(BackendAsyncView):
"""A view to make async call to create a new cluster"""
@sync_to_async
def get_orm(self, workbench_id):
workbench = Workbench.objects.get(pk=workbench_id)
creds = workbench.cloud_credential.detail
return (workbench, creds)
def cmd(self, unused_task_id, unused_token, workbench, creds):
WorkbenchInfo(workbench).create_workbench_dir(creds)
async def get(self, request, pk):
"""this will invoke the background tasks and return immediately"""
# Mixins don't yet work with Async views
if not await sync_to_async(lambda: request.user.is_authenticated)():
return redirect_to_login(request.get_full_path)
await self.test_user_is_cluster_admin(request.user)
args = await self.get_orm(pk)
await self.create_task("Create Workbench", *args)
return HttpResponseRedirect(
reverse("workbench-update", kwargs={"pk": pk})
)
class BackendStartWorkbench(BackendAsyncView):
"""A view to make async call to create a new cluster"""
@sync_to_async
def get_orm(self, workbench_id):
workbench = Workbench.objects.get(pk=workbench_id)
return (workbench,)
def cmd(self, unused_task_id, unused_token, workbench):
WorkbenchInfo(workbench).start()
async def get(self, request, pk):
"""this will invoke the background tasks and return immediately"""
# Mixins don't yet work with Async views
if not await sync_to_async(lambda: request.user.is_authenticated)():
return redirect_to_login(request.get_full_path)
await self.test_user_is_cluster_admin(request.user)
args = await self.get_orm(pk)
await self.create_task("Start Workbench", *args)
return HttpResponseRedirect(
reverse("workbench-detail", kwargs={"pk": pk})
)
class BackendDestroyWorkbench(BackendAsyncView):
"""Backend handler for workbench teardown"""
@sync_to_async
def get_orm(self, workbench_id):
workbench = Workbench.objects.get(pk=workbench_id)
return (workbench,)
def cmd(self, unused_task_id, unused_token, workbench):
WorkbenchInfo(workbench).terminate()
async def get(self, request, pk):
"""this will invoke the background tasks and return immediately"""
# Mixins don't yet work with Async views
if not await sync_to_async(lambda: request.user.is_authenticated)():
return redirect_to_login(request.get_full_path)
await self.test_user_is_cluster_admin(request.user)
args = await self.get_orm(pk)
await self.create_task("Destroy workbench", *args)
return HttpResponseRedirect(
reverse("workbench-detail", kwargs={"pk": pk})
)
class BackendUpdateWorkbench(BackendAsyncView):
"""A view to make async call to create a new cluster"""
@sync_to_async
def get_orm(self, workbench_id):
workbench = Workbench.objects.get(pk=workbench_id)
return (workbench,)
def cmd(self, unused_task_id, unused_token, workbench):
WorkbenchInfo(workbench).copy_startup_script()
async def get(self, request, pk):
"""this will invoke the background tasks and return immediately"""
# Mixins don't yet work with Async views
if not await sync_to_async(lambda: request.user.is_authenticated)():
return redirect_to_login(request.get_full_path)
await self.test_user_is_cluster_admin(request.user)
args = await self.get_orm(pk)
await self.create_task("Update Workbench", *args)
return HttpResponseRedirect(
reverse("workbench-detail", kwargs={"pk": pk})
)