netbox-device-type-sync 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- netbox_device_type_sync/__init__.py +23 -0
- netbox_device_type_sync/apply.py +402 -0
- netbox_device_type_sync/components.py +641 -0
- netbox_device_type_sync/diff.py +248 -0
- netbox_device_type_sync/forms.py +35 -0
- netbox_device_type_sync/templates/netbox_device_type_sync/device_sync.html +239 -0
- netbox_device_type_sync/tests/__init__.py +0 -0
- netbox_device_type_sync/tests/test_apply.py +427 -0
- netbox_device_type_sync/tests/test_coverage.py +174 -0
- netbox_device_type_sync/tests/test_diff.py +313 -0
- netbox_device_type_sync/tests/test_load.py +15 -0
- netbox_device_type_sync/tests/test_ui.py +111 -0
- netbox_device_type_sync/tests/test_views.py +123 -0
- netbox_device_type_sync/tests/utils.py +41 -0
- netbox_device_type_sync/ui.py +96 -0
- netbox_device_type_sync/urls.py +7 -0
- netbox_device_type_sync/version.py +1 -0
- netbox_device_type_sync/views.py +109 -0
- netbox_device_type_sync-1.0.0.dist-info/METADATA +267 -0
- netbox_device_type_sync-1.0.0.dist-info/RECORD +23 -0
- netbox_device_type_sync-1.0.0.dist-info/WHEEL +5 -0
- netbox_device_type_sync-1.0.0.dist-info/licenses/LICENSE +21 -0
- netbox_device_type_sync-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The component types and the properties that the plugin compares.
|
|
3
|
+
|
|
4
|
+
A component template defines only some properties of a component. The plugin
|
|
5
|
+
compares exactly those properties; everything else on a device component
|
|
6
|
+
(cables, IP addresses, VLANs, MAC addresses, custom fields, tags, ...) is never
|
|
7
|
+
read or written.
|
|
8
|
+
|
|
9
|
+
Each property knows how to read its value from a template and from a device
|
|
10
|
+
component, and how to write the template value to a component. Values are
|
|
11
|
+
compared as plain Python values; every read also returns a human-readable
|
|
12
|
+
display string for the sync page.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
from django.utils.text import capfirst
|
|
18
|
+
from django.utils.translation import gettext_lazy as _
|
|
19
|
+
|
|
20
|
+
from dcim.models import (
|
|
21
|
+
ConsolePortTemplate,
|
|
22
|
+
ConsoleServerPortTemplate,
|
|
23
|
+
CoolingIntakeTemplate,
|
|
24
|
+
CoolingOutflowTemplate,
|
|
25
|
+
DeviceBayTemplate,
|
|
26
|
+
FrontPortTemplate,
|
|
27
|
+
InterfaceTemplate,
|
|
28
|
+
ModuleBayTemplate,
|
|
29
|
+
PowerOutletTemplate,
|
|
30
|
+
PowerPortTemplate,
|
|
31
|
+
RearPortTemplate,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
from dcim.models import InventoryItem, InventoryItemTemplate
|
|
36
|
+
except ImportError:
|
|
37
|
+
# Inventory items are deprecated since NetBox 4.3 and planned for removal.
|
|
38
|
+
# Without them the plugin simply does not sync inventory items.
|
|
39
|
+
InventoryItem = InventoryItemTemplate = None
|
|
40
|
+
|
|
41
|
+
__all__ = (
|
|
42
|
+
'COMPONENT_KINDS',
|
|
43
|
+
'DEVICE_FIELDS',
|
|
44
|
+
'ComponentKind',
|
|
45
|
+
'DeviceField',
|
|
46
|
+
'EMPTY',
|
|
47
|
+
'display_field',
|
|
48
|
+
'SyncContext',
|
|
49
|
+
'get_kind',
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Display string for an empty value
|
|
53
|
+
EMPTY = '—'
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _normalize(value):
|
|
57
|
+
# NetBox stores "no value" as both None and '' in optional fields
|
|
58
|
+
return None if value == '' else value
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def display_field(obj, field_name, value):
|
|
62
|
+
"""
|
|
63
|
+
Return a human-readable representation of a model field value.
|
|
64
|
+
"""
|
|
65
|
+
if value is None:
|
|
66
|
+
return EMPTY
|
|
67
|
+
field = obj._meta.get_field(field_name)
|
|
68
|
+
if field.is_relation:
|
|
69
|
+
return str(getattr(obj, field_name))
|
|
70
|
+
if field.choices:
|
|
71
|
+
return str(dict(field.flatchoices).get(value, value))
|
|
72
|
+
if isinstance(value, bool):
|
|
73
|
+
return str(_('Yes')) if value else str(_('No'))
|
|
74
|
+
return str(value)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class SyncContext:
|
|
78
|
+
"""
|
|
79
|
+
The device being synced, with lookups shared by all properties.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self, device):
|
|
83
|
+
self.device = device
|
|
84
|
+
self._inventory_paths = None
|
|
85
|
+
|
|
86
|
+
def template_name(self, template):
|
|
87
|
+
"""
|
|
88
|
+
Return the name a template gives a component on this device.
|
|
89
|
+
"""
|
|
90
|
+
if hasattr(template, 'resolve_name'):
|
|
91
|
+
return template.resolve_name(device=self.device)
|
|
92
|
+
return template.name
|
|
93
|
+
|
|
94
|
+
def find_component(self, model, name):
|
|
95
|
+
"""
|
|
96
|
+
Return the component of this device with the given model and name, or None.
|
|
97
|
+
"""
|
|
98
|
+
return model.objects.filter(device=self.device, name=name).first()
|
|
99
|
+
|
|
100
|
+
#
|
|
101
|
+
# Inventory items have no unique name on a device; they are identified by
|
|
102
|
+
# the names of their parents and their own name.
|
|
103
|
+
#
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def template_path(template):
|
|
107
|
+
path = []
|
|
108
|
+
while template is not None:
|
|
109
|
+
path.insert(0, template.name)
|
|
110
|
+
template = template.parent
|
|
111
|
+
return tuple(path)
|
|
112
|
+
|
|
113
|
+
def component_path(self, item):
|
|
114
|
+
if item is None:
|
|
115
|
+
return None
|
|
116
|
+
path = []
|
|
117
|
+
while item is not None:
|
|
118
|
+
path.insert(0, item.name)
|
|
119
|
+
item = item.parent
|
|
120
|
+
return tuple(path)
|
|
121
|
+
|
|
122
|
+
def find_inventory_item(self, path):
|
|
123
|
+
if self._inventory_paths is None:
|
|
124
|
+
self._inventory_paths = {}
|
|
125
|
+
if path not in self._inventory_paths:
|
|
126
|
+
self._inventory_paths[path] = None
|
|
127
|
+
for item in InventoryItem.objects.filter(device=self.device, name=path[-1]):
|
|
128
|
+
if self.component_path(item) == path:
|
|
129
|
+
self._inventory_paths[path] = item
|
|
130
|
+
break
|
|
131
|
+
return self._inventory_paths[path]
|
|
132
|
+
|
|
133
|
+
def forget_inventory_items(self):
|
|
134
|
+
self._inventory_paths = None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
#
|
|
138
|
+
# Properties
|
|
139
|
+
#
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class Property:
|
|
143
|
+
"""
|
|
144
|
+
A comparable property of a component.
|
|
145
|
+
|
|
146
|
+
Attributes:
|
|
147
|
+
key: The identifier of the property within its component type
|
|
148
|
+
template_fields: The template model fields covered by the property
|
|
149
|
+
deferred: True if the property is written after the component is saved
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
key = None
|
|
153
|
+
template_fields = ()
|
|
154
|
+
deferred = False
|
|
155
|
+
|
|
156
|
+
def label(self, component_model):
|
|
157
|
+
raise NotImplementedError
|
|
158
|
+
|
|
159
|
+
def read_template(self, template, ctx):
|
|
160
|
+
"""Return (value, display) of the property for a template."""
|
|
161
|
+
raise NotImplementedError
|
|
162
|
+
|
|
163
|
+
def read_component(self, component, ctx):
|
|
164
|
+
"""Return (value, display) of the property for a device component."""
|
|
165
|
+
raise NotImplementedError
|
|
166
|
+
|
|
167
|
+
def write(self, component, template, ctx):
|
|
168
|
+
"""
|
|
169
|
+
Copy the value of the template to the component. Return an error message
|
|
170
|
+
if the value cannot be written, otherwise None.
|
|
171
|
+
"""
|
|
172
|
+
raise NotImplementedError
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class FieldProperty(Property):
|
|
176
|
+
"""
|
|
177
|
+
One or more model fields that the template and the component share, for
|
|
178
|
+
example "type", or "diameter" together with its unit.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
# Template fields whose value contains {vc_position} or {module} tokens
|
|
182
|
+
RESOLVED_FIELDS = ('label', 'position')
|
|
183
|
+
|
|
184
|
+
def __init__(self, *fields):
|
|
185
|
+
self.key = fields[0]
|
|
186
|
+
self.template_fields = fields
|
|
187
|
+
|
|
188
|
+
def label(self, component_model):
|
|
189
|
+
return capfirst(component_model._meta.get_field(self.key).verbose_name)
|
|
190
|
+
|
|
191
|
+
def _template_value(self, template, field_name, ctx):
|
|
192
|
+
resolver = getattr(template, f'resolve_{field_name}', None)
|
|
193
|
+
if field_name in self.RESOLVED_FIELDS and resolver is not None:
|
|
194
|
+
return resolver(device=ctx.device)
|
|
195
|
+
return getattr(template, template._meta.get_field(field_name).attname)
|
|
196
|
+
|
|
197
|
+
def _read(self, obj, values):
|
|
198
|
+
value = tuple(_normalize(v) for v in values)
|
|
199
|
+
if value[0] is None:
|
|
200
|
+
# A unit without a value (e.g. diameter_unit without diameter) means nothing
|
|
201
|
+
return (None,) * len(value), EMPTY
|
|
202
|
+
display = ' '.join(
|
|
203
|
+
display_field(obj, name, v)
|
|
204
|
+
for name, v in zip(self.template_fields, value)
|
|
205
|
+
if v is not None
|
|
206
|
+
)
|
|
207
|
+
return value, display
|
|
208
|
+
|
|
209
|
+
def read_template(self, template, ctx):
|
|
210
|
+
return self._read(
|
|
211
|
+
template,
|
|
212
|
+
[self._template_value(template, f, ctx) for f in self.template_fields],
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def read_component(self, component, ctx):
|
|
216
|
+
return self._read(
|
|
217
|
+
component,
|
|
218
|
+
[
|
|
219
|
+
getattr(component, component._meta.get_field(f).attname)
|
|
220
|
+
for f in self.template_fields
|
|
221
|
+
],
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def write(self, component, template, ctx):
|
|
225
|
+
for field_name in self.template_fields:
|
|
226
|
+
value = self._template_value(template, field_name, ctx)
|
|
227
|
+
field = component._meta.get_field(field_name)
|
|
228
|
+
if value is None and not field.null:
|
|
229
|
+
value = ''
|
|
230
|
+
setattr(component, field.attname, value)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class RelationProperty(Property):
|
|
234
|
+
"""
|
|
235
|
+
A reference to another component of the same device (for example the
|
|
236
|
+
parent of an interface), matched by name.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
def __init__(self, field_name):
|
|
240
|
+
self.key = field_name
|
|
241
|
+
self.template_fields = (field_name,)
|
|
242
|
+
|
|
243
|
+
def label(self, component_model):
|
|
244
|
+
return capfirst(component_model._meta.get_field(self.key).verbose_name)
|
|
245
|
+
|
|
246
|
+
def read_template(self, template, ctx):
|
|
247
|
+
target = getattr(template, self.key)
|
|
248
|
+
name = ctx.template_name(target) if target is not None else None
|
|
249
|
+
return name, name or EMPTY
|
|
250
|
+
|
|
251
|
+
def read_component(self, component, ctx):
|
|
252
|
+
target = getattr(component, self.key)
|
|
253
|
+
name = target.name if target is not None else None
|
|
254
|
+
return name, name or EMPTY
|
|
255
|
+
|
|
256
|
+
def write(self, component, template, ctx):
|
|
257
|
+
name, _display = self.read_template(template, ctx)
|
|
258
|
+
if name is None:
|
|
259
|
+
setattr(component, self.key, None)
|
|
260
|
+
return None
|
|
261
|
+
model = component._meta.get_field(self.key).related_model
|
|
262
|
+
target = ctx.find_component(model, name)
|
|
263
|
+
if target is None:
|
|
264
|
+
return _('{model} {name} does not exist on the device.').format(
|
|
265
|
+
model=capfirst(model._meta.verbose_name), name=name
|
|
266
|
+
)
|
|
267
|
+
setattr(component, self.key, target)
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class InventoryParentProperty(Property):
|
|
272
|
+
"""
|
|
273
|
+
The parent of an inventory item, identified by the names of its parents.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
key = 'parent'
|
|
277
|
+
template_fields = ('parent',)
|
|
278
|
+
|
|
279
|
+
def label(self, component_model):
|
|
280
|
+
return capfirst(component_model._meta.get_field('parent').verbose_name)
|
|
281
|
+
|
|
282
|
+
@staticmethod
|
|
283
|
+
def _display(path):
|
|
284
|
+
return ' / '.join(path) if path else EMPTY
|
|
285
|
+
|
|
286
|
+
def read_template(self, template, ctx):
|
|
287
|
+
path = ctx.template_path(template.parent) if template.parent else None
|
|
288
|
+
return path, self._display(path)
|
|
289
|
+
|
|
290
|
+
def read_component(self, component, ctx):
|
|
291
|
+
path = ctx.component_path(component.parent)
|
|
292
|
+
return path, self._display(path)
|
|
293
|
+
|
|
294
|
+
def write(self, component, template, ctx):
|
|
295
|
+
path, display = self.read_template(template, ctx)
|
|
296
|
+
if path is None:
|
|
297
|
+
component.parent = None
|
|
298
|
+
return None
|
|
299
|
+
parent = ctx.find_inventory_item(path)
|
|
300
|
+
if parent is None:
|
|
301
|
+
return _('Inventory item {name} does not exist on the device.').format(
|
|
302
|
+
name=display
|
|
303
|
+
)
|
|
304
|
+
component.parent = parent
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class InventoryComponentProperty(Property):
|
|
309
|
+
"""
|
|
310
|
+
The component an inventory item is assigned to, identified by its type and
|
|
311
|
+
name.
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
key = 'component'
|
|
315
|
+
template_fields = ('component_type', 'component_id')
|
|
316
|
+
|
|
317
|
+
def label(self, component_model):
|
|
318
|
+
return _('Component')
|
|
319
|
+
|
|
320
|
+
@staticmethod
|
|
321
|
+
def _value(model, name):
|
|
322
|
+
value = (model._meta.label_lower, name)
|
|
323
|
+
display = f'{capfirst(model._meta.verbose_name)} {name}'
|
|
324
|
+
return value, display
|
|
325
|
+
|
|
326
|
+
def read_template(self, template, ctx):
|
|
327
|
+
target = template.component
|
|
328
|
+
if target is None:
|
|
329
|
+
return None, EMPTY
|
|
330
|
+
return self._value(target.component_model, ctx.template_name(target))
|
|
331
|
+
|
|
332
|
+
def read_component(self, component, ctx):
|
|
333
|
+
target = component.component
|
|
334
|
+
if target is None:
|
|
335
|
+
return None, EMPTY
|
|
336
|
+
return self._value(type(target), target.name)
|
|
337
|
+
|
|
338
|
+
def write(self, component, template, ctx):
|
|
339
|
+
target_template = template.component
|
|
340
|
+
if target_template is None:
|
|
341
|
+
component.component = None
|
|
342
|
+
return None
|
|
343
|
+
model = target_template.component_model
|
|
344
|
+
name = ctx.template_name(target_template)
|
|
345
|
+
target = ctx.find_component(model, name)
|
|
346
|
+
if target is None:
|
|
347
|
+
return _('{model} {name} does not exist on the device.').format(
|
|
348
|
+
model=capfirst(model._meta.verbose_name), name=name
|
|
349
|
+
)
|
|
350
|
+
component.component = target
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class ModuleBayTypesProperty(Property):
|
|
355
|
+
"""
|
|
356
|
+
The module bay types of a module bay (a many-to-many relation).
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
key = 'module_bay_types'
|
|
360
|
+
template_fields = ('module_bay_types',)
|
|
361
|
+
deferred = True
|
|
362
|
+
|
|
363
|
+
def label(self, component_model):
|
|
364
|
+
return capfirst(component_model._meta.get_field(self.key).verbose_name)
|
|
365
|
+
|
|
366
|
+
@staticmethod
|
|
367
|
+
def _read(manager):
|
|
368
|
+
types = sorted(manager.all(), key=lambda t: t.name)
|
|
369
|
+
value = frozenset(t.pk for t in types)
|
|
370
|
+
return value, ', '.join(t.name for t in types) or EMPTY
|
|
371
|
+
|
|
372
|
+
def read_template(self, template, ctx):
|
|
373
|
+
return self._read(template.module_bay_types)
|
|
374
|
+
|
|
375
|
+
def read_component(self, component, ctx):
|
|
376
|
+
return self._read(component.module_bay_types)
|
|
377
|
+
|
|
378
|
+
def write(self, component, template, ctx):
|
|
379
|
+
component.module_bay_types.set(template.module_bay_types.all())
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class PortMappingsProperty(Property):
|
|
384
|
+
"""
|
|
385
|
+
The mappings of a front port to rear port positions. The mappings are
|
|
386
|
+
written by the apply step, which must remove obsolete mappings before port
|
|
387
|
+
positions change and create new mappings after.
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
key = 'mappings'
|
|
391
|
+
template_fields = ()
|
|
392
|
+
deferred = True
|
|
393
|
+
|
|
394
|
+
def label(self, component_model):
|
|
395
|
+
return _('Port mappings')
|
|
396
|
+
|
|
397
|
+
@staticmethod
|
|
398
|
+
def _display(value):
|
|
399
|
+
if not value:
|
|
400
|
+
return EMPTY
|
|
401
|
+
return ', '.join(
|
|
402
|
+
f'{front_pos} → {rear}:{rear_pos}'
|
|
403
|
+
for front_pos, rear, rear_pos in sorted(value)
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
@staticmethod
|
|
407
|
+
def desired(template, ctx):
|
|
408
|
+
return frozenset(
|
|
409
|
+
(
|
|
410
|
+
m.front_port_position,
|
|
411
|
+
ctx.template_name(m.rear_port),
|
|
412
|
+
m.rear_port_position,
|
|
413
|
+
)
|
|
414
|
+
for m in template.mappings.all()
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
@staticmethod
|
|
418
|
+
def current(component):
|
|
419
|
+
return frozenset(
|
|
420
|
+
(m.front_port_position, m.rear_port.name, m.rear_port_position)
|
|
421
|
+
for m in component.mappings.all()
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
def read_template(self, template, ctx):
|
|
425
|
+
value = self.desired(template, ctx)
|
|
426
|
+
return value, self._display(value)
|
|
427
|
+
|
|
428
|
+
def read_component(self, component, ctx):
|
|
429
|
+
value = self.current(component)
|
|
430
|
+
return value, self._display(value)
|
|
431
|
+
|
|
432
|
+
def write(self, component, template, ctx):
|
|
433
|
+
# Written by apply.sync_port_mappings()
|
|
434
|
+
raise NotImplementedError
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
#
|
|
438
|
+
# Component types
|
|
439
|
+
#
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
@dataclass(frozen=True)
|
|
443
|
+
class ComponentKind:
|
|
444
|
+
"""
|
|
445
|
+
A type of device component and the properties its template defines.
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
template_model: type
|
|
449
|
+
properties: tuple
|
|
450
|
+
|
|
451
|
+
@property
|
|
452
|
+
def component_model(self):
|
|
453
|
+
return self.template_model.component_model
|
|
454
|
+
|
|
455
|
+
@property
|
|
456
|
+
def key(self):
|
|
457
|
+
return self.component_model._meta.model_name
|
|
458
|
+
|
|
459
|
+
@property
|
|
460
|
+
def template_accessor(self):
|
|
461
|
+
# related_name='%(class)ss' on DeviceType
|
|
462
|
+
return f'{self.template_model._meta.model_name}s'
|
|
463
|
+
|
|
464
|
+
@property
|
|
465
|
+
def component_accessor(self):
|
|
466
|
+
# related_name='%(class)ss' on Device
|
|
467
|
+
return f'{self.component_model._meta.model_name}s'
|
|
468
|
+
|
|
469
|
+
@property
|
|
470
|
+
def verbose_name(self):
|
|
471
|
+
return capfirst(self.component_model._meta.verbose_name)
|
|
472
|
+
|
|
473
|
+
@property
|
|
474
|
+
def verbose_name_plural(self):
|
|
475
|
+
return capfirst(self.component_model._meta.verbose_name_plural)
|
|
476
|
+
|
|
477
|
+
@property
|
|
478
|
+
def is_modular(self):
|
|
479
|
+
# Modular components may belong to an installed module
|
|
480
|
+
return any(f.name == 'module' for f in self.component_model._meta.fields)
|
|
481
|
+
|
|
482
|
+
def get_property(self, key):
|
|
483
|
+
for prop in self.properties:
|
|
484
|
+
if prop.key == key:
|
|
485
|
+
return prop
|
|
486
|
+
raise KeyError(key)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
LABEL = FieldProperty('label')
|
|
490
|
+
DESCRIPTION = FieldProperty('description')
|
|
491
|
+
TYPE = FieldProperty('type')
|
|
492
|
+
|
|
493
|
+
# In the order NetBox instantiates components for a new device
|
|
494
|
+
# (dcim.models.Device.save()); components that others refer to come first.
|
|
495
|
+
COMPONENT_KINDS = (
|
|
496
|
+
ComponentKind(ConsolePortTemplate, (TYPE, LABEL, DESCRIPTION)),
|
|
497
|
+
ComponentKind(ConsoleServerPortTemplate, (TYPE, LABEL, DESCRIPTION)),
|
|
498
|
+
ComponentKind(
|
|
499
|
+
PowerPortTemplate,
|
|
500
|
+
(
|
|
501
|
+
TYPE,
|
|
502
|
+
FieldProperty('maximum_draw'),
|
|
503
|
+
FieldProperty('allocated_draw'),
|
|
504
|
+
LABEL,
|
|
505
|
+
DESCRIPTION,
|
|
506
|
+
),
|
|
507
|
+
),
|
|
508
|
+
ComponentKind(
|
|
509
|
+
PowerOutletTemplate,
|
|
510
|
+
(
|
|
511
|
+
TYPE,
|
|
512
|
+
FieldProperty('color'),
|
|
513
|
+
RelationProperty('power_port'),
|
|
514
|
+
FieldProperty('feed_leg'),
|
|
515
|
+
LABEL,
|
|
516
|
+
DESCRIPTION,
|
|
517
|
+
),
|
|
518
|
+
),
|
|
519
|
+
ComponentKind(
|
|
520
|
+
CoolingIntakeTemplate,
|
|
521
|
+
(
|
|
522
|
+
TYPE,
|
|
523
|
+
FieldProperty('diameter', 'diameter_unit'),
|
|
524
|
+
FieldProperty('max_flow', 'max_flow_unit'),
|
|
525
|
+
LABEL,
|
|
526
|
+
DESCRIPTION,
|
|
527
|
+
),
|
|
528
|
+
),
|
|
529
|
+
ComponentKind(
|
|
530
|
+
CoolingOutflowTemplate,
|
|
531
|
+
(
|
|
532
|
+
TYPE,
|
|
533
|
+
FieldProperty('diameter', 'diameter_unit'),
|
|
534
|
+
RelationProperty('cooling_intake'),
|
|
535
|
+
LABEL,
|
|
536
|
+
DESCRIPTION,
|
|
537
|
+
),
|
|
538
|
+
),
|
|
539
|
+
ComponentKind(
|
|
540
|
+
InterfaceTemplate,
|
|
541
|
+
(
|
|
542
|
+
TYPE,
|
|
543
|
+
FieldProperty('enabled'),
|
|
544
|
+
FieldProperty('mgmt_only'),
|
|
545
|
+
FieldProperty('poe_mode'),
|
|
546
|
+
FieldProperty('poe_type'),
|
|
547
|
+
FieldProperty('rf_role'),
|
|
548
|
+
FieldProperty('channels'),
|
|
549
|
+
FieldProperty('channel_id'),
|
|
550
|
+
RelationProperty('parent'),
|
|
551
|
+
RelationProperty('bridge'),
|
|
552
|
+
LABEL,
|
|
553
|
+
DESCRIPTION,
|
|
554
|
+
),
|
|
555
|
+
),
|
|
556
|
+
ComponentKind(
|
|
557
|
+
RearPortTemplate,
|
|
558
|
+
(TYPE, FieldProperty('color'), FieldProperty('positions'), LABEL, DESCRIPTION),
|
|
559
|
+
),
|
|
560
|
+
ComponentKind(
|
|
561
|
+
FrontPortTemplate,
|
|
562
|
+
(
|
|
563
|
+
TYPE,
|
|
564
|
+
FieldProperty('color'),
|
|
565
|
+
FieldProperty('positions'),
|
|
566
|
+
PortMappingsProperty(),
|
|
567
|
+
LABEL,
|
|
568
|
+
DESCRIPTION,
|
|
569
|
+
),
|
|
570
|
+
),
|
|
571
|
+
ComponentKind(
|
|
572
|
+
ModuleBayTemplate,
|
|
573
|
+
(
|
|
574
|
+
FieldProperty('position'),
|
|
575
|
+
FieldProperty('enabled'),
|
|
576
|
+
ModuleBayTypesProperty(),
|
|
577
|
+
LABEL,
|
|
578
|
+
DESCRIPTION,
|
|
579
|
+
),
|
|
580
|
+
),
|
|
581
|
+
ComponentKind(DeviceBayTemplate, (FieldProperty('enabled'), LABEL, DESCRIPTION)),
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
if InventoryItemTemplate is not None:
|
|
585
|
+
COMPONENT_KINDS += (
|
|
586
|
+
ComponentKind(
|
|
587
|
+
InventoryItemTemplate,
|
|
588
|
+
(
|
|
589
|
+
InventoryParentProperty(),
|
|
590
|
+
InventoryComponentProperty(),
|
|
591
|
+
FieldProperty('role'),
|
|
592
|
+
FieldProperty('manufacturer'),
|
|
593
|
+
FieldProperty('part_id'),
|
|
594
|
+
LABEL,
|
|
595
|
+
DESCRIPTION,
|
|
596
|
+
),
|
|
597
|
+
),
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def get_kind(key):
|
|
602
|
+
for kind in COMPONENT_KINDS:
|
|
603
|
+
if kind.key == key:
|
|
604
|
+
return kind
|
|
605
|
+
raise KeyError(key)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
#
|
|
609
|
+
# Device fields
|
|
610
|
+
#
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
@dataclass(frozen=True)
|
|
614
|
+
class DeviceField:
|
|
615
|
+
"""
|
|
616
|
+
A device field that NetBox copies from the device type only when a device
|
|
617
|
+
is created (dcim.models.Device.save()).
|
|
618
|
+
|
|
619
|
+
Attributes:
|
|
620
|
+
name: The field of Device
|
|
621
|
+
source: The field of DeviceType that holds the value
|
|
622
|
+
"""
|
|
623
|
+
|
|
624
|
+
name: str
|
|
625
|
+
source: str
|
|
626
|
+
|
|
627
|
+
@property
|
|
628
|
+
def key(self):
|
|
629
|
+
return self.name
|
|
630
|
+
|
|
631
|
+
def label(self):
|
|
632
|
+
from dcim.models import Device
|
|
633
|
+
|
|
634
|
+
return capfirst(Device._meta.get_field(self.name).verbose_name)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
DEVICE_FIELDS = (
|
|
638
|
+
DeviceField('platform', 'default_platform'),
|
|
639
|
+
DeviceField('airflow', 'airflow'),
|
|
640
|
+
DeviceField('cooling_method', 'cooling_method'),
|
|
641
|
+
)
|