Integrations

Duplicating F5 Objects

Why can't F5 just allow renaming?

2025-09-24

If you've ever wanted to rename an object on the F5 Big-IP, you've likely found that names are fixed for most (maybe all?) objects. This means if you misspelled something and realized it after submitting your new configuration object, you have to do it all over again. Or, if you're in the unfortunate situation where a new naming convention is being established, you have to create a ton of objects all over again.

The F5 GUI is a bit of a nightmare - especially when you're trying to figure out how config objects relate to one another. Helpfully, SSL certificates show which SSL profiles they are referenced by, and they allow you to navigate directly to the SSL profile config page. But SSL profiles don't show which LTMs or access policies reference them. LTMs reference pools, but only as a drop-down selector which does not link directly to the pool object in the navigation. Conversely, pools just float out there by themselves without any linking reference to the LTMs that use them.

All that to say, our workaround is to rename all objects according to their LTM, and the LTM is the FQDN of the service. For example, if the service is hosted at foo.lab.local, the name of the LTM is foo.lab.local, the name of the SSL profile is foo.lab.local, the name of the pool is foo.lab.local, and the name of any unique config objects (iRules, persistence profiles, service monitors, ASMS policies, etc.) will be foo.lab.local. This helps us tie everything together without having to bounce between pages in the UI.

This is where the ability to rename an object would really be helpful. F5 has a mv command that allows for this, but there's also a big scary disclaimer and some anecdotal evidence that it shouldn't be used in production. Given that the documentation still says it's experimental, I decided to pass on that option. Additionally, F5's official KB still indicates you can't rename objects. I find it kind of funny that the support article is titled "Renaming BIG-IP configuration objects." Apparently, BIG-IQ has a Duplicate button in the UI that's never made it to the BIG-IP. The official guidance: delete the old and create new. Never mind that these may be production services, and there could be a lot of them.

There are other recommendations out there, many tmos one-liners piping to sed to rename and then generate new. But make sure you merge or you will overwrite your entire config with the single object you're trying to rename. Also, make sure you save the config, or it won't persist the changes on reboot. No thanks. Instead, we'll just do it with the API.

Copying Pools

The script below takes a sourcePoolName and destPoolName and copies an existing pool with each of its members to a new pool with a new name.

[CmdletBinding()]
param (
    [Parameter(Mandatory=$true, Position=0)]
    [string]
    $sourcePoolName,
    [Parameter(Mandatory=$true, Position=1)]
    [string]
    $destPoolName,
    [Parameter(Mandatory=$false)]
    $bigIpHostIpOrHostname,
    [Parameter(Mandatory=$false)]
    [pscredential]
    $credentials
)

if(-not $bigIpHostIpOrHostname){
    $bigIP = "https://myf5.some.domain"
}
else{
    $bigIp = "https://$($bigIpHostIPorHostName)"
}
if(-not $credentials){
    $credentials = Get-Credential
}


$poolUrl = "$($bigIP)/mgmt/tm/ltm/pool/$($sourcePoolName)"
try{
    $pool = Invoke-Restmethod -Uri $poolUrl `
                            -Method Get `
                            -Credential $credentials `
                            -SkipCertificateCheck `
                            -ErrorAction Stop
    if($pool.count -gt 0){
        Write-Output "Fetched $($sourcePoolName) from $($bigIP)"
    }
    else{
        Write-Warning "No matching pool found: $($sourcePoolName). Exiting..."
        exit 1
    }
}
catch{
    Write-Error "Failed to fetch source pool: $($sourcePoolName) from $($bigIP). Exception: $($_.Exception.Message)"
    exit 1
}

$NewPool = @{
    name = $destPoolName
    loadBalancingMode = $pool.loadBalancingMode
    monitor = $pool.monitor
    members = @()
}

try{
    $members = Invoke-RestMethod -uri $pool.membersReference.link.replace("https://localhost",$bigIP) `
                -method Get `
                -Credential $credentials `
                -SkipCertificateCheck `
                -ErrorAction Stop
    if($members.count -gt 0){
        Write-Output "Fetched source pool members for $($sourcePoolName)"
    }
    else{
        Write-Warning "No members in pool: $($sourcePoolName). Exiting..."
        exit 1
    }
}
catch{
    Write-Error "Failed to fetch source pool members for $($sourcePoolName)"
    exit 1
}

