Backing Up Portgroup Data with PowerShell and XML

Managing virtual standard switches (vSwitches) in a VMware environment, large or small, can cause a lot of headaches. Ensuring each portgroup is named the same, has the same VLAN and is present on each host can be a challenge. Virtual distributed switches (dvSwitches) exist to make our lives easier, but aren’t always available due to licensing or other restrictions. They have their own drawbacks, but the good generally outweighs the bad.

As I have been spending a significant amount of time in PowerShell and XML recently I wrote a script designed to backup the Portgroup configuration of all your clusters to ensure that all hosts will have the proper networking configuration during host rebuilds or additions. Let’s walk through this script and show you all the different elements.

1. Here we define and connect to the vCenter server

$vCenterFqdn = "vcenter01.domain.local"
Connect-viserver $vCenterFqdn

2. Here we are defining the path for our Portgroups.xml file. We’ll reference this variable later on.

$xmlNetworkPath="C:\Scripts\XML\Portgroups.xml"

3. Here we are checking to see if this file already exists. If it does we’ll skip ahead to the population side, but if not we’ll have to create it. $networkCheck tests the path defined in the variable $xmlNetworkPath. If that file is there, we’re writing into the console that it exists and we’re moving on. If it doesn’t exist we move on to step 4.

$networkCheck = Test-Path $xmlNetworkPath
IF ($networkCheck -eq $True){Write-Host "Portgroup file already exists. Continuing..." -foreground "Green"}
ELSE
{Write-Host "Portgroup file not created. Creating..." -foreground "Yellow"}

4. Since this is our first run, this file doesn’t exist yet so we need to create it. Populating an empty XML file is not something I ever figured out how to do. Someone much smarter than me in XML and PowerShell can figure it out, but I found a nice work around by creating an XML template. This is an outline of what our XML file will be and we’re just filling in the data as we go.

$xmlNetwork=[xml]@’ is saying everything between @’ and ‘@ will be part of this file. At the end, we’re saving this output ($xmlNetwork.Save) to the file path we defined earlier ($xmlNetworkPath).


$xmlNetwork=[xml]@'

<vSwitchConfig>
  <templates>
    <Portgroup>
      <vlanId></vlanId>
      <virtualSwitch></virtualSwitch>
      <cluster></cluster>
    </Portgroup>
  </templates>
  <Portgroups>
    <test />
  </Portgroups>
</vSwitchConfig>
‘@
$xmlNetwork.Save($xmlNetworkPath)

5. In case we run into permissions issues we want to perform an additional check to ensure that file was actually written. For the most part we’re just repeating Step 3. The difference here is we’re changing the output in “Write-Host” but also if the file isn’t created, we need to kill the script. The message is written that the file was not created, then we wait 20 seconds, then exit the script. The reason we add the “Start-Sleep” is in case this script is run by just double-clicking and not triggered from within a powershell window, the script will exit and you’ll never know why.

$networkCheck = Test-Path $xmlNetworkPath
IF ($networkCheck -eq $True){Write-Host "Portgroup file created successfully. Continuing..." -foreground "Green"}
ELSE
{Write-Host "Portgroup file not created. Exiting..." -foreground "Red"; Start-Sleep 20; Break}}

6. Now we need to read the contents of that XML file we created in XML format. The [XML] denotes that this is an XML file. $xmlNetwork is just the variable name I chose (this can be anything you want) and $xmlNetworkPath is the path to the file we defined in step 2

[XML]$xmlNetwork = Get-Content $xmlNetworkPath

7. Now comes the fun part where we actually start pulling data from vCenter. We’re going to get all the clusters in the vCenter we’re connected to. The ForEach command says that for every cluster we need to run this same command. So in each cluster we’re looking for an ESXi host that is currently connected, then we choose a random host from the cluster (get-random) and on that random host we get all the virtual portgroups that aren’t on dvSwitches.

