Skip to content

Netbox Move IP Task¤

task api name: move_ip

Moves every IPv4 and IPv6 address assigned directly to a source device interface to a destination device interface. Existing destination addresses stay assigned. The task preserves IP record IDs, address masks, VRFs, roles, and metadata. It does not change device configuration or move addresses assigned to FHRP groups.

Output¤

The result contains source and destination names, ip_addresses (a list of id and address dictionaries), source_primary_updates, and destination_primary_updates. Primary updates map primary_ip4 or primary_ip6 to an IP record ID or null for clearing. An empty dictionary means no change. Status is updated for a move, including a planned dry-run move, or unchanged for an empty source or identical source/destination interfaces.

Examples¤

nf# netbox move ip from-device router-1 from-interface Ethernet1 device router-2 interface Ethernet2 dry-run
nf# netbox move ip from-device router-1 from-interface Ethernet1 device router-2 interface Ethernet2 is-primary
from norfab.core.nfapi import NorFab

with NorFab(inventory="./inventory.yaml") as nf:
    client = nf.make_client()
    result = client.run_job(
        "netbox", "move_ip", workers="any",
        kwargs={
            "from_device": "router-1", "from_interface": "Ethernet1",
            "device": "router-2", "interface": "Ethernet2",
            "is_primary": True, "dry_run": True,
        },
    )

Primary IP behavior¤

Primary IPs are references on devices. The task clears source primary_ip4 and primary_ip6 references that point to moved IPs, including same-device moves. By default, is_primary=False: the task does not set destination primaries. For a same-device move this clears the affected primaries.

With is_primary=True, moved addresses become destination primaries, replacing any existing primary for each moved address family. Families with no moved addresses retain their destination primary. One address in a family selects that address. With multiple addresses, the moved source primary is selected; if none is the source primary, the task fails before writing.

Notes / Gotchas¤

  • Empty sources and identical interfaces return unchanged without modifying primaries.
  • Repeating a successful move normally finds an empty source.
  • Dry runs resolve objects and report proposed changes without writes.
  • Writes clear source primaries, bulk-update assignments, then set destination primaries. A later request failure leaves earlier successful writes applied.
  • Branch operations require the Netbox branching plugin.
  • Design moves execute before explicit IP writes and allocations. Avoid chained moves within one design: dry runs do not simulate preceding moves.
  • Concurrent changes to the same interfaces are not locked by this task.

Troubleshooting¤

Missing or non-unique device/interface lookups fail. Check exact device and port names. For ambiguous primary selection, arrange a source primary before the move or use is_primary=False. Netbox validation and permission errors propagate; inspect current assignments and primary references before retrying failed writes.

Task Command Shell Reference¤

nf# man tree netbox move ip

Python API Reference¤

Move all IP addresses from one device interface to another.

Addresses, VRFs, roles, and metadata remain unchanged; existing destination addresses are retained. Identical interfaces and empty sources return unchanged without modifying primary references.

Source primary references pointing to moved IPs are cleared even when is_primary is False, including same-device moves. With is_primary=True, moved addresses replace destination primaries for the affected families. A sole address in a family is selected automatically. Multiple addresses use the moved source primary; without one, selection fails before writes. Other destination primaries are preserved.

Writes clear source primaries, move IPs, then set destination primaries. These requests are separate; a later failure does not undo earlier writes. Dry runs return proposed changes without writing. Branch operations require the Netbox branching plugin.

Parameters:

Name Type Description Default
job Job

Current worker job used to report progress.

required
from_device str

Source Netbox device name; must identify one device.

required
from_interface str

Source interface name on that device.

required
device str

Destination Netbox device name; must identify one device.

required
interface str

Destination interface name on that device.

required
is_primary bool

Set destination primaries to moved addresses, overriding existing primaries for the moved address families. Defaults to False.

False
instance Union[None, str]

Netbox instance name; defaults to the worker default.

None
dry_run bool

Return proposed moves and primary changes without writes.

False
branch Union[None, str]

Branch to use; requires the Netbox branching plugin.

None

Returns:

Name Type Description
Result Result

Source/destination, IP IDs and addresses, and primary updates. Status is updated for a move or unchanged for an empty source or identical interfaces. Dry runs include the proposed updates.

Raises:

Type Description
ValueError

A device/interface is missing or primary selection is ambiguous. Non-unique lookups and Netbox API errors propagate.