foreach($member in $members.items){
    $NewPool.members += @{
        name = $member.name
        address = $member.address
    }
}
$jsonbody = $newpool | convertto-json -Depth 3 -Compress
Write-Output "New pool definition for $($destPoolName): $($jsonbody)"

try{
    $res = Invoke-RestMethod -Uri "$bigIP/mgmt/tm/ltm/pool" `
                    -Method Post `
                    -Credential $credentials `
                    -Body $jsonbody `
                    -ContentType "application/json" `
                    -SkipCertificateCheck `
                    -ErrorAction Stop
    if($res.name){
        Write-Output "Generated pool $($destPoolName) successfully on $($bigIP)"
    }
    else{
        Write-Warning "Pool generation unsuccessful. Output: $($res | Out-String)"
    }

}
catch{
    Write-Error "Pool generation failed with exception: $($_.Exception.Message)"
}

Copying LTMs

LTMs are more complicated and have the potential to impact services. You can't have an LTM with the same source and destination values. There are two approaches with the script below.

Two-stage approach

The first approach allows you to stage your copies before a scheduled cutover. In the staging process, an LTM is cloned to a new LTM on an alternate destination port (e.g. port 444 for an HTTPS service on 443). In the second stage - typically completed during a scheduled maintenance window - the original LTM is either deleted or moved to yet another alternate destination port (e.g. 442), and the new LTM claims the original service port (e.g. HTTPS/443).

One-shot approach

If you intend to do this in a single step, the script provides a replaceExisting parameter. When this parameter is set to $true, the service port is updated to an alternate value (current port + 1) on the source LTM prior to posting the payload for the new LTM. The original LTM will remain in the config, but the LTM will be disabled. This allows for rollback in the event something goes wrong - just enable the LTM and update the destination port.

script

[CmdletBinding()]
param (
    [Parameter(Mandatory=$true, Position=0)]
    [string]
    $sourceVipName,
    [Parameter(Mandatory=$true, Position=1)]
    [string]
    $destVipName,
    [Parameter(Mandatory=$true)]
    [bool]
    $enable=$true,
    [Parameter(Mandatory=$false)]
    [int]
    $destPortNumber,
    [Parameter(Mandatory=$true)]
    [bool]
    $replaceExisting=$false,
    [Parameter(Mandatory=$false)]
    $bigIpHostIpOrHostname,
    [Parameter(Mandatory=$false)]
    [pscredential]
    $credentials
)

# Define some variables if missing from input
if(-not $bigIpHostIpOrHostname){
    $bigIP = "https://myf5.some.domain"
}
else{
    $bigIp = "https://$($bigIpHostIpOrHostname)"
}
if(-not $credentials){
    $credentials = Get-Credential
}


# Function to remove general properties from objects
function Remove-ObjectProperties($object){
    $propsToRemove = @("selfLink", "fullPath", "generation", "kind", "enabled", "disabled",
                       "lastModifiedTime", "creationTime", "policiesReference",
                       "vsIndex", "profilesReference", "rulesReference", "description")
    foreach($attr in $propsToRemove){
        if($object.psobject.Properties.name -contains $attr){
            $object.psobject.Properties.Remove($attr)
        }
    }
    return $object
}


# Fetch source LTM details
$vipUrl = "$($bigIP)/mgmt/tm/ltm/virtual/$($sourceVipName)"
try{
    $vip = Invoke-Restmethod -Uri $vipUrl `
                            -Method Get `
                            -Credential $credentials `
                            -SkipCertificateCheck `
                            -ErrorAction Stop
    if($vip.Count -gt 0){
        Write-Output "Fetched $($sourceVipName) from $($bigIP)"
    }
    else{
        Write-Warning "No matching VIP found: $($sourceVipName). Exiting..."
        exit 1
    }
}
catch{
    Write-Error "Failed to fetch source VIP: $($sourceVipName) from $($bigIP). EXCEPTION: $($_.Exception.Message)"
    exit 1
}

