Skip to content

Commit

Permalink
Initial work on virtualization support (netbox-community#142)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremystretch authored and John Anderson committed Oct 13, 2017
1 parent 34b69b6 commit 4eb45cb
Show file tree
Hide file tree
Showing 21 changed files with 1,210 additions and 102 deletions.
110 changes: 8 additions & 102 deletions netbox/dcim/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator
from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView, ComponentEditView,
ObjectDeleteView, ObjectEditView, ObjectListView,
)
from . import filters, forms, tables
from .models import (
Expand Down Expand Up @@ -61,107 +62,6 @@ def expand_pattern(string):
yield "{0}{1}".format(lead, i)


class ComponentCreateView(View):
parent_model = None
parent_field = None
model = None
form = None
model_form = None

def get(self, request, pk):

parent = get_object_or_404(self.parent_model, pk=pk)
form = self.form(parent, initial=request.GET)

return render(request, 'dcim/device_component_add.html', {
'parent': parent,
'component_type': self.model._meta.verbose_name,
'form': form,
'return_url': parent.get_absolute_url(),
})

def post(self, request, pk):

parent = get_object_or_404(self.parent_model, pk=pk)

form = self.form(parent, request.POST)
if form.is_valid():

new_components = []
data = deepcopy(form.cleaned_data)

for name in form.cleaned_data['name_pattern']:
component_data = {
self.parent_field: parent.pk,
'name': name,
}
# Replace objects with their primary key to keep component_form.clean() happy
for k, v in data.items():
if hasattr(v, 'pk'):
component_data[k] = v.pk
else:
component_data[k] = v
component_form = self.model_form(component_data)
if component_form.is_valid():
new_components.append(component_form.save(commit=False))
else:
for field, errors in component_form.errors.as_data().items():
# Assign errors on the child form's name field to name_pattern on the parent form
if field == 'name':
field = 'name_pattern'
for e in errors:
form.add_error(field, '{}: {}'.format(name, ', '.join(e)))

if not form.errors:
self.model.objects.bulk_create(new_components)

# ManyToMany relations are bulk created via the through model
m2m_fields = [field for field in component_form.fields if type(component_form.fields[field]) in M2M_FIELD_TYPES]
if m2m_fields:
for field in m2m_fields:
field_links = []
for new_component in new_components:
for related_obj in component_form.cleaned_data[field]:
# The through model columns are the id's of our M2M relation objects
through_kwargs = {}
new_component_column = new_component.__class__.__name__ + '_id'
related_obj_column = related_obj.__class__.__name__ + '_id'
through_kwargs.update({
new_component_column.lower(): new_component.id,
related_obj_column.lower(): related_obj.id
})
field_link = getattr(self.model, field).through(**through_kwargs)
field_links.append(field_link)
getattr(self.model, field).through.objects.bulk_create(field_links)

messages.success(request, "Added {} {} to {}.".format(
len(new_components), self.model._meta.verbose_name_plural, parent
))
if '_addanother' in request.POST:
return redirect(request.path)
else:
return redirect(parent.get_absolute_url())

return render(request, 'dcim/device_component_add.html', {
'parent': parent,
'component_type': self.model._meta.verbose_name,
'form': form,
'return_url': parent.get_absolute_url(),
})


class ComponentEditView(ObjectEditView):

def get_return_url(self, request, obj):
return obj.device.get_absolute_url()


class ComponentDeleteView(ObjectDeleteView):

def get_return_url(self, request, obj):
return obj.device.get_absolute_url()


class BulkDisconnectView(View):
"""
An extendable view for disconnection console/power/interface components in bulk.
Expand Down Expand Up @@ -711,6 +611,7 @@ class ConsolePortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView
model = ConsolePortTemplate
form = forms.ConsolePortTemplateCreateForm
model_form = forms.ConsolePortTemplateForm
template_name = 'dcim/device_component_add.html'


class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
Expand All @@ -729,6 +630,7 @@ class ConsoleServerPortTemplateCreateView(PermissionRequiredMixin, ComponentCrea
model = ConsoleServerPortTemplate
form = forms.ConsoleServerPortTemplateCreateForm
model_form = forms.ConsoleServerPortTemplateForm
template_name = 'dcim/device_component_add.html'


class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
Expand All @@ -745,6 +647,7 @@ class PowerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
model = PowerPortTemplate
form = forms.PowerPortTemplateCreateForm
model_form = forms.PowerPortTemplateForm
template_name = 'dcim/device_component_add.html'


class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
Expand All @@ -761,6 +664,7 @@ class PowerOutletTemplateCreateView(PermissionRequiredMixin, ComponentCreateView
model = PowerOutletTemplate
form = forms.PowerOutletTemplateCreateForm
model_form = forms.PowerOutletTemplateForm
template_name = 'dcim/device_component_add.html'


class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
Expand All @@ -777,6 +681,7 @@ class InterfaceTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
model = InterfaceTemplate
form = forms.InterfaceTemplateCreateForm
model_form = forms.InterfaceTemplateForm
template_name = 'dcim/device_component_add.html'


class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
Expand All @@ -801,6 +706,7 @@ class DeviceBayTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
model = DeviceBayTemplate
form = forms.DeviceBayTemplateCreateForm
model_form = forms.DeviceBayTemplateForm
template_name = 'dcim/device_component_add.html'


class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
Expand Down
1 change: 1 addition & 0 deletions netbox/netbox/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
'tenancy',
'users',
'utilities',
'virtualization',
)

# Middleware
Expand Down
1 change: 1 addition & 0 deletions netbox/netbox/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
url(r'^secrets/', include('secrets.urls')),
url(r'^tenancy/', include('tenancy.urls')),
url(r'^user/', include('users.urls')),
url(r'^virtualization/', include('virtualization.urls')),

# API
url(r'^api/$', APIRootView.as_view(), name='api-root'),
Expand Down
28 changes: 28 additions & 0 deletions netbox/templates/_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,34 @@
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|contains:'/virtualization/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Virtualization <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'virtualization:cluster_list' %}"><strong>Clusters</strong></a></li>
{% if perms.virtualization.add_cluster %}
<li class="subnav"><a href="{% url 'virtualization:cluster_add' %}"><i class="fa fa-plus"></i> Add a Cluster</a></li>
<li class="subnav"><a href="{% url 'virtualization:cluster_import' %}"><i class="fa fa-download"></i> Import Clusters</a></li>
{% endif %}
{% if perms.virtualization.add_cluster or perms.virtualization.add_virtualmachine %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'virtualization:virtualmachine_list' %}"><strong>Virtual Machines</strong></a></li>
{% if perms.virtualization.add_virtualmachine %}
<li class="subnav"><a href="{% url 'virtualization:virtualmachine_add' %}"><i class="fa fa-plus"></i> Add a Virtual Machine</a></li>
<li class="subnav"><a href="{% url 'virtualization:virtualmachine_import' %}"><i class="fa fa-download"></i> Import Virtual Machines</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'virtualization:clustertype_list' %}"><strong>Cluster Types</strong></a></li>
{% if perms.virtualization.add_clustertype %}
<li class="subnav"><a href="{% url 'virtualization:clustertype_add' %}"><i class="fa fa-plus"></i> Add a Cluster Type</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'virtualization:clustergroup_list' %}"><strong>Cluster Groups</strong></a></li>
{% if perms.virtualization.add_clustergroup %}
<li class="subnav"><a href="{% url 'virtualization:clustergroup_add' %}"><i class="fa fa-plus"></i> Add a Cluster Group</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|contains:'/circuits/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Circuits <span class="caret"></span></a>
<ul class="dropdown-menu">
Expand Down
92 changes: 92 additions & 0 deletions netbox/templates/virtualization/cluster.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
{% extends '_base.html' %}
{% load helpers %}

{% block content %}
<div class="row">
<div class="col-sm-8 col-md-9">
</div>
<div class="col-sm-4 col-md-3">
<form action="{% url 'virtualization:cluster_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Search clusters" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="fa fa-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>
<div class="pull-right">
{% if perms.virtualization.change_cluster %}
<a href="{% url 'virtualization:cluster_edit' pk=cluster.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span>
Edit this cluster
</a>
{% endif %}
{% if perms.dcim.delete_cluster %}
<a href="{% url 'virtualization:cluster_delete' pk=cluster.pk %}" class="btn btn-danger">
<span class="fa fa-trash" aria-hidden="true"></span>
Delete this cluster
</a>
{% endif %}
</div>
<h1>{% block title %}{{ cluster }}{% endblock %}</h1>
{% include 'inc/created_updated.html' with obj=cluster %}
<div class="row">
<div class="col-md-5">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Cluster</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Name</td>
<td>{{ cluster.name }}</td>
</tr>
<tr>
<td>Type</td>
<td><a href="{{ cluster.type.get_absolute_url }}">{{ cluster.type }}</a></td>
</tr>
<tr>
<td>Group</td>
<td>
{% if cluster.group %}
<a href="{{ cluster.group.get_absolute_url }}">{{ cluster.group }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Virtual Machines</td>
<td><a href="{% url 'virtualization:virtualmachine_list' %}?cluster={{ cluster.pk }}">{{ cluster.virtual_machines.count }}</a></td>
</tr>
</table>
</div>
{% include 'inc/custom_fields_panel.html' with custom_fields=cluster.get_custom_fields %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Comments</strong>
</div>
<div class="panel-body">
{% if cluster.comments %}
{{ cluster.comments|gfm }}
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
</div>
</div>
<div class="col-md-7">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Devices</strong>
</div>
<div class="panel-body">
</div>
</div>
</div>
</div>
{% endblock %}
26 changes: 26 additions & 0 deletions netbox/templates/virtualization/cluster_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{% extends '_base.html' %}

{% block content %}
<div class="pull-right">
{% if perms.virtualization.add_cluster %}
<a href="{% url 'virtualization:cluster_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a cluster
</a>
<a href="{% url 'virtualization:cluster_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import clusters
</a>
{% endif %}
{% include 'inc/export_button.html' with obj_type='clusters' %}
</div>
<h1>{% block title %}Clusters{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' %}
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}
19 changes: 19 additions & 0 deletions netbox/templates/virtualization/clustergroup_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% extends '_base.html' %}
{% load helpers %}

{% block content %}
<div class="pull-right">
{% if perms.virtualization.add_clustergroup %}
<a href="{% url 'virtualization:clustergroup_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a cluster group
</a>
{% endif %}
</div>
<h1>{% block title %}Cluster Groups{% endblock %}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='virtualization:clustergroup_bulk_delete' %}
</div>
</div>
{% endblock %}
19 changes: 19 additions & 0 deletions netbox/templates/virtualization/clustertype_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% extends '_base.html' %}
{% load helpers %}

{% block content %}
<div class="pull-right">
{% if perms.virtualization.add_clustertype %}
<a href="{% url 'virtualization:clustertype_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a cluster type
</a>
{% endif %}
</div>
<h1>{% block title %}Cluster Types{% endblock %}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='virtualization:clustertype_bulk_delete' %}
</div>
</div>
{% endblock %}
Loading

0 comments on commit 4eb45cb

Please sign in to comment.