We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Bulk edit of Interfaces of VMs without cluster fails
### NetBox version

v3.6.5

### Python version

3.8

### Steps to Reproduce

1. Create VM and assign only a Site, not a Cluster
2. Create Interface for VM
3. Mark Interface and click on "Edit Selected"

### Expected Behavior

Edit form for selected VM Interface(s) appear

### Observed Behavior

Exception Window with the following Content:
```
<class 'AttributeError'>

'NoneType' object has no attribute 'site'

Python version: 3.8.10
NetBox version: 3.6.5
Plugins: 
  netbox_demo: 0.3.1
```

When generating the bulk edit form, the site is extracted from the cluster of the virtual machine, which fails if no cluster is assigned to the VM:
```
  File "/opt/netbox/netbox/virtualization/forms/bulk_edit.py", line 272, in __init__
     site = interface.virtual_machine.cluster.site
AttributeError: 'NoneType' object has no attribute 'site'
```

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `netbox/virtualization/forms/bulk_edit.py`
Content:
```
1 from django import forms
2 from django.utils.translation import gettext_lazy as _
3 
4 from dcim.choices import InterfaceModeChoices
5 from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN
6 from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
7 from extras.models import ConfigTemplate
8 from ipam.models import VLAN, VLANGroup, VRF
9 from netbox.forms import NetBoxModelBulkEditForm
10 from tenancy.models import Tenant
11 from utilities.forms import BulkRenameForm, add_blank_choice
12 from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
13 from utilities.forms.widgets import BulkEditNullBooleanSelect
14 from virtualization.choices import *
15 from virtualization.models import *
16 
17 __all__ = (
18     'ClusterBulkEditForm',
19     'ClusterGroupBulkEditForm',
20     'ClusterTypeBulkEditForm',
21     'VirtualMachineBulkEditForm',
22     'VMInterfaceBulkEditForm',
23     'VMInterfaceBulkRenameForm',
24 )
25 
26 
27 class ClusterTypeBulkEditForm(NetBoxModelBulkEditForm):
28     description = forms.CharField(
29         label=_('Description'),
30         max_length=200,
31         required=False
32     )
33 
34     model = ClusterType
35     fieldsets = (
36         (None, ('description',)),
37     )
38     nullable_fields = ('description',)
39 
40 
41 class ClusterGroupBulkEditForm(NetBoxModelBulkEditForm):
42     description = forms.CharField(
43         label=_('Description'),
44         max_length=200,
45         required=False
46     )
47 
48     model = ClusterGroup
49     fieldsets = (
50         (None, ('description',)),
51     )
52     nullable_fields = ('description',)
53 
54 
55 class ClusterBulkEditForm(NetBoxModelBulkEditForm):
56     type = DynamicModelChoiceField(
57         label=_('Type'),
58         queryset=ClusterType.objects.all(),
59         required=False
60     )
61     group = DynamicModelChoiceField(
62         label=_('Group'),
63         queryset=ClusterGroup.objects.all(),
64         required=False
65     )
66     status = forms.ChoiceField(
67         label=_('Status'),
68         choices=add_blank_choice(ClusterStatusChoices),
69         required=False,
70         initial=''
71     )
72     tenant = DynamicModelChoiceField(
73         label=_('Tenant'),
74         queryset=Tenant.objects.all(),
75         required=False
76     )
77     region = DynamicModelChoiceField(
78         label=_('Region'),
79         queryset=Region.objects.all(),
80         required=False,
81     )
82     site_group = DynamicModelChoiceField(
83         label=_('Site group'),
84         queryset=SiteGroup.objects.all(),
85         required=False,
86     )
87     site = DynamicModelChoiceField(
88         label=_('Site'),
89         queryset=Site.objects.all(),
90         required=False,
91         query_params={
92             'region_id': '$region',
93             'group_id': '$site_group',
94         }
95     )
96     description = forms.CharField(
97         label=_('Site'),
98         max_length=200,
99         required=False
100     )
101     comments = CommentField()
102 
103     model = Cluster
104     fieldsets = (
105         (None, ('type', 'group', 'status', 'tenant', 'description')),
106         (_('Site'), ('region', 'site_group', 'site')),
107     )
108     nullable_fields = (
109         'group', 'site', 'tenant', 'description', 'comments',
110     )
111 
112 
113 class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
114     status = forms.ChoiceField(
115         label=_('Status'),
116         choices=add_blank_choice(VirtualMachineStatusChoices),
117         required=False,
118         initial='',
119     )
120     site = DynamicModelChoiceField(
121         label=_('Site'),
122         queryset=Site.objects.all(),
123         required=False
124     )
125     cluster = DynamicModelChoiceField(
126         label=_('Cluster'),
127         queryset=Cluster.objects.all(),
128         required=False,
129         query_params={
130             'site_id': '$site'
131         }
132     )
133     device = DynamicModelChoiceField(
134         label=_('Device'),
135         queryset=Device.objects.all(),
136         required=False,
137         query_params={
138             'cluster_id': '$cluster'
139         }
140     )
141     role = DynamicModelChoiceField(
142         label=_('Role'),
143         queryset=DeviceRole.objects.filter(
144             vm_role=True
145         ),
146         required=False,
147         query_params={
148             "vm_role": "True"
149         }
150     )
151     tenant = DynamicModelChoiceField(
152         label=_('Tenant'),
153         queryset=Tenant.objects.all(),
154         required=False
155     )
156     platform = DynamicModelChoiceField(
157         label=_('Platform'),
158         queryset=Platform.objects.all(),
159         required=False
160     )
161     vcpus = forms.IntegerField(
162         required=False,
163         label=_('vCPUs')
164     )
165     memory = forms.IntegerField(
166         required=False,
167         label=_('Memory (MB)')
168     )
169     disk = forms.IntegerField(
170         required=False,
171         label=_('Disk (GB)')
172     )
173     description = forms.CharField(
174         label=_('Description'),
175         max_length=200,
176         required=False
177     )
178     config_template = DynamicModelChoiceField(
179         queryset=ConfigTemplate.objects.all(),
180         required=False
181     )
182     comments = CommentField()
183 
184     model = VirtualMachine
185     fieldsets = (
186         (None, ('site', 'cluster', 'device', 'status', 'role', 'tenant', 'platform', 'description')),
187         (_('Resources'), ('vcpus', 'memory', 'disk')),
188         ('Configuration', ('config_template',)),
189     )
190     nullable_fields = (
191         'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'description', 'comments',
192     )
193 
194 
195 class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm):
196     virtual_machine = forms.ModelChoiceField(
197         label=_('Virtual machine'),
198         queryset=VirtualMachine.objects.all(),
199         required=False,
200         disabled=True,
201         widget=forms.HiddenInput()
202     )
203     parent = DynamicModelChoiceField(
204         label=_('Parent'),
205         queryset=VMInterface.objects.all(),
206         required=False
207     )
208     bridge = DynamicModelChoiceField(
209         label=_('Bridge'),
210         queryset=VMInterface.objects.all(),
211         required=False
212     )
213     enabled = forms.NullBooleanField(
214         label=_('Enabled'),
215         required=False,
216         widget=BulkEditNullBooleanSelect()
217     )
218     mtu = forms.IntegerField(
219         required=False,
220         min_value=INTERFACE_MTU_MIN,
221         max_value=INTERFACE_MTU_MAX,
222         label=_('MTU')
223     )
224     description = forms.CharField(
225         label=_('Description'),
226         max_length=100,
227         required=False
228     )
229     mode = forms.ChoiceField(
230         label=_('Mode'),
231         choices=add_blank_choice(InterfaceModeChoices),
232         required=False
233     )
234     vlan_group = DynamicModelChoiceField(
235         queryset=VLANGroup.objects.all(),
236         required=False,
237         label=_('VLAN group')
238     )
239     untagged_vlan = DynamicModelChoiceField(
240         queryset=VLAN.objects.all(),
241         required=False,
242         query_params={
243             'group_id': '$vlan_group',
244         },
245         label=_('Untagged VLAN')
246     )
247     tagged_vlans = DynamicModelMultipleChoiceField(
248         queryset=VLAN.objects.all(),
249         required=False,
250         query_params={
251             'group_id': '$vlan_group',
252         },
253         label=_('Tagged VLANs')
254     )
255     vrf = DynamicModelChoiceField(
256         queryset=VRF.objects.all(),
257         required=False,
258         label=_('VRF')
259     )
260 
261     model = VMInterface
262     fieldsets = (
263         (None, ('mtu', 'enabled', 'vrf', 'description')),
264         (_('Related Interfaces'), ('parent', 'bridge')),
265         (_('802.1Q Switching'), ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
266     )
267     nullable_fields = (
268         'parent', 'bridge', 'mtu', 'vrf', 'description',
269     )
270 
271     def __init__(self, *args, **kwargs):
272         super().__init__(*args, **kwargs)
273         if 'virtual_machine' in self.initial:
274             vm_id = self.initial.get('virtual_machine')
275 
276             # Restrict parent/bridge interface assignment by VM
277             self.fields['parent'].widget.add_query_param('virtual_machine_id', vm_id)
278             self.fields['bridge'].widget.add_query_param('virtual_machine_id', vm_id)
279 
280             # Limit VLAN choices by virtual machine
281             self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id)
282             self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id)
283 
284         else:
285             # See 5643
286             if 'pk' in self.initial:
287                 site = None
288                 interfaces = VMInterface.objects.filter(
289                     pk__in=self.initial['pk']
290                 ).prefetch_related(
291                     'virtual_machine__site'
292                 )
293 
294                 # Check interface sites.  First interface should set site, further interfaces will either continue the
295                 # loop or reset back to no site and break the loop.
296                 for interface in interfaces:
297                     if site is None:
298                         site = interface.virtual_machine.cluster.site
299                     elif interface.virtual_machine.cluster.site is not site:
300                         site = None
301                         break
302 
303                 if site is not None:
304                     self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)
305                     self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)
306 
307             self.fields['parent'].choices = ()
308             self.fields['parent'].widget.attrs['disabled'] = True
309             self.fields['bridge'].choices = ()
310             self.fields['bridge'].widget.attrs['disabled'] = True
311 
312 
313 class VMInterfaceBulkRenameForm(BulkRenameForm):
314     pk = forms.ModelMultipleChoiceField(
315         queryset=VMInterface.objects.all(),
316         widget=forms.MultipleHiddenInput()
317     )
318
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/netbox/virtualization/forms/bulk_edit.py b/netbox/virtualization/forms/bulk_edit.py
--- a/netbox/virtualization/forms/bulk_edit.py
+++ b/netbox/virtualization/forms/bulk_edit.py
@@ -294,9 +294,10 @@
                 # Check interface sites.  First interface should set site, further interfaces will either continue the
                 # loop or reset back to no site and break the loop.
                 for interface in interfaces:
+                    vm_site = interface.virtual_machine.site or interface.virtual_machine.cluster.site
                     if site is None:
-                        site = interface.virtual_machine.cluster.site
-                    elif interface.virtual_machine.cluster.site is not site:
+                        site = vm_site
+                    elif vm_site is not site:
                         site = None
                         break

{"golden_diff": "diff --git a/netbox/virtualization/forms/bulk_edit.py b/netbox/virtualization/forms/bulk_edit.py\n--- a/netbox/virtualization/forms/bulk_edit.py\n+++ b/netbox/virtualization/forms/bulk_edit.py\n@@ -294,9 +294,10 @@\n                 # Check interface sites.  First interface should set site, further interfaces will either continue the\n                 # loop or reset back to no site and break the loop.\n                 for interface in interfaces:\n+                    vm_site = interface.virtual_machine.site or interface.virtual_machine.cluster.site\n                     if site is None:\n-                        site = interface.virtual_machine.cluster.site\n-                    elif interface.virtual_machine.cluster.site is not site:\n+                        site = vm_site\n+                    elif vm_site is not site:\n                         site = None\n                         break\n", "issue": "Bulk edit of Interfaces of VMs without cluster fails\n### NetBox version\n\nv3.6.5\n\n### Python version\n\n3.8\n\n### Steps to Reproduce\n\n1. Create VM and assign only a Site, not a Cluster\r\n2. Create Interface for VM\r\n3. Mark Interface and click on \"Edit Selected\"\n\n### Expected Behavior\n\nEdit form for selected VM Interface(s) appear\n\n### Observed Behavior\n\nException Window with the following Content:\r\n```\r\n<class 'AttributeError'>\r\n\r\n'NoneType' object has no attribute 'site'\r\n\r\nPython version: 3.8.10\r\nNetBox version: 3.6.5\r\nPlugins: \r\n  netbox_demo: 0.3.1\r\n```\r\n\r\nWhen generating the bulk edit form, the site is extracted from the cluster of the virtual machine, which fails if no cluster is assigned to the VM:\r\n```\r\n  File \"/opt/netbox/netbox/virtualization/forms/bulk_edit.py\", line 272, in __init__\r\n     site = interface.virtual_machine.cluster.site\r\nAttributeError: 'NoneType' object has no attribute 'site'\r\n```\n", "before_files": [{"content": "from django import forms\nfrom django.utils.translation import gettext_lazy as _\n\nfrom dcim.choices import InterfaceModeChoices\nfrom dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN\nfrom dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup\nfrom extras.models import ConfigTemplate\nfrom ipam.models import VLAN, VLANGroup, VRF\nfrom netbox.forms import NetBoxModelBulkEditForm\nfrom tenancy.models import Tenant\nfrom utilities.forms import BulkRenameForm, add_blank_choice\nfrom utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField\nfrom utilities.forms.widgets import BulkEditNullBooleanSelect\nfrom virtualization.choices import *\nfrom virtualization.models import *\n\n__all__ = (\n    'ClusterBulkEditForm',\n    'ClusterGroupBulkEditForm',\n    'ClusterTypeBulkEditForm',\n    'VirtualMachineBulkEditForm',\n    'VMInterfaceBulkEditForm',\n    'VMInterfaceBulkRenameForm',\n)\n\n\nclass ClusterTypeBulkEditForm(NetBoxModelBulkEditForm):\n    description = forms.CharField(\n        label=_('Description'),\n        max_length=200,\n        required=False\n    )\n\n    model = ClusterType\n    fieldsets = (\n        (None, ('description',)),\n    )\n    nullable_fields = ('description',)\n\n\nclass ClusterGroupBulkEditForm(NetBoxModelBulkEditForm):\n    description = forms.CharField(\n        label=_('Description'),\n        max_length=200,\n        required=False\n    )\n\n    model = ClusterGroup\n    fieldsets = (\n        (None, ('description',)),\n    )\n    nullable_fields = ('description',)\n\n\nclass ClusterBulkEditForm(NetBoxModelBulkEditForm):\n    type = DynamicModelChoiceField(\n        label=_('Type'),\n        queryset=ClusterType.objects.all(),\n        required=False\n    )\n    group = DynamicModelChoiceField(\n        label=_('Group'),\n        queryset=ClusterGroup.objects.all(),\n        required=False\n    )\n    status = forms.ChoiceField(\n        label=_('Status'),\n        choices=add_blank_choice(ClusterStatusChoices),\n        required=False,\n        initial=''\n    )\n    tenant = DynamicModelChoiceField(\n        label=_('Tenant'),\n        queryset=Tenant.objects.all(),\n        required=False\n    )\n    region = DynamicModelChoiceField(\n        label=_('Region'),\n        queryset=Region.objects.all(),\n        required=False,\n    )\n    site_group = DynamicModelChoiceField(\n        label=_('Site group'),\n        queryset=SiteGroup.objects.all(),\n        required=False,\n    )\n    site = DynamicModelChoiceField(\n        label=_('Site'),\n        queryset=Site.objects.all(),\n        required=False,\n        query_params={\n            'region_id': '$region',\n            'group_id': '$site_group',\n        }\n    )\n    description = forms.CharField(\n        label=_('Site'),\n        max_length=200,\n        required=False\n    )\n    comments = CommentField()\n\n    model = Cluster\n    fieldsets = (\n        (None, ('type', 'group', 'status', 'tenant', 'description')),\n        (_('Site'), ('region', 'site_group', 'site')),\n    )\n    nullable_fields = (\n        'group', 'site', 'tenant', 'description', 'comments',\n    )\n\n\nclass VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):\n    status = forms.ChoiceField(\n        label=_('Status'),\n        choices=add_blank_choice(VirtualMachineStatusChoices),\n        required=False,\n        initial='',\n    )\n    site = DynamicModelChoiceField(\n        label=_('Site'),\n        queryset=Site.objects.all(),\n        required=False\n    )\n    cluster = DynamicModelChoiceField(\n        label=_('Cluster'),\n        queryset=Cluster.objects.all(),\n        required=False,\n        query_params={\n            'site_id': '$site'\n        }\n    )\n    device = DynamicModelChoiceField(\n        label=_('Device'),\n        queryset=Device.objects.all(),\n        required=False,\n        query_params={\n            'cluster_id': '$cluster'\n        }\n    )\n    role = DynamicModelChoiceField(\n        label=_('Role'),\n        queryset=DeviceRole.objects.filter(\n            vm_role=True\n        ),\n        required=False,\n        query_params={\n            \"vm_role\": \"True\"\n        }\n    )\n    tenant = DynamicModelChoiceField(\n        label=_('Tenant'),\n        queryset=Tenant.objects.all(),\n        required=False\n    )\n    platform = DynamicModelChoiceField(\n        label=_('Platform'),\n        queryset=Platform.objects.all(),\n        required=False\n    )\n    vcpus = forms.IntegerField(\n        required=False,\n        label=_('vCPUs')\n    )\n    memory = forms.IntegerField(\n        required=False,\n        label=_('Memory (MB)')\n    )\n    disk = forms.IntegerField(\n        required=False,\n        label=_('Disk (GB)')\n    )\n    description = forms.CharField(\n        label=_('Description'),\n        max_length=200,\n        required=False\n    )\n    config_template = DynamicModelChoiceField(\n        queryset=ConfigTemplate.objects.all(),\n        required=False\n    )\n    comments = CommentField()\n\n    model = VirtualMachine\n    fieldsets = (\n        (None, ('site', 'cluster', 'device', 'status', 'role', 'tenant', 'platform', 'description')),\n        (_('Resources'), ('vcpus', 'memory', 'disk')),\n        ('Configuration', ('config_template',)),\n    )\n    nullable_fields = (\n        'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'description', 'comments',\n    )\n\n\nclass VMInterfaceBulkEditForm(NetBoxModelBulkEditForm):\n    virtual_machine = forms.ModelChoiceField(\n        label=_('Virtual machine'),\n        queryset=VirtualMachine.objects.all(),\n        required=False,\n        disabled=True,\n        widget=forms.HiddenInput()\n    )\n    parent = DynamicModelChoiceField(\n        label=_('Parent'),\n        queryset=VMInterface.objects.all(),\n        required=False\n    )\n    bridge = DynamicModelChoiceField(\n        label=_('Bridge'),\n        queryset=VMInterface.objects.all(),\n        required=False\n    )\n    enabled = forms.NullBooleanField(\n        label=_('Enabled'),\n        required=False,\n        widget=BulkEditNullBooleanSelect()\n    )\n    mtu = forms.IntegerField(\n        required=False,\n        min_value=INTERFACE_MTU_MIN,\n        max_value=INTERFACE_MTU_MAX,\n        label=_('MTU')\n    )\n    description = forms.CharField(\n        label=_('Description'),\n        max_length=100,\n        required=False\n    )\n    mode = forms.ChoiceField(\n        label=_('Mode'),\n        choices=add_blank_choice(InterfaceModeChoices),\n        required=False\n    )\n    vlan_group = DynamicModelChoiceField(\n        queryset=VLANGroup.objects.all(),\n        required=False,\n        label=_('VLAN group')\n    )\n    untagged_vlan = DynamicModelChoiceField(\n        queryset=VLAN.objects.all(),\n        required=False,\n        query_params={\n            'group_id': '$vlan_group',\n        },\n        label=_('Untagged VLAN')\n    )\n    tagged_vlans = DynamicModelMultipleChoiceField(\n        queryset=VLAN.objects.all(),\n        required=False,\n        query_params={\n            'group_id': '$vlan_group',\n        },\n        label=_('Tagged VLANs')\n    )\n    vrf = DynamicModelChoiceField(\n        queryset=VRF.objects.all(),\n        required=False,\n        label=_('VRF')\n    )\n\n    model = VMInterface\n    fieldsets = (\n        (None, ('mtu', 'enabled', 'vrf', 'description')),\n        (_('Related Interfaces'), ('parent', 'bridge')),\n        (_('802.1Q Switching'), ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),\n    )\n    nullable_fields = (\n        'parent', 'bridge', 'mtu', 'vrf', 'description',\n    )\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        if 'virtual_machine' in self.initial:\n            vm_id = self.initial.get('virtual_machine')\n\n            # Restrict parent/bridge interface assignment by VM\n            self.fields['parent'].widget.add_query_param('virtual_machine_id', vm_id)\n            self.fields['bridge'].widget.add_query_param('virtual_machine_id', vm_id)\n\n            # Limit VLAN choices by virtual machine\n            self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id)\n            self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id)\n\n        else:\n            # See 5643\n            if 'pk' in self.initial:\n                site = None\n                interfaces = VMInterface.objects.filter(\n                    pk__in=self.initial['pk']\n                ).prefetch_related(\n                    'virtual_machine__site'\n                )\n\n                # Check interface sites.  First interface should set site, further interfaces will either continue the\n                # loop or reset back to no site and break the loop.\n                for interface in interfaces:\n                    if site is None:\n                        site = interface.virtual_machine.cluster.site\n                    elif interface.virtual_machine.cluster.site is not site:\n                        site = None\n                        break\n\n                if site is not None:\n                    self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)\n                    self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)\n\n            self.fields['parent'].choices = ()\n            self.fields['parent'].widget.attrs['disabled'] = True\n            self.fields['bridge'].choices = ()\n            self.fields['bridge'].widget.attrs['disabled'] = True\n\n\nclass VMInterfaceBulkRenameForm(BulkRenameForm):\n    pk = forms.ModelMultipleChoiceField(\n        queryset=VMInterface.objects.all(),\n        widget=forms.MultipleHiddenInput()\n    )\n", "path": "netbox/virtualization/forms/bulk_edit.py"}], "after_files": [{"content": "from django import forms\nfrom django.utils.translation import gettext_lazy as _\n\nfrom dcim.choices import InterfaceModeChoices\nfrom dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN\nfrom dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup\nfrom extras.models import ConfigTemplate\nfrom ipam.models import VLAN, VLANGroup, VRF\nfrom netbox.forms import NetBoxModelBulkEditForm\nfrom tenancy.models import Tenant\nfrom utilities.forms import BulkRenameForm, add_blank_choice\nfrom utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField\nfrom utilities.forms.widgets import BulkEditNullBooleanSelect\nfrom virtualization.choices import *\nfrom virtualization.models import *\n\n__all__ = (\n    'ClusterBulkEditForm',\n    'ClusterGroupBulkEditForm',\n    'ClusterTypeBulkEditForm',\n    'VirtualMachineBulkEditForm',\n    'VMInterfaceBulkEditForm',\n    'VMInterfaceBulkRenameForm',\n)\n\n\nclass ClusterTypeBulkEditForm(NetBoxModelBulkEditForm):\n    description = forms.CharField(\n        label=_('Description'),\n        max_length=200,\n        required=False\n    )\n\n    model = ClusterType\n    fieldsets = (\n        (None, ('description',)),\n    )\n    nullable_fields = ('description',)\n\n\nclass ClusterGroupBulkEditForm(NetBoxModelBulkEditForm):\n    description = forms.CharField(\n        label=_('Description'),\n        max_length=200,\n        required=False\n    )\n\n    model = ClusterGroup\n    fieldsets = (\n        (None, ('description',)),\n    )\n    nullable_fields = ('description',)\n\n\nclass ClusterBulkEditForm(NetBoxModelBulkEditForm):\n    type = DynamicModelChoiceField(\n        label=_('Type'),\n        queryset=ClusterType.objects.all(),\n        required=False\n    )\n    group = DynamicModelChoiceField(\n        label=_('Group'),\n        queryset=ClusterGroup.objects.all(),\n        required=False\n    )\n    status = forms.ChoiceField(\n        label=_('Status'),\n        choices=add_blank_choice(ClusterStatusChoices),\n        required=False,\n        initial=''\n    )\n    tenant = DynamicModelChoiceField(\n        label=_('Tenant'),\n        queryset=Tenant.objects.all(),\n        required=False\n    )\n    region = DynamicModelChoiceField(\n        label=_('Region'),\n        queryset=Region.objects.all(),\n        required=False,\n    )\n    site_group = DynamicModelChoiceField(\n        label=_('Site group'),\n        queryset=SiteGroup.objects.all(),\n        required=False,\n    )\n    site = DynamicModelChoiceField(\n        label=_('Site'),\n        queryset=Site.objects.all(),\n        required=False,\n        query_params={\n            'region_id': '$region',\n            'group_id': '$site_group',\n        }\n    )\n    description = forms.CharField(\n        label=_('Site'),\n        max_length=200,\n        required=False\n    )\n    comments = CommentField()\n\n    model = Cluster\n    fieldsets = (\n        (None, ('type', 'group', 'status', 'tenant', 'description')),\n        (_('Site'), ('region', 'site_group', 'site')),\n    )\n    nullable_fields = (\n        'group', 'site', 'tenant', 'description', 'comments',\n    )\n\n\nclass VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):\n    status = forms.ChoiceField(\n        label=_('Status'),\n        choices=add_blank_choice(VirtualMachineStatusChoices),\n        required=False,\n        initial='',\n    )\n    site = DynamicModelChoiceField(\n        label=_('Site'),\n        queryset=Site.objects.all(),\n        required=False\n    )\n    cluster = DynamicModelChoiceField(\n        label=_('Cluster'),\n        queryset=Cluster.objects.all(),\n        required=False,\n        query_params={\n            'site_id': '$site'\n        }\n    )\n    device = DynamicModelChoiceField(\n        label=_('Device'),\n        queryset=Device.objects.all(),\n        required=False,\n        query_params={\n            'cluster_id': '$cluster'\n        }\n    )\n    role = DynamicModelChoiceField(\n        label=_('Role'),\n        queryset=DeviceRole.objects.filter(\n            vm_role=True\n        ),\n        required=False,\n        query_params={\n            \"vm_role\": \"True\"\n        }\n    )\n    tenant = DynamicModelChoiceField(\n        label=_('Tenant'),\n        queryset=Tenant.objects.all(),\n        required=False\n    )\n    platform = DynamicModelChoiceField(\n        label=_('Platform'),\n        queryset=Platform.objects.all(),\n        required=False\n    )\n    vcpus = forms.IntegerField(\n        required=False,\n        label=_('vCPUs')\n    )\n    memory = forms.IntegerField(\n        required=False,\n        label=_('Memory (MB)')\n    )\n    disk = forms.IntegerField(\n        required=False,\n        label=_('Disk (GB)')\n    )\n    description = forms.CharField(\n        label=_('Description'),\n        max_length=200,\n        required=False\n    )\n    config_template = DynamicModelChoiceField(\n        queryset=ConfigTemplate.objects.all(),\n        required=False\n    )\n    comments = CommentField()\n\n    model = VirtualMachine\n    fieldsets = (\n        (None, ('site', 'cluster', 'device', 'status', 'role', 'tenant', 'platform', 'description')),\n        (_('Resources'), ('vcpus', 'memory', 'disk')),\n        ('Configuration', ('config_template',)),\n    )\n    nullable_fields = (\n        'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'description', 'comments',\n    )\n\n\nclass VMInterfaceBulkEditForm(NetBoxModelBulkEditForm):\n    virtual_machine = forms.ModelChoiceField(\n        label=_('Virtual machine'),\n        queryset=VirtualMachine.objects.all(),\n        required=False,\n        disabled=True,\n        widget=forms.HiddenInput()\n    )\n    parent = DynamicModelChoiceField(\n        label=_('Parent'),\n        queryset=VMInterface.objects.all(),\n        required=False\n    )\n    bridge = DynamicModelChoiceField(\n        label=_('Bridge'),\n        queryset=VMInterface.objects.all(),\n        required=False\n    )\n    enabled = forms.NullBooleanField(\n        label=_('Enabled'),\n        required=False,\n        widget=BulkEditNullBooleanSelect()\n    )\n    mtu = forms.IntegerField(\n        required=False,\n        min_value=INTERFACE_MTU_MIN,\n        max_value=INTERFACE_MTU_MAX,\n        label=_('MTU')\n    )\n    description = forms.CharField(\n        label=_('Description'),\n        max_length=100,\n        required=False\n    )\n    mode = forms.ChoiceField(\n        label=_('Mode'),\n        choices=add_blank_choice(InterfaceModeChoices),\n        required=False\n    )\n    vlan_group = DynamicModelChoiceField(\n        queryset=VLANGroup.objects.all(),\n        required=False,\n        label=_('VLAN group')\n    )\n    untagged_vlan = DynamicModelChoiceField(\n        queryset=VLAN.objects.all(),\n        required=False,\n        query_params={\n            'group_id': '$vlan_group',\n        },\n        label=_('Untagged VLAN')\n    )\n    tagged_vlans = DynamicModelMultipleChoiceField(\n        queryset=VLAN.objects.all(),\n        required=False,\n        query_params={\n            'group_id': '$vlan_group',\n        },\n        label=_('Tagged VLANs')\n    )\n    vrf = DynamicModelChoiceField(\n        queryset=VRF.objects.all(),\n        required=False,\n        label=_('VRF')\n    )\n\n    model = VMInterface\n    fieldsets = (\n        (None, ('mtu', 'enabled', 'vrf', 'description')),\n        (_('Related Interfaces'), ('parent', 'bridge')),\n        (_('802.1Q Switching'), ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),\n    )\n    nullable_fields = (\n        'parent', 'bridge', 'mtu', 'vrf', 'description',\n    )\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        if 'virtual_machine' in self.initial:\n            vm_id = self.initial.get('virtual_machine')\n\n            # Restrict parent/bridge interface assignment by VM\n            self.fields['parent'].widget.add_query_param('virtual_machine_id', vm_id)\n            self.fields['bridge'].widget.add_query_param('virtual_machine_id', vm_id)\n\n            # Limit VLAN choices by virtual machine\n            self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id)\n            self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id)\n\n        else:\n            # See 5643\n            if 'pk' in self.initial:\n                site = None\n                interfaces = VMInterface.objects.filter(\n                    pk__in=self.initial['pk']\n                ).prefetch_related(\n                    'virtual_machine__site'\n                )\n\n                # Check interface sites.  First interface should set site, further interfaces will either continue the\n                # loop or reset back to no site and break the loop.\n                for interface in interfaces:\n                    vm_site = interface.virtual_machine.site or interface.virtual_machine.cluster.site\n                    if site is None:\n                        site = vm_site\n                    elif vm_site is not site:\n                        site = None\n                        break\n\n                if site is not None:\n                    self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)\n                    self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)\n\n            self.fields['parent'].choices = ()\n            self.fields['parent'].widget.attrs['disabled'] = True\n            self.fields['bridge'].choices = ()\n            self.fields['bridge'].widget.attrs['disabled'] = True\n\n\nclass VMInterfaceBulkRenameForm(BulkRenameForm):\n    pk = forms.ModelMultipleChoiceField(\n        queryset=VMInterface.objects.all(),\n        widget=forms.MultipleHiddenInput()\n    )\n", "path": "netbox/virtualization/forms/bulk_edit.py"}]}