Exemple #1
0
class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
    site = forms.ModelChoiceField(
        queryset=Site.objects.all(),
        required=False,
        label='Site',
        widget=forms.Select(attrs={
            'filter-for': 'vlan_group',
            'nullable': 'true'
        }))
    vlan_group = ChainedModelChoiceField(
        queryset=VLANGroup.objects.all(),
        chains=(('site', 'site'), ),
        required=False,
        label='VLAN group',
        widget=APISelect(api_url='/api/ipam/vlan-groups/?site_id={{site}}',
                         attrs={
                             'filter-for': 'vlan',
                             'nullable': 'true'
                         }))
    vlan = ChainedModelChoiceField(
        queryset=VLAN.objects.all(),
        chains=(
            ('site', 'site'),
            ('group', 'vlan_group'),
        ),
        required=False,
        label='VLAN',
        widget=APISelect(
            api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}',
            display_field='display_name'))
    tags = TagField(required=False)

    class Meta:
        model = Prefix
        fields = [
            'prefix',
            'vrf',
            'site',
            'vlan',
            'status',
            'role',
            'is_pool',
            'description',
            'tenant_group',
            'tenant',
            'tags',
        ]

    def __init__(self, *args, **kwargs):

        # Initialize helper selectors
        instance = kwargs.get('instance')
        initial = kwargs.get('initial', {}).copy()
        if instance and instance.vlan is not None:
            initial['vlan_group'] = instance.vlan.group
        kwargs['initial'] = initial

        super(PrefixForm, self).__init__(*args, **kwargs)

        self.fields['vrf'].empty_label = 'Global'
Exemple #2
0
class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
    region = TreeNodeChoiceField(queryset=Region.objects.all(),
                                 required=False,
                                 widget=forms.Select(attrs={
                                     'filter-for': 'site',
                                     'nullable': 'true'
                                 }))
    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'),
        ),
        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, cluster, *args, **kwargs):

        self.cluster = cluster

        super(ClusterAddDevicesForm, self).__init__(*args, **kwargs)

        self.fields['devices'].choices = []

    def clean(self):

        super(ClusterAddDevicesForm, self).clean()

        # If the Cluster is assigned to a Site, all Devices must be assigned to that Site.
        if self.cluster.site is not None:
            for device in self.cleaned_data.get('devices', []):
                if device.site != self.cluster.site:
                    raise ValidationError({
                        'devices':
                        "{} belongs to a different site ({}) than the cluster ({})"
                        .format(device, device.site, self.cluster.site)
                    })
Exemple #3
0
class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
    site = forms.ModelChoiceField(queryset=Site.objects.all(),
                                  required=False,
                                  widget=forms.Select(attrs={
                                      'filter-for': 'group',
                                      'nullable': 'true'
                                  }))
    group = ChainedModelChoiceField(
        queryset=VLANGroup.objects.all(),
        chains=(('site', 'site'), ),
        required=False,
        label='Group',
        widget=APISelect(api_url='/api/ipam/vlan-groups/?site_id={{site}}', ))

    class Meta:
        model = VLAN
        fields = [
            'site', 'group', 'vid', 'name', 'status', 'role', 'description',
            'tenant_group', 'tenant'
        ]
        help_texts = {
            'site': "Leave blank if this VLAN spans multiple sites",
            'group': "VLAN group (optional)",
            'vid': "Configured VLAN ID",
            'name': "Configured VLAN name",
            'status': "Operational status of this VLAN",
            'role': "The primary function of this VLAN",
        }
Exemple #4
0
class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
    cluster_group = forms.ModelChoiceField(
        queryset=ClusterGroup.objects.all(),
        required=False,
        widget=forms.Select(
            attrs={'filter-for': 'cluster', 'nullable': 'true'}
        )
    )
    cluster = ChainedModelChoiceField(
        queryset=Cluster.objects.all(),
        chains=(
            ('group', 'cluster_group'),
        ),
        widget=APISelect(
            api_url='/api/virtualization/clusters/?group_id={{cluster_group}}'
        )
    )

    class Meta:
        model = VirtualMachine
        fields = [
            'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk',
            'comments',
        ]

    def __init__(self, *args, **kwargs):

        # Initialize helper selector
        instance = kwargs.get('instance')
        if instance.pk and instance.cluster is not None:
            initial = kwargs.get('initial', {}).copy()
            initial['cluster_group'] = instance.cluster.group
            kwargs['initial'] = initial

        super(VirtualMachineForm, self).__init__(*args, **kwargs)
