1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Initial work on virtualization support (#142)

This commit is contained in:
Jeremy Stretch
2017-08-04 17:02:52 -04:00
parent 36d5debe74
commit d06813f528
21 changed files with 1210 additions and 82 deletions

View File

@ -23,7 +23,8 @@ from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_S
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 (
@ -60,87 +61,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)
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.
@ -662,6 +582,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):
@ -680,6 +601,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):
@ -696,6 +618,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):
@ -712,6 +635,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):
@ -728,6 +652,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):
@ -752,6 +677,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):

View File

@ -145,6 +145,7 @@ INSTALLED_APPS = (
'tenancy',
'users',
'utilities',
'virtualization',
)
# Middleware

View File

@ -32,6 +32,7 @@ _patterns = [
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'),

View File

@ -202,6 +202,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">

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -0,0 +1,77 @@
{% extends '_base.html' %}
{% load helpers %}
{% block content %}
<div class="row">
<div class="col-sm-8 col-md-9">
<ol class="breadcrumb">
{% if vm.cluster %}
<li><a href="{{ vm.cluster.get_absolute_url }}">{{ vm.cluster }}</a></li>
{% endif %}
<li>{{ vm }}</li>
</ol>
</div>
<div class="col-sm-4 col-md-3">
<form action="{% url 'virtualization:virtualmachine_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Search virtual machines" />
<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_virtualmachine %}
<a href="{% url 'virtualization:virtualmachine_edit' pk=vm.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span>
Edit this VM
</a>
{% endif %}
{% if perms.virtualization.delete_virtualmachine %}
<a href="{% url 'virtualization:virtualmachine_delete' pk=vm.pk %}" class="btn btn-danger">
<span class="fa fa-trash" aria-hidden="true"></span>
Delete this VM
</a>
{% endif %}
</div>
<h1>{% block title %}{{ vm }}{% endblock %}</h1>
{% include 'inc/created_updated.html' with obj=vm %}
<div class="row">
<div class="col-md-7">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Virtual Machine</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Name</td>
<td>{{ vm.name }}</td>
</tr>
<tr>
<td>Cluster</td>
<td><a href="{{ vm.cluster.get_absolute_url }}">{{ vm.cluster }}</a></td>
</tr>
</table>
</div>
{% include 'inc/custom_fields_panel.html' with custom_fields=vm.get_custom_fields %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Comments</strong>
</div>
<div class="panel-body">
{% if vm.comments %}
{{ vm.comments|gfm }}
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
</div>
</div>
<div class="col-md-5">
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,39 @@
{% extends 'utilities/obj_edit.html' %}
{% load form_helpers %}
{% block form %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Virtual Machine</strong></div>
<div class="panel-body">
{% render_field form.name %}
{% render_field form.platform %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Cluster</strong></div>
<div class="panel-body">
{% render_field form.cluster %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Tenancy</strong></div>
<div class="panel-body">
{% render_field form.tenant_group %}
{% render_field form.tenant %}
</div>
</div>
{% if form.custom_fields %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Custom Fields</strong></div>
<div class="panel-body">
{% render_custom_fields form %}
</div>
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Comments</strong></div>
<div class="panel-body">
{% render_field form.comments %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,26 @@
{% extends '_base.html' %}
{% block content %}
<div class="pull-right">
{% if perms.virtualization.add_virtualmachine %}
<a href="{% url 'virtualization:virtualmachine_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a virtual machine
</a>
<a href="{% url 'virtualization:virtualmachine_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import virtual machines
</a>
{% endif %}
{% include 'inc/export_button.html' with obj_type='virtualmachines' %}
</div>
<h1>{% block title %}Virtual Machines{% 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 %}

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals
from collections import OrderedDict
from copy import deepcopy
from django_tables2 import RequestConfig
@ -697,3 +698,89 @@ class BulkDeleteView(View):
if self.form:
return self.form
return BulkDeleteForm
#
# Device/VirtualMachine components
#
class ComponentCreateView(View):
parent_model = None
parent_field = None
model = None
form = None
model_form = None
template_name = 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, self.template_name, {
'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)
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, self.template_name, {
'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()

View File

View File

@ -0,0 +1,7 @@
from __future__ import unicode_literals
from django.apps import AppConfig
class VirtualizationConfig(AppConfig):
name = 'virtualization'

View File

@ -0,0 +1,101 @@
from __future__ import unicode_literals
from django import forms
from extras.forms import CustomFieldBulkEditForm, CustomFieldForm
from tenancy.forms import TenancyForm
from tenancy.models import Tenant
from utilities.forms import BootstrapMixin, SlugField
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
#
# Cluster types
#
class ClusterTypeForm(BootstrapMixin, forms.ModelForm):
slug = SlugField()
class Meta:
model = ClusterType
fields = ['name', 'slug']
#
# Cluster groups
#
class ClusterGroupForm(BootstrapMixin, forms.ModelForm):
slug = SlugField()
class Meta:
model = ClusterGroup
fields = ['name', 'slug']
#
# Clusters
#
class ClusterForm(BootstrapMixin, CustomFieldForm):
class Meta:
model = Cluster
fields = ['name', 'type', 'group']
class ClusterCSVForm(forms.ModelForm):
type = forms.ModelChoiceField(
queryset=ClusterType.objects.all(),
to_field_name='name',
help_text='Name of cluster type',
error_messages={
'invalid_choice': 'Invalid cluster type name.',
}
)
group = forms.ModelChoiceField(
queryset=ClusterGroup.objects.all(),
to_field_name='name',
required=False,
help_text='Name of cluster group',
error_messages={
'invalid_choice': 'Invalid cluster group name.',
}
)
class Meta:
fields = ['name', 'type', 'group']
#
# Virtual Machines
#
class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
class Meta:
model = VirtualMachine
fields = ['name', 'cluster', 'tenant', 'platform', 'comments']
class VirtualMachineCSVForm(forms.ModelForm):
cluster = forms.ModelChoiceField(
queryset=Cluster.objects.all(),
to_field_name='name',
help_text='Name of parent cluster',
error_messages={
'invalid_choice': 'Invalid cluster name.',
}
)
class Meta:
fields = ['cluster', 'name', 'tenant']
class VirtualMachineBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=VirtualMachine.objects.all(), widget=forms.MultipleHiddenInput)
cluster = forms.ModelChoiceField(queryset=Cluster.objects.all(), required=False, label='Cluster')
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
class Meta:
nullable_fields = ['tenant']

View File

@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-08-04 20:51
from __future__ import unicode_literals
import dcim.fields
from django.db import migrations, models
import django.db.models.deletion
import extras.models
class Migration(migrations.Migration):
initial = True
dependencies = [
('ipam', '0018_remove_service_uniqueness_constraint'),
('tenancy', '0003_unicode_literals'),
('dcim', '0041_napalm_integration'),
]
operations = [
migrations.CreateModel(
name='Cluster',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=100, unique=True)),
('comments', models.TextField(blank=True)),
('devices', models.ManyToManyField(to='dcim.Device')),
],
options={
'ordering': ['name'],
},
bases=(models.Model, extras.models.CustomFieldModel),
),
migrations.CreateModel(
name='ClusterGroup',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, unique=True)),
('slug', models.SlugField(unique=True)),
],
options={
'ordering': ['name'],
},
),
migrations.CreateModel(
name='ClusterType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, unique=True)),
('slug', models.SlugField(unique=True)),
],
options={
'ordering': ['name'],
},
),
migrations.CreateModel(
name='VirtualMachine',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=64, unique=True)),
('comments', models.TextField(blank=True)),
('cluster', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='virtual_machines', to='virtualization.Cluster')),
('platform', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='virtual_machines', to='dcim.Platform')),
('primary_ip4', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='ipam.IPAddress', verbose_name='Primary IPv4')),
('primary_ip6', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='ipam.IPAddress', verbose_name='Primary IPv6')),
('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='virtual_machines', to='tenancy.Tenant')),
],
options={
'ordering': ['name'],
},
bases=(models.Model, extras.models.CustomFieldModel),
),
migrations.CreateModel(
name='VMInterface',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=30)),
('enabled', models.BooleanField(default=True)),
('mac_address', dcim.fields.MACAddressField(blank=True, null=True, verbose_name='MAC Address')),
('mtu', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='MTU')),
('description', models.CharField(blank=True, max_length=100)),
('virtual_machine', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='virtualization.VirtualMachine')),
],
options={
'ordering': ['virtual_machine', 'name'],
},
),
migrations.AddField(
model_name='cluster',
name='group',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='clusters', to='virtualization.ClusterGroup'),
),
migrations.AddField(
model_name='cluster',
name='type',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='clusters', to='virtualization.ClusterType'),
),
migrations.AlterUniqueTogether(
name='vminterface',
unique_together=set([('virtual_machine', 'name')]),
),
]

