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,427 @@
1
+ from django.test import TestCase
2
+ from ipam.models import VLAN, IPAddress
3
+
4
+ from dcim.choices import (
5
+ DeviceAirflowChoices,
6
+ InterfaceModeChoices,
7
+ InterfaceTypeChoices,
8
+ PortTypeChoices,
9
+ )
10
+ from dcim.models import (
11
+ Cable,
12
+ FrontPortTemplate,
13
+ Interface,
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
+ )
27
+ from utilities.testing import TestCase as NetBoxTestCase
28
+
29
+ from netbox_device_type_sync.apply import SyncError, apply_plan
30
+ from netbox_device_type_sync.diff import build_plan
31
+ from netbox_device_type_sync.tests.utils import SyncTestMixin
32
+
33
+
34
+ def sync(device, selected=(), user=None):
35
+ return apply_plan(build_plan(device), selected, user=user)
36
+
37
+
38
+ class ApplyTestCase(SyncTestMixin, TestCase):
39
+ def setUp(self):
40
+ self.type_1 = self.create_device_type('Type 1')
41
+ InterfaceTemplate.objects.create(
42
+ device_type=self.type_1, name='eth0', type='1000base-t'
43
+ )
44
+ InterfaceTemplate.objects.create(
45
+ device_type=self.type_1, name='eth1', type='1000base-t'
46
+ )
47
+ self.type_2 = self.create_device_type('Type 2')
48
+ InterfaceTemplate.objects.create(
49
+ device_type=self.type_2,
50
+ name='eth0',
51
+ type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
52
+ description='Uplink',
53
+ )
54
+ InterfaceTemplate.objects.create(
55
+ device_type=self.type_2, name='eth2', type='1000base-t'
56
+ )
57
+
58
+ def test_replacement_keeps_cables_ip_addresses_and_vlans(self):
59
+ device = self.create_device(self.type_1)
60
+ peer = self.create_device(self.type_1, name='Peer')
61
+ eth0 = device.interfaces.get(name='eth0')
62
+ Cable(
63
+ a_terminations=[eth0], b_terminations=[peer.interfaces.get(name='eth0')]
64
+ ).save()
65
+ ip = IPAddress.objects.create(address='192.0.2.1/24', assigned_object=eth0)
66
+ vlan = VLAN.objects.create(vid=10, name='VLAN 10')
67
+ eth0.refresh_from_db()
68
+ eth0.mode = InterfaceModeChoices.MODE_ACCESS
69
+ eth0.untagged_vlan = vlan
70
+ eth0.save()
71
+ device = self.change_device_type(device, self.type_2)
72
+
73
+ result = sync(device, selected=['interface.type'])
74
+
75
+ eth0 = device.interfaces.get(name='eth0')
76
+ self.assertEqual(eth0.type, InterfaceTypeChoices.TYPE_10GE_SFP_PLUS)
77
+ self.assertEqual(eth0.description, '') # not selected
78
+ self.assertIsNotNone(eth0.cable)
79
+ self.assertEqual(eth0.untagged_vlan, vlan)
80
+ self.assertEqual(eth0.mode, InterfaceModeChoices.MODE_ACCESS)
81
+ ip.refresh_from_db()
82
+ self.assertEqual(ip.assigned_object, eth0)
83
+ # eth1 exists only on the device: kept as it is
84
+ self.assertEqual(device.interfaces.get(name='eth1').type, '1000base-t')
85
+ # eth2 is created without a description, like NetBox does
86
+ self.assertTrue(device.interfaces.filter(name='eth2').exists())
87
+ self.assertEqual(result.created['interface'], 1)
88
+ self.assertEqual(result.updated['interface'], 1)
89
+ self.assertEqual(build_plan(device).create_count, 0)
90
+
91
+ def test_unselected_properties_are_kept(self):
92
+ device = self.change_device_type(self.create_device(self.type_1), self.type_2)
93
+
94
+ sync(device, selected=[])
95
+
96
+ self.assertEqual(device.interfaces.get(name='eth0').type, '1000base-t')
97
+ self.assertTrue(device.interfaces.filter(name='eth2').exists())
98
+
99
+ def test_selected_description(self):
100
+ device = self.change_device_type(self.create_device(self.type_1), self.type_2)
101
+
102
+ sync(device, selected=['interface.description'])
103
+
104
+ eth0 = device.interfaces.get(name='eth0')
105
+ self.assertEqual(eth0.description, 'Uplink')
106
+ self.assertEqual(eth0.type, '1000base-t')
107
+
108
+ def test_validation_error_rolls_back_everything(self):
109
+ device = self.create_device(self.type_1)
110
+ peer = self.create_device(self.type_1, name='Peer')
111
+ Cable(
112
+ a_terminations=[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
+ device = self.change_device_type(device, self.type_2)
117
+
118
+ with self.assertRaises(SyncError) as cm:
119
+ sync(device, selected=['interface.type', 'interface.description'])
120
+
121
+ self.assertEqual(len(cm.exception.errors), 1)
122
+ self.assertIn('Interface eth0', cm.exception.errors[0])
123
+ self.assertIn('cable', cm.exception.errors[0])
124
+ # Nothing was changed: eth2 was not created, eth0 kept its values
125
+ self.assertFalse(device.interfaces.filter(name='eth2').exists())
126
+ self.assertEqual(device.interfaces.get(name='eth0').description, '')
127
+
128
+ def test_device_fields(self):
129
+ platform = Platform.objects.create(name='Platform 1', slug='platform-1')
130
+ self.type_2.default_platform = platform
131
+ self.type_2.airflow = DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR
132
+ self.type_2.save()
133
+ device = self.change_device_type(self.create_device(self.type_1), self.type_2)
134
+
135
+ result = sync(device, selected=['device.platform'])
136
+
137
+ device.refresh_from_db()
138
+ self.assertEqual(device.platform, platform)
139
+ self.assertIsNone(device.airflow) # not selected
140
+ self.assertEqual(result.device_fields, ['Platform'])
141
+
142
+ def test_unknown_selection_keys_are_ignored(self):
143
+ device = self.change_device_type(self.create_device(self.type_1), self.type_2)
144
+
145
+ sync(device, selected=['interface.name', 'device.serial', 'nothing'])
146
+
147
+ self.assertEqual(device.interfaces.get(name='eth0').type, '1000base-t')
148
+
149
+ def test_module_conflicts_are_skipped(self):
150
+ bay = ModuleBayTemplate.objects.create(device_type=self.type_1, name='Bay 1')
151
+ module_type = ModuleType.objects.create(
152
+ manufacturer=self.manufacturer, model='Module 1'
153
+ )
154
+ InterfaceTemplate.objects.create(
155
+ module_type=module_type, name='eth2', type='1000base-t'
156
+ )
157
+ device = self.create_device(self.type_1)
158
+ Module.objects.create(
159
+ device=device,
160
+ module_bay=device.modulebays.get(name=bay.name),
161
+ module_type=module_type,
162
+ )
163
+ device = self.change_device_type(device, self.type_2)
164
+
165
+ result = sync(device)
166
+
167
+ self.assertEqual(result.created_count, 0)
168
+ self.assertIsNotNone(device.interfaces.get(name='eth2').module)
169
+
170
+
171
+ class ApplyRelationsTestCase(SyncTestMixin, TestCase):
172
+ def test_new_interfaces_get_parent_and_bridge(self):
173
+ device_type = self.create_device_type('Type 1')
174
+ eth0 = InterfaceTemplate.objects.create(
175
+ device_type=device_type, name='eth0', type='1000base-t'
176
+ )
177
+ device = self.create_device(device_type)
178
+ bridge = InterfaceTemplate.objects.create(
179
+ device_type=device_type, name='br0', type='bridge'
180
+ )
181
+ InterfaceTemplate.objects.create(
182
+ device_type=device_type,
183
+ name='eth0.10',
184
+ type='virtual',
185
+ parent=eth0,
186
+ bridge=bridge,
187
+ )
188
+
189
+ sync(device)
190
+
191
+ child = device.interfaces.get(name='eth0.10')
192
+ self.assertEqual(child.parent.name, 'eth0')
193
+ self.assertEqual(child.bridge.name, 'br0')
194
+
195
+ def test_existing_interface_gets_new_parent(self):
196
+ device_type = self.create_device_type('Type 1')
197
+ sub = InterfaceTemplate.objects.create(
198
+ device_type=device_type, name='eth0.10', type='virtual'
199
+ )
200
+ device = self.create_device(device_type)
201
+ sub.parent = InterfaceTemplate.objects.create(
202
+ device_type=device_type, name='eth0', type='1000base-t'
203
+ )
204
+ sub.save()
205
+
206
+ sync(device, selected=['interface.parent'])
207
+
208
+ self.assertEqual(device.interfaces.get(name='eth0.10').parent.name, 'eth0')
209
+
210
+ def test_channelized_interface(self):
211
+ device_type = self.create_device_type('Type 1')
212
+ parent = InterfaceTemplate.objects.create(
213
+ device_type=device_type,
214
+ name='eth0',
215
+ type=InterfaceTypeChoices.TYPE_100GE_QSFP28,
216
+ )
217
+ device = self.create_device(device_type)
218
+ parent.channels = 4
219
+ parent.save()
220
+ InterfaceTemplate.objects.create(
221
+ device_type=device_type,
222
+ name='eth0:1',
223
+ type=InterfaceTypeChoices.TYPE_CHANNEL,
224
+ parent=parent,
225
+ channel_id=1,
226
+ )
227
+
228
+ sync(device, selected=['interface.channels'])
229
+
230
+ self.assertEqual(device.interfaces.get(name='eth0').channels, 4)
231
+ child = device.interfaces.get(name='eth0:1')
232
+ self.assertEqual(child.channel_id, 1)
233
+ self.assertEqual(child.parent.name, 'eth0')
234
+
235
+ def test_power_outlet_refers_to_new_power_port(self):
236
+ device_type = self.create_device_type('Type 1')
237
+ device = self.create_device(device_type)
238
+ power_port = PowerPortTemplate.objects.create(
239
+ device_type=device_type, name='PSU1'
240
+ )
241
+ PowerOutletTemplate.objects.create(
242
+ device_type=device_type, name='Out1', power_port=power_port
243
+ )
244
+
245
+ sync(device)
246
+
247
+ self.assertEqual(device.poweroutlets.get(name='Out1').power_port.name, 'PSU1')
248
+
249
+ def test_port_mappings(self):
250
+ device_type = self.create_device_type('Type 1')
251
+ rear = RearPortTemplate.objects.create(
252
+ device_type=device_type,
253
+ name='Rear',
254
+ type=PortTypeChoices.TYPE_LC,
255
+ positions=2,
256
+ )
257
+ front_1 = FrontPortTemplate.objects.create(
258
+ device_type=device_type, name='Front1', type=PortTypeChoices.TYPE_LC
259
+ )
260
+ mapping = PortTemplateMapping.objects.create(
261
+ front_port=front_1,
262
+ rear_port=rear,
263
+ front_port_position=1,
264
+ rear_port_position=1,
265
+ )
266
+ device = self.create_device(device_type)
267
+ # Re-point Front1 to position 2, and add Front2 on position 1
268
+ mapping.rear_port_position = 2
269
+ mapping.save()
270
+ front_2 = FrontPortTemplate.objects.create(
271
+ device_type=device_type, name='Front2', type=PortTypeChoices.TYPE_LC
272
+ )
273
+ PortTemplateMapping.objects.create(
274
+ front_port=front_2,
275
+ rear_port=rear,
276
+ front_port_position=1,
277
+ rear_port_position=1,
278
+ )
279
+
280
+ sync(device, selected=['frontport.mappings'])
281
+
282
+ mappings = {
283
+ (
284
+ m.front_port.name,
285
+ m.front_port_position,
286
+ m.rear_port.name,
287
+ m.rear_port_position,
288
+ )
289
+ for m in device.port_mappings.all()
290
+ }
291
+ self.assertEqual(mappings, {('Front1', 1, 'Rear', 2), ('Front2', 1, 'Rear', 1)})
292
+ self.assertTrue(build_plan(device).is_empty)
293
+
294
+ def test_rear_port_positions_reduced_with_mappings(self):
295
+ device_type = self.create_device_type('Type 1')
296
+ rear = RearPortTemplate.objects.create(
297
+ device_type=device_type,
298
+ name='Rear',
299
+ type=PortTypeChoices.TYPE_LC,
300
+ positions=2,
301
+ )
302
+ fronts = [
303
+ FrontPortTemplate.objects.create(
304
+ device_type=device_type, name=f'Front{i}', type=PortTypeChoices.TYPE_LC
305
+ )
306
+ for i in (1, 2)
307
+ ]
308
+ mappings = [
309
+ PortTemplateMapping.objects.create(
310
+ front_port=front,
311
+ rear_port=rear,
312
+ front_port_position=1,
313
+ rear_port_position=i,
314
+ )
315
+ for i, front in enumerate(fronts, start=1)
316
+ ]
317
+ device = self.create_device(device_type)
318
+ mappings[1].delete()
319
+ rear.positions = 1
320
+ rear.save()
321
+
322
+ sync(device, selected=['rearport.positions', 'frontport.mappings'])
323
+
324
+ self.assertEqual(device.rearports.get(name='Rear').positions, 1)
325
+ self.assertEqual(device.port_mappings.count(), 1)
326
+
327
+ def test_inventory_items_are_created_parents_first(self):
328
+ device_type = self.create_device_type('Type 1')
329
+ device = self.create_device(device_type)
330
+ eth0 = InterfaceTemplate.objects.create(
331
+ device_type=device_type, name='eth0', type='1000base-t'
332
+ )
333
+ chassis = InventoryItemTemplate.objects.create(
334
+ device_type=device_type, name='Chassis'
335
+ )
336
+ InventoryItemTemplate.objects.create(
337
+ device_type=device_type,
338
+ name='SFP',
339
+ parent=chassis,
340
+ component=eth0,
341
+ part_id='SFP-1',
342
+ )
343
+
344
+ sync(device)
345
+
346
+ sfp = device.inventoryitems.get(name='SFP')
347
+ self.assertEqual(sfp.parent.name, 'Chassis')
348
+ self.assertEqual(sfp.component, device.interfaces.get(name='eth0'))
349
+ self.assertEqual(sfp.part_id, 'SFP-1')
350
+
351
+ def test_inventory_item_fields_match_netbox(self):
352
+ # The plugin builds inventory items itself; compare with instantiate()
353
+ device_type = self.create_device_type('Type 1')
354
+ device = self.create_device(device_type)
355
+ template = InventoryItemTemplate.objects.create(
356
+ device_type=device_type, name='PSU', label='Power', part_id='PWR-1'
357
+ )
358
+ expected = template.instantiate(device=device)
359
+
360
+ sync(device)
361
+
362
+ item = InventoryItem.objects.get(device=device, name='PSU')
363
+ for field_name in (
364
+ 'name',
365
+ 'label',
366
+ 'parent_id',
367
+ 'component_type_id',
368
+ 'component_id',
369
+ 'role_id',
370
+ 'manufacturer_id',
371
+ 'part_id',
372
+ 'description',
373
+ ):
374
+ with self.subTest(field=field_name):
375
+ self.assertEqual(
376
+ getattr(item, field_name), getattr(expected, field_name)
377
+ )
378
+
379
+ def test_module_bay_types(self):
380
+ device_type = self.create_device_type('Type 1')
381
+ bay_1 = ModuleBayTemplate.objects.create(device_type=device_type, name='Bay 1')
382
+ device = self.create_device(device_type)
383
+ sfp = ModuleBayType.objects.create(name='SFP', slug='sfp')
384
+ bay_1.module_bay_types.add(sfp)
385
+ bay_2 = ModuleBayTemplate.objects.create(device_type=device_type, name='Bay 2')
386
+ bay_2.module_bay_types.add(sfp)
387
+
388
+ sync(device, selected=['modulebay.module_bay_types'])
389
+
390
+ for name in ('Bay 1', 'Bay 2'):
391
+ with self.subTest(bay=name):
392
+ bay = device.modulebays.get(name=name)
393
+ self.assertEqual(list(bay.module_bay_types.all()), [sfp])
394
+
395
+
396
+ class ApplyPermissionsTestCase(SyncTestMixin, NetBoxTestCase):
397
+ def test_missing_add_permission_rolls_back(self):
398
+ device_type = self.create_device_type('Type 1')
399
+ InterfaceTemplate.objects.create(
400
+ device_type=device_type, name='eth0', type='1000base-t'
401
+ )
402
+ device = self.create_device(device_type)
403
+ device_type.interfacetemplates.update(description='Uplink')
404
+ InterfaceTemplate.objects.create(
405
+ device_type=device_type, name='eth1', type='1000base-t'
406
+ )
407
+ self.add_permissions('dcim.change_interface')
408
+
409
+ with self.assertRaises(SyncError) as cm:
410
+ sync(device, selected=['interface.description'], user=self.user)
411
+
412
+ self.assertIn('Interface eth1', cm.exception.errors[0])
413
+ self.assertIn('permission', cm.exception.errors[0])
414
+ self.assertEqual(device.interfaces.get(name='eth0').description, '')
415
+
416
+ def test_with_permissions(self):
417
+ device_type = self.create_device_type('Type 1')
418
+ device = self.create_device(device_type)
419
+ InterfaceTemplate.objects.create(
420
+ device_type=device_type, name='eth0', type='1000base-t'
421
+ )
422
+ self.add_permissions('dcim.add_interface')
423
+
424
+ result = sync(device, user=self.user)
425
+
426
+ self.assertEqual(result.created['interface'], 1)
427
+ self.assertTrue(Interface.objects.filter(device=device, name='eth0').exists())
@@ -0,0 +1,174 @@
1
+ """
2
+ Tests that fail when a NetBox version adds something the plugin does not know
3
+ about: a component template model, a template field, a port mapping field or a
4
+ device type field. Update netbox_device_type_sync/components.py (and the
5
+ README) before changing these tests.
6
+ """
7
+
8
+ from django.apps import apps
9
+ from django.test import SimpleTestCase, TestCase
10
+
11
+ from dcim.choices import CoolingMethodChoices, DeviceAirflowChoices
12
+ from dcim.models import DeviceType, Platform, PortTemplateMapping
13
+ from dcim.models.device_component_templates import ComponentTemplateModel
14
+
15
+ from netbox_device_type_sync.components import (
16
+ COMPONENT_KINDS,
17
+ DEVICE_FIELDS,
18
+ FieldProperty,
19
+ RelationProperty,
20
+ )
21
+ from netbox_device_type_sync.tests.utils import SyncTestMixin
22
+
23
+ # Template fields that identify a template rather than describe the component
24
+ IGNORED_TEMPLATE_FIELDS = {
25
+ 'id',
26
+ 'created',
27
+ 'last_updated',
28
+ 'name',
29
+ '_name',
30
+ 'device_type',
31
+ 'module_type',
32
+ 'path',
33
+ '_abs_diameter',
34
+ '_abs_max_flow',
35
+ }
36
+
37
+ # Every DeviceType field, by what the plugin does with it
38
+ DEVICE_TYPE_FIELDS = {
39
+ # Copied to the device only when it is created: synced by the plugin
40
+ 'synced': {'default_platform', 'airflow', 'cooling_method'},
41
+ # Read from the device type, never stored on the device
42
+ 'live': {
43
+ 'manufacturer',
44
+ 'model',
45
+ 'slug',
46
+ 'part_number',
47
+ 'u_height',
48
+ 'exclude_from_utilization',
49
+ 'is_full_depth',
50
+ 'subdevice_role',
51
+ 'end_of_life',
52
+ 'front_image',
53
+ 'rear_image',
54
+ 'weight',
55
+ 'weight_unit',
56
+ '_abs_weight',
57
+ },
58
+ # Describe the device type itself; agreed not to sync
59
+ 'not_synced': {
60
+ 'id',
61
+ 'created',
62
+ 'last_updated',
63
+ 'custom_field_data',
64
+ 'owner',
65
+ 'description',
66
+ 'comments',
67
+ 'tags',
68
+ },
69
+ }
70
+
71
+
72
+ def model_fields(model):
73
+ return {f.name for f in model._meta.concrete_fields} | {
74
+ f.name for f in model._meta.many_to_many
75
+ }
76
+
77
+
78
+ class TemplateCoverageTestCase(SimpleTestCase):
79
+ def test_all_template_models_are_known(self):
80
+ template_models = {
81
+ model
82
+ for model in apps.get_app_config('dcim').get_models()
83
+ if issubclass(model, ComponentTemplateModel)
84
+ }
85
+ self.assertEqual(
86
+ template_models, {kind.template_model for kind in COMPONENT_KINDS}
87
+ )
88
+
89
+ def test_all_template_fields_are_known(self):
90
+ for kind in COMPONENT_KINDS:
91
+ with self.subTest(kind=kind.key):
92
+ covered = set()
93
+ for prop in kind.properties:
94
+ covered.update(prop.template_fields)
95
+ self.assertEqual(
96
+ model_fields(kind.template_model) - IGNORED_TEMPLATE_FIELDS, covered
97
+ )
98
+
99
+ def test_component_models_have_the_template_fields(self):
100
+ for kind in COMPONENT_KINDS:
101
+ component_fields = model_fields(kind.component_model)
102
+ for prop in kind.properties:
103
+ if isinstance(prop, (FieldProperty, RelationProperty)):
104
+ with self.subTest(kind=kind.key, property=prop.key):
105
+ self.assertTrue(set(prop.template_fields) <= component_fields)
106
+
107
+ def test_port_template_mapping_fields(self):
108
+ self.assertEqual(
109
+ model_fields(PortTemplateMapping),
110
+ {
111
+ 'id',
112
+ 'created',
113
+ 'last_updated',
114
+ 'device_type',
115
+ 'module_type',
116
+ 'front_port',
117
+ 'front_port_position',
118
+ 'rear_port',
119
+ 'rear_port_position',
120
+ },
121
+ )
122
+
123
+ def test_kind_order_matches_dependencies(self):
124
+ # Components that other components refer to must be processed first
125
+ keys = [kind.key for kind in COMPONENT_KINDS]
126
+ self.assertLess(keys.index('powerport'), keys.index('poweroutlet'))
127
+ self.assertLess(keys.index('coolingintake'), keys.index('coolingoutflow'))
128
+ self.assertLess(keys.index('rearport'), keys.index('frontport'))
129
+ self.assertEqual(keys[-1], 'inventoryitem')
130
+
131
+
132
+ class DeviceTypeFieldsTestCase(SimpleTestCase):
133
+ def test_all_device_type_fields_are_classified(self):
134
+ fields = {f for f in model_fields(DeviceType) if not f.endswith('_count')}
135
+ classified = set().union(*DEVICE_TYPE_FIELDS.values())
136
+ self.assertEqual(fields, classified)
137
+
138
+ def test_device_fields_match_the_synced_fields(self):
139
+ self.assertEqual(
140
+ {f.source for f in DEVICE_FIELDS}, DEVICE_TYPE_FIELDS['synced']
141
+ )
142
+
143
+
144
+ class DeviceCreationTestCase(SyncTestMixin, TestCase):
145
+ """
146
+ The plugin relies on NetBox copying these fields only when a device is
147
+ created, and never when its device type changes.
148
+ """
149
+
150
+ def test_fields_are_copied_only_on_creation(self):
151
+ platform_1 = Platform.objects.create(name='Platform 1', slug='platform-1')
152
+ platform_2 = Platform.objects.create(name='Platform 2', slug='platform-2')
153
+ type_1 = self.create_device_type(
154
+ 'Type 1',
155
+ default_platform=platform_1,
156
+ airflow=DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR,
157
+ cooling_method=CoolingMethodChoices.METHOD_AIR,
158
+ )
159
+ type_2 = self.create_device_type(
160
+ 'Type 2',
161
+ default_platform=platform_2,
162
+ airflow=DeviceAirflowChoices.AIRFLOW_REAR_TO_FRONT,
163
+ cooling_method=CoolingMethodChoices.METHOD_LIQUID,
164
+ )
165
+
166
+ device = self.create_device(type_1)
167
+ self.assertEqual(device.platform, platform_1)
168
+ self.assertEqual(device.airflow, DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR)
169
+ self.assertEqual(device.cooling_method, CoolingMethodChoices.METHOD_AIR)
170
+
171
+ device = self.change_device_type(device, type_2)
172
+ self.assertEqual(device.platform, platform_1)
173
+ self.assertEqual(device.airflow, DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR)
174
+ self.assertEqual(device.cooling_method, CoolingMethodChoices.METHOD_AIR)