Exemple #5
0
class TenancyForm(ChainedFieldsMixin, forms.Form):
    tenant_group = forms.ModelChoiceField(
        queryset=TenantGroup.objects.all(),
        required=False,
        widget=forms.Select(
            attrs={'filter-for': 'tenant', 'nullable': 'true'}
        )
    )
    tenant = ChainedModelChoiceField(
        queryset=Tenant.objects.all(),
        chains=(
            ('group', 'tenant_group'),
        ),
        required=False,
        widget=APISelect(
            api_url='/api/tenancy/tenants/?group_id={{tenant_group}}'
        )
    )

    def __init__(self, *args, **kwargs):

        # Initialize helper selector
        instance = kwargs.get('instance')
        if instance and instance.tenant is not None:
            initial = kwargs.get('initial', {}).copy()
            initial['tenant_group'] = instance.tenant.group
            kwargs['initial'] = initial

        super(TenancyForm, self).__init__(*args, **kwargs)
Exemple #6
0
class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
    site = forms.ModelChoiceField(queryset=Site.objects.all(),
                                  required=False,
                                  label='Site',
                                  widget=forms.Select(attrs={
                                      'filter-for': 'vlan',
                                      'nullable': 'true'
                                  }))
    vlan = ChainedModelChoiceField(
        queryset=VLAN.objects.all(),
        chains={'site': 'site'},
        required=False,
        label='VLAN',
        widget=APISelect(api_url='/api/ipam/vlans/?site_id={{site}}',
                         display_field='display_name'))

    class Meta:
        model = Prefix
        fields = [
            'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool',
            'description', 'tenant_group', 'tenant'
        ]

    def __init__(self, *args, **kwargs):
        super(PrefixForm, self).__init__(*args, **kwargs)

        self.fields['vrf'].empty_label = 'Global'
Exemple #7
0
class VRFForm(BootstrapMixin, CustomFieldForm):
    tags = TagField(required=False)
    tenant_group = forms.ModelChoiceField(
        queryset=TenantGroup.objects.all(),
        required=False,
        widget=forms.Select(
            attrs={'filter-for': 'tenant', 'nullable': 'true'}
        )
    )
    tenant = ChainedModelChoiceField(
        queryset=Tenant.objects.all(),
        chains=(
            ('group', 'tenant_group'),
        ),
        required=False,
        widget=APISelect(
            api_url='/api/tenancy/tenants/?group_id={{tenant_group}}',
            attrs={'filter-for': 'device', 'nullable': 'true'},
        )
    )
    device = ChainedModelChoiceField(
        queryset=Device.objects.all(),
        chains=(
            ('tenant', 'tenant'),
        ),
        required=True,
        widget=APISelect(
            api_url='/api/dcim/devices/?tenant_id={{tenant}}'
        )
    )

    class Meta:
        model = VRF
        fields = ['name', 'rd', 'enforce_unique', 'description', 'tenant_group', 'tenant', 'device', 'tags']
        labels = {
            'rd': "RD",
        }
        help_texts = {
            'rd': "Route distinguisher in any format",
            'device': "Device on which the VRF is configured"
        }
