test_models.py 102 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696
  1. from django.core.exceptions import ValidationError
  2. from django.db.models.signals import post_save
  3. from django.test import TestCase, tag
  4. from circuits.models import *
  5. from core.models import ObjectType
  6. from dcim.choices import *
  7. from dcim.models import *
  8. from extras.events import serialize_for_event
  9. from extras.models import CustomField
  10. from ipam.models import Prefix
  11. from netbox.choices import WeightUnitChoices
  12. from tenancy.models import Tenant
  13. from utilities.data import drange
  14. from virtualization.models import Cluster, ClusterType
  15. class MACAddressTestCase(TestCase):
  16. @classmethod
  17. def setUpTestData(cls):
  18. site = Site.objects.create(name='Test Site 1', slug='test-site-1')
  19. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  20. device_type = DeviceType.objects.create(
  21. manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
  22. )
  23. device_role = DeviceRole.objects.create(name='Test Role 1', slug='test-role-1')
  24. device = Device.objects.create(
  25. name='Device 1', device_type=device_type, role=device_role, site=site,
  26. )
  27. cls.interface = Interface.objects.create(
  28. device=device,
  29. name='Interface 1',
  30. type=InterfaceTypeChoices.TYPE_1GE_FIXED,
  31. mgmt_only=True
  32. )
  33. cls.mac_a = MACAddress.objects.create(mac_address='1234567890ab', assigned_object=cls.interface)
  34. cls.mac_b = MACAddress.objects.create(mac_address='1234567890ba', assigned_object=cls.interface)
  35. cls.interface.primary_mac_address = cls.mac_a
  36. cls.interface.save()
  37. @tag('regression')
  38. def test_clean_will_not_allow_removal_of_assigned_object_if_primary(self):
  39. self.mac_a.assigned_object = None
  40. with self.assertRaisesMessage(ValidationError, 'Cannot unassign MAC Address while'):
  41. self.mac_a.clean()
  42. @tag('regression')
  43. def test_clean_will_allow_removal_of_assigned_object_if_not_primary(self):
  44. self.mac_b.assigned_object = None
  45. self.mac_b.clean()
  46. class LocationTestCase(TestCase):
  47. def test_change_location_site(self):
  48. """
  49. Check that all child Locations and Racks get updated when a Location is moved to a new Site. Topology:
  50. Site A
  51. - Location A1
  52. - Location A2
  53. - Rack 2
  54. - Device 2
  55. - Rack 1
  56. - Device 1
  57. """
  58. manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
  59. device_type = DeviceType.objects.create(
  60. manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
  61. )
  62. role = DeviceRole.objects.create(
  63. name='Device Role 1', slug='device-role-1', color='ff0000'
  64. )
  65. site_a = Site.objects.create(name='Site A', slug='site-a')
  66. site_b = Site.objects.create(name='Site B', slug='site-b')
  67. location_a1 = Location(site=site_a, name='Location A1', slug='location-a1')
  68. location_a1.save()
  69. location_a2 = Location(site=site_a, parent=location_a1, name='Location A2', slug='location-a2')
  70. location_a2.save()
  71. rack1 = Rack.objects.create(site=site_a, location=location_a1, name='Rack 1')
  72. rack2 = Rack.objects.create(site=site_a, location=location_a2, name='Rack 2')
  73. device1 = Device.objects.create(
  74. site=site_a,
  75. location=location_a1,
  76. name='Device 1',
  77. device_type=device_type,
  78. role=role
  79. )
  80. device2 = Device.objects.create(
  81. site=site_a,
  82. location=location_a2,
  83. name='Device 2',
  84. device_type=device_type,
  85. role=role
  86. )
  87. powerpanel1 = PowerPanel.objects.create(site=site_a, location=location_a1, name='Power Panel 1')
  88. # Move Location A1 to Site B
  89. location_a1.site = site_b
  90. location_a1.save()
  91. # Check that all objects within Location A1 now belong to Site B
  92. self.assertEqual(Location.objects.get(pk=location_a1.pk).site, site_b)
  93. self.assertEqual(Location.objects.get(pk=location_a2.pk).site, site_b)
  94. self.assertEqual(Rack.objects.get(pk=rack1.pk).site, site_b)
  95. self.assertEqual(Rack.objects.get(pk=rack2.pk).site, site_b)
  96. self.assertEqual(Device.objects.get(pk=device1.pk).site, site_b)
  97. self.assertEqual(Device.objects.get(pk=device2.pk).site, site_b)
  98. self.assertEqual(PowerPanel.objects.get(pk=powerpanel1.pk).site, site_b)
  99. class DeviceTypeTestCase(TestCase):
  100. def test_component_template_counts(self):
  101. """
  102. DeviceType component template counters should track the addition and removal of templates.
  103. """
  104. manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
  105. device_type = DeviceType.objects.create(
  106. manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
  107. )
  108. # Counters should start at zero
  109. self.assertEqual(device_type.interface_template_count, 0)
  110. self.assertEqual(device_type.console_port_template_count, 0)
  111. self.assertEqual(device_type.module_bay_template_count, 0)
  112. self.assertEqual(device_type.device_bay_template_count, 0)
  113. # Adding templates should increment the relevant counters
  114. InterfaceTemplate.objects.create(device_type=device_type, name='Interface 1')
  115. InterfaceTemplate.objects.create(device_type=device_type, name='Interface 2')
  116. ConsolePortTemplate.objects.create(device_type=device_type, name='Console 1')
  117. ModuleBayTemplate.objects.create(device_type=device_type, name='Module Bay 1')
  118. DeviceBayTemplate.objects.create(device_type=device_type, name='Device Bay 1')
  119. device_type.refresh_from_db()
  120. self.assertEqual(device_type.interface_template_count, 2)
  121. self.assertEqual(device_type.console_port_template_count, 1)
  122. self.assertEqual(device_type.module_bay_template_count, 1)
  123. self.assertEqual(device_type.device_bay_template_count, 1)
  124. # Deleting a template should decrement the counter
  125. InterfaceTemplate.objects.get(device_type=device_type, name='Interface 1').delete()
  126. device_type.refresh_from_db()
  127. self.assertEqual(device_type.interface_template_count, 1)
  128. class ModuleTypeTestCase(TestCase):
  129. def test_component_template_counts(self):
  130. """
  131. ModuleType component template counters should track the addition and removal of templates.
  132. """
  133. manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
  134. module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
  135. # Counters should start at zero
  136. self.assertEqual(module_type.interface_template_count, 0)
  137. self.assertEqual(module_type.console_port_template_count, 0)
  138. self.assertEqual(module_type.module_bay_template_count, 0)
  139. # Adding templates should increment the relevant counters
  140. InterfaceTemplate.objects.create(module_type=module_type, name='Interface 1')
  141. InterfaceTemplate.objects.create(module_type=module_type, name='Interface 2')
  142. ConsolePortTemplate.objects.create(module_type=module_type, name='Console 1')
  143. ModuleBayTemplate.objects.create(module_type=module_type, name='Module Bay 1')
  144. module_type.refresh_from_db()
  145. self.assertEqual(module_type.interface_template_count, 2)
  146. self.assertEqual(module_type.console_port_template_count, 1)
  147. self.assertEqual(module_type.module_bay_template_count, 1)
  148. # Deleting a template should decrement the counter
  149. InterfaceTemplate.objects.get(module_type=module_type, name='Interface 1').delete()
  150. module_type.refresh_from_db()
  151. self.assertEqual(module_type.interface_template_count, 1)
  152. class RackTypeTestCase(TestCase):
  153. @classmethod
  154. def setUpTestData(cls):
  155. manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
  156. RackType.objects.create(
  157. manufacturer=manufacturer,
  158. model='RackType 1',
  159. slug='rack-type-1',
  160. width=11,
  161. u_height=22,
  162. starting_unit=3,
  163. desc_units=True,
  164. outer_width=444,
  165. outer_depth=5,
  166. outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER,
  167. weight=66,
  168. weight_unit=WeightUnitChoices.UNIT_POUND,
  169. max_weight=7777,
  170. mounting_depth=8,
  171. )
  172. def test_rack_creation(self):
  173. rack_type = RackType.objects.first()
  174. sites = (
  175. Site(name='Site 1', slug='site-1'),
  176. )
  177. Site.objects.bulk_create(sites)
  178. locations = (
  179. Location(name='Location 1', slug='location-1', site=sites[0]),
  180. )
  181. for location in locations:
  182. location.save()
  183. rack = Rack.objects.create(
  184. name='Rack 1',
  185. facility_id='A101',
  186. site=sites[0],
  187. location=locations[0],
  188. rack_type=rack_type
  189. )
  190. self.assertEqual(rack.width, rack_type.width)
  191. self.assertEqual(rack.u_height, rack_type.u_height)
  192. self.assertEqual(rack.starting_unit, rack_type.starting_unit)
  193. self.assertEqual(rack.desc_units, rack_type.desc_units)
  194. self.assertEqual(rack.outer_width, rack_type.outer_width)
  195. self.assertEqual(rack.outer_depth, rack_type.outer_depth)
  196. self.assertEqual(rack.outer_unit, rack_type.outer_unit)
  197. self.assertEqual(rack.weight, rack_type.weight)
  198. self.assertEqual(rack.weight_unit, rack_type.weight_unit)
  199. self.assertEqual(rack.max_weight, rack_type.max_weight)
  200. self.assertEqual(rack.mounting_depth, rack_type.mounting_depth)
  201. class RackTestCase(TestCase):
  202. @classmethod
  203. def setUpTestData(cls):
  204. sites = (
  205. Site(name='Site 1', slug='site-1'),
  206. Site(name='Site 2', slug='site-2'),
  207. )
  208. Site.objects.bulk_create(sites)
  209. locations = (
  210. Location(name='Location 1', slug='location-1', site=sites[0]),
  211. Location(name='Location 2', slug='location-2', site=sites[1]),
  212. )
  213. for location in locations:
  214. location.save()
  215. Rack.objects.create(
  216. name='Rack 1',
  217. facility_id='A101',
  218. site=sites[0],
  219. location=locations[0],
  220. u_height=42
  221. )
  222. manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
  223. device_types = (
  224. DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', u_height=1),
  225. DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2', u_height=0),
  226. DeviceType(manufacturer=manufacturer, model='Device Type 3', slug='device-type-3', u_height=0.5),
  227. )
  228. DeviceType.objects.bulk_create(device_types)
  229. DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
  230. def test_rack_device_outside_height(self):
  231. site = Site.objects.first()
  232. rack = Rack.objects.first()
  233. device1 = Device(
  234. name='Device 1',
  235. device_type=DeviceType.objects.first(),
  236. role=DeviceRole.objects.first(),
  237. site=site,
  238. rack=rack,
  239. position=43,
  240. face=DeviceFaceChoices.FACE_FRONT,
  241. )
  242. device1.save()
  243. with self.assertRaises(ValidationError):
  244. rack.clean()
  245. def test_location_site(self):
  246. site1 = Site.objects.get(name='Site 1')
  247. location2 = Location.objects.get(name='Location 2')
  248. rack2 = Rack(
  249. name='Rack 2',
  250. site=site1,
  251. location=location2,
  252. u_height=42
  253. )
  254. rack2.save()
  255. with self.assertRaises(ValidationError):
  256. rack2.clean()
  257. def test_mount_single_device(self):
  258. site = Site.objects.first()
  259. rack = Rack.objects.first()
  260. device1 = Device(
  261. name='TestSwitch1',
  262. device_type=DeviceType.objects.first(),
  263. role=DeviceRole.objects.first(),
  264. site=site,
  265. rack=rack,
  266. position=10.0,
  267. face=DeviceFaceChoices.FACE_REAR,
  268. )
  269. device1.save()
  270. # Validate rack height
  271. self.assertEqual(list(rack.units), list(drange(42.5, 0.5, -0.5)))
  272. # Validate inventory (front face)
  273. rack1_inventory_front = {
  274. u['id']: u for u in rack.get_rack_units(face=DeviceFaceChoices.FACE_FRONT)
  275. }
  276. self.assertEqual(rack1_inventory_front[10.0]['device'], device1)
  277. self.assertEqual(rack1_inventory_front[10.5]['device'], device1)
  278. del rack1_inventory_front[10.0]
  279. del rack1_inventory_front[10.5]
  280. for u in rack1_inventory_front.values():
  281. self.assertIsNone(u['device'])
  282. # Validate inventory (rear face)
  283. rack1_inventory_rear = {
  284. u['id']: u for u in rack.get_rack_units(face=DeviceFaceChoices.FACE_REAR)
  285. }
  286. self.assertEqual(rack1_inventory_rear[10.0]['device'], device1)
  287. self.assertEqual(rack1_inventory_rear[10.5]['device'], device1)
  288. del rack1_inventory_rear[10.0]
  289. del rack1_inventory_rear[10.5]
  290. for u in rack1_inventory_rear.values():
  291. self.assertIsNone(u['device'])
  292. def test_mount_zero_ru(self):
  293. """
  294. Check that a 0RU device can be mounted in a rack with no face/position.
  295. """
  296. site = Site.objects.first()
  297. rack = Rack.objects.first()
  298. Device(
  299. name='Device 1',
  300. role=DeviceRole.objects.first(),
  301. device_type=DeviceType.objects.first(),
  302. site=site,
  303. rack=rack
  304. ).save()
  305. def test_mount_half_u_devices(self):
  306. """
  307. Check that two 0.5U devices can be mounted in the same rack unit.
  308. """
  309. rack = Rack.objects.first()
  310. attrs = {
  311. 'device_type': DeviceType.objects.get(u_height=0.5),
  312. 'role': DeviceRole.objects.first(),
  313. 'site': Site.objects.first(),
  314. 'rack': rack,
  315. 'face': DeviceFaceChoices.FACE_FRONT,
  316. }
  317. Device(name='Device 1', position=1, **attrs).save()
  318. Device(name='Device 2', position=1.5, **attrs).save()
  319. self.assertEqual(len(rack.get_available_units()), rack.u_height * 2 - 3)
  320. def test_change_rack_site(self):
  321. """
  322. Check that child Devices get updated when a Rack is moved to a new Site.
  323. """
  324. site_a = Site.objects.create(name='Site A', slug='site-a')
  325. site_b = Site.objects.create(name='Site B', slug='site-b')
  326. # Create Rack1 in Site A
  327. rack1 = Rack.objects.create(site=site_a, name='Rack 1')
  328. # Create Device1 in Rack1
  329. device1 = Device.objects.create(
  330. site=site_a,
  331. rack=rack1,
  332. device_type=DeviceType.objects.first(),
  333. role=DeviceRole.objects.first()
  334. )
  335. # Move Rack1 to Site B
  336. rack1.site = site_b
  337. rack1.save()
  338. # Check that Device1 is now assigned to Site B
  339. self.assertEqual(Device.objects.get(pk=device1.pk).site, site_b)
  340. def test_utilization(self):
  341. site = Site.objects.first()
  342. rack = Rack.objects.first()
  343. Device(
  344. name='Device 1',
  345. role=DeviceRole.objects.first(),
  346. device_type=DeviceType.objects.first(),
  347. site=site,
  348. rack=rack,
  349. position=1
  350. ).save()
  351. rack.refresh_from_db()
  352. self.assertEqual(rack.get_utilization(), 1 / 42 * 100)
  353. # create device excluded from utilization calculations
  354. dt = DeviceType.objects.create(
  355. manufacturer=Manufacturer.objects.first(),
  356. model='Device Type 4',
  357. slug='device-type-4',
  358. u_height=1,
  359. exclude_from_utilization=True
  360. )
  361. Device(
  362. name='Device 2',
  363. role=DeviceRole.objects.first(),
  364. device_type=dt,
  365. site=site,
  366. rack=rack,
  367. position=5
  368. ).save()
  369. rack.refresh_from_db()
  370. self.assertEqual(rack.get_utilization(), 1 / 42 * 100)
  371. class DeviceTestCase(TestCase):
  372. @classmethod
  373. def setUpTestData(cls):
  374. Site.objects.create(name='Test Site 1', slug='test-site-1')
  375. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  376. device_type = DeviceType.objects.create(
  377. manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
  378. )
  379. roles = (
  380. DeviceRole(name='Test Role 1', slug='test-role-1'),
  381. DeviceRole(name='Test Role 2', slug='test-role-2'),
  382. )
  383. for role in roles:
  384. role.save()
  385. # Create a CustomField with a default value & assign it to all component models
  386. cf1 = CustomField.objects.create(name='cf1', default='foo')
  387. cf1.object_types.set(
  388. ObjectType.objects.filter(app_label='dcim', model__in=[
  389. 'consoleport',
  390. 'consoleserverport',
  391. 'powerport',
  392. 'poweroutlet',
  393. 'interface',
  394. 'rearport',
  395. 'frontport',
  396. 'modulebay',
  397. 'devicebay',
  398. 'inventoryitem',
  399. ])
  400. )
  401. # Create DeviceType components
  402. ConsolePortTemplate(
  403. device_type=device_type,
  404. name='Console Port 1'
  405. ).save()
  406. ConsoleServerPortTemplate(
  407. device_type=device_type,
  408. name='Console Server Port 1'
  409. ).save()
  410. powerport = PowerPortTemplate(
  411. device_type=device_type,
  412. name='Power Port 1',
  413. maximum_draw=1000,
  414. allocated_draw=500
  415. )
  416. powerport.save()
  417. PowerOutletTemplate(
  418. device_type=device_type,
  419. name='Power Outlet 1',
  420. power_port=powerport,
  421. feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A
  422. ).save()
  423. InterfaceTemplate(
  424. device_type=device_type,
  425. name='Interface 1',
  426. type=InterfaceTypeChoices.TYPE_1GE_FIXED,
  427. mgmt_only=True
  428. ).save()
  429. rearport = RearPortTemplate(
  430. device_type=device_type,
  431. name='Rear Port 1',
  432. type=PortTypeChoices.TYPE_8P8C,
  433. positions=8
  434. )
  435. rearport.save()
  436. frontport = FrontPortTemplate(
  437. device_type=device_type,
  438. name='Front Port 1',
  439. type=PortTypeChoices.TYPE_8P8C,
  440. )
  441. frontport.save()
  442. PortTemplateMapping.objects.create(
  443. device_type=device_type,
  444. front_port=frontport,
  445. rear_port=rearport,
  446. rear_port_position=2,
  447. )
  448. ModuleBayTemplate(
  449. device_type=device_type,
  450. name='Module Bay 1'
  451. ).save()
  452. DeviceBayTemplate(
  453. device_type=device_type,
  454. name='Device Bay 1'
  455. ).save()
  456. InventoryItemTemplate(
  457. device_type=device_type,
  458. name='Inventory Item 1'
  459. ).save()
  460. def test_device_creation(self):
  461. """
  462. Ensure that all Device components are copied automatically from the DeviceType.
  463. """
  464. device = Device(
  465. site=Site.objects.first(),
  466. device_type=DeviceType.objects.first(),
  467. role=DeviceRole.objects.first(),
  468. name='Test Device 1'
  469. )
  470. device.save()
  471. consoleport = ConsolePort.objects.get(
  472. device=device,
  473. name='Console Port 1'
  474. )
  475. self.assertEqual(consoleport.cf['cf1'], 'foo')
  476. consoleserverport = ConsoleServerPort.objects.get(
  477. device=device,
  478. name='Console Server Port 1'
  479. )
  480. self.assertEqual(consoleserverport.cf['cf1'], 'foo')
  481. powerport = PowerPort.objects.get(
  482. device=device,
  483. name='Power Port 1',
  484. maximum_draw=1000,
  485. allocated_draw=500
  486. )
  487. self.assertEqual(powerport.cf['cf1'], 'foo')
  488. poweroutlet = PowerOutlet.objects.get(
  489. device=device,
  490. name='Power Outlet 1',
  491. power_port=powerport,
  492. feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A,
  493. status=PowerOutletStatusChoices.STATUS_ENABLED,
  494. )
  495. self.assertEqual(poweroutlet.cf['cf1'], 'foo')
  496. interface = Interface.objects.get(
  497. device=device,
  498. name='Interface 1',
  499. type=InterfaceTypeChoices.TYPE_1GE_FIXED,
  500. mgmt_only=True
  501. )
  502. self.assertEqual(interface.cf['cf1'], 'foo')
  503. rearport = RearPort.objects.get(
  504. device=device,
  505. name='Rear Port 1',
  506. type=PortTypeChoices.TYPE_8P8C,
  507. positions=8
  508. )
  509. self.assertEqual(rearport.cf['cf1'], 'foo')
  510. frontport = FrontPort.objects.get(
  511. device=device,
  512. name='Front Port 1',
  513. type=PortTypeChoices.TYPE_8P8C,
  514. positions=1
  515. )
  516. self.assertEqual(frontport.cf['cf1'], 'foo')
  517. self.assertTrue(PortMapping.objects.filter(front_port=frontport, rear_port=rearport).exists())
  518. modulebay = ModuleBay.objects.get(
  519. device=device,
  520. name='Module Bay 1'
  521. )
  522. self.assertEqual(modulebay.cf['cf1'], 'foo')
  523. devicebay = DeviceBay.objects.get(
  524. device=device,
  525. name='Device Bay 1'
  526. )
  527. self.assertEqual(devicebay.cf['cf1'], 'foo')
  528. inventoryitem = InventoryItem.objects.get(
  529. device=device,
  530. name='Inventory Item 1'
  531. )
  532. self.assertEqual(inventoryitem.cf['cf1'], 'foo')
  533. def test_multiple_unnamed_devices(self):
  534. device1 = Device(
  535. site=Site.objects.first(),
  536. device_type=DeviceType.objects.first(),
  537. role=DeviceRole.objects.first(),
  538. name=None
  539. )
  540. device1.save()
  541. device2 = Device(
  542. site=device1.site,
  543. device_type=device1.device_type,
  544. role=device1.role,
  545. name=None
  546. )
  547. device2.full_clean()
  548. device2.save()
  549. self.assertEqual(Device.objects.filter(name__isnull=True).count(), 2)
  550. def test_device_name_case_sensitivity(self):
  551. device1 = Device(
  552. site=Site.objects.first(),
  553. device_type=DeviceType.objects.first(),
  554. role=DeviceRole.objects.first(),
  555. name='device 1'
  556. )
  557. device1.save()
  558. device2 = Device(
  559. site=device1.site,
  560. device_type=device1.device_type,
  561. role=device1.role,
  562. name='DEVICE 1'
  563. )
  564. # Uniqueness validation for name should ignore case
  565. with self.assertRaises(ValidationError):
  566. device2.full_clean()
  567. def test_device_duplicate_names(self):
  568. device1 = Device(
  569. site=Site.objects.first(),
  570. device_type=DeviceType.objects.first(),
  571. role=DeviceRole.objects.first(),
  572. name='Test Device 1'
  573. )
  574. device1.save()
  575. device2 = Device(
  576. site=device1.site,
  577. device_type=device1.device_type,
  578. role=device1.role,
  579. name=device1.name
  580. )
  581. # Two devices assigned to the same Site and no Tenant should fail validation
  582. with self.assertRaises(ValidationError):
  583. device2.full_clean()
  584. tenant = Tenant.objects.create(name='Test Tenant 1', slug='test-tenant-1')
  585. device1.tenant = tenant
  586. device1.save()
  587. device2.tenant = tenant
  588. # Two devices assigned to the same Site and the same Tenant should fail validation
  589. with self.assertRaises(ValidationError):
  590. device2.full_clean()
  591. device2.tenant = None
  592. # Two devices assigned to the same Site and different Tenants should pass validation
  593. device2.full_clean()
  594. device2.save()
  595. def test_empty_asset_tag_coerced_to_null_on_clean(self):
  596. """
  597. An empty string assigned to a unique nullable CharField (e.g. asset_tag) must be coerced
  598. to None on save so that multiple objects can be saved without violating the unique
  599. constraint. Test that this is done on clean().
  600. """
  601. common_kwargs = {
  602. 'site': Site.objects.first(),
  603. 'device_type': DeviceType.objects.first(),
  604. 'role': DeviceRole.objects.first(),
  605. }
  606. device1 = Device(name='Device 1', asset_tag='', **common_kwargs)
  607. device1.clean()
  608. self.assertIsNone(device1.asset_tag)
  609. def test_empty_asset_tag_coerced_to_null_on_save(self):
  610. """
  611. An empty string assigned to a unique nullable CharField (e.g. asset_tag) must be coerced
  612. to None on save so that multiple objects can be saved without violating the unique
  613. constraint. Test that this is done on save().
  614. """
  615. common_kwargs = {
  616. 'site': Site.objects.first(),
  617. 'device_type': DeviceType.objects.first(),
  618. 'role': DeviceRole.objects.first(),
  619. }
  620. device1 = Device(name='Device 1', asset_tag='', **common_kwargs)
  621. device1.save()
  622. device2 = Device(name='Device 2', asset_tag='', **common_kwargs)
  623. device2.save()
  624. device1.refresh_from_db()
  625. device2.refresh_from_db()
  626. self.assertIsNone(device1.asset_tag)
  627. self.assertIsNone(device2.asset_tag)
  628. def test_device_label(self):
  629. device1 = Device(
  630. site=Site.objects.first(),
  631. device_type=DeviceType.objects.first(),
  632. role=DeviceRole.objects.first(),
  633. name=None,
  634. )
  635. self.assertEqual(device1.label, None)
  636. device1.name = 'Test Device 1'
  637. self.assertEqual(device1.label, 'Test Device 1')
  638. virtual_chassis = VirtualChassis.objects.create(name='VC 1')
  639. device2 = Device(
  640. site=Site.objects.first(),
  641. device_type=DeviceType.objects.first(),
  642. role=DeviceRole.objects.first(),
  643. name=None,
  644. virtual_chassis=virtual_chassis,
  645. vc_position=2,
  646. )
  647. self.assertEqual(device2.label, 'VC 1:2')
  648. device2.name = 'Test Device 2'
  649. self.assertEqual(device2.label, 'Test Device 2')
  650. def test_device_mismatched_site_cluster(self):
  651. cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
  652. Cluster.objects.create(name='Cluster 1', type=cluster_type)
  653. sites = (
  654. Site(name='Site 1', slug='site-1'),
  655. Site(name='Site 2', slug='site-2'),
  656. )
  657. Site.objects.bulk_create(sites)
  658. clusters = (
  659. Cluster(name='Cluster 1', type=cluster_type, scope=sites[0]),
  660. Cluster(name='Cluster 2', type=cluster_type, scope=sites[1]),
  661. Cluster(name='Cluster 3', type=cluster_type, scope=None),
  662. )
  663. for cluster in clusters:
  664. cluster.save()
  665. device_type = DeviceType.objects.first()
  666. device_role = DeviceRole.objects.first()
  667. # Device with site only should pass
  668. Device(
  669. name='device1',
  670. site=sites[0],
  671. device_type=device_type,
  672. role=device_role
  673. ).full_clean()
  674. # Device with site, cluster non-site should pass
  675. Device(
  676. name='device1',
  677. site=sites[0],
  678. device_type=device_type,
  679. role=device_role,
  680. cluster=clusters[2]
  681. ).full_clean()
  682. # Device with mismatched site & cluster should fail
  683. with self.assertRaises(ValidationError):
  684. Device(
  685. name='device1',
  686. site=sites[0],
  687. device_type=device_type,
  688. role=device_role,
  689. cluster=clusters[1]
  690. ).full_clean()
  691. class DeviceBayTestCase(TestCase):
  692. @classmethod
  693. def setUpTestData(cls):
  694. site = Site.objects.create(name='Test Site 1', slug='test-site-1')
  695. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  696. # Parent device type must support device bays (is_parent_device=True)
  697. parent_device_type = DeviceType.objects.create(
  698. manufacturer=manufacturer,
  699. model='Parent Device Type',
  700. slug='parent-device-type',
  701. subdevice_role=SubdeviceRoleChoices.ROLE_PARENT
  702. )
  703. # Child device type for installation
  704. child_device_type = DeviceType.objects.create(
  705. manufacturer=manufacturer,
  706. model='Child Device Type',
  707. slug='child-device-type',
  708. u_height=0,
  709. subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
  710. )
  711. device_role = DeviceRole.objects.create(name='Test Role 1', slug='test-role-1')
  712. cls.parent_device = Device.objects.create(
  713. name='Parent Device',
  714. device_type=parent_device_type,
  715. role=device_role,
  716. site=site
  717. )
  718. cls.child_device = Device.objects.create(
  719. name='Child Device',
  720. device_type=child_device_type,
  721. role=device_role,
  722. site=site
  723. )
  724. cls.child_device_2 = Device.objects.create(
  725. name='Child Device 2',
  726. device_type=child_device_type,
  727. role=device_role,
  728. site=site
  729. )
  730. def test_cannot_install_device_in_disabled_bay(self):
  731. """
  732. Test that a device cannot be installed into a disabled DeviceBay.
  733. """
  734. # Create a disabled device bay with a device being installed
  735. device_bay = DeviceBay(
  736. device=self.parent_device,
  737. name='Disabled Bay',
  738. enabled=False,
  739. installed_device=self.child_device
  740. )
  741. with self.assertRaises(ValidationError) as cm:
  742. device_bay.clean()
  743. self.assertIn('installed_device', cm.exception.message_dict)
  744. self.assertIn('disabled device bay', str(cm.exception.message_dict['installed_device']))
  745. def test_can_disable_bay_with_existing_device(self):
  746. """
  747. Test that disabling a bay that already has a device installed does NOT raise an error
  748. (same installed_device_id).
  749. """
  750. # First, create an enabled device bay with a device installed
  751. device_bay = DeviceBay.objects.create(
  752. device=self.parent_device,
  753. name='Bay To Disable',
  754. enabled=True,
  755. installed_device=self.child_device
  756. )
  757. # Now disable the bay while keeping the same installed device
  758. device_bay.enabled = False
  759. # This should NOT raise a ValidationError
  760. device_bay.clean()
  761. device_bay.save()
  762. device_bay.refresh_from_db()
  763. self.assertFalse(device_bay.enabled)
  764. self.assertEqual(device_bay.installed_device, self.child_device)
  765. def test_cannot_change_installed_device_in_disabled_bay(self):
  766. """
  767. Test that changing the installed device in a disabled bay raises a ValidationError.
  768. """
  769. # Create an enabled device bay with a device installed
  770. device_bay = DeviceBay.objects.create(
  771. device=self.parent_device,
  772. name='Bay With Device',
  773. enabled=True,
  774. installed_device=self.child_device
  775. )
  776. # Disable the bay and try to change the installed device
  777. device_bay.enabled = False
  778. device_bay.installed_device = self.child_device_2
  779. with self.assertRaises(ValidationError) as cm:
  780. device_bay.clean()
  781. self.assertIn('installed_device', cm.exception.message_dict)
  782. class ModuleBayTestCase(TestCase):
  783. @classmethod
  784. def setUpTestData(cls):
  785. site = Site.objects.create(name='Test Site 1', slug='test-site-1')
  786. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  787. device_type = DeviceType.objects.create(
  788. manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
  789. )
  790. device_role = DeviceRole.objects.create(name='Test Role 1', slug='test-role-1')
  791. # Create a CustomField with a default value & assign it to all component models
  792. location = Location.objects.create(name='Location 1', slug='location-1', site=site)
  793. rack = Rack.objects.create(name='Rack 1', site=site)
  794. device = Device.objects.create(
  795. name='Device 1', device_type=device_type, role=device_role, site=site, location=location, rack=rack
  796. )
  797. module_bays = (
  798. ModuleBay(device=device, name='Module Bay 1', label='A', description='First'),
  799. ModuleBay(device=device, name='Module Bay 2', label='B', description='Second'),
  800. ModuleBay(device=device, name='Module Bay 3', label='C', description='Third'),
  801. )
  802. for module_bay in module_bays:
  803. module_bay.save()
  804. manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
  805. module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
  806. modules = (
  807. Module(device=device, module_bay=module_bays[0], module_type=module_type),
  808. Module(device=device, module_bay=module_bays[1], module_type=module_type),
  809. Module(device=device, module_bay=module_bays[2], module_type=module_type),
  810. )
  811. # M3 -> MB3 -> M2 -> MB2 -> M1 -> MB1
  812. Module.objects.bulk_create(modules)
  813. module_bays[1].module = modules[0]
  814. module_bays[1].clean()
  815. module_bays[1].save()
  816. module_bays[2].module = modules[1]
  817. module_bays[2].clean()
  818. module_bays[2].save()
  819. def test_module_bay_recursion(self):
  820. module_bay_1 = ModuleBay.objects.get(name='Module Bay 1')
  821. module_bay_3 = ModuleBay.objects.get(name='Module Bay 3')
  822. module_1 = Module.objects.get(module_bay=module_bay_1)
  823. module_3 = Module.objects.get(module_bay=module_bay_3)
  824. # Confirm error if ModuleBay recurses
  825. with self.assertRaises(ValidationError):
  826. module_bay_1.module = module_3
  827. module_bay_1.clean()
  828. module_bay_1.save()
  829. # Confirm error if Module recurses
  830. with self.assertRaises(ValidationError):
  831. module_1.module_bay = module_bay_3
  832. module_1.clean()
  833. module_1.save()
  834. @tag('regression') # #22146
  835. def test_module_bay_ordering_after_recreate(self):
  836. """
  837. Module bays must remain in name order after a delete-and-recreate cycle,
  838. even though MPTT no longer renumbers tree_ids on root insertion.
  839. """
  840. device_type = DeviceType.objects.first()
  841. device_role = DeviceRole.objects.first()
  842. site = Site.objects.first()
  843. device = Device.objects.create(
  844. name='Ordering Test Device',
  845. device_type=device_type,
  846. role=device_role,
  847. site=site,
  848. )
  849. for name in ('Bay 1', 'Bay 2', 'Bay 3', 'Bay 4'):
  850. ModuleBay.objects.create(device=device, name=name)
  851. ModuleBay.objects.get(device=device, name='Bay 3').delete()
  852. ModuleBay.objects.create(device=device, name='Bay 3')
  853. names = list(ModuleBay.objects.filter(device=device).values_list('name', flat=True))
  854. self.assertEqual(names, ['Bay 1', 'Bay 2', 'Bay 3', 'Bay 4'])
  855. @tag('regression') # #22146
  856. def test_module_bay_natural_ordering(self):
  857. """
  858. Module bays must be returned in natural (numeric-aware) order, e.g.
  859. "Bay 2" before "Bay 10".
  860. """
  861. device_type = DeviceType.objects.first()
  862. device_role = DeviceRole.objects.first()
  863. site = Site.objects.first()
  864. device = Device.objects.create(
  865. name='Natural Sort Device',
  866. device_type=device_type,
  867. role=device_role,
  868. site=site,
  869. )
  870. # Insert in non-natural order to confirm sort is not insertion-driven.
  871. for name in ('Bay 10', 'Bay 1', 'Bay 2', 'Bay 11'):
  872. ModuleBay.objects.create(device=device, name=name)
  873. names = list(ModuleBay.objects.filter(device=device).values_list('name', flat=True))
  874. self.assertEqual(names, ['Bay 1', 'Bay 2', 'Bay 10', 'Bay 11'])
  875. @tag('regression') # #22146
  876. def test_child_module_bay_ordering(self):
  877. """
  878. Child module bays inside a module must be returned in name order even
  879. when inserted out of order.
  880. """
  881. device_type = DeviceType.objects.first()
  882. device_role = DeviceRole.objects.first()
  883. site = Site.objects.first()
  884. device = Device.objects.create(
  885. name='Child Ordering Device',
  886. device_type=device_type,
  887. role=device_role,
  888. site=site,
  889. )
  890. root_bay = ModuleBay.objects.create(device=device, name='Bay 1')
  891. manufacturer = Manufacturer.objects.first()
  892. module_type = ModuleType.objects.create(
  893. manufacturer=manufacturer, model='Child Ordering Type'
  894. )
  895. module = Module.objects.create(
  896. device=device, module_bay=root_bay, module_type=module_type
  897. )
  898. # Insert children out of name order.
  899. for name in ('Bay 1.1', 'Bay 1.3', 'Bay 1.2'):
  900. ModuleBay.objects.create(device=device, module=module, name=name)
  901. names = list(ModuleBay.objects.filter(device=device).values_list('name', flat=True))
  902. self.assertEqual(names, ['Bay 1', 'Bay 1.1', 'Bay 1.2', 'Bay 1.3'])
  903. @tag('regression') # #22146
  904. def test_root_module_bay_rename_preserves_tree_ids(self):
  905. """
  906. Renaming a root module bay must not renumber any other root tree's
  907. tree_id. The renamed bay's own tree_id is also expected to remain
  908. stable, but the load-bearing assertion is that the *other* bays are
  909. not shifted.
  910. """
  911. device_type = DeviceType.objects.first()
  912. device_role = DeviceRole.objects.first()
  913. site = Site.objects.first()
  914. device = Device.objects.create(
  915. name='Rename TreeID Device',
  916. device_type=device_type,
  917. role=device_role,
  918. site=site,
  919. )
  920. for name in ('Bay 1', 'Bay 2', 'Bay 3', 'Bay 4'):
  921. ModuleBay.objects.create(device=device, name=name)
  922. tree_ids_before = {
  923. bay.name: bay.tree_id
  924. for bay in ModuleBay.objects.filter(device=device)
  925. }
  926. bay = ModuleBay.objects.get(device=device, name='Bay 2')
  927. bay.name = 'Bay 99'
  928. bay.save()
  929. tree_ids_after = {
  930. bay.name: bay.tree_id
  931. for bay in ModuleBay.objects.filter(device=device)
  932. }
  933. for name in ('Bay 1', 'Bay 3', 'Bay 4'):
  934. self.assertEqual(tree_ids_after[name], tree_ids_before[name])
  935. self.assertEqual(tree_ids_after['Bay 99'], tree_ids_before['Bay 2'])
  936. @tag('regression') # #22146
  937. def test_root_module_bay_rename_updates_display_order(self):
  938. """
  939. Even though renaming a root module bay does not renumber tree_ids,
  940. the manager's _root_name annotation must reflect the new name so the
  941. display ordering is correct.
  942. """
  943. device_type = DeviceType.objects.first()
  944. device_role = DeviceRole.objects.first()
  945. site = Site.objects.first()
  946. device = Device.objects.create(
  947. name='Rename Order Device',
  948. device_type=device_type,
  949. role=device_role,
  950. site=site,
  951. )
  952. for name in ('Bay 1', 'Bay 2', 'Bay 3'):
  953. ModuleBay.objects.create(device=device, name=name)
  954. bay = ModuleBay.objects.get(device=device, name='Bay 1')
  955. bay.name = 'Bay 4'
  956. bay.save()
  957. names = list(ModuleBay.objects.filter(device=device).values_list('name', flat=True))
  958. self.assertEqual(names, ['Bay 2', 'Bay 3', 'Bay 4'])
  959. @tag('regression') # #22146
  960. def test_child_module_bay_rename_preserves_intra_tree_ordering(self):
  961. """
  962. Renaming a *child* module bay must still trigger MPTT's intra-tree
  963. reorder, so siblings appear in name order after the rename. The
  964. rename-bypass only covers root bays.
  965. """
  966. device_type = DeviceType.objects.first()
  967. device_role = DeviceRole.objects.first()
  968. site = Site.objects.first()
  969. device = Device.objects.create(
  970. name='Child Rename Device',
  971. device_type=device_type,
  972. role=device_role,
  973. site=site,
  974. )
  975. root_bay = ModuleBay.objects.create(device=device, name='Bay 1')
  976. manufacturer = Manufacturer.objects.first()
  977. module_type = ModuleType.objects.create(
  978. manufacturer=manufacturer, model='Child Rename Type'
  979. )
  980. module = Module.objects.create(
  981. device=device, module_bay=root_bay, module_type=module_type
  982. )
  983. for name in ('Bay 1.1', 'Bay 1.2', 'Bay 1.3'):
  984. ModuleBay.objects.create(device=device, module=module, name=name)
  985. child = ModuleBay.objects.get(device=device, name='Bay 1.1')
  986. child.name = 'Bay 1.4'
  987. child.save()
  988. names = list(ModuleBay.objects.filter(device=device).values_list('name', flat=True))
  989. self.assertEqual(names, ['Bay 1', 'Bay 1.2', 'Bay 1.3', 'Bay 1.4'])
  990. @tag('regression') # #22146
  991. def test_root_to_child_transition_still_relocates(self):
  992. """
  993. Promoting an existing root module bay to a child (by assigning a
  994. module) must still flow through MPTT's normal move logic. The
  995. rename-bypass must not suppress legitimate parent changes.
  996. """
  997. device_type = DeviceType.objects.first()
  998. device_role = DeviceRole.objects.first()
  999. site = Site.objects.first()
  1000. device = Device.objects.create(
  1001. name='Root To Child Device',
  1002. device_type=device_type,
  1003. role=device_role,
  1004. site=site,
  1005. )
  1006. host_bay = ModuleBay.objects.create(device=device, name='Host Bay')
  1007. movable_bay = ModuleBay.objects.create(device=device, name='Movable Bay')
  1008. manufacturer = Manufacturer.objects.first()
  1009. module_type = ModuleType.objects.create(
  1010. manufacturer=manufacturer, model='Root To Child Type'
  1011. )
  1012. host_module = Module.objects.create(
  1013. device=device, module_bay=host_bay, module_type=module_type
  1014. )
  1015. movable_bay.module = host_module
  1016. movable_bay.save()
  1017. movable_bay.refresh_from_db()
  1018. host_bay.refresh_from_db()
  1019. self.assertEqual(movable_bay.parent_id, host_bay.pk)
  1020. self.assertEqual(movable_bay.tree_id, host_bay.tree_id)
  1021. @tag('regression') # #22251
  1022. def test_moving_module_reparents_child_module_bays(self):
  1023. """
  1024. When a module is moved to a different module bay, each child ModuleBay
  1025. (a bay that belongs to the module) must have its MPTT parent updated
  1026. to the new host bay. Without the fix the children stay parented to the
  1027. old bay even though Module.module_bay_id has changed.
  1028. """
  1029. device_type = DeviceType.objects.first()
  1030. device_role = DeviceRole.objects.first()
  1031. site = Site.objects.first()
  1032. device = Device.objects.create(
  1033. name='Move Module Device',
  1034. device_type=device_type,
  1035. role=device_role,
  1036. site=site,
  1037. )
  1038. bay_a = ModuleBay.objects.create(device=device, name='Bay A')
  1039. bay_b = ModuleBay.objects.create(device=device, name='Bay B')
  1040. manufacturer = Manufacturer.objects.first()
  1041. module_type = ModuleType.objects.create(
  1042. manufacturer=manufacturer, model='Move Module Type'
  1043. )
  1044. module = Module.objects.create(
  1045. device=device, module_bay=bay_a, module_type=module_type
  1046. )
  1047. child_1 = ModuleBay.objects.create(device=device, module=module, name='Child Bay 1')
  1048. child_2 = ModuleBay.objects.create(device=device, module=module, name='Child Bay 2')
  1049. self.assertEqual(child_1.parent_id, bay_a.pk)
  1050. self.assertEqual(child_2.parent_id, bay_a.pk)
  1051. # Move the module to bay_b.
  1052. module.module_bay = bay_b
  1053. module.save()
  1054. child_1.refresh_from_db()
  1055. child_2.refresh_from_db()
  1056. self.assertEqual(child_1.parent_id, bay_b.pk)
  1057. self.assertEqual(child_2.parent_id, bay_b.pk)
  1058. # Children must share the same MPTT tree as their new parent.
  1059. bay_b.refresh_from_db()
  1060. self.assertEqual(child_1.tree_id, bay_b.tree_id)
  1061. self.assertEqual(child_2.tree_id, bay_b.tree_id)
  1062. @tag('regression') # #22251
  1063. def test_moving_module_reparents_grandchild_module_bays(self):
  1064. """
  1065. When a module is moved, grandchild ModuleBays (bays inside a module
  1066. that is itself installed inside a child bay of the moved module) must
  1067. also land in the new MPTT tree. MPTT moves subtrees atomically, so
  1068. calling save() only on direct children is sufficient — this test
  1069. documents and preserves that invariant for future tree-backend changes.
  1070. """
  1071. device_type = DeviceType.objects.first()
  1072. device_role = DeviceRole.objects.first()
  1073. site = Site.objects.first()
  1074. device = Device.objects.create(
  1075. name='Grandchild Move Device',
  1076. device_type=device_type,
  1077. role=device_role,
  1078. site=site,
  1079. )
  1080. bay_a = ModuleBay.objects.create(device=device, name='Bay A')
  1081. bay_b = ModuleBay.objects.create(device=device, name='Bay B')
  1082. manufacturer = Manufacturer.objects.first()
  1083. module_type = ModuleType.objects.create(
  1084. manufacturer=manufacturer, model='Grandchild Move Type'
  1085. )
  1086. # Depth-1: module installed in bay_a, with one child bay.
  1087. module_1 = Module.objects.create(device=device, module_bay=bay_a, module_type=module_type)
  1088. child_bay = ModuleBay.objects.create(device=device, module=module_1, name='Child Bay')
  1089. # Depth-2: module installed in child_bay, with one grandchild bay.
  1090. module_2 = Module.objects.create(device=device, module_bay=child_bay, module_type=module_type)
  1091. grandchild_bay = ModuleBay.objects.create(device=device, module=module_2, name='Grandchild Bay')
  1092. self.assertEqual(child_bay.parent_id, bay_a.pk)
  1093. self.assertEqual(grandchild_bay.parent_id, child_bay.pk)
  1094. self.assertEqual(grandchild_bay.tree_id, bay_a.tree_id)
  1095. # Move the top-level module to bay_b.
  1096. module_1.module_bay = bay_b
  1097. module_1.save()
  1098. child_bay.refresh_from_db()
  1099. grandchild_bay.refresh_from_db()
  1100. bay_b.refresh_from_db()
  1101. self.assertEqual(child_bay.parent_id, bay_b.pk)
  1102. self.assertEqual(child_bay.tree_id, bay_b.tree_id)
  1103. # Grandchild's direct parent (child_bay) is unchanged; only tree placement moves.
  1104. self.assertEqual(grandchild_bay.parent_id, child_bay.pk)
  1105. self.assertEqual(grandchild_bay.tree_id, bay_b.tree_id)
  1106. def test_single_module_token(self):
  1107. device_type = DeviceType.objects.first()
  1108. device_role = DeviceRole.objects.first()
  1109. site = Site.objects.first()
  1110. location = Location.objects.first()
  1111. rack = Rack.objects.first()
  1112. # Create DeviceType components
  1113. ConsolePortTemplate.objects.create(
  1114. device_type=device_type,
  1115. name='{module}',
  1116. label='{module}',
  1117. )
  1118. ModuleBayTemplate.objects.create(
  1119. device_type=device_type,
  1120. name='Module Bay 1'
  1121. )
  1122. device = Device.objects.create(
  1123. name='Device 2',
  1124. device_type=device_type,
  1125. role=device_role,
  1126. site=site,
  1127. location=location,
  1128. rack=rack
  1129. )
  1130. device.consoleports.first()
  1131. @tag('regression') # #19918
  1132. def test_nested_module_bay_label_resolution(self):
  1133. """Test that nested module bay labels properly resolve {module} placeholders"""
  1134. manufacturer = Manufacturer.objects.first()
  1135. site = Site.objects.first()
  1136. device_role = DeviceRole.objects.first()
  1137. # Create device type with module bay template (position='A')
  1138. device_type = DeviceType.objects.create(
  1139. manufacturer=manufacturer,
  1140. model='Device with Bays',
  1141. slug='device-with-bays'
  1142. )
  1143. ModuleBayTemplate.objects.create(
  1144. device_type=device_type,
  1145. name='Bay A',
  1146. position='A'
  1147. )
  1148. # Create module type with nested bay template using {module} placeholder
  1149. module_type = ModuleType.objects.create(
  1150. manufacturer=manufacturer,
  1151. model='Module with Nested Bays'
  1152. )
  1153. ModuleBayTemplate.objects.create(
  1154. module_type=module_type,
  1155. name='SFP {module}-21',
  1156. label='{module}-21',
  1157. position='21'
  1158. )
  1159. # Create device and install module
  1160. device = Device.objects.create(
  1161. name='Test Device',
  1162. device_type=device_type,
  1163. role=device_role,
  1164. site=site
  1165. )
  1166. module_bay = device.modulebays.get(name='Bay A')
  1167. module = Module.objects.create(
  1168. device=device,
  1169. module_bay=module_bay,
  1170. module_type=module_type
  1171. )
  1172. # Verify nested bay label resolves {module} to parent position
  1173. nested_bay = module.modulebays.get(name='SFP A-21')
  1174. self.assertEqual(nested_bay.label, 'A-21')
  1175. @tag('regression') # #20467
  1176. def test_nested_module_bay_position_resolution(self):
  1177. """Test that {module} in a module bay template's position field is resolved when the module is installed."""
  1178. manufacturer = Manufacturer.objects.first()
  1179. site = Site.objects.first()
  1180. device_role = DeviceRole.objects.first()
  1181. device_type = DeviceType.objects.create(
  1182. manufacturer=manufacturer,
  1183. model='Device with Position Test',
  1184. slug='device-with-position-test'
  1185. )
  1186. ModuleBayTemplate.objects.create(
  1187. device_type=device_type,
  1188. name='Slot 1',
  1189. position='1'
  1190. )
  1191. module_type = ModuleType.objects.create(
  1192. manufacturer=manufacturer,
  1193. model='Module with Position Placeholder'
  1194. )
  1195. ModuleBayTemplate.objects.create(
  1196. module_type=module_type,
  1197. name='Sub-bay {module}-1',
  1198. position='{module}-1'
  1199. )
  1200. device = Device.objects.create(
  1201. name='Position Test Device',
  1202. device_type=device_type,
  1203. role=device_role,
  1204. site=site
  1205. )
  1206. module_bay = device.modulebays.get(name='Slot 1')
  1207. module = Module.objects.create(
  1208. device=device,
  1209. module_bay=module_bay,
  1210. module_type=module_type
  1211. )
  1212. nested_bay = module.modulebays.get(name='Sub-bay 1-1')
  1213. self.assertEqual(nested_bay.position, '1-1')
  1214. #
  1215. # Position inheritance tests (#19796)
  1216. #
  1217. def test_position_inheritance_depth_2(self):
  1218. """
  1219. A module bay with position '{module}/2' under a parent bay with position '1'
  1220. should resolve to position '1/2'. A single {module} in the interface template
  1221. should then resolve to '1/2'.
  1222. """
  1223. manufacturer = Manufacturer.objects.first()
  1224. site = Site.objects.first()
  1225. device_role = DeviceRole.objects.first()
  1226. device_type = DeviceType.objects.create(
  1227. manufacturer=manufacturer,
  1228. model='Chassis for Inheritance',
  1229. slug='chassis-for-inheritance'
  1230. )
  1231. ModuleBayTemplate.objects.create(
  1232. device_type=device_type,
  1233. name='Line card slot 1',
  1234. position='1'
  1235. )
  1236. line_card_type = ModuleType.objects.create(
  1237. manufacturer=manufacturer,
  1238. model='Line Card with Inherited Bays'
  1239. )
  1240. ModuleBayTemplate.objects.create(
  1241. module_type=line_card_type,
  1242. name='SFP bay {module}/1',
  1243. position='{module}/1'
  1244. )
  1245. ModuleBayTemplate.objects.create(
  1246. module_type=line_card_type,
  1247. name='SFP bay {module}/2',
  1248. position='{module}/2'
  1249. )
  1250. sfp_type = ModuleType.objects.create(
  1251. manufacturer=manufacturer,
  1252. model='SFP with Inherited Path'
  1253. )
  1254. InterfaceTemplate.objects.create(
  1255. module_type=sfp_type,
  1256. name='SFP {module}',
  1257. type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
  1258. )
  1259. device = Device.objects.create(
  1260. name='Inheritance Chassis',
  1261. device_type=device_type,
  1262. role=device_role,
  1263. site=site
  1264. )
  1265. lc_bay = device.modulebays.get(name='Line card slot 1')
  1266. line_card = Module.objects.create(
  1267. device=device,
  1268. module_bay=lc_bay,
  1269. module_type=line_card_type
  1270. )
  1271. sfp_bay = line_card.modulebays.get(name='SFP bay 1/2')
  1272. sfp_module = Module.objects.create(
  1273. device=device,
  1274. module_bay=sfp_bay,
  1275. module_type=sfp_type
  1276. )
  1277. interface = sfp_module.interfaces.first()
  1278. self.assertEqual(interface.name, 'SFP 1/2')
  1279. def test_position_inheritance_depth_3(self):
  1280. """
  1281. Position inheritance at depth 3: positions should chain through the tree.
  1282. """
  1283. manufacturer = Manufacturer.objects.first()
  1284. site = Site.objects.first()
  1285. device_role = DeviceRole.objects.first()
  1286. device_type = DeviceType.objects.create(
  1287. manufacturer=manufacturer,
  1288. model='Deep Chassis',
  1289. slug='deep-chassis'
  1290. )
  1291. ModuleBayTemplate.objects.create(
  1292. device_type=device_type,
  1293. name='Slot A',
  1294. position='A'
  1295. )
  1296. mid_type = ModuleType.objects.create(
  1297. manufacturer=manufacturer,
  1298. model='Mid Module'
  1299. )
  1300. ModuleBayTemplate.objects.create(
  1301. module_type=mid_type,
  1302. name='Sub {module}-1',
  1303. position='{module}-1'
  1304. )
  1305. leaf_type = ModuleType.objects.create(
  1306. manufacturer=manufacturer,
  1307. model='Leaf Module'
  1308. )
  1309. InterfaceTemplate.objects.create(
  1310. module_type=leaf_type,
  1311. name='Port {module}',
  1312. type=InterfaceTypeChoices.TYPE_1GE_FIXED
  1313. )
  1314. device = Device.objects.create(
  1315. name='Deep Device',
  1316. device_type=device_type,
  1317. role=device_role,
  1318. site=site
  1319. )
  1320. slot_a = device.modulebays.get(name='Slot A')
  1321. mid_module = Module.objects.create(
  1322. device=device,
  1323. module_bay=slot_a,
  1324. module_type=mid_type
  1325. )
  1326. sub_bay = mid_module.modulebays.get(name='Sub A-1')
  1327. self.assertEqual(sub_bay.position, 'A-1')
  1328. leaf_module = Module.objects.create(
  1329. device=device,
  1330. module_bay=sub_bay,
  1331. module_type=leaf_type
  1332. )
  1333. interface = leaf_module.interfaces.first()
  1334. self.assertEqual(interface.name, 'Port A-1')
  1335. def test_position_inheritance_custom_separator(self):
  1336. """
  1337. Users control the separator through the position field template.
  1338. Using '.' instead of '/' should work correctly.
  1339. """
  1340. manufacturer = Manufacturer.objects.first()
  1341. site = Site.objects.first()
  1342. device_role = DeviceRole.objects.first()
  1343. device_type = DeviceType.objects.create(
  1344. manufacturer=manufacturer,
  1345. model='Dot Separator Chassis',
  1346. slug='dot-separator-chassis'
  1347. )
  1348. ModuleBayTemplate.objects.create(
  1349. device_type=device_type,
  1350. name='Bay 1',
  1351. position='1'
  1352. )
  1353. card_type = ModuleType.objects.create(
  1354. manufacturer=manufacturer,
  1355. model='Card with Dot Separator'
  1356. )
  1357. ModuleBayTemplate.objects.create(
  1358. module_type=card_type,
  1359. name='Port {module}.1',
  1360. position='{module}.1'
  1361. )
  1362. sfp_type = ModuleType.objects.create(
  1363. manufacturer=manufacturer,
  1364. model='SFP Dot'
  1365. )
  1366. InterfaceTemplate.objects.create(
  1367. module_type=sfp_type,
  1368. name='eth{module}',
  1369. type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
  1370. )
  1371. device = Device.objects.create(
  1372. name='Dot Device',
  1373. device_type=device_type,
  1374. role=device_role,
  1375. site=site
  1376. )
  1377. bay = device.modulebays.get(name='Bay 1')
  1378. card = Module.objects.create(
  1379. device=device,
  1380. module_bay=bay,
  1381. module_type=card_type
  1382. )
  1383. port_bay = card.modulebays.get(name='Port 1.1')
  1384. sfp = Module.objects.create(
  1385. device=device,
  1386. module_bay=port_bay,
  1387. module_type=sfp_type
  1388. )
  1389. interface = sfp.interfaces.first()
  1390. self.assertEqual(interface.name, 'eth1.1')
  1391. def test_multi_token_backwards_compat(self):
  1392. """
  1393. Multi-token {module}/{module} at matching depth should still resolve
  1394. level-by-level (backwards compatibility).
  1395. """
  1396. manufacturer = Manufacturer.objects.first()
  1397. site = Site.objects.first()
  1398. device_role = DeviceRole.objects.first()
  1399. device_type = DeviceType.objects.create(
  1400. manufacturer=manufacturer,
  1401. model='Multi Token Chassis',
  1402. slug='multi-token-chassis'
  1403. )
  1404. ModuleBayTemplate.objects.create(
  1405. device_type=device_type,
  1406. name='Slot 1',
  1407. position='1'
  1408. )
  1409. card_type = ModuleType.objects.create(
  1410. manufacturer=manufacturer,
  1411. model='Card for Multi Token'
  1412. )
  1413. ModuleBayTemplate.objects.create(
  1414. module_type=card_type,
  1415. name='Port 1',
  1416. position='2'
  1417. )
  1418. iface_type = ModuleType.objects.create(
  1419. manufacturer=manufacturer,
  1420. model='Interface Module Multi Token'
  1421. )
  1422. InterfaceTemplate.objects.create(
  1423. module_type=iface_type,
  1424. name='Gi{module}/{module}',
  1425. type=InterfaceTypeChoices.TYPE_1GE_FIXED
  1426. )
  1427. device = Device.objects.create(
  1428. name='Multi Token Device',
  1429. device_type=device_type,
  1430. role=device_role,
  1431. site=site
  1432. )
  1433. slot = device.modulebays.get(name='Slot 1')
  1434. card = Module.objects.create(
  1435. device=device,
  1436. module_bay=slot,
  1437. module_type=card_type
  1438. )
  1439. port = card.modulebays.get(name='Port 1')
  1440. iface_module = Module.objects.create(
  1441. device=device,
  1442. module_bay=port,
  1443. module_type=iface_type
  1444. )
  1445. interface = iface_module.interfaces.first()
  1446. self.assertEqual(interface.name, 'Gi1/2')
  1447. @tag('regression') # #20912
  1448. def test_module_bay_parent_cleared_when_module_removed(self):
  1449. """Test that the parent field is properly cleared when a module bay's module assignment is removed"""
  1450. device = Device.objects.first()
  1451. manufacturer = Manufacturer.objects.first()
  1452. module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Test Module Type')
  1453. bay1 = ModuleBay.objects.create(device=device, name='Test Bay 1')
  1454. bay2 = ModuleBay.objects.create(device=device, name='Test Bay 2')
  1455. # Install a module in bay1
  1456. module1 = Module.objects.create(device=device, module_bay=bay1, module_type=module_type)
  1457. # Assign bay2 to module1 and verify parent is now set to bay1 (module1's bay)
  1458. bay2.module = module1
  1459. bay2.save()
  1460. bay2.refresh_from_db()
  1461. self.assertEqual(bay2.parent, bay1)
  1462. self.assertEqual(bay2.module, module1)
  1463. # Clear the module assignment (return bay2 to device level) Verify parent is cleared
  1464. bay2.module = None
  1465. bay2.save()
  1466. bay2.refresh_from_db()
  1467. self.assertIsNone(bay2.parent)
  1468. self.assertIsNone(bay2.module)
  1469. def test_module_installation_creates_port_mappings(self):
  1470. """
  1471. Test that installing a module with front/rear port templates correctly
  1472. creates PortMapping instances for the device.
  1473. """
  1474. device = Device.objects.first()
  1475. manufacturer = Manufacturer.objects.first()
  1476. module_bay = ModuleBay.objects.create(device=device, name='Test Bay PortMapping 1')
  1477. # Create a module type with a rear port template
  1478. module_type_with_mappings = ModuleType.objects.create(
  1479. manufacturer=manufacturer,
  1480. model='Module Type With Mappings',
  1481. )
  1482. # Create a rear port template with 12 positions (splice)
  1483. rear_port_template = RearPortTemplate.objects.create(
  1484. module_type=module_type_with_mappings,
  1485. name='Rear Port 1',
  1486. type=PortTypeChoices.TYPE_SPLICE,
  1487. positions=12,
  1488. )
  1489. # Create 12 front port templates mapped to the rear port
  1490. front_port_templates = []
  1491. for i in range(1, 13):
  1492. front_port_template = FrontPortTemplate.objects.create(
  1493. module_type=module_type_with_mappings,
  1494. name=f'port {i}',
  1495. type=PortTypeChoices.TYPE_LC,
  1496. positions=1,
  1497. )
  1498. front_port_templates.append(front_port_template)
  1499. # Create port template mapping
  1500. PortTemplateMapping.objects.create(
  1501. device_type=None,
  1502. module_type=module_type_with_mappings,
  1503. front_port=front_port_template,
  1504. front_port_position=1,
  1505. rear_port=rear_port_template,
  1506. rear_port_position=i,
  1507. )
  1508. # Install the module
  1509. module = Module.objects.create(
  1510. device=device,
  1511. module_bay=module_bay,
  1512. module_type=module_type_with_mappings,
  1513. status=ModuleStatusChoices.STATUS_ACTIVE,
  1514. )
  1515. # Verify that front ports were created
  1516. front_ports = FrontPort.objects.filter(device=device, module=module)
  1517. self.assertEqual(front_ports.count(), 12)
  1518. # Verify that the rear port was created
  1519. rear_ports = RearPort.objects.filter(device=device, module=module)
  1520. self.assertEqual(rear_ports.count(), 1)
  1521. rear_port = rear_ports.first()
  1522. self.assertEqual(rear_port.positions, 12)
  1523. # Verify that port mappings were created
  1524. port_mappings = PortMapping.objects.filter(front_port__module=module)
  1525. self.assertEqual(port_mappings.count(), 12)
  1526. # Verify each mapping is correct
  1527. for i, front_port_template in enumerate(front_port_templates, start=1):
  1528. front_port = FrontPort.objects.get(
  1529. device=device,
  1530. name=front_port_template.name,
  1531. module=module,
  1532. )
  1533. # Check that a mapping exists for this front port
  1534. mapping = PortMapping.objects.get(
  1535. device=device,
  1536. front_port=front_port,
  1537. front_port_position=1,
  1538. )
  1539. self.assertEqual(mapping.rear_port, rear_port)
  1540. self.assertEqual(mapping.front_port_position, 1)
  1541. self.assertEqual(mapping.rear_port_position, i)
  1542. def test_module_installation_without_mappings(self):
  1543. """
  1544. Test that installing a module without port template mappings
  1545. doesn't create any PortMapping instances.
  1546. """
  1547. device = Device.objects.first()
  1548. manufacturer = Manufacturer.objects.first()
  1549. module_bay = ModuleBay.objects.create(device=device, name='Test Bay PortMapping 2')
  1550. # Create a module type without any port template mappings
  1551. module_type_no_mappings = ModuleType.objects.create(
  1552. manufacturer=manufacturer,
  1553. model='Module Type Without Mappings',
  1554. )
  1555. # Create a rear port template
  1556. RearPortTemplate.objects.create(
  1557. module_type=module_type_no_mappings,
  1558. name='Rear Port 1',
  1559. type=PortTypeChoices.TYPE_SPLICE,
  1560. positions=12,
  1561. )
  1562. # Create front port templates but DO NOT create PortTemplateMapping rows
  1563. for i in range(1, 13):
  1564. FrontPortTemplate.objects.create(
  1565. module_type=module_type_no_mappings,
  1566. name=f'port {i}',
  1567. type=PortTypeChoices.TYPE_LC,
  1568. positions=1,
  1569. )
  1570. # Install the module
  1571. module = Module.objects.create(
  1572. device=device,
  1573. module_bay=module_bay,
  1574. module_type=module_type_no_mappings,
  1575. status=ModuleStatusChoices.STATUS_ACTIVE,
  1576. )
  1577. # Verify no port mappings were created for this module
  1578. port_mappings = PortMapping.objects.filter(
  1579. device=device,
  1580. front_port__module=module,
  1581. front_port_position=1,
  1582. )
  1583. self.assertEqual(port_mappings.count(), 0)
  1584. self.assertEqual(FrontPort.objects.filter(module=module).count(), 12)
  1585. self.assertEqual(RearPort.objects.filter(module=module).count(), 1)
  1586. self.assertEqual(PortMapping.objects.filter(front_port__module=module).count(), 0)
  1587. def test_cannot_install_module_in_disabled_bay(self):
  1588. """
  1589. Test that a Module cannot be installed into a disabled ModuleBay.
  1590. """
  1591. device = Device.objects.first()
  1592. manufacturer = Manufacturer.objects.first()
  1593. module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Test Module Type Disabled')
  1594. # Create a disabled module bay
  1595. disabled_bay = ModuleBay.objects.create(device=device, name='Disabled Bay', enabled=False)
  1596. # Attempt to install a module into the disabled bay
  1597. module = Module(device=device, module_bay=disabled_bay, module_type=module_type)
  1598. with self.assertRaises(ValidationError) as cm:
  1599. module.clean()
  1600. self.assertIn('module_bay', cm.exception.message_dict)
  1601. self.assertIn('disabled module bay', str(cm.exception.message_dict['module_bay']))
  1602. class CableTestCase(TestCase):
  1603. @classmethod
  1604. def setUpTestData(cls):
  1605. site = Site.objects.create(name='Test Site 1', slug='test-site-1')
  1606. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  1607. devicetype = DeviceType.objects.create(
  1608. manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
  1609. )
  1610. role = DeviceRole.objects.create(
  1611. name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
  1612. )
  1613. device1 = Device.objects.create(
  1614. device_type=devicetype, role=role, name='TestDevice1', site=site
  1615. )
  1616. device2 = Device.objects.create(
  1617. device_type=devicetype, role=role, name='TestDevice2', site=site
  1618. )
  1619. interfaces = (
  1620. Interface(device=device1, name='eth0'),
  1621. Interface(device=device2, name='eth0'),
  1622. Interface(device=device2, name='eth1'),
  1623. )
  1624. Interface.objects.bulk_create(interfaces)
  1625. Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[1]]).save()
  1626. PowerPort.objects.create(device=device2, name='psu1')
  1627. patch_panel = Device.objects.create(
  1628. device_type=devicetype, role=role, name='TestPatchPanel', site=site
  1629. )
  1630. rear_ports = (
  1631. RearPort(device=patch_panel, name='RP1', type='8p8c'),
  1632. RearPort(device=patch_panel, name='RP2', type='8p8c', positions=2),
  1633. RearPort(device=patch_panel, name='RP3', type='8p8c', positions=3),
  1634. RearPort(device=patch_panel, name='RP4', type='8p8c', positions=3),
  1635. )
  1636. RearPort.objects.bulk_create(rear_ports)
  1637. front_ports = (
  1638. FrontPort(device=patch_panel, name='FP1', type='8p8c'),
  1639. FrontPort(device=patch_panel, name='FP2', type='8p8c'),
  1640. FrontPort(device=patch_panel, name='FP3', type='8p8c'),
  1641. FrontPort(device=patch_panel, name='FP4', type='8p8c'),
  1642. )
  1643. FrontPort.objects.bulk_create(front_ports)
  1644. PortMapping.objects.bulk_create([
  1645. PortMapping(device=patch_panel, front_port=front_ports[0], rear_port=rear_ports[0]),
  1646. PortMapping(device=patch_panel, front_port=front_ports[1], rear_port=rear_ports[1]),
  1647. PortMapping(device=patch_panel, front_port=front_ports[2], rear_port=rear_ports[2]),
  1648. PortMapping(device=patch_panel, front_port=front_ports[3], rear_port=rear_ports[3]),
  1649. ])
  1650. provider = Provider.objects.create(name='Provider 1', slug='provider-1')
  1651. provider_network = ProviderNetwork.objects.create(name='Provider Network 1', provider=provider)
  1652. circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
  1653. circuit1 = Circuit.objects.create(provider=provider, type=circuittype, cid='1')
  1654. circuit2 = Circuit.objects.create(provider=provider, type=circuittype, cid='2')
  1655. CircuitTermination.objects.create(circuit=circuit1, termination=site, term_side='A')
  1656. CircuitTermination.objects.create(circuit=circuit1, termination=site, term_side='Z')
  1657. CircuitTermination.objects.create(circuit=circuit2, termination=provider_network, term_side='A')
  1658. def test_cable_creation(self):
  1659. """
  1660. When a new Cable is created, it must be cached on either termination point.
  1661. """
  1662. interface1 = Interface.objects.get(device__name='TestDevice1', name='eth0')
  1663. interface2 = Interface.objects.get(device__name='TestDevice2', name='eth0')
  1664. cable = Cable.objects.first()
  1665. self.assertEqual(interface1.cable, cable)
  1666. self.assertEqual(interface2.cable, cable)
  1667. self.assertEqual(interface1.cable_end, 'A')
  1668. self.assertEqual(interface2.cable_end, 'B')
  1669. self.assertEqual(interface1.link_peers, [interface2])
  1670. self.assertEqual(interface2.link_peers, [interface1])
  1671. def test_cable_deletion(self):
  1672. """
  1673. When a Cable is deleted, the `cable` field on its termination points must be nullified. The str() method
  1674. should still return the PK of the string even after being nullified.
  1675. """
  1676. interface1 = Interface.objects.get(device__name='TestDevice1', name='eth0')
  1677. interface2 = Interface.objects.get(device__name='TestDevice2', name='eth0')
  1678. cable = Cable.objects.first()
  1679. cable.delete()
  1680. self.assertIsNone(cable.pk)
  1681. self.assertNotEqual(str(cable), '#None')
  1682. interface1 = Interface.objects.get(pk=interface1.pk)
  1683. self.assertIsNone(interface1.cable)
  1684. self.assertListEqual(interface1.link_peers, [])
  1685. interface2 = Interface.objects.get(pk=interface2.pk)
  1686. self.assertIsNone(interface2.cable)
  1687. self.assertListEqual(interface2.link_peers, [])
  1688. def test_cable_validates_same_parent_object(self):
  1689. """
  1690. The clean method should ensure that all terminations at either end of a Cable belong to the same parent object.
  1691. """
  1692. interface1 = Interface.objects.get(device__name='TestDevice1', name='eth0')
  1693. powerport1 = PowerPort.objects.get(device__name='TestDevice2', name='psu1')
  1694. cable = Cable(a_terminations=[interface1], b_terminations=[powerport1])
  1695. with self.assertRaises(ValidationError):
  1696. cable.clean()
  1697. def test_cable_validates_same_type(self):
  1698. """
  1699. The clean method should ensure that all terminations at either end of a Cable are of the same type.
  1700. """
  1701. interface1 = Interface.objects.get(device__name='TestDevice1', name='eth0')
  1702. frontport1 = FrontPort.objects.get(device__name='TestPatchPanel', name='FP1')
  1703. rearport1 = RearPort.objects.get(device__name='TestPatchPanel', name='RP1')
  1704. cable = Cable(a_terminations=[frontport1, rearport1], b_terminations=[interface1])
  1705. with self.assertRaises(ValidationError):
  1706. cable.clean()
  1707. def test_cable_validates_compatible_types(self):
  1708. """
  1709. The clean method should have a check to ensure only compatible port types can be connected by a cable
  1710. """
  1711. interface1 = Interface.objects.get(device__name='TestDevice1', name='eth0')
  1712. powerport1 = PowerPort.objects.get(device__name='TestDevice2', name='psu1')
  1713. # An interface cannot be connected to a power port, for example
  1714. cable = Cable(a_terminations=[interface1], b_terminations=[powerport1])
  1715. with self.assertRaises(ValidationError):
  1716. cable.clean()
  1717. def test_cable_cannot_terminate_to_a_provider_network_circuittermination(self):
  1718. """
  1719. Neither side of a cable can be terminated to a CircuitTermination which is attached to a ProviderNetwork
  1720. """
  1721. interface3 = Interface.objects.get(device__name='TestDevice2', name='eth1')
  1722. circuittermination3 = CircuitTermination.objects.get(circuit__cid='2', term_side='A')
  1723. cable = Cable(a_terminations=[interface3], b_terminations=[circuittermination3])
  1724. with self.assertRaises(ValidationError):
  1725. cable.clean()
  1726. def test_cable_cannot_terminate_to_a_virtual_interface(self):
  1727. """
  1728. A cable cannot terminate to a virtual interface
  1729. """
  1730. device1 = Device.objects.get(name='TestDevice1')
  1731. interface2 = Interface.objects.get(device__name='TestDevice2', name='eth0')
  1732. virtual_interface = Interface(device=device1, name="V1", type=InterfaceTypeChoices.TYPE_VIRTUAL)
  1733. cable = Cable(a_terminations=[interface2], b_terminations=[virtual_interface])
  1734. with self.assertRaises(ValidationError):
  1735. cable.clean()
  1736. def test_cable_cannot_terminate_to_a_wireless_interface(self):
  1737. """
  1738. A cable cannot terminate to a wireless interface
  1739. """
  1740. device1 = Device.objects.get(name='TestDevice1')
  1741. interface2 = Interface.objects.get(device__name='TestDevice2', name='eth0')
  1742. wireless_interface = Interface(device=device1, name="W1", type=InterfaceTypeChoices.TYPE_80211A)
  1743. cable = Cable(a_terminations=[interface2], b_terminations=[wireless_interface])
  1744. with self.assertRaises(ValidationError):
  1745. cable.clean()
  1746. @tag('regression')
  1747. def test_cable_cannot_terminate_to_a_cellular_interface(self):
  1748. """
  1749. A cable cannot terminate to a cellular interface
  1750. """
  1751. device1 = Device.objects.get(name='TestDevice1')
  1752. interface2 = Interface.objects.get(device__name='TestDevice2', name='eth0')
  1753. cellular_interface = Interface(device=device1, name="W1", type=InterfaceTypeChoices.TYPE_LTE)
  1754. cable = Cable(a_terminations=[interface2], b_terminations=[cellular_interface])
  1755. with self.assertRaises(ValidationError):
  1756. cable.clean()
  1757. def test_cannot_cable_to_mark_connected(self):
  1758. """
  1759. Test that a cable cannot be connected to an interface marked as connected.
  1760. """
  1761. device1 = Device.objects.get(name='TestDevice1')
  1762. interface1 = Interface.objects.get(device__name='TestDevice2', name='eth1')
  1763. mark_connected_interface = Interface(device=device1, name='mark_connected1', mark_connected=True)
  1764. cable = Cable(a_terminations=[mark_connected_interface], b_terminations=[interface1])
  1765. with self.assertRaises(ValidationError):
  1766. cable.clean()
  1767. def test_cable_profile_change_preserves_terminations(self):
  1768. """
  1769. When a Cable's profile is changed via save() without explicitly setting terminations (as happens during
  1770. bulk edit), the existing termination points must be preserved.
  1771. """
  1772. cable = Cable.objects.first()
  1773. interface1 = Interface.objects.get(device__name='TestDevice1', name='eth0')
  1774. interface2 = Interface.objects.get(device__name='TestDevice2', name='eth0')
  1775. # Verify initial state: cable has terminations and no profile
  1776. self.assertEqual(cable.profile, '')
  1777. self.assertEqual(CableTermination.objects.filter(cable=cable).count(), 2)
  1778. # Simulate what bulk edit does: load the cable from DB, set profile via setattr, and save.
  1779. # Crucially, do NOT set a_terminations or b_terminations on the instance.
  1780. cable_from_db = Cable.objects.get(pk=cable.pk)
  1781. cable_from_db.profile = CableProfileChoices.SINGLE_1C1P
  1782. cable_from_db.save()
  1783. # Verify terminations are preserved
  1784. self.assertEqual(CableTermination.objects.filter(cable=cable).count(), 2)
  1785. # Verify the correct interfaces are still terminated
  1786. cable_from_db.refresh_from_db()
  1787. a_terms = [ct.termination for ct in CableTermination.objects.filter(cable=cable, cable_end='A')]
  1788. b_terms = [ct.termination for ct in CableTermination.objects.filter(cable=cable, cable_end='B')]
  1789. self.assertEqual(a_terms, [interface1])
  1790. self.assertEqual(b_terms, [interface2])
  1791. @tag('regression') # #21498
  1792. def test_path_refreshes_replaced_cablepath_reference(self):
  1793. """
  1794. An already-instantiated interface should refresh its denormalized
  1795. `_path` foreign key when the referenced CablePath row has been
  1796. replaced in the database.
  1797. """
  1798. stale_interface = Interface.objects.get(device__name='TestDevice1', name='eth0')
  1799. old_path = CablePath.objects.get(pk=stale_interface._path_id)
  1800. new_path = CablePath(
  1801. path=old_path.path,
  1802. is_active=old_path.is_active,
  1803. is_complete=old_path.is_complete,
  1804. is_split=old_path.is_split,
  1805. )
  1806. old_path_id = old_path.pk
  1807. old_path.delete()
  1808. new_path.save()
  1809. # The old CablePath no longer exists
  1810. self.assertFalse(CablePath.objects.filter(pk=old_path_id).exists())
  1811. # The already-instantiated interface still points to the deleted path
  1812. # until the accessor refreshes `_path` from the database.
  1813. self.assertEqual(stale_interface._path_id, old_path_id)
  1814. self.assertEqual(stale_interface.path.pk, new_path.pk)
  1815. @tag('regression') # #21498
  1816. def test_serialize_for_event_handles_stale_cablepath_reference_after_retermination(self):
  1817. """
  1818. Serializing an interface whose previously cached `_path` row has been
  1819. deleted during cable retermination must not raise.
  1820. """
  1821. stale_interface = Interface.objects.get(device__name='TestDevice2', name='eth0')
  1822. old_path_id = stale_interface._path_id
  1823. new_peer = Interface.objects.get(device__name='TestDevice2', name='eth1')
  1824. cable = stale_interface.cable
  1825. self.assertIsNotNone(cable)
  1826. self.assertIsNotNone(old_path_id)
  1827. self.assertEqual(stale_interface.cable_end, 'B')
  1828. cable.b_terminations = [new_peer]
  1829. cable.save()
  1830. # The old CablePath was deleted during retrace.
  1831. self.assertFalse(CablePath.objects.filter(pk=old_path_id).exists())
  1832. # The stale in-memory instance still holds the deleted FK value.
  1833. self.assertEqual(stale_interface._path_id, old_path_id)
  1834. # Serialization must not raise ObjectDoesNotExist. Because this interface
  1835. # was the former B-side termination, it is now disconnected.
  1836. data = serialize_for_event(stale_interface)
  1837. self.assertIsNone(data['connected_endpoints'])
  1838. self.assertIsNone(data['connected_endpoints_type'])
  1839. self.assertFalse(data['connected_endpoints_reachable'])
  1840. @tag('regression') # #21338
  1841. def test_path_refreshes_unset_cablepath_reference(self):
  1842. """
  1843. An endpoint instance saved during cable creation, before path tracing,
  1844. should resolve its path and connected endpoints.
  1845. The stale-instance preconditions rely on Cable.save() saving each
  1846. CableTermination (which re-saves the endpoint) before trace_paths
  1847. creates the CablePath records.
  1848. """
  1849. device = Device.objects.get(name='TestDevice2')
  1850. interface_a = Interface.objects.create(device=device, name='eth2')
  1851. interface_b = Interface.objects.create(device=device, name='eth3')
  1852. # Capture the instances handed to the event machinery on save
  1853. saved_instances = []
  1854. def capture(sender, instance, **kwargs):
  1855. saved_instances.append(instance)
  1856. post_save.connect(capture, sender=Interface)
  1857. try:
  1858. Cable(a_terminations=[interface_a], b_terminations=[interface_b]).save()
  1859. finally:
  1860. post_save.disconnect(capture, sender=Interface)
  1861. self.assertEqual(len(saved_instances), 2)
  1862. captured_a = next(i for i in saved_instances if i.pk == interface_a.pk)
  1863. captured_b = next(i for i in saved_instances if i.pk == interface_b.pk)
  1864. # The captured instances predate path tracing: cabled, but no path yet
  1865. self.assertIsNotNone(captured_a.cable_id)
  1866. self.assertIsNone(captured_a._path_id)
  1867. self.assertIsNone(captured_b._path_id)
  1868. # The accessor must repair the unset denormalized reference
  1869. self.assertIsNotNone(captured_a.path)
  1870. self.assertEqual(captured_a.connected_endpoints, [interface_b])
  1871. # Serialization as performed by the event queue must see the peer
  1872. data = serialize_for_event(captured_b)
  1873. self.assertEqual([endpoint['id'] for endpoint in data['connected_endpoints']], [interface_a.pk])
  1874. self.assertEqual([peer['id'] for peer in data['link_peers']], [interface_a.pk])
  1875. self.assertTrue(data['connected_endpoints_reachable'])
  1876. def test_path_returns_none_for_unsaved_endpoint(self):
  1877. """
  1878. An unsaved endpoint with a link assigned should report no path rather
  1879. than attempting a database refresh.
  1880. """
  1881. device = Device.objects.get(name='TestDevice1')
  1882. cable = Cable.objects.first()
  1883. interface = Interface(device=device, name='tmp', cable=cable)
  1884. self.assertIsNone(interface.path)
  1885. class CableTerminationTestCase(TestCase):
  1886. def test_cache_related_objects_requires_resolvable_termination(self):
  1887. """cache_related_objects raises ValueError when the termination cannot be resolved."""
  1888. cable_termination = CableTermination(
  1889. termination_type=ObjectType.objects.get_for_model(Interface),
  1890. termination_id=0,
  1891. )
  1892. with self.assertRaises(ValueError):
  1893. cable_termination.cache_related_objects()
  1894. class VirtualDeviceContextTestCase(TestCase):
  1895. @classmethod
  1896. def setUpTestData(cls):
  1897. site = Site.objects.create(name='Test Site 1', slug='test-site-1')
  1898. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  1899. devicetype = DeviceType.objects.create(
  1900. manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
  1901. )
  1902. role = DeviceRole.objects.create(
  1903. name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
  1904. )
  1905. Device.objects.create(
  1906. device_type=devicetype, role=role, name='TestDevice1', site=site
  1907. )
  1908. def test_vdc_and_interface_creation(self):
  1909. device = Device.objects.first()
  1910. vdc = VirtualDeviceContext(device=device, name="VDC 1", identifier=1, status='active')
  1911. vdc.full_clean()
  1912. vdc.save()
  1913. interface = Interface(device=device, name='Eth1/1', type='10gbase-t')
  1914. interface.full_clean()
  1915. interface.save()
  1916. interface.vdcs.set([vdc])
  1917. def test_vdc_duplicate_name(self):
  1918. device = Device.objects.first()
  1919. vdc1 = VirtualDeviceContext(device=device, name="VDC 1", identifier=1, status='active')
  1920. vdc1.full_clean()
  1921. vdc1.save()
  1922. vdc2 = VirtualDeviceContext(device=device, name="VDC 1", identifier=2, status='active')
  1923. with self.assertRaises(ValidationError):
  1924. vdc2.full_clean()
  1925. def test_vdc_duplicate_identifier(self):
  1926. device = Device.objects.first()
  1927. vdc1 = VirtualDeviceContext(device=device, name="VDC 1", identifier=1, status='active')
  1928. vdc1.full_clean()
  1929. vdc1.save()
  1930. vdc2 = VirtualDeviceContext(device=device, name="VDC 2", identifier=1, status='active')
  1931. with self.assertRaises(ValidationError):
  1932. vdc2.full_clean()
  1933. class VirtualChassisTestCase(TestCase):
  1934. @classmethod
  1935. def setUpTestData(cls):
  1936. site = Site.objects.create(name='Test Site 1', slug='test-site-1')
  1937. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  1938. devicetype = DeviceType.objects.create(
  1939. manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
  1940. )
  1941. role = DeviceRole.objects.create(
  1942. name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
  1943. )
  1944. Device.objects.create(
  1945. device_type=devicetype, role=role, name='TestDevice1', site=site
  1946. )
  1947. Device.objects.create(
  1948. device_type=devicetype, role=role, name='TestDevice2', site=site
  1949. )
  1950. def test_virtualchassis_deletion_clears_vc_position(self):
  1951. """
  1952. Test that when a VirtualChassis is deleted, member devices have their
  1953. vc_position and vc_priority fields set to None.
  1954. """
  1955. devices = Device.objects.all()
  1956. device1 = devices[0]
  1957. device2 = devices[1]
  1958. # Create a VirtualChassis with two member devices
  1959. vc = VirtualChassis.objects.create(name='Test VC', master=device1)
  1960. device1.virtual_chassis = vc
  1961. device1.vc_position = 1
  1962. device1.vc_priority = 10
  1963. device1.save()
  1964. device2.virtual_chassis = vc
  1965. device2.vc_position = 2
  1966. device2.vc_priority = 20
  1967. device2.save()
  1968. # Verify devices are members of the VC with positions set
  1969. device1.refresh_from_db()
  1970. device2.refresh_from_db()
  1971. self.assertEqual(device1.virtual_chassis, vc)
  1972. self.assertEqual(device1.vc_position, 1)
  1973. self.assertEqual(device1.vc_priority, 10)
  1974. self.assertEqual(device2.virtual_chassis, vc)
  1975. self.assertEqual(device2.vc_position, 2)
  1976. self.assertEqual(device2.vc_priority, 20)
  1977. # Delete the VirtualChassis
  1978. vc.delete()
  1979. # Verify devices have vc_position and vc_priority set to None
  1980. device1.refresh_from_db()
  1981. device2.refresh_from_db()
  1982. self.assertIsNone(device1.virtual_chassis)
  1983. self.assertIsNone(device1.vc_position)
  1984. self.assertIsNone(device1.vc_priority)
  1985. self.assertIsNone(device2.virtual_chassis)
  1986. self.assertIsNone(device2.vc_position)
  1987. self.assertIsNone(device2.vc_priority)
  1988. def test_virtualchassis_duplicate_vc_position(self):
  1989. """
  1990. Test that two devices cannot be assigned to the same vc_position
  1991. within the same VirtualChassis.
  1992. """
  1993. devices = Device.objects.all()
  1994. device1 = devices[0]
  1995. device2 = devices[1]
  1996. # Create a VirtualChassis
  1997. vc = VirtualChassis.objects.create(name='Test VC')
  1998. # Assign first device to vc_position 1
  1999. device1.virtual_chassis = vc
  2000. device1.vc_position = 1
  2001. device1.full_clean()
  2002. device1.save()
  2003. # Try to assign second device to the same vc_position
  2004. device2.virtual_chassis = vc
  2005. device2.vc_position = 1
  2006. with self.assertRaises(ValidationError):
  2007. device2.full_clean()
  2008. class VCPositionTokenTestCase(TestCase):
  2009. @classmethod
  2010. def setUpTestData(cls):
  2011. Site.objects.create(name='Test Site 1', slug='test-site-1')
  2012. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  2013. DeviceType.objects.create(
  2014. manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
  2015. )
  2016. ModuleType.objects.create(
  2017. manufacturer=manufacturer, model='Test Module Type 1'
  2018. )
  2019. DeviceRole.objects.create(name='Test Role 1', slug='test-role-1')
  2020. def test_vc_position_token_in_vc(self):
  2021. site = Site.objects.first()
  2022. device_type = DeviceType.objects.first()
  2023. module_type = ModuleType.objects.first()
  2024. device_role = DeviceRole.objects.first()
  2025. InterfaceTemplate.objects.create(
  2026. module_type=module_type,
  2027. name='ge-{vc_position}/{module}/0',
  2028. type='1000base-t',
  2029. )
  2030. vc = VirtualChassis.objects.create(name='Test VC 1')
  2031. device = Device.objects.create(
  2032. name='Device VC 1', device_type=device_type, role=device_role,
  2033. site=site, virtual_chassis=vc, vc_position=8,
  2034. )
  2035. module_bay = ModuleBay.objects.create(device=device, name='Bay 1', position='1')
  2036. Module.objects.create(device=device, module_bay=module_bay, module_type=module_type)
  2037. interface = device.interfaces.get(name='ge-8/1/0')
  2038. self.assertEqual(interface.name, 'ge-8/1/0')
  2039. def test_vc_position_token_not_in_vc_default_fallback(self):
  2040. site = Site.objects.first()
  2041. device_type = DeviceType.objects.first()
  2042. module_type = ModuleType.objects.first()
  2043. device_role = DeviceRole.objects.first()
  2044. InterfaceTemplate.objects.create(
  2045. module_type=module_type,
  2046. name='ge-{vc_position}/{module}/0',
  2047. type='1000base-t',
  2048. )
  2049. device = Device.objects.create(
  2050. name='Device NoVC 1', device_type=device_type, role=device_role,
  2051. site=site,
  2052. )
  2053. module_bay = ModuleBay.objects.create(device=device, name='Bay 1', position='1')
  2054. Module.objects.create(device=device, module_bay=module_bay, module_type=module_type)
  2055. interface = device.interfaces.get(name='ge-0/1/0')
  2056. self.assertEqual(interface.name, 'ge-0/1/0')
  2057. def test_vc_position_token_explicit_fallback(self):
  2058. site = Site.objects.first()
  2059. device_type = DeviceType.objects.first()
  2060. module_type = ModuleType.objects.first()
  2061. device_role = DeviceRole.objects.first()
  2062. InterfaceTemplate.objects.create(
  2063. module_type=module_type,
  2064. name='ge-{vc_position:18}/{module}/0',
  2065. type='1000base-t',
  2066. )
  2067. device = Device.objects.create(
  2068. name='Device NoVC 2', device_type=device_type, role=device_role,
  2069. site=site,
  2070. )
  2071. module_bay = ModuleBay.objects.create(device=device, name='Bay 1', position='1')
  2072. Module.objects.create(device=device, module_bay=module_bay, module_type=module_type)
  2073. interface = device.interfaces.get(name='ge-18/1/0')
  2074. self.assertEqual(interface.name, 'ge-18/1/0')
  2075. def test_vc_position_token_explicit_fallback_ignored_when_in_vc(self):
  2076. site = Site.objects.first()
  2077. device_type = DeviceType.objects.first()
  2078. module_type = ModuleType.objects.first()
  2079. device_role = DeviceRole.objects.first()
  2080. InterfaceTemplate.objects.create(
  2081. module_type=module_type,
  2082. name='ge-{vc_position:99}/{module}/0',
  2083. type='1000base-t',
  2084. )
  2085. vc = VirtualChassis.objects.create(name='Test VC 2')
  2086. device = Device.objects.create(
  2087. name='Device VC 2', device_type=device_type, role=device_role,
  2088. site=site, virtual_chassis=vc, vc_position=2,
  2089. )
  2090. module_bay = ModuleBay.objects.create(device=device, name='Bay 1', position='1')
  2091. Module.objects.create(device=device, module_bay=module_bay, module_type=module_type)
  2092. interface = device.interfaces.get(name='ge-2/1/0')
  2093. self.assertEqual(interface.name, 'ge-2/1/0')
  2094. def test_vc_position_token_device_type_template(self):
  2095. site = Site.objects.first()
  2096. device_type = DeviceType.objects.first()
  2097. device_role = DeviceRole.objects.first()
  2098. InterfaceTemplate.objects.create(
  2099. device_type=device_type,
  2100. name='ge-{vc_position:0}/0/0',
  2101. type='1000base-t',
  2102. )
  2103. vc = VirtualChassis.objects.create(name='Test VC 3')
  2104. device = Device.objects.create(
  2105. name='Device VC 3', device_type=device_type, role=device_role,
  2106. site=site, virtual_chassis=vc, vc_position=3,
  2107. )
  2108. interface = device.interfaces.get(name='ge-3/0/0')
  2109. self.assertEqual(interface.name, 'ge-3/0/0')
  2110. def test_vc_position_token_device_type_template_not_in_vc(self):
  2111. site = Site.objects.first()
  2112. device_type = DeviceType.objects.first()
  2113. device_role = DeviceRole.objects.first()
  2114. InterfaceTemplate.objects.create(
  2115. device_type=device_type,
  2116. name='ge-{vc_position:0}/0/0',
  2117. type='1000base-t',
  2118. )
  2119. device = Device.objects.create(
  2120. name='Device NoVC 3', device_type=device_type, role=device_role,
  2121. site=site,
  2122. )
  2123. interface = device.interfaces.get(name='ge-0/0/0')
  2124. self.assertEqual(interface.name, 'ge-0/0/0')
  2125. def test_vc_position_token_label_resolution(self):
  2126. site = Site.objects.first()
  2127. device_type = DeviceType.objects.first()
  2128. module_type = ModuleType.objects.first()
  2129. device_role = DeviceRole.objects.first()
  2130. InterfaceTemplate.objects.create(
  2131. module_type=module_type,
  2132. name='ge-{vc_position}/{module}/0',
  2133. label='Member {vc_position:0} / Slot {module}',
  2134. type='1000base-t',
  2135. )
  2136. vc = VirtualChassis.objects.create(name='Test VC 4')
  2137. device = Device.objects.create(
  2138. name='Device VC 4', device_type=device_type, role=device_role,
  2139. site=site, virtual_chassis=vc, vc_position=2,
  2140. )
  2141. module_bay = ModuleBay.objects.create(device=device, name='Bay 1', position='1')
  2142. Module.objects.create(device=device, module_bay=module_bay, module_type=module_type)
  2143. interface = device.interfaces.get(name='ge-2/1/0')
  2144. self.assertEqual(interface.label, 'Member 2 / Slot 1')
  2145. class SiteSignalTestCase(TestCase):
  2146. @tag('regression')
  2147. def test_edit_site_with_prefix_no_vrf(self):
  2148. site = Site.objects.create(name='Test Site', slug='test-site')
  2149. Prefix.objects.create(prefix='192.0.2.0/24', scope=site, vrf=None)
  2150. # Regression test for #21045: should not raise ValueError
  2151. site.save()
  2152. class PowerPortDrawTestCase(TestCase):
  2153. """
  2154. Tests for PowerPort.get_power_draw() power aggregation logic.
  2155. """
  2156. @classmethod
  2157. def setUpTestData(cls):
  2158. cls.site = Site.objects.create(name='Test Site', slug='test-site')
  2159. manufacturer = Manufacturer.objects.create(name='Generic', slug='generic')
  2160. device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Test Device Type')
  2161. role = DeviceRole.objects.create(name='Test Role', slug='test-role')
  2162. cls.pdu = Device.objects.create(
  2163. device_type=device_type, role=role, site=cls.site, name='pdu'
  2164. )
  2165. cls.server = Device.objects.create(
  2166. device_type=device_type, role=role, site=cls.site, name='server'
  2167. )
  2168. def test_direct_draw_aggregation(self):
  2169. """
  2170. Sanity check: with one PowerOutlet chained directly to a downstream PSU PowerPort,
  2171. the upstream PowerPort should reflect the PSU's allocated/maximum draw.
  2172. [main] -- [outlet] --C-- [psu]
  2173. """
  2174. main = PowerPort.objects.create(device=self.pdu, name='main')
  2175. outlet = PowerOutlet.objects.create(device=self.pdu, name='outlet', power_port=main)
  2176. psu = PowerPort.objects.create(
  2177. device=self.server, name='psu', allocated_draw=200, maximum_draw=400
  2178. )
  2179. Cable(a_terminations=[outlet], b_terminations=[psu]).save()
  2180. draw = main.get_power_draw()
  2181. self.assertEqual(draw['allocated'], 200)
  2182. self.assertEqual(draw['maximum'], 400)
  2183. @tag('regression')
  2184. def test_recursive_draw_through_intermediate_powerport(self):
  2185. """
  2186. Regression test for #21949: A PDU modeled with internal fuses (intermediate PowerPorts in
  2187. auto mode) should still aggregate downstream PSU draw up to the main PowerPort.
  2188. [main] -- [feedback] --C-- [fuse] -- [outlet] --C-- [psu]
  2189. Both `main` and `fuse` are in auto mode (no allocated_draw/maximum_draw set). The draw
  2190. reported by `psu` must propagate through `fuse` and be reflected at `main`.
  2191. """
  2192. main = PowerPort.objects.create(device=self.pdu, name='main')
  2193. feedback = PowerOutlet.objects.create(device=self.pdu, name='feedback', power_port=main)
  2194. fuse = PowerPort.objects.create(device=self.pdu, name='fuse')
  2195. outlet = PowerOutlet.objects.create(device=self.pdu, name='outlet', power_port=fuse)
  2196. psu = PowerPort.objects.create(
  2197. device=self.server, name='psu', allocated_draw=150, maximum_draw=300
  2198. )
  2199. Cable(a_terminations=[feedback], b_terminations=[fuse]).save()
  2200. Cable(a_terminations=[outlet], b_terminations=[psu]).save()
  2201. fuse_draw = fuse.get_power_draw()
  2202. self.assertEqual(fuse_draw['allocated'], 150)
  2203. self.assertEqual(fuse_draw['maximum'], 300)
  2204. main_draw = main.get_power_draw()
  2205. self.assertEqual(main_draw['allocated'], 150)
  2206. self.assertEqual(main_draw['maximum'], 300)
  2207. def test_intermediate_manual_override_stops_recursion(self):
  2208. """
  2209. When an intermediate PowerPort has an explicit allocated_draw/maximum_draw, recursion should
  2210. stop there and the administratively defined values should be used.
  2211. """
  2212. main = PowerPort.objects.create(device=self.pdu, name='main')
  2213. feedback = PowerOutlet.objects.create(device=self.pdu, name='feedback', power_port=main)
  2214. fuse = PowerPort.objects.create(
  2215. device=self.pdu, name='fuse', allocated_draw=500, maximum_draw=1000
  2216. )
  2217. outlet = PowerOutlet.objects.create(device=self.pdu, name='outlet', power_port=fuse)
  2218. psu = PowerPort.objects.create(
  2219. device=self.server, name='psu', allocated_draw=150, maximum_draw=300
  2220. )
  2221. Cable(a_terminations=[feedback], b_terminations=[fuse]).save()
  2222. Cable(a_terminations=[outlet], b_terminations=[psu]).save()
  2223. main_draw = main.get_power_draw()
  2224. self.assertEqual(main_draw['allocated'], 500)
  2225. self.assertEqual(main_draw['maximum'], 1000)
  2226. def _connect_three_phase_feed(self, powerport):
  2227. """
  2228. Helper: attach `powerport` via cable to a newly-created three-phase PowerFeed.
  2229. """
  2230. power_panel = PowerPanel.objects.create(site=self.site, name='Panel')
  2231. power_feed = PowerFeed.objects.create(
  2232. power_panel=power_panel,
  2233. name='Feed',
  2234. phase=PowerFeedPhaseChoices.PHASE_3PHASE,
  2235. )
  2236. Cable(a_terminations=[powerport], b_terminations=[power_feed]).save()
  2237. @tag('regression')
  2238. def test_three_phase_per_leg_aggregation(self):
  2239. """
  2240. Regression test: per-leg totals for a main PowerPort connected to a three-phase PowerFeed
  2241. must be populated even when the full aggregation runs first. Previously, a shared visited
  2242. set caused downstream ports to be skipped during the per-leg passes, zeroing the legs.
  2243. [main] --C-- [3-phase PowerFeed]
  2244. ├── [outlet_A] (leg A) --C-- [portA] (allocated=100, maximum=200)
  2245. ├── [outlet_B] (leg B) --C-- [portB] (allocated=200, maximum=400)
  2246. └── [outlet_C] (leg C) --C-- [portC] (allocated=300, maximum=600)
  2247. """
  2248. main = PowerPort.objects.create(device=self.pdu, name='main')
  2249. self._connect_three_phase_feed(main)
  2250. leg_specs = [
  2251. (PowerOutletFeedLegChoices.FEED_LEG_A, 100, 200),
  2252. (PowerOutletFeedLegChoices.FEED_LEG_B, 200, 400),
  2253. (PowerOutletFeedLegChoices.FEED_LEG_C, 300, 600),
  2254. ]
  2255. for leg, allocated, maximum in leg_specs:
  2256. outlet = PowerOutlet.objects.create(
  2257. device=self.pdu, name=f'outlet_{leg}', power_port=main, feed_leg=leg
  2258. )
  2259. port = PowerPort.objects.create(
  2260. device=self.server, name=f'psu_{leg}',
  2261. allocated_draw=allocated, maximum_draw=maximum,
  2262. )
  2263. Cable(a_terminations=[outlet], b_terminations=[port]).save()
  2264. # Re-fetch to clear cached_property values populated before cable creation
  2265. main = PowerPort.objects.get(pk=main.pk)
  2266. draw = main.get_power_draw()
  2267. self.assertEqual(draw['allocated'], 600)
  2268. self.assertEqual(draw['maximum'], 1200)
  2269. legs_by_name = {leg['name']: leg for leg in draw['legs']}
  2270. self.assertEqual(legs_by_name['A']['allocated'], 100)
  2271. self.assertEqual(legs_by_name['A']['maximum'], 200)
  2272. self.assertEqual(legs_by_name['B']['allocated'], 200)
  2273. self.assertEqual(legs_by_name['B']['maximum'], 400)
  2274. self.assertEqual(legs_by_name['C']['allocated'], 300)
  2275. self.assertEqual(legs_by_name['C']['maximum'], 600)
  2276. @tag('regression')
  2277. def test_three_phase_per_leg_recursive_aggregation(self):
  2278. """
  2279. Regression test for #21949 on three-phase feeds: per-leg totals must aggregate through
  2280. intermediate auto-mode PowerPorts (the PDU-internal "fuse" pattern).
  2281. [main] --C-- [3-phase PowerFeed]
  2282. └── [feedback_A] (leg A) --C-- [fuse_A] (auto)
  2283. └── [outlet_A] (leg A) --C-- [psu_A] (allocated=100)
  2284. """
  2285. main = PowerPort.objects.create(device=self.pdu, name='main')
  2286. self._connect_three_phase_feed(main)
  2287. feedback = PowerOutlet.objects.create(
  2288. device=self.pdu, name='feedback_A', power_port=main,
  2289. feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A,
  2290. )
  2291. fuse = PowerPort.objects.create(device=self.pdu, name='fuse_A')
  2292. outlet = PowerOutlet.objects.create(
  2293. device=self.pdu, name='outlet_A', power_port=fuse,
  2294. feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A,
  2295. )
  2296. psu = PowerPort.objects.create(
  2297. device=self.server, name='psu_A', allocated_draw=100, maximum_draw=200
  2298. )
  2299. Cable(a_terminations=[feedback], b_terminations=[fuse]).save()
  2300. Cable(a_terminations=[outlet], b_terminations=[psu]).save()
  2301. # Re-fetch to clear cached_property values populated before cable creation
  2302. main = PowerPort.objects.get(pk=main.pk)
  2303. draw = main.get_power_draw()
  2304. self.assertEqual(draw['allocated'], 100)
  2305. self.assertEqual(draw['maximum'], 200)
  2306. legs_by_name = {leg['name']: leg for leg in draw['legs']}
  2307. self.assertEqual(legs_by_name['A']['allocated'], 100)
  2308. self.assertEqual(legs_by_name['A']['maximum'], 200)
  2309. self.assertEqual(legs_by_name['B']['allocated'], 0)
  2310. self.assertEqual(legs_by_name['C']['allocated'], 0)