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.
@@ -0,0 +1,313 @@
1
+ from django.test import TestCase
2
+
3
+ from dcim.choices import (
4
+ DeviceAirflowChoices,
5
+ InterfacePoEModeChoices,
6
+ InterfaceTypeChoices,
7
+ PortTypeChoices,
8
+ PowerPortTypeChoices,
9
+ )
10
+ from dcim.models import (
11
+ ConsolePortTemplate,
12
+ CoolingIntakeTemplate,
13
+ FrontPortTemplate,
14
+ InterfaceTemplate,
15
+ InventoryItem,
16
+ InventoryItemTemplate,
17
+ Module,
18
+ ModuleBayTemplate,
19
+ ModuleBayType,
20
+ ModuleType,
21
+ Platform,
22
+ PortTemplateMapping,
23
+ PowerOutletTemplate,
24
+ PowerPortTemplate,
25
+ RearPortTemplate,
26
+ VirtualChassis,
27
+ )
28
+ from netbox.choices import DiameterUnitChoices
29
+
30
+ from netbox_device_type_sync.diff import build_plan
31
+ from netbox_device_type_sync.tests.utils import SyncTestMixin
32
+
33
+
34
+ def kind_diff(plan, key):
35
+ return next(k for k in plan.kinds if k.kind.key == key)
36
+
37
+
38
+ class DiffTestCase(SyncTestMixin, TestCase):
39
+ def test_new_device_is_in_sync(self):
40
+ device_type = self.create_device_type('Type 1')
41
+ InterfaceTemplate.objects.create(
42
+ device_type=device_type, name='eth0', type='1000base-t'
43
+ )
44
+ ConsolePortTemplate.objects.create(
45
+ device_type=device_type, name='con0', type='rj-45'
46
+ )
47
+ device = self.create_device(device_type)
48
+
49
+ plan = build_plan(device)
50
+
51
+ self.assertTrue(plan.is_empty)
52
+ self.assertEqual(plan.selection_keys, [])
53
+
54
+ def test_replaced_device_type(self):
55
+ type_1 = self.create_device_type('Type 1')
56
+ InterfaceTemplate.objects.create(
57
+ device_type=type_1, name='eth0', type='1000base-t'
58
+ )
59
+ InterfaceTemplate.objects.create(
60
+ device_type=type_1, name='eth1', type='1000base-t'
61
+ )
62
+ type_2 = self.create_device_type('Type 2')
63
+ InterfaceTemplate.objects.create(
64
+ device_type=type_2,
65
+ name='eth0',
66
+ type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
67
+ poe_mode=InterfacePoEModeChoices.MODE_PSE,
68
+ )
69
+ InterfaceTemplate.objects.create(
70
+ device_type=type_2, name='eth2', type='1000base-t'
71
+ )
72
+ device = self.change_device_type(self.create_device(type_1), type_2)
73
+
74
+ plan = build_plan(device)
75
+ interfaces = kind_diff(plan, 'interface')
76
+
77
+ self.assertEqual([c.name for c in interfaces.create], ['eth2'])
78
+ self.assertEqual(len(interfaces.update), 1)
79
+ update = interfaces.update[0]
80
+ self.assertEqual(update.name, 'eth0')
81
+ self.assertEqual(set(update.changes), {'type', 'poe_mode'})
82
+ self.assertEqual(update.changes['type'].current, '1000BASE-T (1GE)')
83
+ self.assertEqual(update.changes['type'].desired, 'SFP+ (10GE)')
84
+ self.assertEqual(update.changes['poe_mode'].current, '—')
85
+ self.assertEqual(update.changes['poe_mode'].desired, 'PSE')
86
+ # eth1 exists only on the device: ignored
87
+ self.assertNotIn('eth1', [c.name for c in interfaces.update])
88
+ self.assertEqual(
89
+ [s.selection_key for s in interfaces.property_summaries],
90
+ ['interface.type', 'interface.poe_mode'],
91
+ )
92
+ self.assertEqual(plan.create_count, 1)
93
+
94
+ def test_description_is_compared(self):
95
+ # NetBox does not copy the description to a new device
96
+ device_type = self.create_device_type('Type 1')
97
+ InterfaceTemplate.objects.create(
98
+ device_type=device_type,
99
+ name='eth0',
100
+ type='1000base-t',
101
+ description='Uplink',
102
+ )
103
+ device = self.create_device(device_type)
104
+
105
+ interfaces = kind_diff(build_plan(device), 'interface')
106
+
107
+ self.assertEqual(interfaces.update[0].changes['description'].desired, 'Uplink')
108
+
109
+ def test_virtual_chassis_position_is_resolved(self):
110
+ device_type = self.create_device_type('Type 1')
111
+ InterfaceTemplate.objects.create(
112
+ device_type=device_type,
113
+ name='Gi{vc_position}/0/1',
114
+ label='Port {vc_position}/1',
115
+ type='1000base-t',
116
+ )
117
+ vc = VirtualChassis.objects.create(name='VC 1')
118
+ device = self.create_device(device_type, virtual_chassis=vc, vc_position=2)
119
+ self.assertTrue(device.interfaces.filter(name='Gi2/0/1').exists())
120
+ InterfaceTemplate.objects.create(
121
+ device_type=device_type, name='Gi{vc_position}/0/2', type='1000base-t'
122
+ )
123
+
124
+ plan = build_plan(device)
125
+ interfaces = kind_diff(plan, 'interface')
126
+
127
+ self.assertEqual([c.name for c in interfaces.create], ['Gi2/0/2'])
128
+ self.assertEqual(interfaces.update, [])
129
+
130
+ def test_module_components_are_ignored(self):
131
+ device_type = self.create_device_type('Type 1')
132
+ ModuleBayTemplate.objects.create(device_type=device_type, name='Bay 1')
133
+ module_type = ModuleType.objects.create(
134
+ manufacturer=self.manufacturer, model='Module 1'
135
+ )
136
+ InterfaceTemplate.objects.create(
137
+ module_type=module_type, name='mod0', type='1000base-t'
138
+ )
139
+ device = self.create_device(device_type)
140
+ Module.objects.create(
141
+ device=device,
142
+ module_bay=device.modulebays.get(name='Bay 1'),
143
+ module_type=module_type,
144
+ )
145
+ InterfaceTemplate.objects.create(
146
+ device_type=device_type,
147
+ name='mod0',
148
+ type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
149
+ )
150
+
151
+ interfaces = kind_diff(build_plan(device), 'interface')
152
+
153
+ self.assertEqual(interfaces.create, [])
154
+ self.assertEqual(interfaces.update, [])
155
+ self.assertEqual([c.name for c in interfaces.conflicts], ['mod0'])
156
+
157
+ def test_relations_are_compared_by_name(self):
158
+ device_type = self.create_device_type('Type 1')
159
+ lag = InterfaceTemplate.objects.create(
160
+ device_type=device_type, name='lag0', type='lag'
161
+ )
162
+ eth0 = InterfaceTemplate.objects.create(
163
+ device_type=device_type, name='eth0', type='virtual'
164
+ )
165
+ pp1 = PowerPortTemplate.objects.create(device_type=device_type, name='PSU1')
166
+ PowerPortTemplate.objects.create(device_type=device_type, name='PSU2')
167
+ outlet = PowerOutletTemplate.objects.create(
168
+ device_type=device_type, name='Out1', power_port=pp1
169
+ )
170
+ device = self.create_device(device_type)
171
+ eth0.parent = lag
172
+ eth0.save()
173
+ outlet.power_port = device_type.powerporttemplates.get(name='PSU2')
174
+ outlet.save()
175
+
176
+ plan = build_plan(device)
177
+ interface = kind_diff(plan, 'interface').update[0]
178
+ outlet_diff = kind_diff(plan, 'poweroutlet').update[0]
179
+
180
+ self.assertEqual(interface.changes['parent'].current, '—')
181
+ self.assertEqual(interface.changes['parent'].desired, 'lag0')
182
+ self.assertEqual(outlet_diff.changes['power_port'].current, 'PSU1')
183
+ self.assertEqual(outlet_diff.changes['power_port'].desired, 'PSU2')
184
+
185
+ def test_port_mappings(self):
186
+ device_type = self.create_device_type('Type 1')
187
+ rear = RearPortTemplate.objects.create(
188
+ device_type=device_type,
189
+ name='Rear',
190
+ type=PortTypeChoices.TYPE_LC,
191
+ positions=2,
192
+ )
193
+ front_1 = FrontPortTemplate.objects.create(
194
+ device_type=device_type, name='Front1', type=PortTypeChoices.TYPE_LC
195
+ )
196
+ mapping = PortTemplateMapping.objects.create(
197
+ front_port=front_1,
198
+ rear_port=rear,
199
+ front_port_position=1,
200
+ rear_port_position=1,
201
+ )
202
+ device = self.create_device(device_type)
203
+ self.assertTrue(build_plan(device).is_empty)
204
+ mapping.rear_port_position = 2
205
+ mapping.save()
206
+
207
+ front = kind_diff(build_plan(device), 'frontport').update[0]
208
+
209
+ self.assertEqual(front.changes['mappings'].current, '1 → Rear:1')
210
+ self.assertEqual(front.changes['mappings'].desired, '1 → Rear:2')
211
+
212
+ def test_inventory_items_are_matched_by_path(self):
213
+ # NetBox 4.7.4 cannot create a device from nested inventory item templates
214
+ # (Device._instantiate_components() instantiates all templates before it
215
+ # saves any), so the device items are created by hand.
216
+ device_type = self.create_device_type('Type 1')
217
+ device = self.create_device(device_type)
218
+ item = InventoryItem.objects.create(device=device, name='Chassis')
219
+ InventoryItem.objects.create(
220
+ device=device, name='Fan', parent=item, part_id='FAN-1'
221
+ )
222
+ InventoryItem.objects.create(device=device, name='Fan', part_id='FAN-1')
223
+ chassis = InventoryItemTemplate.objects.create(
224
+ device_type=device_type, name='Chassis'
225
+ )
226
+ fan = InventoryItemTemplate.objects.create(
227
+ device_type=device_type, name='Fan', parent=chassis, part_id='FAN-1'
228
+ )
229
+ self.assertTrue(build_plan(device).is_empty)
230
+ fan.part_id = 'FAN-2'
231
+ fan.save()
232
+ InventoryItemTemplate.objects.create(
233
+ device_type=device_type, name='PSU', parent=chassis
234
+ )
235
+
236
+ items = kind_diff(build_plan(device), 'inventoryitem')
237
+
238
+ self.assertEqual(items.update[0].name, 'Chassis / Fan')
239
+ self.assertEqual(items.update[0].changes['part_id'].desired, 'FAN-2')
240
+ self.assertEqual([c.name for c in items.create], ['Chassis / PSU'])
241
+
242
+ def test_module_bay_types(self):
243
+ device_type = self.create_device_type('Type 1')
244
+ bay = ModuleBayTemplate.objects.create(device_type=device_type, name='Bay 1')
245
+ device = self.create_device(device_type)
246
+ bay.module_bay_types.add(ModuleBayType.objects.create(name='SFP', slug='sfp'))
247
+
248
+ bays = kind_diff(build_plan(device), 'modulebay')
249
+
250
+ self.assertEqual(bays.update[0].changes['module_bay_types'].current, '—')
251
+ self.assertEqual(bays.update[0].changes['module_bay_types'].desired, 'SFP')
252
+
253
+ def test_values_with_units(self):
254
+ device_type = self.create_device_type('Type 1')
255
+ intake = CoolingIntakeTemplate.objects.create(
256
+ device_type=device_type, name='In1'
257
+ )
258
+ device = self.create_device(device_type)
259
+ intake.diameter = 25
260
+ intake.diameter_unit = DiameterUnitChoices.UNIT_MILLIMETER
261
+ intake.save()
262
+
263
+ change = (
264
+ kind_diff(build_plan(device), 'coolingintake').update[0].changes['diameter']
265
+ )
266
+
267
+ self.assertEqual(change.current, '—')
268
+ self.assertEqual(change.desired, '25.00 Millimeters')
269
+
270
+ def test_power_port_values(self):
271
+ device_type = self.create_device_type('Type 1')
272
+ PowerPortTemplate.objects.create(
273
+ device_type=device_type, name='PSU1', type=PowerPortTypeChoices.TYPE_IEC_C14
274
+ )
275
+ device = self.create_device(device_type)
276
+ device_type.powerporttemplates.update(maximum_draw=350)
277
+
278
+ changes = kind_diff(build_plan(device), 'powerport').update[0].changes
279
+
280
+ self.assertEqual(set(changes), {'maximum_draw'})
281
+ self.assertEqual(changes['maximum_draw'].desired, '350')
282
+
283
+
284
+ class DeviceFieldDiffTestCase(SyncTestMixin, TestCase):
285
+ def test_device_fields(self):
286
+ platform = Platform.objects.create(name='Platform 1', slug='platform-1')
287
+ type_1 = self.create_device_type('Type 1')
288
+ type_2 = self.create_device_type(
289
+ 'Type 2',
290
+ default_platform=platform,
291
+ airflow=DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR,
292
+ )
293
+ device = self.change_device_type(self.create_device(type_1), type_2)
294
+
295
+ plan = build_plan(device)
296
+
297
+ self.assertEqual(plan.selection_keys, ['device.platform', 'device.airflow'])
298
+ platform_diff, airflow_diff = plan.device_fields
299
+ self.assertEqual(platform_diff.current, '—')
300
+ self.assertEqual(platform_diff.desired, 'Platform 1')
301
+ self.assertEqual(airflow_diff.desired, 'Front to rear')
302
+
303
+ def test_empty_device_type_values_are_not_offered(self):
304
+ platform = Platform.objects.create(name='Platform 1', slug='platform-1')
305
+ type_1 = self.create_device_type(
306
+ 'Type 1',
307
+ default_platform=platform,
308
+ airflow=DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR,
309
+ )
310
+ type_2 = self.create_device_type('Type 2')
311
+ device = self.change_device_type(self.create_device(type_1), type_2)
312
+
313
+ self.assertEqual(build_plan(device).device_fields, [])
@@ -0,0 +1,15 @@
1
+ from django.apps import apps
2
+ from django.test import SimpleTestCase
3
+
4
+ from netbox_device_type_sync import __version__
5
+
6
+
7
+ class VersionTestCase(SimpleTestCase):
8
+ def test_version(self):
9
+ assert __version__ == '1.0.0'
10
+
11
+
12
+ class PluginLoadTestCase(SimpleTestCase):
13
+ def test_plugin_is_installed(self):
14
+ config = apps.get_app_config('netbox_device_type_sync')
15
+ self.assertEqual(config.version, __version__)
@@ -0,0 +1,111 @@
1
+ """
2
+ Tests of the Sync button on the Device page. The button relies on NetBox
3
+ internals (see CLAUDE.md); these tests must pass for every supported NetBox
4
+ version.
5
+ """
6
+
7
+ import re
8
+ from unittest import mock
9
+
10
+ from django.test import SimpleTestCase
11
+ from django.urls import reverse
12
+
13
+ from core.models import ObjectType
14
+ from dcim.models import Device
15
+ from dcim.ui.panels import DeviceDeviceTypePanel
16
+ from dcim.views import DeviceView
17
+ from netbox.ui import layout
18
+ from users.models import ObjectPermission
19
+ from utilities.testing import TestCase
20
+
21
+ from netbox_device_type_sync.tests.utils import SyncTestMixin
22
+ from netbox_device_type_sync.ui import SyncDeviceAction, add_sync_button
23
+
24
+
25
+ def device_type_panels():
26
+ return [
27
+ panel
28
+ for row in DeviceView.layout
29
+ for column in row
30
+ for panel in column
31
+ if isinstance(panel, DeviceDeviceTypePanel)
32
+ ]
33
+
34
+
35
+ class AddSyncButtonTestCase(SimpleTestCase):
36
+ def test_button_is_added_on_startup(self):
37
+ panels = device_type_panels()
38
+
39
+ self.assertEqual(len(panels), 1)
40
+ actions = [a for a in panels[0].actions if isinstance(a, SyncDeviceAction)]
41
+ self.assertEqual(len(actions), 1)
42
+
43
+ def test_adding_twice_adds_one_button(self):
44
+ self.assertTrue(add_sync_button())
45
+
46
+ actions = device_type_panels()[0].actions
47
+ self.assertEqual(
48
+ len([a for a in actions if isinstance(a, SyncDeviceAction)]), 1
49
+ )
50
+
51
+ def test_missing_panel_logs_a_warning(self):
52
+ other_layout = layout.SimpleLayout(left_panels=[], right_panels=[])
53
+ with mock.patch.object(DeviceView, 'layout', other_layout):
54
+ with self.assertLogs(
55
+ 'netbox.plugins.netbox_device_type_sync', 'WARNING'
56
+ ) as logs:
57
+ self.assertFalse(add_sync_button())
58
+
59
+ self.assertIn('Sync button was not added', logs.output[0])
60
+
61
+ def test_changed_netbox_internals_log_a_warning(self):
62
+ with mock.patch('dcim.views.DeviceView.layout', None):
63
+ with self.assertLogs('netbox.plugins.netbox_device_type_sync', 'WARNING'):
64
+ self.assertFalse(add_sync_button())
65
+
66
+
67
+ class SyncButtonViewTestCase(SyncTestMixin, TestCase):
68
+ def setUp(self):
69
+ super().setUp()
70
+ self.device = self.create_device(self.create_device_type('Type 1'))
71
+ self.sync_url = reverse(
72
+ 'plugins:netbox_device_type_sync:device_sync', kwargs={'pk': self.device.pk}
73
+ )
74
+
75
+ def device_type_card(self):
76
+ response = self.client.get(self.device.get_absolute_url())
77
+ self.assertEqual(response.status_code, 200)
78
+ match = re.search(
79
+ r'<!-- begin DeviceDeviceTypePanel -->(.*?)<!-- end DeviceDeviceTypePanel -->',
80
+ response.content.decode(),
81
+ re.DOTALL,
82
+ )
83
+ self.assertIsNotNone(
84
+ match, 'The Device Type card is missing on the Device page'
85
+ )
86
+ return match.group(1)
87
+
88
+ def test_button_in_device_type_card(self):
89
+ self.add_permissions('dcim.view_device', 'dcim.change_device')
90
+
91
+ card = self.device_type_card()
92
+
93
+ self.assertIn(f'href="{self.sync_url}"', card)
94
+ self.assertIn('mdi-sync', card)
95
+
96
+ def test_button_hidden_without_change_permission(self):
97
+ self.add_permissions('dcim.view_device')
98
+
99
+ self.assertNotIn(self.sync_url, self.device_type_card())
100
+
101
+ def test_button_hidden_by_object_constraints(self):
102
+ self.add_permissions('dcim.view_device')
103
+ permission = ObjectPermission.objects.create(
104
+ name='Change other devices',
105
+ actions=['change'],
106
+ constraints={'name': 'Other'},
107
+ )
108
+ permission.users.add(self.user)
109
+ permission.object_types.add(ObjectType.objects.get_for_model(Device))
110
+
111
+ self.assertNotIn(self.sync_url, self.device_type_card())
@@ -0,0 +1,123 @@
1
+ import re
2
+
3
+ from django.urls import reverse
4
+
5
+ from core.choices import ObjectChangeActionChoices
6
+ from core.models import ObjectChange
7
+ from dcim.choices import InterfaceTypeChoices
8
+ from dcim.models import Cable, InterfaceTemplate
9
+ from utilities.testing import TestCase
10
+
11
+ from netbox_device_type_sync.tests.utils import SyncTestMixin
12
+
13
+
14
+ class DeviceSyncViewTestCase(SyncTestMixin, TestCase):
15
+ def setUp(self):
16
+ super().setUp()
17
+ self.type_1 = self.create_device_type('Type 1')
18
+ InterfaceTemplate.objects.create(
19
+ device_type=self.type_1, name='eth0', type='1000base-t'
20
+ )
21
+ self.type_2 = self.create_device_type('Type 2')
22
+ InterfaceTemplate.objects.create(
23
+ device_type=self.type_2,
24
+ name='eth0',
25
+ type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
26
+ )
27
+ InterfaceTemplate.objects.create(
28
+ device_type=self.type_2, name='eth1', type='1000base-t'
29
+ )
30
+ self.device = self.change_device_type(
31
+ self.create_device(self.type_1), self.type_2
32
+ )
33
+ self.url = reverse(
34
+ 'plugins:netbox_device_type_sync:device_sync', kwargs={'pk': self.device.pk}
35
+ )
36
+
37
+ def test_get_without_permission(self):
38
+ self.add_permissions('dcim.view_device')
39
+
40
+ response = self.client.get(self.url)
41
+
42
+ self.assertEqual(response.status_code, 403)
43
+
44
+ def test_get(self):
45
+ self.add_permissions('dcim.change_device')
46
+
47
+ response = self.client.get(self.url)
48
+
49
+ self.assertEqual(response.status_code, 200)
50
+ content = response.content.decode()
51
+ self.assertIn('name="interface__type"', content)
52
+ # All checkboxes are unchecked
53
+ self.assertIsNone(re.search(r'<input type="checkbox"[^>]*\schecked', content))
54
+ self.assertIn('SFP+ (10GE)', content)
55
+ self.assertIn('eth1', content)
56
+ self.assertIn('1 interface will be created', content)
57
+
58
+ def test_get_in_sync(self):
59
+ self.add_permissions('dcim.change_device')
60
+ device = self.create_device(self.type_2, name='Device 2')
61
+ url = reverse(
62
+ 'plugins:netbox_device_type_sync:device_sync', kwargs={'pk': device.pk}
63
+ )
64
+
65
+ response = self.client.get(url)
66
+
67
+ self.assertEqual(response.status_code, 200)
68
+ self.assertIn(
69
+ 'The device is in sync with its device type', response.content.decode()
70
+ )
71
+
72
+ def test_post(self):
73
+ self.add_permissions(
74
+ 'dcim.change_device', 'dcim.add_interface', 'dcim.change_interface'
75
+ )
76
+
77
+ response = self.client.post(self.url, {'interface__type': 'on'})
78
+
79
+ self.assertRedirects(
80
+ response, self.device.get_absolute_url(), fetch_redirect_response=False
81
+ )
82
+ self.assertEqual(
83
+ self.device.interfaces.get(name='eth0').type,
84
+ InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
85
+ )
86
+ self.assertTrue(self.device.interfaces.filter(name='eth1').exists())
87
+ changes = ObjectChange.objects.filter(message='Synced with device type Type 2')
88
+ self.assertEqual(
89
+ sorted(changes.values_list('action', flat=True)),
90
+ [
91
+ ObjectChangeActionChoices.ACTION_CREATE,
92
+ ObjectChangeActionChoices.ACTION_UPDATE,
93
+ ],
94
+ )
95
+
96
+ def test_post_unchecked_keeps_values(self):
97
+ self.add_permissions(
98
+ 'dcim.change_device', 'dcim.add_interface', 'dcim.change_interface'
99
+ )
100
+
101
+ self.client.post(self.url, {})
102
+
103
+ self.assertEqual(self.device.interfaces.get(name='eth0').type, '1000base-t')
104
+ self.assertTrue(self.device.interfaces.filter(name='eth1').exists())
105
+
106
+ def test_post_with_errors(self):
107
+ self.add_permissions(
108
+ 'dcim.change_device', 'dcim.add_interface', 'dcim.change_interface'
109
+ )
110
+ peer = self.create_device(self.type_1, name='Peer')
111
+ Cable(
112
+ a_terminations=[self.device.interfaces.get(name='eth0')],
113
+ b_terminations=[peer.interfaces.get(name='eth0')],
114
+ ).save()
115
+ self.type_2.interfacetemplates.filter(name='eth0').update(type='virtual')
116
+
117
+ response = self.client.post(self.url, {'interface__type': 'on'})
118
+
119
+ self.assertEqual(response.status_code, 200)
120
+ content = response.content.decode()
121
+ self.assertIn('Nothing was changed', content)
122
+ self.assertIn('Interface eth0', content)
123
+ self.assertFalse(self.device.interfaces.filter(name='eth1').exists())
@@ -0,0 +1,41 @@
1
+ from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
2
+
3
+
4
+ class SyncTestMixin:
5
+ """
6
+ Objects shared by the tests: a site, a manufacturer and a device role.
7
+ """
8
+
9
+ @classmethod
10
+ def setUpTestData(cls):
11
+ cls.site = Site.objects.create(name='Site 1', slug='site-1')
12
+ cls.manufacturer = Manufacturer.objects.create(
13
+ name='Manufacturer 1', slug='manufacturer-1'
14
+ )
15
+ cls.role = DeviceRole.objects.create(name='Role 1', slug='role-1')
16
+
17
+ @classmethod
18
+ def create_device_type(cls, model, **kwargs):
19
+ return DeviceType.objects.create(
20
+ manufacturer=cls.manufacturer,
21
+ model=model,
22
+ slug=model.lower().replace(' ', '-'),
23
+ **kwargs,
24
+ )
25
+
26
+ @classmethod
27
+ def create_device(cls, device_type, name='Device 1', **kwargs):
28
+ return Device.objects.create(
29
+ device_type=device_type,
30
+ role=cls.role,
31
+ site=cls.site,
32
+ name=name,
33
+ **kwargs,
34
+ )
35
+
36
+ @staticmethod
37
+ def change_device_type(device, device_type):
38
+ device.device_type = device_type
39
+ device.full_clean()
40
+ device.save()
41
+ return Device.objects.get(pk=device.pk)