Exemple #8
0
class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm,
                    CustomFieldForm):
    interface = forms.ModelChoiceField(queryset=Interface.objects.all(),
                                       required=False)
    nat_site = forms.ModelChoiceField(
        queryset=Site.objects.all(),
        required=False,
        label='Site',
        widget=forms.Select(attrs={'filter-for': 'nat_rack'}))
    nat_rack = ChainedModelChoiceField(
        queryset=Rack.objects.all(),
        chains=(('site', 'nat_site'), ),
        required=False,
        label='Rack',
        widget=APISelect(api_url='/api/dcim/racks/?site_id={{nat_site}}',
                         display_field='display_name',
                         attrs={
                             'filter-for': 'nat_device',
                             'nullable': 'true'
                         }))
    nat_device = ChainedModelChoiceField(
        queryset=Device.objects.all(),
        chains=(
            ('site', 'nat_site'),
            ('rack', 'nat_rack'),
        ),
        required=False,
        label='Device',
        widget=APISelect(
            api_url=
            '/api/dcim/devices/?site_id={{nat_site}}&rack_id={{nat_rack}}',
            display_field='display_name',
            attrs={'filter-for': 'nat_inside'}))
    nat_inside = ChainedModelChoiceField(
        queryset=IPAddress.objects.all(),
        chains=(('interface__device', 'nat_device'), ),
        required=False,
        label='IP Address',
        widget=APISelect(
            api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}',
            display_field='address'))
    livesearch = forms.CharField(required=False,
                                 label='Search',
                                 widget=Livesearch(
                                     query_key='q',
                                     query_url='ipam-api:ipaddress-list',
                                     field_to_update='nat_inside',
                                     obj_label='address'))
    primary_for_parent = forms.BooleanField(
        required=False, label='Make this the primary IP for the device/VM')

    class Meta:
        model = IPAddress
        fields = [
            'address',
            'vrf',
            'status',
            'role',
            'description',
            'interface',
            'primary_for_parent',
            'nat_site',
            'nat_rack',
            'nat_inside',
            'tenant_group',
            'tenant',
        ]

    def __init__(self, *args, **kwargs):

        # Initialize helper selectors
        instance = kwargs.get('instance')
        initial = kwargs.get('initial', {}).copy()
        if instance and instance.nat_inside and instance.nat_inside.device is not None:
            initial['nat_site'] = instance.nat_inside.device.site
            initial['nat_rack'] = instance.nat_inside.device.rack
            initial['nat_device'] = instance.nat_inside.device
        kwargs['initial'] = initial

        super(IPAddressForm, self).__init__(*args, **kwargs)

        self.fields['vrf'].empty_label = 'Global'

        # Limit interface selections to those belonging to the parent device/VM
        if self.instance and self.instance.interface:
            self.fields['interface'].queryset = Interface.objects.filter(
                device=self.instance.interface.device,
                virtual_machine=self.instance.interface.virtual_machine)
        else:
            self.fields['interface'].choices = []

        # Initialize primary_for_parent if IP address is already assigned
        if self.instance.pk and self.instance.interface is not None:
            parent = self.instance.interface.parent
            if (self.instance.address.version == 4
                    and parent.primary_ip4_id == self.instance.pk
                    or self.instance.address.version == 6
                    and parent.primary_ip6_id == self.instance.pk):
                self.initial['primary_for_parent'] = True

    def clean(self):
        super(IPAddressForm, self).clean()

        # Primary IP assignment is only available if an interface has been assigned.
        if self.cleaned_data.get('primary_for_parent'
                                 ) and not self.cleaned_data.get('interface'):
            self.add_error(
                'primary_for_parent',
                "Only IP addresses assigned to an interface can be designated as primary IPs."
            )

    def save(self, *args, **kwargs):

        ipaddress = super(IPAddressForm, self).save(*args, **kwargs)

        # Assign this IPAddress as the primary for the associated Device.
        if self.cleaned_data['primary_for_parent']:
            parent = self.cleaned_data['interface'].parent
            if ipaddress.address.version == 4:
                parent.primary_ip4 = ipaddress
            else:
                parent.primary_ip6 = ipaddress
            parent.save()

        # Clear assignment as primary for device if set.
        else:
            try:
                if ipaddress.address.version == 4:
                    device = ipaddress.primary_ip4_for
                    device.primary_ip4 = None
                else:
                    device = ipaddress.primary_ip6_for
                    device.primary_ip6 = None
                device.save()
            except Device.DoesNotExist:
                pass

        return ipaddress