View File

@ -0,0 +1,218 @@
from __future__ import unicode_literals
from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.urls import reverse
from django.utils.encoding import python_2_unicode_compatible
from dcim.fields import MACAddressField
from extras.models import CustomFieldModel, CustomFieldValue
from utilities.models import CreatedUpdatedModel
#
# Cluster types
#
@python_2_unicode_compatible
class ClusterType(models.Model):
"""
A type of Cluster.
"""
name = models.CharField(
max_length=50,
unique=True
)
slug = models.SlugField(
unique=True
)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return "{}?type={}".format(reverse('virtualization:cluster_list'), self.slug)
#
# Cluster groups
#
@python_2_unicode_compatible
class ClusterGroup(models.Model):
"""
An organizational group of Clusters.
"""
name = models.CharField(
max_length=50,
unique=True
)
slug = models.SlugField(
unique=True
)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return "{}?group={}".format(reverse('virtualization:cluster_list'), self.slug)
#
# Clusters
#
@python_2_unicode_compatible
class Cluster(CreatedUpdatedModel, CustomFieldModel):
"""
A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices.
"""
name = models.CharField(
max_length=100,
unique=True
)
type = models.ForeignKey(
to=ClusterType,
on_delete=models.PROTECT,
related_name='clusters'
)
group = models.ForeignKey(
to=ClusterGroup,
on_delete=models.PROTECT,
related_name='clusters',
blank=True,
null=True
)
devices = models.ManyToManyField(
to='dcim.Device'
)
comments = models.TextField(
blank=True
)
custom_field_values = GenericRelation(
to=CustomFieldValue,
content_type_field='obj_type',
object_id_field='obj_id'
)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('virtualization:cluster', args=[self.pk])
#
# Virtual machines
#
@python_2_unicode_compatible
class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
"""
A virtual machine which runs inside a Cluster.
"""
cluster = models.ForeignKey(
to=Cluster,
on_delete=models.PROTECT,
related_name='virtual_machines'
)
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,
related_name='virtual_machines',
blank=True,
null=True
)
platform = models.ForeignKey(
to='dcim.Platform',
on_delete=models.SET_NULL,
related_name='virtual_machines',
blank=True,
null=True
)
name = models.CharField(
max_length=64,
unique=True
)
primary_ip4 = models.OneToOneField(
to='ipam.IPAddress',
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True,
verbose_name='Primary IPv4'
)
primary_ip6 = models.OneToOneField(
to='ipam.IPAddress',
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True,
verbose_name='Primary IPv6'
)
comments = models.TextField(
blank=True
)
custom_field_values = GenericRelation(
to=CustomFieldValue,
content_type_field='obj_type',
object_id_field='obj_id'
)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('virtualization:virtualmachine', args=[self.pk])
@python_2_unicode_compatible
class VMInterface(models.Model):
"""
A virtual interface which belongs to a VirtualMachine. Like the dcim.Interface model, IPAddresses can be assigned to
VMInterfaces.
"""
virtual_machine = models.ForeignKey(
to=VirtualMachine,
on_delete=models.CASCADE,
related_name='interfaces'
)
name = models.CharField(
max_length=30
)
enabled = models.BooleanField(
default=True
)
mac_address = MACAddressField(
blank=True,
null=True,
verbose_name='MAC Address'
)
mtu = models.PositiveSmallIntegerField(
blank=True,
null=True,
verbose_name='MTU'
)
description = models.CharField(
max_length=100,
blank=True
)
class Meta:
ordering = ['virtual_machine', 'name']
unique_together = ['virtual_machine', 'name']
def __str__(self):
return self.name

