diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index b360108bf..05dc0ea6f 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -706,7 +706,7 @@ class PlatformCSVForm(forms.ModelForm): slug = SlugField() manufacturer = forms.ModelChoiceField( queryset=Manufacturer.objects.all(), - required=True, + required=False, to_field_name='name', help_text='Manufacturer name', error_messages={ diff --git a/netbox/dcim/querysets.py b/netbox/dcim/querysets.py index 3e977ddc6..32275ce01 100644 --- a/netbox/dcim/querysets.py +++ b/netbox/dcim/querysets.py @@ -43,13 +43,13 @@ class InterfaceQuerySet(QuerySet): }[method] TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')" - ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)" - SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?([0-9]+)\/') AS integer)" - SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/)([0-9]+)') AS integer), 0)" - POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{2}}([0-9]+)') AS integer), 0)" - SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{3}}([0-9]+)') AS integer), 0)" - CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)" - VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)" + ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(\d{{1,9}})$') AS integer)" + SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(\d{{1,9}})\/') AS integer)" + SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/)(\d{{1,9}})') AS integer), 0)" + POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/){{2}}(\d{{1,9}})') AS integer), 0)" + SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/){{3}}(\d{{1,9}})') AS integer), 0)" + CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':(\d{{1,9}})(\.\d{{1,9}})?$') AS integer), 0)" + VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.(\d{{1,9}})$') AS integer), 0)" fields = { '_type': RawSQL(TYPE_RE.format(sql_col), []), diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 5b2c6e672..3353d981f 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -508,7 +508,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) ipaddress = super(IPAddressForm, self).save(*args, **kwargs) - # Assign this IPAddress as the primary for the associated Device. + # Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine. if self.cleaned_data['primary_for_parent']: parent = self.cleaned_data['interface'].parent if ipaddress.address.version == 4: @@ -516,14 +516,12 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) else: parent.primary_ip6 = ipaddress parent.save() - - # Clear assignment as primary for device if set. elif self.cleaned_data['interface']: parent = self.cleaned_data['interface'].parent - if ipaddress.address.version == 4 and parent.primary_ip4 == self: + if ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress: parent.primary_ip4 = None parent.save() - elif ipaddress.address.version == 6 and parent.primary_ip6 == self: + elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress: parent.primary_ip6 = None parent.save() diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 8abc92e23..7a90506b4 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -22,7 +22,7 @@ if sys.version_info[0] < 3: DeprecationWarning ) -VERSION = '2.3.3-dev' +VERSION = '2.4-dev' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index d64af0105..0992a3460 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -205,7 +205,8 @@ class ArrayFieldSelectMultiple(SelectWithDisabled, forms.SelectMultiple): def optgroups(self, name, value, attrs=None): # Split the delimited string of values into a list - value = value[0].split(self.delimiter) + if value: + value = value[0].split(self.delimiter) return super(ArrayFieldSelectMultiple, self).optgroups(name, value, attrs) def value_from_datadict(self, data, files, name): diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index c08bfef8c..9e96a66fd 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -14,7 +14,7 @@ def csv_format(data): for value in data: # Represent None or False with empty string - if value in [None, False]: + if value is None or value is False: csv.append('') continue diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 267526fe0..8cee708ba 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -3,10 +3,10 @@ from __future__ import unicode_literals from rest_framework import serializers from dcim.api.serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer -from dcim.constants import IFACE_FF_VIRTUAL +from dcim.constants import IFACE_FF_VIRTUAL, IFACE_MODE_CHOICES from dcim.models import Interface from extras.api.customfields import CustomFieldModelSerializer -from ipam.models import IPAddress +from ipam.models import IPAddress, VLAN from tenancy.api.serializers import NestedTenantSerializer from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer, WritableNestedSerializer from virtualization.constants import VM_STATUS_CHOICES @@ -116,14 +116,26 @@ class NestedVirtualMachineSerializer(WritableNestedSerializer): # VM interfaces # -class InterfaceSerializer(ValidatedModelSerializer): +# Cannot import ipam.api.serializers.NestedVLANSerializer due to circular dependency +class InterfaceVLANSerializer(serializers.ModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail') + + class Meta: + model = VLAN + fields = ['id', 'url', 'vid', 'name', 'display_name'] + + +class InterfaceSerializer(serializers.ModelSerializer): virtual_machine = NestedVirtualMachineSerializer() - form_factor = serializers.IntegerField(default=IFACE_FF_VIRTUAL) + mode = ChoiceFieldSerializer(choices=IFACE_MODE_CHOICES) + untagged_vlan = InterfaceVLANSerializer() + tagged_vlans = InterfaceVLANSerializer(many=True) class Meta: model = Interface fields = [ - 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'description', + 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'untagged_vlan', 'tagged_vlans', + 'description', ]