Exemple #9
0
class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm,
                    CustomFieldForm):
    interface = forms.ModelChoiceField(queryset=Interface.objects.all(),
                                       required=False)
    nat_site = forms.ModelChoiceField(queryset=Site.objects.all(),
                                      required=False,
                                      label='Site',
                                      widget=APISelect(
                                          api_url="/api/dcim/sites/",
                                          filter_for={
                                              'nat_rack': 'site_id',
                                              'nat_device': 'site_id'
                                          }))
    nat_rack = ChainedModelChoiceField(
        queryset=Rack.objects.all(),
        chains=(('site', 'nat_site'), ),
        required=False,
        label='Rack',
        widget=APISelect(api_url='/api/dcim/racks/',
                         display_field='display_name',
                         filter_for={'nat_device': 'rack_id'},
                         attrs={'nullable': 'true'}))
    nat_device = ChainedModelChoiceField(
        queryset=Device.objects.all(),
        chains=(
            ('site', 'nat_site'),
            ('rack', 'nat_rack'),
        ),
        required=False,
        label='Device',
        widget=APISelect(api_url='/api/dcim/devices/',
                         display_field='display_name',
                         filter_for={'nat_inside': 'device_id'}))
    nat_vrf = forms.ModelChoiceField(queryset=VRF.objects.all(),
                                     required=False,
                                     label='VRF',
                                     widget=APISelect(
                                         api_url="/api/ipam/vrfs/",
                                         filter_for={'nat_inside': 'vrf_id'}))
    nat_inside = ChainedModelChoiceField(
        queryset=IPAddress.objects.all(),
        chains=(('interface__device', 'nat_device'), ),
        required=False,
        label='IP Address',
        widget=APISelect(api_url='/api/ipam/ip-addresses/',
                         display_field='address'))
    primary_for_parent = forms.BooleanField(
        required=False, label='Make this the primary IP for the device/VM')
    tags = TagField(required=False)

    class Meta:
        model = IPAddress
        fields = [
            'address',
            'vrf',
            'status',
            'role',
            'dns_name',
            'description',
            'interface',
            'primary_for_parent',
            'nat_site',
            'nat_rack',
            'nat_inside',
            'tenant_group',
            'tenant',
            'tags',
        ]
        widgets = {
            'status': StaticSelect2(),
            'role': StaticSelect2(),
            'vrf': APISelect(api_url="/api/ipam/vrfs/")
        }

    def __init__(self, *args, **kwargs):

        # Initialize helper selectors
        instance = kwargs.get('instance')
        initial = kwargs.get('initial', {}).copy()
        if instance and instance.nat_inside and instance.nat_inside.device is not None:
            initial['nat_site'] = instance.nat_inside.device.site
            initial['nat_rack'] = instance.nat_inside.device.rack
            initial['nat_device'] = instance.nat_inside.device
        kwargs['initial'] = initial

        super().__init__(*args, **kwargs)

        self.fields['vrf'].empty_label = 'Global'

        # Limit interface selections to those belonging to the parent device/VM
        if self.instance and self.instance.interface:
            self.fields['interface'].queryset = Interface.objects.filter(
                device=self.instance.interface.device,
                virtual_machine=self.instance.interface.virtual_machine)
        else:
            self.fields['interface'].choices = []

        # Initialize primary_for_parent if IP address is already assigned
        if self.instance.pk and self.instance.interface is not None:
            parent = self.instance.interface.parent
            if (self.instance.address.version == 4
                    and parent.primary_ip4_id == self.instance.pk
                    or self.instance.address.version == 6
                    and parent.primary_ip6_id == self.instance.pk):
                self.initial['primary_for_parent'] = True

    def clean(self):
        super().clean()

        # Primary IP assignment is only available if an interface has been assigned.
        if self.cleaned_data.get('primary_for_parent'
                                 ) and not self.cleaned_data.get('interface'):
            self.add_error(
                'primary_for_parent',
                "Only IP addresses assigned to an interface can be designated as primary IPs."
            )

    def save(self, *args, **kwargs):

        ipaddress = super().save(*args, **kwargs)

        # 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:
                parent.primary_ip4 = ipaddress
            else:
                parent.primary_ip6 = ipaddress
            parent.save()
        elif self.cleaned_data['interface']:
            parent = self.cleaned_data['interface'].parent
            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 == ipaddress:
                parent.primary_ip6 = None
                parent.save()

        return ipaddress