View File

@ -0,0 +1,84 @@
from __future__ import unicode_literals
import django_tables2 as tables
from django_tables2.utils import Accessor
from utilities.tables import BaseTable, ToggleColumn
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
CLUSTERTYPE_ACTIONS = """
{% if perms.virtualization.change_clustertype %}
<a href="{% url 'virtualization:clustertype_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
CLUSTERGROUP_ACTIONS = """
{% if perms.virtualization.change_clustergroup %}
<a href="{% url 'virtualization:clustergroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
#
# Cluster types
#
class ClusterTypeTable(BaseTable):
pk = ToggleColumn()
cluster_count = tables.Column(verbose_name='Clusters')
actions = tables.TemplateColumn(
template_code=CLUSTERTYPE_ACTIONS,
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)
class Meta(BaseTable.Meta):
model = ClusterType
fields = ('pk', 'name', 'cluster_count', 'actions')
#
# Cluster groups
#
class ClusterGroupTable(BaseTable):
pk = ToggleColumn()
cluster_count = tables.Column(verbose_name='Clusters')
actions = tables.TemplateColumn(
template_code=CLUSTERGROUP_ACTIONS,
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)
class Meta(BaseTable.Meta):
model = ClusterGroup
fields = ('pk', 'name', 'cluster_count', 'actions')
#
# Clusters
#
class ClusterTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn()
vm_count = tables.Column(verbose_name='VMs')
class Meta(BaseTable.Meta):
model = Cluster
fields = ('pk', 'name', 'type', 'group', 'vm_count')
#
# Virtual machines
#
class VirtualMachineTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn()
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
class Meta(BaseTable.Meta):
model = VirtualMachine
fields = ('pk', 'name', 'tenant')