Source code in norfab\workers\netbox_worker\ip_tasks.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
@Task(
    input=MoveIpInput,
    output=MoveIpResult,
    fastapi={"methods": ["POST"], "schema": NetboxFastApiArgs.model_json_schema()},
    mcp={
        "annotations": {
            "title": "Move IP Addresses",
            "readOnlyHint": False,
            "destructiveHint": True,
            "idempotentHint": True,
            "openWorldHint": True,
        }
    },
)
def move_ip(
    self,
    job: Job,
    from_device: str,
    from_interface: str,
    device: str,
    interface: str,
    is_primary: bool = False,
    instance: Union[None, str] = None,
    dry_run: bool = False,
    branch: Union[None, str] = None,
) -> Result:
    """Move all IP addresses from one device interface to another.

    Addresses, VRFs, roles, and metadata remain unchanged; existing destination
    addresses are retained. Identical interfaces and empty sources return
    unchanged without modifying primary references.

    Source primary references pointing to moved IPs are cleared even when
    is_primary is False, including same-device moves. With is_primary=True,
    moved addresses replace destination primaries for the affected families.
    A sole address in a family is selected automatically. Multiple addresses
    use the moved source primary; without one, selection fails before writes.
    Other destination primaries are preserved.

    Writes clear source primaries, move IPs, then set destination primaries.
    These requests are separate; a later failure does not undo earlier writes.
    Dry runs return proposed changes without writing. Branch operations
    require the Netbox branching plugin.

    Args:
        job: Current worker job used to report progress.
        from_device: Source Netbox device name; must identify one device.
        from_interface: Source interface name on that device.
        device: Destination Netbox device name; must identify one device.
        interface: Destination interface name on that device.
        is_primary: Set destination primaries to moved addresses, overriding
            existing primaries for the moved address families. Defaults to False.
        instance: Netbox instance name; defaults to the worker default.
        dry_run: Return proposed moves and primary changes without writes.
        branch: Branch to use; requires the Netbox branching plugin.

    Returns:
        Result: Source/destination, IP IDs and addresses, and primary updates.
            Status is updated for a move or unchanged for an empty source or
            identical interfaces. Dry runs include the proposed updates.

    Raises:
        ValueError: A device/interface is missing or primary selection is
            ambiguous. Non-unique lookups and Netbox API errors propagate.
    """
    instance = instance or self.default_instance
    nb = self._get_pynetbox(instance, branch=branch, job=job)
    # Resolve both ends before changing assignments or primary references.
    source = nb.dcim.devices.get(name=from_device)
    if source is None:
        raise ValueError(f"source device '{from_device}' not found")
    target = source if from_device == device else nb.dcim.devices.get(name=device)
    if target is None:
        raise ValueError(f"destination device '{device}' not found")
    source_interface = nb.dcim.interfaces.get(
        device_id=source.id, name=from_interface
    )
    target_interface = nb.dcim.interfaces.get(device_id=target.id, name=interface)
    if source_interface is None or target_interface is None:
        raise ValueError("source or destination interface not found")
    ret = Result(
        task=f"{self.name}:move_ip",
        resources=[instance],
        status="unchanged",
        dry_run=dry_run,
        result={
            "from_device": from_device,
            "from_interface": from_interface,
            "device": device,
            "interface": interface,
            "ip_addresses": [],
            "source_primary_updates": {},
            "destination_primary_updates": {},
        },
    )
    if source_interface.id == target_interface.id:
        return ret
    addresses = list(nb.ipam.ip_addresses.filter(interface_id=source_interface.id))
    if not addresses:
        return ret
    # Update only assignment fields so IP records and metadata are preserved.
    updates = []
    candidates = {4: [], 6: []}
    for address in addresses:
        candidates[ipaddress.ip_interface(address.address).version].append(
            address.id
        )
        updates.append(
            {
                "id": address.id,
                "assigned_object_type": "dcim.interface",
                "assigned_object_id": target_interface.id,
            }
        )
        ret.result["ip_addresses"].append(
            {"id": address.id, "address": address.address}
        )
    # Choose all primaries before any writes; ambiguous choices must not move IPs.
    source_updates = {}
    target_updates = {}
    for family in (4, 6):
        field = f"primary_ip{family}"
        primary = getattr(source, field)
        primary_is_moving = primary is not None and primary.id in candidates[family]
        if primary_is_moving:
            source_updates[field] = None
        if not is_primary or not candidates[family]:
            continue
        if primary_is_moving:
            target_updates[field] = primary.id
        elif len(candidates[family]) == 1:
            target_updates[field] = candidates[family][0]
        else:
            raise ValueError(
                f"cannot select IPv{family} primary from multiple moved addresses"
            )
    ret.result["source_primary_updates"] = source_updates
    ret.result["destination_primary_updates"] = target_updates
    job.event(
        f"moving {len(addresses)} IP addresses from '{from_device}:{from_interface}' to '{device}:{interface}', dry_run={dry_run}"
    )
    if not dry_run:
        # Clear source primaries before their IPs belong to another device.
        if source_updates:
            source.update(source_updates)
        nb.ipam.ip_addresses.update(updates)
        if target_updates:
            target.update(target_updates)
    ret.status = "updated"
    return ret