bulk_import.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  1. from django import forms
  2. from django.contrib.contenttypes.models import ContentType
  3. from django.utils.translation import gettext_lazy as _
  4. from dcim.forms.mixins import ScopedImportForm
  5. from dcim.models import Device, Interface, Site
  6. from ipam.choices import *
  7. from ipam.constants import *
  8. from ipam.models import *
  9. from netbox.forms import NetBoxModelImportForm, OrganizationalModelImportForm, PrimaryModelImportForm
  10. from tenancy.models import Tenant
  11. from utilities.forms.fields import (
  12. CSVChoiceField,
  13. CSVContentTypeField,
  14. CSVModelChoiceField,
  15. CSVModelMultipleChoiceField,
  16. NumericRangeArrayField,
  17. SlugField,
  18. )
  19. from virtualization.models import VirtualMachine, VMInterface
  20. __all__ = (
  21. 'ASNImportForm',
  22. 'ASNRangeImportForm',
  23. 'AggregateImportForm',
  24. 'FHRPGroupImportForm',
  25. 'IPAddressImportForm',
  26. 'IPRangeImportForm',
  27. 'PrefixImportForm',
  28. 'RIRImportForm',
  29. 'RoleImportForm',
  30. 'RouteTargetImportForm',
  31. 'ServiceImportForm',
  32. 'ServiceTemplateImportForm',
  33. 'VLANGroupImportForm',
  34. 'VLANImportForm',
  35. 'VLANTranslationPolicyImportForm',
  36. 'VLANTranslationRuleImportForm',
  37. 'VRFImportForm',
  38. )
  39. class VRFImportForm(PrimaryModelImportForm):
  40. tenant = CSVModelChoiceField(
  41. label=_('Tenant'),
  42. queryset=Tenant.objects.all(),
  43. required=False,
  44. to_field_name='name',
  45. help_text=_('Assigned tenant')
  46. )
  47. import_targets = CSVModelMultipleChoiceField(
  48. queryset=RouteTarget.objects.all(),
  49. required=False,
  50. to_field_name='name',
  51. help_text=_('Import route targets')
  52. )
  53. export_targets = CSVModelMultipleChoiceField(
  54. queryset=RouteTarget.objects.all(),
  55. required=False,
  56. to_field_name='name',
  57. help_text=_('Export route targets')
  58. )
  59. class Meta:
  60. model = VRF
  61. fields = (
  62. 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'owner',
  63. 'comments', 'tags',
  64. )
  65. class RouteTargetImportForm(PrimaryModelImportForm):
  66. tenant = CSVModelChoiceField(
  67. label=_('Tenant'),
  68. queryset=Tenant.objects.all(),
  69. required=False,
  70. to_field_name='name',
  71. help_text=_('Assigned tenant')
  72. )
  73. class Meta:
  74. model = RouteTarget
  75. fields = ('name', 'tenant', 'description', 'owner', 'comments', 'tags')
  76. class RIRImportForm(OrganizationalModelImportForm):
  77. slug = SlugField()
  78. class Meta:
  79. model = RIR
  80. fields = ('name', 'slug', 'is_private', 'description', 'owner', 'comments', 'tags')
  81. class AggregateImportForm(PrimaryModelImportForm):
  82. rir = CSVModelChoiceField(
  83. label=_('RIR'),
  84. queryset=RIR.objects.all(),
  85. to_field_name='name',
  86. help_text=_('Assigned RIR')
  87. )
  88. tenant = CSVModelChoiceField(
  89. label=_('Tenant'),
  90. queryset=Tenant.objects.all(),
  91. required=False,
  92. to_field_name='name',
  93. help_text=_('Assigned tenant')
  94. )
  95. class Meta:
  96. model = Aggregate
  97. fields = ('prefix', 'rir', 'tenant', 'date_added', 'description', 'owner', 'comments', 'tags')
  98. class ASNRangeImportForm(OrganizationalModelImportForm):
  99. rir = CSVModelChoiceField(
  100. label=_('RIR'),
  101. queryset=RIR.objects.all(),
  102. to_field_name='name',
  103. help_text=_('Assigned RIR')
  104. )
  105. tenant = CSVModelChoiceField(
  106. label=_('Tenant'),
  107. queryset=Tenant.objects.all(),
  108. required=False,
  109. to_field_name='name',
  110. help_text=_('Assigned tenant')
  111. )
  112. class Meta:
  113. model = ASNRange
  114. fields = ('name', 'slug', 'rir', 'start', 'end', 'tenant', 'description', 'owner', 'comments', 'tags')
  115. class ASNImportForm(PrimaryModelImportForm):
  116. rir = CSVModelChoiceField(
  117. label=_('RIR'),
  118. queryset=RIR.objects.all(),
  119. to_field_name='name',
  120. help_text=_('Assigned RIR')
  121. )
  122. role = CSVModelChoiceField(
  123. label=_('Role'),
  124. queryset=Role.objects.all(),
  125. required=False,
  126. to_field_name='name',
  127. help_text=_('Functional role')
  128. )
  129. tenant = CSVModelChoiceField(
  130. label=_('Tenant'),
  131. queryset=Tenant.objects.all(),
  132. required=False,
  133. to_field_name='name',
  134. help_text=_('Assigned tenant')
  135. )
  136. class Meta:
  137. model = ASN
  138. fields = ('asn', 'rir', 'role', 'tenant', 'description', 'owner', 'comments', 'tags')
  139. class RoleImportForm(OrganizationalModelImportForm):
  140. class Meta:
  141. model = Role
  142. fields = ('name', 'slug', 'weight', 'description', 'owner', 'comments', 'tags')
  143. class PrefixImportForm(ScopedImportForm, PrimaryModelImportForm):
  144. vrf = CSVModelChoiceField(
  145. label=_('VRF'),
  146. queryset=VRF.objects.all(),
  147. to_field_name='name',
  148. required=False,
  149. help_text=_('Assigned VRF')
  150. )
  151. tenant = CSVModelChoiceField(
  152. label=_('Tenant'),
  153. queryset=Tenant.objects.all(),
  154. required=False,
  155. to_field_name='name',
  156. help_text=_('Assigned tenant')
  157. )
  158. vlan_group = CSVModelChoiceField(
  159. label=_('VLAN group'),
  160. queryset=VLANGroup.objects.all(),
  161. required=False,
  162. to_field_name='name',
  163. help_text=_("VLAN's group (if any)")
  164. )
  165. vlan_site = CSVModelChoiceField(
  166. label=_('VLAN Site'),
  167. queryset=Site.objects.all(),
  168. required=False,
  169. to_field_name='name',
  170. help_text=_("VLAN's site (if any)")
  171. )
  172. vlan = CSVModelChoiceField(
  173. label=_('VLAN'),
  174. queryset=VLAN.objects.all(),
  175. required=False,
  176. to_field_name='vid',
  177. help_text=_("Assigned VLAN")
  178. )
  179. status = CSVChoiceField(
  180. label=_('Status'),
  181. choices=PrefixStatusChoices,
  182. help_text=_('Operational status')
  183. )
  184. role = CSVModelChoiceField(
  185. label=_('Role'),
  186. queryset=Role.objects.all(),
  187. required=False,
  188. to_field_name='name',
  189. help_text=_('Functional role')
  190. )
  191. class Meta:
  192. model = Prefix
  193. fields = (
  194. 'prefix', 'vrf', 'tenant', 'vlan_group', 'vlan_site', 'vlan', 'status', 'role', 'scope_type', 'scope_name',
  195. 'scope_id', 'is_pool', 'mark_utilized', 'description', 'owner', 'comments', 'tags',
  196. )
  197. labels = {
  198. 'scope_id': _('Scope ID'),
  199. }
  200. def __init__(self, data=None, *args, **kwargs):
  201. super().__init__(data, *args, **kwargs)
  202. if not data:
  203. return
  204. vlan_site = data.get('vlan_site')
  205. vlan_group = data.get('vlan_group')
  206. # Limit VLAN queryset by assigned site and/or group (if specified)
  207. query = Q()
  208. if vlan_site:
  209. query |= Q(**{
  210. f"site__{self.fields['vlan_site'].to_field_name}": vlan_site
  211. })
  212. if vlan_group:
  213. query &= Q(**{
  214. f"group__{self.fields['vlan_group'].to_field_name}": vlan_group
  215. })
  216. queryset = self.fields['vlan'].queryset.filter(query)
  217. self.fields['vlan'].queryset = queryset
  218. class IPRangeImportForm(PrimaryModelImportForm):
  219. vrf = CSVModelChoiceField(
  220. label=_('VRF'),
  221. queryset=VRF.objects.all(),
  222. to_field_name='name',
  223. required=False,
  224. help_text=_('Assigned VRF')
  225. )
  226. tenant = CSVModelChoiceField(
  227. label=_('Tenant'),
  228. queryset=Tenant.objects.all(),
  229. required=False,
  230. to_field_name='name',
  231. help_text=_('Assigned tenant')
  232. )
  233. status = CSVChoiceField(
  234. label=_('Status'),
  235. choices=IPRangeStatusChoices,
  236. help_text=_('Operational status')
  237. )
  238. role = CSVModelChoiceField(
  239. label=_('Role'),
  240. queryset=Role.objects.all(),
  241. required=False,
  242. to_field_name='name',
  243. help_text=_('Functional role')
  244. )
  245. class Meta:
  246. model = IPRange
  247. fields = (
  248. 'start_address', 'end_address', 'vrf', 'tenant', 'status', 'role', 'mark_populated', 'mark_utilized',
  249. 'description', 'owner', 'comments', 'tags',
  250. )
  251. class IPAddressImportForm(PrimaryModelImportForm):
  252. vrf = CSVModelChoiceField(
  253. label=_('VRF'),
  254. queryset=VRF.objects.all(),
  255. to_field_name='name',
  256. required=False,
  257. help_text=_('Assigned VRF')
  258. )
  259. tenant = CSVModelChoiceField(
  260. label=_('Tenant'),
  261. queryset=Tenant.objects.all(),
  262. to_field_name='name',
  263. required=False,
  264. help_text=_('Assigned tenant')
  265. )
  266. status = CSVChoiceField(
  267. label=_('Status'),
  268. choices=IPAddressStatusChoices,
  269. help_text=_('Operational status')
  270. )
  271. role = CSVChoiceField(
  272. label=_('Role'),
  273. choices=IPAddressRoleChoices,
  274. required=False,
  275. help_text=_('Functional role')
  276. )
  277. device = CSVModelChoiceField(
  278. label=_('Device'),
  279. queryset=Device.objects.all(),
  280. required=False,
  281. to_field_name='name',
  282. help_text=_('Parent device of assigned interface (if any)')
  283. )
  284. virtual_machine = CSVModelChoiceField(
  285. label=_('Virtual machine'),
  286. queryset=VirtualMachine.objects.all(),
  287. required=False,
  288. to_field_name='name',
  289. help_text=_('Parent VM of assigned interface (if any)')
  290. )
  291. interface = CSVModelChoiceField(
  292. label=_('Interface'),
  293. queryset=Interface.objects.none(), # Can also refer to VMInterface
  294. required=False,
  295. to_field_name='name',
  296. help_text=_('Assigned interface')
  297. )
  298. fhrp_group = CSVModelChoiceField(
  299. label=_('FHRP Group'),
  300. queryset=FHRPGroup.objects.all(),
  301. required=False,
  302. to_field_name='name',
  303. help_text=_('Assigned FHRP Group name')
  304. )
  305. is_primary = forms.BooleanField(
  306. label=_('Is primary'),
  307. help_text=_('Make this the primary IP for the assigned device'),
  308. required=False
  309. )
  310. is_oob = forms.BooleanField(
  311. label=_('Is out-of-band'),
  312. help_text=_('Designate this as the out-of-band IP address for the assigned device'),
  313. required=False
  314. )
  315. class Meta:
  316. model = IPAddress
  317. fields = [
  318. 'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'fhrp_group',
  319. 'is_primary', 'is_oob', 'dns_name', 'description', 'owner', 'comments', 'tags',
  320. ]
  321. def __init__(self, data=None, *args, **kwargs):
  322. super().__init__(data, *args, **kwargs)
  323. if data:
  324. # Limit interface queryset by assigned device
  325. if data.get('device'):
  326. self.fields['interface'].queryset = Interface.objects.filter(
  327. **{f"device__{self.fields['device'].to_field_name}": data['device']}
  328. )
  329. # Limit interface queryset by assigned VM
  330. elif data.get('virtual_machine'):
  331. self.fields['interface'].queryset = VMInterface.objects.filter(
  332. **{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
  333. )
  334. def clean_is_primary(self):
  335. # Make sure is_primary is None when it's not included in the uploaded data
  336. if 'is_primary' not in self.data:
  337. return None
  338. return self.cleaned_data['is_primary']
  339. def clean_is_oob(self):
  340. # Make sure is_oob is None when it's not included in the uploaded data
  341. if 'is_oob' not in self.data:
  342. return None
  343. return self.cleaned_data['is_oob']
  344. def clean(self):
  345. super().clean()
  346. device = self.cleaned_data.get('device')
  347. virtual_machine = self.cleaned_data.get('virtual_machine')
  348. interface = self.cleaned_data.get('interface')
  349. is_primary = self.cleaned_data.get('is_primary')
  350. is_oob = self.cleaned_data.get('is_oob')
  351. # Validate is_primary and is_oob
  352. if is_primary and not device and not virtual_machine:
  353. raise forms.ValidationError({
  354. "is_primary": _("No device or virtual machine specified; cannot set as primary IP")
  355. })
  356. if is_oob and not device:
  357. raise forms.ValidationError({
  358. "is_oob": _("No device specified; cannot set as out-of-band IP")
  359. })
  360. if is_oob and virtual_machine:
  361. raise forms.ValidationError({
  362. "is_oob": _("Cannot set out-of-band IP for virtual machines")
  363. })
  364. if is_primary and not interface:
  365. raise forms.ValidationError({
  366. "is_primary": _("No interface specified; cannot set as primary IP")
  367. })
  368. if is_oob and not interface:
  369. raise forms.ValidationError({
  370. "is_oob": _("No interface specified; cannot set as out-of-band IP")
  371. })
  372. def save(self, *args, **kwargs):
  373. # Set interface assignment
  374. if self.cleaned_data.get('interface'):
  375. self.instance.assigned_object = self.cleaned_data['interface']
  376. if self.cleaned_data.get('fhrp_group'):
  377. self.instance.assigned_object = self.cleaned_data['fhrp_group']
  378. ipaddress = super().save(*args, **kwargs)
  379. # Set as primary for device/VM
  380. if self.cleaned_data.get('is_primary') is not None:
  381. parent = self.cleaned_data.get('device') or self.cleaned_data.get('virtual_machine')
  382. if self.cleaned_data.get('is_primary'):
  383. parent.snapshot()
  384. if self.instance.address.version == 4:
  385. parent.primary_ip4 = ipaddress
  386. elif self.instance.address.version == 6:
  387. parent.primary_ip6 = ipaddress
  388. parent.save()
  389. else:
  390. # Only clear the primary IP if this IP is currently set as primary
  391. if self.instance.address.version == 4 and parent.primary_ip4 == ipaddress:
  392. parent.snapshot()
  393. parent.primary_ip4 = None
  394. parent.save()
  395. elif self.instance.address.version == 6 and parent.primary_ip6 == ipaddress:
  396. parent.snapshot()
  397. parent.primary_ip6 = None
  398. parent.save()
  399. # Set as OOB for device
  400. if self.cleaned_data.get('is_oob') is not None:
  401. parent = self.cleaned_data.get('device')
  402. if self.cleaned_data.get('is_oob'):
  403. parent.snapshot()
  404. parent.oob_ip = ipaddress
  405. parent.save()
  406. elif parent.oob_ip == ipaddress:
  407. # Only clear OOB if this IP is currently set as the OOB IP
  408. parent.snapshot()
  409. parent.oob_ip = None
  410. parent.save()
  411. return ipaddress
  412. class FHRPGroupImportForm(PrimaryModelImportForm):
  413. protocol = CSVChoiceField(
  414. label=_('Protocol'),
  415. choices=FHRPGroupProtocolChoices
  416. )
  417. auth_type = CSVChoiceField(
  418. label=_('Auth type'),
  419. choices=FHRPGroupAuthTypeChoices,
  420. required=False
  421. )
  422. class Meta:
  423. model = FHRPGroup
  424. fields = ('protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'description', 'owner', 'comments', 'tags')
  425. class VLANGroupImportForm(ScopedImportForm, OrganizationalModelImportForm):
  426. # Override ScopedImportForm.scope_type to set custom queryset
  427. scope_type = CSVContentTypeField(
  428. queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
  429. required=False,
  430. label=_('Scope type (app & model)')
  431. )
  432. vid_ranges = NumericRangeArrayField(
  433. required=False
  434. )
  435. tenant = CSVModelChoiceField(
  436. label=_('Tenant'),
  437. queryset=Tenant.objects.all(),
  438. required=False,
  439. to_field_name='name',
  440. help_text=_('Assigned tenant')
  441. )
  442. class Meta:
  443. model = VLANGroup
  444. fields = (
  445. 'name', 'slug', 'scope_type', 'scope_name', 'scope_id', 'vid_ranges', 'tenant', 'description', 'owner',
  446. 'comments', 'tags',
  447. )
  448. labels = {
  449. 'scope_id': _('Scope ID'),
  450. }
  451. class VLANImportForm(PrimaryModelImportForm):
  452. site = CSVModelChoiceField(
  453. label=_('Site'),
  454. queryset=Site.objects.all(),
  455. required=False,
  456. to_field_name='name',
  457. help_text=_('Assigned site')
  458. )
  459. group = CSVModelChoiceField(
  460. label=_('Group'),
  461. queryset=VLANGroup.objects.all(),
  462. required=False,
  463. to_field_name='name',
  464. help_text=_('Assigned VLAN group')
  465. )
  466. tenant = CSVModelChoiceField(
  467. label=_('Tenant'),
  468. queryset=Tenant.objects.all(),
  469. to_field_name='name',
  470. required=False,
  471. help_text=_('Assigned tenant')
  472. )
  473. status = CSVChoiceField(
  474. label=_('Status'),
  475. choices=VLANStatusChoices,
  476. help_text=_('Operational status')
  477. )
  478. role = CSVModelChoiceField(
  479. label=_('Role'),
  480. queryset=Role.objects.all(),
  481. required=False,
  482. to_field_name='name',
  483. help_text=_('Functional role')
  484. )
  485. qinq_role = CSVChoiceField(
  486. label=_('Q-in-Q role'),
  487. choices=VLANQinQRoleChoices,
  488. required=False,
  489. help_text=_('Operational status')
  490. )
  491. qinq_svlan = CSVModelChoiceField(
  492. label=_('Q-in-Q SVLAN'),
  493. queryset=VLAN.objects.all(),
  494. required=False,
  495. to_field_name='vid',
  496. help_text=_("Service VLAN (for Q-in-Q/802.1ad customer VLANs)")
  497. )
  498. class Meta:
  499. model = VLAN
  500. fields = (
  501. 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'qinq_role', 'qinq_svlan',
  502. 'owner', 'comments', 'tags',
  503. )
  504. class VLANTranslationPolicyImportForm(PrimaryModelImportForm):
  505. class Meta:
  506. model = VLANTranslationPolicy
  507. fields = ('name', 'description', 'owner', 'comments', 'tags')
  508. class VLANTranslationRuleImportForm(NetBoxModelImportForm):
  509. policy = CSVModelChoiceField(
  510. label=_('Policy'),
  511. queryset=VLANTranslationPolicy.objects.all(),
  512. to_field_name='name',
  513. help_text=_('VLAN translation policy')
  514. )
  515. class Meta:
  516. model = VLANTranslationRule
  517. fields = ('policy', 'local_vid', 'remote_vid')
  518. class ServiceTemplateImportForm(PrimaryModelImportForm):
  519. protocol = CSVChoiceField(
  520. label=_('Protocol'),
  521. choices=ServiceProtocolChoices,
  522. help_text=_('IP protocol')
  523. )
  524. class Meta:
  525. model = ServiceTemplate
  526. fields = ('name', 'protocol', 'ports', 'description', 'owner', 'comments', 'tags')
  527. class ServiceImportForm(PrimaryModelImportForm):
  528. parent_object_type = CSVContentTypeField(
  529. queryset=ContentType.objects.filter(SERVICE_ASSIGNMENT_MODELS),
  530. required=True,
  531. label=_('Parent type (app & model)')
  532. )
  533. parent = CSVModelChoiceField(
  534. label=_('Parent'),
  535. queryset=Device.objects.all(),
  536. required=False,
  537. to_field_name='name',
  538. help_text=_('Parent object name')
  539. )
  540. parent_object_id = forms.IntegerField(
  541. required=False,
  542. help_text=_('Parent object ID'),
  543. )
  544. protocol = CSVChoiceField(
  545. label=_('Protocol'),
  546. choices=ServiceProtocolChoices,
  547. help_text=_('IP protocol')
  548. )
  549. ipaddresses = CSVModelMultipleChoiceField(
  550. queryset=IPAddress.objects.all(),
  551. required=False,
  552. to_field_name='address',
  553. help_text=_('IP Address'),
  554. )
  555. class Meta:
  556. model = Service
  557. fields = (
  558. 'ipaddresses', 'name', 'protocol', 'ports', 'description', 'owner', 'comments', 'tags',
  559. )
  560. def __init__(self, data=None, *args, **kwargs):
  561. super().__init__(data, *args, **kwargs)
  562. # Limit parent queryset by assigned parent object type
  563. if data:
  564. match data.get('parent_object_type'):
  565. case 'dcim.device':
  566. self.fields['parent'].queryset = Device.objects.all()
  567. case 'ipam.fhrpgroup':
  568. self.fields['parent'].queryset = FHRPGroup.objects.all()
  569. case 'virtualization.virtualmachine':
  570. self.fields['parent'].queryset = VirtualMachine.objects.all()
  571. def save(self, *args, **kwargs):
  572. if (parent := self.cleaned_data.get('parent')):
  573. self.instance.parent = parent
  574. return super().save(*args, **kwargs)
  575. def clean(self):
  576. super().clean()
  577. if (parent_ct := self.cleaned_data.get('parent_object_type')):
  578. if (parent := self.cleaned_data.get('parent')):
  579. self.cleaned_data['parent_object_id'] = parent.pk
  580. elif (parent_id := self.cleaned_data.get('parent_object_id')):
  581. parent = parent_ct.model_class().objects.filter(id=parent_id).first()
  582. self.cleaned_data['parent'] = parent
  583. else:
  584. # If a parent object type is passed and we've made it here, then raise a validation
  585. # error since an associated parent object or parent object id has not been passed
  586. raise forms.ValidationError(
  587. _("One of parent or parent_object_id must be included with parent_object_type")
  588. )
  589. # making sure parent is defined. In cases where an import is resulting in an update, the
  590. # import data might not include the parent object and so the above logic might not be
  591. # triggered
  592. parent = self.cleaned_data.get('parent')
  593. for ip_address in self.cleaned_data.get('ipaddresses', []):
  594. if not (assigned := ip_address.assigned_object) or ( # no assigned object
  595. (isinstance(parent, FHRPGroup) and assigned != parent) # assigned to FHRPGroup
  596. and getattr(assigned, 'parent_object') != parent # assigned to [VM]Interface
  597. ):
  598. raise forms.ValidationError(
  599. _("{ip} is not assigned to this parent.").format(ip=ip_address)
  600. )
  601. return self.cleaned_data