test_api.py 158 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127
  1. import json
  2. from django.conf import settings
  3. from django.db import connection
  4. from django.test import tag
  5. from django.test.utils import CaptureQueriesContext
  6. from django.urls import reverse
  7. from django.utils.translation import gettext as _
  8. from rest_framework import status
  9. from core.models import ObjectType
  10. from dcim.choices import *
  11. from dcim.constants import *
  12. from dcim.graphql.types import _CABLE_TERMINATION_MODELS
  13. from dcim.models import *
  14. from extras.models import ConfigTemplate, Tag
  15. from ipam.choices import VLANQinQRoleChoices
  16. from ipam.models import ASN, RIR, VLAN, VRF, IPAddress
  17. from netbox.api.serializers import GenericObjectSerializer
  18. from tenancy.models import Tenant
  19. from users.constants import TOKEN_PREFIX
  20. from users.models import ObjectPermission, Token, User
  21. from utilities.testing import (
  22. APITestCase,
  23. APIViewTestCases,
  24. GraphQLFilterTest,
  25. GraphQLQueryTest,
  26. create_test_device,
  27. create_test_nat_ip_pair,
  28. disable_logging,
  29. )
  30. from virtualization.models import Cluster, ClusterType
  31. from wireless.choices import WirelessChannelChoices
  32. from wireless.models import WirelessLAN
  33. class AppTestCase(APITestCase):
  34. def test_root(self):
  35. url = reverse('dcim-api:api-root')
  36. response = self.client.get('{}?format=api'.format(url), **self.header)
  37. self.assertEqual(response.status_code, 200)
  38. class Mixins:
  39. class ComponentTraceMixin(APITestCase):
  40. peer_termination_type = None
  41. def test_trace(self):
  42. """
  43. Test tracing a device component's attached cable.
  44. """
  45. obj = self.model.objects.first()
  46. peer_device = Device.objects.create(
  47. site=Site.objects.first(),
  48. device_type=DeviceType.objects.first(),
  49. role=DeviceRole.objects.first(),
  50. name='Peer Device'
  51. )
  52. if self.peer_termination_type is None:
  53. raise NotImplementedError(_("Test case must set peer_termination_type"))
  54. peer_obj = self.peer_termination_type.objects.create(
  55. device=peer_device,
  56. name='Peer Termination'
  57. )
  58. cable = Cable(a_terminations=[obj], b_terminations=[peer_obj], label='Cable 1')
  59. cable.save()
  60. self.add_permissions(f'dcim.view_{self.model._meta.model_name}')
  61. url = reverse(f'dcim-api:{self.model._meta.model_name}-trace', kwargs={'pk': obj.pk})
  62. response = self.client.get(url, **self.header)
  63. self.assertHttpStatus(response, status.HTTP_200_OK)
  64. self.assertEqual(len(response.data), 1)
  65. segment1 = response.data[0]
  66. self.assertEqual(segment1[0][0]['name'], obj.name)
  67. self.assertEqual(segment1[1]['label'], cable.label)
  68. self.assertEqual(segment1[2][0]['name'], peer_obj.name)
  69. class RegionTestCase(APIViewTestCases.APIViewTestCase):
  70. model = Region
  71. brief_fields = ['_depth', 'description', 'display', 'id', 'name', 'site_count', 'slug', 'url']
  72. create_data = [
  73. {
  74. 'name': 'Region 4',
  75. 'slug': 'region-4',
  76. 'comments': 'this is region 4, not region 5',
  77. },
  78. {
  79. 'name': 'Region 5',
  80. 'slug': 'region-5',
  81. },
  82. {
  83. 'name': 'Region 6',
  84. 'slug': 'region-6',
  85. },
  86. ]
  87. bulk_update_data = {
  88. 'description': 'New description',
  89. 'comments': 'New comments',
  90. }
  91. @classmethod
  92. def setUpTestData(cls):
  93. Region.objects.create(name='Region 1', slug='region-1')
  94. Region.objects.create(name='Region 2', slug='region-2', comments='what in the world is happening?')
  95. Region.objects.create(name='Region 3', slug='region-3')
  96. class SiteGroupTestCase(APIViewTestCases.APIViewTestCase):
  97. model = SiteGroup
  98. brief_fields = ['_depth', 'description', 'display', 'id', 'name', 'site_count', 'slug', 'url']
  99. create_data = [
  100. {
  101. 'name': 'Site Group 4',
  102. 'slug': 'site-group-4',
  103. 'comments': '',
  104. },
  105. {
  106. 'name': 'Site Group 5',
  107. 'slug': 'site-group-5',
  108. 'comments': 'not actually empty',
  109. },
  110. {
  111. 'name': 'Site Group 6',
  112. 'slug': 'site-group-6',
  113. 'comments': 'Do I really exist?',
  114. },
  115. ]
  116. bulk_update_data = {
  117. 'description': 'New description',
  118. 'comments': 'I do exist!',
  119. }
  120. @classmethod
  121. def setUpTestData(cls):
  122. SiteGroup.objects.create(name='Site Group 1', slug='site-group-1')
  123. SiteGroup.objects.create(name='Site Group 2', slug='site-group-2', comments='')
  124. SiteGroup.objects.create(name='Site Group 3', slug='site-group-3', comments='Hi!')
  125. class SiteTestCase(APIViewTestCases.APIViewTestCase):
  126. model = Site
  127. brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url']
  128. bulk_update_data = {
  129. 'status': 'planned',
  130. }
  131. graphql_filter_tests = (
  132. GraphQLFilterTest(
  133. name='tenant__name__exact',
  134. filters='tenant: {name: {exact: "Tenant 1"}}',
  135. expected=lambda qs: qs.filter(tenant__name='Tenant 1'),
  136. permissions=('tenancy.view_tenant',),
  137. ),
  138. )
  139. def assert_nested_locations_active(self, data):
  140. site_data = data.get('site') or {}
  141. location_names = sorted(location['name'] for location in site_data.get('locations', []))
  142. self.assertEqual(location_names, ['Site1 Active A', 'Site1 Active B'])
  143. @classmethod
  144. def setUpTestData(cls):
  145. regions = (
  146. Region.objects.create(name='Region 1', slug='region-1'),
  147. Region.objects.create(name='Region 2', slug='region-2'),
  148. )
  149. groups = (
  150. SiteGroup.objects.create(name='Site Group 1', slug='site-group-1'),
  151. SiteGroup.objects.create(name='Site Group 2', slug='site-group-2'),
  152. )
  153. tenant = Tenant.objects.create(name='Tenant 1', slug='tenant-1')
  154. # Site 1's tenant activates the dynamic tenant prefetch (+1 in api_list_objects baseline).
  155. sites = (
  156. Site(region=regions[0], group=groups[0], tenant=tenant, name='Site 1', slug='site-1'),
  157. Site(region=regions[0], group=groups[0], name='Site 2', slug='site-2'),
  158. Site(region=regions[0], group=groups[0], name='Site 3', slug='site-3'),
  159. )
  160. Site.objects.bulk_create(sites)
  161. nested_site = Site.objects.get(slug='site-1')
  162. cls.nested_site_pk = nested_site.pk
  163. Location.objects.create(
  164. site=nested_site, name='Site1 Active A', slug='site1-active-a',
  165. status=LocationStatusChoices.STATUS_ACTIVE,
  166. )
  167. Location.objects.create(
  168. site=nested_site, name='Site1 Active B', slug='site1-active-b',
  169. status=LocationStatusChoices.STATUS_ACTIVE,
  170. )
  171. Location.objects.create(
  172. site=nested_site, name='Site1 Planned', slug='site1-planned',
  173. status=LocationStatusChoices.STATUS_PLANNED,
  174. )
  175. rir = RIR.objects.create(name='RFC 6996', is_private=True)
  176. asns = [
  177. ASN(asn=65000 + i, rir=rir) for i in range(8)
  178. ]
  179. ASN.objects.bulk_create(asns)
  180. cls.create_data = [
  181. {
  182. 'name': 'Site 4',
  183. 'slug': 'site-4',
  184. 'region': regions[1].pk,
  185. 'group': groups[1].pk,
  186. 'status': SiteStatusChoices.STATUS_ACTIVE,
  187. 'asns': [asns[0].pk, asns[1].pk],
  188. 'tenant': tenant.pk,
  189. },
  190. {
  191. 'name': 'Site 5',
  192. 'slug': 'site-5',
  193. 'region': regions[1].pk,
  194. 'group': groups[1].pk,
  195. 'status': SiteStatusChoices.STATUS_ACTIVE,
  196. 'asns': [asns[2].pk, asns[3].pk],
  197. },
  198. {
  199. 'name': 'Site 6',
  200. 'slug': 'site-6',
  201. 'region': regions[1].pk,
  202. 'group': groups[1].pk,
  203. 'status': SiteStatusChoices.STATUS_ACTIVE,
  204. 'asns': [asns[4].pk, asns[5].pk],
  205. },
  206. ]
  207. cls.graphql_query_tests = (
  208. GraphQLQueryTest(
  209. name='nested_locations_by_status',
  210. query=(
  211. '{ site(id: ' + str(cls.nested_site_pk) + ') { '
  212. 'locations(filters: {status: {exact: STATUS_ACTIVE}}) { name } '
  213. '} }'
  214. ),
  215. assert_result=cls.assert_nested_locations_active,
  216. permissions=('dcim.view_location',),
  217. ),
  218. )
  219. def test_add_tags(self):
  220. """
  221. Add tags to an existing object via the add_tags field.
  222. """
  223. site = Site.objects.first()
  224. tags = Tag.objects.bulk_create((
  225. Tag(name='Alpha', slug='alpha'),
  226. Tag(name='Bravo', slug='bravo'),
  227. Tag(name='Charlie', slug='charlie'),
  228. ))
  229. site.tags.set([tags[0], tags[1]])
  230. # Grant change permission
  231. obj_perm = ObjectPermission(name='Test permission', actions=['change'])
  232. obj_perm.save()
  233. obj_perm.users.add(self.user)
  234. obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
  235. url = self._get_detail_url(site)
  236. data = {
  237. 'add_tags': [{'name': 'Charlie'}],
  238. }
  239. response = self.client.patch(url, data, format='json', **self.header)
  240. self.assertHttpStatus(response, status.HTTP_200_OK)
  241. # Verify all three tags are now assigned
  242. tag_names = sorted(site.tags.values_list('name', flat=True))
  243. self.assertEqual(tag_names, ['Alpha', 'Bravo', 'Charlie'])
  244. # Verify add_tags and remove_tags are not in the response
  245. self.assertNotIn('add_tags', response.data)
  246. self.assertNotIn('remove_tags', response.data)
  247. self.assertIn('tags', response.data)
  248. def test_remove_tags(self):
  249. """
  250. Remove tags from an existing object via the remove_tags field.
  251. """
  252. site = Site.objects.first()
  253. tags = Tag.objects.bulk_create((
  254. Tag(name='Alpha', slug='alpha'),
  255. Tag(name='Bravo', slug='bravo'),
  256. Tag(name='Charlie', slug='charlie'),
  257. ))
  258. site.tags.set(tags)
  259. # Grant change permission
  260. obj_perm = ObjectPermission(name='Test permission', actions=['change'])
  261. obj_perm.save()
  262. obj_perm.users.add(self.user)
  263. obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
  264. url = self._get_detail_url(site)
  265. data = {
  266. 'remove_tags': [{'name': 'Charlie'}],
  267. }
  268. response = self.client.patch(url, data, format='json', **self.header)
  269. self.assertHttpStatus(response, status.HTTP_200_OK)
  270. # Verify only Alpha and Bravo remain
  271. tag_names = sorted(site.tags.values_list('name', flat=True))
  272. self.assertEqual(tag_names, ['Alpha', 'Bravo'])
  273. def test_remove_tags_not_assigned(self):
  274. """
  275. Removing a tag that is not assigned should not raise an error.
  276. """
  277. site = Site.objects.first()
  278. tags = Tag.objects.bulk_create((
  279. Tag(name='Alpha', slug='alpha'),
  280. Tag(name='Bravo', slug='bravo'),
  281. Tag(name='Charlie', slug='charlie'),
  282. ))
  283. site.tags.set([tags[0], tags[1]])
  284. # Grant change permission
  285. obj_perm = ObjectPermission(name='Test permission', actions=['change'])
  286. obj_perm.save()
  287. obj_perm.users.add(self.user)
  288. obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
  289. url = self._get_detail_url(site)
  290. data = {
  291. 'remove_tags': [{'name': 'Charlie'}],
  292. }
  293. response = self.client.patch(url, data, format='json', **self.header)
  294. self.assertHttpStatus(response, status.HTTP_200_OK)
  295. # Tags should be unchanged
  296. tag_names = sorted(site.tags.values_list('name', flat=True))
  297. self.assertEqual(tag_names, ['Alpha', 'Bravo'])
  298. def test_add_and_remove_tags(self):
  299. """
  300. Add and remove tags in the same request.
  301. """
  302. site = Site.objects.first()
  303. tags = Tag.objects.bulk_create((
  304. Tag(name='Alpha', slug='alpha'),
  305. Tag(name='Bravo', slug='bravo'),
  306. Tag(name='Charlie', slug='charlie'),
  307. ))
  308. site.tags.set([tags[0], tags[1]])
  309. # Grant change permission
  310. obj_perm = ObjectPermission(name='Test permission', actions=['change'])
  311. obj_perm.save()
  312. obj_perm.users.add(self.user)
  313. obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
  314. url = self._get_detail_url(site)
  315. data = {
  316. 'add_tags': [{'name': 'Charlie'}],
  317. 'remove_tags': [{'name': 'Alpha'}],
  318. }
  319. response = self.client.patch(url, data, format='json', **self.header)
  320. self.assertHttpStatus(response, status.HTTP_200_OK)
  321. # Verify Bravo and Charlie remain
  322. tag_names = sorted(site.tags.values_list('name', flat=True))
  323. self.assertEqual(tag_names, ['Bravo', 'Charlie'])
  324. def test_tags_with_add_tags_error(self):
  325. """
  326. Specifying tags together with add_tags or remove_tags should raise a validation error.
  327. """
  328. site = Site.objects.first()
  329. Tag.objects.bulk_create((
  330. Tag(name='Alpha', slug='alpha'),
  331. Tag(name='Bravo', slug='bravo'),
  332. ))
  333. # Grant change permission
  334. obj_perm = ObjectPermission(name='Test permission', actions=['change'])
  335. obj_perm.save()
  336. obj_perm.users.add(self.user)
  337. obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
  338. url = self._get_detail_url(site)
  339. data = {
  340. 'tags': [{'name': 'Alpha'}],
  341. 'add_tags': [{'name': 'Bravo'}],
  342. }
  343. response = self.client.patch(url, data, format='json', **self.header)
  344. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  345. def test_create_with_add_tags(self):
  346. """
  347. Create a new object using add_tags.
  348. """
  349. Tag.objects.bulk_create((
  350. Tag(name='Alpha', slug='alpha'),
  351. Tag(name='Bravo', slug='bravo'),
  352. ))
  353. obj_perm = ObjectPermission(name='Test permission', actions=['add'])
  354. obj_perm.save()
  355. obj_perm.users.add(self.user)
  356. obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
  357. data = {
  358. 'name': 'Site 10',
  359. 'slug': 'site-10',
  360. 'add_tags': [{'name': 'Alpha'}, {'name': 'Bravo'}],
  361. }
  362. response = self.client.post(self._get_list_url(), data, format='json', **self.header)
  363. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  364. site = Site.objects.get(pk=response.data['id'])
  365. tag_names = sorted(site.tags.values_list('name', flat=True))
  366. self.assertEqual(tag_names, ['Alpha', 'Bravo'])
  367. def test_create_with_remove_tags_error(self):
  368. """
  369. Using remove_tags when creating a new object should raise a validation error.
  370. """
  371. Tag.objects.bulk_create((
  372. Tag(name='Alpha', slug='alpha'),
  373. ))
  374. obj_perm = ObjectPermission(name='Test permission', actions=['add'])
  375. obj_perm.save()
  376. obj_perm.users.add(self.user)
  377. obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
  378. data = {
  379. 'name': 'Site 10',
  380. 'slug': 'site-10',
  381. 'remove_tags': [{'name': 'Alpha'}],
  382. }
  383. response = self.client.post(self._get_list_url(), data, format='json', **self.header)
  384. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  385. def test_add_and_remove_same_tag_error(self):
  386. """
  387. Including the same tag in both add_tags and remove_tags should raise a validation error.
  388. """
  389. site = Site.objects.first()
  390. Tag.objects.bulk_create((
  391. Tag(name='Alpha', slug='alpha'),
  392. Tag(name='Bravo', slug='bravo'),
  393. ))
  394. obj_perm = ObjectPermission(name='Test permission', actions=['change'])
  395. obj_perm.save()
  396. obj_perm.users.add(self.user)
  397. obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
  398. url = self._get_detail_url(site)
  399. data = {
  400. 'add_tags': [{'name': 'Alpha'}, {'name': 'Bravo'}],
  401. 'remove_tags': [{'name': 'Alpha'}],
  402. }
  403. response = self.client.patch(url, data, format='json', **self.header)
  404. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  405. class LocationTestCase(APIViewTestCases.APIViewTestCase):
  406. model = Location
  407. brief_fields = ['_depth', 'description', 'display', 'id', 'name', 'rack_count', 'slug', 'url']
  408. bulk_update_data = {
  409. 'description': 'New description',
  410. }
  411. user_permissions = ('dcim.view_site',)
  412. graphql_filter_tests = (
  413. GraphQLFilterTest(
  414. name='status__in_list',
  415. filters='status: {in_list: [STATUS_PLANNED, STATUS_STAGING]}',
  416. expected=lambda qs: qs.filter(status__in=[
  417. LocationStatusChoices.STATUS_PLANNED,
  418. LocationStatusChoices.STATUS_STAGING,
  419. ]),
  420. ),
  421. )
  422. @classmethod
  423. def setUpTestData(cls):
  424. sites = (
  425. Site(name='Site 1', slug='site-1'),
  426. Site(name='Site 2', slug='site-2'),
  427. )
  428. Site.objects.bulk_create(sites)
  429. parent_locations = (
  430. Location.objects.create(
  431. site=sites[0],
  432. name='Parent Location 1',
  433. slug='parent-location-1',
  434. status=LocationStatusChoices.STATUS_ACTIVE,
  435. comments='First!'
  436. ),
  437. Location.objects.create(
  438. site=sites[1],
  439. name='Parent Location 2',
  440. slug='parent-location-2',
  441. status=LocationStatusChoices.STATUS_ACTIVE,
  442. comments='Second!'
  443. ),
  444. )
  445. Location.objects.create(
  446. site=sites[0],
  447. name='Location 1',
  448. slug='location-1',
  449. parent=parent_locations[0],
  450. status=LocationStatusChoices.STATUS_ACTIVE,
  451. comments='Third!'
  452. )
  453. Location.objects.create(
  454. site=sites[0],
  455. name='Location 2',
  456. slug='location-2',
  457. parent=parent_locations[0],
  458. status=LocationStatusChoices.STATUS_ACTIVE,
  459. )
  460. Location.objects.create(
  461. site=sites[0],
  462. name='Location 3',
  463. slug='location-3',
  464. parent=parent_locations[0],
  465. status=LocationStatusChoices.STATUS_ACTIVE,
  466. )
  467. Location.objects.create(
  468. site=sites[0],
  469. name='GraphQL Planned Location',
  470. slug='graphql-planned-location',
  471. parent=parent_locations[0],
  472. status=LocationStatusChoices.STATUS_PLANNED,
  473. )
  474. Location.objects.create(
  475. site=sites[0],
  476. name='GraphQL Staging Location',
  477. slug='graphql-staging-location',
  478. parent=parent_locations[0],
  479. status=LocationStatusChoices.STATUS_STAGING,
  480. )
  481. cls.create_data = [
  482. {
  483. 'name': 'Test Location 4',
  484. 'slug': 'test-location-4',
  485. 'site': sites[1].pk,
  486. 'parent': parent_locations[1].pk,
  487. 'status': LocationStatusChoices.STATUS_PLANNED,
  488. 'comments': '',
  489. },
  490. {
  491. 'name': 'Test Location 5',
  492. 'slug': 'test-location-5',
  493. 'site': sites[1].pk,
  494. 'parent': parent_locations[1].pk,
  495. 'status': LocationStatusChoices.STATUS_PLANNED,
  496. 'comments': 'Somebody should check on this location',
  497. },
  498. {
  499. 'name': 'Test Location 6',
  500. 'slug': 'test-location-6',
  501. 'site': sites[1].pk,
  502. # Omit parent to test uniqueness constraint
  503. 'status': LocationStatusChoices.STATUS_PLANNED,
  504. },
  505. ]
  506. class RackGroupTestCase(APIViewTestCases.APIViewTestCase):
  507. model = RackGroup
  508. brief_fields = ['description', 'display', 'id', 'name', 'rack_count', 'slug', 'url']
  509. create_data = [
  510. {
  511. 'name': 'Rack Group 4',
  512. 'slug': 'rack-group-4',
  513. },
  514. {
  515. 'name': 'Rack Group 5',
  516. 'slug': 'rack-group-5',
  517. },
  518. {
  519. 'name': 'Rack Group 6',
  520. 'slug': 'rack-group-6',
  521. },
  522. ]
  523. bulk_update_data = {
  524. 'description': 'New description',
  525. }
  526. @classmethod
  527. def setUpTestData(cls):
  528. rack_groups = (
  529. RackGroup(name='Rack Group 1', slug='rack-group-1'),
  530. RackGroup(name='Rack Group 2', slug='rack-group-2'),
  531. RackGroup(name='Rack Group 3', slug='rack-group-3'),
  532. )
  533. RackGroup.objects.bulk_create(rack_groups)
  534. class RackRoleTestCase(APIViewTestCases.APIViewTestCase):
  535. model = RackRole
  536. brief_fields = ['description', 'display', 'id', 'name', 'rack_count', 'slug', 'url']
  537. create_data = [
  538. {
  539. 'name': 'Rack Role 4',
  540. 'slug': 'rack-role-4',
  541. 'color': 'ffff00',
  542. },
  543. {
  544. 'name': 'Rack Role 5',
  545. 'slug': 'rack-role-5',
  546. 'color': 'ffff00',
  547. },
  548. {
  549. 'name': 'Rack Role 6',
  550. 'slug': 'rack-role-6',
  551. 'color': 'ffff00',
  552. },
  553. ]
  554. bulk_update_data = {
  555. 'description': 'New description',
  556. }
  557. @classmethod
  558. def setUpTestData(cls):
  559. rack_roles = (
  560. RackRole(name='Rack Role 1', slug='rack-role-1', color='ff0000'),
  561. RackRole(name='Rack Role 2', slug='rack-role-2', color='00ff00'),
  562. RackRole(name='Rack Role 3', slug='rack-role-3', color='0000ff'),
  563. )
  564. RackRole.objects.bulk_create(rack_roles)
  565. class RackTypeTestCase(APIViewTestCases.APIViewTestCase):
  566. model = RackType
  567. brief_fields = ['description', 'display', 'id', 'manufacturer', 'model', 'rack_count', 'slug', 'url']
  568. bulk_update_data = {
  569. 'description': 'new description',
  570. }
  571. user_permissions = ('dcim.view_manufacturer',)
  572. @classmethod
  573. def setUpTestData(cls):
  574. manufacturers = (
  575. Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
  576. Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
  577. )
  578. Manufacturer.objects.bulk_create(manufacturers)
  579. rack_types = (
  580. RackType(
  581. manufacturer=manufacturers[0],
  582. model='Rack Type 1',
  583. slug='rack-type-1',
  584. form_factor=RackFormFactorChoices.TYPE_CABINET,
  585. ),
  586. RackType(
  587. manufacturer=manufacturers[0],
  588. model='Rack Type 2',
  589. slug='rack-type-2',
  590. form_factor=RackFormFactorChoices.TYPE_CABINET,
  591. ),
  592. RackType(
  593. manufacturer=manufacturers[0],
  594. model='Rack Type 3',
  595. slug='rack-type-3',
  596. form_factor=RackFormFactorChoices.TYPE_CABINET,
  597. ),
  598. )
  599. RackType.objects.bulk_create(rack_types)
  600. cls.create_data = [
  601. {
  602. 'manufacturer': manufacturers[1].pk,
  603. 'model': 'Rack Type 4',
  604. 'slug': 'rack-type-4',
  605. 'form_factor': RackFormFactorChoices.TYPE_CABINET,
  606. },
  607. {
  608. 'manufacturer': manufacturers[1].pk,
  609. 'model': 'Rack Type 5',
  610. 'slug': 'rack-type-5',
  611. 'form_factor': RackFormFactorChoices.TYPE_CABINET,
  612. },
  613. {
  614. 'manufacturer': manufacturers[1].pk,
  615. 'model': 'Rack Type 6',
  616. 'slug': 'rack-type-6',
  617. 'form_factor': RackFormFactorChoices.TYPE_CABINET,
  618. },
  619. ]
  620. class RackTestCase(APIViewTestCases.APIViewTestCase):
  621. model = Rack
  622. brief_fields = ['description', 'device_count', 'display', 'id', 'name', 'url']
  623. bulk_update_data = {
  624. 'status': 'planned',
  625. }
  626. user_permissions = ('dcim.view_site', )
  627. @classmethod
  628. def setUpTestData(cls):
  629. sites = (
  630. Site(name='Site 1', slug='site-1'),
  631. Site(name='Site 2', slug='site-2'),
  632. )
  633. Site.objects.bulk_create(sites)
  634. locations = (
  635. Location.objects.create(site=sites[0], name='Location 1', slug='location-1'),
  636. Location.objects.create(site=sites[1], name='Location 2', slug='location-2'),
  637. )
  638. rack_groups = (
  639. RackGroup(name='Rack Group 1', slug='rack-group-1'),
  640. RackGroup(name='Rack Group 2', slug='rack-group-2'),
  641. )
  642. RackGroup.objects.bulk_create(rack_groups)
  643. rack_roles = (
  644. RackRole(name='Rack Role 1', slug='rack-role-1', color='ff0000'),
  645. RackRole(name='Rack Role 2', slug='rack-role-2', color='00ff00'),
  646. )
  647. RackRole.objects.bulk_create(rack_roles)
  648. racks = (
  649. Rack(site=sites[0], location=locations[0], group=rack_groups[0], role=rack_roles[0], name='Rack 1'),
  650. Rack(site=sites[0], location=locations[0], group=rack_groups[0], role=rack_roles[0], name='Rack 2'),
  651. Rack(site=sites[0], location=locations[0], group=rack_groups[0], role=rack_roles[0], name='Rack 3'),
  652. )
  653. Rack.objects.bulk_create(racks)
  654. cls.create_data = [
  655. {
  656. 'name': 'Test Rack 4',
  657. 'site': sites[1].pk,
  658. 'location': locations[1].pk,
  659. 'group': rack_groups[1].pk,
  660. 'role': rack_roles[1].pk,
  661. },
  662. {
  663. 'name': 'Test Rack 5',
  664. 'site': sites[1].pk,
  665. 'location': locations[1].pk,
  666. 'group': rack_groups[1].pk,
  667. 'role': rack_roles[1].pk,
  668. },
  669. {
  670. 'name': 'Test Rack 6',
  671. 'site': sites[1].pk,
  672. 'location': locations[1].pk,
  673. 'group': rack_groups[1].pk,
  674. 'role': rack_roles[1].pk,
  675. },
  676. ]
  677. def test_get_rack_elevation(self):
  678. """
  679. GET a single rack elevation.
  680. """
  681. rack = Rack.objects.first()
  682. self.add_permissions('dcim.view_rack')
  683. url = reverse('dcim-api:rack-elevation', kwargs={'pk': rack.pk})
  684. # Retrieve all units
  685. response = self.client.get(url, **self.header)
  686. self.assertEqual(response.data['count'], 84)
  687. # Search for specific units
  688. response = self.client.get(f'{url}?q=3', **self.header)
  689. self.assertEqual(response.data['count'], 26)
  690. response = self.client.get(f'{url}?q=U3', **self.header)
  691. self.assertEqual(response.data['count'], 22)
  692. response = self.client.get(f'{url}?q=U10', **self.header)
  693. self.assertEqual(response.data['count'], 2)
  694. def test_get_rack_elevation_description_is_occupying_device_name(self):
  695. """
  696. Verify occupied rack units include the occupying device in their description.
  697. """
  698. rack = Rack.objects.first()
  699. self.add_permissions('dcim.view_rack', 'dcim.view_device')
  700. url = reverse('dcim-api:rack-elevation', kwargs={'pk': rack.pk})
  701. device = create_test_device(
  702. name='Device A',
  703. site=rack.site,
  704. rack=rack,
  705. position=40,
  706. face=DeviceFaceChoices.FACE_FRONT,
  707. )
  708. # Retrieve all units
  709. response = self.client.get(url, **self.header)
  710. self.assertHttpStatus(response, status.HTTP_200_OK)
  711. occupied_unit = next(unit for unit in response.data['results'] if unit['name'] == 'U40')
  712. self.assertEqual(occupied_unit['device']['id'], device.pk)
  713. self.assertEqual(occupied_unit['description'], f'{device}')
  714. unoccupied_unit = next(unit for unit in response.data['results'] if unit['name'] == 'U39')
  715. self.assertEqual(unoccupied_unit['device'], None)
  716. self.assertEqual(unoccupied_unit['description'], None)
  717. def test_get_rack_elevation_svg(self):
  718. """
  719. GET a single rack elevation in SVG format.
  720. """
  721. rack = Rack.objects.first()
  722. self.add_permissions('dcim.view_rack')
  723. url = '{}?render=svg'.format(reverse('dcim-api:rack-elevation', kwargs={'pk': rack.pk}))
  724. response = self.client.get(url, **self.header)
  725. self.assertHttpStatus(response, status.HTTP_200_OK)
  726. self.assertEqual(response.get('Content-Type'), 'image/svg+xml')
  727. class RackReservationTestCase(APIViewTestCases.APIViewTestCase):
  728. model = RackReservation
  729. brief_fields = ['description', 'display', 'id', 'status', 'units', 'url', 'user']
  730. bulk_update_data = {
  731. 'description': 'New description',
  732. }
  733. user_permissions = ('dcim.view_rack', 'users.view_user')
  734. @classmethod
  735. def setUpTestData(cls):
  736. user = User.objects.create(username='user1', is_active=True)
  737. site = Site.objects.create(name='Test Site 1', slug='test-site-1')
  738. racks = (
  739. Rack(site=site, name='Rack 1'),
  740. Rack(site=site, name='Rack 2'),
  741. )
  742. Rack.objects.bulk_create(racks)
  743. rack_reservations = (
  744. RackReservation(
  745. rack=racks[0],
  746. units=[1, 2, 3],
  747. user=user,
  748. description='Reservation #1',
  749. ),
  750. RackReservation(
  751. rack=racks[0],
  752. units=[4, 5, 6],
  753. user=user,
  754. description='Reservation #2'
  755. ),
  756. RackReservation(
  757. rack=racks[0],
  758. units=[7, 8, 9],
  759. user=user,
  760. description='Reservation #3',
  761. ),
  762. )
  763. RackReservation.objects.bulk_create(rack_reservations)
  764. cls.create_data = [
  765. {
  766. 'rack': racks[1].pk,
  767. 'units': [10, 11, 12],
  768. 'status': RackReservationStatusChoices.STATUS_ACTIVE,
  769. 'user': user.pk,
  770. 'description': 'Reservation #4',
  771. },
  772. {
  773. 'rack': racks[1].pk,
  774. 'units': [13, 14, 15],
  775. 'status': RackReservationStatusChoices.STATUS_PENDING,
  776. 'user': user.pk,
  777. 'description': 'Reservation #5',
  778. },
  779. {
  780. 'rack': racks[1].pk,
  781. 'units': [16, 17, 18],
  782. 'status': RackReservationStatusChoices.STATUS_STALE,
  783. 'user': user.pk,
  784. 'description': 'Reservation #6',
  785. },
  786. ]
  787. def test_unit_count(self):
  788. """unit_count should reflect the number of units in the reservation."""
  789. url = reverse('dcim-api:rackreservation-list')
  790. self.add_permissions('dcim.view_rackreservation')
  791. response = self.client.get(url, **self.header)
  792. self.assertHttpStatus(response, 200)
  793. for result in response.data['results']:
  794. self.assertEqual(result['unit_count'], len(result['units']))
  795. class ManufacturerTestCase(APIViewTestCases.APIViewTestCase):
  796. model = Manufacturer
  797. brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url']
  798. create_data = [
  799. {
  800. 'name': 'Manufacturer 4',
  801. 'slug': 'manufacturer-4',
  802. },
  803. {
  804. 'name': 'Manufacturer 5',
  805. 'slug': 'manufacturer-5',
  806. },
  807. {
  808. 'name': 'Manufacturer 6',
  809. 'slug': 'manufacturer-6',
  810. },
  811. ]
  812. bulk_update_data = {
  813. 'description': 'New description',
  814. }
  815. @classmethod
  816. def setUpTestData(cls):
  817. manufacturers = (
  818. Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
  819. Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
  820. Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
  821. )
  822. Manufacturer.objects.bulk_create(manufacturers)
  823. class DeviceTypeTestCase(APIViewTestCases.APIViewTestCase):
  824. model = DeviceType
  825. brief_fields = ['description', 'device_count', 'display', 'id', 'manufacturer', 'model', 'slug', 'url']
  826. bulk_update_data = {
  827. 'part_number': 'ABC123',
  828. }
  829. user_permissions = ('dcim.view_manufacturer', )
  830. @classmethod
  831. def setUpTestData(cls):
  832. manufacturers = (
  833. Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
  834. Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
  835. )
  836. Manufacturer.objects.bulk_create(manufacturers)
  837. device_types = (
  838. DeviceType(manufacturer=manufacturers[0], model='Device Type 1', slug='device-type-1'),
  839. DeviceType(manufacturer=manufacturers[0], model='Device Type 2', slug='device-type-2'),
  840. DeviceType(manufacturer=manufacturers[0], model='Device Type 3', slug='device-type-3'),
  841. )
  842. DeviceType.objects.bulk_create(device_types)
  843. cls.create_data = [
  844. {
  845. 'manufacturer': manufacturers[1].pk,
  846. 'model': 'Device Type 4',
  847. 'slug': 'device-type-4',
  848. 'u_height': 0,
  849. },
  850. {
  851. 'manufacturer': manufacturers[1].pk,
  852. 'model': 'Device Type 5',
  853. 'slug': 'device-type-5',
  854. 'u_height': 0.5,
  855. },
  856. {
  857. 'manufacturer': manufacturers[1].pk,
  858. 'model': 'Device Type 6',
  859. 'slug': 'device-type-6',
  860. 'u_height': 1,
  861. },
  862. ]
  863. class ModuleTypeTestCase(APIViewTestCases.APIViewTestCase):
  864. model = ModuleType
  865. brief_fields = ['description', 'display', 'id', 'manufacturer', 'model', 'module_count', 'profile', 'url']
  866. bulk_update_data = {
  867. 'part_number': 'ABC123',
  868. }
  869. user_permissions = ('dcim.view_manufacturer', )
  870. @classmethod
  871. def setUpTestData(cls):
  872. manufacturers = (
  873. Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
  874. Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
  875. )
  876. Manufacturer.objects.bulk_create(manufacturers)
  877. module_types = (
  878. ModuleType(manufacturer=manufacturers[0], model='Module Type 1'),
  879. ModuleType(manufacturer=manufacturers[0], model='Module Type 2'),
  880. ModuleType(manufacturer=manufacturers[0], model='Module Type 3'),
  881. )
  882. ModuleType.objects.bulk_create(module_types)
  883. cls.create_data = [
  884. {
  885. 'manufacturer': manufacturers[1].pk,
  886. 'model': 'Module Type 4',
  887. },
  888. {
  889. 'manufacturer': manufacturers[1].pk,
  890. 'model': 'Module Type 5',
  891. },
  892. {
  893. 'manufacturer': manufacturers[1].pk,
  894. 'model': 'Module Type 6',
  895. },
  896. ]
  897. class ModuleTypeProfileTestCase(APIViewTestCases.APIViewTestCase):
  898. model = ModuleTypeProfile
  899. brief_fields = ['description', 'display', 'id', 'name', 'url']
  900. SCHEMAS = [
  901. {
  902. "properties": {
  903. "foo": {
  904. "type": "string"
  905. }
  906. }
  907. },
  908. {
  909. "properties": {
  910. "foo": {
  911. "type": "integer"
  912. }
  913. }
  914. },
  915. {
  916. "properties": {
  917. "foo": {
  918. "type": "boolean"
  919. }
  920. }
  921. },
  922. ]
  923. create_data = [
  924. {
  925. 'name': 'Module Type Profile 4',
  926. 'schema': SCHEMAS[0],
  927. },
  928. {
  929. 'name': 'Module Type Profile 5',
  930. 'schema': SCHEMAS[1],
  931. },
  932. {
  933. 'name': 'Module Type Profile 6',
  934. 'schema': SCHEMAS[2],
  935. },
  936. ]
  937. bulk_update_data = {
  938. 'description': 'New description',
  939. 'comments': 'New comments',
  940. }
  941. @classmethod
  942. def setUpTestData(cls):
  943. module_type_profiles = (
  944. ModuleTypeProfile(
  945. name='Module Type Profile 1',
  946. schema=cls.SCHEMAS[0]
  947. ),
  948. ModuleTypeProfile(
  949. name='Module Type Profile 2',
  950. schema=cls.SCHEMAS[1]
  951. ),
  952. ModuleTypeProfile(
  953. name='Module Type Profile 3',
  954. schema=cls.SCHEMAS[2]
  955. ),
  956. )
  957. ModuleTypeProfile.objects.bulk_create(module_type_profiles)
  958. class ConsolePortTemplateTestCase(APIViewTestCases.APIViewTestCase):
  959. model = ConsolePortTemplate
  960. brief_fields = ['description', 'display', 'id', 'name', 'url']
  961. bulk_update_data = {
  962. 'description': 'New description',
  963. }
  964. @classmethod
  965. def setUpTestData(cls):
  966. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  967. devicetype = DeviceType.objects.create(
  968. manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
  969. )
  970. moduletype = ModuleType.objects.create(
  971. manufacturer=manufacturer, model='Module Type 1'
  972. )
  973. console_port_templates = (
  974. ConsolePortTemplate(device_type=devicetype, name='Console Port Template 1'),
  975. ConsolePortTemplate(device_type=devicetype, name='Console Port Template 2'),
  976. ConsolePortTemplate(device_type=devicetype, name='Console Port Template 3'),
  977. )
  978. ConsolePortTemplate.objects.bulk_create(console_port_templates)
  979. cls.create_data = [
  980. {
  981. 'device_type': devicetype.pk,
  982. 'name': 'Console Port Template 4',
  983. },
  984. {
  985. 'device_type': devicetype.pk,
  986. 'name': 'Console Port Template 5',
  987. },
  988. {
  989. 'module_type': moduletype.pk,
  990. 'name': 'Console Port Template 6',
  991. },
  992. {
  993. 'module_type': moduletype.pk,
  994. 'name': 'Console Port Template 7',
  995. },
  996. ]
  997. class ConsoleServerPortTemplateTestCase(APIViewTestCases.APIViewTestCase):
  998. model = ConsoleServerPortTemplate
  999. brief_fields = ['description', 'display', 'id', 'name', 'url']
  1000. bulk_update_data = {
  1001. 'description': 'New description',
  1002. }
  1003. @classmethod
  1004. def setUpTestData(cls):
  1005. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  1006. devicetype = DeviceType.objects.create(
  1007. manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
  1008. )
  1009. moduletype = ModuleType.objects.create(
  1010. manufacturer=manufacturer, model='Module Type 1'
  1011. )
  1012. console_server_port_templates = (
  1013. ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port Template 1'),
  1014. ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port Template 2'),
  1015. ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port Template 3'),
  1016. )
  1017. ConsoleServerPortTemplate.objects.bulk_create(console_server_port_templates)
  1018. cls.create_data = [
  1019. {
  1020. 'device_type': devicetype.pk,
  1021. 'name': 'Console Server Port Template 4',
  1022. },
  1023. {
  1024. 'device_type': devicetype.pk,
  1025. 'name': 'Console Server Port Template 5',
  1026. },
  1027. {
  1028. 'module_type': moduletype.pk,
  1029. 'name': 'Console Server Port Template 6',
  1030. },
  1031. {
  1032. 'module_type': moduletype.pk,
  1033. 'name': 'Console Server Port Template 7',
  1034. },
  1035. ]
  1036. class PowerPortTemplateTestCase(APIViewTestCases.APIViewTestCase):
  1037. model = PowerPortTemplate
  1038. brief_fields = ['description', 'display', 'id', 'name', 'url']
  1039. bulk_update_data = {
  1040. 'description': 'New description',
  1041. }
  1042. @classmethod
  1043. def setUpTestData(cls):
  1044. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  1045. devicetype = DeviceType.objects.create(
  1046. manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
  1047. )
  1048. moduletype = ModuleType.objects.create(
  1049. manufacturer=manufacturer, model='Module Type 1'
  1050. )
  1051. power_port_templates = (
  1052. PowerPortTemplate(device_type=devicetype, name='Power Port Template 1'),
  1053. PowerPortTemplate(device_type=devicetype, name='Power Port Template 2'),
  1054. PowerPortTemplate(device_type=devicetype, name='Power Port Template 3'),
  1055. )
  1056. PowerPortTemplate.objects.bulk_create(power_port_templates)
  1057. cls.create_data = [
  1058. {
  1059. 'device_type': devicetype.pk,
  1060. 'name': 'Power Port Template 4',
  1061. },
  1062. {
  1063. 'device_type': devicetype.pk,
  1064. 'name': 'Power Port Template 5',
  1065. },
  1066. {
  1067. 'module_type': moduletype.pk,
  1068. 'name': 'Power Port Template 6',
  1069. },
  1070. {
  1071. 'module_type': moduletype.pk,
  1072. 'name': 'Power Port Template 7',
  1073. },
  1074. ]
  1075. class PowerOutletTemplateTestCase(APIViewTestCases.APIViewTestCase):
  1076. model = PowerOutletTemplate
  1077. brief_fields = ['description', 'display', 'id', 'name', 'url']
  1078. bulk_update_data = {
  1079. 'description': 'New description',
  1080. }
  1081. user_permissions = ('dcim.view_devicetype', )
  1082. @classmethod
  1083. def setUpTestData(cls):
  1084. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  1085. devicetype = DeviceType.objects.create(
  1086. manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
  1087. )
  1088. moduletype = ModuleType.objects.create(
  1089. manufacturer=manufacturer, model='Module Type 1'
  1090. )
  1091. power_port_templates = (
  1092. PowerPortTemplate(device_type=devicetype, name='Power Port Template 1'),
  1093. PowerPortTemplate(device_type=devicetype, name='Power Port Template 2'),
  1094. )
  1095. PowerPortTemplate.objects.bulk_create(power_port_templates)
  1096. power_outlet_templates = (
  1097. PowerOutletTemplate(device_type=devicetype, name='Power Outlet Template 1'),
  1098. PowerOutletTemplate(device_type=devicetype, name='Power Outlet Template 2'),
  1099. PowerOutletTemplate(device_type=devicetype, name='Power Outlet Template 3'),
  1100. )
  1101. PowerOutletTemplate.objects.bulk_create(power_outlet_templates)
  1102. cls.create_data = [
  1103. {
  1104. 'device_type': devicetype.pk,
  1105. 'name': 'Power Outlet Template 4',
  1106. 'power_port': power_port_templates[0].pk,
  1107. },
  1108. {
  1109. 'device_type': devicetype.pk,
  1110. 'name': 'Power Outlet Template 5',
  1111. 'power_port': power_port_templates[1].pk,
  1112. },
  1113. {
  1114. 'device_type': devicetype.pk,
  1115. 'name': 'Power Outlet Template 6',
  1116. 'power_port': None,
  1117. },
  1118. {
  1119. 'module_type': moduletype.pk,
  1120. 'name': 'Power Outlet Template 7',
  1121. },
  1122. {
  1123. 'module_type': moduletype.pk,
  1124. 'name': 'Power Outlet Template 8',
  1125. },
  1126. ]
  1127. class InterfaceTemplateTestCase(APIViewTestCases.APIViewTestCase):
  1128. model = InterfaceTemplate
  1129. brief_fields = ['description', 'display', 'id', 'name', 'url']
  1130. bulk_update_data = {
  1131. 'description': 'New description',
  1132. }
  1133. @classmethod
  1134. def setUpTestData(cls):
  1135. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  1136. devicetype = DeviceType.objects.create(
  1137. manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
  1138. )
  1139. moduletype = ModuleType.objects.create(
  1140. manufacturer=manufacturer, model='Module Type 1'
  1141. )
  1142. interface_templates = (
  1143. InterfaceTemplate(device_type=devicetype, name='Interface Template 1', type='1000base-t'),
  1144. InterfaceTemplate(device_type=devicetype, name='Interface Template 2', type='1000base-t'),
  1145. InterfaceTemplate(device_type=devicetype, name='Interface Template 3', type='1000base-t'),
  1146. )
  1147. InterfaceTemplate.objects.bulk_create(interface_templates)
  1148. cls.create_data = [
  1149. {
  1150. 'device_type': devicetype.pk,
  1151. 'name': 'Interface Template 4',
  1152. 'type': '1000base-t',
  1153. },
  1154. {
  1155. 'device_type': devicetype.pk,
  1156. 'name': 'Interface Template 5',
  1157. 'type': '1000base-t',
  1158. },
  1159. {
  1160. 'module_type': moduletype.pk,
  1161. 'name': 'Interface Template 6',
  1162. 'type': '1000base-t',
  1163. },
  1164. {
  1165. 'module_type': moduletype.pk,
  1166. 'name': 'Interface Template 7',
  1167. 'type': '1000base-t',
  1168. },
  1169. ]
  1170. class FrontPortTemplateTestCase(APIViewTestCases.APIViewTestCase):
  1171. model = FrontPortTemplate
  1172. brief_fields = ['description', 'display', 'id', 'name', 'url']
  1173. bulk_update_data = {
  1174. 'description': 'New description',
  1175. }
  1176. user_permissions = ('dcim.view_rearporttemplate', )
  1177. @classmethod
  1178. def setUpTestData(cls):
  1179. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  1180. devicetype = DeviceType.objects.create(
  1181. manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
  1182. )
  1183. moduletype = ModuleType.objects.create(
  1184. manufacturer=manufacturer, model='Module Type 1'
  1185. )
  1186. rear_port_templates = (
  1187. RearPortTemplate(device_type=devicetype, name='Rear Port Template 1', type=PortTypeChoices.TYPE_8P8C),
  1188. RearPortTemplate(device_type=devicetype, name='Rear Port Template 2', type=PortTypeChoices.TYPE_8P8C),
  1189. RearPortTemplate(device_type=devicetype, name='Rear Port Template 3', type=PortTypeChoices.TYPE_8P8C),
  1190. RearPortTemplate(device_type=devicetype, name='Rear Port Template 4', type=PortTypeChoices.TYPE_8P8C),
  1191. RearPortTemplate(device_type=devicetype, name='Rear Port Template 5', type=PortTypeChoices.TYPE_8P8C),
  1192. RearPortTemplate(device_type=devicetype, name='Rear Port Template 6', type=PortTypeChoices.TYPE_8P8C),
  1193. )
  1194. RearPortTemplate.objects.bulk_create(rear_port_templates)
  1195. front_port_templates = (
  1196. FrontPortTemplate(device_type=devicetype, name='Front Port Template 1', type=PortTypeChoices.TYPE_8P8C),
  1197. FrontPortTemplate(device_type=devicetype, name='Front Port Template 2', type=PortTypeChoices.TYPE_8P8C),
  1198. FrontPortTemplate(module_type=moduletype, name='Front Port Template 3', type=PortTypeChoices.TYPE_8P8C),
  1199. )
  1200. FrontPortTemplate.objects.bulk_create(front_port_templates)
  1201. PortTemplateMapping.objects.bulk_create([
  1202. PortTemplateMapping(
  1203. device_type=devicetype,
  1204. front_port=front_port_templates[0],
  1205. rear_port=rear_port_templates[0],
  1206. ),
  1207. PortTemplateMapping(
  1208. device_type=devicetype,
  1209. front_port=front_port_templates[1],
  1210. rear_port=rear_port_templates[1],
  1211. ),
  1212. PortTemplateMapping(
  1213. module_type=moduletype,
  1214. front_port=front_port_templates[2],
  1215. rear_port=rear_port_templates[2],
  1216. ),
  1217. ])
  1218. cls.create_data = [
  1219. {
  1220. 'device_type': devicetype.pk,
  1221. 'name': 'Front Port Template 3',
  1222. 'type': PortTypeChoices.TYPE_8P8C,
  1223. 'rear_ports': [
  1224. {
  1225. 'position': 1,
  1226. 'rear_port': rear_port_templates[3].pk,
  1227. 'rear_port_position': 1,
  1228. },
  1229. ],
  1230. },
  1231. {
  1232. 'device_type': devicetype.pk,
  1233. 'name': 'Front Port Template 4',
  1234. 'type': PortTypeChoices.TYPE_8P8C,
  1235. 'rear_ports': [
  1236. {
  1237. 'position': 1,
  1238. 'rear_port': rear_port_templates[4].pk,
  1239. 'rear_port_position': 1,
  1240. },
  1241. ],
  1242. },
  1243. {
  1244. 'module_type': moduletype.pk,
  1245. 'name': 'Front Port Template 7',
  1246. 'type': PortTypeChoices.TYPE_8P8C,
  1247. 'rear_ports': [
  1248. {
  1249. 'position': 1,
  1250. 'rear_port': rear_port_templates[5].pk,
  1251. 'rear_port_position': 1,
  1252. },
  1253. ],
  1254. },
  1255. ]
  1256. cls.update_data = {
  1257. 'type': PortTypeChoices.TYPE_LC,
  1258. 'rear_ports': [
  1259. {
  1260. 'position': 1,
  1261. 'rear_port': rear_port_templates[3].pk,
  1262. 'rear_port_position': 1,
  1263. },
  1264. ],
  1265. }
  1266. def test_update_object(self):
  1267. super().test_update_object()
  1268. # Check that the port mapping was updated after modifying the front port template
  1269. front_port_template = FrontPortTemplate.objects.get(name='Front Port Template 1')
  1270. rear_port_template = RearPortTemplate.objects.get(name='Rear Port Template 4')
  1271. self.assertTrue(
  1272. PortTemplateMapping.objects.filter(
  1273. front_port=front_port_template,
  1274. front_port_position=1,
  1275. rear_port=rear_port_template,
  1276. rear_port_position=1,
  1277. ).exists()
  1278. )
  1279. class RearPortTemplateTestCase(APIViewTestCases.APIViewTestCase):
  1280. model = RearPortTemplate
  1281. brief_fields = ['description', 'display', 'id', 'name', 'url']
  1282. bulk_update_data = {
  1283. 'description': 'New description',
  1284. }
  1285. @classmethod
  1286. def setUpTestData(cls):
  1287. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  1288. devicetype = DeviceType.objects.create(
  1289. manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
  1290. )
  1291. moduletype = ModuleType.objects.create(
  1292. manufacturer=manufacturer, model='Module Type 1'
  1293. )
  1294. front_port_templates = (
  1295. FrontPortTemplate(device_type=devicetype, name='Front Port Template 1', type=PortTypeChoices.TYPE_8P8C),
  1296. FrontPortTemplate(device_type=devicetype, name='Front Port Template 2', type=PortTypeChoices.TYPE_8P8C),
  1297. FrontPortTemplate(module_type=moduletype, name='Front Port Template 3', type=PortTypeChoices.TYPE_8P8C),
  1298. FrontPortTemplate(module_type=moduletype, name='Front Port Template 4', type=PortTypeChoices.TYPE_8P8C),
  1299. FrontPortTemplate(module_type=moduletype, name='Front Port Template 5', type=PortTypeChoices.TYPE_8P8C),
  1300. FrontPortTemplate(module_type=moduletype, name='Front Port Template 6', type=PortTypeChoices.TYPE_8P8C),
  1301. )
  1302. FrontPortTemplate.objects.bulk_create(front_port_templates)
  1303. rear_port_templates = (
  1304. RearPortTemplate(device_type=devicetype, name='Rear Port Template 1', type=PortTypeChoices.TYPE_8P8C),
  1305. RearPortTemplate(device_type=devicetype, name='Rear Port Template 2', type=PortTypeChoices.TYPE_8P8C),
  1306. RearPortTemplate(device_type=devicetype, name='Rear Port Template 3', type=PortTypeChoices.TYPE_8P8C),
  1307. )
  1308. RearPortTemplate.objects.bulk_create(rear_port_templates)
  1309. PortTemplateMapping.objects.bulk_create([
  1310. PortTemplateMapping(
  1311. device_type=devicetype,
  1312. front_port=front_port_templates[0],
  1313. rear_port=rear_port_templates[0],
  1314. ),
  1315. PortTemplateMapping(
  1316. device_type=devicetype,
  1317. front_port=front_port_templates[1],
  1318. rear_port=rear_port_templates[1],
  1319. ),
  1320. PortTemplateMapping(
  1321. module_type=moduletype,
  1322. front_port=front_port_templates[2],
  1323. rear_port=rear_port_templates[2],
  1324. ),
  1325. ])
  1326. cls.create_data = [
  1327. {
  1328. 'device_type': devicetype.pk,
  1329. 'name': 'Rear Port Template 4',
  1330. 'type': PortTypeChoices.TYPE_8P8C,
  1331. 'front_ports': [
  1332. {
  1333. 'position': 1,
  1334. 'front_port': front_port_templates[3].pk,
  1335. 'front_port_position': 1,
  1336. },
  1337. ],
  1338. },
  1339. {
  1340. 'device_type': devicetype.pk,
  1341. 'name': 'Rear Port Template 5',
  1342. 'type': PortTypeChoices.TYPE_8P8C,
  1343. 'front_ports': [
  1344. {
  1345. 'position': 1,
  1346. 'front_port': front_port_templates[4].pk,
  1347. 'front_port_position': 1,
  1348. },
  1349. ],
  1350. },
  1351. {
  1352. 'module_type': moduletype.pk,
  1353. 'name': 'Rear Port Template 6',
  1354. 'type': PortTypeChoices.TYPE_8P8C,
  1355. 'front_ports': [
  1356. {
  1357. 'position': 1,
  1358. 'front_port': front_port_templates[5].pk,
  1359. 'front_port_position': 1,
  1360. },
  1361. ],
  1362. },
  1363. ]
  1364. cls.update_data = {
  1365. 'type': PortTypeChoices.TYPE_LC,
  1366. 'front_ports': [
  1367. {
  1368. 'position': 1,
  1369. 'front_port': front_port_templates[3].pk,
  1370. 'front_port_position': 1,
  1371. },
  1372. ],
  1373. }
  1374. def test_update_object(self):
  1375. super().test_update_object()
  1376. # Check that the port mapping was updated after modifying the rear port template
  1377. front_port_template = FrontPortTemplate.objects.get(name='Front Port Template 4')
  1378. rear_port_template = RearPortTemplate.objects.get(name='Rear Port Template 1')
  1379. self.assertTrue(
  1380. PortTemplateMapping.objects.filter(
  1381. front_port=front_port_template,
  1382. front_port_position=1,
  1383. rear_port=rear_port_template,
  1384. rear_port_position=1,
  1385. ).exists()
  1386. )
  1387. class ModuleBayTemplateTestCase(APIViewTestCases.APIViewTestCase):
  1388. model = ModuleBayTemplate
  1389. brief_fields = ['description', 'display', 'enabled', 'id', 'name', 'url']
  1390. bulk_update_data = {
  1391. 'description': 'New description',
  1392. }
  1393. user_permissions = ('dcim.view_devicetype', )
  1394. @classmethod
  1395. def setUpTestData(cls):
  1396. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  1397. devicetype = DeviceType.objects.create(
  1398. manufacturer=manufacturer,
  1399. model='Device Type 1',
  1400. slug='device-type-1',
  1401. subdevice_role=SubdeviceRoleChoices.ROLE_PARENT
  1402. )
  1403. module_bay_templates = (
  1404. ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 1', enabled=True),
  1405. ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 2', enabled=False),
  1406. ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 3', enabled=True),
  1407. )
  1408. ModuleBayTemplate.objects.bulk_create(module_bay_templates)
  1409. cls.create_data = [
  1410. {
  1411. 'device_type': devicetype.pk,
  1412. 'name': 'Module Bay Template 4',
  1413. 'enabled': False,
  1414. },
  1415. {
  1416. 'device_type': devicetype.pk,
  1417. 'name': 'Module Bay Template 5',
  1418. },
  1419. {
  1420. 'device_type': devicetype.pk,
  1421. 'name': 'Module Bay Template 6',
  1422. },
  1423. ]
  1424. class DeviceBayTemplateTestCase(APIViewTestCases.APIViewTestCase):
  1425. model = DeviceBayTemplate
  1426. brief_fields = ['description', 'display', 'enabled', 'id', 'name', 'url']
  1427. bulk_update_data = {
  1428. 'description': 'New description',
  1429. }
  1430. user_permissions = ('dcim.view_devicetype', )
  1431. @classmethod
  1432. def setUpTestData(cls):
  1433. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  1434. devicetype = DeviceType.objects.create(
  1435. manufacturer=manufacturer,
  1436. model='Device Type 1',
  1437. slug='device-type-1',
  1438. subdevice_role=SubdeviceRoleChoices.ROLE_PARENT
  1439. )
  1440. device_bay_templates = (
  1441. DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 1', enabled=True),
  1442. DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 2', enabled=False),
  1443. DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 3', enabled=True),
  1444. )
  1445. DeviceBayTemplate.objects.bulk_create(device_bay_templates)
  1446. cls.create_data = [
  1447. {
  1448. 'device_type': devicetype.pk,
  1449. 'name': 'Device Bay Template 4',
  1450. 'enabled': False,
  1451. },
  1452. {
  1453. 'device_type': devicetype.pk,
  1454. 'name': 'Device Bay Template 5',
  1455. },
  1456. {
  1457. 'device_type': devicetype.pk,
  1458. 'name': 'Device Bay Template 6',
  1459. },
  1460. ]
  1461. class InventoryItemTemplateTestCase(APIViewTestCases.APIViewTestCase):
  1462. model = InventoryItemTemplate
  1463. brief_fields = ['_depth', 'description', 'display', 'id', 'name', 'url']
  1464. bulk_update_data = {
  1465. 'description': 'New description',
  1466. }
  1467. user_permissions = ('dcim.view_devicetype', 'dcim.view_manufacturer',)
  1468. @classmethod
  1469. def setUpTestData(cls):
  1470. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  1471. devicetype = DeviceType.objects.create(
  1472. manufacturer=manufacturer,
  1473. model='Device Type 1',
  1474. slug='device-type-1'
  1475. )
  1476. role = InventoryItemRole.objects.create(name='Inventory Item Role 1', slug='inventory-item-role-1')
  1477. inventory_item_templates = (
  1478. InventoryItemTemplate(
  1479. device_type=devicetype, name='Inventory Item Template 1', manufacturer=manufacturer, role=role
  1480. ),
  1481. InventoryItemTemplate(
  1482. device_type=devicetype, name='Inventory Item Template 2', manufacturer=manufacturer, role=role
  1483. ),
  1484. InventoryItemTemplate(
  1485. device_type=devicetype, name='Inventory Item Template 3', manufacturer=manufacturer, role=role
  1486. ),
  1487. InventoryItemTemplate(
  1488. device_type=devicetype, name='Inventory Item Template 4', manufacturer=manufacturer, role=role
  1489. ),
  1490. )
  1491. for item in inventory_item_templates:
  1492. item.save()
  1493. cls.create_data = [
  1494. {
  1495. 'device_type': devicetype.pk,
  1496. 'name': 'Inventory Item Template 5',
  1497. 'manufacturer': manufacturer.pk,
  1498. 'role': role.pk,
  1499. 'parent': inventory_item_templates[3].pk,
  1500. },
  1501. {
  1502. 'device_type': devicetype.pk,
  1503. 'name': 'Inventory Item Template 6',
  1504. 'manufacturer': manufacturer.pk,
  1505. 'role': role.pk,
  1506. 'parent': inventory_item_templates[3].pk,
  1507. },
  1508. {
  1509. 'device_type': devicetype.pk,
  1510. 'name': 'Inventory Item Template 7',
  1511. 'manufacturer': manufacturer.pk,
  1512. 'role': role.pk,
  1513. 'parent': inventory_item_templates[3].pk,
  1514. },
  1515. ]
  1516. class DeviceRoleTestCase(APIViewTestCases.APIViewTestCase):
  1517. model = DeviceRole
  1518. brief_fields = [
  1519. '_depth', 'description', 'device_count', 'display', 'id', 'name', 'slug', 'url', 'virtualmachine_count'
  1520. ]
  1521. create_data = [
  1522. {
  1523. 'name': 'Device Role 4',
  1524. 'slug': 'device-role-4',
  1525. 'color': 'ffff00',
  1526. },
  1527. {
  1528. 'name': 'Device Role 5',
  1529. 'slug': 'device-role-5',
  1530. 'color': 'ffff00',
  1531. },
  1532. {
  1533. 'name': 'Device Role 6',
  1534. 'slug': 'device-role-6',
  1535. 'color': 'ffff00',
  1536. },
  1537. ]
  1538. bulk_update_data = {
  1539. 'description': 'New description',
  1540. }
  1541. @classmethod
  1542. def setUpTestData(cls):
  1543. DeviceRole.objects.create(name='Device Role 1', slug='device-role-1', color='ff0000')
  1544. DeviceRole.objects.create(name='Device Role 2', slug='device-role-2', color='00ff00')
  1545. DeviceRole.objects.create(name='Device Role 3', slug='device-role-3', color='0000ff')
  1546. class PlatformTestCase(APIViewTestCases.APIViewTestCase):
  1547. model = Platform
  1548. brief_fields = [
  1549. '_depth', 'description', 'device_count', 'display', 'id', 'name', 'slug', 'url', 'virtualmachine_count',
  1550. ]
  1551. create_data = [
  1552. {
  1553. 'name': 'Platform 4',
  1554. 'slug': 'platform-4',
  1555. },
  1556. {
  1557. 'name': 'Platform 5',
  1558. 'slug': 'platform-5',
  1559. },
  1560. {
  1561. 'name': 'Platform 6',
  1562. 'slug': 'platform-6',
  1563. },
  1564. ]
  1565. bulk_update_data = {
  1566. 'description': 'New description',
  1567. }
  1568. @classmethod
  1569. def setUpTestData(cls):
  1570. platforms = (
  1571. Platform(name='Platform 1', slug='platform-1'),
  1572. Platform(name='Platform 2', slug='platform-2'),
  1573. Platform(name='Platform 3', slug='platform-3'),
  1574. )
  1575. for platform in platforms:
  1576. platform.save()
  1577. class DeviceTestCase(APIViewTestCases.APIViewTestCase):
  1578. model = Device
  1579. brief_fields = ['description', 'display', 'id', 'name', 'url']
  1580. bulk_update_data = {
  1581. 'status': 'failed',
  1582. }
  1583. user_permissions = (
  1584. 'dcim.view_site', 'dcim.view_rack', 'dcim.view_location', 'dcim.view_devicerole', 'dcim.view_devicetype',
  1585. )
  1586. @classmethod
  1587. def setUpTestData(cls):
  1588. sites = (
  1589. Site(name='Site 1', slug='site-1'),
  1590. Site(name='Site 2', slug='site-2'),
  1591. )
  1592. Site.objects.bulk_create(sites)
  1593. racks = (
  1594. Rack(name='Rack 1', site=sites[0]),
  1595. Rack(name='Rack 2', site=sites[1]),
  1596. )
  1597. Rack.objects.bulk_create(racks)
  1598. manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
  1599. device_types = (
  1600. DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
  1601. DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2', u_height=2),
  1602. )
  1603. DeviceType.objects.bulk_create(device_types)
  1604. roles = (
  1605. DeviceRole(name='Device Role 1', slug='device-role-1', color='ff0000'),
  1606. DeviceRole(name='Device Role 2', slug='device-role-2', color='00ff00'),
  1607. )
  1608. for role in roles:
  1609. role.save()
  1610. cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
  1611. clusters = (
  1612. Cluster(name='Cluster 1', type=cluster_type),
  1613. Cluster(name='Cluster 2', type=cluster_type),
  1614. )
  1615. Cluster.objects.bulk_create(clusters)
  1616. devices = (
  1617. Device(
  1618. device_type=device_types[0],
  1619. role=roles[0],
  1620. name='Device 1',
  1621. site=sites[0],
  1622. rack=racks[0],
  1623. cluster=clusters[0],
  1624. local_context_data={'A': 1}
  1625. ),
  1626. Device(
  1627. device_type=device_types[0],
  1628. role=roles[0],
  1629. name='Device 2',
  1630. site=sites[0],
  1631. rack=racks[0],
  1632. cluster=clusters[0],
  1633. local_context_data={'B': 2}
  1634. ),
  1635. Device(
  1636. device_type=device_types[0],
  1637. role=roles[0],
  1638. name='Device 3',
  1639. site=sites[0],
  1640. rack=racks[0],
  1641. cluster=clusters[0],
  1642. local_context_data={'C': 3}
  1643. ),
  1644. )
  1645. Device.objects.bulk_create(devices)
  1646. cls.create_data = [
  1647. {
  1648. 'device_type': device_types[1].pk,
  1649. 'role': roles[1].pk,
  1650. 'name': 'Test Device 4',
  1651. 'site': sites[1].pk,
  1652. 'rack': racks[1].pk,
  1653. 'cluster': clusters[1].pk,
  1654. },
  1655. {
  1656. 'device_type': device_types[1].pk,
  1657. 'role': roles[1].pk,
  1658. 'name': 'Test Device 5',
  1659. 'site': sites[1].pk,
  1660. 'rack': racks[1].pk,
  1661. 'cluster': clusters[1].pk,
  1662. },
  1663. {
  1664. 'device_type': device_types[1].pk,
  1665. 'role': roles[1].pk,
  1666. 'name': 'Test Device 6',
  1667. 'site': sites[1].pk,
  1668. 'rack': racks[1].pk,
  1669. 'cluster': clusters[1].pk,
  1670. },
  1671. ]
  1672. def test_config_context_included_by_default_in_list_view(self):
  1673. """
  1674. Check that config context data is included by default in the devices list.
  1675. """
  1676. self.add_permissions('dcim.view_device')
  1677. url = reverse('dcim-api:device-list') + '?slug=device-with-context-data'
  1678. response = self.client.get(url, **self.header)
  1679. self.assertEqual(response.data['results'][0].get('config_context', {}).get('A'), 1)
  1680. def test_config_context_excluded(self):
  1681. """
  1682. Check that config context data can be excluded by passing ?exclude=config_context.
  1683. """
  1684. self.add_permissions('dcim.view_device')
  1685. url = reverse('dcim-api:device-list') + '?exclude=config_context'
  1686. response = self.client.get(url, **self.header)
  1687. self.assertFalse('config_context' in response.data['results'][0])
  1688. def test_unique_name_per_site_constraint(self):
  1689. """
  1690. Check that creating a device with a duplicate name within a site fails.
  1691. """
  1692. device = Device.objects.first()
  1693. data = {
  1694. 'device_type': device.device_type.pk,
  1695. 'role': device.role.pk,
  1696. 'site': device.site.pk,
  1697. 'name': device.name,
  1698. }
  1699. self.add_permissions('dcim.add_device')
  1700. url = reverse('dcim-api:device-list')
  1701. response = self.client.post(url, data, format='json', **self.header)
  1702. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  1703. def test_rack_fit(self):
  1704. """
  1705. Check that creating multiple devices with overlapping position fails.
  1706. """
  1707. device = Device.objects.first()
  1708. device_type = DeviceType.objects.all()[1]
  1709. data = [
  1710. {
  1711. 'device_type': device_type.pk,
  1712. 'role': device.role.pk,
  1713. 'site': device.site.pk,
  1714. 'name': 'Test Device 7',
  1715. 'rack': device.rack.pk,
  1716. 'face': 'front',
  1717. 'position': 1
  1718. },
  1719. {
  1720. 'device_type': device_type.pk,
  1721. 'role': device.role.pk,
  1722. 'site': device.site.pk,
  1723. 'name': 'Test Device 8',
  1724. 'rack': device.rack.pk,
  1725. 'face': 'front',
  1726. 'position': 2
  1727. }
  1728. ]
  1729. self.add_permissions('dcim.add_device')
  1730. url = reverse('dcim-api:device-list')
  1731. response = self.client.post(url, data, format='json', **self.header)
  1732. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  1733. def test_render_config(self):
  1734. configtemplate = ConfigTemplate.objects.create(
  1735. name='Config Template 1',
  1736. template_code='Config for device {{ device.name }}'
  1737. )
  1738. device = Device.objects.first()
  1739. device.config_template = configtemplate
  1740. device.save()
  1741. self.add_permissions('dcim.render_config_device', 'dcim.view_device')
  1742. url = reverse('dcim-api:device-render-config', kwargs={'pk': device.pk})
  1743. response = self.client.post(url, {}, format='json', **self.header)
  1744. self.assertHttpStatus(response, status.HTTP_200_OK)
  1745. self.assertEqual(response.data['content'], f'Config for device {device.name}')
  1746. def test_render_config_without_permission(self):
  1747. configtemplate = ConfigTemplate.objects.create(
  1748. name='Config Template 1',
  1749. template_code='Config for device {{ device.name }}'
  1750. )
  1751. device = Device.objects.first()
  1752. device.config_template = configtemplate
  1753. device.save()
  1754. # No permissions added - user has no render_config permission
  1755. url = reverse('dcim-api:device-render-config', kwargs={'pk': device.pk})
  1756. response = self.client.post(url, {}, format='json', **self.header)
  1757. self.assertHttpStatus(response, status.HTTP_404_NOT_FOUND)
  1758. def test_render_config_token_write_enabled(self):
  1759. configtemplate = ConfigTemplate.objects.create(
  1760. name='Config Template 1',
  1761. template_code='Config for device {{ device.name }}'
  1762. )
  1763. device = Device.objects.first()
  1764. device.config_template = configtemplate
  1765. device.save()
  1766. self.add_permissions('dcim.render_config_device', 'dcim.view_device')
  1767. url = reverse('dcim-api:device-render-config', kwargs={'pk': device.pk})
  1768. # Request without token auth should fail with PermissionDenied
  1769. response = self.client.post(url, {}, format='json')
  1770. self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
  1771. # Create token with write_enabled=False
  1772. token = Token.objects.create(version=2, user=self.user, write_enabled=False)
  1773. token_header = f'Bearer {TOKEN_PREFIX}{token.key}.{token.token}'
  1774. # Request with write-disabled token should fail
  1775. response = self.client.post(url, {}, format='json', HTTP_AUTHORIZATION=token_header)
  1776. self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
  1777. # Enable write and retry
  1778. token.write_enabled = True
  1779. token.save()
  1780. response = self.client.post(url, {}, format='json', HTTP_AUTHORIZATION=token_header)
  1781. self.assertHttpStatus(response, status.HTTP_200_OK)
  1782. def test_list_object_includes_nat_inside_on_primary_ip(self):
  1783. device = create_test_device('natted-device')
  1784. interface = Interface.objects.create(device=device, name='eth0', type='other')
  1785. real_ip, nat_ip = create_test_nat_ip_pair(
  1786. real_address='10.0.0.10/32',
  1787. nat_address='198.51.100.10/32',
  1788. inside_interface=interface,
  1789. )
  1790. device.primary_ip4 = nat_ip
  1791. device.save()
  1792. self.add_permissions('dcim.view_device', 'ipam.view_ipaddress')
  1793. response = self.client.get(f'{self._get_list_url()}?id={device.pk}', **self.header)
  1794. self.assertHttpStatus(response, status.HTTP_200_OK)
  1795. result = response.data['results'][0]
  1796. for field in ('primary_ip', 'primary_ip4'):
  1797. self.assertEqual(result[field]['address'], str(nat_ip.address))
  1798. self.assertEqual(result[field]['nat_inside']['address'], str(real_ip.address))
  1799. self.assertEqual(result[field]['nat_outside'], [])
  1800. def test_get_object_includes_nat_outside_on_primary_ip(self):
  1801. device = create_test_device('real-ip-device')
  1802. interface = Interface.objects.create(device=device, name='eth0', type='other')
  1803. real_ip, nat_ip = create_test_nat_ip_pair(
  1804. real_address='10.0.0.11/32',
  1805. nat_address='198.51.100.11/32',
  1806. inside_interface=interface,
  1807. )
  1808. device.primary_ip4 = real_ip
  1809. device.save()
  1810. self.add_permissions('dcim.view_device', 'ipam.view_ipaddress')
  1811. response = self.client.get(
  1812. f'{self._get_detail_url(device)}?exclude=config_context',
  1813. **self.header,
  1814. )
  1815. self.assertHttpStatus(response, status.HTTP_200_OK)
  1816. for field in ('primary_ip', 'primary_ip4'):
  1817. self.assertEqual(response.data[field]['address'], str(real_ip.address))
  1818. self.assertIsNone(response.data[field]['nat_inside'])
  1819. self.assertCountEqual(
  1820. [ip['address'] for ip in response.data[field]['nat_outside']],
  1821. [str(nat_ip.address)],
  1822. )
  1823. def test_get_object_includes_nat_on_oob_ip(self):
  1824. device = create_test_device('oob-nat-device')
  1825. interface = Interface.objects.create(device=device, name='oob0', type='other')
  1826. real_ip, nat_ip = create_test_nat_ip_pair(
  1827. real_address='10.0.0.12/32',
  1828. nat_address='198.51.100.12/32',
  1829. inside_interface=interface,
  1830. )
  1831. device.oob_ip = nat_ip
  1832. device.save()
  1833. self.add_permissions('dcim.view_device', 'ipam.view_ipaddress')
  1834. response = self.client.get(
  1835. f'{self._get_detail_url(device)}?exclude=config_context',
  1836. **self.header,
  1837. )
  1838. self.assertHttpStatus(response, status.HTTP_200_OK)
  1839. self.assertEqual(response.data['oob_ip']['address'], str(nat_ip.address))
  1840. self.assertEqual(response.data['oob_ip']['nat_inside']['address'], str(real_ip.address))
  1841. self.assertEqual(response.data['oob_ip']['nat_outside'], [])
  1842. def test_get_object_includes_dns_name_on_primary_ip(self):
  1843. device = create_test_device('dns-device')
  1844. interfaces = (
  1845. Interface.objects.create(device=device, name='eth0', type='other'),
  1846. Interface.objects.create(device=device, name='eth1', type='other'),
  1847. )
  1848. ip4 = IPAddress(address='192.0.2.10/32', dns_name='device4.example.com')
  1849. ip4.assigned_object = interfaces[0]
  1850. ip4.save()
  1851. ip6 = IPAddress(address='2001:db8::10/128', dns_name='device6.example.com')
  1852. ip6.assigned_object = interfaces[1]
  1853. ip6.save()
  1854. device.primary_ip4 = ip4
  1855. device.primary_ip6 = ip6
  1856. device.save()
  1857. self.add_permissions('dcim.view_device', 'ipam.view_ipaddress')
  1858. response = self.client.get(
  1859. f'{self._get_detail_url(device)}?exclude=config_context',
  1860. **self.header,
  1861. )
  1862. self.assertHttpStatus(response, status.HTTP_200_OK)
  1863. self.assertEqual(response.data['primary_ip4']['dns_name'], 'device4.example.com')
  1864. self.assertEqual(response.data['primary_ip6']['dns_name'], 'device6.example.com')
  1865. self.assertIn(
  1866. response.data['primary_ip']['dns_name'],
  1867. ('device4.example.com', 'device6.example.com'),
  1868. )
  1869. def test_get_object_includes_dns_name_on_oob_ip(self):
  1870. device = create_test_device('dns-oob-device')
  1871. interface = Interface.objects.create(device=device, name='oob0', type='other')
  1872. ip = IPAddress(address='192.0.2.20/32', dns_name='oob.example.com')
  1873. ip.assigned_object = interface
  1874. ip.save()
  1875. device.oob_ip = ip
  1876. device.save()
  1877. self.add_permissions('dcim.view_device', 'ipam.view_ipaddress')
  1878. response = self.client.get(
  1879. f'{self._get_detail_url(device)}?exclude=config_context',
  1880. **self.header,
  1881. )
  1882. self.assertHttpStatus(response, status.HTTP_200_OK)
  1883. self.assertEqual(response.data['oob_ip']['dns_name'], 'oob.example.com')
  1884. def test_render_config_with_config_template_id(self):
  1885. default_template = ConfigTemplate.objects.create(
  1886. name='Default Template',
  1887. template_code='Default config for {{ device.name }}'
  1888. )
  1889. override_template = ConfigTemplate.objects.create(
  1890. name='Override Template',
  1891. template_code='Override config for {{ device.name }}'
  1892. )
  1893. device = Device.objects.first()
  1894. device.config_template = default_template
  1895. device.save()
  1896. self.add_permissions('dcim.render_config_device', 'dcim.view_device', 'extras.view_configtemplate')
  1897. url = reverse('dcim-api:device-render-config', kwargs={'pk': device.pk})
  1898. # Render with override template
  1899. response = self.client.post(url, {'config_template_id': override_template.pk}, format='json', **self.header)
  1900. self.assertHttpStatus(response, status.HTTP_200_OK)
  1901. self.assertEqual(response.data['content'], f'Override config for {device.name}')
  1902. # Render with nonexistent config_template_id
  1903. response = self.client.post(url, {'config_template_id': 999999}, format='json', **self.header)
  1904. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  1905. # Render with non-integer config_template_id
  1906. response = self.client.post(url, {'config_template_id': 'abc'}, format='json', **self.header)
  1907. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  1908. # Without view_configtemplate permission, override template should not be accessible
  1909. self.remove_permissions('extras.view_configtemplate')
  1910. response = self.client.post(url, {'config_template_id': override_template.pk}, format='json', **self.header)
  1911. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  1912. class ModuleTestCase(APIViewTestCases.APIViewTestCase):
  1913. model = Module
  1914. brief_fields = ['description', 'device', 'display', 'id', 'module_bay', 'module_type', 'url']
  1915. bulk_update_data = {
  1916. 'serial': '1234ABCD',
  1917. }
  1918. user_permissions = (
  1919. 'dcim.view_modulebay', 'dcim.view_moduletype', 'dcim.view_moduletypeprofile', 'dcim.view_device'
  1920. )
  1921. @classmethod
  1922. def setUpTestData(cls):
  1923. manufacturer = Manufacturer.objects.create(name='Generic', slug='generic')
  1924. profiles = (
  1925. ModuleTypeProfile(name='Test CPU'),
  1926. ModuleTypeProfile(name='Test Hard disk'),
  1927. )
  1928. ModuleTypeProfile.objects.bulk_create(profiles)
  1929. device = create_test_device('Test Device 1')
  1930. module_types = (
  1931. ModuleType(manufacturer=manufacturer, model='Module Type 1', profile=profiles[0]),
  1932. ModuleType(manufacturer=manufacturer, model='Module Type 2', profile=profiles[1]),
  1933. ModuleType(manufacturer=manufacturer, model='Module Type 3'),
  1934. )
  1935. ModuleType.objects.bulk_create(module_types)
  1936. module_bays = (
  1937. ModuleBay(device=device, name='Module Bay 1'),
  1938. ModuleBay(device=device, name='Module Bay 2'),
  1939. ModuleBay(device=device, name='Module Bay 3'),
  1940. ModuleBay(device=device, name='Module Bay 4'),
  1941. ModuleBay(device=device, name='Module Bay 5'),
  1942. ModuleBay(device=device, name='Module Bay 6'),
  1943. )
  1944. for module_bay in module_bays:
  1945. module_bay.save()
  1946. modules = (
  1947. Module(device=device, module_bay=module_bays[0], module_type=module_types[0]),
  1948. Module(device=device, module_bay=module_bays[1], module_type=module_types[1]),
  1949. Module(device=device, module_bay=module_bays[2], module_type=module_types[2]),
  1950. )
  1951. Module.objects.bulk_create(modules)
  1952. cls.create_data = [
  1953. {
  1954. 'device': device.pk,
  1955. 'module_bay': module_bays[3].pk,
  1956. 'module_type': module_types[0].pk,
  1957. 'status': ModuleStatusChoices.STATUS_ACTIVE,
  1958. 'serial': 'ABC123',
  1959. 'asset_tag': 'Foo1',
  1960. },
  1961. {
  1962. 'device': device.pk,
  1963. 'module_bay': module_bays[4].pk,
  1964. 'module_type': module_types[1].pk,
  1965. 'status': ModuleStatusChoices.STATUS_ACTIVE,
  1966. 'serial': 'DEF456',
  1967. 'asset_tag': 'Foo2',
  1968. },
  1969. {
  1970. 'device': device.pk,
  1971. 'module_bay': module_bays[5].pk,
  1972. 'module_type': module_types[2].pk,
  1973. 'status': ModuleStatusChoices.STATUS_ACTIVE,
  1974. 'serial': 'GHI789',
  1975. 'asset_tag': 'Foo3',
  1976. },
  1977. ]
  1978. def test_replicate_components(self):
  1979. """
  1980. Installing a module with replicate_components=True (the default) should create
  1981. components from the module type's templates on the parent device.
  1982. """
  1983. self.add_permissions('dcim.add_module')
  1984. manufacturer = Manufacturer.objects.get(name='Generic')
  1985. device = create_test_device('Device for Replication Test')
  1986. module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Replication Test Module Type')
  1987. InterfaceTemplate.objects.create(module_type=module_type, name='eth0', type='1000base-t')
  1988. module_bay = ModuleBay.objects.create(device=device, name='Replication Bay')
  1989. url = reverse('dcim-api:module-list')
  1990. data = {
  1991. 'device': device.pk,
  1992. 'module_bay': module_bay.pk,
  1993. 'module_type': module_type.pk,
  1994. 'replicate_components': True,
  1995. }
  1996. response = self.client.post(url, data, format='json', **self.header)
  1997. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  1998. self.assertTrue(device.interfaces.filter(name='eth0').exists())
  1999. def test_no_replicate_components(self):
  2000. """
  2001. Installing a module with replicate_components=False should NOT create components
  2002. from the module type's templates.
  2003. """
  2004. self.add_permissions('dcim.add_module')
  2005. manufacturer = Manufacturer.objects.get(name='Generic')
  2006. device = create_test_device('Device for No Replication Test')
  2007. module_type = ModuleType.objects.create(manufacturer=manufacturer, model='No Replication Test Module Type')
  2008. InterfaceTemplate.objects.create(module_type=module_type, name='eth0', type='1000base-t')
  2009. module_bay = ModuleBay.objects.create(device=device, name='No Replication Bay')
  2010. url = reverse('dcim-api:module-list')
  2011. data = {
  2012. 'device': device.pk,
  2013. 'module_bay': module_bay.pk,
  2014. 'module_type': module_type.pk,
  2015. 'replicate_components': False,
  2016. }
  2017. response = self.client.post(url, data, format='json', **self.header)
  2018. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  2019. self.assertFalse(device.interfaces.filter(name='eth0').exists())
  2020. def test_adopt_components(self):
  2021. """
  2022. Installing a module with adopt_components=True should assign existing unattached
  2023. device components to the new module.
  2024. """
  2025. self.add_permissions('dcim.add_module')
  2026. manufacturer = Manufacturer.objects.get(name='Generic')
  2027. device = create_test_device('Device for Adopt Test')
  2028. module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Adopt Test Module Type')
  2029. InterfaceTemplate.objects.create(module_type=module_type, name='eth0', type='1000base-t')
  2030. module_bay = ModuleBay.objects.create(device=device, name='Adopt Bay')
  2031. existing_iface = Interface.objects.create(device=device, name='eth0', type='1000base-t')
  2032. url = reverse('dcim-api:module-list')
  2033. data = {
  2034. 'device': device.pk,
  2035. 'module_bay': module_bay.pk,
  2036. 'module_type': module_type.pk,
  2037. 'adopt_components': True,
  2038. 'replicate_components': False,
  2039. }
  2040. response = self.client.post(url, data, format='json', **self.header)
  2041. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  2042. existing_iface.refresh_from_db()
  2043. self.assertIsNotNone(existing_iface.module)
  2044. def test_replicate_components_conflict(self):
  2045. """
  2046. Installing a module with replicate_components=True when a component with the same name
  2047. already exists should return a validation error.
  2048. """
  2049. self.add_permissions('dcim.add_module')
  2050. manufacturer = Manufacturer.objects.get(name='Generic')
  2051. device = create_test_device('Device for Conflict Test')
  2052. module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Conflict Test Module Type')
  2053. InterfaceTemplate.objects.create(module_type=module_type, name='eth0', type='1000base-t')
  2054. module_bay = ModuleBay.objects.create(device=device, name='Conflict Bay')
  2055. Interface.objects.create(device=device, name='eth0', type='1000base-t')
  2056. url = reverse('dcim-api:module-list')
  2057. data = {
  2058. 'device': device.pk,
  2059. 'module_bay': module_bay.pk,
  2060. 'module_type': module_type.pk,
  2061. 'replicate_components': True,
  2062. }
  2063. response = self.client.post(url, data, format='json', **self.header)
  2064. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  2065. def test_adopt_components_already_owned(self):
  2066. """
  2067. Installing a module with adopt_components=True when an existing component already
  2068. belongs to another module should return a validation error.
  2069. """
  2070. self.add_permissions('dcim.add_module')
  2071. manufacturer = Manufacturer.objects.get(name='Generic')
  2072. device = create_test_device('Device for Adopt Owned Test')
  2073. owner_module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Owner Module Type')
  2074. module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Adopt Owned Test Module Type')
  2075. InterfaceTemplate.objects.create(module_type=module_type, name='eth0', type='1000base-t')
  2076. owner_bay = ModuleBay.objects.create(device=device, name='Owner Bay')
  2077. target_bay = ModuleBay.objects.create(device=device, name='Adopt Owned Bay')
  2078. # Install a module that owns the interface
  2079. owner_module = Module.objects.create(device=device, module_bay=owner_bay, module_type=owner_module_type)
  2080. Interface.objects.create(device=device, name='eth0', type='1000base-t', module=owner_module)
  2081. url = reverse('dcim-api:module-list')
  2082. data = {
  2083. 'device': device.pk,
  2084. 'module_bay': target_bay.pk,
  2085. 'module_type': module_type.pk,
  2086. 'adopt_components': True,
  2087. }
  2088. response = self.client.post(url, data, format='json', **self.header)
  2089. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  2090. def test_patch_ignores_replicate_and_adopt(self):
  2091. """
  2092. PATCH requests that include replicate_components or adopt_components should not
  2093. trigger component replication or adoption (these fields are create-only).
  2094. """
  2095. self.add_permissions('dcim.change_module')
  2096. manufacturer = Manufacturer.objects.get(name='Generic')
  2097. device = create_test_device('Device for PATCH Test')
  2098. module_type = ModuleType.objects.create(manufacturer=manufacturer, model='PATCH Test Module Type')
  2099. InterfaceTemplate.objects.create(module_type=module_type, name='eth0', type='1000base-t')
  2100. module_bay = ModuleBay.objects.create(device=device, name='PATCH Bay')
  2101. # Create the module without replication so we can verify PATCH doesn't trigger it
  2102. module = Module(device=device, module_bay=module_bay, module_type=module_type)
  2103. module._disable_replication = True
  2104. module.save()
  2105. url = reverse('dcim-api:module-detail', kwargs={'pk': module.pk})
  2106. data = {
  2107. 'replicate_components': True,
  2108. 'adopt_components': True,
  2109. 'serial': 'PATCHED',
  2110. }
  2111. response = self.client.patch(url, data, format='json', **self.header)
  2112. self.assertHttpStatus(response, status.HTTP_200_OK)
  2113. self.assertEqual(response.data['serial'], 'PATCHED')
  2114. # No interfaces should have been created by the PATCH
  2115. self.assertFalse(device.interfaces.exists())
  2116. def test_adopt_and_replicate_components(self):
  2117. """
  2118. Installing a module with both adopt_components=True and replicate_components=True
  2119. should adopt existing unowned components and create new components for templates
  2120. that have no matching existing component.
  2121. """
  2122. self.add_permissions('dcim.add_module')
  2123. manufacturer = Manufacturer.objects.get(name='Generic')
  2124. device = create_test_device('Device for Adopt+Replicate Test')
  2125. module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Adopt+Replicate Test Module Type')
  2126. InterfaceTemplate.objects.create(module_type=module_type, name='eth0', type='1000base-t')
  2127. InterfaceTemplate.objects.create(module_type=module_type, name='eth1', type='1000base-t')
  2128. module_bay = ModuleBay.objects.create(device=device, name='Adopt+Replicate Bay')
  2129. # eth0 already exists (unowned); eth1 does not
  2130. existing_iface = Interface.objects.create(device=device, name='eth0', type='1000base-t')
  2131. url = reverse('dcim-api:module-list')
  2132. data = {
  2133. 'device': device.pk,
  2134. 'module_bay': module_bay.pk,
  2135. 'module_type': module_type.pk,
  2136. 'adopt_components': True,
  2137. 'replicate_components': True,
  2138. }
  2139. response = self.client.post(url, data, format='json', **self.header)
  2140. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  2141. # eth0 should have been adopted (now owned by the new module)
  2142. existing_iface.refresh_from_db()
  2143. self.assertIsNotNone(existing_iface.module)
  2144. # eth1 should have been created
  2145. self.assertTrue(device.interfaces.filter(name='eth1').exists())
  2146. def test_module_token_no_position(self):
  2147. """
  2148. Installing a module whose type has a template with a MODULE_TOKEN placeholder into a
  2149. module bay with no position defined should return a validation error.
  2150. """
  2151. self.add_permissions('dcim.add_module')
  2152. manufacturer = Manufacturer.objects.get(name='Generic')
  2153. device = create_test_device('Device for Token No-Position Test')
  2154. module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Token No-Position Module Type')
  2155. # Template name contains the MODULE_TOKEN placeholder
  2156. InterfaceTemplate.objects.create(
  2157. module_type=module_type, name=f'{MODULE_TOKEN}-eth0', type='1000base-t'
  2158. )
  2159. # Module bay has no position
  2160. module_bay = ModuleBay.objects.create(device=device, name='No-Position Bay')
  2161. url = reverse('dcim-api:module-list')
  2162. data = {
  2163. 'device': device.pk,
  2164. 'module_bay': module_bay.pk,
  2165. 'module_type': module_type.pk,
  2166. }
  2167. response = self.client.post(url, data, format='json', **self.header)
  2168. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  2169. def test_module_token_depth_mismatch(self):
  2170. """
  2171. Installing a module whose template name has more MODULE_TOKEN placeholders than the
  2172. depth of the module bay tree should return a validation error.
  2173. """
  2174. self.add_permissions('dcim.add_module')
  2175. manufacturer = Manufacturer.objects.get(name='Generic')
  2176. device = create_test_device('Device for Token Depth Mismatch Test')
  2177. module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Token Depth Mismatch Module Type')
  2178. # Template name has two placeholders but the bay is at depth 1
  2179. InterfaceTemplate.objects.create(
  2180. module_type=module_type, name=f'{MODULE_TOKEN}-{MODULE_TOKEN}-eth0', type='1000base-t'
  2181. )
  2182. module_bay = ModuleBay.objects.create(device=device, name='Depth 1 Bay', position='1')
  2183. url = reverse('dcim-api:module-list')
  2184. data = {
  2185. 'device': device.pk,
  2186. 'module_bay': module_bay.pk,
  2187. 'module_type': module_type.pk,
  2188. }
  2189. response = self.client.post(url, data, format='json', **self.header)
  2190. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  2191. def test_list_objects_by_profile_id(self):
  2192. profiles = ModuleTypeProfile.objects.filter(name__startswith='Test').order_by('name')
  2193. self.add_permissions('dcim.view_module')
  2194. response = self.client.get(self._get_list_url(), {'profile_id': [profiles[0].pk]}, **self.header)
  2195. self.assertHttpStatus(response, status.HTTP_200_OK)
  2196. self.assertEqual(len(response.data['results']), 1)
  2197. response = self.client.get(self._get_list_url(), {'profile_id': [profiles[1].pk]}, **self.header)
  2198. self.assertHttpStatus(response, status.HTTP_200_OK)
  2199. self.assertEqual(len(response.data['results']), 1)
  2200. response = self.client.get(
  2201. self._get_list_url(),
  2202. {'profile_id': [settings.FILTERS_NULL_CHOICE_VALUE]},
  2203. **self.header,
  2204. )
  2205. self.assertHttpStatus(response, status.HTTP_200_OK)
  2206. self.assertEqual(len(response.data['results']), 1)
  2207. def test_list_objects_by_profile(self):
  2208. profiles = ModuleTypeProfile.objects.filter(name__startswith='Test').order_by('name')
  2209. self.add_permissions('dcim.view_module')
  2210. response = self.client.get(self._get_list_url(), {'profile': [profiles[0].name]}, **self.header)
  2211. self.assertHttpStatus(response, status.HTTP_200_OK)
  2212. self.assertEqual(len(response.data['results']), 1)
  2213. response = self.client.get(self._get_list_url(), {'profile': [profiles[1].name]}, **self.header)
  2214. self.assertHttpStatus(response, status.HTTP_200_OK)
  2215. self.assertEqual(len(response.data['results']), 1)
  2216. class ConsolePortTestCase(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
  2217. model = ConsolePort
  2218. brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
  2219. bulk_update_data = {
  2220. 'description': 'New description',
  2221. }
  2222. peer_termination_type = ConsoleServerPort
  2223. user_permissions = ('dcim.view_device', )
  2224. @classmethod
  2225. def setUpTestData(cls):
  2226. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  2227. devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
  2228. site = Site.objects.create(name='Site 1', slug='site-1')
  2229. role = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000')
  2230. device = Device.objects.create(device_type=devicetype, role=role, name='Device 1', site=site)
  2231. console_ports = (
  2232. ConsolePort(device=device, name='Console Port 1'),
  2233. ConsolePort(device=device, name='Console Port 2'),
  2234. ConsolePort(device=device, name='Console Port 3'),
  2235. )
  2236. ConsolePort.objects.bulk_create(console_ports)
  2237. cls.create_data = [
  2238. {
  2239. 'device': device.pk,
  2240. 'name': 'Console Port 4',
  2241. 'speed': 9600,
  2242. },
  2243. {
  2244. 'device': device.pk,
  2245. 'name': 'Console Port 5',
  2246. 'speed': 115200,
  2247. },
  2248. {
  2249. 'device': device.pk,
  2250. 'name': 'Console Port 6',
  2251. 'speed': None,
  2252. },
  2253. ]
  2254. class ConsoleServerPortTestCase(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
  2255. model = ConsoleServerPort
  2256. brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
  2257. bulk_update_data = {
  2258. 'description': 'New description',
  2259. }
  2260. peer_termination_type = ConsolePort
  2261. user_permissions = ('dcim.view_device', )
  2262. @classmethod
  2263. def setUpTestData(cls):
  2264. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  2265. devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
  2266. site = Site.objects.create(name='Site 1', slug='site-1')
  2267. role = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000')
  2268. device = Device.objects.create(device_type=devicetype, role=role, name='Device 1', site=site)
  2269. console_server_ports = (
  2270. ConsoleServerPort(device=device, name='Console Server Port 1'),
  2271. ConsoleServerPort(device=device, name='Console Server Port 2'),
  2272. ConsoleServerPort(device=device, name='Console Server Port 3'),
  2273. )
  2274. ConsoleServerPort.objects.bulk_create(console_server_ports)
  2275. cls.create_data = [
  2276. {
  2277. 'device': device.pk,
  2278. 'name': 'Console Server Port 4',
  2279. 'speed': 9600,
  2280. },
  2281. {
  2282. 'device': device.pk,
  2283. 'name': 'Console Server Port 5',
  2284. 'speed': 115200,
  2285. },
  2286. {
  2287. 'device': device.pk,
  2288. 'name': 'Console Server Port 6',
  2289. 'speed': None,
  2290. },
  2291. ]
  2292. class PowerPortTestCase(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
  2293. model = PowerPort
  2294. brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
  2295. bulk_update_data = {
  2296. 'description': 'New description',
  2297. }
  2298. peer_termination_type = PowerOutlet
  2299. user_permissions = ('dcim.view_device', )
  2300. @classmethod
  2301. def setUpTestData(cls):
  2302. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  2303. devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
  2304. site = Site.objects.create(name='Site 1', slug='site-1')
  2305. role = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000')
  2306. device = Device.objects.create(device_type=devicetype, role=role, name='Device 1', site=site)
  2307. power_ports = (
  2308. PowerPort(device=device, name='Power Port 1'),
  2309. PowerPort(device=device, name='Power Port 2'),
  2310. PowerPort(device=device, name='Power Port 3'),
  2311. )
  2312. PowerPort.objects.bulk_create(power_ports)
  2313. cls.create_data = [
  2314. {
  2315. 'device': device.pk,
  2316. 'name': 'Power Port 4',
  2317. },
  2318. {
  2319. 'device': device.pk,
  2320. 'name': 'Power Port 5',
  2321. },
  2322. {
  2323. 'device': device.pk,
  2324. 'name': 'Power Port 6',
  2325. },
  2326. ]
  2327. class PowerOutletTestCase(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
  2328. model = PowerOutlet
  2329. brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
  2330. bulk_update_data = {
  2331. 'description': 'New description',
  2332. }
  2333. peer_termination_type = PowerPort
  2334. user_permissions = ('dcim.view_device', )
  2335. @classmethod
  2336. def setUpTestData(cls):
  2337. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  2338. devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
  2339. site = Site.objects.create(name='Site 1', slug='site-1')
  2340. role = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000')
  2341. device = Device.objects.create(device_type=devicetype, role=role, name='Device 1', site=site)
  2342. power_ports = (
  2343. PowerPort(device=device, name='Power Port 1'),
  2344. PowerPort(device=device, name='Power Port 2'),
  2345. )
  2346. PowerPort.objects.bulk_create(power_ports)
  2347. power_outlets = (
  2348. PowerOutlet(device=device, name='Power Outlet 1'),
  2349. PowerOutlet(device=device, name='Power Outlet 2'),
  2350. PowerOutlet(device=device, name='Power Outlet 3'),
  2351. )
  2352. PowerOutlet.objects.bulk_create(power_outlets)
  2353. cls.create_data = [
  2354. {
  2355. 'device': device.pk,
  2356. 'name': 'Power Outlet 4',
  2357. 'power_port': power_ports[0].pk,
  2358. },
  2359. {
  2360. 'device': device.pk,
  2361. 'name': 'Power Outlet 5',
  2362. 'power_port': power_ports[1].pk,
  2363. },
  2364. {
  2365. 'device': device.pk,
  2366. 'name': 'Power Outlet 6',
  2367. 'power_port': None,
  2368. },
  2369. ]
  2370. class InterfaceTestCase(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
  2371. model = Interface
  2372. brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
  2373. bulk_update_data = {
  2374. 'description': 'New description',
  2375. }
  2376. peer_termination_type = Interface
  2377. user_permissions = ('dcim.view_device', )
  2378. @classmethod
  2379. def setUpTestData(cls):
  2380. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  2381. devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
  2382. site = Site.objects.create(name='Site 1', slug='site-1')
  2383. role = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000')
  2384. device = Device.objects.create(device_type=devicetype, role=role, name='Device 1', site=site)
  2385. interfaces = (
  2386. Interface(device=device, name='Interface 1', type='1000base-t'),
  2387. Interface(device=device, name='Interface 2', type='1000base-t'),
  2388. Interface(device=device, name='Interface 3', type='1000base-t'),
  2389. )
  2390. Interface.objects.bulk_create(interfaces)
  2391. vdcs = (
  2392. VirtualDeviceContext(name='VDC 1', identifier=1, device=device),
  2393. VirtualDeviceContext(name='VDC 2', identifier=2, device=device)
  2394. )
  2395. VirtualDeviceContext.objects.bulk_create(vdcs)
  2396. for interface in interfaces:
  2397. interface.vdcs.set(vdcs)
  2398. vlans = (
  2399. VLAN(name='VLAN 1', vid=1),
  2400. VLAN(name='VLAN 2', vid=2),
  2401. VLAN(name='VLAN 3', vid=3),
  2402. VLAN(name='SVLAN 1', vid=1001, qinq_role=VLANQinQRoleChoices.ROLE_SERVICE),
  2403. )
  2404. VLAN.objects.bulk_create(vlans)
  2405. wireless_lans = (
  2406. WirelessLAN(ssid='WLAN1'),
  2407. WirelessLAN(ssid='WLAN2'),
  2408. )
  2409. WirelessLAN.objects.bulk_create(wireless_lans)
  2410. vrfs = (
  2411. VRF(name='VRF 1'),
  2412. VRF(name='VRF 2'),
  2413. VRF(name='VRF 3'),
  2414. )
  2415. VRF.objects.bulk_create(vrfs)
  2416. cls.create_data = [
  2417. {
  2418. 'device': device.pk,
  2419. 'name': 'Interface 4',
  2420. 'type': 'other',
  2421. 'mode': InterfaceModeChoices.MODE_TAGGED,
  2422. 'speed': 16_000_000_000,
  2423. 'duplex': 'full',
  2424. 'vrf': vrfs[0].pk,
  2425. 'poe_mode': InterfacePoEModeChoices.MODE_PD,
  2426. 'poe_type': InterfacePoETypeChoices.TYPE_1_8023AF,
  2427. 'tagged_vlans': [vlans[0].pk, vlans[1].pk],
  2428. 'untagged_vlan': vlans[2].pk,
  2429. },
  2430. {
  2431. 'device': device.pk,
  2432. 'name': 'Interface 5',
  2433. 'type': '1000base-t',
  2434. 'mode': InterfaceModeChoices.MODE_TAGGED,
  2435. 'bridge': interfaces[0].pk,
  2436. 'speed': 100000,
  2437. 'duplex': 'half',
  2438. 'vrf': vrfs[1].pk,
  2439. 'tagged_vlans': [vlans[0].pk, vlans[1].pk],
  2440. 'untagged_vlan': vlans[2].pk,
  2441. },
  2442. {
  2443. 'device': device.pk,
  2444. 'vdcs': [vdcs[0].pk],
  2445. 'name': 'Interface 6',
  2446. 'type': 'virtual',
  2447. 'mode': InterfaceModeChoices.MODE_TAGGED,
  2448. 'parent': interfaces[1].pk,
  2449. 'vrf': vrfs[2].pk,
  2450. 'tagged_vlans': [vlans[0].pk, vlans[1].pk],
  2451. 'untagged_vlan': vlans[2].pk,
  2452. },
  2453. {
  2454. 'device': device.pk,
  2455. 'vdcs': [vdcs[1].pk],
  2456. 'name': 'Interface 7',
  2457. 'type': InterfaceTypeChoices.TYPE_80211A,
  2458. 'mode': InterfaceModeChoices.MODE_Q_IN_Q,
  2459. 'tx_power': 10,
  2460. 'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
  2461. 'rf_channel': WirelessChannelChoices.CHANNEL_5G_32,
  2462. 'qinq_svlan': vlans[3].pk,
  2463. },
  2464. {
  2465. 'device': device.pk,
  2466. 'vdcs': [vdcs[1].pk],
  2467. 'name': 'Interface 8',
  2468. 'type': InterfaceTypeChoices.TYPE_80211A,
  2469. 'mode': InterfaceModeChoices.MODE_Q_IN_Q,
  2470. 'tx_power': 10,
  2471. 'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
  2472. 'rf_channel': "",
  2473. 'qinq_svlan': vlans[3].pk,
  2474. },
  2475. ]
  2476. def _perform_interface_test_with_invalid_data(self, mode: str = None, invalid_data: dict = {}):
  2477. device = Device.objects.first()
  2478. data = {
  2479. 'device': device.pk,
  2480. 'name': 'Interface 1',
  2481. 'type': InterfaceTypeChoices.TYPE_1GE_FIXED,
  2482. }
  2483. data.update({'mode': mode})
  2484. data.update(invalid_data)
  2485. response = self.client.post(self._get_list_url(), data, format='json', **self.header)
  2486. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  2487. content = json.loads(response.content)
  2488. for key in invalid_data.keys():
  2489. self.assertIn(key, content)
  2490. self.assertIsNone(content.get('data'))
  2491. def test_bulk_delete_child_interfaces(self):
  2492. interface1 = Interface.objects.get(name='Interface 1')
  2493. device = interface1.device
  2494. self.add_permissions('dcim.delete_interface')
  2495. # Create a child interface
  2496. child = Interface.objects.create(
  2497. device=device,
  2498. name='Interface 1A',
  2499. type=InterfaceTypeChoices.TYPE_VIRTUAL,
  2500. parent=interface1
  2501. )
  2502. self.assertEqual(device.interfaces.count(), 4)
  2503. # Attempt to delete only the parent interface
  2504. url = self._get_detail_url(interface1)
  2505. with disable_logging():
  2506. self.client.delete(url, **self.header)
  2507. self.assertEqual(device.interfaces.count(), 4) # Parent was not deleted
  2508. # Attempt to bulk delete parent & child together
  2509. data = [
  2510. {"id": interface1.pk},
  2511. {"id": child.pk},
  2512. ]
  2513. self.client.delete(self._get_list_url(), data, format='json', **self.header)
  2514. self.assertEqual(device.interfaces.count(), 2) # Child & parent were both deleted
  2515. def test_create_child_interfaces_mode_invalid_data(self):
  2516. """
  2517. POST data to test interface mode check and invalid tagged/untagged VLANS.
  2518. """
  2519. self.add_permissions('dcim.add_interface')
  2520. vlans = VLAN.objects.all()[0:3]
  2521. # Routed mode, untagged, tagged and qinq service vlan
  2522. invalid_data = {
  2523. 'untagged_vlan': vlans[0].pk,
  2524. 'tagged_vlans': [vlans[1].pk, vlans[2].pk],
  2525. 'qinq_svlan': vlans[2].pk
  2526. }
  2527. self._perform_interface_test_with_invalid_data(None, invalid_data)
  2528. # Routed mode, untagged and tagged vlan
  2529. invalid_data = {
  2530. 'untagged_vlan': vlans[0].pk,
  2531. 'tagged_vlans': [vlans[1].pk, vlans[2].pk],
  2532. }
  2533. self._perform_interface_test_with_invalid_data(None, invalid_data)
  2534. # Routed mode, untagged vlan
  2535. invalid_data = {
  2536. 'untagged_vlan': vlans[0].pk,
  2537. }
  2538. self._perform_interface_test_with_invalid_data(None, invalid_data)
  2539. invalid_data = {
  2540. 'tagged_vlans': [vlans[1].pk, vlans[2].pk],
  2541. }
  2542. # Routed mode, qinq service vlan
  2543. self._perform_interface_test_with_invalid_data(None, invalid_data)
  2544. # Access mode, tagged vlans
  2545. self._perform_interface_test_with_invalid_data(InterfaceModeChoices.MODE_ACCESS, invalid_data)
  2546. # All tagged mode, tagged vlans
  2547. self._perform_interface_test_with_invalid_data(InterfaceModeChoices.MODE_TAGGED_ALL, invalid_data)
  2548. invalid_data = {
  2549. 'qinq_svlan': vlans[0].pk,
  2550. }
  2551. # Routed mode, qinq service vlan
  2552. self._perform_interface_test_with_invalid_data(None, invalid_data)
  2553. # Access mode, qinq service vlan
  2554. self._perform_interface_test_with_invalid_data(InterfaceModeChoices.MODE_ACCESS, invalid_data)
  2555. # Tagged mode, qinq service vlan
  2556. self._perform_interface_test_with_invalid_data(InterfaceModeChoices.MODE_TAGGED, invalid_data)
  2557. # Tagged-all mode, qinq service vlan
  2558. self._perform_interface_test_with_invalid_data(InterfaceModeChoices.MODE_TAGGED_ALL, invalid_data)
  2559. class FrontPortTestCase(APIViewTestCases.APIViewTestCase):
  2560. model = FrontPort
  2561. brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
  2562. bulk_update_data = {
  2563. 'description': 'New description',
  2564. }
  2565. peer_termination_type = Interface
  2566. user_permissions = ('dcim.view_device', 'dcim.view_rearport')
  2567. @classmethod
  2568. def setUpTestData(cls):
  2569. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  2570. devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
  2571. site = Site.objects.create(name='Site 1', slug='site-1')
  2572. role = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000')
  2573. device = Device.objects.create(device_type=devicetype, role=role, name='Device 1', site=site)
  2574. rear_ports = (
  2575. RearPort(device=device, name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C),
  2576. RearPort(device=device, name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C),
  2577. RearPort(device=device, name='Rear Port 3', type=PortTypeChoices.TYPE_8P8C),
  2578. RearPort(device=device, name='Rear Port 4', type=PortTypeChoices.TYPE_8P8C),
  2579. RearPort(device=device, name='Rear Port 5', type=PortTypeChoices.TYPE_8P8C),
  2580. RearPort(device=device, name='Rear Port 6', type=PortTypeChoices.TYPE_8P8C),
  2581. )
  2582. RearPort.objects.bulk_create(rear_ports)
  2583. front_ports = (
  2584. FrontPort(device=device, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C),
  2585. FrontPort(device=device, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C),
  2586. FrontPort(device=device, name='Front Port 3', type=PortTypeChoices.TYPE_8P8C),
  2587. )
  2588. FrontPort.objects.bulk_create(front_ports)
  2589. PortMapping.objects.bulk_create([
  2590. PortMapping(device=device, front_port=front_ports[0], rear_port=rear_ports[0]),
  2591. PortMapping(device=device, front_port=front_ports[1], rear_port=rear_ports[1]),
  2592. PortMapping(device=device, front_port=front_ports[2], rear_port=rear_ports[2]),
  2593. ])
  2594. cls.create_data = [
  2595. {
  2596. 'device': device.pk,
  2597. 'name': 'Front Port 4',
  2598. 'type': PortTypeChoices.TYPE_8P8C,
  2599. 'rear_ports': [
  2600. {
  2601. 'position': 1,
  2602. 'rear_port': rear_ports[3].pk,
  2603. 'rear_port_position': 1,
  2604. },
  2605. ],
  2606. },
  2607. {
  2608. 'device': device.pk,
  2609. 'name': 'Front Port 5',
  2610. 'type': PortTypeChoices.TYPE_8P8C,
  2611. 'rear_ports': [
  2612. {
  2613. 'position': 1,
  2614. 'rear_port': rear_ports[4].pk,
  2615. 'rear_port_position': 1,
  2616. },
  2617. ],
  2618. },
  2619. {
  2620. 'device': device.pk,
  2621. 'name': 'Front Port 6',
  2622. 'type': PortTypeChoices.TYPE_8P8C,
  2623. 'rear_ports': [
  2624. {
  2625. 'position': 1,
  2626. 'rear_port': rear_ports[5].pk,
  2627. 'rear_port_position': 1,
  2628. },
  2629. ],
  2630. },
  2631. ]
  2632. cls.update_data = {
  2633. 'type': PortTypeChoices.TYPE_LC,
  2634. 'rear_ports': [
  2635. {
  2636. 'position': 1,
  2637. 'rear_port': rear_ports[3].pk,
  2638. 'rear_port_position': 1,
  2639. },
  2640. ],
  2641. }
  2642. def test_update_object(self):
  2643. super().test_update_object()
  2644. # Check that the port mapping was updated after modifying the front port
  2645. front_port = FrontPort.objects.get(name='Front Port 1')
  2646. rear_port = RearPort.objects.get(name='Rear Port 4')
  2647. self.assertTrue(
  2648. PortMapping.objects.filter(
  2649. front_port=front_port,
  2650. front_port_position=1,
  2651. rear_port=rear_port,
  2652. rear_port_position=1,
  2653. ).exists()
  2654. )
  2655. @tag('regression') # Issue #18991
  2656. def test_front_port_paths(self):
  2657. device = Device.objects.first()
  2658. interface1 = Interface.objects.create(device=device, name='Interface 1')
  2659. rear_port = RearPort.objects.create(device=device, name='Rear Port 10', type=PortTypeChoices.TYPE_8P8C)
  2660. front_port = FrontPort.objects.create(device=device, name='Front Port 10', type=PortTypeChoices.TYPE_8P8C)
  2661. PortMapping.objects.create(device=device, front_port=front_port, rear_port=rear_port)
  2662. Cable.objects.create(a_terminations=[interface1], b_terminations=[front_port])
  2663. self.add_permissions(f'dcim.view_{self.model._meta.model_name}')
  2664. url = reverse(f'dcim-api:{self.model._meta.model_name}-paths', kwargs={'pk': front_port.pk})
  2665. response = self.client.get(url, **self.header)
  2666. self.assertHttpStatus(response, status.HTTP_200_OK)
  2667. class RearPortTestCase(APIViewTestCases.APIViewTestCase):
  2668. model = RearPort
  2669. brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
  2670. bulk_update_data = {
  2671. 'description': 'New description',
  2672. }
  2673. peer_termination_type = Interface
  2674. user_permissions = ('dcim.view_device', )
  2675. @classmethod
  2676. def setUpTestData(cls):
  2677. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  2678. devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
  2679. site = Site.objects.create(name='Site 1', slug='site-1')
  2680. role = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000')
  2681. device = Device.objects.create(device_type=devicetype, role=role, name='Device 1', site=site)
  2682. front_ports = (
  2683. FrontPort(device=device, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C),
  2684. FrontPort(device=device, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C),
  2685. FrontPort(device=device, name='Front Port 3', type=PortTypeChoices.TYPE_8P8C),
  2686. FrontPort(device=device, name='Front Port 4', type=PortTypeChoices.TYPE_8P8C),
  2687. FrontPort(device=device, name='Front Port 5', type=PortTypeChoices.TYPE_8P8C),
  2688. FrontPort(device=device, name='Front Port 6', type=PortTypeChoices.TYPE_8P8C),
  2689. )
  2690. FrontPort.objects.bulk_create(front_ports)
  2691. rear_ports = (
  2692. RearPort(device=device, name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C),
  2693. RearPort(device=device, name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C),
  2694. RearPort(device=device, name='Rear Port 3', type=PortTypeChoices.TYPE_8P8C),
  2695. )
  2696. RearPort.objects.bulk_create(rear_ports)
  2697. cls.create_data = [
  2698. {
  2699. 'device': device.pk,
  2700. 'name': 'Rear Port 4',
  2701. 'type': PortTypeChoices.TYPE_8P8C,
  2702. 'front_ports': [
  2703. {
  2704. 'position': 1,
  2705. 'front_port': front_ports[3].pk,
  2706. 'front_port_position': 1,
  2707. },
  2708. ],
  2709. },
  2710. {
  2711. 'device': device.pk,
  2712. 'name': 'Rear Port 5',
  2713. 'type': PortTypeChoices.TYPE_8P8C,
  2714. 'front_ports': [
  2715. {
  2716. 'position': 1,
  2717. 'front_port': front_ports[4].pk,
  2718. 'front_port_position': 1,
  2719. },
  2720. ],
  2721. },
  2722. {
  2723. 'device': device.pk,
  2724. 'name': 'Rear Port 6',
  2725. 'type': PortTypeChoices.TYPE_8P8C,
  2726. 'front_ports': [
  2727. {
  2728. 'position': 1,
  2729. 'front_port': front_ports[5].pk,
  2730. 'front_port_position': 1,
  2731. },
  2732. ],
  2733. },
  2734. ]
  2735. cls.update_data = {
  2736. 'type': PortTypeChoices.TYPE_LC,
  2737. 'front_ports': [
  2738. {
  2739. 'position': 1,
  2740. 'front_port': front_ports[3].pk,
  2741. 'front_port_position': 1,
  2742. },
  2743. ],
  2744. }
  2745. def test_update_object(self):
  2746. super().test_update_object()
  2747. # Check that the port mapping was updated after modifying the rear port
  2748. front_port = FrontPort.objects.get(name='Front Port 4')
  2749. rear_port = RearPort.objects.get(name='Rear Port 1')
  2750. self.assertTrue(
  2751. PortMapping.objects.filter(
  2752. front_port=front_port,
  2753. front_port_position=1,
  2754. rear_port=rear_port,
  2755. rear_port_position=1,
  2756. ).exists()
  2757. )
  2758. @tag('regression') # Issue #18991
  2759. def test_rear_port_paths(self):
  2760. device = Device.objects.first()
  2761. interface1 = Interface.objects.create(device=device, name='Interface 1')
  2762. rear_port = RearPort.objects.create(
  2763. device=device,
  2764. name='Rear Port 10',
  2765. type=PortTypeChoices.TYPE_8P8C,
  2766. )
  2767. Cable.objects.create(a_terminations=[interface1], b_terminations=[rear_port])
  2768. self.add_permissions(f'dcim.view_{self.model._meta.model_name}')
  2769. url = reverse(f'dcim-api:{self.model._meta.model_name}-paths', kwargs={'pk': rear_port.pk})
  2770. response = self.client.get(url, **self.header)
  2771. self.assertHttpStatus(response, status.HTTP_200_OK)
  2772. class ModuleBayTestCase(APIViewTestCases.APIViewTestCase):
  2773. model = ModuleBay
  2774. brief_fields = ['_occupied', 'description', 'display', 'enabled', 'id', 'installed_module', 'name', 'url']
  2775. bulk_update_data = {
  2776. 'description': 'New description',
  2777. }
  2778. user_permissions = ('dcim.view_device', )
  2779. @classmethod
  2780. def setUpTestData(cls):
  2781. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  2782. site = Site.objects.create(name='Site 1', slug='site-1')
  2783. role = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000')
  2784. device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
  2785. device = Device.objects.create(device_type=device_type, role=role, name='Device 1', site=site)
  2786. module_bays = (
  2787. ModuleBay(device=device, name='Device Bay 1', enabled=True),
  2788. ModuleBay(device=device, name='Device Bay 2', enabled=False),
  2789. ModuleBay(device=device, name='Device Bay 3', enabled=True),
  2790. )
  2791. for module_bay in module_bays:
  2792. module_bay.save()
  2793. cls.create_data = [
  2794. {
  2795. 'device': device.pk,
  2796. 'name': 'Device Bay 4',
  2797. 'enabled': False,
  2798. },
  2799. {
  2800. 'device': device.pk,
  2801. 'name': 'Device Bay 5',
  2802. },
  2803. {
  2804. 'device': device.pk,
  2805. 'name': 'Device Bay 6',
  2806. },
  2807. ]
  2808. class DeviceBayTestCase(APIViewTestCases.APIViewTestCase):
  2809. model = DeviceBay
  2810. brief_fields = ['_occupied', 'description', 'device', 'display', 'enabled', 'id', 'name', 'url']
  2811. bulk_update_data = {
  2812. 'description': 'New description',
  2813. }
  2814. user_permissions = ('dcim.view_device', )
  2815. @classmethod
  2816. def setUpTestData(cls):
  2817. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  2818. site = Site.objects.create(name='Site 1', slug='site-1')
  2819. role = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000')
  2820. device_types = (
  2821. DeviceType(
  2822. manufacturer=manufacturer,
  2823. model='Device Type 1',
  2824. slug='device-type-1',
  2825. subdevice_role=SubdeviceRoleChoices.ROLE_PARENT
  2826. ),
  2827. DeviceType(
  2828. manufacturer=manufacturer,
  2829. model='Device Type 2',
  2830. slug='device-type-2',
  2831. subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
  2832. ),
  2833. )
  2834. DeviceType.objects.bulk_create(device_types)
  2835. devices = (
  2836. Device(device_type=device_types[0], role=role, name='Device 1', site=site),
  2837. Device(device_type=device_types[1], role=role, name='Device 2', site=site),
  2838. Device(device_type=device_types[1], role=role, name='Device 3', site=site),
  2839. Device(device_type=device_types[1], role=role, name='Device 4', site=site),
  2840. )
  2841. Device.objects.bulk_create(devices)
  2842. device_bays = (
  2843. DeviceBay(device=devices[0], name='Device Bay 1', enabled=True),
  2844. DeviceBay(device=devices[0], name='Device Bay 2', enabled=False),
  2845. DeviceBay(device=devices[0], name='Device Bay 3', enabled=True),
  2846. )
  2847. DeviceBay.objects.bulk_create(device_bays)
  2848. cls.create_data = [
  2849. {
  2850. 'device': devices[0].pk,
  2851. 'name': 'Device Bay 4',
  2852. 'installed_device': devices[1].pk,
  2853. },
  2854. {
  2855. 'device': devices[0].pk,
  2856. 'name': 'Device Bay 5',
  2857. 'installed_device': devices[2].pk,
  2858. },
  2859. {
  2860. 'device': devices[0].pk,
  2861. 'name': 'Device Bay 6',
  2862. 'installed_device': devices[3].pk,
  2863. },
  2864. ]
  2865. class InventoryItemTestCase(APIViewTestCases.APIViewTestCase):
  2866. model = InventoryItem
  2867. brief_fields = ['_depth', 'description', 'device', 'display', 'id', 'name', 'url']
  2868. bulk_update_data = {
  2869. 'description': 'New description',
  2870. }
  2871. user_permissions = ('dcim.view_device', 'dcim.view_manufacturer')
  2872. @classmethod
  2873. def setUpTestData(cls):
  2874. manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
  2875. devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
  2876. site = Site.objects.create(name='Site 1', slug='site-1')
  2877. role = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000')
  2878. device = Device.objects.create(device_type=devicetype, role=role, name='Device 1', site=site)
  2879. roles = (
  2880. InventoryItemRole(name='Inventory Item Role 1', slug='inventory-item-role-1'),
  2881. InventoryItemRole(name='Inventory Item Role 2', slug='inventory-item-role-2'),
  2882. )
  2883. InventoryItemRole.objects.bulk_create(roles)
  2884. interfaces = (
  2885. Interface(device=device, name='Interface 1'),
  2886. Interface(device=device, name='Interface 2'),
  2887. Interface(device=device, name='Interface 3'),
  2888. )
  2889. Interface.objects.bulk_create(interfaces)
  2890. InventoryItem.objects.create(
  2891. device=device, name='Inventory Item 1', role=roles[0], manufacturer=manufacturer, component=interfaces[0]
  2892. )
  2893. InventoryItem.objects.create(
  2894. device=device, name='Inventory Item 2', role=roles[0], manufacturer=manufacturer, component=interfaces[1]
  2895. )
  2896. InventoryItem.objects.create(
  2897. device=device, name='Inventory Item 3', role=roles[0], manufacturer=manufacturer, component=interfaces[2]
  2898. )
  2899. cls.create_data = [
  2900. {
  2901. 'device': device.pk,
  2902. 'name': 'Inventory Item 4',
  2903. 'role': roles[1].pk,
  2904. 'manufacturer': manufacturer.pk,
  2905. 'component_type': 'dcim.interface',
  2906. 'component_id': interfaces[0].pk,
  2907. },
  2908. {
  2909. 'device': device.pk,
  2910. 'name': 'Inventory Item 5',
  2911. 'role': roles[1].pk,
  2912. 'manufacturer': manufacturer.pk,
  2913. 'component_type': 'dcim.interface',
  2914. 'component_id': interfaces[1].pk,
  2915. },
  2916. {
  2917. 'device': device.pk,
  2918. 'name': 'Inventory Item 6',
  2919. 'role': roles[1].pk,
  2920. 'manufacturer': manufacturer.pk,
  2921. 'component_type': 'dcim.interface',
  2922. 'component_id': interfaces[2].pk,
  2923. },
  2924. ]
  2925. class InventoryItemRoleTestCase(APIViewTestCases.APIViewTestCase):
  2926. model = InventoryItemRole
  2927. brief_fields = ['description', 'display', 'id', 'inventoryitem_count', 'name', 'slug', 'url']
  2928. create_data = [
  2929. {
  2930. 'name': 'Inventory Item Role 4',
  2931. 'slug': 'inventory-item-role-4',
  2932. 'color': 'ffff00',
  2933. },
  2934. {
  2935. 'name': 'Inventory Item Role 5',
  2936. 'slug': 'inventory-item-role-5',
  2937. 'color': 'ffff00',
  2938. },
  2939. {
  2940. 'name': 'Inventory Item Role 6',
  2941. 'slug': 'inventory-item-role-6',
  2942. 'color': 'ffff00',
  2943. },
  2944. ]
  2945. bulk_update_data = {
  2946. 'description': 'New description',
  2947. }
  2948. @classmethod
  2949. def setUpTestData(cls):
  2950. roles = (
  2951. InventoryItemRole(name='Inventory Item Role 1', slug='inventory-item-role-1', color='ff0000'),
  2952. InventoryItemRole(name='Inventory Item Role 2', slug='inventory-item-role-2', color='00ff00'),
  2953. InventoryItemRole(name='Inventory Item Role 3', slug='inventory-item-role-3', color='0000ff'),
  2954. )
  2955. InventoryItemRole.objects.bulk_create(roles)
  2956. class CableBundleTestCase(APIViewTestCases.APIViewTestCase):
  2957. model = CableBundle
  2958. brief_fields = ['description', 'display', 'id', 'name', 'url']
  2959. create_data = [
  2960. {'name': 'Cable Bundle 4'},
  2961. {'name': 'Cable Bundle 5'},
  2962. {'name': 'Cable Bundle 6'},
  2963. ]
  2964. bulk_update_data = {
  2965. 'description': 'New description',
  2966. }
  2967. @classmethod
  2968. def setUpTestData(cls):
  2969. cable_bundles = (
  2970. CableBundle(name='Cable Bundle 1'),
  2971. CableBundle(name='Cable Bundle 2'),
  2972. CableBundle(name='Cable Bundle 3'),
  2973. )
  2974. CableBundle.objects.bulk_create(cable_bundles)
  2975. def test_cable_count(self):
  2976. """cable_count annotation is returned correctly in the API response."""
  2977. self.add_permissions('dcim.view_cablebundle')
  2978. bundle = CableBundle.objects.first()
  2979. site = Site.objects.create(name='CB Test Site', slug='cb-test-site')
  2980. manufacturer = Manufacturer.objects.create(name='CB Manufacturer', slug='cb-manufacturer')
  2981. device_type = DeviceType.objects.create(
  2982. manufacturer=manufacturer, model='CB Device Type', slug='cb-device-type'
  2983. )
  2984. role = DeviceRole.objects.create(name='CB Role', slug='cb-role', color='ff0000')
  2985. devices = (
  2986. Device(device_type=device_type, role=role, name='CB Device 1', site=site),
  2987. Device(device_type=device_type, role=role, name='CB Device 2', site=site),
  2988. )
  2989. Device.objects.bulk_create(devices)
  2990. interfaces = (
  2991. Interface(device=devices[0], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
  2992. Interface(device=devices[0], name='eth1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
  2993. Interface(device=devices[1], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
  2994. Interface(device=devices[1], name='eth1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
  2995. )
  2996. Interface.objects.bulk_create(interfaces)
  2997. for a, b in [(interfaces[0], interfaces[2]), (interfaces[1], interfaces[3])]:
  2998. cable = Cable(a_terminations=[a], b_terminations=[b], bundle=bundle)
  2999. cable.save()
  3000. url = reverse('dcim-api:cablebundle-detail', kwargs={'pk': bundle.pk})
  3001. response = self.client.get(url, **self.header)
  3002. self.assertHttpStatus(response, status.HTTP_200_OK)
  3003. self.assertEqual(response.data['cable_count'], 2)
  3004. class CableTestCase(APIViewTestCases.APIViewTestCase):
  3005. model = Cable
  3006. brief_fields = ['description', 'display', 'id', 'label', 'url']
  3007. bulk_update_data = {
  3008. 'length': 100,
  3009. 'length_unit': 'm',
  3010. }
  3011. # TODO: Allow updating cable terminations
  3012. test_update_object = None
  3013. def model_to_dict(self, *args, **kwargs):
  3014. data = super().model_to_dict(*args, **kwargs)
  3015. # Serialize termination objects
  3016. if 'a_terminations' in data:
  3017. data['a_terminations'] = GenericObjectSerializer(data['a_terminations'], many=True).data
  3018. if 'b_terminations' in data:
  3019. data['b_terminations'] = GenericObjectSerializer(data['b_terminations'], many=True).data
  3020. return data
  3021. @classmethod
  3022. def setUpTestData(cls):
  3023. site = Site.objects.create(name='Site 1', slug='site-1')
  3024. manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
  3025. devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
  3026. role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1', color='ff0000')
  3027. devices = (
  3028. Device(device_type=devicetype, role=role, name='Device 1', site=site),
  3029. Device(device_type=devicetype, role=role, name='Device 2', site=site),
  3030. )
  3031. Device.objects.bulk_create(devices)
  3032. interfaces = []
  3033. for device in devices:
  3034. for i in range(0, 10):
  3035. interfaces.append(Interface(device=device, type=InterfaceTypeChoices.TYPE_1GE_FIXED, name=f'eth{i}'))
  3036. Interface.objects.bulk_create(interfaces)
  3037. cables = (
  3038. Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[10]], label='Cable 1'),
  3039. Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[11]], label='Cable 2'),
  3040. Cable(a_terminations=[interfaces[2]], b_terminations=[interfaces[12]], label='Cable 3'),
  3041. )
  3042. for cable in cables:
  3043. cable.save()
  3044. cls.create_data = [
  3045. {
  3046. 'a_terminations': [{
  3047. 'object_type': 'dcim.interface',
  3048. 'object_id': interfaces[4].pk,
  3049. }],
  3050. 'b_terminations': [{
  3051. 'object_type': 'dcim.interface',
  3052. 'object_id': interfaces[14].pk,
  3053. }],
  3054. 'label': 'Cable 4',
  3055. 'profile': CableProfileChoices.SINGLE_1C1P,
  3056. },
  3057. {
  3058. 'a_terminations': [{
  3059. 'object_type': 'dcim.interface',
  3060. 'object_id': interfaces[5].pk,
  3061. }],
  3062. 'b_terminations': [{
  3063. 'object_type': 'dcim.interface',
  3064. 'object_id': interfaces[15].pk,
  3065. }],
  3066. 'label': 'Cable 5',
  3067. 'profile': CableProfileChoices.SINGLE_1C1P,
  3068. },
  3069. {
  3070. 'a_terminations': [{
  3071. 'object_type': 'dcim.interface',
  3072. 'object_id': interfaces[6].pk,
  3073. }],
  3074. 'b_terminations': [{
  3075. 'object_type': 'dcim.interface',
  3076. 'object_id': interfaces[16].pk,
  3077. }],
  3078. 'label': 'Cable 6',
  3079. # No profile (legacy behavior)
  3080. },
  3081. ]
  3082. def test_graphql_cable_termination_cached_filters(self):
  3083. """
  3084. Validate filtering cables by cached CableTermination relations via GraphQL:
  3085. cable_list(filters: { terminations: { <relation>: {...}, DISTINCT: true } })
  3086. Also asserts deduplication when both ends match (cable between two interfaces
  3087. on the same device/rack/location/site).
  3088. """
  3089. self.add_permissions(
  3090. 'dcim.view_cable',
  3091. 'dcim.view_device',
  3092. 'dcim.view_interface',
  3093. 'dcim.view_rack',
  3094. 'dcim.view_location',
  3095. 'dcim.view_site',
  3096. )
  3097. # Reuse existing fixtures from setUpTestData()
  3098. devicetype = DeviceType.objects.get(slug='device-type-1')
  3099. role = DeviceRole.objects.get(slug='device-role-1')
  3100. # Create an isolated topology for this test
  3101. site_a = Site.objects.create(name='GQL Site A', slug='gql-site-a')
  3102. site_b = Site.objects.create(name='GQL Site B', slug='gql-site-b')
  3103. location_a = Location.objects.create(
  3104. site=site_a,
  3105. name='GQL Location A',
  3106. slug='gql-location-a',
  3107. status=LocationStatusChoices.STATUS_ACTIVE,
  3108. )
  3109. location_b = Location.objects.create(
  3110. site=site_b,
  3111. name='GQL Location B',
  3112. slug='gql-location-b',
  3113. status=LocationStatusChoices.STATUS_ACTIVE,
  3114. )
  3115. rack_a = Rack.objects.create(site=site_a, location=location_a, name='GQL Rack A', u_height=42)
  3116. rack_b = Rack.objects.create(site=site_b, location=location_b, name='GQL Rack B', u_height=42)
  3117. device_a = Device.objects.create(
  3118. device_type=devicetype,
  3119. role=role,
  3120. name='GQL Device A',
  3121. site=site_a,
  3122. location=location_a,
  3123. rack=rack_a,
  3124. )
  3125. device_b = Device.objects.create(
  3126. device_type=devicetype,
  3127. role=role,
  3128. name='GQL Device B',
  3129. site=site_b,
  3130. location=location_b,
  3131. rack=rack_b,
  3132. )
  3133. a0 = Interface.objects.create(device=device_a, type=InterfaceTypeChoices.TYPE_1GE_FIXED, name='eth0')
  3134. a1 = Interface.objects.create(device=device_a, type=InterfaceTypeChoices.TYPE_1GE_FIXED, name='eth1')
  3135. a2 = Interface.objects.create(device=device_a, type=InterfaceTypeChoices.TYPE_1GE_FIXED, name='eth2')
  3136. b0 = Interface.objects.create(device=device_b, type=InterfaceTypeChoices.TYPE_1GE_FIXED, name='eth0')
  3137. # Both ends on Device A (duplication risk without DISTINCT)
  3138. cable_same_device = Cable(a_terminations=[a0], b_terminations=[a1], label='GQL Cable Same Device')
  3139. cable_same_device.save()
  3140. # Cross to Device B
  3141. cable_cross = Cable(a_terminations=[a2], b_terminations=[b0], label='GQL Cable Cross')
  3142. cable_cross.save()
  3143. expected_a = {str(cable_same_device.pk), str(cable_cross.pk)}
  3144. expected_b = {str(cable_cross.pk)}
  3145. url = reverse('graphql')
  3146. test_cases = (
  3147. # Device (ID + name)
  3148. (f'device: {{ id: {{ exact: "{device_a.pk}" }} }}', expected_a),
  3149. (f'device: {{ name: {{ exact: "{device_a.name}" }} }}', expected_a),
  3150. (f'device: {{ id: {{ exact: "{device_b.pk}" }} }}', expected_b),
  3151. (f'device: {{ name: {{ exact: "{device_b.name}" }} }}', expected_b),
  3152. # Rack (ID + name)
  3153. (f'rack: {{ id: {{ exact: "{rack_a.pk}" }} }}', expected_a),
  3154. (f'rack: {{ name: {{ exact: "{rack_a.name}" }} }}', expected_a),
  3155. (f'rack: {{ id: {{ exact: "{rack_b.pk}" }} }}', expected_b),
  3156. (f'rack: {{ name: {{ exact: "{rack_b.name}" }} }}', expected_b),
  3157. # Location (ID + name)
  3158. (f'location: {{ id: {{ exact: "{location_a.pk}" }} }}', expected_a),
  3159. (f'location: {{ name: {{ exact: "{location_a.name}" }} }}', expected_a),
  3160. (f'location: {{ id: {{ exact: "{location_b.pk}" }} }}', expected_b),
  3161. (f'location: {{ name: {{ exact: "{location_b.name}" }} }}', expected_b),
  3162. # Site (ID + slug)
  3163. (f'site: {{ id: {{ exact: "{site_a.pk}" }} }}', expected_a),
  3164. (f'site: {{ slug: {{ exact: "{site_a.slug}" }} }}', expected_a),
  3165. (f'site: {{ id: {{ exact: "{site_b.pk}" }} }}', expected_b),
  3166. (f'site: {{ slug: {{ exact: "{site_b.slug}" }} }}', expected_b),
  3167. )
  3168. for inner_filter, expected in test_cases:
  3169. with self.subTest(filter=inner_filter):
  3170. query = f"""{{
  3171. cable_list(filters: {{ terminations: {{ {inner_filter} DISTINCT: true }} }})
  3172. {{ id }}
  3173. }}"""
  3174. response = self.client.post(url, data={'query': query}, format='json', **self.header)
  3175. self.assertHttpStatus(response, status.HTTP_200_OK)
  3176. data = response.json()
  3177. self.assertNotIn('errors', data)
  3178. rows = data['data']['cable_list']
  3179. ids = [row['id'] for row in rows]
  3180. # Ensure DISTINCT is actually effective (no duplicate cables when both ends match)
  3181. self.assertEqual(len(ids), len(set(ids)), f'Duplicate cables returned for: {inner_filter}')
  3182. self.assertSetEqual(set(ids), expected)
  3183. def test_graphql_cable_terminations_query_count(self):
  3184. """
  3185. Resolving CableType.a_terminations and CableType.b_terminations must take a constant number
  3186. of queries, regardless of how many cables (and hence terminations) are returned.
  3187. Also exercises selecting both cable ends in a single query: each end must be prefetched
  3188. under its own attribute, as two prefetches of the same relation cannot be merged.
  3189. """
  3190. self.add_permissions(
  3191. 'dcim.view_cable',
  3192. 'dcim.view_device',
  3193. 'dcim.view_devicerole',
  3194. 'dcim.view_devicetype',
  3195. 'dcim.view_interface',
  3196. 'dcim.view_platform',
  3197. )
  3198. # Reuse existing fixtures from setUpTestData()
  3199. site = Site.objects.get(slug='site-1')
  3200. devicetype = DeviceType.objects.get(slug='device-type-1')
  3201. role = DeviceRole.objects.get(slug='device-role-1')
  3202. # Create an isolated topology of cables between two devices
  3203. devices = (
  3204. Device(device_type=devicetype, role=role, name='GQL Count Device A', site=site),
  3205. Device(device_type=devicetype, role=role, name='GQL Count Device B', site=site),
  3206. )
  3207. Device.objects.bulk_create(devices)
  3208. interfaces = []
  3209. for device in devices:
  3210. for i in range(0, 8):
  3211. interfaces.append(
  3212. Interface(device=device, type=InterfaceTypeChoices.TYPE_1GE_FIXED, name=f'gql{i}')
  3213. )
  3214. Interface.objects.bulk_create(interfaces)
  3215. expected_terminations = {}
  3216. for i in range(0, 8):
  3217. cable = Cable(
  3218. a_terminations=[interfaces[i]],
  3219. b_terminations=[interfaces[i + 8]],
  3220. label=f'GQL Count Cable {i}',
  3221. )
  3222. cable.save()
  3223. expected_terminations[str(cable.pk)] = (interfaces[i].pk, interfaces[i + 8].pk)
  3224. url = reverse('graphql')
  3225. termination_fields = """
  3226. ... on InterfaceType {
  3227. id
  3228. name
  3229. device { id name platform { id } role { id } device_type { id } }
  3230. }
  3231. """
  3232. def build_query(limit):
  3233. return f"""{{
  3234. cable_list(
  3235. filters: {{ label: {{ contains: "GQL Count Cable " }} }},
  3236. pagination: {{ limit: {limit} }}
  3237. ) {{
  3238. id
  3239. a_terminations {{ {termination_fields} }}
  3240. b_terminations {{ {termination_fields} }}
  3241. }}
  3242. }}"""
  3243. # Warm per-process caches (e.g. ContentType) so they are not counted below
  3244. self.client.post(url, data={'query': build_query(1)}, format='json', **self.header)
  3245. query_counts = {}
  3246. for limit in (2, 8):
  3247. with CaptureQueriesContext(connection) as queries:
  3248. response = self.client.post(
  3249. url, data={'query': build_query(limit)}, format='json', **self.header
  3250. )
  3251. self.assertHttpStatus(response, status.HTTP_200_OK)
  3252. data = response.json()
  3253. self.assertNotIn('errors', data)
  3254. rows = data['data']['cable_list']
  3255. self.assertEqual(len(rows), limit)
  3256. # Both ends must resolve to the expected interfaces
  3257. for row in rows:
  3258. interface_a, interface_b = expected_terminations[row['id']]
  3259. self.assertEqual([t['id'] for t in row['a_terminations']], [str(interface_a)])
  3260. self.assertEqual([t['id'] for t in row['b_terminations']], [str(interface_b)])
  3261. query_counts[limit] = len(queries.captured_queries)
  3262. self.assertEqual(
  3263. query_counts[2],
  3264. query_counts[8],
  3265. f"Query count scales with the number of cables returned: {query_counts}"
  3266. )
  3267. def test_graphql_cable_termination_models(self):
  3268. """
  3269. The GraphQL prefetch hint for a cable termination enumerates the terminating models
  3270. explicitly; a model missing from that list silently falls back to an unoptimized query
  3271. rather than raising, so guard against drift from CABLE_TERMINATION_MODELS.
  3272. """
  3273. self.assertSetEqual(
  3274. {(model._meta.app_label, model._meta.model_name) for model in _CABLE_TERMINATION_MODELS},
  3275. {(ot.app_label, ot.model) for ot in ObjectType.objects.filter(CABLE_TERMINATION_MODELS)},
  3276. )
  3277. class CableTerminationTestCase(
  3278. APIViewTestCases.GetObjectViewTestCase,
  3279. APIViewTestCases.ListObjectsViewTestCase,
  3280. ):
  3281. model = CableTermination
  3282. brief_fields = [
  3283. 'cable', 'cable_end', 'connector', 'display', 'id', 'positions', 'termination_id', 'termination_type', 'url',
  3284. ]
  3285. @classmethod
  3286. def setUpTestData(cls):
  3287. device1 = create_test_device('Device 1')
  3288. device2 = create_test_device('Device 2')
  3289. interfaces = []
  3290. for device in (device1, device2):
  3291. for i in range(0, 10):
  3292. interfaces.append(Interface(device=device, type=InterfaceTypeChoices.TYPE_1GE_FIXED, name=f'eth{i}'))
  3293. Interface.objects.bulk_create(interfaces)
  3294. cables = (
  3295. Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[10]], label='Cable 1'),
  3296. Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[11]], label='Cable 2'),
  3297. Cable(a_terminations=[interfaces[2]], b_terminations=[interfaces[12]], label='Cable 3'),
  3298. )
  3299. for cable in cables:
  3300. cable.save()
  3301. class ConnectedDeviceTestCase(APITestCase):
  3302. @classmethod
  3303. def setUpTestData(cls):
  3304. site = Site.objects.create(name='Site 1', slug='site-1')
  3305. manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
  3306. devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
  3307. role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1', color='ff0000')
  3308. devices = (
  3309. Device(device_type=devicetype, role=role, name='TestDevice1', site=site),
  3310. Device(device_type=devicetype, role=role, name='TestDevice2', site=site),
  3311. )
  3312. Device.objects.bulk_create(devices)
  3313. interfaces = (
  3314. Interface(device=devices[0], name='eth0'),
  3315. Interface(device=devices[1], name='eth0'),
  3316. Interface(device=devices[0], name='eth1'), # Not connected
  3317. )
  3318. Interface.objects.bulk_create(interfaces)
  3319. cable = Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[1]])
  3320. cable.save()
  3321. def test_get_connected_device(self):
  3322. self.add_permissions('dcim.view_device', 'dcim.view_interface')
  3323. url = reverse('dcim-api:connected-device-list')
  3324. url_params = '?peer_device=TestDevice1&peer_interface=eth0'
  3325. response = self.client.get(url + url_params, **self.header)
  3326. self.assertHttpStatus(response, status.HTTP_200_OK)
  3327. self.assertEqual(response.data['name'], 'TestDevice2')
  3328. url_params = '?peer_device=TestDevice1&peer_interface=eth1'
  3329. response = self.client.get(url + url_params, **self.header)
  3330. self.assertHttpStatus(response, status.HTTP_404_NOT_FOUND)
  3331. class VirtualChassisTestCase(APIViewTestCases.APIViewTestCase):
  3332. model = VirtualChassis
  3333. brief_fields = ['description', 'display', 'id', 'master', 'member_count', 'name', 'url']
  3334. @classmethod
  3335. def setUpTestData(cls):
  3336. site = Site.objects.create(name='Test Site', slug='test-site')
  3337. manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
  3338. devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type', slug='device-type')
  3339. role = DeviceRole.objects.create(name='Device Role', slug='device-role', color='ff0000')
  3340. devices = (
  3341. Device(name='Device 1', device_type=devicetype, role=role, site=site),
  3342. Device(name='Device 2', device_type=devicetype, role=role, site=site),
  3343. Device(name='Device 3', device_type=devicetype, role=role, site=site),
  3344. Device(name='Device 4', device_type=devicetype, role=role, site=site),
  3345. Device(name='Device 5', device_type=devicetype, role=role, site=site),
  3346. Device(name='Device 6', device_type=devicetype, role=role, site=site),
  3347. Device(name='Device 7', device_type=devicetype, role=role, site=site),
  3348. Device(name='Device 8', device_type=devicetype, role=role, site=site),
  3349. Device(name='Device 9', device_type=devicetype, role=role, site=site),
  3350. Device(name='Device 10', device_type=devicetype, role=role, site=site),
  3351. Device(name='Device 11', device_type=devicetype, role=role, site=site),
  3352. Device(name='Device 12', device_type=devicetype, role=role, site=site),
  3353. )
  3354. Device.objects.bulk_create(devices)
  3355. # Create 12 interfaces per device
  3356. interfaces = []
  3357. for i, device in enumerate(devices):
  3358. for j in range(0, 13):
  3359. interfaces.append(
  3360. # Interface name starts with parent device's position in VC; e.g. 1/1, 1/2, 1/3...
  3361. Interface(device=device, name=f'{i % 3 + 1}/{j}', type=InterfaceTypeChoices.TYPE_1GE_FIXED)
  3362. )
  3363. Interface.objects.bulk_create(interfaces)
  3364. # Create three VirtualChassis with three members each
  3365. virtual_chassis = (
  3366. VirtualChassis(name='Virtual Chassis 1', master=devices[0], domain='domain-1'),
  3367. VirtualChassis(name='Virtual Chassis 2', master=devices[3], domain='domain-2'),
  3368. VirtualChassis(name='Virtual Chassis 3', master=devices[6], domain='domain-3'),
  3369. )
  3370. VirtualChassis.objects.bulk_create(virtual_chassis)
  3371. Device.objects.filter(pk=devices[0].pk).update(virtual_chassis=virtual_chassis[0], vc_position=1)
  3372. Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis[0], vc_position=2)
  3373. Device.objects.filter(pk=devices[2].pk).update(virtual_chassis=virtual_chassis[0], vc_position=3)
  3374. Device.objects.filter(pk=devices[3].pk).update(virtual_chassis=virtual_chassis[1], vc_position=1)
  3375. Device.objects.filter(pk=devices[4].pk).update(virtual_chassis=virtual_chassis[1], vc_position=2)
  3376. Device.objects.filter(pk=devices[5].pk).update(virtual_chassis=virtual_chassis[1], vc_position=3)
  3377. Device.objects.filter(pk=devices[6].pk).update(virtual_chassis=virtual_chassis[2], vc_position=1)
  3378. Device.objects.filter(pk=devices[7].pk).update(virtual_chassis=virtual_chassis[2], vc_position=2)
  3379. Device.objects.filter(pk=devices[8].pk).update(virtual_chassis=virtual_chassis[2], vc_position=3)
  3380. cls.update_data = {
  3381. 'name': 'Virtual Chassis X',
  3382. 'domain': 'domain-x',
  3383. 'master': devices[1].pk,
  3384. }
  3385. cls.create_data = [
  3386. {
  3387. 'name': 'Virtual Chassis 4',
  3388. 'domain': 'domain-4',
  3389. },
  3390. {
  3391. 'name': 'Virtual Chassis 5',
  3392. 'domain': 'domain-5',
  3393. },
  3394. {
  3395. 'name': 'Virtual Chassis 6',
  3396. 'domain': 'domain-6',
  3397. },
  3398. ]
  3399. cls.bulk_update_data = {
  3400. 'domain': 'newdomain',
  3401. 'master': None
  3402. }
  3403. class PowerPanelTestCase(APIViewTestCases.APIViewTestCase):
  3404. model = PowerPanel
  3405. brief_fields = ['description', 'display', 'id', 'name', 'powerfeed_count', 'url']
  3406. user_permissions = ('dcim.view_site', )
  3407. @classmethod
  3408. def setUpTestData(cls):
  3409. sites = (
  3410. Site.objects.create(name='Site 1', slug='site-1'),
  3411. Site.objects.create(name='Site 2', slug='site-2'),
  3412. )
  3413. locations = (
  3414. Location.objects.create(name='Location 1', slug='location-1', site=sites[0]),
  3415. Location.objects.create(name='Location 2', slug='location-2', site=sites[0]),
  3416. Location.objects.create(name='Location 3', slug='location-3', site=sites[0]),
  3417. Location.objects.create(name='Location 4', slug='location-3', site=sites[1]),
  3418. )
  3419. power_panels = (
  3420. PowerPanel(site=sites[0], location=locations[0], name='Power Panel 1'),
  3421. PowerPanel(site=sites[0], location=locations[1], name='Power Panel 2'),
  3422. PowerPanel(site=sites[0], location=locations[2], name='Power Panel 3'),
  3423. )
  3424. PowerPanel.objects.bulk_create(power_panels)
  3425. cls.create_data = [
  3426. {
  3427. 'name': 'Power Panel 4',
  3428. 'site': sites[0].pk,
  3429. 'location': locations[0].pk,
  3430. },
  3431. {
  3432. 'name': 'Power Panel 5',
  3433. 'site': sites[0].pk,
  3434. 'location': locations[1].pk,
  3435. },
  3436. {
  3437. 'name': 'Power Panel 6',
  3438. 'site': sites[0].pk,
  3439. 'location': locations[2].pk,
  3440. },
  3441. ]
  3442. cls.bulk_update_data = {
  3443. 'site': sites[1].pk,
  3444. 'location': locations[3].pk
  3445. }
  3446. class PowerFeedTestCase(APIViewTestCases.APIViewTestCase):
  3447. model = PowerFeed
  3448. brief_fields = ['_occupied', 'cable', 'description', 'display', 'id', 'name', 'url']
  3449. bulk_update_data = {
  3450. 'status': 'planned',
  3451. }
  3452. user_permissions = ('dcim.view_powerpanel', )
  3453. @classmethod
  3454. def setUpTestData(cls):
  3455. site = Site.objects.create(name='Site 1', slug='site-1')
  3456. location = Location.objects.create(site=site, name='Location 1', slug='location-1')
  3457. rackrole = RackRole.objects.create(name='Rack Role 1', slug='rack-role-1', color='ff0000')
  3458. racks = (
  3459. Rack(site=site, location=location, role=rackrole, name='Rack 1'),
  3460. Rack(site=site, location=location, role=rackrole, name='Rack 2'),
  3461. Rack(site=site, location=location, role=rackrole, name='Rack 3'),
  3462. Rack(site=site, location=location, role=rackrole, name='Rack 4'),
  3463. )
  3464. Rack.objects.bulk_create(racks)
  3465. power_panels = (
  3466. PowerPanel(site=site, location=location, name='Power Panel 1'),
  3467. PowerPanel(site=site, location=location, name='Power Panel 2'),
  3468. )
  3469. PowerPanel.objects.bulk_create(power_panels)
  3470. PRIMARY = PowerFeedTypeChoices.TYPE_PRIMARY
  3471. REDUNDANT = PowerFeedTypeChoices.TYPE_REDUNDANT
  3472. power_feeds = (
  3473. PowerFeed(power_panel=power_panels[0], rack=racks[0], name='Power Feed 1A', type=PRIMARY),
  3474. PowerFeed(power_panel=power_panels[1], rack=racks[0], name='Power Feed 1B', type=REDUNDANT),
  3475. PowerFeed(power_panel=power_panels[0], rack=racks[1], name='Power Feed 2A', type=PRIMARY),
  3476. PowerFeed(power_panel=power_panels[1], rack=racks[1], name='Power Feed 2B', type=REDUNDANT),
  3477. PowerFeed(power_panel=power_panels[0], rack=racks[2], name='Power Feed 3A', type=PRIMARY),
  3478. PowerFeed(power_panel=power_panels[1], rack=racks[2], name='Power Feed 3B', type=REDUNDANT),
  3479. )
  3480. PowerFeed.objects.bulk_create(power_feeds)
  3481. cls.create_data = [
  3482. {
  3483. 'name': 'Power Feed 4A',
  3484. 'power_panel': power_panels[0].pk,
  3485. 'rack': racks[3].pk,
  3486. 'type': PRIMARY,
  3487. },
  3488. {
  3489. 'name': 'Power Feed 4B',
  3490. 'power_panel': power_panels[1].pk,
  3491. 'rack': racks[3].pk,
  3492. 'type': REDUNDANT,
  3493. },
  3494. ]
  3495. class VirtualDeviceContextTestCase(APIViewTestCases.APIViewTestCase):
  3496. model = VirtualDeviceContext
  3497. brief_fields = ['description', 'device', 'display', 'id', 'identifier', 'name', 'url']
  3498. bulk_update_data = {
  3499. 'status': 'planned',
  3500. }
  3501. @classmethod
  3502. def setUpTestData(cls):
  3503. site = Site.objects.create(name='Test Site', slug='test-site')
  3504. manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
  3505. devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type', slug='device-type')
  3506. role = DeviceRole.objects.create(name='Device Role', slug='device-role', color='ff0000')
  3507. devices = (
  3508. Device(name='Device 1', device_type=devicetype, role=role, site=site),
  3509. Device(name='Device 2', device_type=devicetype, role=role, site=site),
  3510. Device(name='Device 3', device_type=devicetype, role=role, site=site),
  3511. )
  3512. Device.objects.bulk_create(devices)
  3513. vdcs = (
  3514. VirtualDeviceContext(device=devices[1], name='VDC 1', identifier=1, status='active'),
  3515. VirtualDeviceContext(device=devices[1], name='VDC 2', identifier=2, status='active'),
  3516. VirtualDeviceContext(device=devices[2], name='VDC 1', identifier=1, status='active'),
  3517. VirtualDeviceContext(device=devices[2], name='VDC 2', identifier=2, status='active'),
  3518. VirtualDeviceContext(device=devices[2], name='VDC 3', identifier=3, status='active'),
  3519. VirtualDeviceContext(device=devices[2], name='VDC 4', identifier=4, status='active'),
  3520. VirtualDeviceContext(device=devices[2], name='VDC 5', identifier=5, status='active'),
  3521. )
  3522. VirtualDeviceContext.objects.bulk_create(vdcs)
  3523. cls.create_data = [
  3524. {
  3525. 'device': devices[0].pk,
  3526. 'status': 'active',
  3527. 'name': 'VDC 1',
  3528. 'identifier': 1,
  3529. },
  3530. {
  3531. 'device': devices[0].pk,
  3532. 'status': 'active',
  3533. 'name': 'VDC 2',
  3534. 'identifier': 2,
  3535. },
  3536. {
  3537. 'device': devices[1].pk,
  3538. 'status': 'active',
  3539. 'name': 'VDC 3',
  3540. # Omit identifier to test uniqueness constraint
  3541. },
  3542. ]
  3543. def test_get_object_includes_nat_on_primary_ip(self):
  3544. device = create_test_device('vdc-nat-device')
  3545. interfaces = (
  3546. Interface.objects.create(device=device, name='eth0', type='other'),
  3547. Interface.objects.create(device=device, name='eth1', type='other'),
  3548. )
  3549. real_ip4, nat_ip4 = create_test_nat_ip_pair(
  3550. real_address='10.0.2.10/32',
  3551. nat_address='198.51.100.30/32',
  3552. inside_interface=interfaces[0],
  3553. )
  3554. real_ip6, nat_ip6 = create_test_nat_ip_pair(
  3555. real_address='2001:db8:2::10/128',
  3556. nat_address='2001:db8:2::20/128',
  3557. inside_interface=interfaces[1],
  3558. )
  3559. vdc = VirtualDeviceContext.objects.create(
  3560. name='VDC NAT', identifier=98, device=device, status='active'
  3561. )
  3562. vdc.primary_ip4 = nat_ip4
  3563. vdc.primary_ip6 = real_ip6
  3564. vdc.save()
  3565. self.add_permissions('dcim.view_virtualdevicecontext', 'ipam.view_ipaddress')
  3566. response = self.client.get(self._get_detail_url(vdc), **self.header)
  3567. self.assertHttpStatus(response, status.HTTP_200_OK)
  3568. self.assertEqual(response.data['primary_ip4']['nat_inside']['address'], str(real_ip4.address))
  3569. self.assertEqual(response.data['primary_ip4']['nat_outside'], [])
  3570. self.assertIsNone(response.data['primary_ip6']['nat_inside'])
  3571. self.assertCountEqual(
  3572. [ip['address'] for ip in response.data['primary_ip6']['nat_outside']],
  3573. [str(nat_ip6.address)],
  3574. )
  3575. def test_get_object_includes_dns_name_on_primary_ip(self):
  3576. device = create_test_device('vdc-dns-device')
  3577. interfaces = (
  3578. Interface.objects.create(device=device, name='eth0', type='other'),
  3579. Interface.objects.create(device=device, name='eth1', type='other'),
  3580. )
  3581. ip4 = IPAddress(address='192.0.2.30/32', dns_name='vdc4.example.com')
  3582. ip4.assigned_object = interfaces[0]
  3583. ip4.save()
  3584. ip6 = IPAddress(address='2001:db8::30/128', dns_name='vdc6.example.com')
  3585. ip6.assigned_object = interfaces[1]
  3586. ip6.save()
  3587. vdc = VirtualDeviceContext.objects.create(
  3588. name='VDC DNS', identifier=99, device=device, status='active'
  3589. )
  3590. vdc.primary_ip4 = ip4
  3591. vdc.primary_ip6 = ip6
  3592. vdc.save()
  3593. self.add_permissions('dcim.view_virtualdevicecontext', 'ipam.view_ipaddress')
  3594. response = self.client.get(self._get_detail_url(vdc), **self.header)
  3595. self.assertHttpStatus(response, status.HTTP_200_OK)
  3596. self.assertEqual(response.data['primary_ip4']['dns_name'], 'vdc4.example.com')
  3597. self.assertEqual(response.data['primary_ip6']['dns_name'], 'vdc6.example.com')
  3598. self.assertIn(
  3599. response.data['primary_ip']['dns_name'],
  3600. ('vdc4.example.com', 'vdc6.example.com'),
  3601. )
  3602. class MACAddressTestCase(APIViewTestCases.APIViewTestCase):
  3603. model = MACAddress
  3604. brief_fields = ['description', 'display', 'id', 'mac_address', 'url']
  3605. bulk_update_data = {
  3606. 'description': 'New description',
  3607. }
  3608. @classmethod
  3609. def setUpTestData(cls):
  3610. device = create_test_device(name='Device 1')
  3611. interfaces = (
  3612. Interface(device=device, name='Interface 1', type='1000base-t'),
  3613. Interface(device=device, name='Interface 2', type='1000base-t'),
  3614. Interface(device=device, name='Interface 3', type='1000base-t'),
  3615. Interface(device=device, name='Interface 4', type='1000base-t'),
  3616. Interface(device=device, name='Interface 5', type='1000base-t'),
  3617. )
  3618. Interface.objects.bulk_create(interfaces)
  3619. mac_addresses = (
  3620. MACAddress(mac_address='00:00:00:00:00:01', assigned_object=interfaces[0]),
  3621. MACAddress(mac_address='00:00:00:00:00:02', assigned_object=interfaces[1]),
  3622. MACAddress(mac_address='00:00:00:00:00:03', assigned_object=interfaces[2]),
  3623. )
  3624. MACAddress.objects.bulk_create(mac_addresses)
  3625. cls.create_data = [
  3626. {
  3627. 'mac_address': '00:00:00:00:00:04',
  3628. 'assigned_object_type': 'dcim.interface',
  3629. 'assigned_object_id': interfaces[3].pk,
  3630. },
  3631. {
  3632. 'mac_address': '00:00:00:00:00:05',
  3633. 'assigned_object_type': 'dcim.interface',
  3634. 'assigned_object_id': interfaces[4].pk,
  3635. },
  3636. {
  3637. 'mac_address': '00:00:00:00:00:06',
  3638. },
  3639. ]