def delete(self, request, team_id, tenant_id, pk): """ Instance detail DELETE """ tenant = get_tenant_for_user(request.user, team_id=team_id, tenant_id=tenant_id) # may raise lease = get_object_or_404(ServerLease, server_id=pk) try: lease.delete_server() except OpenstackNotFoundError: raise drf_exceptions.NotFound except OpenstackServiceError as e: raise UnexpectedOpenstackException(detail=str(e)) # Slack notification slack_template = "openstack/slack/entity_deleted.txt" slack_context = { "entity_type": "Server", "entity_name": lease.server_name, "tenant": tenant, "user": request.user, } slack_post_templated_message(slack_template, slack_context) return Response(status=status.HTTP_204_NO_CONTENT)
def delete(self, request, team_id, tenant_id, pk): tenant = get_tenant_for_user(request.user, team_id=team_id, tenant_id=tenant_id) # may raise openstack = OpenstackService(tenant=tenant) try: _response, deleted = methodcaller("delete", pk)(getattr( openstack, self.service.value)) except OpenstackNotFoundError: raise drf_exceptions.NotFound except OpenstackServiceError as e: raise UnexpectedOpenstackException(detail=str(e)) # Slack notification slack_template = "openstack/slack/entity_deleted.txt" slack_context = { "entity_type": self.entity_type, "entity_name": getattr(deleted, "name", "anonymous"), "tenant": tenant, "user": request.user, } slack_post_templated_message(slack_template, slack_context) return Response(status=status.HTTP_204_NO_CONTENT)
def post(self, request, team_id, tenant_id): tenant = get_tenant_for_user(request.user, team_id=team_id, tenant_id=tenant_id) # may raise openstack = OpenstackService(tenant=tenant) annotation_func = self.annotation_factory(tenant) serialized = self.serializer_class(data=request.data) serialized.is_valid(raise_exception=True) serialized_data = serialized.data serialized_data["team"] = team_id serialized_data["tenant_id"] = tenant_id try: response = methodcaller("create", serialized_data)(getattr( openstack, self.service.value)) annotated_response = annotation_func(response) except OpenstackServiceError as e: raise UnexpectedOpenstackException(detail=str(e)) # Slack notification slack_template = "openstack/slack/entity_created.txt" slack_context = { "entity_type": self.entity_type, "entity_name": serialized_data.get("name", "anonymous"), "tenant": tenant, "user": request.user, } slack_post_templated_message(slack_template, slack_context) return Response(self.serializer_class(annotated_response).data)
def _state_transition(self, target_status, request, team_id, tenant_id, pk): """ Instance detail PATCH: state transition """ tenant = get_tenant_for_user(request.user, team_id=team_id, tenant_id=tenant_id) # may raise lease = get_object_or_404(ServerLease, server_id=pk) try: # Get current server status server = lease.get_annotated_server() current_status = server.status if current_status == target_status: # Nothing to do return # Check unshelving restrictions if (current_status in ["SHELVED", "SHELVED_OFFLOADED"] and tenant.region.unshelving_disabled): raise drf_exceptions.PermissionDenied( f"Unshelving is disabled at {tenant.region.description}") # Lookup action action = self.state_transitions[current_status].get( target_status, None) if action: methodcaller(action)(lease) else: raise ConflictError( f"Action unavailble due to a conflict with the current server status ({current_status})" ) except OpenstackNotFoundError: raise drf_exceptions.NotFound except OpenstackServiceError as e: raise UnexpectedOpenstackException(detail=str(e)) # Slack notification slack_template = "openstack/slack/server_action.txt" slack_context = { "action": action, "entity_type": self.entity_type, "entity_name": getattr(server, "name", "anonymous"), "tenant": tenant, "user": request.user, } slack_post_templated_message(slack_template, slack_context)
def post(self, request, team_id, tenant_id): # Data tenant = get_tenant_for_user(request.user, tenant_id=tenant_id, team_id=team_id) # may raise keypair = get_object_or_404(KeyPair, pk=request.data["keypair"]) assigned_teammember = TeamMember.objects.get(team=tenant.team, user=self.request.user) serialized = self.serializer_class(data=request.data) serialized.is_valid(raise_exception=True) serialized_data = serialized.data name = serialized_data["name"] image = serialized_data["image"] flavor = serialized_data["flavor"] # Create leased server try: server = ServerLease.objects.create_leased_server( tenant=tenant, keypair=keypair, name=name, image=image, flavor=flavor, assigned_teammember=assigned_teammember, ) except OpenstackServiceError as exc: raise UnexpectedOpenstackException(detail=exc) # Annotate annotation_func = self.annotation_factory(tenant) annotated_response = annotation_func(server) # Slack notification slack_template = "openstack/slack/entity_created.txt" slack_context = { "entity_type": self.entity_type, "entity_name": serialized_data.get("name", "anonymous"), "tenant": tenant, "user": request.user, } slack_post_templated_message(slack_template, slack_context) return Response(self.serializer_class(annotated_response).data)
def send_server_lease_expiry_reminder_emails(): """Send server lease expiry reminder emails, on specified days until expiry""" reminder_days = settings.SERVER_LEASE_REMINDER_DAYS due_leases = ServerLease.objects.active_due() sent_count = 0 for lease in due_leases: if lease.time_remaining.days in reminder_days: last_reminder = lease.last_reminder_sent_at if last_reminder and (timezone.now() - last_reminder).days < 1: continue # Don't send reminder more than once every 24 hours lease.send_email_renewal_reminder() logger.info( f"Sent server lease expiry reminder for '{lease.server_name}' to {lease.assigned_teammember.user.email}" ) sent_count += 1 # Slack notification if sent_count: slack_template = "openstack/slack/sent_server_lease_reminder_emails.txt" slack_post_templated_message(slack_template, {"sent_count": sent_count})
def delete(self, request, team_id, bucket_name): ceph_user = get_object_or_404(CephUser, team_id=team_id, is_owner=True) try: ceph_user.delete_bucket(bucket_name) except CephS3NoSuchBucket: raise drf_exceptions.NotFound except CephS3BucketNotEmpty: raise CephS3BucketNoEmptyException except CephS3ServiceError as exc: raise CephS3UnexpectedException(exc) # Slack notification slack_template = "ceph/slack/bucket_deleted.txt" slack_context = { "bucket_name": bucket_name, "bryn_user": self.request.user, "team": ceph_user.team, } slack_post_templated_message(slack_template, slack_context) return Response(status=status.HTTP_204_NO_CONTENT)
def server_lease_renewal_view(request, server_id, renewal_count): """ GET: Renew server lease and redirect to login (with message) POST: Renew server lease and return json serialized representation """ did_renew = False # Renew lease lease = get_object_or_404(ServerLease, server_id=server_id) if lease.renewal_count == renewal_count: lease.renew_lease(user=request.user) did_renew = True # Slack notification slack_template = "openstack/slack/server_lease_renewed.txt" slack_context = { "lease": lease, "user": request.user, } slack_post_templated_message(slack_template, slack_context) # GET: Redirect with message if request.method == "GET": if did_renew: messages.success( request, f"The lease for server {lease.server_name} has been renewed for {settings.SERVER_LEASE_DEFAULT_DAYS} " "days.", ) else: messages.warning( request, "This server lease has already been renewed.", ) return HttpResponseRedirect(reverse("home:home")) # POST: Return JSON serialized representation serialized = ServerLeaseSerializer(lease) return JsonResponse(serialized.data)
def send_team_licence_expiry_reminder_emails(): """Send team licence expiry reminder emails, on specified days until expiry""" reminder_days = settings.LICENCE_RENEWAL_REMINDER_DAYS licenced_teams = Team.objects.licence_valid() sent_count = 0 for team in licenced_teams: time_remaining = team.licence_expiry - timezone.now() if time_remaining.days in reminder_days: last_reminder = team.licence_last_reminder_sent_at if last_reminder and (timezone.now() - last_reminder).days < 1: continue # Don't send reminder more than once every 24 hours team.send_team_licence_reminder_emails() logger.info( f"Sent licence renewal reminder emails for team '{team.name}' with {time_remaining.days} days until " "expiry") sent_count += 1 # Slack notification if sent_count: slack_template = "userdb/slack/sent_team_licence_renewal_reminder_emails.txt" slack_post_templated_message(slack_template, {"sent_count": sent_count})
def perform_create(self, serializer): team_id = self.request.resolver_match.kwargs["team_id"] team = get_object_or_404(Team, pk=team_id) if team.s3_disabled: raise drf_exceptions.PermissionDenied( f"S3 Buckets are disabled for team {team.name}") queryset = CephUser.objects.filter(team=team, is_owner=True) if queryset.exists(): raise drf_exceptions.ValidationError( "A CephUser already exists for this team.") ceph_user = serializer.save(team=team, is_owner=True) # Slack notification slack_template = "ceph/slack/ceph_user_created.txt" slack_context = { "uid": ceph_user.uid, "bryn_user": self.request.user, "team": team, } slack_post_templated_message(slack_template, slack_context)
def post(self, request, team_id): ceph_user = get_object_or_404(CephUser, team_id=team_id, is_owner=True) serialized = BucketSerializer(data=request.data) serialized.is_valid(raise_exception=True) try: bucket = ceph_user.create_namespaced_bucket(**serialized.data) except ValueError as exc: raise drf_exceptions.ValidationError(exc) except CephS3BucketAlreadyExistsError as exc: raise drf_exceptions.ValidationError(exc) except CephS3ServiceError as exc: raise CephS3UnexpectedException(exc) serialized = BucketSerializer(bucket) # Slack notification slack_template = "ceph/slack/bucket_created.txt" slack_context = { "bucket_name": serialized.data["name"], "bryn_user": self.request.user, "team": ceph_user.team, } slack_post_templated_message(slack_template, slack_context) return Response(serialized.data, status=status.HTTP_201_CREATED)
def hard_reboot_servers(self, request, queryset): """ Admin action: ServerLease -> Hard Reboot Servers """ opts = self.model._meta if request.POST.get("post"): # User clicked submit after confirmation rebooted = 0 for server_lease in queryset: try: # Reboot server_lease.reboot_server() rebooted += 1 # Slack notification slack_template = "openstack/slack/server_action.txt" slack_context = { "action": "HARD_REBOOT_SERVER", "entity_type": "Server", "entity_name": server_lease.server_name, "tenant": server_lease.tenant, "user": request.user, } slack_post_templated_message(slack_template, slack_context) except (OpenstackNotAllowedError, OpenstackNotFoundError, ValueError): pass # deleted or method not allowed for status if rebooted == 0: self.message_user( request, "All of the selected servers were in an incompatible state for rebooting.", level=messages.WARNING, ) else: self.message_user( request, ngettext( f"Rebooted {rebooted} server.", f"Rebooted {rebooted} servers.", rebooted, ), ) # Return None to display the change list page again. return None objects_name = ngettext("server", "servers", queryset.count()) context = { **self.admin_site.each_context(request), "title": "Are you sure?", "action": "hard_reboot_servers", "action_verb": "hard reboot", "objects_name": str(objects_name), "queryset": queryset, "opts": opts, "action_checkbox_name": helpers.ACTION_CHECKBOX_NAME, "media": self.media, } # Render confirmation page return TemplateResponse(request, "admin/confirm_action.html", context)
def shelve_servers(self, request, queryset): """ Admin action: ServerLease -> Shelve Servers """ # Logic and template adapted from Django source for delete action # https://github.com/django/django/blob/main/django/contrib/admin/actions.py # Note: request type is always POST, so must check for 'post' key opts = self.model._meta if request.POST.get("post"): # User clicked submit after confirmation shelved = 0 for server_lease in queryset: if server_lease.has_expired: # safeguard try: server_lease.shelve_server() shelved += 1 # Slack notification slack_template = "openstack/slack/server_action.txt" slack_context = { "action": "SHELVE_SERVER", "entity_type": "Server", "entity_name": server_lease.server_name, "tenant": server_lease.tenant, "user": request.user, } slack_post_templated_message(slack_template, slack_context) except ( OpenstackNotAllowedError, OpenstackNotFoundError, ValueError, ): pass # deleted or method not allowed for status if shelved == 0: self.message_user( request, "All of the selected servers were ineligible for shelving.", level=messages.WARNING, ) else: self.message_user( request, ngettext( f"Shelved {shelved} server.", f"Shelved {shelved} servers.", shelved, ), ) # Return None to display the change list page again. return None objects_name = ngettext("server", "servers", queryset.count()) context = { **self.admin_site.each_context(request), "title": "Are you sure?", "action": "shelve_servers", "action_verb": "shelve", "objects_name": str(objects_name), "queryset": queryset, "opts": opts, "action_checkbox_name": helpers.ACTION_CHECKBOX_NAME, "media": self.media, } # Render confirmation page return TemplateResponse(request, "admin/confirm_action.html", context)
def setup_openstack_project(team, region, request): """ Setup an openstack project (tenant) for a team at a particular region. """ # No duplicate team/region combinations if Tenant.objects.filter(team=team, region=region).count(): raise ExistingTenantError( f"There is an existing project for '{team.name}' at '{region.name}'." ) # Create (but don't save) a tenant tenant = Tenant(team=team, region=region) # Get an admin client and create the project admin_client = OpenstackService(region=region) domain = "default" project_name = tenant.get_tenant_name() with keystone_exception_handling(action="create", entity="project"): openstack_project = admin_client.keystone.projects.create(project_name, domain, enabled=True) # Update & save Bryn tenant record tenant.created_tenant_id = openstack_project.id tenant.created_tenant_name = project_name tenant.save() # Slack notification slack_template = "openstack/slack/entity_created.txt" slack_context = { "entity_type": "Tenant", "entity_name": project_name, "tenant": tenant, "user": request.user, } slack_post_templated_message(slack_template, slack_context) # Set quotas with keystone_exception_handling(), nova_exception_handling( action="update", entity="quota"): admin_client.nova.quotas.update(openstack_project.id, cores=32, ram=270000, instances=8) admin_client.cinder.quotas.update(openstack_project.id, volumes=20, gigabytes=2200) # Grant _member_ role to Bryn 'service user' username = admin_client.auth_settings["SERVICE_USERNAME"] with keystone_exception_handling(action="grant project role", entity="service user"): service_user = admin_client.keystone.users.list(name=username)[0] role = admin_client.keystone.roles.list(name="_member_")[0] admin_client.keystone.roles.grant(role, user=service_user, project=openstack_project) # Create default security rules project_client = OpenstackService(tenant=tenant) with keystone_exception_handling(), neutron_exception_handling( action="update", entity="default security group"): security_group_id = project_client.neutron.list_security_groups( name="default")["security_groups"][0]["id"] ingress_ports = [22, 80, 443] rule_defaults = { "security_group_id": security_group_id, "direction": "ingress", "protocol": "tcp", "remote_group_id": None, "remote_ip_prefix": "0.0.0.0/0", } for port in ingress_ports: rule = rule_defaults rule["port_range_min"] = port rule["port_range_max"] = port project_client.neutron.create_security_group_rule( {"security_group_rule": rule}) # Update team team.tenants_available = True team.save()