mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-29 16:47:46 -06:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
242cb7c7cb | ||
|
|
edb49c7f0a | ||
|
|
3e0a7e7f8a | ||
|
|
cfab9a6a0a | ||
|
|
91b5f6d799 | ||
|
|
d5488ca7da | ||
|
|
f9911bff0d | ||
|
|
d5239191fe | ||
|
|
db7148350e | ||
|
|
c51c20a301 | ||
|
|
f4485dc72a | ||
|
|
f59682a7c9 | ||
|
|
507a023f41 | ||
|
|
ea7386b04b | ||
|
|
81479ac177 | ||
|
|
c7acddbc5c | ||
|
|
1905516536 | ||
|
|
64f34d9cd7 | ||
|
|
98bdb0cb3c | ||
|
|
bba88b2be4 | ||
|
|
12dfd4b6e0 | ||
|
|
209e721219 |
@@ -7,10 +7,18 @@ NetBox uses [PostgreSQL](https://www.postgresql.org/) for its database, so gener
|
||||
|
||||
## Export the Database
|
||||
|
||||
Use the `pg_dump` utility to export the entire database to a file:
|
||||
|
||||
```no-highlight
|
||||
pg_dump netbox > netbox.sql
|
||||
```
|
||||
|
||||
When replicating a production database for development purposes, you may find it convenient to exclude changelog data, which can easily account for the bulk of a database's size. To do this, exclude the `extras_objectchange` table data from the export. The table will still be included in the output file, but will not be populated with any data.
|
||||
|
||||
```no-highlight
|
||||
pg_dump --exclude-table-data=extras_objectchange netbox > netbox.sql
|
||||
```
|
||||
|
||||
## Load an Exported Database
|
||||
|
||||
!!! warning
|
||||
|
||||
@@ -123,7 +123,7 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
group = NestedRackGroupSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
||||
type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False)
|
||||
type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False, allow_null=True)
|
||||
width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
@@ -223,7 +223,7 @@ class NestedManufacturerSerializer(WritableNestedSerializer):
|
||||
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
manufacturer = NestedManufacturerSerializer()
|
||||
interface_ordering = ChoiceField(choices=IFACE_ORDERING_CHOICES, required=False)
|
||||
subdevice_role = ChoiceField(choices=SUBDEVICE_ROLE_CHOICES, required=False)
|
||||
subdevice_role = ChoiceField(choices=SUBDEVICE_ROLE_CHOICES, required=False, allow_null=True)
|
||||
instance_count = serializers.IntegerField(source='instances.count', read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
@@ -362,7 +362,7 @@ class NestedPlatformSerializer(WritableNestedSerializer):
|
||||
#
|
||||
|
||||
# Cannot import ipam.api.NestedIPAddressSerializer due to circular dependency
|
||||
class DeviceIPAddressSerializer(serializers.ModelSerializer):
|
||||
class DeviceIPAddressSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
||||
|
||||
class Meta:
|
||||
@@ -371,7 +371,7 @@ class DeviceIPAddressSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
# Cannot import virtualization.api.NestedClusterSerializer due to circular dependency
|
||||
class NestedClusterSerializer(serializers.ModelSerializer):
|
||||
class NestedClusterSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
|
||||
|
||||
class Meta:
|
||||
@@ -380,7 +380,7 @@ class NestedClusterSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
# Cannot import NestedVirtualChassisSerializer due to circular dependency
|
||||
class DeviceVirtualChassisSerializer(serializers.ModelSerializer):
|
||||
class DeviceVirtualChassisSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||
master = NestedDeviceSerializer()
|
||||
|
||||
@@ -396,7 +396,7 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
||||
site = NestedSiteSerializer()
|
||||
rack = NestedRackSerializer(required=False, allow_null=True)
|
||||
face = ChoiceField(choices=RACK_FACE_CHOICES, required=False)
|
||||
face = ChoiceField(choices=RACK_FACE_CHOICES, required=False, allow_null=True)
|
||||
status = ChoiceField(choices=DEVICE_STATUS_CHOICES, required=False)
|
||||
primary_ip = DeviceIPAddressSerializer(read_only=True)
|
||||
primary_ip4 = DeviceIPAddressSerializer(required=False, allow_null=True)
|
||||
@@ -576,7 +576,7 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
is_connected = serializers.SerializerMethodField(read_only=True)
|
||||
interface_connection = serializers.SerializerMethodField(read_only=True)
|
||||
circuit_termination = InterfaceCircuitTerminationSerializer(read_only=True)
|
||||
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False)
|
||||
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
|
||||
untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
|
||||
tagged_vlans = SerializedPKRelatedField(
|
||||
queryset=VLAN.objects.all(),
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.contrib.postgres.fields import ArrayField, JSONField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Count, Q, ObjectDoesNotExist
|
||||
@@ -1933,11 +1933,20 @@ class Interface(ComponentModel):
|
||||
"""
|
||||
Include the connected Interface (if any).
|
||||
"""
|
||||
|
||||
# It's possible that an Interface can be deleted _after_ its parent Device/VM, in which case trying to resolve
|
||||
# the component parent will raise DoesNotExist. For more discussion, see
|
||||
# https://github.com/digitalocean/netbox/issues/2323
|
||||
try:
|
||||
parent_obj = self.get_component_parent()
|
||||
except ObjectDoesNotExist:
|
||||
parent_obj = None
|
||||
|
||||
ObjectChange(
|
||||
user=user,
|
||||
request_id=request_id,
|
||||
changed_object=self,
|
||||
related_object=self.get_component_parent(),
|
||||
related_object=parent_obj,
|
||||
action=action,
|
||||
object_data=serialize_object(self, extra={
|
||||
'connected_interface': self.connected_interface.pk if self.connection else None,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.urls import reverse
|
||||
from netaddr import IPNetwork
|
||||
from rest_framework import status
|
||||
|
||||
from dcim.constants import (
|
||||
@@ -13,9 +14,10 @@ from dcim.models import (
|
||||
InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup,
|
||||
RackReservation, RackRole, Region, Site, VirtualChassis,
|
||||
)
|
||||
from ipam.models import VLAN
|
||||
from ipam.models import IPAddress, VLAN
|
||||
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||
from utilities.testing import APITestCase
|
||||
from virtualization.models import Cluster, ClusterType
|
||||
|
||||
|
||||
class RegionTest(APITestCase):
|
||||
@@ -1680,14 +1682,28 @@ class DeviceTest(APITestCase):
|
||||
self.devicerole2 = DeviceRole.objects.create(
|
||||
name='Test Device Role 2', slug='test-device-role-2', color='00ff00'
|
||||
)
|
||||
cluster_type = ClusterType.objects.create(name='Test Cluster Type 1', slug='test-cluster-type-1')
|
||||
self.cluster1 = Cluster.objects.create(name='Test Cluster 1', type=cluster_type)
|
||||
self.device1 = Device.objects.create(
|
||||
device_type=self.devicetype1, device_role=self.devicerole1, name='Test Device 1', site=self.site1
|
||||
device_type=self.devicetype1,
|
||||
device_role=self.devicerole1,
|
||||
name='Test Device 1',
|
||||
site=self.site1,
|
||||
cluster=self.cluster1
|
||||
)
|
||||
self.device2 = Device.objects.create(
|
||||
device_type=self.devicetype1, device_role=self.devicerole1, name='Test Device 2', site=self.site1
|
||||
device_type=self.devicetype1,
|
||||
device_role=self.devicerole1,
|
||||
name='Test Device 2',
|
||||
site=self.site1,
|
||||
cluster=self.cluster1
|
||||
)
|
||||
self.device3 = Device.objects.create(
|
||||
device_type=self.devicetype1, device_role=self.devicerole1, name='Test Device 3', site=self.site1
|
||||
device_type=self.devicetype1,
|
||||
device_role=self.devicerole1,
|
||||
name='Test Device 3',
|
||||
site=self.site1,
|
||||
cluster=self.cluster1
|
||||
)
|
||||
|
||||
def test_get_device(self):
|
||||
@@ -1696,6 +1712,8 @@ class DeviceTest(APITestCase):
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['name'], self.device1.name)
|
||||
self.assertEqual(response.data['device_role']['id'], self.devicerole1.pk)
|
||||
self.assertEqual(response.data['cluster']['id'], self.cluster1.pk)
|
||||
|
||||
def test_list_devices(self):
|
||||
|
||||
@@ -1711,6 +1729,7 @@ class DeviceTest(APITestCase):
|
||||
'device_role': self.devicerole1.pk,
|
||||
'name': 'Test Device 4',
|
||||
'site': self.site1.pk,
|
||||
'cluster': self.cluster1.pk,
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:device-list')
|
||||
@@ -1722,7 +1741,8 @@ class DeviceTest(APITestCase):
|
||||
self.assertEqual(device4.device_type_id, data['device_type'])
|
||||
self.assertEqual(device4.device_role_id, data['device_role'])
|
||||
self.assertEqual(device4.name, data['name'])
|
||||
self.assertEqual(device4.site_id, data['site'])
|
||||
self.assertEqual(device4.site.pk, data['site'])
|
||||
self.assertEqual(device4.cluster.pk, data['cluster'])
|
||||
|
||||
def test_create_device_bulk(self):
|
||||
|
||||
@@ -1758,11 +1778,17 @@ class DeviceTest(APITestCase):
|
||||
|
||||
def test_update_device(self):
|
||||
|
||||
interface = Interface.objects.create(name='Test Interface 1', device=self.device1)
|
||||
ip4_address = IPAddress.objects.create(address=IPNetwork('192.0.2.1/24'), interface=interface)
|
||||
ip6_address = IPAddress.objects.create(address=IPNetwork('2001:db8::1/64'), interface=interface)
|
||||
|
||||
data = {
|
||||
'device_type': self.devicetype2.pk,
|
||||
'device_role': self.devicerole2.pk,
|
||||
'name': 'Test Device X',
|
||||
'site': self.site2.pk,
|
||||
'primary_ip4': ip4_address.pk,
|
||||
'primary_ip6': ip6_address.pk,
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:device-detail', kwargs={'pk': self.device1.pk})
|
||||
@@ -1774,7 +1800,9 @@ class DeviceTest(APITestCase):
|
||||
self.assertEqual(device1.device_type_id, data['device_type'])
|
||||
self.assertEqual(device1.device_role_id, data['device_role'])
|
||||
self.assertEqual(device1.name, data['name'])
|
||||
self.assertEqual(device1.site_id, data['site'])
|
||||
self.assertEqual(device1.site.pk, data['site'])
|
||||
self.assertEqual(device1.primary_ip4.pk, data['primary_ip4'])
|
||||
self.assertEqual(device1.primary_ip6.pk, data['primary_ip6'])
|
||||
|
||||
def test_delete_device(self):
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
import importlib
|
||||
import inspect
|
||||
import pkgutil
|
||||
from collections import OrderedDict
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
@@ -16,19 +17,36 @@ def is_report(obj):
|
||||
"""
|
||||
Returns True if the given object is a Report.
|
||||
"""
|
||||
if obj in Report.__subclasses__():
|
||||
return True
|
||||
return False
|
||||
return obj in Report.__subclasses__()
|
||||
|
||||
|
||||
def get_report(module_name, report_name):
|
||||
"""
|
||||
Return a specific report from within a module.
|
||||
"""
|
||||
module = importlib.import_module('reports.{}'.format(module_name))
|
||||
file_path = '{}/{}.py'.format(settings.REPORTS_ROOT, module_name)
|
||||
|
||||
# Python 3.5+
|
||||
if sys.version_info >= (3, 5):
|
||||
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
try:
|
||||
spec.loader.exec_module(module)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
# Python 2.7
|
||||
else:
|
||||
import imp
|
||||
try:
|
||||
module = imp.load_source(module_name, file_path)
|
||||
except IOError:
|
||||
return None
|
||||
|
||||
report = getattr(module, report_name, None)
|
||||
if report is None:
|
||||
return None
|
||||
|
||||
return report()
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
|
||||
from extras.models import Webhook
|
||||
from extras.constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
|
||||
@@ -18,23 +17,16 @@ def enqueue_webhooks(instance, action):
|
||||
if not settings.WEBHOOKS_ENABLED or instance._meta.model_name not in WEBHOOK_MODELS:
|
||||
return
|
||||
|
||||
type_create = action == OBJECTCHANGE_ACTION_CREATE
|
||||
type_update = action == OBJECTCHANGE_ACTION_UPDATE
|
||||
type_delete = action == OBJECTCHANGE_ACTION_DELETE
|
||||
|
||||
# Find assigned webhooks
|
||||
# Retrieve any applicable Webhooks
|
||||
action_flag = {
|
||||
OBJECTCHANGE_ACTION_CREATE: 'type_create',
|
||||
OBJECTCHANGE_ACTION_UPDATE: 'type_update',
|
||||
OBJECTCHANGE_ACTION_DELETE: 'type_delete',
|
||||
}[action]
|
||||
obj_type = ContentType.objects.get_for_model(instance.__class__)
|
||||
webhooks = Webhook.objects.filter(
|
||||
Q(enabled=True) &
|
||||
(
|
||||
Q(type_create=type_create) |
|
||||
Q(type_update=type_update) |
|
||||
Q(type_delete=type_delete)
|
||||
) &
|
||||
Q(obj_type=obj_type)
|
||||
)
|
||||
webhooks = Webhook.objects.filter(obj_type=obj_type, enabled=True, **{action_flag: True})
|
||||
|
||||
if webhooks:
|
||||
if webhooks.exists():
|
||||
# Get the Model's API serializer class and serialize the object
|
||||
serializer_class = get_serializer_for_model(instance.__class__)
|
||||
serializer_context = {
|
||||
|
||||
@@ -37,8 +37,12 @@ def process_webhook(webhook, data, model_class, event, timestamp):
|
||||
prepared_request = requests.Request(**params).prepare()
|
||||
|
||||
if webhook.secret != '':
|
||||
# sign the request with the secret
|
||||
hmac_prep = hmac.new(bytearray(webhook.secret, 'utf8'), prepared_request.body, digestmod=hashlib.sha512)
|
||||
# Sign the request with a hash of the secret key and its content.
|
||||
hmac_prep = hmac.new(
|
||||
key=webhook.secret.encode('utf8'),
|
||||
msg=prepared_request.body.encode('utf8'),
|
||||
digestmod=hashlib.sha512
|
||||
)
|
||||
prepared_request.headers['X-Hook-Signature'] = hmac_prep.hexdigest()
|
||||
|
||||
with requests.Session() as session:
|
||||
|
||||
@@ -258,7 +258,7 @@ class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
status = ChoiceField(choices=IPADDRESS_STATUS_CHOICES, required=False)
|
||||
role = ChoiceField(choices=IPADDRESS_ROLE_CHOICES, required=False)
|
||||
role = ChoiceField(choices=IPADDRESS_ROLE_CHOICES, required=False, allow_null=True)
|
||||
interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ if sys.version_info[0] < 3:
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
VERSION = '2.4.0'
|
||||
VERSION = '2.4.2'
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
@@ -553,7 +553,7 @@
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if interfaces and perms.dcim.delete_interface %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' pk=device.pk %}" class="btn btn-danger btn-xs">
|
||||
<button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
<div class="panel-footer">
|
||||
{% if table.rows %}
|
||||
{% if edit_url %}
|
||||
<button type="submit" name="_edit" formaction="{% url edit_url pk=devicetype.pk %}" class="btn btn-xs btn-warning">
|
||||
<button type="submit" name="_edit" formaction="{% url edit_url pk=devicetype.pk %}?return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-warning">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit Selected
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if delete_url %}
|
||||
<button type="submit" name="_delete" formaction="{% url delete_url pk=devicetype.pk %}" class="btn btn-xs btn-danger">
|
||||
<button type="submit" name="_delete" formaction="{% url delete_url pk=devicetype.pk %}?return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-danger">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<a href="{% if iface.device_id %}{% url 'dcim:interface_edit' pk=iface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=iface.pk %}{% endif %}" class="btn btn-info btn-xs" title="Edit interface">
|
||||
<a href="{% if iface.device_id %}{% url 'dcim:interface_edit' pk=iface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=iface.pk %}{% endif %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs" title="Edit interface">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -194,10 +194,13 @@
|
||||
</small>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% if forloop.last %}
|
||||
<div class="list-group-item text-right">
|
||||
<a href="{% url 'extras:objectchange_list' %}">View All Changes</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% empty %}
|
||||
<div class="list-group-item">
|
||||
Welcome to NetBox! {% if perms.add_site %} <a href="{% url 'dcim:site_add' %}">Add a site</a> to get started.{% endif %}
|
||||
</div>
|
||||
<div class="list-group-item text-muted">No change history found</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% with custom_fields=obj.custom_fields %}
|
||||
{% with custom_fields=obj.get_custom_fields %}
|
||||
{% if custom_fields %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
{% include 'inc/created_updated.html' with obj=vrf %}
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{{ aggregate.get_absolute_url }}">VRF</a>
|
||||
<a href="{{ vrf.get_absolute_url }}">VRF</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url 'ipam:vrf_changelog' pk=vrf.pk %}">Changelog</a>
|
||||
|
||||
@@ -282,12 +282,12 @@
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
<button type="submit" name="_edit" formaction="{% url 'virtualization:interface_bulk_edit' pk=virtualmachine.pk %}" class="btn btn-warning btn-xs">
|
||||
<button type="submit" name="_edit" formaction="{% url 'virtualization:interface_bulk_edit' pk=virtualmachine.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if interfaces and perms.dcim.delete_interface %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'virtualization:interface_bulk_delete' pk=virtualmachine.pk %}" class="btn btn-danger btn-xs">
|
||||
<button type="submit" name="_delete" formaction="{% url 'virtualization:interface_bulk_delete' pk=virtualmachine.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
@@ -74,6 +74,12 @@ class ChoiceField(Field):
|
||||
return {'value': obj, 'label': self._choices[obj]}
|
||||
|
||||
def to_internal_value(self, data):
|
||||
# Hotwiring boolean values
|
||||
if hasattr(data, 'lower'):
|
||||
if data.lower() == 'true':
|
||||
return True
|
||||
if data.lower() == 'false':
|
||||
return False
|
||||
return data
|
||||
|
||||
|
||||
|
||||
@@ -101,8 +101,8 @@ def serialize_object(obj, extra=None):
|
||||
}
|
||||
|
||||
# Include any tags
|
||||
# if hasattr(obj, 'tags'):
|
||||
# data['tags'] = [tag.name for tag in obj.tags.all()]
|
||||
if hasattr(obj, 'tags'):
|
||||
data['tags'] = [tag.name for tag in obj.tags.all()]
|
||||
|
||||
# Append any extra data
|
||||
if extra is not None:
|
||||
|
||||
@@ -82,7 +82,7 @@ class NestedClusterSerializer(WritableNestedSerializer):
|
||||
#
|
||||
|
||||
# Cannot import ipam.api.NestedIPAddressSerializer due to circular dependency
|
||||
class VirtualMachineIPAddressSerializer(serializers.ModelSerializer):
|
||||
class VirtualMachineIPAddressSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
||||
|
||||
class Meta:
|
||||
@@ -146,7 +146,7 @@ class InterfaceVLANSerializer(WritableNestedSerializer):
|
||||
class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
virtual_machine = NestedVirtualMachineSerializer()
|
||||
form_factor = ChoiceField(choices=IFACE_FF_CHOICES, default=IFACE_FF_VIRTUAL, required=False)
|
||||
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False)
|
||||
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
|
||||
untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
|
||||
tagged_vlans = SerializedPKRelatedField(
|
||||
queryset=VLAN.objects.all(),
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.urls import reverse
|
||||
from netaddr import IPNetwork
|
||||
from rest_framework import status
|
||||
|
||||
from dcim.constants import IFACE_FF_VIRTUAL, IFACE_MODE_TAGGED
|
||||
from dcim.models import Interface
|
||||
from ipam.models import VLAN
|
||||
from ipam.models import IPAddress, VLAN
|
||||
from utilities.testing import APITestCase
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||
|
||||
@@ -367,6 +368,10 @@ class VirtualMachineTest(APITestCase):
|
||||
|
||||
def test_update_virtualmachine(self):
|
||||
|
||||
interface = Interface.objects.create(name='Test Interface 1', virtual_machine=self.virtualmachine1)
|
||||
ip4_address = IPAddress.objects.create(address=IPNetwork('192.0.2.1/24'), interface=interface)
|
||||
ip6_address = IPAddress.objects.create(address=IPNetwork('2001:db8::1/64'), interface=interface)
|
||||
|
||||
cluster2 = Cluster.objects.create(
|
||||
name='Test Cluster 2',
|
||||
type=ClusterType.objects.first(),
|
||||
@@ -375,6 +380,8 @@ class VirtualMachineTest(APITestCase):
|
||||
data = {
|
||||
'name': 'Test Virtual Machine X',
|
||||
'cluster': cluster2.pk,
|
||||
'primary_ip4': ip4_address.pk,
|
||||
'primary_ip6': ip6_address.pk,
|
||||
}
|
||||
|
||||
url = reverse('virtualization-api:virtualmachine-detail', kwargs={'pk': self.virtualmachine1.pk})
|
||||
@@ -385,6 +392,8 @@ class VirtualMachineTest(APITestCase):
|
||||
virtualmachine1 = VirtualMachine.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(virtualmachine1.name, data['name'])
|
||||
self.assertEqual(virtualmachine1.cluster.pk, data['cluster'])
|
||||
self.assertEqual(virtualmachine1.primary_ip4.pk, data['primary_ip4'])
|
||||
self.assertEqual(virtualmachine1.primary_ip6.pk, data['primary_ip6'])
|
||||
|
||||
def test_delete_virtualmachine(self):
|
||||
|
||||
|
||||
Reference in New Issue
Block a user