VMware Event Broker Appliance – Part IX – Building a New PowerCLI Function – Writing Code

In Part VIII, we prepared to write code for a new PowerCLI function using the PowerCLI template. In this post, we will dive into the code for the kn-pcli-pg-check function. This is a very long post where I explain all 200+ lines of custom code for this function. Not every function is this complex – some are much simpler.

In the right pane of my split window, I always connect to my vCenter server. I use this to test PowerCLI cmdlets so I know what kind of output to expect.

C:\git\Flings\vcenter-event-broker-appliance\examples\knative\powercli\kn-pcli-pg-check\test [pg-check ≡ +0 ~3 -0 !]> Connect-VIServer vc02.ad.patrickkremer.com

Specify Credential
Please specify server credential
User: administrator@vsphere.local
Password for user administrator@vsphere.local: ********************************


Name                           Port  User
----                           ----  ----
vc02.ad.patrickkremer.com      443   VSPHERE.LOCAL\Administrator

In the previous post I showed a little testing to ensure connectivity to vCenter. Here I’m showing you a fresh copy of handler.ps1 from the template, starting on line 78.

   #
   # Your custom code goes here 
   #
   # When sending messages back to the console, please conform to the following standards: 
   # Write-Host "$(Get-Date) - DEBUG: "
   # Write-Host "$(Get-Date) - WARN: "
   # Write-Host "$(Get-Date) - ERROR: "

   # This is the final line of your custom code.
   # Replace #REPLACE-ME# with a meaningful message showing the end of your custom code
   #Write-Host "$(Get-Date) - #REPLACE-ME# operation complete ...`n"

   Write-Host "$(Get-Date) - Handler Processing Completed ...`n"

Here is the entire completed function for reference. Custom code begins on line 78.

