test_models.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. import tempfile
  2. from pathlib import Path
  3. from django.forms import ValidationError
  4. from django.test import tag, TestCase
  5. from core.models import DataSource, ObjectType
  6. from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup
  7. from extras.models import ConfigContext, ConfigTemplate, Tag
  8. from tenancy.models import Tenant, TenantGroup
  9. from utilities.exceptions import AbortRequest
  10. from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
  11. class TagTest(TestCase):
  12. def test_default_ordering_weight_then_name_is_set(self):
  13. Tag.objects.create(name='Tag 1', slug='tag-1', weight=3000)
  14. Tag.objects.create(name='Tag 2', slug='tag-2') # Default: 1000
  15. Tag.objects.create(name='Tag 3', slug='tag-3', weight=2000)
  16. Tag.objects.create(name='Tag 4', slug='tag-4', weight=2000)
  17. tags = Tag.objects.all()
  18. self.assertEqual(tags[0].slug, 'tag-2')
  19. self.assertEqual(tags[1].slug, 'tag-3')
  20. self.assertEqual(tags[2].slug, 'tag-4')
  21. self.assertEqual(tags[3].slug, 'tag-1')
  22. def test_tag_related_manager_ordering_weight_then_name(self):
  23. tags = [
  24. Tag.objects.create(name='Tag 1', slug='tag-1', weight=3000),
  25. Tag.objects.create(name='Tag 2', slug='tag-2'), # Default: 1000
  26. Tag.objects.create(name='Tag 3', slug='tag-3', weight=2000),
  27. Tag.objects.create(name='Tag 4', slug='tag-4', weight=2000),
  28. ]
  29. site = Site.objects.create(name='Site 1')
  30. for _tag in tags:
  31. site.tags.add(_tag)
  32. site.save()
  33. site = Site.objects.first()
  34. tags = site.tags.all()
  35. self.assertEqual(tags[0].slug, 'tag-2')
  36. self.assertEqual(tags[1].slug, 'tag-3')
  37. self.assertEqual(tags[2].slug, 'tag-4')
  38. self.assertEqual(tags[3].slug, 'tag-1')
  39. def test_create_tag_unicode(self):
  40. tag = Tag(name='Testing Unicode: 台灣')
  41. tag.save()
  42. self.assertEqual(tag.slug, 'testing-unicode-台灣')
  43. def test_object_type_validation(self):
  44. region = Region.objects.create(name='Region 1', slug='region-1')
  45. sitegroup = SiteGroup.objects.create(name='Site Group 1', slug='site-group-1')
  46. # Create a Tag that can only be applied to Regions
  47. tag = Tag.objects.create(name='Tag 1', slug='tag-1')
  48. tag.object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'region'))
  49. # Apply the Tag to a Region
  50. region.tags.add(tag)
  51. self.assertIn(tag, region.tags.all())
  52. # Apply the Tag to a SiteGroup
  53. with self.assertRaises(AbortRequest):
  54. sitegroup.tags.add(tag)
  55. class ConfigContextTest(TestCase):
  56. """
  57. These test cases deal with the weighting, ordering, and deep merge logic of config context data.
  58. It also ensures the various config context querysets are consistent.
  59. """
  60. @classmethod
  61. def setUpTestData(cls):
  62. manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
  63. devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
  64. role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
  65. region = Region.objects.create(name='Region')
  66. sitegroup = SiteGroup.objects.create(name='Site Group')
  67. site = Site.objects.create(name='Site 1', slug='site-1', region=region, group=sitegroup)
  68. location = Location.objects.create(name='Location 1', slug='location-1', site=site)
  69. Platform.objects.create(name='Platform')
  70. tenantgroup = TenantGroup.objects.create(name='Tenant Group')
  71. Tenant.objects.create(name='Tenant', group=tenantgroup)
  72. Tag.objects.create(name='Tag', slug='tag')
  73. Tag.objects.create(name='Tag2', slug='tag2')
  74. Device.objects.create(
  75. name='Device 1',
  76. device_type=devicetype,
  77. role=role,
  78. site=site,
  79. location=location
  80. )
  81. def test_higher_weight_wins(self):
  82. device = Device.objects.first()
  83. context1 = ConfigContext(
  84. name="context 1",
  85. weight=101,
  86. data={
  87. "a": 123,
  88. "b": 456,
  89. "c": 777
  90. }
  91. )
  92. context2 = ConfigContext(
  93. name="context 2",
  94. weight=100,
  95. data={
  96. "a": 123,
  97. "b": 456,
  98. "c": 789
  99. }
  100. )
  101. ConfigContext.objects.bulk_create([context1, context2])
  102. expected_data = {
  103. "a": 123,
  104. "b": 456,
  105. "c": 777
  106. }
  107. self.assertEqual(device.get_config_context(), expected_data)
  108. def test_name_ordering_after_weight(self):
  109. device = Device.objects.first()
  110. context1 = ConfigContext(
  111. name="context 1",
  112. weight=100,
  113. data={
  114. "a": 123,
  115. "b": 456,
  116. "c": 777
  117. }
  118. )
  119. context2 = ConfigContext(
  120. name="context 2",
  121. weight=100,
  122. data={
  123. "a": 123,
  124. "b": 456,
  125. "c": 789
  126. }
  127. )
  128. ConfigContext.objects.bulk_create([context1, context2])
  129. expected_data = {
  130. "a": 123,
  131. "b": 456,
  132. "c": 789
  133. }
  134. self.assertEqual(device.get_config_context(), expected_data)
  135. def test_annotation_same_as_get_for_object(self):
  136. """
  137. This test incorporates features from all of the above tests cases to ensure
  138. the annotate_config_context_data() and get_for_object() queryset methods are the same.
  139. """
  140. device = Device.objects.first()
  141. context1 = ConfigContext(
  142. name="context 1",
  143. weight=101,
  144. data={
  145. "a": 123,
  146. "b": 456,
  147. "c": 777
  148. }
  149. )
  150. context2 = ConfigContext(
  151. name="context 2",
  152. weight=100,
  153. data={
  154. "a": 123,
  155. "b": 456,
  156. "c": 789
  157. }
  158. )
  159. context3 = ConfigContext(
  160. name="context 3",
  161. weight=99,
  162. data={
  163. "d": 1
  164. }
  165. )
  166. context4 = ConfigContext(
  167. name="context 4",
  168. weight=99,
  169. data={
  170. "d": 2
  171. }
  172. )
  173. ConfigContext.objects.bulk_create([context1, context2, context3, context4])
  174. annotated_queryset = Device.objects.filter(name=device.name).annotate_config_context_data()
  175. self.assertEqual(device.get_config_context(), annotated_queryset[0].get_config_context())
  176. def test_annotation_same_as_get_for_object_device_relations(self):
  177. region = Region.objects.first()
  178. sitegroup = SiteGroup.objects.first()
  179. site = Site.objects.first()
  180. location = Location.objects.first()
  181. platform = Platform.objects.first()
  182. tenantgroup = TenantGroup.objects.first()
  183. tenant = Tenant.objects.first()
  184. tag = Tag.objects.first()
  185. region_context = ConfigContext.objects.create(
  186. name="region",
  187. weight=100,
  188. data={
  189. "region": 1
  190. }
  191. )
  192. region_context.regions.add(region)
  193. sitegroup_context = ConfigContext.objects.create(
  194. name="sitegroup",
  195. weight=100,
  196. data={
  197. "sitegroup": 1
  198. }
  199. )
  200. sitegroup_context.site_groups.add(sitegroup)
  201. site_context = ConfigContext.objects.create(
  202. name="site",
  203. weight=100,
  204. data={
  205. "site": 1
  206. }
  207. )
  208. site_context.sites.add(site)
  209. location_context = ConfigContext.objects.create(
  210. name="location",
  211. weight=100,
  212. data={
  213. "location": 1
  214. }
  215. )
  216. location_context.locations.add(location)
  217. platform_context = ConfigContext.objects.create(
  218. name="platform",
  219. weight=100,
  220. data={
  221. "platform": 1
  222. }
  223. )
  224. platform_context.platforms.add(platform)
  225. tenant_group_context = ConfigContext.objects.create(
  226. name="tenant group",
  227. weight=100,
  228. data={
  229. "tenant_group": 1
  230. }
  231. )
  232. tenant_group_context.tenant_groups.add(tenantgroup)
  233. tenant_context = ConfigContext.objects.create(
  234. name="tenant",
  235. weight=100,
  236. data={
  237. "tenant": 1
  238. }
  239. )
  240. tenant_context.tenants.add(tenant)
  241. tag_context = ConfigContext.objects.create(
  242. name="tag",
  243. weight=100,
  244. data={
  245. "tag": 1
  246. }
  247. )
  248. tag_context.tags.add(tag)
  249. device = Device.objects.create(
  250. name="Device 2",
  251. site=site,
  252. location=location,
  253. tenant=tenant,
  254. platform=platform,
  255. role=DeviceRole.objects.first(),
  256. device_type=DeviceType.objects.first()
  257. )
  258. device.tags.add(tag)
  259. annotated_queryset = Device.objects.filter(name=device.name).annotate_config_context_data()
  260. self.assertEqual(device.get_config_context(), annotated_queryset[0].get_config_context())
  261. def test_annotation_same_as_get_for_object_virtualmachine_relations(self):
  262. region = Region.objects.first()
  263. sitegroup = SiteGroup.objects.first()
  264. site = Site.objects.first()
  265. platform = Platform.objects.first()
  266. tenantgroup = TenantGroup.objects.first()
  267. tenant = Tenant.objects.first()
  268. tag = Tag.objects.first()
  269. cluster_type = ClusterType.objects.create(name="Cluster Type")
  270. cluster_group = ClusterGroup.objects.create(name="Cluster Group")
  271. cluster = Cluster.objects.create(
  272. name="Cluster",
  273. group=cluster_group,
  274. type=cluster_type,
  275. scope=site,
  276. )
  277. region_context = ConfigContext.objects.create(
  278. name="region",
  279. weight=100,
  280. data={"region": 1}
  281. )
  282. region_context.regions.add(region)
  283. sitegroup_context = ConfigContext.objects.create(
  284. name="sitegroup",
  285. weight=100,
  286. data={"sitegroup": 1}
  287. )
  288. sitegroup_context.site_groups.add(sitegroup)
  289. site_context = ConfigContext.objects.create(
  290. name="site",
  291. weight=100,
  292. data={"site": 1}
  293. )
  294. site_context.sites.add(site)
  295. platform_context = ConfigContext.objects.create(
  296. name="platform",
  297. weight=100,
  298. data={"platform": 1}
  299. )
  300. platform_context.platforms.add(platform)
  301. tenant_group_context = ConfigContext.objects.create(
  302. name="tenant group",
  303. weight=100,
  304. data={"tenant_group": 1}
  305. )
  306. tenant_group_context.tenant_groups.add(tenantgroup)
  307. tenant_context = ConfigContext.objects.create(
  308. name="tenant",
  309. weight=100,
  310. data={"tenant": 1}
  311. )
  312. tenant_context.tenants.add(tenant)
  313. tag_context = ConfigContext.objects.create(
  314. name="tag",
  315. weight=100,
  316. data={"tag": 1}
  317. )
  318. tag_context.tags.add(tag)
  319. cluster_type_context = ConfigContext.objects.create(
  320. name="cluster type",
  321. weight=100,
  322. data={"cluster_type": 1}
  323. )
  324. cluster_type_context.cluster_types.add(cluster_type)
  325. cluster_group_context = ConfigContext.objects.create(
  326. name="cluster group",
  327. weight=100,
  328. data={"cluster_group": 1}
  329. )
  330. cluster_group_context.cluster_groups.add(cluster_group)
  331. cluster_context = ConfigContext.objects.create(
  332. name="cluster",
  333. weight=100,
  334. data={"cluster": 1}
  335. )
  336. cluster_context.clusters.add(cluster)
  337. virtual_machine = VirtualMachine.objects.create(
  338. name="VM 1",
  339. cluster=cluster,
  340. tenant=tenant,
  341. platform=platform,
  342. role=DeviceRole.objects.first()
  343. )
  344. virtual_machine.tags.add(tag)
  345. annotated_queryset = VirtualMachine.objects.filter(name=virtual_machine.name).annotate_config_context_data()
  346. self.assertEqual(virtual_machine.get_config_context(), annotated_queryset[0].get_config_context())
  347. def test_virtualmachine_site_context(self):
  348. """
  349. Check that config context associated with a site applies to a VM whether the VM is assigned
  350. directly to that site or via its cluster.
  351. """
  352. site = Site.objects.first()
  353. cluster_type = ClusterType.objects.create(name="Cluster Type")
  354. cluster = Cluster.objects.create(name="Cluster", type=cluster_type, scope=site)
  355. vm_role = DeviceRole.objects.first()
  356. # Create a ConfigContext associated with the site
  357. context = ConfigContext.objects.create(
  358. name="context1",
  359. weight=100,
  360. data={"foo": True}
  361. )
  362. context.sites.add(site)
  363. # Create one VM assigned directly to the site, and one assigned via the cluster
  364. vm1 = VirtualMachine.objects.create(name="VM 1", site=site, role=vm_role)
  365. vm2 = VirtualMachine.objects.create(name="VM 2", cluster=cluster, role=vm_role)
  366. # Check that their individually-rendered config contexts are identical
  367. self.assertEqual(
  368. vm1.get_config_context(),
  369. vm2.get_config_context()
  370. )
  371. # Check that their annotated config contexts are identical
  372. vms = VirtualMachine.objects.filter(pk__in=(vm1.pk, vm2.pk)).annotate_config_context_data()
  373. self.assertEqual(
  374. vms[0].get_config_context(),
  375. vms[1].get_config_context()
  376. )
  377. def test_multiple_tags_return_distinct_objects(self):
  378. """
  379. Tagged items use a generic relationship, which results in duplicate rows being returned when queried.
  380. This is combated by appending distinct() to the config context querysets. This test creates a config
  381. context assigned to two tags and ensures objects related by those same two tags result in only a single
  382. config context record being returned.
  383. See https://github.com/netbox-community/netbox/issues/5314
  384. """
  385. site = Site.objects.first()
  386. platform = Platform.objects.first()
  387. tenant = Tenant.objects.first()
  388. tags = Tag.objects.all()
  389. tag_context = ConfigContext.objects.create(
  390. name="tag",
  391. weight=100,
  392. data={
  393. "tag": 1
  394. }
  395. )
  396. tag_context.tags.set(tags)
  397. device = Device.objects.create(
  398. name="Device 3",
  399. site=site,
  400. tenant=tenant,
  401. platform=platform,
  402. role=DeviceRole.objects.first(),
  403. device_type=DeviceType.objects.first()
  404. )
  405. device.tags.set(tags)
  406. annotated_queryset = Device.objects.filter(name=device.name).annotate_config_context_data()
  407. self.assertEqual(ConfigContext.objects.get_for_object(device).count(), 1)
  408. self.assertEqual(device.get_config_context(), annotated_queryset[0].get_config_context())
  409. def test_multiple_tags_return_distinct_objects_with_seperate_config_contexts(self):
  410. """
  411. Tagged items use a generic relationship, which results in duplicate rows being returned when queried.
  412. This is combatted by by appending distinct() to the config context querysets. This test creates a config
  413. context assigned to two tags and ensures objects related by those same two tags result in only a single
  414. config context record being returned.
  415. This test case is seperate from the above in that it deals with multiple config context objects in play.
  416. See https://github.com/netbox-community/netbox/issues/5387
  417. """
  418. site = Site.objects.first()
  419. platform = Platform.objects.first()
  420. tenant = Tenant.objects.first()
  421. tag1, tag2 = list(Tag.objects.all())
  422. tag_context_1 = ConfigContext.objects.create(
  423. name="tag-1",
  424. weight=100,
  425. data={
  426. "tag": 1
  427. }
  428. )
  429. tag_context_1.tags.add(tag1)
  430. tag_context_2 = ConfigContext.objects.create(
  431. name="tag-2",
  432. weight=100,
  433. data={
  434. "tag": 1
  435. }
  436. )
  437. tag_context_2.tags.add(tag2)
  438. device = Device.objects.create(
  439. name="Device 3",
  440. site=site,
  441. tenant=tenant,
  442. platform=platform,
  443. role=DeviceRole.objects.first(),
  444. device_type=DeviceType.objects.first()
  445. )
  446. device.tags.set([tag1, tag2])
  447. annotated_queryset = Device.objects.filter(name=device.name).annotate_config_context_data()
  448. self.assertEqual(ConfigContext.objects.get_for_object(device).count(), 2)
  449. self.assertEqual(device.get_config_context(), annotated_queryset[0].get_config_context())
  450. def test_valid_local_context_data(self):
  451. device = Device.objects.first()
  452. device.local_context_data = None
  453. device.clean()
  454. device.local_context_data = {"foo": "bar"}
  455. device.clean()
  456. def test_invalid_local_context_data(self):
  457. device = Device.objects.first()
  458. device.local_context_data = ""
  459. with self.assertRaises(ValidationError):
  460. device.clean()
  461. device.local_context_data = 0
  462. with self.assertRaises(ValidationError):
  463. device.clean()
  464. device.local_context_data = False
  465. with self.assertRaises(ValidationError):
  466. device.clean()
  467. device.local_context_data = 'foo'
  468. with self.assertRaises(ValidationError):
  469. device.clean()
  470. class ConfigTemplateTest(TestCase):
  471. """
  472. TODO: These test cases deal with the weighting, ordering, and deep merge logic of config context data.
  473. """
  474. MAIN_TEMPLATE = """
  475. {%- include 'base.j2' %}
  476. """.strip()
  477. BASE_TEMPLATE = """
  478. Hi
  479. """.strip()
  480. @classmethod
  481. def _create_template_file(cls, templates_dir, file_name, content):
  482. template_file_name = file_name
  483. if not template_file_name.endswith('j2'):
  484. template_file_name += '.j2'
  485. temp_file_path = templates_dir / template_file_name
  486. with open(temp_file_path, 'w') as f:
  487. f.write(content)
  488. @classmethod
  489. def setUpTestData(cls):
  490. temp_dir = tempfile.TemporaryDirectory()
  491. templates_dir = Path(temp_dir.name) / "templates"
  492. templates_dir.mkdir(parents=True, exist_ok=True)
  493. cls._create_template_file(templates_dir, 'base.j2', cls.BASE_TEMPLATE)
  494. cls._create_template_file(templates_dir, 'main.j2', cls.MAIN_TEMPLATE)
  495. data_source = DataSource(
  496. name="Test DataSource",
  497. type="local",
  498. source_url=str(templates_dir),
  499. )
  500. data_source.save()
  501. data_source.sync()
  502. base_config_template = ConfigTemplate(
  503. name="BaseTemplate",
  504. data_file=data_source.datafiles.filter(path__endswith='base.j2').first()
  505. )
  506. base_config_template.clean()
  507. base_config_template.save()
  508. cls.base_config_template = base_config_template
  509. main_config_template = ConfigTemplate(
  510. name="MainTemplate",
  511. data_file=data_source.datafiles.filter(path__endswith='main.j2').first()
  512. )
  513. main_config_template.clean()
  514. main_config_template.save()
  515. cls.main_config_template = main_config_template
  516. @tag('regression')
  517. def test_config_template_with_data_source(self):
  518. self.assertEqual(self.BASE_TEMPLATE, self.base_config_template.render({}))
  519. @tag('regression')
  520. def test_config_template_with_data_source_nested_templates(self):
  521. self.assertEqual(self.BASE_TEMPLATE, self.main_config_template.render({}))