Переглянути джерело

Closes #33: Add ability to clone objects (pre-populate form fields)

Jeremy Stretch 6 роки тому
батько
коміт
446acbdf82

+ 1 - 0
docs/release-notes/version-2.7.md

@@ -127,6 +127,7 @@ PATCH) to maintain backward compatibility. This behavior will be discontinued be
 
 ## Enhancements
 
+* [#33](https://github.com/digitalocean/netbox/issues/33) - Add ability to clone objects (pre-populate form fields)
 * [#792](https://github.com/digitalocean/netbox/issues/792) - Add power port and power outlet types
 * [#1865](https://github.com/digitalocean/netbox/issues/1865) - Add console port and console server port types
 * [#2902](https://github.com/digitalocean/netbox/issues/2902) - Replace `supervisord` with `systemd`

+ 9 - 1
netbox/circuits/models.py

@@ -57,7 +57,12 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
 
     tags = TaggableManager(through=TaggedItem)
 
-    csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
+    csv_headers = [
+        'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
+    ]
+    clone_fields = [
+        'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact',
+    ]
 
     class Meta:
         ordering = ['name']
@@ -171,6 +176,9 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
     csv_headers = [
         'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
     ]
+    clone_fields = [
+        'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
+    ]
 
     STATUS_CLASS_MAP = {
         CircuitStatusChoices.STATUS_DEPROVISIONING: 'warning',

+ 18 - 0
netbox/dcim/models.py

@@ -331,6 +331,10 @@ class Site(ChangeLoggedModel, CustomFieldModel):
         'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
         'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email', 'comments',
     ]
+    clone_fields = [
+        'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
+        'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email',
+    ]
 
     STATUS_CLASS_MAP = {
         SiteStatusChoices.STATUS_ACTIVE: 'success',
@@ -559,6 +563,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
         'site', 'group_name', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width',
         'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
     ]
+    clone_fields = [
+        'site', 'group', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
+        'outer_depth', 'outer_unit',
+    ]
 
     STATUS_CLASS_MAP = {
         RackStatusChoices.STATUS_RESERVED: 'warning',
@@ -948,6 +956,9 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
     csv_headers = [
         'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'comments',
     ]
+    clone_fields = [
+        'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role',
+    ]
 
     class Meta:
         ordering = ['manufacturer', 'model']
@@ -1617,6 +1628,9 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
         'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
         'site', 'rack_group', 'rack_name', 'position', 'face', 'comments',
     ]
+    clone_fields = [
+        'device_type', 'device_role', 'tenant', 'platform', 'site', 'rack', 'status', 'cluster',
+    ]
 
     STATUS_CLASS_MAP = {
         DeviceStatusChoices.STATUS_OFFLINE: 'warning',
@@ -3159,6 +3173,10 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
         'site', 'panel_name', 'rack_group', 'rack_name', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
         'amperage', 'max_utilization', 'comments',
     ]
+    clone_fields = [
+        'power_panel', 'rack', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization',
+        'available_power',
+    ]
 
     STATUS_CLASS_MAP = {
         PowerFeedStatusChoices.STATUS_OFFLINE: 'warning',

+ 15 - 0
netbox/ipam/models.py

@@ -78,6 +78,9 @@ class VRF(ChangeLoggedModel, CustomFieldModel):
     tags = TaggableManager(through=TaggedItem)
 
     csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
+    clone_fields = [
+        'tenant', 'enforce_unique', 'description',
+    ]
 
     class Meta:
         ordering = ['name', 'rd']
@@ -177,6 +180,9 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel):
     tags = TaggableManager(through=TaggedItem)
 
     csv_headers = ['prefix', 'rir', 'date_added', 'description']
+    clone_fields = [
+        'rir', 'date_added', 'description',
+    ]
 
     class Meta:
         ordering = ['family', 'prefix']
@@ -350,6 +356,9 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
     csv_headers = [
         'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
     ]
+    clone_fields = [
+        'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description',
+    ]
 
     STATUS_CLASS_MAP = {
         'container': 'default',
@@ -627,6 +636,9 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
         'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface_name', 'is_primary',
         'dns_name', 'description',
     ]
+    clone_fields = [
+        'vrf', 'tenant', 'status', 'role', 'description',
+    ]
 
     STATUS_CLASS_MAP = {
         'active': 'primary',
@@ -898,6 +910,9 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
     tags = TaggableManager(through=TaggedItem)
 
     csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
+    clone_fields = [
+        'site', 'group', 'tenant', 'status', 'role', 'description',
+    ]
 
     STATUS_CLASS_MAP = {
         'active': 'primary',

+ 4 - 0
netbox/templates/circuits/circuit.html

@@ -1,4 +1,5 @@
 {% extends '_base.html' %}
+{% load buttons %}
 {% load custom_links %}
 {% load helpers %}
 
@@ -27,6 +28,9 @@
         </div>
     </div>
     <div class="pull-right noprint">
+        {% if perms.circuits.add_circuit %}
+            {% clone_button 'circuits:circuit_add' circuit %}
+        {% endif %}
         {% if perms.circuits.change_circuit %}
             <a href="{% url 'circuits:circuit_edit' pk=circuit.pk %}" class="btn btn-warning">
                 <span class="fa fa-pencil" aria-hidden="true"></span>

+ 4 - 0
netbox/templates/circuits/provider.html

@@ -1,4 +1,5 @@
 {% extends '_base.html' %}
+{% load buttons %}
 {% load static %}
 {% load custom_links %}
 {% load helpers %}
@@ -33,6 +34,9 @@
                 Graphs
             </button>
         {% endif %}
+        {% if perms.circuits.add_provider %}
+            {% clone_button 'circuits:provider_add' provider %}
+        {% endif %}
         {% if perms.circuits.change_provider %}
             <a href="{% url 'circuits:provider_edit' slug=provider.slug %}" class="btn btn-warning">
                 <span class="fa fa-pencil" aria-hidden="true"></span>

+ 6 - 0
netbox/templates/dcim/device.html

@@ -1,4 +1,5 @@
 {% extends '_base.html' %}
+{% load buttons %}
 {% load static %}
 {% load helpers %}
 {% load custom_links %}
@@ -57,6 +58,11 @@
                     {% if perms.dcim.add_devicebay %}<li><a href="{% url 'dcim:devicebay_add' pk=device.pk %}">Device Bays</a></li>{% endif %}
                 </ul>
             </div>
+        {% endif %}
+        {% if perms.dcim.add_device %}
+            {% clone_button 'dcim:device_add' device %}
+        {% 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>
                 Edit this device

+ 33 - 29
netbox/templates/dcim/devicetype.html

@@ -1,4 +1,5 @@
 {% extends '_base.html' %}
+{% load buttons %}
 {% load custom_links %}
 {% load helpers %}
 
@@ -14,37 +15,40 @@
             </ol>
         </div>
     </div>
-    {% if perms.dcim.change_devicetype or perms.dcim.delete_devicetype %}
-        <div class="pull-right noprint">
-            {% if perms.dcim.change_devicetype %}
-                <div class="btn-group">
-                    <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
-                    </button>
-                    <ul class="dropdown-menu">
-                        {% if perms.dcim.add_consoleporttemplate %}<li><a href="{% url 'dcim:devicetype_add_consoleport' pk=devicetype.pk %}">Console Ports</a></li>{% endif %}
-                        {% if perms.dcim.add_consoleserverporttemplate %}<li><a href="{% url 'dcim:devicetype_add_consoleserverport' pk=devicetype.pk %}">Console Server Ports</a></li>{% endif %}
-                        {% if perms.dcim.add_powerporttemplate %}<li><a href="{% url 'dcim:devicetype_add_powerport' pk=devicetype.pk %}">Power Ports</a></li>{% endif %}
-                        {% if perms.dcim.add_poweroutlettemplate %}<li><a href="{% url 'dcim:devicetype_add_poweroutlet' pk=devicetype.pk %}">Power Outlets</a></li>{% endif %}
-                        {% if perms.dcim.add_interfacetemplate %}<li><a href="{% url 'dcim:devicetype_add_interface' pk=devicetype.pk %}">Interfaces</a></li>{% endif %}
-                        {% if perms.dcim.add_frontporttemplate %}<li><a href="{% url 'dcim:devicetype_add_frontport' pk=devicetype.pk %}">Front Ports</a></li>{% endif %}
-                        {% if perms.dcim.add_rearporttemplate %}<li><a href="{% url 'dcim:devicetype_add_rearport' pk=devicetype.pk %}">Rear Ports</a></li>{% endif %}
-                        {% if perms.dcim.add_devicebaytemplate %}<li><a href="{% url 'dcim:devicetype_add_devicebay' pk=devicetype.pk %}">Device Bays</a></li>{% endif %}
-                    </ul>
-                </div>
-                <a href="{% url 'dcim:devicetype_edit' pk=devicetype.pk %}" class="btn btn-warning">
-                    <span class="fa fa-pencil" aria-hidden="true"></span>
-                    Edit this device type
-                </a>
-            {% endif %}
-          {% if perms.dcim.delete_devicetype %}
-              <a href="{% url 'dcim:devicetype_delete' pk=devicetype.pk %}" class="btn btn-danger">
+    <div class="pull-right noprint">
+        {% if perms.dcim.change_devicetype %}
+            <div class="btn-group">
+                <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                    <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
+                </button>
+                <ul class="dropdown-menu">
+                    {% if perms.dcim.add_consoleporttemplate %}<li><a href="{% url 'dcim:devicetype_add_consoleport' pk=devicetype.pk %}">Console Ports</a></li>{% endif %}
+                    {% if perms.dcim.add_consoleserverporttemplate %}<li><a href="{% url 'dcim:devicetype_add_consoleserverport' pk=devicetype.pk %}">Console Server Ports</a></li>{% endif %}
+                    {% if perms.dcim.add_powerporttemplate %}<li><a href="{% url 'dcim:devicetype_add_powerport' pk=devicetype.pk %}">Power Ports</a></li>{% endif %}
+                    {% if perms.dcim.add_poweroutlettemplate %}<li><a href="{% url 'dcim:devicetype_add_poweroutlet' pk=devicetype.pk %}">Power Outlets</a></li>{% endif %}
+                    {% if perms.dcim.add_interfacetemplate %}<li><a href="{% url 'dcim:devicetype_add_interface' pk=devicetype.pk %}">Interfaces</a></li>{% endif %}
+                    {% if perms.dcim.add_frontporttemplate %}<li><a href="{% url 'dcim:devicetype_add_frontport' pk=devicetype.pk %}">Front Ports</a></li>{% endif %}
+                    {% if perms.dcim.add_rearporttemplate %}<li><a href="{% url 'dcim:devicetype_add_rearport' pk=devicetype.pk %}">Rear Ports</a></li>{% endif %}
+                    {% if perms.dcim.add_devicebaytemplate %}<li><a href="{% url 'dcim:devicetype_add_devicebay' pk=devicetype.pk %}">Device Bays</a></li>{% endif %}
+                </ul>
+            </div>
+        {% endif %}
+        {% if perms.dcim.add_devicetype %}
+            {% clone_button 'dcim:devicetype_add' devicetype %}
+        {% endif %}
+        {% if perms.dcim.change_devicetype %}
+            <a href="{% url 'dcim:devicetype_edit' pk=devicetype.pk %}" class="btn btn-warning">
+                <span class="fa fa-pencil" aria-hidden="true"></span>
+                Edit this device type
+            </a>
+        {% endif %}
+        {% if perms.dcim.delete_devicetype %}
+            <a href="{% url 'dcim:devicetype_delete' pk=devicetype.pk %}" class="btn btn-danger">
                 <span class="fa fa-trash" aria-hidden="true"></span>
                 Delete this device type
-              </a>
-          {% endif %}
-        </div>
-    {% endif %}
+            </a>
+        {% endif %}
+    </div>
     <h1>{{ devicetype.manufacturer }} {{ devicetype.model }}</h1>
     {% include 'inc/created_updated.html' with obj=devicetype %}
     <div class="pull-right noprint">

+ 4 - 0
netbox/templates/dcim/powerfeed.html

@@ -1,4 +1,5 @@
 {% extends '_base.html' %}
+{% load buttons %}
 {% load static %}
 {% load custom_links %}
 {% load helpers %}
@@ -30,6 +31,9 @@
         </div>
     </div>
     <div class="pull-right noprint">
+        {% if perms.dcim.add_powerfeed %}
+            {% clone_button 'dcim:powerfeed_add' powerfeed %}
+        {% endif %}
         {% if perms.dcim.change_powerfeed %}
             <a href="{% url 'dcim:powerfeed_edit' pk=powerfeed.pk %}" class="btn btn-warning">
                 <span class="fa fa-pencil" aria-hidden="true"></span>

+ 4 - 0
netbox/templates/dcim/rack.html

@@ -1,4 +1,5 @@
 {% extends '_base.html' %}
+{% load buttons %}
 {% load custom_links %}
 {% load helpers %}
 
@@ -31,6 +32,9 @@
         <a {% if next_rack %}href="{% url 'dcim:rack' pk=next_rack.pk %}"{% else %}disabled="disabled"{% endif %} class="btn btn-primary">
             <span class="fa fa-chevron-right" aria-hidden="true"></span> Next Rack
         </a>
+        {% if perms.dcim.add_rack %}
+            {% clone_button 'dcim:rack_add' rack %}
+        {% endif %}
         {% if perms.dcim.change_rack %}
             <a href="{% url 'dcim:rack_edit' pk=rack.pk %}" class="btn btn-warning">
                 <span class="fa fa-pencil" aria-hidden="true"></span> Edit this rack

+ 6 - 2
netbox/templates/dcim/site.html

@@ -1,8 +1,9 @@
 {% extends '_base.html' %}
-{% load static %}
-{% load tz %}
+{% load buttons %}
 {% load custom_links %}
 {% load helpers %}
+{% load static %}
+{% load tz %}
 
 {% block header %}
     <div class="row noprint">
@@ -38,6 +39,9 @@
                 Graphs
             </button>
         {% endif %}
+        {% if perms.dcim.add_site %}
+            {% clone_button 'dcim:site_add' site %}
+        {% endif %}
         {% if perms.dcim.change_site %}
             <a href="{% url 'dcim:site_edit' slug=site.slug %}" class="btn btn-warning">
                 <span class="fa fa-pencil" aria-hidden="true"></span>

+ 4 - 0
netbox/templates/ipam/aggregate.html

@@ -1,4 +1,5 @@
 {% extends '_base.html' %}
+{% load buttons %}
 {% load custom_links %}
 {% load helpers %}
 
@@ -25,6 +26,9 @@
         </div>
     </div>
     <div class="pull-right noprint">
+        {% if perms.ipam.add_aggregate %}
+            {% clone_button 'ipam:aggregate_add' aggregate %}
+        {% endif %}
         {% if perms.ipam.change_aggregate %}
             <a href="{% url 'ipam:aggregate_edit' pk=aggregate.pk %}" class="btn btn-warning">
                 <span class="fa fa-pencil" aria-hidden="true"></span>

+ 4 - 0
netbox/templates/ipam/ipaddress.html

@@ -1,4 +1,5 @@
 {% extends '_base.html' %}
+{% load buttons %}
 {% load custom_links %}
 {% load helpers %}
 
@@ -27,6 +28,9 @@
         </div>
     </div>
     <div class="pull-right noprint">
+        {% if perms.ipam.add_ipaddress %}
+            {% clone_button 'ipam:ipaddress_add' ipaddress %}
+        {% endif %}
         {% if perms.ipam.change_ipaddress %}
             <a href="{% url 'ipam:ipaddress_edit' pk=ipaddress.pk %}" class="btn btn-warning">
                 <span class="fa fa-pencil" aria-hidden="true"></span>

+ 4 - 0
netbox/templates/ipam/prefix.html

@@ -1,4 +1,5 @@
 {% extends '_base.html' %}
+{% load buttons %}
 {% load custom_links %}
 {% load helpers %}
 
@@ -38,6 +39,9 @@
                 Add an IP Address
             </a>
         {% endif %}
+        {% if perms.ipam.add_prefix %}
+            {% clone_button 'ipam:prefix_add' prefix %}
+        {% endif %}
         {% if perms.ipam.change_prefix %}
             <a href="{% url 'ipam:prefix_edit' pk=prefix.pk %}" class="btn btn-warning">
                 <span class="fa fa-pencil" aria-hidden="true"></span>

+ 4 - 0
netbox/templates/ipam/vlan.html

@@ -1,4 +1,5 @@
 {% extends '_base.html' %}
+{% load buttons %}
 {% load custom_links %}
 {% load helpers %}
 
@@ -30,6 +31,9 @@
         </div>
     </div>
     <div class="pull-right noprint">
+        {% if perms.ipam.add_vlan %}
+            {% clone_button 'ipam:vlan_add' vlan %}
+        {% endif %}
         {% if perms.ipam.change_vlan %}
             <a href="{% url 'ipam:vlan_edit' pk=vlan.pk %}" class="btn btn-warning">
                 <span class="fa fa-pencil" aria-hidden="true"></span>

+ 4 - 0
netbox/templates/ipam/vrf.html

@@ -1,4 +1,5 @@
 {% extends '_base.html' %}
+{% load buttons %}
 {% load custom_links %}
 {% load helpers %}
 
@@ -24,6 +25,9 @@
         </div>
     </div>
     <div class="pull-right noprint">
+        {% if perms.ipam.add_vrf %}
+            {% clone_button 'ipam:vrf_add' vrf %}
+        {% endif %}
         {% if perms.ipam.change_vrf %}
             <a href="{% url 'ipam:vrf_edit' pk=vrf.pk %}" class="btn btn-warning">
                 <span class="fa fa-pencil" aria-hidden="true"></span>

+ 4 - 0
netbox/templates/tenancy/tenant.html

@@ -1,4 +1,5 @@
 {% extends '_base.html' %}
+{% load buttons %}
 {% load custom_links %}
 {% load helpers %}
 
@@ -27,6 +28,9 @@
         </div>
     </div>
     <div class="pull-right noprint">
+        {% if perms.tenancy.add_tenant %}
+            {% clone_button 'tenancy:tenant_add' tenant %}
+        {% endif %}
         {% if perms.tenancy.change_tenant %}
             <a href="{% url 'tenancy:tenant_edit' slug=tenant.slug %}" class="btn btn-warning">
                 <span class="fa fa-pencil" aria-hidden="true"></span>

+ 4 - 0
netbox/templates/virtualization/cluster.html

@@ -1,4 +1,5 @@
 {% extends '_base.html' %}
+{% load buttons %}
 {% load custom_links %}
 {% load helpers %}
 
@@ -27,6 +28,9 @@
         </div>
     </div>
     <div class="pull-right noprint">
+        {% if perms.virtualization.add_cluster %}
+            {% clone_button 'virtualization:cluster_add' cluster %}
+        {% endif %}
         {% 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>

+ 4 - 0
netbox/templates/virtualization/virtualmachine.html

@@ -1,4 +1,5 @@
 {% extends '_base.html' %}
+{% load buttons %}
 {% load custom_links %}
 {% load helpers %}
 
@@ -26,6 +27,9 @@
         </div>
     </div>
     <div class="pull-right noprint">
+        {% if perms.virtualization.add_virtualmachine %}
+            {% clone_button 'virtualization:virtualmachine_add' virtualmachine %}
+        {% endif %}
         {% if perms.virtualization.change_virtualmachine %}
             <a href="{% url 'virtualization:virtualmachine_edit' pk=virtualmachine.pk %}" class="btn btn-warning">
                 <span class="fa fa-pencil"></span>

+ 3 - 0
netbox/tenancy/models.py

@@ -73,6 +73,9 @@ class Tenant(ChangeLoggedModel, CustomFieldModel):
     tags = TaggableManager(through=TaggedItem)
 
     csv_headers = ['name', 'slug', 'group', 'description', 'comments']
+    clone_fields = [
+        'group', 'description',
+    ]
 
     class Meta:
         ordering = ['group', 'name']

+ 3 - 0
netbox/utilities/templates/buttons/clone.html

@@ -0,0 +1,3 @@
+<a href="{{ url }}" class="btn btn-success">
+    <span class="fa fa-plus" aria-hidden="true"></span> Clone
+</a>

+ 38 - 2
netbox/utilities/templatetags/buttons.py

@@ -1,4 +1,5 @@
 from django import template
+from django.urls import reverse
 
 from extras.models import ExportTemplate
 
@@ -7,12 +8,47 @@ register = template.Library()
 
 @register.inclusion_tag('buttons/add.html')
 def add_button(url):
-    return {'add_url': url}
+    return {
+        'add_url': url,
+    }
 
 
 @register.inclusion_tag('buttons/import.html')
 def import_button(url):
-    return {'import_url': url}
+    return {
+        'import_url': url,
+    }
+
+
+@register.inclusion_tag('buttons/clone.html')
+def clone_button(url, instance):
+
+    url = reverse(url)
+
+    # Populate form field values
+    params = {}
+    for field_name in getattr(instance, 'clone_fields', []):
+        field = instance._meta.get_field(field_name)
+        field_value = field.value_from_object(instance)
+
+        # Swap out False with URL-friendly value
+        if field_value is False:
+            field_value = ''
+
+        # Omit empty values
+        if field_value not in (None, ''):
+            params[field_name] = field_value
+
+        # TODO: Tag replication
+
+    # Append parameters to URL
+    param_string = '&'.join(['{}={}'.format(k, v) for k, v in params.items()])
+    if param_string:
+        url = '{}?{}'.format(url, param_string)
+
+    return {
+        'url': url,
+    }
 
 
 @register.inclusion_tag('buttons/export.html', takes_context=True)

+ 6 - 0
netbox/virtualization/models.py

@@ -129,6 +129,9 @@ class Cluster(ChangeLoggedModel, CustomFieldModel):
     tags = TaggableManager(through=TaggedItem)
 
     csv_headers = ['name', 'type', 'group', 'site', 'comments']
+    clone_fields = [
+        'type', 'group', 'tenant', 'site',
+    ]
 
     class Meta:
         ordering = ['name']
@@ -252,6 +255,9 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
     csv_headers = [
         'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
     ]
+    clone_fields = [
+        'cluster', 'tenant', 'platform', 'status', 'role', 'vcpus', 'memory', 'disk',
+    ]
 
     STATUS_CLASS_MAP = {
         'active': 'success',