Function Process-Init {
   [CmdletBinding()]
   param()
   Write-Host "$(Get-Date) - Processing Init`n"

   try {
      $jsonSecrets = ${env:PG_CHECK_SECRET} | ConvertFrom-Json
   }
   catch {
      throw "`nK8s secret `$env:PG_CHECK_SECRET does not look to be defined"
   }

   # Extract all tag secrets for ease of use in function
   $VCENTER_SERVER = ${jsonSecrets}.VCENTER_SERVER
   $VCENTER_USERNAME = ${jsonSecrets}.VCENTER_USERNAME
   $VCENTER_PASSWORD = ${jsonSecrets}.VCENTER_PASSWORD
   $VCENTER_CERTIFICATE_ACTION = ${jsonSecrets}.VCENTER_CERTIFICATE_ACTION

   # Configure TLS 1.2/1.3 support as this is required for latest vSphere release
   [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 -bor [System.Net.SecurityProtocolType]::Tls13

   Write-Host "$(Get-Date) - Configuring PowerCLI Configuration Settings`n"
   Set-PowerCLIConfiguration -InvalidCertificateAction:${VCENTER_CERTIFICATE_ACTION} -ParticipateInCeip:$true -Confirm:$false

   Write-Host "$(Get-Date) - Connecting to vCenter Server $VCENTER_SERVER`n"

   try {
      Connect-VIServer -Server $VCENTER_SERVER -User $VCENTER_USERNAME -Password $VCENTER_PASSWORD
   }
   catch {
      Write-Error "$(Get-Date) - ERROR: Failed to connect to vCenter Server"
      throw $_
   }

   Write-Host "$(Get-Date) - Successfully connected to $VCENTER_SERVER`n"

   Write-Host "$(Get-Date) - Init Processing Completed`n"
}

Function Process-Shutdown {
   [CmdletBinding()]
   param()
   Write-Host "$(Get-Date) - Processing Shutdown`n"

   Write-Host "$(Get-Date) - Disconnecting from vCenter Server`n"

   try {
      Disconnect-VIServer * -Confirm:$false
   }
   catch {
      Write-Error "$(Get-Date) - Error: Failed to Disconnect from vCenter Server"
   }

   Write-Host "$(Get-Date) - Shutdown Processing Completed`n"
}

Function Process-Handler {
   [CmdletBinding()]
   param(
      [Parameter(Position=0,Mandatory=$true)][CloudNative.CloudEvents.CloudEvent]$CloudEvent
   )

   # Decode CloudEvent
   try {
      $cloudEventData = $cloudEvent | Read-CloudEventJsonData -Depth 10
   }
   catch {
      throw "`nPayload must be JSON encoded"
   }

   try {
      $jsonSecrets = ${env:PG_CHECK_SECRET} | ConvertFrom-Json
   }
   catch {
      throw "`nK8s secret `$env:PG_CHECK_SECRET does not look to be defined"
   }

   $VM_WATCH_TAGS = ${jsonSecrets}.VM_WATCH_TAGS
   $PG_WATCH_TAGS = ${jsonSecrets}.PG_WATCH_TAGS

   $deviceUnchanged = ($NULL -eq $cloudEventData.ConfigSpec.DeviceChange )
   if(${env:FUNCTION_DEBUG} -eq "true") {
      Write-Host "$(Get-Date) - DEBUG: ConfigSpec.DeviceChange Is Null? $($deviceUnchanged)`n"
   }

   # If no devices changed, then the NIC could not have been updated to a different portgroup
   if ($deviceUnchanged)
   {
      Write-Host "$(Get-Date) - No devices changed.`n"
      return
   }

   # Build the MoRef ID
   $vmID = $cloudEventData.Vm.Vm.Type + "-" + $cloudEventData.Vm.Vm.Value
   if(${env:FUNCTION_DEBUG} -eq "true") {
      Write-Host "$(Get-Date) - DEBUG: Vm.Vm.Type Is Null? $($NULL -eq $cloudEventData.Vm.Vm.Type)`n"
      Write-Host "$(Get-Date) - DEBUG: Vm.Vm.Value Is Null? $($NULL -eq $cloudEventData.Vm.Vm.Value)`n"
      Write-Host "$(Get-Date) - DEBUG: vmID is $vmID`n"
   }

   # Retrieve the VM object by MoRef ID
   try {
      $vm = Get-VM -id $vmID
   }
   catch {
      Write-Host "$(Get-Date) - ERROR: unable to retrieve VM ID $vmID`n"
      throw $_
   }

   # Retrieve all tags on the VM
   Write-Host "$(Get-Date) - Retrieving tags on $($vm.Name)`n"
   try {
      $vmTags = $vm | Get-TagAssignment
   }
   catch {
      Write-Host "$(Get-Date) - ERROR: unable to retrieve tags for $($vm.Name)`n"
      throw $_
   }

   if(${env:FUNCTION_DEBUG} -eq "true") {
      Write-Host "$(Get-Date) - DEBUG: Tags found on VM: $($vmTags.tag)`n"
      Write-Host "$(Get-Date) - DEBUG: Tags to monitor: $($VM_WATCH_TAGS)`n"
   }

   # Search through the tags found on the VM and compare them to the VM tags specified in the secret. 
   # If any match is found, break out of the loop - finding any match means the VM is tagged for further inspection
   $checkVM = $false
   :outer foreach ($tag in $vmTags) {
      if(${env:FUNCTION_DEBUG} -eq "true") {
         Write-Host "$(Get-Date) - DEBUG: Comparing VM tag: $($tag.Tag) on VM $($vm.Name)`n"
      }
      foreach ($watchTag in $VM_WATCH_TAGS) {
         if(${env:FUNCTION_DEBUG} -eq "true") {
            Write-Host "$(Get-Date) - DEBUG: Comparing watch tag: $($watchTag)`n"
         }
         if ($watchTag -eq $tag.Tag) {
            if(${env:FUNCTION_DEBUG} -eq "true") {
               Write-Host "$(Get-Date) - DEBUG: Match found for: $($watchTag), breaking outer loop`n"
            }
            $checkVM = $true
            break outer
         }
      }
   }

   # If the VM isn't tagged, no further inspection is necessary
   if ($checkVM -eq $false) {
      Write-Host "$(Get-Date) - VM not tagged for monitoring, no inspection required.`n"
      return
   }

   Write-Host "$(Get-Date) - Match found for $($vm.Name), checking portgroups`n"

   # Try retrieving the VM's network adapters
   try {
      $networkAdapters = $vm | Get-NetworkAdapter
   }
   catch {
      Write-Host "$(Get-Date) - ERROR: unable to retrieve retrieve network adapters for $($vm.Name)`n"
      throw $_
   }

   # This hash will store any network adapters that are attached to unapproved portgroups
   $networkAdapterHash = @{}

   foreach ($nic in $networkAdapters) {
      if(${env:FUNCTION_DEBUG} -eq "true") {
         Write-Host "$(Get-Date) - DEBUG: VM $($vm.Name) - NIC $($nic.Name) - PortGroup $($nic.NetworkName)`n"
      }
      # If the portgroup is a VSS portgroup, it will have a backing network property
      $vssBackingNetwork = $nic.extensiondata.Backing.Network
      # If the portgroup is a DVS portgroup, it will have a PortgroupKey property
      $dvPortGroupKey = $nic.extensiondata.Backing.Port.PortgroupKey
      $vNetworkID = $null

      if ($NULL -ne $vssBackingNetwork) {
         if(${env:FUNCTION_DEBUG} -eq "true") {
            Write-Host "$(Get-Date) - DEBUG: VSS Backing network is $($vssBackingNetwork)`n"
         }
         # vssBackingNetwork is already in MoRef ID format
         $vNetworkID = $vssBackingNetwork
      }
      elseif ($NULL -ne $dvPortGroupKey) {
         if (${env:FUNCTION_DEBUG} -eq "true") {
            Write-Host "$(Get-Date) - DEBUG: DVS Port Group Key is $($dvPortGroupKey)`n"
         }
         # Unlike the VSS above, We have to build the MoRef ID for VDS
         $vNetworkID = "DistributedVirtualPortgroup-" + $dvPortGroupKey
      } else {
         Write-Host "$(Get-Date) - ERROR: Could not determine network type`n"
         throw "$(Get-Date) - ERROR: Could not determine network type for $($nic.Name)`n"
      }

      if(${env:FUNCTION_DEBUG} -eq "true") {
         Write-Host "$(Get-Date) - DEBUG: Virtual Network ID: $($vNetworkID)`n"
      }

      # Now that we've determined the ID, we can try retrieving the portgroup virtual network information
      try {
         $pg = Get-VirtualNetwork -Id $vNetworkID
      }
      catch {
         Write-Host "$(Get-Date) - ERROR: unable to retrieve retrieve virtual network information for $($vNetworkID)`n"
         throw $_
      }

      # Retrieve the tag assignments for the port group
      try {
         $pgTags = $pg | Get-TagAssignment
      }
      catch {
         Write-Host "$(Get-Date) - ERROR: unable to retrieve retrieve tag assignments for $($pg.Name)`n"
         throw $_
      }

      # If $null, the NIC is on a pg with no tags - add it to the hash table
      if ($null -eq $pgTags) {
         $networkAdapterHash[$nic.id] = "$($pg.Name)"
      } else {
         $pgMatch = $false
         # Search through all tags on the portgroup, looking for match on any of the watch tags 
         # provided in the secret. If there is a match on any tag, stop processing - we only need
         # one tag match for the portgroup to be marked as OK
         :outer foreach ($pgTag in $pgTags) {
            $fullTag = $pgTag.Tag.Category.ToString() + "/" + $pgTag.Tag.Name.ToString()
            if(${env:FUNCTION_DEBUG} -eq "true") {
               Write-Host "$(Get-Date) - DEBUG: PortGroup Tag: $($fullTag)`n"
            }

            foreach ($watchTag in $PG_WATCH_TAGS) {
               if ($fullTag -eq $watchTag) {
                  Write-Host "$(Get-Date) - INFO: Found a match on $($watchTag)`n"
                  $pgMatch = $true
                  break outer
               }
            }
         }

         # If none of the portgroup's tags are a match, the vNIC is added to the hash table
         if ($pgMatch -eq $false ) {
            Write-Host "$(Get-Date) - INFO: No permitted tags were found on the portgroup`n"
            $networkAdapterHash[$nic.id] = "$($pg.Name)"
         }
      }
   }

   # Check to see if the hash is empty
   if ( $networkAdapterHash.Count -eq 0 ) {
      Write-Host "$(Get-Date) - INFO: All NICs are on approved portgroups"
      return
   }

   $msg = "NICs using unapproved portgroups:`n"
   Write-Host "$(Get-Date) - $($msg)"
   # Build a list of NICs and unapproved portgroups
   foreach ($nic in $networkAdapters) {
      if ($networkAdapterHash.ContainsKey($nic.Id)) {
         Write-Host "$(Get-Date) - $($nic.Name): on unapproved portgroup $($networkAdapterHash[$nic.Id])"
         $msg += $nic.Name + " - " + $networkAdapterHash[$nic.Id] + "`n"
      }
   }

   # Payload for Slack
   $payload = @{
      attachments = @(
         @{
            pretext = $(${jsonSecrets}.SLACK_MESSAGE_PRETEXT);
            fields = @(
               @{
                     title = "EventType";
                     value = $cloudEvent.Subject;
                     short = "false";
               }
               @{
                     title = "Username";
                     value = $cloudEventData.UserName;
                     short = "false";
               }
               @{
                     title = "DateTime";
                     value = $cloudEventData.CreatedTime;
                     short = "false";
               }
               @{
                  title = "Full Message";
                  value = $msg + "`n`n" + $cloudEventData.FullFormattedMessage ;
                  short = "false";
               }
            )
         }
      )
   }

   # Convert Slack message object into JSON
   $body = $payload | ConvertTo-Json -Depth 5

   if(${env:FUNCTION_DEBUG} -eq "true") {
      Write-Host "$(Get-Date) - DEBUG: `"$body`""
   }

   Write-Host "$(Get-Date) - Sending Webhook payload to Slack ..."
   $ProgressPreference = "SilentlyContinue"

   try {
      Invoke-WebRequest -Uri $(${jsonSecrets}.SLACK_WEBHOOK_URL) -Method POST -ContentType "application/json" -Body $body
   } catch {
      throw "$(Get-Date) - Failed to send Slack Message: $($_)"
   }

   Write-Host "$(Get-Date) - Successfully sent Webhook ..."

   Write-Host "$(Get-Date) - PG Check operation complete ...`n"

   Write-Host "$(Get-Date) - Handler Processing Completed ...`n"
}

Step 1 – Environment variables

The first thing I do is grab 2 of the new environment variables on lines 78-79:

   $VM_WATCH_TAGS = ${jsonSecrets}.VM_WATCH_TAGS
   $PG_WATCH_TAGS = ${jsonSecrets}.PG_WATCH_TAGS

I copied this syntax from the existing code in the handler on lines 14-17:

   # Extract all tag secrets for ease of use in function
   $VCENTER_SERVER = ${jsonSecrets}.VCENTER_SERVER
   $VCENTER_USERNAME = ${jsonSecrets}.VCENTER_USERNAME
   $VCENTER_PASSWORD = ${jsonSecrets}.VCENTER_PASSWORD
   $VCENTER_CERTIFICATE_ACTION = ${jsonSecrets}.VCENTER_CERTIFICATE_ACTION

Step 2 – Look for a change

In the previous post, I used Sockeye to figure out the event I wanted to monitor for – ‘VmReconfiguredEvent’. I observed the resulting payloads, finding that any time a vNIC was changed, the “ConfigSpec.DeviceChange” section was present in the payload. When no change was made, the “ConfigSpec.DeviceChange” section was absent. Here is a snippet of a payload with a device change:

	"ConfigSpec": {
	  "ChangeVersion": "2022-02-24T20:00:58.726047Z",
	  "Name": "",
	  "Version": "",
	  "CreateDate": "2021-05-25T20:15:49.026445Z",
	  "Uuid": "",
	  "InstanceUuid": "",
	  "NpivNodeWorldWideName": null,
	  "NpivPortWorldWideName": null,
	  "NpivWorldWideNameType": "",
	  "NpivDesiredNodeWwns": 0,
	  "NpivDesiredPortWwns": 0,
	  "NpivTemporaryDisabled": null,
	  "NpivOnNonRdmDisks": null,
	  "NpivWorldWideNameOp": "",
	  "LocationId": "",
	  "GuestId": "",
	  "AlternateGuestName": "",
	  "Annotation": "",
	  "Files": {
		"VmPathName": "ds:///vmfs/volumes/60883b4b-6596cf2d-0179-001b21a69dd8/tinycore-2/tinycore-2.vmx",
		"SnapshotDirectory": "",
		"SuspendDirectory": "",
		"LogDirectory": "",
		"FtMetadataDirectory": ""
	  },
	  "Tools": null,
	  "Flags": null,
	  "ConsolePreferences": null,
	  "PowerOpInfo": null,
	  "NumCPUs": 0,
	  "VcpuConfig": null,
	  "NumCoresPerSocket": 0,
	  "MemoryMB": 0,
	  "MemoryHotAddEnabled": null,
	  "CpuHotAddEnabled": null,
	  "CpuHotRemoveEnabled": null,
	  "VirtualICH7MPresent": null,
	  "VirtualSMCPresent": null,
	  "DeviceChange": [
		{
		  "Operation": "edit",
		  "FileOperation": "",
		  "Device": {
			"Key": 4000,
			"DeviceInfo": {
			  "Label": "Network adapter 1",
			  "Summary": "DVSwitch: 50 17 d5 a1 f0 17 6c 1d-d7 f9 b4 a8 21 62 1e 6b"

The entire payload is loaded into a variable named $cloudEventData on line 65.

Line 81 checks to see if ConfigSpec.DeviceChange is NULL:

   $deviceUnchanged = ($NULL -eq $cloudEventData.ConfigSpec.DeviceChange )

Lines 82-84 are an example of a debug statement. It’s a good idea to add debugging statements based on the FUNCTION_DEBUG flag. This limits the amount of output under normal circumstances, while still allowing the end user of your function to enable verbose debugging mode when needed. This snippet writes a debug message stating whether or not the ConfigSpec.DeviceChange property was NULL:

if(${env:FUNCTION_DEBUG} -eq "true") {
      Write-Host "$(Get-Date) - DEBUG: ConfigSpec.DeviceChange Is Null? $($deviceUnchanged)`n"
   }

In lines 87-91, I return from the function if there is no ConfigSpec.DeviceChange. It’s a good idea to exit a function as soon as possible for peak efficiency.

   if ($deviceUnchanged)
   {
      Write-Host "$(Get-Date) - No devices changed.`n"
      return
   }

Step 3a – Retrieve the VM object

I need to access the VM in order to read its tags. To do that, I could try calling the VM by name. However, you can name objects the same name in vCenter. I need to access the VM with a unique identifier.

Line 94 builds the MoRef ID. You can think of a managed object reference ID (MoRef ID) as an object’s primary key in the vCenter database – a unique identifier.

   # Build the MoRef ID
   $vmID = $cloudEventData.Vm.Vm.Type + "-" + $cloudEventData.Vm.Vm.Value

If you look at the Vm key in the payload JSON, you will find an array named ‘Vm’ beneath it with a Type and a Value. In this example, the MoRefID for the VM that caused this event is ‘VirtualMachine-vm-6001’

	"Vm": {
		"Name": "tinycore-2",
		"Vm": {
		  "Type": "VirtualMachine",
		  "Value": "vm-6001"
		}
	},

Lines 95-99 are debugging statements. If debugging is enabled, they output whether the Type or Value is null, as well as the fully constructed MoRef ID.

   if(${env:FUNCTION_DEBUG} -eq "true") {
      Write-Host "$(Get-Date) - DEBUG: Vm.Vm.Type Is Null? $($NULL -eq $cloudEventData.Vm.Vm.Type)`n"
      Write-Host "$(Get-Date) - DEBUG: Vm.Vm.Value Is Null? $($NULL -eq $cloudEventData.Vm.Vm.Value)`n"
      Write-Host "$(Get-Date) - DEBUG: vmID is $vmID`n"
   }

Lines 101 to 108 are an attempt to retrieve a virtual machine object with PowerCLI.

Any call that might fail should be in a try/catch block. Always print out a descriptive error with Get-Date as shown. If the error is unrecoverable, make sure to ‘throw $_’ at the end of the catch block. In this case, if I cannot retrieve the VM, I can’t move farther into the function, so I need to throw.

Throw causes a terminating error, ending processing of the script. The ‘$_’ variable contains the error message that got trapped by the try block.

   # Retrieve the VM object by MoRef ID
   try {
      $vm = Get-VM -id $vmID
   }
   catch {
      Write-Host "$(Get-Date) - ERROR: unable to retrieve VM ID $vmID`n"
      throw $_
   }

I hardly ever type a line of PowerCLI into the code editor without testing it first in my terminal window. Why try to figure out the syntax by running the function when I can figure it out directly in the terminal? Before I wrote the above lines of code, I typed this in the terminal:

Get-VM -id "VirtualMachine-vm-6001"

Now I know the PowerCLI syntax is correct and I can use it in the function

Step 3b – Test

In the left pane, I rebuild the container

cd .. && docker build -t ${IMAGE} . && cd test && docker run -e FUNCTION_DEBUG=true -e PORT=8080 --env-file docker-test-env-variable -it --rm -p 8080:8080 ${IMAGE}

In the right pane I send the test cloud event

.\send-cloudevent-test.ps1

I have successfully printed out the VM’s MoRef ID, I can continue with development.

Step 4a – Retrieve VM tags

I need to check the tags on the VM to see if it’s a VM that is tagged as PCI. Lines 110-123 use the $vm variable that I set in the previous step as an input for the Get-TagAssignment cmdlet. As always, I tested the cmdlets in my terminal window before putting them in the function code.

In the debug block, I write the list of tags found on the VM as well as the list of tags to watch for passed in via environment variable.

# Retrieve all tags on the VM
   Write-Host "$(Get-Date) - Retrieving tags on $($vm.Name)`n"
   try {
      $vmTags = $vm | Get-TagAssignment
   }
   catch {
      Write-Host "$(Get-Date) - ERROR: unable to retrieve tags for $($vm.Name)`n"
      throw $_
   }

   if(${env:FUNCTION_DEBUG} -eq "true") {
      Write-Host "$(Get-Date) - DEBUG: Tags found on VM: $($vmTags.tag)`n"
      Write-Host "$(Get-Date) - DEBUG: Tags to monitor: $($VM_WATCH_TAGS)`n"
   }

Step 4b – Test

I rebuild the container and run the test script – I see the tags show up in the debug statement. On to the next part of the function.

Step 5a – Tag comparison

I now need to figure out if the VM that triggered the function has any tags that match the tags passed in the environment variables. Lines 125-144 accomplish this task

   # Search through the tags found on the VM and compare them to the VM tags specified in the secret. 
   # If any match is found, break out of the loop - finding any match means the VM is tagged for further inspection
   $checkVM = $false
   :outer foreach ($tag in $vmTags) {
      if(${env:FUNCTION_DEBUG} -eq "true") {
         Write-Host "$(Get-Date) - DEBUG: Comparing VM tag: $($tag.Tag) on VM $($vm.Name)`n"
      }
      foreach ($watchTag in $VM_WATCH_TAGS) {
         if(${env:FUNCTION_DEBUG} -eq "true") {
            Write-Host "$(Get-Date) - DEBUG: Comparing watch tag: $($watchTag)`n"
         }
         if ($watchTag -eq $tag.Tag) {
            if(${env:FUNCTION_DEBUG} -eq "true") {
               Write-Host "$(Get-Date) - DEBUG: Match found for: $($watchTag), breaking outer loop`n"
            }
            $checkVM = $true
            break outer
         }
      }
   }

Below, I have removed all the debug statements to make it easier to read. I set a flag variable $checkVM to $false – if a tag gets matched, we will set this flag to $true – this will tell us whether we need to check the VM’s vNICs. Then there are two loops – an outer and an inner loop. The outer loop cycles through all of the tags found on the VM. The inner loop cycles through all of the tags passed in the environment variables.

If a match is found, the $checkVM flag gets set to $true. Then we need to break out of the loops – we only need to match any tag to know that we have to check the VM’s vNICs. PowerShell lets you tag a loop with a name – in this case I tagged the outer loop with the name ‘:outer’. With the loop name in place, the ‘break outer’ statement will break out of both the inner loop and outer loop.

   $checkVM = $false
   :outer foreach ($tag in $vmTags) {
      foreach ($watchTag in $VM_WATCH_TAGS) {
         if ($watchTag -eq $tag.Tag) {
            $checkVM = $true
            break outer
         }
      }
   }

Step 5b – Test

I rebuild and test again. The debug statements show the tag on the VM as well as finding a match in the environment variables.

Step 6a – Stop processing function if the VM isn’t tagged

Lines 146-152 will exit the function if the VM doesn’t need to be inspected. Otherwise it will drop a message on the screen showing that there was a tag match.

   # If the VM isn't tagged, no further inspection is necessary
   if ($checkVM -eq $false) {
      Write-Host "$(Get-Date) - VM not tagged for monitoring, no inspection required.`n"
      return
   }

   Write-Host "$(Get-Date) - Match found for $($vm.Name), checking portgroups`n"

Step 6b – Test

I need to test three cases here – our existing VM tinycore-2 with tags, a VM without tags, and a VM with tags that don’t match. I don’t want to make changes to my main testing VM – tinycore-2. I’m going to set up tinycore-1 to do what I need.

I put a non-PCI tag on tinycore-1.

Now I need an event payload. I could go into Sockeye, edit the VM, and capture a new payload. But I can also create the payload faster myself by copying and modifying my current ‘test-payload.json’. I duplicate ‘test-payload.json’ and name it ‘test-payload-tc1.json’

Looking at a snippet of the JSON, I only need to change a couple of things – Vm.Name and Vm.VM.Value. I should also change the VM name in FullFormattedMessage just so I don’t get confused

	"Vm": {
		"Name": "tinycore-2",
		"Vm": {
		  "Type": "VirtualMachine",
		  "Value": "vm-6001"
		}
	},
	"Ds": null,
	"Net": null,
	"Dvs": null,
	"FullFormattedMessage": "Reconfigured tinycore-2 on esx02.ad.patrickkremer.com in HomeLab.  \n \nModified:  \n \nconfig.hardware.device(4000).backing.port.portgroupKey: \"dvportgroup-1006\" -> \"dvportgroup-8004\"; \n\nconfig.hardware.device(4000).backing.port.portKey: \"4\" -> \"65\"; \n\nconfig.hardware.device(4000).backing.port.connectionCookie: 174899823 -> 193390024; \n\n Added:  \n \n Deleted:  \n \n",
	"ChangeTag": "",

I need the VM ID for tinycore-1, which I retrieve from PowerCLI

(get-vm "tinycore-1").id

I make the three changes to ‘test-payload-tc1.json’

I rebuild the container. When you run send-cloudevent-test with no arguments, it defaults to sending ‘test-payload.json’. But you can specify a different JSON file as an argument.

.\send-cloudevent-test.ps1 .\test-payload-tc1.json

Now when I test with the tc1 payload, I get the message that tinycore-1 doesn’t need inspection.

If I test again with test-payload.json (remember, it’s the default if you don’t pass an argument), I get a match for tinycore-2

Now I remove the tag on tinycore-1 and repeat the test – there are no tags found and it correctly reports that no inspection is required.

Step 7a – Check the VM’s vNICs

Lines 154-161 retrieve the vNICs attached to the VM

   # Try retrieving the VM's network adapters
   try {
      $networkAdapters = $vm | Get-NetworkAdapter
   }
   catch {
      Write-Host "$(Get-Date) - ERROR: unable to retrieve retrieve network adapters for $($vm.Name)`n"
      throw $_
   }

On line 164, I create a hash table to store a mapping between the network adapter ID and the portgroup name the adapter is sitting on. A hash table is a key/value store.

   # This hash will store any network adapters that are attached to unapproved portgroups
   $networkAdapterHash = @{}

Lines 166 through 245 are a loop through each vNIC on the VM. In this loop, I need to:

  • Figure out if the vNIC is backed by a Standard or Distributed virtual switch
  • Build the appropriate MoRef ID
  • Retrieve the virtual network by MoRef ID
  • Find tag assignments for the virtual network
  • If the vNIC is on a port group that doesn’t have an allowed tag, add it to the hash table

Line 166 starts the for loop, 167-169 prints a debug message so we know what objects we’re working with.

foreach ($nic in $networkAdapters) {
      if(${env:FUNCTION_DEBUG} -eq "true") {
         Write-Host "$(Get-Date) - DEBUG: VM $($vm.Name) - NIC $($nic.Name) - PortGroup $($nic.NetworkName)`n"
      }

Lines 170-174 attempt to load data from the network adapter object. A standard switch has a network backing it, but a distributed virtual switch has a PortgroupKey. vNetworkID is a placeholder variable for the MoRef ID. You will only have one or the other – one of the values should be $NULL.

      # If the portgroup is a VSS portgroup, it will have a backing network property
      $vssBackingNetwork = $nic.extensiondata.Backing.Network
      # If the portgroup is a DVS portgroup, it will have a PortgroupKey property
      $dvPortGroupKey = $nic.extensiondata.Backing.Port.PortgroupKey
      $vNetworkID = $null

Lines 176-196 build the MoRef ID for the network adapter based on the backing network. One oddity with the Get-NetworkAdapter cmdlet is that the Backing.Network property is already in MoRef ID format, but the PortgroupKey property is not. The correct MoRef ID is saved in $vNetworkID. If both VSS and VDS are found to be null, an error is thrown.

If $vNetworkID gets populated, it gets displayed in a debug statement on line 195.

if ($NULL -ne $vssBackingNetwork) {
         if(${env:FUNCTION_DEBUG} -eq "true") {
            Write-Host "$(Get-Date) - DEBUG: VSS Backing network is $($vssBackingNetwork)`n"
         }
         # vssBackingNetwork is already in MoRef ID format
         $vNetworkID = $vssBackingNetwork
      }
      elseif ($NULL -ne $dvPortGroupKey) {
         if (${env:FUNCTION_DEBUG} -eq "true") {
            Write-Host "$(Get-Date) - DEBUG: DVS Port Group Key is $($dvPortGroupKey)`n"
         }
         # Unlike the VSS above, We have to build the MoRef ID for VDS
         $vNetworkID = "DistributedVirtualPortgroup-" + $dvPortGroupKey
      } else {
         Write-Host "$(Get-Date) - ERROR: Could not determine network type`n"
         throw "$(Get-Date) - ERROR: Could not determine network type for $($nic.Name)`n"
      }

      if(${env:FUNCTION_DEBUG} -eq "true") {
         Write-Host "$(Get-Date) - DEBUG: Virtual Network ID: $($vNetworkID)`n"
      }

Lines 198-214 retrieve the virtual network by MoRef ID, then retrieves the tags that are assigned to the virtual network.

      # Now that we've determined the ID, we can try retrieving the portgroup virtual network information
      try {
         $pg = Get-VirtualNetwork -Id $vNetworkID
      }
      catch {
         Write-Host "$(Get-Date) - ERROR: unable to retrieve retrieve virtual network information for $($vNetworkID)`n"
         throw $_
      }

      # Retrieve the tag assignments for the port group
      try {
         $pgTags = $pg | Get-TagAssignment
      }
      catch {
         Write-Host "$(Get-Date) - ERROR: unable to retrieve retrieve tag assignments for $($pg.Name)`n"
         throw $_
      }

Lines 216 – 237 determine whether or not the vNIC is on an allowed port group.

Lines 217-218 add the vNIC to the hash table if the connected portgroup has no tags on it. If the VM is tagged as PCI, we can’t have a port group with no tags attached to the VM.

Lines 220 through 237 are the same type of outer loop I showed you in step 5. I set a control variable $pgMatch to $false at the top. I cycle through the tags on the portgroup in the outer loop, then cycle through the list of allowed tags in the inner loop. If there is any match, I set $pgMatch to $true and break the loops. I only need one tag match for the port group to be allowed.

# If $null, the NIC is on a pg with no tags - add it to the hash table
      if ($null -eq $pgTags) {
         $networkAdapterHash[$nic.id] = "$($pg.Name)"
      } else {
         $pgMatch = $false
         # Search through all tags on the portgroup, looking for match on any of the watch tags 
         # provided in the secret. If there is a match on any tag, stop processing - we only need
         # one tag match for the portgroup to be marked as OK
         :outer foreach ($pgTag in $pgTags) {
            $fullTag = $pgTag.Tag.Category.ToString() + "/" + $pgTag.Tag.Name.ToString()
            if(${env:FUNCTION_DEBUG} -eq "true") {
               Write-Host "$(Get-Date) - DEBUG: PortGroup Tag: $($fullTag)`n"
            }

            foreach ($watchTag in $PG_WATCH_TAGS) {
               if ($fullTag -eq $watchTag) {
                  Write-Host "$(Get-Date) - INFO: Found a match on $($watchTag)`n"
                  $pgMatch = $true
                  break outer
               }
            }
         }

Lines 239-242 add the vNIC to the hash table based on the $pgMatch variable.

         # If none of the portgroup's tags are a match, the vNIC is added to the hash table
         if ($pgMatch -eq $false ) {
            Write-Host "$(Get-Date) - INFO: No permitted tags were found on the portgroup`n"
            $networkAdapterHash[$nic.id] = "$($pg.Name)"
         }

Lines 247-250 return if the hash table is empty. If the hash table is empty, this means that all vNICs are on approved portgroups.

   # Check to see if the hash is empty
   if ( $networkAdapterHash.Count -eq 0 ) {
      Write-Host "$(Get-Date) - INFO: All NICs are on approved portgroups"
      return
   }

Lines 253-261 build a message for both display and Slack notification. If we reach this point, at least one vNIC is connected to a disallowed port group. I cycle through the hash table, building a string variable $msg for the Slack notification and also writing the information out to the screen.

   $msg = "NICs using unapproved portgroups:`n"
   Write-Host "$(Get-Date) - $($msg)"
   # Build a list of NICs and unapproved portgroups
   foreach ($nic in $networkAdapters) {
      if ($networkAdapterHash.ContainsKey($nic.Id)) {
         Write-Host "$(Get-Date) - $($nic.Name): on unapproved portgroup $($networkAdapterHash[$nic.Id])"
         $msg += $nic.Name + " - " + $networkAdapterHash[$nic.Id] + "`n"
      }
   }

Part 7b – Testing

I rebuild the container and run the default test

For tinycore-2, I see that network adapters 2 and 3 are flagged as unapproved

Looking in vCenter, this is correct – adapter 1 is on a PCI portgroup, but 2 and 3 are not.

I test tinycore-1, first with no tag and I get the expected message

I add the PCI tag to tinycore-1 and connect the NIC to a PCI portgroup, then test again

I move tinycore-1 to a non-PCI network and test again

Part 8a – Slack notification

I borrowed all of the Slack notification code from the kn-ps-slack function. The only thing I changed is on line 286 – I prepended the FullFormattedMessage with the $msg variable I built in part 7.

value = $msg + "`n`n" + $cloudEventData.FullFormattedMessage ;

The function ends with notifications of completion

   Write-Host "$(Get-Date) - Successfully sent Webhook ..."

   Write-Host "$(Get-Date) - PG Check operation complete ...`n"

   Write-Host "$(Get-Date) - Handler Processing Completed ...`n"

Part 8b – Testing

I rebuild the container and run the test script – it looks like the Slack message went through.

I check Slack and the function worked – this looks identical to kn-ps-slack other than the additional display of the contents of $msg

Summary

This was a long, detailed post. Don’t worry if you didn’t fully understand every line of code in it! Drop a comment if you have any questions.

In the Part X, I explain how to go from the function writing phase to the function deployment phase.

2 comments

Leave a Reply

Your email address will not be published. Required fields are marked *