Thursday, May 21, 2026

Copy groups from one NSX to another

Migrating from one NSX deployment to another in parallel, there are some things that VMware hasn't offered. Among them is the easy export/import of configurations, particularly static items that (theoretically) could be created before any VMs are actually alive and present on the new system.

I'm slowly working through these details, and I've found that while I can manually create groups, it won't let me create most of the criteria—like membership via tag—until VMs are present. But playing with the REST API—which I wanted to do anyway, because who actually wants to manually create several hundred groups?!—I discovered that I could programmatically create the criteria—including tags!—for groups. The result is this script, which will copy non-system-owned groups from one NSX instance to another.


Note: Group IDs are case sensitive and may not match the display name! I have several instances where I would end up with a seemingly-duplicated group when I ran the script, but it was just a group I had built manually having a different ID than what was in the source, even though the display names were identical.

Copy-NsxGroups.ps1

<# Copy-NsxGroups.ps1 .SYNOPSIS Copies groups and their criteria from one NSX manager instance to another. .DESCRIPTION Using the REST API, copy groups from source NSX manager to another. If the target group doesn't exist, it is created and criterion for group membership is added. If the target group exists, criterion for group membership is updated. System-owned groups are ignored. .PARAMETER SrcNSXmgr (mandatory) The NSX Manager instance with groups to copy .PARAMETER SrcUsr (mandatory) .PARAMETER SrcPwd (mandatory) The username & password used to connect to the source NSX Manager .PARAMETER DstNSXmgr (mandatory) The vCenter where one or more VMs will be migrated to .PARAMETER DstUsr (mandatory) .PARAMETER DstPwd (mandatory) The username & password used to connect to the NSX Manager .NOTES Version: 1.0 Author: Jim Millard Creation Date: 19-May-2026 Purpose/Change: Initial development #> param ( [string] $SrcNSXmgr, [string] $SrcUsr, [string] $SrcPwd, [string] $DstNSXmgr, [string] $DstUsr, [string] $DstPwd ) #---------------------------------------------------------[Initialisations]-------------------------------------------------------- #Requires -Modules VMware.VimAutomation.Core #Set Error Action to throw an exception $ErrorActionPreference = 'Stop' $DfltSrcURL = $null $DfltDstURL = $null #-----------------------------------------------------------[Functions]------------------------------------------------------------ function Read-Param { <# generic function to grab input from the user, providing for defaults if there's no entry provided #> param( $prompt, $default, [switch] $AsSecureString, [switch] $MaskInput ) # Get the input if($AsSecureString) { # Special handling for password entry if($default){ $value = Read-Host "$prompt [*****]" -MaskInput } else { $value = Read-Host "$prompt" -MaskInput } if ($value) { #user entered something return ConvertTo-SecureString -AsPlainText -Force $value } if ($default.Value) { #user didn't enter anything, and default is non-null return ConvertTo-SecureString -AsPlainText -Force $default.Value } return '' } elseif($MaskInput){ if($default){ $value = Read-Host "$prompt [*****]" -MaskInput } else { $value = Read-Host "$prompt" -MaskInput } if ($value) { return $value } else { return $default } } else { if($default){ $value = Read-Host ("$prompt [{0}]" -f $default) } else { $value = Read-Host "$prompt" } if ($value) { return $value } else { return $default } } } function Get-ApiCred { param ( [string] $usr, [string] $pswd ) $pair = "$usr`:$pswd" return [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($pair)) } function Get-NSXApi { param ( [string] $URL, #NSX Manager address by FQDN or input [string] $usr, #username [string] $pswd, #plaintext password [string] $DfltURL, #use as default URL [string] $DfltUsr, [string] $DfltPwd, [string] $FriendlyName # Friendly/common name to identify NSX Manager instannce ) # Get NSX Manager details if not provided on the command line if(-not $URL) { $URL = Read-Param "Enter $FriendlyName NSX Manager IP or FQDN" $DfltURL } if(-not $usr) { $usr = Read-Param "Enter $FriendlyName username" $DfltUsr } if(-not $pswd) { $pswd = Read-Param "Enter $FriendlyName password" $DfltPwd -MaskInput } if (-not $pswd) { throw 'Password is NULL.' } # Create API object $hNSXmgr = [PSCustomObject]@{ URL = "https://$URL/policy/api/v1" Credential = Get-ApiCred -usr $usr -pswd $pswd } return $hNSXmgr } function Tidy-Expressions{ param ( $expression ) $explist = @() foreach ($exp in $expression) { $newExp = [PSCustomObject]@{ resource_type = $exp.resource_type } switch ($newExp.resource_type){ 'Condition' { $newExp | Add-Member -MemberType NoteProperty -Name 'member_type' -Value $exp.member_type $newExp | Add-Member -MemberType NoteProperty -Name 'key' -Value $exp.key $newExp | Add-Member -MemberType NoteProperty -Name 'operator' -Value $exp.operator if ($null -ne $exp.scope_operator) { $newExp | Add-Member -MemberType NoteProperty -Name 'scope_operator' -Value $exp.scope_operator } $newExp | Add-Member -MemberType NoteProperty -Name 'value' -Value $exp.value } 'ConjunctionOperator' { $newExp | Add-Member -MemberType NoteProperty -Name 'conjunction_operator' -Value $exp.conjunction_operator } 'NestedExpression' { # nested expressions are not individual expressions, so recurse through the nested criteria $nested = @(Tidy-Expressions $exp.expressions) $newExp | Add-Member -MemberType NoteProperty -Name 'expressions' -Value $nested } 'MACAddressExpression' { $newExp | Add-Member -MemberType NoteProperty -Name 'mac_addresses' -Value $exp.mac_addresses } 'IPAddressExpression' { $newExp | Add-Member -MemberType NoteProperty -Name 'ip_addresses' -Value $exp.ip_addresses } 'ExternalIDExpression' { $newExp | Add-Member -MemberType NoteProperty -Name 'member_type' -Value $exp.member_type $newExp | Add-Member -MemberType NoteProperty -Name 'external_ids' -Value $exp.external_ids } default { throw ("unexpected resource type: {$}" -f $exp.resource_type) } } $explist += $newExp } return $explist } #-----------------------------------------------------------[Execution]------------------------------------------------------------ $DfltSrcUsr = $null $DfltSrcPwd = $null $DfltDstUsr = $null $DfltDstPwd = $null $SrcAPI = Get-NSXApi -FriendlyName '*SOURCE*' -DfltURL $DfltSrcURL -DfltUsr $DfltSrcUsr -DfltPwd $DfltSrcPwd $DstAPI = Get-NSXApi -FriendlyName '*DESTINATION*' -DfltURL $DfltDstURL -DfltUsr $DfltDstUsr -DfltPwd $DfltDstPwd $SrcJSON = Invoke-RestMethod -Uri "$($SrcAPI.URL)/infra/domains/default/groups?sort_ascending=true&sort_by=display_name" -Method Get -Headers @{ "Authorization" = "Basic $($SrcAPI.Credential)" "Content-Type" = "application/json" } -Body '{}' -SkipCertificateCheck #### Get the source details foreach ($grp in $SrcJSON.results) { ###ignore system-owned groups; these should be auto-created & maintained anyway if ($grp._system_owned) { continue } ### DefaultMaliciousIpGroup isn't identified as a system group, but we need to treat it as one if ($grp.display_name -eq 'DefaultMaliciousIpGroup') {continue} <# In order to revise an existing object, the _revision property is required to keep multiple clients from causing conflicting updates. Get the current revision #> $rev = -1 try { $DstGrp = Invoke-RestMethod -Uri "$($DstAPI.URL)/infra/domains/default/groups/$($grp.id)" -Method Get -Headers @{ "Authorization" = "Basic $($DstAPI.Credential)" "Content-Type" = "application/json" } -Body '' -SkipCertificateCheck } catch { # group doesn't exist, so create it as part of updating it $rev = 0 } if($rev -eq -1) { $rev = $DstGrp._revision } $oBody = [PSCustomObject]@{ expression = @(Tidy-Expressions $grp.expression) group_type = $grp.group_type id = $grp.id display_name = $grp.display_name description = "programmatically transferred from $($SrcAPI.URL)" _revision = $rev } $jBody = ConvertTo-Json $oBody -Depth 10 Write-Host ("Copying [{0}]..." -f $grp.display_name) try { Invoke-RestMethod -Uri "$($DstAPI.URL)/infra/domains/default/groups/$($grp.id)" -Method Patch -Headers @{ "Authorization" = "Basic $($DstAPI.Credential)" "Content-Type" = "application/json" } -Body $jBody -SkipCertificateCheck } catch { Write-Host $_.ErrorDetails } }

No comments:

Post a Comment