Skip to content

Commit aa9ee0e

Browse files
Closes #19977: Denormalize device relationships on component models (#19984)
* Closes #19977: Denormalize site, location, and rack for device components * Set blank=True on denormalized ForeignKeys * Populate denormalized field in test data * Ignore private fields when constructing test GraphQL requests
1 parent 35b9d80 commit aa9ee0e

File tree

6 files changed

+546
-25
lines changed

6 files changed

+546
-25
lines changed

netbox/dcim/filtersets.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,34 +1515,34 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
15151515
label=_('Site group (slug)'),
15161516
)
15171517
site_id = django_filters.ModelMultipleChoiceFilter(
1518-
field_name='device__site',
1518+
field_name='_site',
15191519
queryset=Site.objects.all(),
15201520
label=_('Site (ID)'),
15211521
)
15221522
site = django_filters.ModelMultipleChoiceFilter(
1523-
field_name='device__site__slug',
1523+
field_name='_site__slug',
15241524
queryset=Site.objects.all(),
15251525
to_field_name='slug',
15261526
label=_('Site name (slug)'),
15271527
)
15281528
location_id = django_filters.ModelMultipleChoiceFilter(
1529-
field_name='device__location',
1529+
field_name='_location',
15301530
queryset=Location.objects.all(),
15311531
label=_('Location (ID)'),
15321532
)
15331533
location = django_filters.ModelMultipleChoiceFilter(
1534-
field_name='device__location__slug',
1534+
field_name='_location__slug',
15351535
queryset=Location.objects.all(),
15361536
to_field_name='slug',
15371537
label=_('Location (slug)'),
15381538
)
15391539
rack_id = django_filters.ModelMultipleChoiceFilter(
1540-
field_name='device__rack',
1540+
field_name='_rack',
15411541
queryset=Rack.objects.all(),
15421542
label=_('Rack (ID)'),
15431543
)
15441544
rack = django_filters.ModelMultipleChoiceFilter(
1545-
field_name='device__rack__name',
1545+
field_name='_rack__name',
15461546
queryset=Rack.objects.all(),
15471547
to_field_name='name',
15481548
label=_('Rack (name)'),
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
import django.db.models.deletion
2+
from django.db import migrations, models
3+
from django.db.models import OuterRef, Subquery
4+
5+
6+
def populate_denormalized_data(apps, schema_editor):
7+
Device = apps.get_model('dcim', 'Device')
8+
component_models = (
9+
apps.get_model('dcim', 'ConsolePort'),
10+
apps.get_model('dcim', 'ConsoleServerPort'),
11+
apps.get_model('dcim', 'PowerPort'),
12+
apps.get_model('dcim', 'PowerOutlet'),
13+
apps.get_model('dcim', 'Interface'),
14+
apps.get_model('dcim', 'FrontPort'),
15+
apps.get_model('dcim', 'RearPort'),
16+
apps.get_model('dcim', 'DeviceBay'),
17+
apps.get_model('dcim', 'ModuleBay'),
18+
apps.get_model('dcim', 'InventoryItem'),
19+
)
20+
21+
for model in component_models:
22+
subquery = Device.objects.filter(pk=OuterRef('device_id'))
23+
model.objects.update(
24+
_site=Subquery(subquery.values('site_id')[:1]),
25+
_location=Subquery(subquery.values('location_id')[:1]),
26+
_rack=Subquery(subquery.values('rack_id')[:1]),
27+
)
28+
29+
30+
class Migration(migrations.Migration):
31+
dependencies = [
32+
('dcim', '0208_devicerole_uniqueness'),
33+
]
34+
35+
operations = [
36+
migrations.AddField(
37+
model_name='consoleport',
38+
name='_location',
39+
field=models.ForeignKey(
40+
blank=True,
41+
null=True,
42+
on_delete=django.db.models.deletion.SET_NULL,
43+
related_name='+',
44+
to='dcim.location',
45+
),
46+
),
47+
migrations.AddField(
48+
model_name='consoleport',
49+
name='_rack',
50+
field=models.ForeignKey(
51+
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
52+
),
53+
),
54+
migrations.AddField(
55+
model_name='consoleport',
56+
name='_site',
57+
field=models.ForeignKey(
58+
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
59+
),
60+
),
61+
migrations.AddField(
62+
model_name='consoleserverport',
63+
name='_location',
64+
field=models.ForeignKey(
65+
blank=True,
66+
null=True,
67+
on_delete=django.db.models.deletion.SET_NULL,
68+
related_name='+',
69+
to='dcim.location',
70+
),
71+
),
72+
migrations.AddField(
73+
model_name='consoleserverport',
74+
name='_rack',
75+
field=models.ForeignKey(
76+
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
77+
),
78+
),
79+
migrations.AddField(
80+
model_name='consoleserverport',
81+
name='_site',
82+
field=models.ForeignKey(
83+
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
84+
),
85+
),
86+
migrations.AddField(
87+
model_name='devicebay',
88+
name='_location',
89+
field=models.ForeignKey(
90+
blank=True,
91+
null=True,
92+
on_delete=django.db.models.deletion.SET_NULL,
93+
related_name='+',
94+
to='dcim.location',
95+
),
96+
),
97+
migrations.AddField(
98+
model_name='devicebay',
99+
name='_rack',
100+
field=models.ForeignKey(
101+
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
102+
),
103+
),
104+
migrations.AddField(
105+
model_name='devicebay',
106+
name='_site',
107+
field=models.ForeignKey(
108+
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
109+
),
110+
),
111+
migrations.AddField(
112+
model_name='frontport',
113+
name='_location',
114+
field=models.ForeignKey(
115+
blank=True,
116+
null=True,
117+
on_delete=django.db.models.deletion.SET_NULL,
118+
related_name='+',
119+
to='dcim.location',
120+
),
121+
),
122+
migrations.AddField(
123+
model_name='frontport',
124+
name='_rack',
125+
field=models.ForeignKey(
126+
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
127+
),
128+
),
129+
migrations.AddField(
130+
model_name='frontport',
131+
name='_site',
132+
field=models.ForeignKey(
133+
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
134+
),
135+
),
136+
migrations.AddField(
137+
model_name='interface',
138+
name='_location',
139+
field=models.ForeignKey(
140+
blank=True,
141+
null=True,
142+
on_delete=django.db.models.deletion.SET_NULL,
143+
related_name='+',
144+
to='dcim.location',
145+
),
146+
),
147+
migrations.AddField(
148+
model_name='interface',
149+
name='_rack',
150+
field=models.ForeignKey(
151+
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
152+
),
153+
),
154+
migrations.AddField(
155+
model_name='interface',
156+
name='_site',
157+
field=models.ForeignKey(
158+
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
159+
),
160+
),
161+
migrations.AddField(
162+
model_name='inventoryitem',
163+
name='_location',
164+
field=models.ForeignKey(
165+
blank=True,
166+
null=True,
167+
on_delete=django.db.models.deletion.SET_NULL,
168+
related_name='+',
169+
to='dcim.location',
170+
),
171+
),
172+
migrations.AddField(
173+
model_name='inventoryitem',
174+
name='_rack',
175+
field=models.ForeignKey(
176+
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
177+
),
178+
),
179+
migrations.AddField(
180+
model_name='inventoryitem',
181+
name='_site',
182+
field=models.ForeignKey(
183+
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
184+
),
185+
),
186+
migrations.AddField(
187+
model_name='modulebay',
188+
name='_location',
189+
field=models.ForeignKey(
190+
blank=True,
191+
null=True,
192+
on_delete=django.db.models.deletion.SET_NULL,
193+
related_name='+',
194+
to='dcim.location',
195+
),
196+
),
197+
migrations.AddField(
198+
model_name='modulebay',
199+
name='_rack',
200+
field=models.ForeignKey(
201+
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
202+
),
203+
),
204+
migrations.AddField(
205+
model_name='modulebay',
206+
name='_site',
207+
field=models.ForeignKey(
208+
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
209+
),
210+
),
211+
migrations.AddField(
212+
model_name='poweroutlet',
213+
name='_location',
214+
field=models.ForeignKey(
215+
blank=True,
216+
null=True,
217+
on_delete=django.db.models.deletion.SET_NULL,
218+
related_name='+',
219+
to='dcim.location',
220+
),
221+
),
222+
migrations.AddField(
223+
model_name='poweroutlet',
224+
name='_rack',
225+
field=models.ForeignKey(
226+
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
227+
),
228+
),
229+
migrations.AddField(
230+
model_name='poweroutlet',
231+
name='_site',
232+
field=models.ForeignKey(
233+
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
234+
),
235+
),
236+
migrations.AddField(
237+
model_name='powerport',
238+
name='_location',
239+
field=models.ForeignKey(
240+
blank=True,
241+
null=True,
242+
on_delete=django.db.models.deletion.SET_NULL,
243+
related_name='+',
244+
to='dcim.location',
245+
),
246+
),
247+
migrations.AddField(
248+
model_name='powerport',
249+
name='_rack',
250+
field=models.ForeignKey(
251+
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
252+
),
253+
),
254+
migrations.AddField(
255+
model_name='powerport',
256+
name='_site',
257+
field=models.ForeignKey(
258+
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
259+
),
260+
),
261+
migrations.AddField(
262+
model_name='rearport',
263+
name='_location',
264+
field=models.ForeignKey(
265+
blank=True,
266+
null=True,
267+
on_delete=django.db.models.deletion.SET_NULL,
268+
related_name='+',
269+
to='dcim.location',
270+
),
271+
),
272+
migrations.AddField(
273+
model_name='rearport',
274+
name='_rack',
275+
field=models.ForeignKey(
276+
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
277+
),
278+
),
279+
migrations.AddField(
280+
model_name='rearport',
281+
name='_site',
282+
field=models.ForeignKey(
283+
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
284+
),
285+
),
286+
migrations.RunPython(populate_denormalized_data),
287+
]

netbox/dcim/models/device_components.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,29 @@ class ComponentModel(NetBoxModel):
6565
blank=True
6666
)
6767

68+
# Denormalized references replicated from the parent Device
69+
_site = models.ForeignKey(
70+
to='dcim.Site',
71+
on_delete=models.SET_NULL,
72+
related_name='+',
73+
blank=True,
74+
null=True,
75+
)
76+
_location = models.ForeignKey(
77+
to='dcim.Location',
78+
on_delete=models.SET_NULL,
79+
related_name='+',
80+
blank=True,
81+
null=True,
82+
)
83+
_rack = models.ForeignKey(
84+
to='dcim.Rack',
85+
on_delete=models.SET_NULL,
86+
related_name='+',
87+
blank=True,
88+
null=True,
89+
)
90+
6891
class Meta:
6992
abstract = True
7093
ordering = ('device', 'name')
@@ -100,6 +123,14 @@ def clean(self):
100123
"device": _("Components cannot be moved to a different device.")
101124
})
102125

126+
def save(self, *args, **kwargs):
127+
# Save denormalized references
128+
self._site = self.device.site
129+
self._location = self.device.location
130+
self._rack = self.device.rack
131+
132+
super().save(*args, **kwargs)
133+
103134
@property
104135
def parent_object(self):
105136
return self.device

0 commit comments

Comments
 (0)