test_api.py 56 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529
  1. import json
  2. import logging
  3. from django.test import tag
  4. from django.urls import reverse
  5. from netaddr import IPNetwork
  6. from rest_framework import status
  7. from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
  8. from ipam.choices import *
  9. from ipam.models import *
  10. from tenancy.models import Tenant
  11. from utilities.data import string_to_ranges
  12. from utilities.testing import APITestCase, APIViewTestCases, create_test_device, disable_logging
  13. class AppTestCase(APITestCase):
  14. def test_root(self):
  15. url = reverse('ipam-api:api-root')
  16. response = self.client.get('{}?format=api'.format(url), **self.header)
  17. self.assertEqual(response.status_code, 200)
  18. class ASNRangeTestCase(APIViewTestCases.APIViewTestCase):
  19. model = ASNRange
  20. brief_fields = ['description', 'display', 'id', 'name', 'url']
  21. bulk_update_data = {
  22. 'description': 'New description',
  23. }
  24. @classmethod
  25. def setUpTestData(cls):
  26. rirs = (
  27. RIR(name='RIR 1', slug='rir-1', is_private=True),
  28. RIR(name='RIR 2', slug='rir-2', is_private=True),
  29. )
  30. RIR.objects.bulk_create(rirs)
  31. tenants = (
  32. Tenant(name='Tenant 1', slug='tenant-1'),
  33. Tenant(name='Tenant 2', slug='tenant-2'),
  34. )
  35. Tenant.objects.bulk_create(tenants)
  36. asn_ranges = (
  37. ASNRange(name='ASN Range 1', slug='asn-range-1', rir=rirs[0], tenant=tenants[0], start=100, end=199),
  38. ASNRange(name='ASN Range 2', slug='asn-range-2', rir=rirs[0], tenant=tenants[0], start=200, end=299),
  39. ASNRange(name='ASN Range 3', slug='asn-range-3', rir=rirs[0], tenant=tenants[0], start=300, end=399),
  40. )
  41. ASNRange.objects.bulk_create(asn_ranges)
  42. cls.create_data = [
  43. {
  44. 'name': 'ASN Range 4',
  45. 'slug': 'asn-range-4',
  46. 'rir': rirs[1].pk,
  47. 'start': 400,
  48. 'end': 499,
  49. 'tenant': tenants[1].pk,
  50. },
  51. {
  52. 'name': 'ASN Range 5',
  53. 'slug': 'asn-range-5',
  54. 'rir': rirs[1].pk,
  55. 'start': 500,
  56. 'end': 599,
  57. 'tenant': tenants[1].pk,
  58. },
  59. {
  60. 'name': 'ASN Range 6',
  61. 'slug': 'asn-range-6',
  62. 'rir': rirs[1].pk,
  63. 'start': 600,
  64. 'end': 699,
  65. 'tenant': tenants[1].pk,
  66. },
  67. ]
  68. def test_list_available_asns(self):
  69. """
  70. Test retrieval of all available ASNs within a parent range.
  71. """
  72. rir = RIR.objects.first()
  73. asnrange = ASNRange.objects.create(name='Range 1', slug='range-1', rir=rir, start=101, end=110)
  74. url = reverse('ipam-api:asnrange-available-asns', kwargs={'pk': asnrange.pk})
  75. self.add_permissions('ipam.view_asnrange', 'ipam.view_asn')
  76. response = self.client.get(url, **self.header)
  77. self.assertHttpStatus(response, status.HTTP_200_OK)
  78. self.assertEqual(len(response.data), 10)
  79. def test_create_single_available_asn(self):
  80. """
  81. Test creation of the first available ASN within a range.
  82. """
  83. rir = RIR.objects.first()
  84. asnrange = ASNRange.objects.create(name='Range 1', slug='range-1', rir=rir, start=101, end=110)
  85. url = reverse('ipam-api:asnrange-available-asns', kwargs={'pk': asnrange.pk})
  86. self.add_permissions('ipam.view_asnrange', 'ipam.add_asn')
  87. data = {
  88. 'description': 'New ASN'
  89. }
  90. response = self.client.post(url, data, format='json', **self.header)
  91. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  92. self.assertEqual(response.data['rir']['id'], asnrange.rir.pk)
  93. self.assertEqual(response.data['description'], data['description'])
  94. def test_create_multiple_available_asns(self):
  95. """
  96. Test the creation of several available ASNs within a parent range.
  97. """
  98. rir = RIR.objects.first()
  99. asnrange = ASNRange.objects.create(name='Range 1', slug='range-1', rir=rir, start=101, end=110)
  100. url = reverse('ipam-api:asnrange-available-asns', kwargs={'pk': asnrange.pk})
  101. self.add_permissions('ipam.view_asnrange', 'ipam.add_asn')
  102. # Try to create eleven ASNs (only ten are available)
  103. data = [
  104. {'description': f'New ASN {i}'}
  105. for i in range(1, 12)
  106. ]
  107. assert len(data) == 11
  108. response = self.client.post(url, data, format='json', **self.header)
  109. self.assertHttpStatus(response, status.HTTP_409_CONFLICT)
  110. self.assertIn('detail', response.data)
  111. # Create all ten available ASNs in a single request
  112. data.pop()
  113. assert len(data) == 10
  114. response = self.client.post(url, data, format='json', **self.header)
  115. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  116. self.assertEqual(len(response.data), 10)
  117. class ASNTestCase(APIViewTestCases.APIViewTestCase):
  118. model = ASN
  119. brief_fields = ['asn', 'description', 'display', 'id', 'url']
  120. bulk_update_data = {
  121. 'description': 'New description',
  122. }
  123. @classmethod
  124. def setUpTestData(cls):
  125. rirs = (
  126. RIR(name='RIR 1', slug='rir-1', is_private=True),
  127. RIR(name='RIR 2', slug='rir-2', is_private=True),
  128. )
  129. RIR.objects.bulk_create(rirs)
  130. roles = (
  131. Role(name='Role 1', slug='role-1'),
  132. Role(name='Role 2', slug='role-2'),
  133. )
  134. Role.objects.bulk_create(roles)
  135. sites = (
  136. Site(name='Site 1', slug='site-1'),
  137. Site(name='Site 2', slug='site-2')
  138. )
  139. Site.objects.bulk_create(sites)
  140. tenants = (
  141. Tenant(name='Tenant 1', slug='tenant-1'),
  142. Tenant(name='Tenant 2', slug='tenant-2'),
  143. )
  144. Tenant.objects.bulk_create(tenants)
  145. asns = (
  146. ASN(asn=65000, rir=rirs[0], role=roles[0], tenant=tenants[0]),
  147. ASN(asn=65001, rir=rirs[0], role=roles[0], tenant=tenants[1]),
  148. ASN(asn=4200000000, rir=rirs[1], role=roles[1], tenant=tenants[0]),
  149. ASN(asn=4200000001, rir=rirs[1], role=roles[1], tenant=tenants[1]),
  150. )
  151. ASN.objects.bulk_create(asns)
  152. asns[0].sites.set([sites[0]])
  153. asns[1].sites.set([sites[1]])
  154. asns[2].sites.set([sites[0]])
  155. asns[3].sites.set([sites[1]])
  156. cls.create_data = [
  157. {
  158. 'asn': 64512,
  159. 'rir': rirs[0].pk,
  160. 'role': roles[0].pk,
  161. },
  162. {
  163. 'asn': 65002,
  164. 'rir': rirs[0].pk,
  165. 'role': roles[1].pk,
  166. },
  167. {
  168. 'asn': 4200000002,
  169. 'rir': rirs[1].pk,
  170. },
  171. ]
  172. class VRFTestCase(APIViewTestCases.APIViewTestCase):
  173. model = VRF
  174. brief_fields = ['description', 'display', 'id', 'name', 'prefix_count', 'rd', 'url']
  175. create_data = [
  176. {
  177. 'name': 'VRF 4',
  178. 'rd': '65000:4',
  179. },
  180. {
  181. 'name': 'VRF 5',
  182. 'rd': '65000:5',
  183. },
  184. {
  185. 'name': 'VRF 6',
  186. 'rd': '65000:6',
  187. },
  188. ]
  189. bulk_update_data = {
  190. 'description': 'New description',
  191. }
  192. @classmethod
  193. def setUpTestData(cls):
  194. vrfs = (
  195. VRF(name='VRF 1', rd='65000:1'),
  196. VRF(name='VRF 2', rd='65000:2'),
  197. VRF(name='VRF 3'), # No RD
  198. )
  199. VRF.objects.bulk_create(vrfs)
  200. class RouteTargetTestCase(APIViewTestCases.APIViewTestCase):
  201. model = RouteTarget
  202. brief_fields = ['description', 'display', 'id', 'name', 'url']
  203. create_data = [
  204. {
  205. 'name': '65000:1004',
  206. },
  207. {
  208. 'name': '65000:1005',
  209. },
  210. {
  211. 'name': '65000:1006',
  212. },
  213. ]
  214. bulk_update_data = {
  215. 'description': 'New description',
  216. }
  217. @classmethod
  218. def setUpTestData(cls):
  219. route_targets = (
  220. RouteTarget(name='65000:1001'),
  221. RouteTarget(name='65000:1002'),
  222. RouteTarget(name='65000:1003'),
  223. )
  224. RouteTarget.objects.bulk_create(route_targets)
  225. class RIRTestCase(APIViewTestCases.APIViewTestCase):
  226. model = RIR
  227. brief_fields = ['aggregate_count', 'description', 'display', 'id', 'name', 'slug', 'url']
  228. create_data = [
  229. {
  230. 'name': 'RIR 4',
  231. 'slug': 'rir-4',
  232. },
  233. {
  234. 'name': 'RIR 5',
  235. 'slug': 'rir-5',
  236. },
  237. {
  238. 'name': 'RIR 6',
  239. 'slug': 'rir-6',
  240. },
  241. ]
  242. bulk_update_data = {
  243. 'description': 'New description',
  244. }
  245. @classmethod
  246. def setUpTestData(cls):
  247. rirs = (
  248. RIR(name='RIR 1', slug='rir-1'),
  249. RIR(name='RIR 2', slug='rir-2'),
  250. RIR(name='RIR 3', slug='rir-3'),
  251. )
  252. RIR.objects.bulk_create(rirs)
  253. class AggregateTestCase(APIViewTestCases.APIViewTestCase):
  254. model = Aggregate
  255. brief_fields = ['description', 'display', 'family', 'id', 'prefix', 'url']
  256. bulk_update_data = {
  257. 'description': 'New description',
  258. }
  259. @classmethod
  260. def setUpTestData(cls):
  261. rirs = (
  262. RIR(name='RIR 1', slug='rir-1'),
  263. RIR(name='RIR 2', slug='rir-2'),
  264. )
  265. RIR.objects.bulk_create(rirs)
  266. aggregates = (
  267. Aggregate(prefix=IPNetwork('10.0.0.0/8'), rir=rirs[0]),
  268. Aggregate(prefix=IPNetwork('172.16.0.0/12'), rir=rirs[0]),
  269. Aggregate(prefix=IPNetwork('192.168.0.0/16'), rir=rirs[0]),
  270. )
  271. Aggregate.objects.bulk_create(aggregates)
  272. cls.create_data = [
  273. {
  274. 'prefix': '100.0.0.0/8',
  275. 'rir': rirs[1].pk,
  276. },
  277. {
  278. 'prefix': '101.0.0.0/8',
  279. 'rir': rirs[1].pk,
  280. },
  281. {
  282. 'prefix': '102.0.0.0/8',
  283. 'rir': rirs[1].pk,
  284. },
  285. ]
  286. @tag('regression')
  287. def test_graphql_aggregate_prefix_exact(self):
  288. """
  289. Test case to verify aggregate prefix equality via field lookup in GraphQL API.
  290. """
  291. self.add_permissions('ipam.view_aggregate', 'ipam.view_rir')
  292. rir = RIR.objects.create(name='RFC6598', slug='rfc6598', is_private=True)
  293. aggregate1 = Aggregate.objects.create(prefix='100.64.0.0/10', rir=rir)
  294. Aggregate.objects.create(prefix='203.0.113.0/24', rir=rir)
  295. url = reverse('graphql')
  296. query = """{
  297. aggregate_list(filters: { prefix: { exact: "100.64.0.0/10" } }) { prefix }
  298. }"""
  299. response = self.client.post(url, data={'query': query}, format='json', **self.header)
  300. self.assertHttpStatus(response, status.HTTP_200_OK)
  301. data = response.json()
  302. self.assertNotIn('errors', data)
  303. prefixes = {row['prefix'] for row in data['data']['aggregate_list']}
  304. self.assertIn(str(aggregate1.prefix), prefixes)
  305. @tag('regression')
  306. def test_graphql_aggregate_contains_skips_invalid(self):
  307. """
  308. Test the GraphQL API Aggregate `contains` filter skips invalid input.
  309. """
  310. self.add_permissions('ipam.view_aggregate', 'ipam.view_rir')
  311. rir = RIR.objects.create(name='RIR 3', slug='rir-3', is_private=False)
  312. aggregate1 = Aggregate.objects.create(prefix='100.64.0.0/10', rir=rir)
  313. Aggregate.objects.create(prefix='203.0.113.0/24', rir=rir)
  314. url = reverse('graphql')
  315. query = """{
  316. aggregate_list(filters: { contains: ["100.64.16.0/24", "not-a-cidr", ""] }) { prefix }
  317. }"""
  318. response = self.client.post(url, data={'query': query}, format='json', **self.header)
  319. self.assertHttpStatus(response, status.HTTP_200_OK)
  320. data = response.json()
  321. self.assertNotIn('errors', data)
  322. prefixes = {row['prefix'] for row in data['data']['aggregate_list']}
  323. self.assertIn(str(aggregate1.prefix), prefixes)
  324. # No exception occurred; invalid entries were ignored
  325. class RoleTestCase(APIViewTestCases.APIViewTestCase):
  326. model = Role
  327. brief_fields = ['asn_count', 'description', 'display', 'id', 'name', 'prefix_count', 'slug', 'url', 'vlan_count']
  328. create_data = [
  329. {
  330. 'name': 'Role 4',
  331. 'slug': 'role-4',
  332. },
  333. {
  334. 'name': 'Role 5',
  335. 'slug': 'role-5',
  336. },
  337. {
  338. 'name': 'Role 6',
  339. 'slug': 'role-6',
  340. },
  341. ]
  342. bulk_update_data = {
  343. 'description': 'New description',
  344. }
  345. @classmethod
  346. def setUpTestData(cls):
  347. roles = (
  348. Role(name='Role 1', slug='role-1'),
  349. Role(name='Role 2', slug='role-2'),
  350. Role(name='Role 3', slug='role-3'),
  351. )
  352. Role.objects.bulk_create(roles)
  353. rirs = (
  354. RIR(name='RIR 1', slug='rir-1', is_private=True),
  355. )
  356. RIR.objects.bulk_create(rirs)
  357. asns = (
  358. ASN(asn=65000, rir=rirs[0], role=roles[0]),
  359. ASN(asn=65001, rir=rirs[0], role=roles[0]),
  360. )
  361. ASN.objects.bulk_create(asns)
  362. class PrefixTestCase(APIViewTestCases.APIViewTestCase):
  363. model = Prefix
  364. brief_fields = ['_depth', 'description', 'display', 'family', 'id', 'prefix', 'url']
  365. create_data = [
  366. {
  367. 'prefix': '192.168.4.0/24',
  368. },
  369. {
  370. 'prefix': '192.168.5.0/24',
  371. },
  372. {
  373. 'prefix': '192.168.6.0/24',
  374. },
  375. ]
  376. bulk_update_data = {
  377. 'description': 'New description',
  378. }
  379. @classmethod
  380. def setUpTestData(cls):
  381. prefixes = (
  382. Prefix(prefix=IPNetwork('192.168.1.0/24')),
  383. Prefix(prefix=IPNetwork('192.168.2.0/24')),
  384. Prefix(prefix=IPNetwork('192.168.3.0/24')),
  385. )
  386. Prefix.objects.bulk_create(prefixes)
  387. @tag('regression')
  388. def test_create_with_invalid_prefix(self):
  389. """
  390. POST of a malformed prefix value returns a 400 validation error.
  391. """
  392. self.add_permissions('ipam.add_prefix')
  393. url = reverse('ipam-api:prefix-list')
  394. response = self.client.post(url, {'prefix': 'invalid'}, format='json', **self.header)
  395. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  396. self.assertEqual(response.data['prefix'][0], 'Invalid IP prefix format: invalid')
  397. @tag('regression')
  398. def test_clean_validates_scope(self):
  399. prefix = Prefix.objects.first()
  400. site = Site.objects.create(name='Test Site', slug='test-site')
  401. data = {'scope_type': 'dcim.site', 'scope_id': site.id}
  402. url = reverse('ipam-api:prefix-detail', kwargs={'pk': prefix.pk})
  403. self.add_permissions('ipam.change_prefix')
  404. response = self.client.patch(url, data, format='json', **self.header)
  405. self.assertHttpStatus(response, status.HTTP_200_OK)
  406. def test_list_available_prefixes(self):
  407. """
  408. Test retrieval of all available prefixes within a parent prefix.
  409. """
  410. vrf = VRF.objects.create(name='VRF 1')
  411. prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/24'), vrf=vrf)
  412. Prefix.objects.create(prefix=IPNetwork('192.0.2.64/26'), vrf=vrf)
  413. Prefix.objects.create(prefix=IPNetwork('192.0.2.192/27'), vrf=vrf)
  414. url = reverse('ipam-api:prefix-available-prefixes', kwargs={'pk': prefix.pk})
  415. self.add_permissions('ipam.view_prefix')
  416. # Retrieve all available IPs
  417. response = self.client.get(url, **self.header)
  418. available_prefixes = ['192.0.2.0/26', '192.0.2.128/26', '192.0.2.224/27']
  419. for i, p in enumerate(response.data):
  420. self.assertEqual(p['prefix'], available_prefixes[i])
  421. def test_create_single_available_prefix(self):
  422. """
  423. Test retrieval of the first available prefix within a parent prefix.
  424. """
  425. vrf = VRF.objects.create(name='VRF 1')
  426. prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/28'), vrf=vrf, is_pool=True)
  427. url = reverse('ipam-api:prefix-available-prefixes', kwargs={'pk': prefix.pk})
  428. self.add_permissions('ipam.view_prefix', 'ipam.add_prefix')
  429. # Create four available prefixes with individual requests
  430. prefixes_to_be_created = [
  431. '192.0.2.0/30',
  432. '192.0.2.4/30',
  433. '192.0.2.8/30',
  434. '192.0.2.12/30',
  435. ]
  436. for i in range(4):
  437. data = {
  438. 'prefix_length': 30,
  439. 'description': 'Test Prefix {}'.format(i + 1)
  440. }
  441. response = self.client.post(url, data, format='json', **self.header)
  442. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  443. self.assertEqual(response.data['prefix'], prefixes_to_be_created[i])
  444. self.assertEqual(response.data['vrf']['id'], vrf.pk)
  445. self.assertEqual(response.data['description'], data['description'])
  446. # Try to create one more prefix
  447. response = self.client.post(url, {'prefix_length': 30}, format='json', **self.header)
  448. self.assertHttpStatus(response, status.HTTP_409_CONFLICT)
  449. self.assertIn('detail', response.data)
  450. # Try to create invalid prefix type
  451. response = self.client.post(url, {'prefix_length': '30'}, format='json', **self.header)
  452. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  453. self.assertIn('prefix_length', response.data[0])
  454. def test_create_multiple_available_prefixes(self):
  455. """
  456. Test the creation of available prefixes within a parent prefix.
  457. """
  458. vrf = VRF.objects.create(name='VRF 1')
  459. prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/28'), vrf=vrf, is_pool=True)
  460. url = reverse('ipam-api:prefix-available-prefixes', kwargs={'pk': prefix.pk})
  461. self.add_permissions('ipam.view_prefix', 'ipam.add_prefix')
  462. # Try to create five /30s (only four are available)
  463. data = [
  464. {'prefix_length': 30, 'description': 'Prefix 1'},
  465. {'prefix_length': 30, 'description': 'Prefix 2'},
  466. {'prefix_length': 30, 'description': 'Prefix 3'},
  467. {'prefix_length': 30, 'description': 'Prefix 4'},
  468. {'prefix_length': 30, 'description': 'Prefix 5'},
  469. ]
  470. response = self.client.post(url, data, format='json', **self.header)
  471. self.assertHttpStatus(response, status.HTTP_409_CONFLICT)
  472. self.assertIn('detail', response.data)
  473. # Verify that no prefixes were created (the entire /28 is still available)
  474. response = self.client.get(url, **self.header)
  475. self.assertHttpStatus(response, status.HTTP_200_OK)
  476. self.assertEqual(response.data[0]['prefix'], '192.0.2.0/28')
  477. # Create four /30s in a single request
  478. response = self.client.post(url, data[:4], format='json', **self.header)
  479. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  480. self.assertEqual(len(response.data), 4)
  481. def test_list_available_ips(self):
  482. """
  483. Test retrieval of all available IP addresses within a parent prefix.
  484. """
  485. vrf = VRF.objects.create(name='VRF 1')
  486. prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/29'), vrf=vrf, is_pool=True)
  487. url = reverse('ipam-api:prefix-available-ips', kwargs={'pk': prefix.pk})
  488. self.add_permissions('ipam.view_prefix', 'ipam.view_ipaddress')
  489. # Retrieve all available IPs
  490. response = self.client.get(url, **self.header)
  491. self.assertHttpStatus(response, status.HTTP_200_OK)
  492. self.assertEqual(len(response.data), 8) # 8 because prefix.is_pool = True
  493. # Change the prefix to not be a pool and try again
  494. prefix.is_pool = False
  495. prefix.save()
  496. response = self.client.get(url, **self.header)
  497. self.assertEqual(len(response.data), 6) # 8 - 2 because prefix.is_pool = False
  498. def test_create_single_available_ip(self):
  499. """
  500. Test retrieval of the first available IP address within a parent prefix.
  501. """
  502. vrf = VRF.objects.create(name='VRF 1')
  503. prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/30'), vrf=vrf, is_pool=True)
  504. url = reverse('ipam-api:prefix-available-ips', kwargs={'pk': prefix.pk})
  505. self.add_permissions('ipam.view_prefix', 'ipam.add_ipaddress')
  506. # Create all four available IPs with individual requests
  507. for i in range(1, 5):
  508. data = {
  509. 'description': 'Test IP {}'.format(i)
  510. }
  511. response = self.client.post(url, data, format='json', **self.header)
  512. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  513. self.assertEqual(response.data['vrf']['id'], vrf.pk)
  514. self.assertEqual(response.data['description'], data['description'])
  515. # Try to create one more IP
  516. response = self.client.post(url, {}, format='json', **self.header)
  517. self.assertHttpStatus(response, status.HTTP_409_CONFLICT)
  518. self.assertIn('detail', response.data)
  519. def test_create_multiple_available_ips(self):
  520. """
  521. Test the creation of available IP addresses within a parent prefix.
  522. """
  523. vrf = VRF.objects.create(name='VRF 1')
  524. prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/29'), vrf=vrf, is_pool=True)
  525. url = reverse('ipam-api:prefix-available-ips', kwargs={'pk': prefix.pk})
  526. self.add_permissions('ipam.view_prefix', 'ipam.add_ipaddress')
  527. # Try to create nine IPs (only eight are available)
  528. data = [{'description': f'Test IP {i}'} for i in range(1, 10)] # 9 IPs
  529. response = self.client.post(url, data, format='json', **self.header)
  530. self.assertHttpStatus(response, status.HTTP_409_CONFLICT)
  531. self.assertIn('detail', response.data)
  532. # Create all eight available IPs in a single request
  533. data = [{'description': 'Test IP {}'.format(i)} for i in range(1, 9)] # 8 IPs
  534. response = self.client.post(url, data, format='json', **self.header)
  535. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  536. self.assertEqual(len(response.data), 8)
  537. def test_create_available_ip_with_mask(self):
  538. """
  539. Test the creation of an available IP address with a specific prefix length.
  540. """
  541. prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/24'))
  542. url = reverse('ipam-api:prefix-available-ips', kwargs={'pk': prefix.pk})
  543. self.add_permissions('ipam.view_prefix', 'ipam.add_ipaddress')
  544. # Create an available IP with a specific prefix length
  545. data = {
  546. 'prefix_length': 32,
  547. 'description': 'Test IP 1',
  548. }
  549. response = self.client.post(url, data, format='json', **self.header)
  550. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  551. self.assertEqual(response.data['address'], '192.0.2.1/32')
  552. self.assertEqual(response.data['description'], data['description'])
  553. # Attempt to create an available IP with a prefix length less than its parent prefix
  554. data = {
  555. 'prefix_length': 23, # Prefix is a /24
  556. }
  557. response = self.client.post(url, data, format='json', **self.header)
  558. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  559. @tag('regression')
  560. def test_graphql_tenant_prefixes_contains_nested_skips_invalid(self):
  561. """
  562. Test the GraphQL API Tenant nested Prefix `contains` filter skips invalid input.
  563. """
  564. self.add_permissions('ipam.view_prefix', 'ipam.view_vrf', 'tenancy.view_tenant')
  565. tenant = Tenant.objects.create(name='Tenant 1', slug='tenant-1')
  566. vrf = VRF.objects.create(name='Test VRF 1', rd='64512:1')
  567. Prefix.objects.create(prefix='10.20.0.0/16', vrf=vrf, tenant=tenant)
  568. Prefix.objects.create(prefix='198.51.100.0/24', vrf=vrf) # non-tenant
  569. url = reverse('graphql')
  570. query = """{
  571. tenant_list(filters: { prefixes: { contains: ["10.20.1.0/24", "not-a-cidr"] } }) { id }
  572. }"""
  573. response = self.client.post(url, data={'query': query}, format='json', **self.header)
  574. self.assertHttpStatus(response, status.HTTP_200_OK)
  575. data = response.json()
  576. self.assertNotIn('errors', data)
  577. self.assertTrue(data['data']['tenant_list']) # tenant returned
  578. class IPRangeTestCase(APIViewTestCases.APIViewTestCase):
  579. model = IPRange
  580. brief_fields = ['description', 'display', 'end_address', 'family', 'id', 'start_address', 'url']
  581. create_data = [
  582. {
  583. 'start_address': '192.168.4.10/24',
  584. 'end_address': '192.168.4.50/24',
  585. },
  586. {
  587. 'start_address': '192.168.5.10/24',
  588. 'end_address': '192.168.5.50/24',
  589. },
  590. {
  591. 'start_address': '192.168.6.10/24',
  592. 'end_address': '192.168.6.50/24',
  593. },
  594. {
  595. # Single-address range (start == end)
  596. 'start_address': '192.168.7.10/24',
  597. 'end_address': '192.168.7.10/24',
  598. },
  599. ]
  600. bulk_update_data = {
  601. 'description': 'New description',
  602. }
  603. @classmethod
  604. def setUpTestData(cls):
  605. ip_ranges = (
  606. IPRange(start_address=IPNetwork('192.168.1.10/24'), end_address=IPNetwork('192.168.1.50/24'), size=51),
  607. IPRange(start_address=IPNetwork('192.168.2.10/24'), end_address=IPNetwork('192.168.2.50/24'), size=51),
  608. IPRange(start_address=IPNetwork('192.168.3.10/24'), end_address=IPNetwork('192.168.3.50/24'), size=51),
  609. )
  610. IPRange.objects.bulk_create(ip_ranges)
  611. def test_list_available_ips(self):
  612. """
  613. Test retrieval of all available IP addresses within a parent IP range.
  614. """
  615. iprange = IPRange.objects.create(
  616. start_address=IPNetwork('192.0.2.10/24'),
  617. end_address=IPNetwork('192.0.2.19/24')
  618. )
  619. url = reverse('ipam-api:iprange-available-ips', kwargs={'pk': iprange.pk})
  620. self.add_permissions('ipam.view_iprange', 'ipam.view_ipaddress')
  621. # Retrieve all available IPs
  622. response = self.client.get(url, **self.header)
  623. self.assertHttpStatus(response, status.HTTP_200_OK)
  624. self.assertEqual(len(response.data), 10)
  625. def test_create_single_available_ip(self):
  626. """
  627. Test retrieval of the first available IP address within a parent IP range.
  628. """
  629. vrf = VRF.objects.create(name='Test VRF 1', rd='1234')
  630. iprange = IPRange.objects.create(
  631. start_address=IPNetwork('192.0.2.1/24'),
  632. end_address=IPNetwork('192.0.2.3/24'),
  633. vrf=vrf
  634. )
  635. url = reverse('ipam-api:iprange-available-ips', kwargs={'pk': iprange.pk})
  636. self.add_permissions('ipam.view_iprange', 'ipam.add_ipaddress')
  637. # Create all three available IPs with individual requests
  638. for i in range(1, 4):
  639. data = {
  640. 'description': f'Test IP #{i}'
  641. }
  642. response = self.client.post(url, data, format='json', **self.header)
  643. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  644. self.assertEqual(response.data['vrf']['id'], vrf.pk)
  645. self.assertEqual(response.data['description'], data['description'])
  646. # Try to create one more IP
  647. response = self.client.post(url, {}, format='json', **self.header)
  648. self.assertHttpStatus(response, status.HTTP_409_CONFLICT)
  649. self.assertIn('detail', response.data)
  650. def test_create_multiple_available_ips(self):
  651. """
  652. Test the creation of available IP addresses within a parent IP range.
  653. """
  654. iprange = IPRange.objects.create(
  655. start_address=IPNetwork('192.0.2.1/24'),
  656. end_address=IPNetwork('192.0.2.8/24')
  657. )
  658. url = reverse('ipam-api:iprange-available-ips', kwargs={'pk': iprange.pk})
  659. self.add_permissions('ipam.view_iprange', 'ipam.add_ipaddress')
  660. # Try to create nine IPs (only eight are available)
  661. data = [{'description': f'Test IP #{i}'} for i in range(1, 10)] # 9 IPs
  662. response = self.client.post(url, data, format='json', **self.header)
  663. self.assertHttpStatus(response, status.HTTP_409_CONFLICT)
  664. self.assertIn('detail', response.data)
  665. # Create all eight available IPs in a single request
  666. data = [{'description': f'Test IP #{i}'} for i in range(1, 9)] # 8 IPs
  667. response = self.client.post(url, data, format='json', **self.header)
  668. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  669. self.assertEqual(len(response.data), 8)
  670. @tag('regression')
  671. def test_graphql_tenant_ip_ranges_parent_nested_skips_invalid(self):
  672. """
  673. Test the GraphQL API Tenant nested IP Range `parent` filter skips invalid input.
  674. """
  675. self.add_permissions('tenancy.view_tenant', 'ipam.view_iprange', 'ipam.view_vrf')
  676. tenant = Tenant.objects.create(name='Tenant 1', slug='tenant-1')
  677. vrf = VRF.objects.create(name='Test VRF 1', rd='64512:1')
  678. IPRange.objects.create(
  679. start_address=IPNetwork('10.30.0.1/24'), end_address=IPNetwork('10.30.0.255/24'), vrf=vrf, tenant=tenant
  680. )
  681. IPRange.objects.create(
  682. start_address=IPNetwork('10.31.0.1/24'), end_address=IPNetwork('10.31.0.255/24'), vrf=vrf, tenant=tenant
  683. )
  684. url = reverse('graphql')
  685. query = """{
  686. tenant_list(filters: {
  687. name: { exact: "Tenant 1" }
  688. ip_ranges: { parent: ["10.30.0.0/24", "bogus"] }
  689. }) { id }
  690. }"""
  691. response = self.client.post(url, data={'query': query}, format='json', **self.header)
  692. self.assertHttpStatus(response, status.HTTP_200_OK)
  693. data = response.json()
  694. self.assertNotIn('errors', data)
  695. self.assertTrue(data['data']['tenant_list']) # tenant returned
  696. # No exception occurred; invalid entries were ignored
  697. @tag('regression')
  698. def test_graphql_tenant_ip_ranges_contains_nested_skips_invalid(self):
  699. """
  700. Test the GraphQL API Tenant nested IP Range `contains` filter skips invalid input.
  701. """
  702. self.add_permissions('tenancy.view_tenant', 'ipam.view_iprange', 'ipam.view_vrf')
  703. tenant = Tenant.objects.create(name='Tenant 2', slug='tenant-2')
  704. vrf = VRF.objects.create(name='Test VRF 1', rd='64512:2')
  705. IPRange.objects.create(
  706. start_address=IPNetwork('10.40.0.1/24'), end_address=IPNetwork('10.40.0.255/24'), vrf=vrf, tenant=tenant
  707. )
  708. url = reverse('graphql')
  709. query = """{
  710. tenant_list(filters: {
  711. name: { exact: "Tenant 2" }
  712. ip_ranges: { contains: ["10.40.0.128/25", "###"] }
  713. }) { id }
  714. }"""
  715. response = self.client.post(url, data={'query': query}, format='json', **self.header)
  716. self.assertHttpStatus(response, status.HTTP_200_OK)
  717. data = response.json()
  718. self.assertNotIn('errors', data)
  719. self.assertTrue(data['data']['tenant_list']) # tenant returned
  720. # No exception occurred; invalid entries were ignored
  721. class IPAddressTestCase(APIViewTestCases.APIViewTestCase):
  722. model = IPAddress
  723. brief_fields = ['address', 'description', 'display', 'family', 'id', 'url']
  724. create_data = [
  725. {
  726. 'address': '192.168.0.4/24',
  727. },
  728. {
  729. 'address': '192.168.0.5/24',
  730. },
  731. {
  732. 'address': '192.168.0.6/24',
  733. },
  734. ]
  735. bulk_update_data = {
  736. 'description': 'New description',
  737. }
  738. graphql_filter = {
  739. 'address': {'lookup': 'i_exact', 'value': '192.168.0.1/24'},
  740. }
  741. @classmethod
  742. def setUpTestData(cls):
  743. ip_addresses = (
  744. IPAddress(address=IPNetwork('192.168.0.1/24')),
  745. IPAddress(address=IPNetwork('192.168.0.2/24')),
  746. IPAddress(address=IPNetwork('192.168.0.3/24')),
  747. )
  748. IPAddress.objects.bulk_create(ip_addresses)
  749. @tag('regression')
  750. def test_create_with_invalid_address(self):
  751. """
  752. POST of a malformed address value returns a 400 validation error.
  753. """
  754. self.add_permissions('ipam.add_ipaddress')
  755. url = reverse('ipam-api:ipaddress-list')
  756. response = self.client.post(url, {'address': 'invalid'}, format='json', **self.header)
  757. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  758. self.assertEqual(response.data['address'][0], 'Invalid IP address format: invalid')
  759. def test_assign_object(self):
  760. """
  761. Test the creation of available IP addresses within a parent IP range.
  762. """
  763. site = Site.objects.create(name='Site 1')
  764. manufacturer = Manufacturer.objects.create(name='Manufacturer 1')
  765. device_type = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer)
  766. role = DeviceRole.objects.create(name='Switch')
  767. device1 = Device.objects.create(
  768. name='Device 1',
  769. site=site,
  770. device_type=device_type,
  771. role=role,
  772. status='active'
  773. )
  774. interface1 = Interface.objects.create(name='Interface 1', device=device1, type='1000baset')
  775. interface2 = Interface.objects.create(name='Interface 2', device=device1, type='1000baset')
  776. device2 = Device.objects.create(
  777. name='Device 2',
  778. site=site,
  779. device_type=device_type,
  780. role=role,
  781. status='active'
  782. )
  783. interface3 = Interface.objects.create(name='Interface 3', device=device2, type='1000baset')
  784. ip_addresses = (
  785. IPAddress(address=IPNetwork('192.168.0.4/24'), assigned_object=interface1),
  786. IPAddress(address=IPNetwork('192.168.1.4/24')),
  787. )
  788. IPAddress.objects.bulk_create(ip_addresses)
  789. ip1 = ip_addresses[0]
  790. ip1.assigned_object = interface1
  791. device1.primary_ip4 = ip_addresses[0]
  792. device1.save()
  793. url = reverse('ipam-api:ipaddress-detail', kwargs={'pk': ip1.pk})
  794. self.add_permissions('ipam.change_ipaddress')
  795. # assign to same parent
  796. data = {
  797. 'assigned_object_id': interface2.pk
  798. }
  799. response = self.client.patch(url, data, format='json', **self.header)
  800. self.assertHttpStatus(response, status.HTTP_200_OK)
  801. # assign to same different parent - should error
  802. data = {
  803. 'assigned_object_id': interface3.pk
  804. }
  805. response = self.client.patch(url, data, format='json', **self.header)
  806. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  807. @tag('regression')
  808. def test_graphql_device_primary_ip4_assigned_nested(self):
  809. """
  810. Test the GraphQL API Device nested IP Address `primary_ip4` filter.
  811. """
  812. self.add_permissions('dcim.view_device', 'dcim.view_interface', 'ipam.view_ipaddress')
  813. site = Site.objects.create(name='Site 1')
  814. manufacturer = Manufacturer.objects.create(name='Manufacturer 1')
  815. device_type = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer)
  816. role = DeviceRole.objects.create(name='Switch')
  817. device1 = Device.objects.create(name='Device 1', site=site, device_type=device_type, role=role, status='active')
  818. interface1 = Interface.objects.create(name='Interface 1', device=device1, type='1000baset')
  819. ip1 = IPAddress.objects.create(address='10.0.0.1/24')
  820. ip1.assigned_object = interface1
  821. ip1.save()
  822. device1.primary_ip4 = ip1
  823. device1.save()
  824. device2 = Device.objects.create(name='Device 2', site=site, device_type=device_type, role=role, status='active')
  825. url = reverse('graphql')
  826. query = """{
  827. device_list(filters: { primary_ip4: { assigned: true } }) { id name }
  828. }"""
  829. response = self.client.post(url, data={'query': query}, format='json', **self.header)
  830. self.assertHttpStatus(response, status.HTTP_200_OK)
  831. data = response.json()
  832. self.assertNotIn('errors', data)
  833. ids = {row['id'] for row in data['data']['device_list']}
  834. self.assertIn(str(device1.pk), ids)
  835. self.assertNotIn(str(device2.pk), ids)
  836. @tag('regression')
  837. def test_graphql_device_primary_ip4_parent_nested_skips_invalid(self):
  838. """
  839. Test the GraphQL API Device nested IP Address `parent` filter skips invalid input.
  840. """
  841. self.add_permissions('dcim.view_device', 'dcim.view_interface', 'ipam.view_ipaddress')
  842. site = Site.objects.create(name='Site 1')
  843. manufacturer = Manufacturer.objects.create(name='Manufacturer 1')
  844. device_type = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer)
  845. role = DeviceRole.objects.create(name='Switch')
  846. device1 = Device.objects.create(name='Device 1', site=site, device_type=device_type, role=role, status='active')
  847. interface1 = Interface.objects.create(name='Interface 1', device=device1, type='1000baset')
  848. ip1 = IPAddress.objects.create(address='192.0.2.10/24')
  849. ip1.assigned_object = interface1
  850. ip1.save()
  851. device1.primary_ip4 = ip1
  852. device1.save()
  853. url = reverse('graphql')
  854. query = """{
  855. device_list(filters: { primary_ip4: { parent: ["192.0.2.0/24", "bad-cidr"] } }) { id }
  856. }"""
  857. response = self.client.post(url, data={'query': query}, format='json', **self.header)
  858. self.assertHttpStatus(response, status.HTTP_200_OK)
  859. data = response.json()
  860. self.assertNotIn('errors', data)
  861. ids = {row['id'] for row in data['data']['device_list']}
  862. self.assertIn(str(device1.pk), ids)
  863. class FHRPGroupTestCase(APIViewTestCases.APIViewTestCase):
  864. model = FHRPGroup
  865. brief_fields = ['description', 'display', 'group_id', 'id', 'protocol', 'url']
  866. bulk_update_data = {
  867. 'protocol': FHRPGroupProtocolChoices.PROTOCOL_GLBP,
  868. 'group_id': 200,
  869. 'auth_type': FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5,
  870. 'auth_key': 'foobarbaz999',
  871. 'name': 'foobar-999',
  872. 'description': 'New description',
  873. }
  874. @classmethod
  875. def setUpTestData(cls):
  876. fhrp_groups = (
  877. FHRPGroup(
  878. protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP2,
  879. group_id=10,
  880. auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_PLAINTEXT,
  881. auth_key='foobar123',
  882. ),
  883. FHRPGroup(
  884. protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP3,
  885. group_id=20,
  886. auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5,
  887. auth_key='foobar123',
  888. ),
  889. FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_HSRP, group_id=30),
  890. )
  891. FHRPGroup.objects.bulk_create(fhrp_groups)
  892. cls.create_data = [
  893. {
  894. 'protocol': FHRPGroupProtocolChoices.PROTOCOL_VRRP2,
  895. 'group_id': 110,
  896. 'auth_type': FHRPGroupAuthTypeChoices.AUTHENTICATION_PLAINTEXT,
  897. 'auth_key': 'foobar123',
  898. },
  899. {
  900. 'protocol': FHRPGroupProtocolChoices.PROTOCOL_VRRP3,
  901. 'group_id': 120,
  902. 'auth_type': FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5,
  903. 'auth_key': 'barfoo456',
  904. },
  905. {
  906. 'protocol': FHRPGroupProtocolChoices.PROTOCOL_GLBP,
  907. 'group_id': 130,
  908. },
  909. ]
  910. class FHRPGroupAssignmentTestCase(APIViewTestCases.APIViewTestCase):
  911. model = FHRPGroupAssignment
  912. brief_fields = ['display', 'group', 'id', 'interface_id', 'interface_type', 'priority', 'url']
  913. bulk_update_data = {
  914. 'priority': 100,
  915. }
  916. user_permissions = ('ipam.view_fhrpgroup', )
  917. @classmethod
  918. def setUpTestData(cls):
  919. device1 = create_test_device('device1')
  920. device2 = create_test_device('device2')
  921. device3 = create_test_device('device3')
  922. interfaces = (
  923. Interface(device=device1, name='eth0', type='other'),
  924. Interface(device=device1, name='eth1', type='other'),
  925. Interface(device=device1, name='eth2', type='other'),
  926. Interface(device=device2, name='eth0', type='other'),
  927. Interface(device=device2, name='eth1', type='other'),
  928. Interface(device=device2, name='eth2', type='other'),
  929. Interface(device=device3, name='eth0', type='other'),
  930. Interface(device=device3, name='eth1', type='other'),
  931. Interface(device=device3, name='eth2', type='other'),
  932. )
  933. Interface.objects.bulk_create(interfaces)
  934. ip_addresses = (
  935. IPAddress(address=IPNetwork('192.168.0.2/24'), assigned_object=interfaces[0]),
  936. IPAddress(address=IPNetwork('192.168.1.2/24'), assigned_object=interfaces[1]),
  937. IPAddress(address=IPNetwork('192.168.2.2/24'), assigned_object=interfaces[2]),
  938. IPAddress(address=IPNetwork('192.168.0.3/24'), assigned_object=interfaces[3]),
  939. IPAddress(address=IPNetwork('192.168.1.3/24'), assigned_object=interfaces[4]),
  940. IPAddress(address=IPNetwork('192.168.2.3/24'), assigned_object=interfaces[5]),
  941. IPAddress(address=IPNetwork('192.168.0.4/24'), assigned_object=interfaces[6]),
  942. IPAddress(address=IPNetwork('192.168.1.4/24'), assigned_object=interfaces[7]),
  943. IPAddress(address=IPNetwork('192.168.2.4/24'), assigned_object=interfaces[8]),
  944. )
  945. IPAddress.objects.bulk_create(ip_addresses)
  946. fhrp_groups = (
  947. FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP2, group_id=10),
  948. FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP2, group_id=20),
  949. FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP2, group_id=30),
  950. )
  951. FHRPGroup.objects.bulk_create(fhrp_groups)
  952. fhrp_group_assignments = (
  953. FHRPGroupAssignment(group=fhrp_groups[0], interface=interfaces[0], priority=10),
  954. FHRPGroupAssignment(group=fhrp_groups[1], interface=interfaces[1], priority=10),
  955. FHRPGroupAssignment(group=fhrp_groups[2], interface=interfaces[2], priority=10),
  956. FHRPGroupAssignment(group=fhrp_groups[0], interface=interfaces[3], priority=20),
  957. FHRPGroupAssignment(group=fhrp_groups[1], interface=interfaces[4], priority=20),
  958. FHRPGroupAssignment(group=fhrp_groups[2], interface=interfaces[5], priority=20),
  959. )
  960. FHRPGroupAssignment.objects.bulk_create(fhrp_group_assignments)
  961. cls.create_data = [
  962. {
  963. 'group': fhrp_groups[0].pk,
  964. 'interface_type': 'dcim.interface',
  965. 'interface_id': interfaces[6].pk,
  966. 'priority': 30,
  967. },
  968. {
  969. 'group': fhrp_groups[1].pk,
  970. 'interface_type': 'dcim.interface',
  971. 'interface_id': interfaces[7].pk,
  972. 'priority': 30,
  973. },
  974. {
  975. 'group': fhrp_groups[2].pk,
  976. 'interface_type': 'dcim.interface',
  977. 'interface_id': interfaces[8].pk,
  978. 'priority': 30,
  979. },
  980. ]
  981. class VLANGroupTestCase(APIViewTestCases.APIViewTestCase):
  982. model = VLANGroup
  983. brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url', 'vlan_count']
  984. create_data = [
  985. {
  986. 'name': 'VLAN Group 4',
  987. 'slug': 'vlan-group-4',
  988. 'vid_ranges': [[1, 4094]]
  989. },
  990. {
  991. 'name': 'VLAN Group 5',
  992. 'slug': 'vlan-group-5',
  993. 'vid_ranges': [[1, 4094]]
  994. },
  995. {
  996. 'name': 'VLAN Group 6',
  997. 'slug': 'vlan-group-6',
  998. 'vid_ranges': [[1, 4094]]
  999. },
  1000. ]
  1001. bulk_update_data = {
  1002. 'description': 'New description',
  1003. }
  1004. @classmethod
  1005. def setUpTestData(cls):
  1006. vlan_groups = (
  1007. VLANGroup(name='VLAN Group 1', slug='vlan-group-1'),
  1008. VLANGroup(name='VLAN Group 2', slug='vlan-group-2'),
  1009. VLANGroup(name='VLAN Group 3', slug='vlan-group-3'),
  1010. )
  1011. VLANGroup.objects.bulk_create(vlan_groups)
  1012. def test_list_available_vlans(self):
  1013. """
  1014. Test retrieval of all available VLANs within a group.
  1015. """
  1016. MIN_VID = 100
  1017. MAX_VID = 199
  1018. self.add_permissions('ipam.view_vlangroup', 'ipam.view_vlan')
  1019. vlangroup = VLANGroup.objects.create(
  1020. name='VLAN Group X',
  1021. slug='vlan-group-x',
  1022. vid_ranges=string_to_ranges(f"{MIN_VID}-{MAX_VID}")
  1023. )
  1024. # Create a set of VLANs within the group
  1025. vlans = (
  1026. VLAN(vid=10, name='VLAN 10', group=vlangroup),
  1027. VLAN(vid=20, name='VLAN 20', group=vlangroup),
  1028. VLAN(vid=30, name='VLAN 30', group=vlangroup),
  1029. )
  1030. VLAN.objects.bulk_create(vlans)
  1031. # Retrieve all available VLANs
  1032. url = reverse('ipam-api:vlangroup-available-vlans', kwargs={'pk': vlangroup.pk})
  1033. response = self.client.get(f'{url}?limit=0', **self.header)
  1034. self.assertEqual(len(response.data), MAX_VID - MIN_VID + 1)
  1035. available_vlans = {vlan['vid'] for vlan in response.data}
  1036. for vlan in vlans:
  1037. self.assertNotIn(vlan.vid, available_vlans)
  1038. # Retrieve a maximum number of available VLANs
  1039. url = reverse('ipam-api:vlangroup-available-vlans', kwargs={'pk': vlangroup.pk})
  1040. response = self.client.get(f'{url}?limit=10', **self.header)
  1041. self.assertEqual(len(response.data), 10)
  1042. def test_create_single_available_vlan(self):
  1043. """
  1044. Test the creation of a single available VLAN.
  1045. """
  1046. self.add_permissions('ipam.view_vlangroup', 'ipam.view_vlan', 'ipam.add_vlan')
  1047. vlangroup = VLANGroup.objects.first()
  1048. VLAN.objects.create(vid=1, name='VLAN 1', group=vlangroup)
  1049. data = {
  1050. "name": "First VLAN",
  1051. }
  1052. url = reverse('ipam-api:vlangroup-available-vlans', kwargs={'pk': vlangroup.pk})
  1053. response = self.client.post(url, data, format='json', **self.header)
  1054. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  1055. self.assertEqual(response.data['name'], data['name'])
  1056. self.assertEqual(response.data['group']['id'], vlangroup.pk)
  1057. self.assertEqual(response.data['vid'], 2)
  1058. def test_create_multiple_available_vlans(self):
  1059. """
  1060. Test the creation of multiple available VLANs.
  1061. """
  1062. self.add_permissions('ipam.view_vlangroup', 'ipam.view_vlan', 'ipam.add_vlan')
  1063. vlangroup = VLANGroup.objects.first()
  1064. vlans = (
  1065. VLAN(vid=1, name='VLAN 1', group=vlangroup),
  1066. VLAN(vid=3, name='VLAN 3', group=vlangroup),
  1067. VLAN(vid=5, name='VLAN 5', group=vlangroup),
  1068. )
  1069. VLAN.objects.bulk_create(vlans)
  1070. data = (
  1071. {"name": "First VLAN"},
  1072. {"name": "Second VLAN"},
  1073. {"name": "Third VLAN"},
  1074. )
  1075. url = reverse('ipam-api:vlangroup-available-vlans', kwargs={'pk': vlangroup.pk})
  1076. response = self.client.post(url, data, format='json', **self.header)
  1077. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  1078. self.assertEqual(len(response.data), 3)
  1079. self.assertEqual(response.data[0]['name'], data[0]['name'])
  1080. self.assertEqual(response.data[0]['group']['id'], vlangroup.pk)
  1081. self.assertEqual(response.data[0]['vid'], 2)
  1082. self.assertEqual(response.data[1]['name'], data[1]['name'])
  1083. self.assertEqual(response.data[1]['group']['id'], vlangroup.pk)
  1084. self.assertEqual(response.data[1]['vid'], 4)
  1085. self.assertEqual(response.data[2]['name'], data[2]['name'])
  1086. self.assertEqual(response.data[2]['group']['id'], vlangroup.pk)
  1087. self.assertEqual(response.data[2]['vid'], 6)
  1088. class VLANTestCase(APIViewTestCases.APIViewTestCase):
  1089. model = VLAN
  1090. brief_fields = ['description', 'display', 'id', 'name', 'url', 'vid']
  1091. bulk_update_data = {
  1092. 'description': 'New description',
  1093. }
  1094. @classmethod
  1095. def setUpTestData(cls):
  1096. vlan_groups = (
  1097. VLANGroup(name='VLAN Group 1', slug='vlan-group-1'),
  1098. VLANGroup(name='VLAN Group 2', slug='vlan-group-2'),
  1099. )
  1100. VLANGroup.objects.bulk_create(vlan_groups)
  1101. vlans = (
  1102. VLAN(name='VLAN 1', vid=1, group=vlan_groups[0]),
  1103. VLAN(name='VLAN 2', vid=2, group=vlan_groups[0]),
  1104. VLAN(name='VLAN 3', vid=3, group=vlan_groups[0]),
  1105. VLAN(name='SVLAN 1', vid=1001, qinq_role=VLANQinQRoleChoices.ROLE_SERVICE),
  1106. )
  1107. VLAN.objects.bulk_create(vlans)
  1108. cls.create_data = [
  1109. {
  1110. 'vid': 4,
  1111. 'name': 'VLAN 4',
  1112. 'group': vlan_groups[1].pk,
  1113. },
  1114. {
  1115. 'vid': 5,
  1116. 'name': 'VLAN 5',
  1117. 'group': vlan_groups[1].pk,
  1118. },
  1119. {
  1120. 'vid': 6,
  1121. 'name': 'VLAN 6',
  1122. 'group': vlan_groups[1].pk,
  1123. },
  1124. {
  1125. 'vid': 2001,
  1126. 'name': 'CVLAN 1',
  1127. 'qinq_role': VLANQinQRoleChoices.ROLE_CUSTOMER,
  1128. 'qinq_svlan': vlans[3].pk,
  1129. },
  1130. ]
  1131. def test_delete_vlan_with_prefix(self):
  1132. """
  1133. Attempt and fail to delete a VLAN with a Prefix assigned to it.
  1134. """
  1135. vlan = VLAN.objects.first()
  1136. Prefix.objects.create(prefix=IPNetwork('192.0.2.0/24'), vlan=vlan)
  1137. self.add_permissions('ipam.delete_vlan')
  1138. url = reverse('ipam-api:vlan-detail', kwargs={'pk': vlan.pk})
  1139. with disable_logging(level=logging.WARNING):
  1140. response = self.client.delete(url, **self.header)
  1141. self.assertHttpStatus(response, status.HTTP_409_CONFLICT)
  1142. content = json.loads(response.content.decode('utf-8'))
  1143. self.assertIn('detail', content)
  1144. self.assertTrue(content['detail'].startswith('Unable to delete object.'))
  1145. class VLANTranslationPolicyTestCase(APIViewTestCases.APIViewTestCase):
  1146. model = VLANTranslationPolicy
  1147. brief_fields = ['description', 'display', 'id', 'name', 'url',]
  1148. bulk_update_data = {
  1149. 'description': 'New description',
  1150. }
  1151. @classmethod
  1152. def setUpTestData(cls):
  1153. vlan_translation_policies = (
  1154. VLANTranslationPolicy(
  1155. name='Policy 1',
  1156. description='foobar1',
  1157. ),
  1158. VLANTranslationPolicy(
  1159. name='Policy 2',
  1160. description='foobar2',
  1161. ),
  1162. VLANTranslationPolicy(
  1163. name='Policy 3',
  1164. description='foobar3',
  1165. ),
  1166. )
  1167. VLANTranslationPolicy.objects.bulk_create(vlan_translation_policies)
  1168. cls.create_data = [
  1169. {
  1170. 'name': 'Policy 4',
  1171. 'description': 'foobar4',
  1172. },
  1173. {
  1174. 'name': 'Policy 5',
  1175. 'description': 'foobar5',
  1176. },
  1177. {
  1178. 'name': 'Policy 6',
  1179. 'description': 'foobar6',
  1180. },
  1181. ]
  1182. class VLANTranslationRuleTestCase(APIViewTestCases.APIViewTestCase):
  1183. model = VLANTranslationRule
  1184. brief_fields = ['description', 'display', 'id', 'local_vid', 'policy', 'remote_vid', 'url']
  1185. @classmethod
  1186. def setUpTestData(cls):
  1187. vlan_translation_policies = (
  1188. VLANTranslationPolicy(
  1189. name='Policy 1',
  1190. description='foobar1',
  1191. ),
  1192. VLANTranslationPolicy(
  1193. name='Policy 2',
  1194. description='foobar2',
  1195. ),
  1196. VLANTranslationPolicy(
  1197. name='Policy 3',
  1198. description='foobar2',
  1199. ),
  1200. )
  1201. VLANTranslationPolicy.objects.bulk_create(vlan_translation_policies)
  1202. vlan_translation_rules = (
  1203. VLANTranslationRule(
  1204. policy=vlan_translation_policies[0],
  1205. local_vid=100,
  1206. remote_vid=200,
  1207. description='foo',
  1208. ),
  1209. VLANTranslationRule(
  1210. policy=vlan_translation_policies[0],
  1211. local_vid=101,
  1212. remote_vid=201,
  1213. description='bar',
  1214. ),
  1215. VLANTranslationRule(
  1216. policy=vlan_translation_policies[1],
  1217. local_vid=102,
  1218. remote_vid=202,
  1219. description='baz',
  1220. ),
  1221. )
  1222. VLANTranslationRule.objects.bulk_create(vlan_translation_rules)
  1223. cls.create_data = [
  1224. {
  1225. 'policy': vlan_translation_policies[0].pk,
  1226. 'local_vid': 300,
  1227. 'remote_vid': 400,
  1228. },
  1229. {
  1230. 'policy': vlan_translation_policies[0].pk,
  1231. 'local_vid': 301,
  1232. 'remote_vid': 401,
  1233. },
  1234. {
  1235. 'policy': vlan_translation_policies[1].pk,
  1236. 'local_vid': 302,
  1237. 'remote_vid': 402,
  1238. },
  1239. ]
  1240. cls.bulk_update_data = {
  1241. 'policy': vlan_translation_policies[2].pk,
  1242. 'description': 'New description',
  1243. }
  1244. class ServiceTemplateTestCase(APIViewTestCases.APIViewTestCase):
  1245. model = ServiceTemplate
  1246. brief_fields = ['description', 'display', 'id', 'name', 'ports', 'protocol', 'url']
  1247. bulk_update_data = {
  1248. 'description': 'New description',
  1249. }
  1250. graphql_base_name = 'service_template'
  1251. @classmethod
  1252. def setUpTestData(cls):
  1253. service_templates = (
  1254. ServiceTemplate(name='Service Template 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[1, 2]),
  1255. ServiceTemplate(name='Service Template 2', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[3, 4]),
  1256. ServiceTemplate(name='Service Template 3', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[5, 6]),
  1257. )
  1258. ServiceTemplate.objects.bulk_create(service_templates)
  1259. cls.create_data = [
  1260. {
  1261. 'name': 'Service Template 4',
  1262. 'protocol': ServiceProtocolChoices.PROTOCOL_TCP,
  1263. 'ports': [7, 8],
  1264. },
  1265. {
  1266. 'name': 'Service Template 5',
  1267. 'protocol': ServiceProtocolChoices.PROTOCOL_TCP,
  1268. 'ports': [9, 10],
  1269. },
  1270. {
  1271. 'name': 'Service Template 6',
  1272. 'protocol': ServiceProtocolChoices.PROTOCOL_TCP,
  1273. 'ports': [11, 12],
  1274. },
  1275. ]
  1276. class ServiceTestCase(APIViewTestCases.APIViewTestCase):
  1277. model = Service
  1278. brief_fields = ['description', 'display', 'id', 'name', 'ports', 'protocol', 'url']
  1279. bulk_update_data = {
  1280. 'description': 'New description',
  1281. }
  1282. graphql_base_name = 'service'
  1283. @classmethod
  1284. def setUpTestData(cls):
  1285. site = Site.objects.create(name='Site 1', slug='site-1')
  1286. manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
  1287. devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
  1288. role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
  1289. devices = (
  1290. Device(name='Device 1', site=site, device_type=devicetype, role=role),
  1291. Device(name='Device 2', site=site, device_type=devicetype, role=role),
  1292. )
  1293. Device.objects.bulk_create(devices)
  1294. services = (
  1295. Service(parent=devices[0], name='Service 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[1]),
  1296. Service(parent=devices[0], name='Service 2', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[2]),
  1297. Service(parent=devices[0], name='Service 3', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[3]),
  1298. )
  1299. Service.objects.bulk_create(services)
  1300. cls.create_data = [
  1301. {
  1302. 'parent_object_id': devices[1].pk,
  1303. 'parent_object_type': 'dcim.device',
  1304. 'name': 'Service 4',
  1305. 'protocol': ServiceProtocolChoices.PROTOCOL_TCP,
  1306. 'ports': [4],
  1307. },
  1308. {
  1309. 'parent_object_id': devices[1].pk,
  1310. 'parent_object_type': 'dcim.device',
  1311. 'name': 'Service 5',
  1312. 'protocol': ServiceProtocolChoices.PROTOCOL_TCP,
  1313. 'ports': [5],
  1314. },
  1315. {
  1316. 'parent_object_id': devices[1].pk,
  1317. 'parent_object_type': 'dcim.device',
  1318. 'name': 'Service 6',
  1319. 'protocol': ServiceProtocolChoices.PROTOCOL_TCP,
  1320. 'ports': [6],
  1321. },
  1322. ]