Exemple #10
0
class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
    cluster_group = forms.ModelChoiceField(
        queryset=ClusterGroup.objects.all(),
        required=False,
        widget=forms.Select(attrs={
            'filter-for': 'cluster',
            'nullable': 'true'
        }))
    cluster = ChainedModelChoiceField(
        queryset=Cluster.objects.all(),
        chains=(('group', 'cluster_group'), ),
        widget=APISelect(
            api_url='/api/virtualization/clusters/?group_id={{cluster_group}}')
    )

    class Meta:
        model = VirtualMachine
        fields = [
            'name',
            'status',
            'cluster_group',
            'cluster',
            'role',
            'tenant',
            'platform',
            'primary_ip4',
            'primary_ip6',
            'vcpus',
            'memory',
            'disk',
            'comments',
        ]

    def __init__(self, *args, **kwargs):

        # Initialize helper selector
        instance = kwargs.get('instance')
        if instance.pk and instance.cluster is not None:
            initial = kwargs.get('initial', {}).copy()
            initial['cluster_group'] = instance.cluster.group
            kwargs['initial'] = initial

        super(VirtualMachineForm, self).__init__(*args, **kwargs)

        if self.instance.pk:

            # Compile list of choices for primary IPv4 and IPv6 addresses
            for family in [4, 6]:
                ip_choices = [(None, '---------')]
                # Collect interface IPs
                interface_ips = IPAddress.objects.select_related(
                    'interface').filter(
                        family=family,
                        interface__virtual_machine=self.instance)
                if interface_ips:
                    ip_choices.append(('Interface IPs', [
                        (ip.id, '{} ({})'.format(ip.address, ip.interface))
                        for ip in interface_ips
                    ]))
                # Collect NAT IPs
                nat_ips = IPAddress.objects.select_related(
                    'nat_inside').filter(
                        family=family,
                        nat_inside__interface__virtual_machine=self.instance)
                if nat_ips:
                    ip_choices.append(('NAT IPs', [
                        (ip.id, '{} ({})'.format(ip.address,
                                                 ip.nat_inside.address))
                        for ip in nat_ips
                    ]))
                self.fields['primary_ip{}'.format(family)].choices = ip_choices

        else:

            # An object that doesn't exist yet can't have any IPs assigned to it
            self.fields['primary_ip4'].choices = []
            self.fields['primary_ip4'].widget.attrs['readonly'] = True
            self.fields['primary_ip6'].choices = []
            self.fields['primary_ip6'].widget.attrs['readonly'] = True