$getClusters = Get-Cluster
ForEach ($cluster in $getClusters) {
$getPortgroups = $cluster | Get-VMHost | Where {$_.ConnectionState -ne "NotResponding"} | Get-Random | Get-VirtualPortGroup | Where {$_.key -notlike "*dvportgroup*"}

8. Now that we have all these portgroups let’s get all the data we need in them. We perform another ForEach on every Portgroup that was on that host. The $testPg variable is used to see if the current Portgroup name ($pgName.Name) is present in the XML file. What we don’t want is to re-add the same Portgroup name over and over again, we just want a single reference to the portgroup name and we’ll add another entry for the clusters that contain it. $testPg -eq $null means if the portgroup name isn’t there, we’ll create a new entry.

foreach ($pgName in $getPortGroups) {
$testPg = $xmlNetwork.vSwitchConfig.Portgroups.Portgroup|Where {$_.name -eq $pgName.Name}
IF ($testPg -eq $null)

9. If the Portgroup isn’t there we need to create a new entry. Looking at the XML file outline we created in Step 4 you see the tag. This is used to work around my inability to create new entries in XML. We copy the section below and then fill out the data as we go


<Portgroup>
  <vlanId></vlanId>
  <virtualSwitch></virtualSwitch>
  <cluster></cluster>
</Portgroup>

The variable “$xmlNetwork” was defined in Step 6 and is the name of our XML file. The addition of “.vSwitchConfig” is the top level tag in our XML file from Step 4.
The variable “$parentNode” defines the top-level of the XML file we’ll be using from our template.
The variable “$destinationNode” defines where this copied data is going to be placed.
The variable “$cloneNode” defines what tag we’re copying from $parentNode.
The variable “$addNameAttribute” is creating an attribute called “name” and the name we’re giving it is the name of the portgroup we’re pulling from vCenter. Adding “.Value = ” to the end of this variable gives us the ability to define the name when we’re creating the new portgroup tag.
The variable $newNode is used to create the new Portgroup tag and then “.InnerXML =” allows us to chose the template tag from the XML file.
(The use of [void] is something I don’t fully grasp. This is the only way it works, but I can’t tell you why.)
Using $destinationNode.AppendChild is saying place this new tag into the $destinationNode location. $newNode is the data that’s being added. “.Attributes” allows us to assign the new name Attribute we defined and “.Append($addNameAttribute)” places that attribute tag.
The variable $updateNetwork is a lookup in the XML file to find a portgroup with the tag we just created.
Once we find that portgroup, $updateVLAN, $updateVirtualSwitch, and $updateCluster are used to assign the values to these empty tags that were created.


{
$parentNode = $xmlNetwork.vSwitchConfig.templates
$destinationNode = $xmlNetwork.vSwitchConfig.Portgroups
$cloneNode = $parentNode.SelectSingleNode("Portgroup")
$addNameAttribute = $xmlNetwork.CreateAttribute("name")
$addNameAttribute.Value = $pgName.Name
$newNode = $xmlNetwork.CreateElement("Portgroup")
$newNode.InnerXML = $cloneNode.InnerXML
[void]$destinationNode.AppendChild($newNode).Attributes.Append($addNameAttribute)
$updateNetwork = ($xmlNetwork.vSwitchConfig.Portgroups.Portgroup|Where {$_.name -eq $pgName.Name})
$updateVLAN = $updateNetwork.vlanId = $pgName.VLanId.ToString()
$updateVirtualSwitch = $updateNetwork.virtualSwitch = $pgName.VirtualSwitchName
$updateCluster = $updateNetwork.cluster = $cluster.Name
}

10. In the event this portgroup already exists, we have the ELSE portion of our IF statement we started in Step 8. Here we’re referencing the portgroup in question and checking to see if the cluster tag has already been added. We look up the portgroup name “$testPg” and search for a cluster that matches the cluster we’re currently working with. If that cluster doesn’t exist, we add a new element ($xmlNetwork.CreateElement(“cluster”)) and populate its value ($addCluster.InnerText = $cluster.Name)

If that cluster tag already exists for that Portgroup, we don’t do anything “{}”


ELSE {($testCluster = $testPg|Where {$_.cluster -eq $cluster.Name});
IF ($testCluster -eq $null) {
$addCluster = $xmlNetwork.CreateElement("cluster");
$addCluster.InnerText = $cluster.Name
$testPg.AppendChild($addCluster) | Out-Null }ELSE{}}
}}

11. Once all of our clusters and Portgroups have been looped through. We need to save that data to the XML file we created.

$xmlNetwork.Save($xmlNetworkPath)

