community/front-end/ofe/website/ghpcfe/views/vpc.py (440 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.
""" vpc.py """
from asgiref.sync import sync_to_async
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from django.shortcuts import redirect, get_object_or_404
from django.contrib.auth.views import redirect_to_login
from django.http import HttpResponseRedirect
from django.urls import reverse, reverse_lazy
from django.forms import inlineformset_factory
from django.views import generic
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.contrib import messages
from ..models import (
Credential,
VirtualNetwork,
VirtualSubnet,
Cluster,
Workbench,
Filesystem,
)
from ..forms import VPCForm, VPCImportForm, VirtualSubnetForm
from ..cluster_manager import cloud_info
from ..cluster_manager.vpc import (
create_subnet,
delete_subnet,
create_vpc,
start_vpc,
destroy_vpc,
)
from ..views.asyncview import BackendAsyncView
from ..serializers import VirtualNetworkSerializer, VirtualSubnetSerializer
from ..permissions import SuperUserRequiredMixin
from collections import defaultdict
import json
import logging
logger = logging.getLogger(__name__)
# list view
class VPCListView(SuperUserRequiredMixin, generic.ListView):
"""Custom ListView for VirtualNetwork model"""
model = VirtualNetwork
template_name = "vpc/list.html"
def get_context_data(self, *args, **kwargs):
loading = 0
for vpc in VirtualNetwork.objects.all():
if "c" in vpc.cloud_state or "d" in vpc.cloud_state:
loading = 1
break
context = super().get_context_data(*args, **kwargs)
context["loading"] = loading
context["navtab"] = "vpc"
return context
# detail view
class VPCDetailView(SuperUserRequiredMixin, generic.DetailView):
"""Custom DetailView for Virtual Network model"""
model = VirtualNetwork
template_name = "vpc/detail.html"
def get_context_data(self, **kwargs):
"""Perform extra query to populate instance types data"""
context = super().get_context_data(**kwargs)
context["navtab"] = "vpc"
context["subnets"] = VirtualSubnet.objects.filter(vpc=self.kwargs["pk"])
vpc = get_object_or_404(VirtualNetwork, pk=self.kwargs["pk"])
used_in_clusters = []
used_in_filesystems = []
used_in_workbenches = []
for c in Cluster.objects.exclude(status="d"):
if vpc == c.subnet.vpc:
used_in_clusters.append(c)
for fs in Filesystem.objects.exclude(cloud_state__in=["cm", "m", "dm"]):
if vpc == fs.vpc:
used_in_filesystems.append(fs)
for wb in Workbench.objects.exclude(status="d"):
if vpc == wb.subnet.vpc:
used_in_workbenches.append(wb)
context["used_in_clusters"] = used_in_clusters
context["used_in_filesystems"] = used_in_filesystems
context["used_in_workbenches"] = used_in_workbenches
return context
class VPCCreateView1(SuperUserRequiredMixin, generic.ListView):
"""Custom view for the first step of VPC creation"""
model = Credential
template_name = "credential/select_form.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["navtab"] = "vpc"
return context
def post(self, request):
return HttpResponseRedirect(
reverse(
"vpc-create2", kwargs={"credential": request.POST["credential"]}
)
)
class VPCImportView1(SuperUserRequiredMixin, generic.ListView):
"""Custom view for the first step of VPC import"""
model = Credential
template_name = "credential/select_form.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["navtab"] = "vpc"
return context
def post(self, request):
return HttpResponseRedirect(
reverse(
"vpc-import2", kwargs={"credential": request.POST["credential"]}
)
)
class VPCCreateView2(SuperUserRequiredMixin, CreateView):
"""Custom CreateView for VirtualNetwork model"""
template_name = "vpc/create_form.html"
form_class = VPCForm
def get_initial(self):
return {
"cloud_credential": self.cloud_credential,
"regions": cloud_info.get_region_zone_info(
"GCP", self.cloud_credential.detail
).keys(),
}
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)
self.object.save()
form.save_m2m()
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)
context["navtab"] = "vpc"
return context
def get_success_url(self):
# Redirect to backend view that creates cluster files
return reverse("backend-create-vpc", kwargs={"pk": self.object.pk})
class VPCImportView2(SuperUserRequiredMixin, CreateView):
"""Custom CreateView for importing externally created VPC"""
template_name = "vpc/import_form.html"
form_class = VPCImportForm
def get_initial(self):
return {
"cloud_credential": self.cloud_credential,
"subnets": [(x[2], x[2]) for x in self.subnet_info],
"vpc": [(x, x) for x in self.vpc_sub_map.keys()],
}
def _setup_data(self, cred_id):
self.cloud_credential = get_object_or_404(Credential, pk=cred_id)
self.subnet_info = cloud_info.get_subnets(
"GCP", self.cloud_credential.detail
)
self.vpc_sub_map = defaultdict(list)
for (vpc, region, subnet, cidr) in self.subnet_info:
self.vpc_sub_map[vpc].append((subnet, region, cidr))
def get(self, request, *args, **kwargs):
self._setup_data(kwargs["credential"])
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self._setup_data(request.POST["cloud_credential"])
return super().post(request, *args, **kwargs)
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.cloud_state = "i"
self.object.cloud_id = form.data["vpc"]
self.object.cloud_region = "N/A" # GCP VPCs are multi-region
self.object.save()
form_subnets = form.cleaned_data["subnets"]
def add_subnet(unused_vpc_name, region, subnet, cidr):
vs = VirtualSubnet(
name=subnet,
vpc=self.object,
cidr=cidr,
cloud_id=subnet,
cloud_region=region,
cloud_state="i",
cloud_credential=self.cloud_credential,
)
vs.save()
for subnet in self.subnet_info:
if subnet[2] in form_subnets:
add_subnet(*subnet)
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)
context["vpc_sub_map"] = json.dumps(self.vpc_sub_map)
context["navtab"] = "vpc"
return context
def get_success_url(self):
messages.success(self.request, f"VPC {self.object.name} imported.")
return reverse("vpcs")
class VPCUpdateView(SuperUserRequiredMixin, UpdateView):
"""Custom UpdateView for VirtualNetwork model"""
model = VirtualNetwork
template_name = "vpc/update_form.html"
form_class = VPCForm
def get_initial(self):
initial = {
"regions": cloud_info.get_region_zone_info(
"GCP", self.get_object().cloud_credential.detail
).keys()
}
if self.get_object().cloud_state == "i":
subnet_info = cloud_info.get_subnets(
"GCP", self.get_object().cloud_credential.detail
)
initial["available_subnets"] = [
(x[2], x[2])
for x in subnet_info
if x[0] == self.get_object().cloud_id
]
initial["subnets"] = [
x.cloud_id for x in self.get_object().subnets.all()
]
return initial
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.save()
form_subnets = form.cleaned_data["subnets"]
def add_subnet(unused_vpc_name, region, subnet, cidr):
vs = VirtualSubnet(
name=subnet,
vpc=self.object,
cidr=cidr,
cloud_id=subnet,
cloud_region=region,
cloud_state="i",
cloud_credential=self.object.cloud_credential,
)
vs.save()
subnet_info = cloud_info.get_subnets(
"GCP", self.get_object().cloud_credential.detail
)
# Need list of subnets to delete - not selected, but are in DB
for sn in self.object.subnets.all():
if sn.cloud_id not in form_subnets:
logger.info(
"Removing subnet %s from VPC %s",
sn.cloud_id,
self.object.name,
)
sn.delete()
# Need list of subnets to add - selected, but aren't in DB
for sn in form_subnets:
logger.debug("Subnet %s has been selected", sn)
if self.object.subnets.filter(cloud_id=sn).count() == 0:
logger.debug("Subnet %s not found in DB", sn)
# Need to find subnet info in 'subnet_info'
for subnet in subnet_info:
if subnet[2] in form_subnets:
logger.info(
"Adding new subnet %s from VPC %s",
subnet[2],
self.object.name,
)
add_subnet(*subnet)
break
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)
context["navtab"] = "vpc"
return context
def get_success_url(self):
vpc = VirtualNetwork.objects.get(pk=self.kwargs["pk"])
messages.success(self.request, f"VPC {vpc.name} updated.")
return reverse("vpc-detail", kwargs={"pk": self.object.pk})
class VPCDeleteView(SuperUserRequiredMixin, DeleteView):
"""Custom DeleteView for VirtualNetwork model"""
model = VirtualNetwork
template_name = "vpc/check_delete.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["navtab"] = "vpc"
return context
def delete(self, *args, **kwargs):
vpc = VirtualNetwork.objects.get(pk=self.kwargs["pk"])
if vpc.in_use():
messages.add_message(
self.request,
messages.ERROR,
"Cannot delete. This network is referenced in other resources "
"see VPC details page for more info",
)
return redirect("vpcs")
try:
vpc.delete()
except Exception as err: # pylint: disable=broad-except
messages.add_message(
self.request,
messages.ERROR,
f"Cannot delete vpc, encountered unknown error {err}",
)
return redirect("vpcs")
messages.success(self.request, f"VPC {vpc.name} deleted.")
success_url = self.get_success_url()
return HttpResponseRedirect(success_url)
def get_success_url(self):
return reverse("vpcs")
class VPCDestroyView(SuperUserRequiredMixin, generic.DetailView):
"""Custom View to confirm VirtualNetwork destroy"""
model = VirtualNetwork
template_name = "vpc/check_destroy.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
subnets = VirtualSubnet.objects.filter(vpc=context["virtualnetwork"].id)
context["subnets"] = subnets
context["navtab"] = "vpc"
return context
class VirtualSubnetView(SuperUserRequiredMixin, generic.TemplateView):
"""Custom view for bulk processing subnets for a VPC"""
template_name = "vpc/virtual_subnet.html"
def get_formset(self, region_info):
def formfield_cb(f, **kwargs):
if f.name == "cloud_region":
kwargs["widget"].choices = [(x, x) for x in region_info]
field = f.formfield(**kwargs)
return field
return inlineformset_factory(
VirtualNetwork,
VirtualSubnet,
form=VirtualSubnetForm,
fk_name="vpc",
formfield_callback=formfield_cb,
fields=("name", "cidr", "cloud_region"),
can_delete=True,
extra=1,
)
def get(self, *args, **kwargs):
vpc = VirtualNetwork.objects.get(pk=kwargs["vpc_id"])
qset = VirtualSubnet.objects.filter(vpc=vpc)
region_list = list(
cloud_info.get_region_zone_info(
"GCP", vpc.cloud_credential.detail
).keys()
)
formset = self.get_formset(region_list)(
queryset=qset,
instance=vpc,
initial=[{"cloud_region": vpc.cloud_region}],
)
return self.render_to_response(
{
"virtual_subnets_formset": formset,
"vpc": vpc,
}
)
def post(self, *args, **kwargs):
vpc = VirtualNetwork.objects.get(pk=kwargs["vpc_id"])
region_list = cloud_info.get_region_zone_info(
"GCP", vpc.cloud_credential.detail
).keys()
formset = self.get_formset(region_list)(
data=self.request.POST, instance=vpc
)
if formset.is_valid():
formset.save(commit=False)
for obj in formset.new_objects:
obj.cloud_credential = vpc.cloud_credential
obj.save()
create_subnet(obj)
for obj, _ in formset.changed_objects:
obj.save()
create_subnet(obj)
for obj in formset.deleted_objects:
delete_subnet(obj)
obj.delete()
messages.success(
self.request,
f'Updated Subnets for VPC {vpc.name}. Click "Edit Subnets" '
'button again to make further changes and click "Apply Cloud '
'Changes" button to create the VPC and subnets on the cloud.',
)
return redirect(
reverse_lazy("vpc-detail", kwargs={"pk": kwargs["vpc_id"]})
)
return self.render_to_response(
{"virtual_subnets_formset": formset, "vpc": vpc}
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["navtab"] = "vpc"
return context
# For APIs
# Other supporting views
class BackendCreateVPC(BackendAsyncView):
"""A view to make async call to create a new VirtualNetwork"""
@sync_to_async
def get_orm(self, vpc_id):
vpc = VirtualNetwork.objects.get(pk=vpc_id)
return (vpc,)
def cmd(self, unused_task_id, unused_token, vpc):
create_vpc(vpc)
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 VPC", *args)
return HttpResponseRedirect(reverse("vpc-detail", kwargs={"pk": pk}))
class BackendStartVPC(BackendAsyncView):
"""A view to make async call to create a new VirtualNetwork"""
@sync_to_async
def get_orm(self, vpc_id):
vpc = VirtualNetwork.objects.get(pk=vpc_id)
vpc.cloud_state = "cm"
vpc.save()
return (vpc,)
def cmd(self, unused_task_id, unused_token, vpc):
start_vpc(vpc)
vpc.cloud_state = "m"
vpc.save()
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 VPC", *args)
return HttpResponseRedirect(reverse("vpc-detail", kwargs={"pk": pk}))
class BackendDestroyVPC(BackendAsyncView):
"""A view to make async call to destroy a VirtualNetwork"""
@sync_to_async
def get_orm(self, vpc_id):
vpc = VirtualNetwork.objects.get(pk=vpc_id)
if vpc.in_use():
messages.add_message(
self.request,
messages.ERROR,
"Cannot destroy - VPC is still referenced by other resources "
"(see below)",
)
else:
vpc.cloud_state = "dm"
vpc.save()
return (vpc,)
def cmd(self, unused_task_id, unused_token, vpc):
if not vpc.in_use():
try:
destroy_vpc(vpc)
vpc.cloud_state = "xm"
vpc.save()
except Exception as err: # pylint: disable=broad-except
messages.add_message(
self.request,
messages.ERROR,
f"Cannot destroy VPC - unexpected error ({err})",
)
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 VPC", *args)
return HttpResponseRedirect(reverse("vpc-detail", kwargs={"pk": pk}))
class VPCViewSet(viewsets.ReadOnlyModelViewSet):
"""Custom ModelViewSet for VirtualNetwork model"""
permission_classes = (IsAuthenticated,)
queryset = VirtualNetwork.objects.all()
serializer_class = VirtualNetworkSerializer
class VirtualSubnetViewSet(viewsets.ReadOnlyModelViewSet):
"""Custom ModelViewSet for VirtualSubnet model"""
permission_classes = (IsAuthenticated,)
queryset = VirtualSubnet.objects.all()
serializer_class = VirtualSubnetSerializer