Exemple #11
0
class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
    cluster_group = forms.ModelChoiceField(
        queryset=ClusterGroup.objects.all(),
        required=False,
        widget=APISelect(
            api_url='/api/virtualization/cluster-groups/',
            filter_for={
                "cluster": "group_id",
            },
            attrs={
                'nullable': 'true',
            }
        )
    )
    cluster = ChainedModelChoiceField(
        queryset=Cluster.objects.all(),
        chains=(
            ('group', 'cluster_group'),
        ),
        widget=APISelect(
            api_url='/api/virtualization/clusters/'
        )
    )
    tags = TagField(
        required=False
    )
    local_context_data = JSONField(
        required=False,
        label=''
    )

    class Meta:
        model = VirtualMachine
        fields = [
            'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4',
            'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data',
        ]
        help_texts = {
            'local_context_data': "Local config context data overwrites all sources contexts in the final rendered "
                                  "config context",
        }
        widgets = {
            "status": StaticSelect2(),
            "role": APISelect(
                api_url="/api/dcim/device-roles/",
                additional_query_params={
                    "vm_role": "True"
                }
            ),
            'primary_ip4': StaticSelect2(),
            'primary_ip6': StaticSelect2(),
            'platform': APISelect(
                api_url='/api/dcim/platforms/'
            )
        }

    def __init__(self, *args, **kwargs):

        # Initialize helper selector
        instance = kwargs.get('instance')
        if instance.pk and instance.cluster is not None:
            initial = kwargs.get('initial', {}).copy()
            initial['cluster_group'] = instance.cluster.group
            kwargs['initial'] = initial

        super().__init__(*args, **kwargs)

        if self.instance.pk:

            # Compile list of choices for primary IPv4 and IPv6 addresses
            for family in [4, 6]:
                ip_choices = [(None, '---------')]
                # Collect interface IPs
                interface_ips = IPAddress.objects.prefetch_related('interface').filter(
                    family=family, interface__virtual_machine=self.instance
                )
                if interface_ips:
                    ip_choices.append(
                        ('Interface IPs', [
                            (ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips
                        ])
                    )
                # Collect NAT IPs
                nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
                    family=family, nat_inside__interface__virtual_machine=self.instance
                )
                if nat_ips:
                    ip_choices.append(
                        ('NAT IPs', [
                            (ip.id, '{} ({})'.format(ip.address, ip.nat_inside.address)) for ip in nat_ips
                        ])
                    )
                self.fields['primary_ip{}'.format(family)].choices = ip_choices

        else:

            # An object that doesn't exist yet can't have any IPs assigned to it
            self.fields['primary_ip4'].choices = []
            self.fields['primary_ip4'].widget.attrs['readonly'] = True
            self.fields['primary_ip6'].choices = []
            self.fields['primary_ip6'].widget.attrs['readonly'] = True
Exemple #12
0
class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin,
                             forms.ModelForm):
    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,
        label='Rack',
        widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}',
                         attrs={
                             'filter-for': 'device',
                             'nullable': 'true'
                         }))
    device = ChainedModelChoiceField(
        queryset=Device.objects.all(),
        chains=(
            ('site', 'site'),
            ('rack', 'rack'),
        ),
        required=False,
        label='Device',
        widget=APISelect(
            api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
            display_field='display_name',
            attrs={'filter-for': 'interface'}))
    interface = ChainedModelChoiceField(
        queryset=Interface.objects.exclude(
            form_factor__in=VIRTUAL_IFACE_TYPES).select_related(
                'circuit_termination', 'connected_as_a', 'connected_as_b'),
        chains=(('device', 'device'), ),
        required=False,
        label='Interface',
        widget=APISelect(
            api_url='/api/dcim/interfaces/?device_id={{device}}&type=physical',
            disabled_indicator='connection'))

    class Meta:
        model = CircuitTermination
        fields = [
            'term_side',
            'site',
            'rack',
            'device',
            'interface',
            'port_speed',
            'upstream_speed',
            'xconnect_id',
            'pp_info',
        ]
        help_texts = {
            'port_speed': "Physical circuit speed",
            'xconnect_id': "ID of the local cross-connect",
            'pp_info': "Patch panel ID and port number(s)"
        }
        widgets = {
            'term_side': forms.HiddenInput(),
        }

    def __init__(self, *args, **kwargs):

        # Initialize helper selectors
        instance = kwargs.get('instance')
        if instance and instance.interface is not None:
            initial = kwargs.get('initial', {})
            initial['rack'] = instance.interface.device.rack
            initial['device'] = instance.interface.device
            kwargs['initial'] = initial

        super(CircuitTerminationForm, self).__init__(*args, **kwargs)

        # Mark connected interfaces as disabled
        self.fields['interface'].choices = []
        for iface in self.fields['interface'].queryset:
            self.fields['interface'].choices.append((iface.id, {
                'label':
                iface.name,
                'disabled':
                iface.is_connected
                and iface.pk != self.initial.get('interface'),
            }))