12. Now that the file has been saved, we want to see what data has been written. This isn’t required, but just adds a nice little output to the screen so you can see that the data has been populated as expected.

We perform the same Get-Content command with our [XML] tag. Then we write the output of every portgroup, sorted by name, into a table so we can see all the data.


[XML]$xmlNetwork = Get-Content $xmlNetworkPath
Write-Host "The following Portgroup configuration exists in $($xmlNetworkPath)" -foreground "Yellow"
$xmlNetwork.vSwitchConfig.Portgroups.Portgroup | Sort name | Format-Table

I have obscured some of the networks and VLANs in use in my environment, but this is the output you can expect.

All together this is what the script looks like:


#Connect to the defined vCenter Server
$vCenterFqdn = "vcenter01.domain.local"
Connect-viserver $vCenterFqdn

#Define XML File Path
$xmlNetworkPath="C:\Scripts\XML\Portgroups.xml"

#Check for Portgroup XML File
$networkCheck = Test-Path $xmlNetworkPath
IF ($networkCheck -eq $True){Write-Host "Portgroup file already exists. Continuing..." -foreground "Green"}
ELSE
{Write-Host "Portgroup file not created. Creating..." -foreground "Yellow"

#Create XML Portgroup File

$xmlNetwork=[xml]@'

<vSwitchConfig>
  <templates>
    <Portgroup>
      <vlanId></vlanId>
      <virtualSwitch></virtualSwitch>
      <cluster></cluster>
    </Portgroup>
  </templates>
  <Portgroups>
    <test />
  </Portgroups>
</vSwitchConfig>
'@
$xmlNetwork.Save($xmlNetworkPath)

$networkCheck = Test-Path $xmlNetworkPath
IF ($networkCheck -eq $True){Write-Host “Portgroup file created successfully. Continuing…” -foreground “Green”}
ELSE
{Write-Host “Portgroup file not created. Exiting…” -foreground “Red”;Start-Sleep 20; BREAK}}

#Get contents of XML file
[XML]$xmlNetwork = Get-Content $xmlNetworkPath

#Gather all hosts in each cluster
$getClusters = Get-Cluster
foreach ($cluster in $getClusters) {
$getPortgroups = $cluster | Get-VMHost | Where {$_.ConnectionState -ne “NotResponding”} | Get-Random | Get-VirtualPortGroup | Where {$_.key -notlike “*dvportgroup*”}
foreach ($pgName in $getPortGroups) {
$testPg = $xmlNetwork.vSwitchConfig.Portgroups.Portgroup|Where {$_.name -eq $pgName.Name}
IF ($testPg -eq $null) {
$parentNode = $xmlNetwork.vSwitchConfig.templates
$destinationNode = $xmlNetwork.vSwitchConfig.Portgroups
$cloneNode = $parentNode.SelectSingleNode(“Portgroup”)
$addNameAttribute = $xmlNetwork.CreateAttribute(“name”)
$addNameAttribute.Value = $pgName.Name
$newNode = $xmlNetwork.CreateElement(“Portgroup”)
$newNode.InnerXML = $cloneNode.InnerXML
[void]$destinationNode.AppendChild($newNode).Attributes.Append($addNameAttribute)
$updateNetwork = ($xmlNetwork.vSwitchConfig.Portgroups.Portgroup|Where {$_.name -eq $pgName.Name})
$updateVLAN = $updateNetwork.vlanId = $pgName.VLanId.ToString()
$updateVirtualSwitch = $updateNetwork.virtualSwitch = $pgName.VirtualSwitchName
$updateCluster = $updateNetwork.cluster = $cluster.Name
} ELSE {($testCluster = $testPg|Where {$_.cluster -eq $cluster.Name});
IF ($testCluster -eq $null) {
$addCluster = $xmlNetwork.CreateElement(“cluster”);
$addCluster.InnerText = $cluster
$testPg.AppendChild($addCluster) | Out-Null }ELSE{}}
}}
$xmlNetwork.Save($xmlNetworkPath)

#Display Results
[XML]$xmlNetwork = Get-Content $xmlNetworkPath
Write-Host “The following Portgroup configuration exists in $($xmlNetworkPath)” -foreground “Yellow”
$xmlNetwork.vSwitchConfig.Portgroups.Portgroup | Sort name | Format-Table