# Clone source VIP to new VIP object, set new values, remove unneeded properties
$newVip = $vip.psobject.copy()
$newVip.name = $destVipName
if($replaceExisting){
    # Need to patch source VIP and keep original destination port on the new VIP
    $destPort = [int]$vip.destination.split(':')[1] + 1
    $vipDest = "$($vip.destination.Split(':')[0]):$($destPort)"
    $body = @{
        destination = $vipDest
        disabled = $true
    } | ConvertTo-Json
    try{
        $res = Invoke-RestMethod -Uri $vipUrl -Method Patch -Credential $credentials -Body $body -ContentType "application/json" -SkipCertificateCheck
        Write-Output "Updated $($vip.name) destination to $($vipDest)"
    }
    catch{
        Write-Error "Failed to update $($vip.name) with new destination $($vipDest). Exception: $($_.Exception.Message)"
    }
}
else{
    $newVip.destination = $vip.destination.Split(":")[0] + ":$($destPortNumber)"
}
$newVip = Remove-ObjectProperties $newVip


if($enable){
    $newVip | Add-Member -NotePropertyName "enabled" -NotePropertyValue $true -Force
}
else{
    if($replaceExisting){
        Write-Warning "Option to replace existing VIP is set, but the enable flag was set to false. The service will be down until the LTM is enabled as a result."
        Read-Host "Press any key to continue or CTRL+C to escape"
    }
    $newVip | Add-Member -NotePropertyName "disabled" -NotePropertyValue $true -Force
}


# Fetch any referenced profile objects from the source VIP and add them to the new VIP
if($vip.profilesReference){
    $profileUrl = $vip.profilesReference.link.Replace("https://localhost", $bigIp)
    try{
        $profiles = (Invoke-RestMethod -Uri $profileUrl -Method Get -Credential $credentials -SkipCertificateCheck -ErrorAction Stop).items
        Write-Output "Fetched profiles for $($sourceVipName)"
    }
    catch{
        Write-Warning "Failed to fetch profiles for $($sourceVipName). EXCEPTION: $($_.Exception.Message)"
    }
}

if($profiles){
    $profileBody = @()
    foreach($item in $profiles){
        $profileBody += Remove-ObjectProperties $item
    }
    $newVip | Add-Member -NotePropertyName "profiles" -NotePropertyValue $profileBody
}


 # Fetch referenced profile objects from the source VIP and add them to the new VIP
if($vip.policiesReference){
    $policyUrl = $vip.policiesReference.link.Replace("https://localhost", $bigIp)
    try{
        $policies = (Invoke-RestMethod -Uri $policyUrl -Method Get -Credential $credentials -SkipCertificateCheck -ErrorAction Stop).items
        Write-Output "Fetched policies for $($sourceVipName)"
    }
    catch{
        Write-Warning "Failed to fetch policy for $($sourceVipName). EXCEPTION: $($_.Exception.Message)"
    }
}

# Fetch referenced policy objects from the source VIP and add them to the new VIP
if($policies){
    $policyBody = @()
    foreach($item in $policies){
        $policyBody += Remove-ObjectProperties $item
    }
    $newVip | Add-Member -NotePropertyName "policies" -NotePropertyValue $policyBody
}

$jsonbody = $newVip | ConvertTo-Json -Depth 10

try{
    $res = Invoke-RestMethod -Uri "$bigIP/mgmt/tm/ltm/virtual" `
                    -Method Post `
                    -Credential $credentials `
                    -Body $jsonbody `
                    -ContentType "application/json" `
                    -SkipCertificateCheck `
                    -ErrorAction Stop
    if($res.name){
        Write-Output "Generated VIP for $($destVipName) successfully on $($bigIP)"
    }
    else{
        Write-Warning "VIP generation unsuccessful. Output: $($res | Out-String)"
    }

}
catch{
    Write-Error "VIP generation failed. EXCEPTION: $($_.Exception.Message)"
}

Example Script

Below is an example of command to execute the script. Since credentials are not specified, they will be requested in a prompt.

.\F5_Copy_VIP.ps1 -sourceVipName myLTM -destVipName myLTM-HTTPS-443 -enable $true -replaceExisting $true -bigIpHostIpOrHostname 192.168.1.1

Note that this script assumes you are running PowerShell 7. The SkipCertificateCheck parameter is not available on earlier versions.