mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-24 17:38:37 -06:00
Added views to add/remove hosts to/from clusters
This commit is contained in:
parent
5930a64203
commit
4587aba1d4
@ -10,6 +10,7 @@ from django.db.models import Q
|
|||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.filters import NullableCharFieldFilter, NullableModelMultipleChoiceFilter, NumericInFilter
|
from utilities.filters import NullableCharFieldFilter, NullableModelMultipleChoiceFilter, NumericInFilter
|
||||||
|
from virtualization.models import Cluster
|
||||||
from .models import (
|
from .models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection,
|
DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection,
|
||||||
@ -407,6 +408,10 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
label='Rack (ID)',
|
label='Rack (ID)',
|
||||||
)
|
)
|
||||||
|
cluster_id = NullableModelMultipleChoiceFilter(
|
||||||
|
queryset=Cluster.objects.all(),
|
||||||
|
label='VM cluster (ID)',
|
||||||
|
)
|
||||||
model = django_filters.ModelMultipleChoiceFilter(
|
model = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='device_type__slug',
|
name='device_type__slug',
|
||||||
queryset=DeviceType.objects.all(),
|
queryset=DeviceType.objects.all(),
|
||||||
|
38
netbox/templates/utilities/obj_bulk_remove.html
Normal file
38
netbox/templates/utilities/obj_bulk_remove.html
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
{% block title %}Remove {{ table.rows|length }} {{ obj_type_plural|bettertitle }}?{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 col-md-offset-2">
|
||||||
|
<div class="panel panel-danger">
|
||||||
|
<div class="panel-heading"><strong>Confirm Bulk Removal</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<strong>Warning:</strong> The following operation will remove {{ table.rows|length }} {{ obj_type_plural }} from {{ parent_obj }}. Please carefully review the {{ obj_type_plural }} to be removed and confirm below.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 col-md-offset-2">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
{% include 'inc/table.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 col-md-offset-3">
|
||||||
|
<form action="." method="post" class="form">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field in form.hidden_fields %}
|
||||||
|
{{ field }}
|
||||||
|
{% endfor %}
|
||||||
|
<div class="text-center">
|
||||||
|
<button type="submit" name="_confirm" class="btn btn-danger">Delete these {{ table.rows|length }} {{ obj_type_plural }}</button>
|
||||||
|
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -2,7 +2,7 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row" xmlns="http://www.w3.org/1999/html">
|
||||||
<div class="col-sm-8 col-md-9">
|
<div class="col-sm-8 col-md-9">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{{ cluster.type.get_absolute_url }}">{{ cluster.type }}</a></li>
|
<li><a href="{{ cluster.type.get_absolute_url }}">{{ cluster.type }}</a></li>
|
||||||
@ -89,10 +89,28 @@
|
|||||||
<div class="col-md-7">
|
<div class="col-md-7">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Devices</strong>
|
<strong>Host Devices</strong>
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
</div>
|
</div>
|
||||||
|
{% if perms.virtualization.change_cluster %}
|
||||||
|
<form action="{% url 'virtualization:cluster_remove_devices' pk=cluster.pk %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% endif %}
|
||||||
|
{% include 'responsive_table.html' with table=device_table %}
|
||||||
|
{% if perms.virtualization.change_cluster %}
|
||||||
|
<div class="panel-footer">
|
||||||
|
<div class="pull-right">
|
||||||
|
<a href="{% url 'virtualization:cluster_add_devices' pk=cluster.pk %}" class="btn btn-primary btn-xs">
|
||||||
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||||
|
Add devices
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<button type="submit" name="_remove" class="btn btn-danger primary btn-xs">
|
||||||
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
|
||||||
|
Remove devices
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
44
netbox/templates/virtualization/cluster_add_devices.html
Normal file
44
netbox/templates/virtualization/cluster_add_devices.html
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load static from staticfiles %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form action="." method="post" class="form form-horizontal">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field in form.hidden_fields %}
|
||||||
|
{{ field }}
|
||||||
|
{% endfor %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 col-md-offset-3">
|
||||||
|
<h3>{% block title %}Add Devices to Cluster {{ cluster }}{% endblock %}</h3>
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<div class="panel panel-danger">
|
||||||
|
<div class="panel-heading"><strong>Errors</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Devices</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.region %}
|
||||||
|
{% render_field form.site %}
|
||||||
|
{% render_field form.rack %}
|
||||||
|
{% render_field form.devices %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 col-md-offset-3 text-right">
|
||||||
|
<button type="submit" name="_add" class="btn btn-primary">Add Devices</button>
|
||||||
|
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block javascript %}
|
||||||
|
<script src="{% static 'js/livesearch.js' %}?v{{ settings.VERSION }}"></script>
|
||||||
|
{% endblock %}
|
@ -187,6 +187,10 @@ class APISelect(SelectWithDisabled):
|
|||||||
self.attrs['disabled-indicator'] = disabled_indicator
|
self.attrs['disabled-indicator'] = disabled_indicator
|
||||||
|
|
||||||
|
|
||||||
|
class APISelectMultiple(APISelect):
|
||||||
|
allow_multiple_selected = True
|
||||||
|
|
||||||
|
|
||||||
class Livesearch(forms.TextInput):
|
class Livesearch(forms.TextInput):
|
||||||
"""
|
"""
|
||||||
A text widget that carries a few extra bits of data for use in AJAX-powered autocomplete search
|
A text widget that carries a few extra bits of data for use in AJAX-powered autocomplete search
|
||||||
@ -385,6 +389,15 @@ class ChainedModelChoiceField(forms.ModelChoiceField):
|
|||||||
super(ChainedModelChoiceField, self).__init__(*args, **kwargs)
|
super(ChainedModelChoiceField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ChainedModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
||||||
|
"""
|
||||||
|
See ChainedModelChoiceField
|
||||||
|
"""
|
||||||
|
def __init__(self, chains=None, *args, **kwargs):
|
||||||
|
self.chains = chains
|
||||||
|
super(ChainedModelMultipleChoiceField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class SlugField(forms.SlugField):
|
class SlugField(forms.SlugField):
|
||||||
|
|
||||||
def __init__(self, slug_source='name', *args, **kwargs):
|
def __init__(self, slug_source='name', *args, **kwargs):
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from mptt.forms import TreeNodeChoiceField
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
|
||||||
from dcim.formfields import MACAddressFormField
|
from dcim.formfields import MACAddressFormField
|
||||||
|
from dcim.models import Device, Rack, Region, Site
|
||||||
from extras.forms import CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm
|
from extras.forms import CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm
|
||||||
from tenancy.forms import TenancyForm
|
from tenancy.forms import TenancyForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedModelChoiceField, ComponentForm,
|
APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin,
|
||||||
ExpandableNameField, FilterChoiceField, SlugField,
|
ChainedModelChoiceField, ChainedModelMultipleChoiceField, ComponentForm, ConfirmationForm, ExpandableNameField,
|
||||||
|
FilterChoiceField, SlugField,
|
||||||
)
|
)
|
||||||
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||||
|
|
||||||
@ -88,6 +92,65 @@ class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
||||||
|
region = TreeNodeChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=forms.Select(
|
||||||
|
attrs={'filter-for': 'site'}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
site = ChainedModelChoiceField(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
chains=(
|
||||||
|
('region', 'region'),
|
||||||
|
),
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/sites/?region_id={{region}}',
|
||||||
|
attrs={'filter-for': 'rack'}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rack = ChainedModelChoiceField(
|
||||||
|
queryset=Rack.objects.all(),
|
||||||
|
chains=(
|
||||||
|
('site', 'site'),
|
||||||
|
),
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/racks/?site_id={{site}}',
|
||||||
|
attrs={'filter-for': 'devices', 'nullable': 'true'}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
devices = ChainedModelMultipleChoiceField(
|
||||||
|
queryset=Device.objects.filter(cluster__isnull=True),
|
||||||
|
chains=(
|
||||||
|
('site', 'site'),
|
||||||
|
('rack', 'rack'),
|
||||||
|
),
|
||||||
|
label='Device',
|
||||||
|
required=False,
|
||||||
|
widget=APISelectMultiple(
|
||||||
|
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
|
||||||
|
display_field='display_name',
|
||||||
|
disabled_indicator='cluster'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = ['region', 'site', 'rack', 'devices']
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
|
super(ClusterAddDevicesForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields['devices'].choices = []
|
||||||
|
|
||||||
|
|
||||||
|
class ClusterRemoveDevicesForm(ConfirmationForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Virtual Machines
|
# Virtual Machines
|
||||||
#
|
#
|
||||||
|
@ -28,6 +28,8 @@ urlpatterns = [
|
|||||||
url(r'^clusters/(?P<pk>\d+)/$', views.ClusterView.as_view(), name='cluster'),
|
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+)/edit/$', views.ClusterEditView.as_view(), name='cluster_edit'),
|
||||||
url(r'^clusters/(?P<pk>\d+)/delete/$', views.ClusterDeleteView.as_view(), name='cluster_delete'),
|
url(r'^clusters/(?P<pk>\d+)/delete/$', views.ClusterDeleteView.as_view(), name='cluster_delete'),
|
||||||
|
url(r'^clusters/(?P<pk>\d+)/devices/add/$', views.ClusterAddDevicesView.as_view(), name='cluster_add_devices'),
|
||||||
|
url(r'^clusters/(?P<pk>\d+)/devices/remove/$', views.ClusterRemoveDevicesView.as_view(), name='cluster_remove_devices'),
|
||||||
|
|
||||||
# Virtual machines
|
# Virtual machines
|
||||||
url(r'^virtual-machines/$', views.VirtualMachineListView.as_view(), name='virtualmachine_list'),
|
url(r'^virtual-machines/$', views.VirtualMachineListView.as_view(), name='virtualmachine_list'),
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
|
from dcim.tables import DeviceTable
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView, ComponentEditView,
|
BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView, ComponentEditView,
|
||||||
ObjectDeleteView, ObjectEditView, ObjectListView,
|
ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
@ -96,11 +98,16 @@ class ClusterView(View):
|
|||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
|
|
||||||
cluster = get_object_or_404(Cluster, pk=pk)
|
cluster = get_object_or_404(Cluster, pk=pk)
|
||||||
devices = Device.objects.filter(cluster=cluster)
|
devices = Device.objects.filter(cluster=cluster).select_related(
|
||||||
|
'site', 'rack', 'tenant', 'device_type__manufacturer'
|
||||||
|
)
|
||||||
|
device_table = DeviceTable(list(devices), orderable=False)
|
||||||
|
if request.user.has_perm('virtualization:change_cluster'):
|
||||||
|
device_table.columns.show('pk')
|
||||||
|
|
||||||
return render(request, 'virtualization/cluster.html', {
|
return render(request, 'virtualization/cluster.html', {
|
||||||
'cluster': cluster,
|
'cluster': cluster,
|
||||||
'devices': devices,
|
'device_table': device_table,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -109,9 +116,6 @@ class ClusterCreateView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
model = Cluster
|
model = Cluster
|
||||||
form_class = forms.ClusterForm
|
form_class = forms.ClusterForm
|
||||||
|
|
||||||
def get_return_url(self, request, obj):
|
|
||||||
return reverse('virtualization:cluster_list')
|
|
||||||
|
|
||||||
|
|
||||||
class ClusterEditView(ClusterCreateView):
|
class ClusterEditView(ClusterCreateView):
|
||||||
permission_required = 'virtualization.change_cluster'
|
permission_required = 'virtualization.change_cluster'
|
||||||
@ -138,6 +142,82 @@ class ClusterBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
default_return_url = 'virtualization:cluster_list'
|
default_return_url = 'virtualization:cluster_list'
|
||||||
|
|
||||||
|
|
||||||
|
class ClusterAddDevicesView(PermissionRequiredMixin, View):
|
||||||
|
permission_required = 'virtualization.change_cluster'
|
||||||
|
form = forms.ClusterAddDevicesForm
|
||||||
|
template_name = 'virtualization/cluster_add_devices.html'
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
|
||||||
|
cluster = get_object_or_404(Cluster, pk=pk)
|
||||||
|
form = self.form()
|
||||||
|
|
||||||
|
return render(request, self.template_name, {
|
||||||
|
'cluster': cluster,
|
||||||
|
'form': form,
|
||||||
|
'return_url': reverse('virtualization:cluster', kwargs={'pk': pk}),
|
||||||
|
})
|
||||||
|
|
||||||
|
def post(self, request, pk):
|
||||||
|
|
||||||
|
cluster = get_object_or_404(Cluster, pk=pk)
|
||||||
|
form = self.form(request.POST)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
|
||||||
|
# Assign the selected Devices to the Cluster
|
||||||
|
devices = form.cleaned_data['devices']
|
||||||
|
Device.objects.filter(pk__in=devices).update(cluster=cluster)
|
||||||
|
|
||||||
|
messages.success(request, "Added {} devices to cluster {}".format(
|
||||||
|
len(devices), cluster
|
||||||
|
))
|
||||||
|
return redirect(cluster.get_absolute_url())
|
||||||
|
|
||||||
|
return render(request, self.template_name, {
|
||||||
|
'cluser': cluster,
|
||||||
|
'form': form,
|
||||||
|
'return_url': cluster.get_absolute_url(),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class ClusterRemoveDevicesView(PermissionRequiredMixin, View):
|
||||||
|
permission_required = 'virtualization.change_cluster'
|
||||||
|
form = forms.ClusterRemoveDevicesForm
|
||||||
|
template_name = 'utilities/obj_bulk_remove.html'
|
||||||
|
|
||||||
|
def post(self, request, pk):
|
||||||
|
|
||||||
|
cluster = get_object_or_404(Cluster, pk=pk)
|
||||||
|
|
||||||
|
if '_confirm' in request.POST:
|
||||||
|
form = self.form(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
|
||||||
|
# Remove the selected Devices from the Cluster
|
||||||
|
devices = form.cleaned_data['pk']
|
||||||
|
Device.objects.filter(pk__in=devices).update(cluster=None)
|
||||||
|
|
||||||
|
messages.success(request, "Removed {} devices from cluster {}".format(
|
||||||
|
len(devices), cluster
|
||||||
|
))
|
||||||
|
return redirect(cluster.get_absolute_url())
|
||||||
|
|
||||||
|
else:
|
||||||
|
form = self.form(initial={'pk': request.POST.getlist('pk')})
|
||||||
|
|
||||||
|
selected_objects = Device.objects.filter(pk__in=form.initial['pk'])
|
||||||
|
device_table = DeviceTable(list(selected_objects), orderable=False)
|
||||||
|
|
||||||
|
return render(request, self.template_name, {
|
||||||
|
'form': form,
|
||||||
|
'parent_obj': cluster,
|
||||||
|
'table': device_table,
|
||||||
|
'obj_type_plural': 'devices',
|
||||||
|
'return_url': cluster.get_absolute_url(),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Virtual machines
|
# Virtual machines
|
||||||
#
|
#
|
||||||
|
Loading…
Reference in New Issue
Block a user