View File

@ -0,0 +1,41 @@
from __future__ import unicode_literals
from django.conf.urls import url
from . import views
app_name = 'virtualization'
urlpatterns = [
# Cluster types
url(r'^cluster-types/$', views.ClusterTypeListView.as_view(), name='clustertype_list'),
url(r'^cluster-types/add/$', views.ClusterTypeCreateView.as_view(), name='clustertype_add'),
url(r'^cluster-types/delete/$', views.ClusterTypeBulkDeleteView.as_view(), name='clustertype_bulk_delete'),
url(r'^cluster-types/(?P<slug>[\w-]+)/edit/$', views.ClusterTypeEditView.as_view(), name='clustertype_edit'),
# Cluster groups
url(r'^cluster-groups/$', views.ClusterGroupListView.as_view(), name='clustergroup_list'),
url(r'^cluster-groups/add/$', views.ClusterGroupCreateView.as_view(), name='clustergroup_add'),
url(r'^cluster-groups/delete/$', views.ClusterGroupBulkDeleteView.as_view(), name='clustergroup_bulk_delete'),
url(r'^cluster-groups/(?P<slug>[\w-]+)/edit/$', views.ClusterGroupEditView.as_view(), name='clustergroup_edit'),
# Clusters
url(r'^clusters/$', views.ClusterListView.as_view(), name='cluster_list'),
url(r'^clusters/add/$', views.ClusterCreateView.as_view(), name='cluster_add'),
url(r'^clusters/import/$', views.ClusterBulkImportView.as_view(), name='cluster_import'),
# url(r'^clusters/edit/$', views.ClusterBulkEditView.as_view(), name='cluster_bulk_edit'),
url(r'^clusters/(?P<pk>\d+)/$', views.ClusterView.as_view(), name='cluster'),
url(r'^clusters/(?P<pk>\d+)/edit/$', views.ClusterEditView.as_view(), name='cluster_edit'),
url(r'^clusters/(?P<pk>\d+)/delete/$', views.ClusterDeleteView.as_view(), name='cluster_delete'),
# Virtual machines
url(r'^virtual-machines/$', views.VirtualMachineListView.as_view(), name='virtualmachine_list'),
url(r'^virtual-machines/add/$', views.VirtualMachineCreateView.as_view(), name='virtualmachine_add'),
url(r'^virtual-machines/import/$', views.VirtualMachineBulkImportView.as_view(), name='virtualmachine_import'),
# url(r'^virtual-machines/edit/$', views.VirtualMachineBulkEditView.as_view(), name='virtualmachine_bulk_edit'),
url(r'^virtual-machines/(?P<pk>\d+)/$', views.VirtualMachineView.as_view(), name='virtualmachine'),
url(r'^virtual-machines/(?P<pk>\d+)/edit/$', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'),
url(r'^virtual-machines/(?P<pk>\d+)/delete/$', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'),
]

View File

@ -0,0 +1,229 @@
from __future__ import unicode_literals
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Count
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views.generic import View
from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView, ComponentEditView,
ObjectDeleteView, ObjectEditView, ObjectListView,
)
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
import forms
import tables
#
# Cluster types
#
class ClusterTypeListView(ObjectListView):
queryset = ClusterType.objects.annotate(cluster_count=Count('clusters'))
table = tables.ClusterTypeTable
template_name = 'virtualization/clustertype_list.html'
class ClusterTypeCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'virtualization.add_clustertype'
model = ClusterType
form_class = forms.ClusterTypeForm
def get_return_url(self, request, obj):
return reverse('virtualization:clustertype_list')
class ClusterTypeEditView(ClusterTypeCreateView):
permission_required = 'virtualization.change_clustertype'
class ClusterTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'virtualization.delete_clustertype'
cls = ClusterType
queryset = ClusterType.objects.annotate(cluster_count=Count('clusters'))
table = tables.ClusterTypeTable
default_return_url = 'virtualization:clustertype_list'
#
# Cluster groups
#
class ClusterGroupListView(ObjectListView):
queryset = ClusterGroup.objects.annotate(cluster_count=Count('clusters'))
table = tables.ClusterGroupTable
template_name = 'virtualization/clustergroup_list.html'
class ClusterGroupCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'virtualization.add_clustergroup'
model = ClusterGroup
form_class = forms.ClusterGroupForm
def get_return_url(self, request, obj):
return reverse('virtualization:clustergroup_list')
class ClusterGroupEditView(ClusterGroupCreateView):
permission_required = 'virtualization.change_clustergroup'
class ClusterGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'virtualization.delete_clustergroup'
cls = ClusterGroup
queryset = ClusterGroup.objects.annotate(cluster_count=Count('clusters'))
table = tables.ClusterGroupTable
default_return_url = 'virtualization:clustergroup_list'
#
# Clusters
#
class ClusterListView(ObjectListView):
queryset = Cluster.objects.annotate(vm_count=Count('virtual_machines'))
table = tables.ClusterTable
template_name = 'virtualization/cluster_list.html'
class ClusterView(View):
def get(self, request, pk):
cluster = get_object_or_404(Cluster, pk=pk)
return render(request, 'virtualization/cluster.html', {
'cluster': cluster,
})
class ClusterCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'virtualization.add_cluster'
model = Cluster
form_class = forms.ClusterForm
def get_return_url(self, request, obj):
return reverse('virtualization:cluster_list')
class ClusterEditView(ClusterCreateView):
permission_required = 'virtualization.change_cluster'
class ClusterDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'virtualization.delete_cluster'
model = Cluster
default_return_url = 'virtualization:cluster_list'
class ClusterBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'virtualization.add_cluster'
model_form = forms.ClusterCSVForm
table = tables.ClusterTable
default_return_url = 'virtualization:cluster_list'
class ClusterBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'virtualization.delete_cluster'
cls = Cluster
queryset = Cluster.objects.annotate(vm_count=Count('virtual_machines'))
table = tables.ClusterTable
default_return_url = 'virtualization:cluster_list'
#
# Virtual machines
#
class VirtualMachineListView(ObjectListView):
queryset = VirtualMachine.objects.select_related('tenant')
# filter = filters.VirtualMachineFilter
# filter_form = forms.VirtualMachineFilterForm
table = tables.VirtualMachineTable
template_name = 'virtualization/virtualmachine_list.html'
class VirtualMachineView(View):
def get(self, request, pk):
vm = get_object_or_404(VirtualMachine.objects.select_related('tenant__group'), pk=pk)
return render(request, 'virtualization/virtualmachine.html', {
'vm': vm,
})
class VirtualMachineCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'virtualization.add_virtualmachine'
model = VirtualMachine
form_class = forms.VirtualMachineForm
template_name = 'virtualization/virtualmachine_edit.html'
default_return_url = 'virtualization:virtualmachine_list'
class VirtualMachineEditView(VirtualMachineCreateView):
permission_required = 'virtualization.change_virtualmachine'
class VirtualMachineDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'virtualization.delete_virtualmachine'
model = VirtualMachine
default_return_url = 'virtualization:virtualmachine_list'
class VirtualMachineBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'virtualization.add_virtualmachine'
model_form = forms.VirtualMachineCSVForm
table = tables.VirtualMachineTable
default_return_url = 'virtualization:virtualmachine_list'
class VirtualMachineBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'virtualization.change_virtualmachine'
cls = VirtualMachine
queryset = VirtualMachine.objects.select_related('tenant')
# filter = filters.VirtualMachineFilter
table = tables.VirtualMachineTable
form = forms.VirtualMachineBulkEditForm
default_return_url = 'virtualization:virtualmachine_list'
#
# VM interfaces
#
# class VMInterfaceCreateView(PermissionRequiredMixin, ComponentCreateView):
# permission_required = 'virtualization.add_vminterface'
# parent_model = VirtualMachine
# parent_field = 'vm'
# model = VMInterface
# form = forms.VMInterfaceCreateForm
# model_form = forms.VMInterfaceForm
#
#
# class VMInterfaceEditView(PermissionRequiredMixin, ComponentEditView):
# permission_required = 'virtualization.change_vminterface'
# model = VMInterface
# form_class = forms.VMInterfaceForm
#
#
# class VMInterfaceDeleteView(PermissionRequiredMixin, ComponentDeleteView):
# permission_required = 'virtualization.delete_vminterface'
# model = VMInterface
#
#
# class VMInterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
# permission_required = 'virtualization.change_vminterface'
# cls = VMInterface
# parent_cls = VirtualMachine
# table = tables.VMInterfaceTable
# form = forms.VMInterfaceBulkEditForm
#
#
# class VMInterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# permission_required = 'virtualization.delete_vminterface'
# cls = VMInterface
# parent_cls = VirtualMachine
# table = tables.VMInterfaceTable