implements #33 - device clone

This commit is contained in:
John Anderson 2018-01-31 03:34:17 -05:00
parent 9ea8dca4e3
commit 6da7813d2f
6 changed files with 355 additions and 0 deletions

View File

@ -827,6 +827,117 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
self.initial['rack'] = self.instance.parent_bay.device.rack_id
class DeviceCloneForm(BootstrapMixin, TenancyForm, CustomFieldForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
widget=forms.Select(
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}}',
display_field='display_name',
attrs={'filter-for': 'position'}
)
)
position = forms.TypedChoiceField(
required=False,
empty_value=None,
help_text="The lowest-numbered unit occupied by the device",
widget=APISelect(
api_url='/api/dcim/racks/{{rack}}/units/?face={{face}}',
disabled_indicator='device'
)
)
manufacturer = forms.ModelChoiceField(
queryset=Manufacturer.objects.all(),
widget=forms.Select(
attrs={'filter-for': 'device_type'}
)
)
device_type = ChainedModelChoiceField(
queryset=DeviceType.objects.all(),
chains=(
('manufacturer', 'manufacturer'),
),
label='Device type',
widget=APISelect(
api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}',
display_field='model'
)
)
comments = CommentField()
class Meta:
model = Device
labels = {
'name': 'New device name'
}
fields = [
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face', 'status',
'platform', 'tenant_group', 'tenant', 'comments',
]
help_texts = {
'device_role': "The function this device serves",
'serial': "Chassis serial number",
}
widgets = {
'face': forms.Select(attrs={'filter-for': 'position'}),
}
def __init__(self, *args, **kwargs):
# Initialize helper selectors
instance = kwargs.get('instance')
super(DeviceCloneForm, self).__init__(*args, **kwargs)
# Rack position
pk = self.instance.pk if self.instance.pk else None
try:
if self.is_bound and self.data.get('rack') and str(self.data.get('face')):
position_choices = Rack.objects.get(pk=self.data['rack'])\
.get_rack_units(face=self.data.get('face'), exclude=pk)
elif self.initial.get('rack') and str(self.initial.get('face')):
position_choices = Rack.objects.get(pk=self.initial['rack'])\
.get_rack_units(face=self.initial.get('face'), exclude=pk)
else:
position_choices = []
except Rack.DoesNotExist:
position_choices = []
self.fields['position'].choices = [('', '---------')] + [
(p['id'], {
'label': p['name'],
'disabled': bool(p['device'] and p['id'] != self.initial.get('position')),
}) for p in position_choices
]
# Disable rack assignment if this is a child device installed in a parent device
if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
self.fields['site'].disabled = True
self.fields['rack'].disabled = True
self.initial['site'] = self.instance.parent_bay.device.site_id
self.initial['rack'] = self.instance.parent_bay.device.rack_id
# set initial manufacturer
self.initial['manufacturer'] = instance.device_type.manufacturer.id
# clear device specific fields
self.initial['name'] = ''
self.initial['position'] = ''
self.initial['serial'] = ''
self.initial['asset_tag'] = ''
# set the instance to None
self.instance = Device()
class BaseDeviceCSVForm(forms.ModelForm):
device_role = forms.ModelChoiceField(
queryset=DeviceRole.objects.all(),

View File

@ -128,6 +128,7 @@ urlpatterns = [
url(r'^devices/(?P<pk>\d+)/$', views.DeviceView.as_view(), name='device'),
url(r'^devices/(?P<pk>\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'),
url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'),
url(r'^devices/(?P<pk>\d+)/clone/$', views.DeviceCloneView.as_view(), name='device_clone'),
url(r'^devices/(?P<pk>\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'),
url(r'^devices/(?P<pk>\d+)/status/$', views.DeviceStatusView.as_view(), name='device_status'),
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals
from collections import defaultdict
from operator import attrgetter
from django.contrib import messages
@ -941,6 +942,118 @@ class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
default_return_url = 'dcim:device_list'
class DeviceCloneView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_device'
model = Device
model_form = forms.DeviceCloneForm
template_name = 'dcim/device_clone.html'
default_return_url = 'dcim:device_list'
def post(self, request, *args, **kwargs):
obj = self.get_object(kwargs)
obj = self.alter_obj(obj, request, args, kwargs)
form = self.model_form(request.POST, request.FILES, instance=obj)
if form.is_valid():
new_obj = form.save()
# When a new device is initialized, components are created from the device type templates.
# Becasue these items can be modified after that point and becasue we have no easy why of
# knowing what components are sourced from the template vs created as one offs, we delete
# them all and clone them from the source device.
# bulk create clones of interfaces
Interface.objects.filter(device=new_obj).delete()
interfaces = Interface.objects.filter(device=obj)
lag_members = defaultdict(list)
for interface in interfaces:
interface.pk = None
interface.device = new_obj
if interface.lag is not None:
lag_members[interface.lag.name].append(interface)
interface.lag = None
interface.description = ''
if interfaces:
Interface.objects.bulk_create(interfaces)
# reassociate lag members now that pk's exist
for lag_name, member_interfaces in lag_members.items():
lag = Interface.objects.get(device=new_obj, name=lag_name)
Interface.objects.filter(id__in=[o.id for o in member_interfaces]).update(lag=lag)
# bulk create clones of console ports
ConsolePort.objects.filter(device=new_obj).delete()
console_ports = ConsolePort.objects.filter(device=obj)
for console_port in console_ports:
console_port.pk = None
console_port.device = new_obj
console_port.cs_port = None
if console_ports:
ConsolePort.objects.bulk_create(console_ports)
# bulk create clones of console server ports
ConsoleServerPort.objects.filter(device=new_obj).delete()
console_server_ports = ConsoleServerPort.objects.filter(device=obj)
for console_server_port in console_server_ports:
console_server_port.pk = None
console_server_port.device = new_obj
if console_server_ports:
ConsoleServerPort.objects.bulk_create(console_server_ports)
# bulk create clones of power ports
PowerPort.objects.filter(device=new_obj).delete()
power_ports = PowerPort.objects.filter(device=obj)
for power_port in power_ports:
power_port.pk = None
power_port.device = new_obj
power_port.power_outlet = None
if power_ports:
PowerPort.objects.bulk_create(power_ports)
# bulk create clones of power outlets
PowerOutlet.objects.filter(device=new_obj).delete()
power_outlets = PowerOutlet.objects.filter(device=obj)
for power_outlet in power_outlets:
power_outlet.pk = None
power_outlet.device = new_obj
if power_outlets:
PowerOutlet.objects.bulk_create(power_outlets)
# bulk create clones of device bays
DeviceBay.objects.filter(device=new_obj).delete()
device_bays = DeviceBay.objects.filter(device=obj)
for device_bay in device_bays:
device_bay.pk = None
device_bay.device = new_obj
device_bay.installed_device = None
if device_bays:
DeviceBay.objects.bulk_create(device_bays)
msg = 'Created {}'.format(self.model._meta.verbose_name)
if hasattr(new_obj, 'get_absolute_url'):
msg = '{} <a href="{}">{}</a>'.format(msg, new_obj.get_absolute_url(), escape(new_obj))
else:
msg = '{} {}'.format(msg, escape(new_obj))
messages.success(request, mark_safe(msg))
UserAction.objects.log_create(request.user, new_obj, msg)
if '_cloneanother' in request.POST:
return redirect(request.get_full_path())
return_url = form.cleaned_data.get('return_url')
if return_url is not None and is_safe_url(url=return_url, host=request.get_host()):
return redirect(return_url)
else:
return redirect(self.get_return_url(request, new_obj))
return render(request, self.template_name, {
'obj': obj,
'obj_type': self.model._meta.verbose_name,
'form': form,
'return_url': self.get_return_url(request, obj),
})
class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_device'
model_form = forms.DeviceCSVForm

View File

@ -0,0 +1,82 @@
{% extends 'dcim/inc/clone.html' %}
{% load form_helpers %}
{% block form %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Device</strong></div>
<div class="panel-body">
{% render_field form.name %}
{% render_field form.device_role %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Hardware</strong></div>
<div class="panel-body">
{% render_field form.manufacturer %}
{% render_field form.device_type %}
{% render_field form.serial %}
{% render_field form.asset_tag %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Location</strong></div>
<div class="panel-body">
{% render_field form.site %}
{% render_field form.rack %}
{% if obj.device_type.is_child_device and obj.parent_bay %}
<div class="form-group">
<label class="col-md-3 control-label">Parent device</label>
<div class="col-md-9">
<p class="form-control-static">
<a href="{% url 'dcim:device' pk=obj.parent_bay.device.pk %}">{{ obj.parent_bay.device }}</a>
</p>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label">Parent bay</label>
<div class="col-md-9">
<p class="form-control-static">
{{ obj.parent_bay.name }}
{% if perms.dcim.change_devicebay %}
<a href="{% url 'dcim:devicebay_depopulate' pk=obj.parent_bay.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-remove" aria-hidden="true" title="Remove device"></i> Remove
</a>
{% endif %}
</p>
</div>
</div>
{% else %}
{% render_field form.face %}
{% render_field form.position %}
{% endif %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Management</strong></div>
<div class="panel-body">
{% render_field form.status %}
{% render_field form.platform %}
</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,42 @@
{% extends '_base.html' %}
{% load form_helpers %}
{% block content %}
<form action="" method="post" enctype="multipart/form-data" 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 %}Cloning {{ obj_type }} {{ obj }}{% endblock %}</h3>
{% block tabs %}{% endblock %}
{% 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 %}
{% block form %}
<div class="panel panel-default">
<div class="panel-heading"><strong>{{ obj_type|capfirst }}</strong></div>
<div class="panel-body">
{% render_form form %}
</div>
</div>
{% endblock %}
</div>
</div>
<div class="row">
<div class="col-md-6 col-md-offset-3 text-right">
{% if obj.pk %}
<button type="submit" name="_clone" class="btn btn-primary">Clone</button>
<button type="submit" name="_cloneanother" class="btn btn-primary">Clone and Clone Another</button>
{% endif %}
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</form>
{% endblock %}

View File

@ -27,6 +27,12 @@
</div>
</div>
<div class="pull-right">
{% if perms.dcim.add_device %}
<a href="{% url 'dcim:device_clone' pk=device.pk %}" class="btn btn-warning">
<span class="glyphicon glyphicon-duplicate" aria-hidden="true"></span>
Clone this device
</a>
{% endif %}
{% if perms.dcim.change_device %}
<a href="{% url 'dcim:device_edit' pk=device.pk %}" class="btn btn-warning">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>