# Standard library imports. from StringIO import StringIO from zipfile import ZipFile, ZIP_DEFLATED # Django imports. from django.contrib.auth.decorators import permission_required from django.contrib import messages from django.core.urlresolvers import reverse, reverse_lazy from django.http import HttpResponse from django.shortcuts import render_to_response, get_object_or_404 from django.views.generic import TemplateView, DetailView, CreateView, UpdateView, DeleteView # Third-party application imports. from braces.views import MultiplePermissionsRequiredMixin # Application imports. from .forms import EntityForm from .models import Project, Entity, Location from .utils import generate_entity_iptables class IndexView(MultiplePermissionsRequiredMixin, TemplateView): """ Custom view used for rendering the index page. """ template_name = 'conntrackt/index.html' # Required permissions. permissions = { "all": ("conntrackt.view",), } # Raise authorisation denied exception for unmet permissions. raise_exception = True def get_context_data(self, **kwargs): """ Returns the context data that should be used for rendering of the template. Adds the 'projects' context object containing all of the projects available sorted aplhabetically by name. """ # Set the context using the parent class. context = super(IndexView, self).get_context_data(**kwargs) # Store information about all projcts in context. context['projects'] = Project.objects.all().order_by('name') # Store information about all locations in context. context['locations'] = Location.objects.all().order_by('name') return context class ProjectView(MultiplePermissionsRequiredMixin, DetailView): """ Custom view for presenting the project information. """ model = Project permissions = { "all": ("conntrackt.view",), } # Raise authorisation denied exception for unmet permissions. raise_exception = True def get_context_data(self, **kwargs): """ Returns the context data that should be used for rendering of the template. Adds the 'location_entities' context object that contains tuples of the form '(location, entities)', where location is an instance of a Location, and entities is a query set of entities that belong to that particular location, and to related project. """ # Set the context using the parent class. context = super(ProjectView, self).get_context_data(**kwargs) # Set-up an array that will contaion (location, entities) tuples. location_entities = [] # Add the (location, entities) tuple for each location that has entities # belonging to the related project. for location in Location.objects.filter(entity__project=self.object).distinct().order_by("name"): entities = Entity.objects.filter(project=self.object, location=location) location_entities.append((location, entities)) # Add the (location, entities) tuples to context. context['location_entities'] = location_entities # Finally return the context. return context class EntityView(MultiplePermissionsRequiredMixin, DetailView): """ Custom view for presenting entity information. """ # Optimise the query to fetch the related data from reverse relationships. queryset = Entity.objects.all() queryset = queryset.prefetch_related('interface_set__destination_set__source__entity') queryset = queryset.prefetch_related('interface_set__destination_set__destination__entity') queryset = queryset.prefetch_related('interface_set__source_set__source__entity') queryset = queryset.prefetch_related('interface_set__source_set__destination__entity') # Required permissions. permissions = { "all": ("conntrackt.view",), } # Raise authorisation denied exception for unmet permissions. raise_exception = True def get_context_data(self, **kwargs): """ Returns the context data that should be used for rendering of the template. Adds the 'entity_iptables' context object that contains full iptables rules generated for the entity. """ # Call the parent class method. context = super(DetailView, self).get_context_data(**kwargs) # Add the rendered iptables rules to the context. context['entity_iptables'] = generate_entity_iptables(self.object) # Add the incoming and outgoing commmunication to the context. context["incoming_communications"] = self.object.incoming_communications() context["outgoing_communications"] = self.object.outgoing_communications() # Add the interfaces to the context. context["interfaces"] = self.object.interface_set.all().order_by("name") # Add project/location to the context. context["project"] = self.object.project context["location"] = self.object.location return context @permission_required("conntrackt.view", raise_exception=True) def entity_iptables(request, pk): """ Custom view that returns response containing iptables rules generated for an entity. Makes sure to set the Content-Disposition of a response in order to signal the browser it should start download of this view's response immediately. Also sets the suggested filename for it. Arguments: pk - Primary key of the Entity object for which the rules should be generated. Returns: Response object that contains the iptables rules for specified entity. """ # Fetch the entity, and construct the response with iptables rules as # content. entity = get_object_or_404(Entity, pk=pk) content = generate_entity_iptables(entity) response = HttpResponse(content, mimetype='text/plain') # Add the Content-Disposition information for the browser, telling the # browser to download the file with suggested filename. response['Content-Disposition'] = "attachment; filename=%s-iptables.conf" % entity.name.lower().replace(" ", "_") return response @permission_required("conntrackt.view", raise_exception=True) def project_iptables(request, project_id, location_id=None): """ Custom view for obtaining iptables for all entities of a project or project location in a single ZIP file. Arguments: request - Request object. project_id - Unique ID of the project for whose entities the iptables rules should be generated. location_id - Optional unique ID of the project location for whose entities the iptables rules should be generated. Default is None, which means generate rules for _all_ entities in a project. Returns: Response object that contains the ZIP file and Content-Disposition information. """ # Fetch the project. project = get_object_or_404(Project, pk=project_id) # Set-up a string IO object to which we'll write the ZIP file (in-memory). buff = StringIO() # Create a new ZIP file in-memory. zipped_iptables = ZipFile(buff, "w", ZIP_DEFLATED) # Create the response object, setting the mime type so browser could offer # to open the file with program as well. response = HttpResponse(mimetype='application/zip') # If specific location was specified, get the entities that are part of that # project location only, otherwise fetch all of the project's entities. Also # set-up the filename that will be suggested to the browser. if location_id: location = get_object_or_404(Location, pk=location_id) entities = project.entity_set.filter(location=location) filename = '%s-%s-iptables.zip' % (project.name.lower().replace(" ", "_"), location.name.lower().replace(" ", "_")) else: entities = project.entity_set.all() filename = '%s-iptables.zip' % (project.name.lower().replace(" ", "_")) # Render iptables rules for each entity, placing them in the ZIP archive. for entity in entities: entity_iptables = generate_entity_iptables(entity) zipped_iptables.writestr("%s-iptables.conf" % entity.name.lower().replace(" ", "_"), entity_iptables) # Close the archive, and flush the buffer. zipped_iptables.close() buff.flush() # Write the contents of our buffer (ZIP archive) to response content, and # close the IO string. response.write(buff.getvalue()) buff.close() # Set the Content-Disposition so the browser would know it should download # the archive, and suggest the filename. response['Content-Disposition'] = 'attachment; filename="%s"' % filename # Finally return the response object. return response class ProjectCreateView(MultiplePermissionsRequiredMixin, CreateView): """ View for creating a new project. """ model = Project template_name_suffix = "_create_form" # Required permissions. permissions = { "all": ("conntrackt.add_project",), } # Raise authorisation denied exception for unmet permissions. raise_exception = True def get_form(self, form_class): """ Implements an override for the default form constructed for the create view that includes some better styling of input widgets. """ form = super(ProjectCreateView, self).get_form(form_class) form.fields["name"].widget.attrs["class"] = "span6" form.fields["name"].widget.attrs["placeholder"] = "New Project" form.fields["description"].widget.attrs["class"] = "span6" form.fields["description"].widget.attrs["placeholder"] = "Description for new project." return form class ProjectUpdateView(MultiplePermissionsRequiredMixin, UpdateView): """ View for modifying an existing project. """ model = Project template_name_suffix = "_update_form" # Required permissions. permissions = { "all": ("conntrackt.change_project",), } # Raise authorisation denied exception for unmet permissions. raise_exception = True def get_form(self, form_class): """ Implements an override for the default form constructed for the create view that includes some better styling of input widgets. """ form = super(ProjectUpdateView, self).get_form(form_class) form.fields["name"].widget.attrs["class"] = "span6" form.fields["name"].widget.attrs["placeholder"] = "Project name" form.fields["description"].widget.attrs["class"] = "span6" form.fields["description"].widget.attrs["placeholder"] = "Description for project." return form class ProjectDeleteView(MultiplePermissionsRequiredMixin, DeleteView): """ View for deleting a project. """ model = Project # Required permissions. permissions = { "all": ("conntrackt.delete_project",), } # Raise authorisation denied exception for unmet permissions. raise_exception = True success_url = reverse_lazy("index") def post(self, *args, **kwargs): """ Add a success message that will be displayed to the user to confirm the project deletion. """ messages.success(self.request, "Project %s has been removed." % self.get_object().name, extra_tags="alert alert-success") return super(ProjectDeleteView, self).post(*args, **kwargs) class LocationCreateView(MultiplePermissionsRequiredMixin, CreateView): """ View for creating a new location. """ model = Location template_name_suffix = "_create_form" # Required permissions. permissions = { "all": ("conntrackt.add_location",), } # Raise authorisation denied exception for unmet permissions. raise_exception = True success_url = reverse_lazy("index") def get_form(self, form_class): """ Implements an override for the default form constructed for the create view that includes some better styling of input widgets. """ form = super(LocationCreateView, self).get_form(form_class) form.fields["name"].widget.attrs["class"] = "span6" form.fields["name"].widget.attrs["placeholder"] = "New Location" form.fields["description"].widget.attrs["class"] = "span6" form.fields["description"].widget.attrs["placeholder"] = "Description for new location." return form class LocationUpdateView(MultiplePermissionsRequiredMixin, UpdateView): """ View for modifying an existing location. """ model = Location template_name_suffix = "_update_form" # Required permissions. permissions = { "all": ("conntrackt.change_location",), } # Raise authorisation denied exception for unmet permissions. raise_exception = True success_url = reverse_lazy("index") def get_form(self, form_class): """ Implements an override for the default form constructed for the create view that includes some better styling of input widgets. """ form = super(LocationUpdateView, self).get_form(form_class) form.fields["name"].widget.attrs["class"] = "span6" form.fields["name"].widget.attrs["placeholder"] = "Location name" form.fields["description"].widget.attrs["class"] = "span6" form.fields["description"].widget.attrs["placeholder"] = "Description for location." return form class LocationDeleteView(MultiplePermissionsRequiredMixin, DeleteView): """ View for deleting a location. """ model = Location # Required permissions. permissions = { "all": ("conntrackt.delete_location",), } # Raise authorisation denied exception for unmet permissions. raise_exception = True success_url = reverse_lazy("index") def post(self, *args, **kwargs): """ Add a success message that will be displayed to the user to confirm the location deletion. """ messages.success(self.request, "Location %s has been removed." % self.get_object().name, extra_tags="alert alert-success") return super(LocationDeleteView, self).post(*args, **kwargs) class EntityCreateView(MultiplePermissionsRequiredMixin, CreateView): """ View for creating a new entity. """ model = Entity form_class = EntityForm template_name_suffix = "_create_form" # Required permissions. permissions = { "all": ("conntrackt.add_entity",), } # Raise authorisation denied exception for unmet permissions. raise_exception = True def get_form(self, form_class): """ Returns an instance of form that can be used by the view. The method will limit the project or location select inputs if request contained this information. """ form = super(EntityCreateView, self).get_form(form_class) # Limit the project selection if required. project_id = self.request.GET.get("project", None) if project_id: form.fields["project"].queryset = Project.objects.filter(pk=project_id) form.fields["project"].widget.attrs["readonly"] = True # Limit the location selection if required. location_id = self.request.GET.get("location", None) if location_id: form.fields["location"].queryset = Location.objects.filter(pk=location_id) form.fields["location"].widget.attrs["readonly"] = True return form def get_initial(self): """ Returns initial values that should be pre-selected (if they were specified through a GET parameter). """ initial = super(EntityCreateView, self).get_initial() initial["project"] = self.request.GET.get("project", None) initial["location"] = self.request.GET.get("location", None) return initial class EntityUpdateView(MultiplePermissionsRequiredMixin, UpdateView): """ View for updating an existing entity. """ model = Entity form_class = EntityForm template_name_suffix = "_update_form" # Required permissions. permissions = { "all": ("conntrackt.change_entity",), } # Raise authorisation denied exception for unmet permissions. raise_exception = True class EntityDeleteView(MultiplePermissionsRequiredMixin, DeleteView): """ View for deleting an entity. """ model = Entity # Required permissions. permissions = { "all": ("conntrackt.delete_entity",), } # Raise authorisation denied exception for unmet permissions. raise_exception = True def post(self, *args, **kwargs): """ Add a success message that will be displayed to the user to confirm the entity deletion. """ messages.success(self.request, "Entity %s has been removed." % self.get_object().name, extra_tags="alert alert-success") return super(EntityDeleteView, self).post(*args, **kwargs) def delete(self, *args, **kwargs): """ Deletes the object. This method is overridden in order to obtain the project ID for success URL. @TODO: Fix this once Django 1.6 comes out with fix from ticket 19044. """ self.success_url = reverse("project", args=(self.get_object().project.id,)) return super(EntityDeleteView, self).delete(*args, **kwargs)