Showing posts with label PowerShell. Show all posts
Showing posts with label PowerShell. Show all posts

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 } }

Thursday, May 14, 2026

Retrieving Groups from NSX-T

This might seem like a bit of a theme developing, but in my work life I'm assisting in the migration from a "standalone" vSphere environment to a VCF 9 environment, and having some of this information from the old environment is helpful for building and auditing the new environment.

So this script will dump groups from NSX-T into a CSV-formatted text. It was a fun one to code because of the way groups are handled: some sort of boolean logic—up to and including selection of objects by individual ID—is possible, and when you're mixing & matching [AND] and [OR] clauses, you are starting to nest evaluation scope, similar to the way parenthesis work in math (remember PEMDAS?).

At any rate, this code uses 'recursion,' where a function calls itself with new arguments rather than creating deeper and deeper loops. When the language permits it—and PowerShell does—it becomes a powerful tool for writing efficient and highly readable (in my experience) code. It can also create pitfalls for a coder who doesn't build in the "escape hatch" that keeps the recursion from happening infinitely, but that's an entirely different discussion.

So with the help of recursion, this script will dive into the criteria used to define group membership and assemble it all together in a single field. This is output along with the group name and any description that is available from the source environment

Get-NsxGroups.ps1

FUNCTION Decode-Expression() { # decode an individual Expression # these can come in several types, so handle each using the switch control param ( [System.Object]$expr ) switch ($expr.resource_type){ 'Condition' { $str += '(' + $expr.member_type + ' [' + $expr.key + '] ' + $expr.operator + ' "' + $expr.value.replace('|','') + '")' } 'ConjunctionOperator' { $str += ' ' + $expr.conjunction_operator + ' ' } 'NestedExpression' { # nested expressions are not individual expressions, so recurse through the nested criteria $str += Decode-Expressions($expr.expressions) } 'MACAddressExpression' { $addr_string = '' foreach ($addr in $expr.mac_addresses){ $addr_string += "$addr," } $addr_string = $addr_string.SubString(0,$addr_string.Length-1) $str += '[MAC in (' + $addr_string + ')]' } 'IPAddressExpression' { $addr_string = '' foreach ($addr in $expr.ip_addresses){ $addr_string += "$addr," } $addr_string = $addr_string.SubString(0,$addr_string.Length-1) $str += '[IP in (' + $addr_string + ')]' } 'ExternalIDExpression' { $str += ($expr.external_ids.Length.ToString() + ' explicit VMs') } default { $str += '<<unhandled>>' } } return $str } FUNCTION Decode-Expressions(){ # One or more expressions--including multi-level nesting--can be used to define # what makes a group member. # This function handles iterating and recursing through all criteria that could be in # any given expression param ( [System.Object]$expr ) $str = '' if (($expr -is [Array]) -and ($expr.Length -gt 1)){ #passed object is an array of objects for ($i = 0; $i -lt $expr.Length; $i++) { $str += Decode-Expressions($expr[$i]) } } else { if ($expr.Length) { $str += Decode-Expression($expr) } else { $str += 'No criteria set' } } return $str } $dfltNSXMGR = "default NSX manager" $nsxtManager = Read-Host ("Enter NSX-T IP or FQDN [$dfltNSXMGR]" -f $dfltNSXMGR) if (-not $nsxtManager) { $nsxtManager = "https://$dfltNSXMGR" } else { $nsxtManager = "https://$nsxtManager" } $output = Read-Host "Enter output filepath" $username = Read-Host "Enter username" $secPwd = Read-Host "Enter password" -AsSecureString $password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secPwd)) $response = Invoke-RestMethod -Uri "$nsxtManager/api/v1/infra/domains/default/groups" -Method Get -Headers @{ "Authorization" = "Basic $( [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("${username}:${password}"))) )" "Content-Type" = "application/json" } -Body '{}' -SkipCertificateCheck $grp_list = @() foreach ($grp in $response.results) { if ($grp._system_owned -ne 'false') { #ignore system-owned groups; these should be auto-created anyway $grp_name = $grp.display_name $grp_descr = $grp.description $rules = Decode-Expressions($grp.expression) $row = New-Object PSObject -Property @{ Grp = $grp.display_name Descr = $grp.description Crit = $rules } $grp_list += $row } } $grp_list | Select-Object 'Grp','Descr','Crit' | Export-CSV -Path $output -NoTypeInformation

Note: I made the decision to ignore any explicit VMs that were in these groups. It was a business decision driven by several factors:

  • There weren't many groups using this feature
  • Most groups using the feature had references to VMs that no longer existed

Wednesday, May 13, 2026

Retrieving tags from NSX-T

In the process of preparing a new NSX-T environment, a request came for a dump of the tags in the production environment so that we could match those in the new one. PowerShell & RESTful API to the rescue!

Note: after working with VMware solutions since 2005 and for the company itself for just over 6 years, I have it ingrained to call it "VMware". Aside from this paragraph, you'll probably never see me write "VMware by Broadcom" or even something as gross as "Broadcom vSphere." Just understand that "by Broadcom" is implied until such time as the tech stack finds new ownership.

At any rate: PowerCLI, the module set for PowerShell that VMware publishes for automating parts/pieces of their software stack, is extremely light in cmdlets for interacting with NSX-T. But NSX-T has a very rich RESTful API, so that's what I'm taking advantage of.

I found several solutions that other folks had written, and for one reason or another, it just wasn't working to create output the way we needed it. So here are a couple of iterations that I wrote. The first dumps a CSV-formatted file that lists the tags and the entities that are associated with them; the second dumps a CSV-formatted file that list the VMs and the tags (if any) that are applied to them.

Get-NsxTags.ps1

function Read-Param { <# generic function to grab input from the user, providing for defaults if there's no entry provided #> param( $prompt, $default ) $value = Read-Host ("$prompt [$default]" -f $default) if (-not $value) { return $default } else { return $value } } function Get-Data { <# Perform a RESTful API call against the NSX-T manager #> param( $nsx, $usr, $secPwd, $apiPath ) $password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secPwd)) $credPair = "$($usr):$($password)" $encCreds = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($credPair)) $params = @{ Uri = "$nsx$apiPath" Method = 'GET' Headers = @{ "Authorization" = "Basic $encCreds" "Content-Type" = "application/json" } Body = '{}' SkipCertificateCheck = $true } $results = Invoke-RestMethod @params return $results.results } # All NSX-T API paths "hang" off the base path $ApiBase = "/policy/api/v1" # DFW-related stuff is off the Policies path, below. Note: the "default" domain seems to be the only one we have; technically it's a variable... $ApiTags = "$ApiBase/infra/tags" <# INPUT SECTION #> # The actual URI must have the "https://" prefix $nsxtManager = "https://" + (Read-Param "Enter NSX-T IP or FQDN" "Default VIP") $output = Read-Param "Enter output filepath" "c:\temp\get-nsxtags.csv" $username = Read-Param "Enter username" "defaultusername" $secPwd = Read-Host "Enter password" -AsSecureString <# ALGORITHM NSX provides an API to list the tags, and then individual entries provide the tag name and the scope required to retrieve the effective resources associated with them #> $itemlist=@() #storage for the output list #get all the tags $tags = Get-Data -nsx $nsxtManager -usr $username -secPwd $secPwd -apiPath $ApiTags write-host '.' -NoNewline #loop through the tags and grab the associated resources, appending the resource names to a single string field for later output for($t =0; $t -lt $tags.Length; $t++) { $items = '' if($tags[$t].tagged_objects_count -gt 0) { #if there are no objects associated with the tag, don't try and retrieve them $ApiItems = $ApiTags + '/effective-resources?scope=' + $tags[$t].scope + '&tag=' + $tags[$t].tag write-host '.' -NoNewline #this will take a while; show progress happening... $itemSet = Get-Data -nsx $nsxtManager -usr $username -secPwd $secPwd -apiPath $ApiItems foreach ($item in $itemSet) { switch ($item.target_type) { 'VirtualMachine' { $items += $item.target_display_name + ' [vm],' } 'HostTransportNode' { $items += $item.target_display_name + ' [Host],' } default { $items += $item.target_display_name + '<<undefined>>,' } } } } #add a row to the output, removing a trailing comma if needed $row = New-Object PSObject -Property @{ Tag = $tags[$t].tag Count = $tags[$t].tagged_objects_count Items = '' } if ($items.Length -gt 0) { $row.Items = $items.substring(0, $items.length-1) } $itemlist += $row } write-host '.' write-host 'Done' $itemlist | Select-Object 'Tag','Count','Items' | Export-CSV -Path $output -NoTypeInformation


Get-NSXvmTags.ps1

$dfltNSXMGR = "default" $nsxtManager = Read-Host ("Enter NSX-T IP or FQDN [$dfltNSXMGR]" -f $dfltNSXMGR) if (-not $nsxtManager) { $nsxtManager = "https://$dfltNSXMGR" } else { $nsxtManager = "https://$nsxtManager" } $output = Read-Host "Enter output filepath" $username = Read-Host "Enter username" $secPwd = Read-Host "Enter password" -AsSecureString $password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secPwd)) $response = Invoke-RestMethod -Uri "$nsxtManager/api/v1/fabric/virtual-machines?included_fields=display_name,tags" -Method Get -Headers @{ "Authorization" = "Basic $( [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("${username}:${password}"))) )" "Content-Type" = "application/json" } -Body '{}' -SkipCertificateCheck $vmlist = @(); foreach ($vm in $response.results) { $tags = '' foreach ($tag in $vm.tags) { $tags += $tag.tag + ',' } if ($tags.Length -gt 0) { $row = New-Object PSObject -Property @{ VM = $vm.display_name Tags = $tags.substring(0, $tags.length-1) } } else { $row = New-Object PSObject -Property @{ VM = $vm.display_name Tags = '' } } $vmlist += $row } $vmlist | Select-Object 'VM','Tags' | Export-CSV -Path $output -NoTypeInformation

These two are interesting in that in the first case, you use one API URI to get the tags—which provide the information (scope, tag) to use as parameters—and a child URI to get the items that are associated with that tag. But in the second case, a single URI provides a list of VMs as well as the tags that are applied to them as properties.