Event Driven Automation: Domain Controller FSMO role has moved

In a prior post (https://enterpriseadmins.org/blog/scripting/event-driven-automation-with-aria-operations-for-logs-webhooks-and-jenkins/), I wrote about using Aria Operations for Logs to call Jenkins using a webhook to automate a process. In that post, Aria Operations for Logs is watching for a specific vSphere event and when that occurs (VM deleted), we check a couple other systems (Active Directory & Aria Automation Config) and make sure the cleanup activates cascade to those other systems.

In this article we are going to do the same type of thing (one event triggers another), but instead of looking a vSphere events, we are going to use an event that comes from an agent deployed to Windows systems, specifically domain controllers, when a flexible single-master operator (FSMO) role is moved. As with the previous example, we could really look for any event, this is just something that is easy to trigger for demonstration purposes.

In my experience, these FSMO roles are only rarely moved in a stable production environment. However, when the PDC Emulator role is moved it is possible that some firewall rules may need to be modified. This is because all clients will send requests to the system holding this role in the event of a bad password attempt (https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/fsmo-roles). Since this type of change occurs rather infrequently it is something that can be easily forgotten. To help with this, we are going to create a similar event driven approach to alert on these events and start the appropriate actions.

Aria Operations for Logs – webhook payload

This webhook will be identical to the payload used for our previous example, with one important change. We’ve updated the ‘token’ portion of the URL we will be posting to.

Aria Operations for Logs – alert definition

The Windows event entry for this is event ID 1458, so we’ll look for that happening. Our alert definition will be very simple:

For testing, I’m using the same ‘Real Time’ query which executes every minute. Due to the infrequent nature of this event, we could likely scale this back quite a bit. However, for building the rule & testing its execution having it run more frequently was helpful.

Jenkins – New Freestyle project

As with the previous example, Jenkins is going to receive this alert via webhook and then run our custom script.

With the FSMO role moves, we will actually receive two events — one from the domain controller which previously held the role, and another event from the domain controller receiving the role. To prevent duplicate actions from triggering, the logic of our code is only going to take action for the event received from the domain controller receiving the role. This will help reduce alert fatigue.

Aria Operations for Logs is sending an event message which contains the event text as well as some additional extracted fields. In our previous post, we only needed to read the text of the message, but in this case we also need the extracted field showing which system was the source of our event. To make this happen, our post content parameter we’ll call logmsg but make the expression be the JSONPath $[0], which is the full event message and not just the text field used in our previous example.

As an example, our code looks like this… again it is just an example for illustration purposes. We could do anything we want/need with this event.

$jsonEvent = $env:logmsg | ConvertFrom-JSON

$previousOwnerCN = [regex]::Match( $jsonEvent.text.split("`n")[7], ",CN=(.*?),CN=Servers").Groups[1].Value.Trim()
$newOwnerCN = [regex]::Match( $jsonEvent.text.split("`n")[5], ",CN=(.*?),CN=Servers").Groups[1].Value.Trim()
$eventSourceHost = ($jsonEvent.fields | ?{$_.name -eq 'hostname'}).content.Trim()
$requestuser = ($jsonEvent.fields | ?{$_.name -eq 'userid'}).content.Trim()

 # Both domain controllers involved in the transfer will send an event message.  To eliminate duplicate actions, 
 # we will only action the event which comes from the new owner of the FSMO role.
if ($eventSourceHost -match $newOwnerCN) {
  $msgObject = $jsonEvent.text.split("`n")[3]
  $dnAsFqdn  = ($msgObject -replace '^.*?,DC=' -replace ',DC=','.').Trim()

  $movedRole = switch ($msgObject) {
    { $_ -match '^DC=' } { 'PDC Emulator - domain: ' + $dnAsFqdn }
    { $_ -match '^CN=RID Manager'} { 'RID Master - domain: ' + $dnAsFqdn }
    { $_ -match '^CN=Infrastructure'} { 'Infrastructure Master - domain: ' + $dnAsFqdn }
    { $_ -match '^CN=Schema'} { 'Schema Master - forest: ' + $dnAsFqdn }
    { $_ -match '^CN=Partitions'} { 'Domain Naming Master - forest: ' + $dnAsFqdn }
  $friendlyMessage = "The role $movedRole has been moved to $newOwnerCN from $previousOwnerCN by $($requestuser)."
  if ($friendlyMessage -match 'PDC Emulator') { 
    $destIP = ([System.Net.Dns]::GetHostAddresses($eventSourceHost) | ?{$_.AddressFamily -eq 'InterNetwork'})[0].IPAddressToString
    $fwcrSubmitt = New-FirewallChangeRequest -requestor $requestuser -hostGroup 'Win-AD-Services' -changeType 'add' -ipAddress $destIP
    $friendlyMessage += '  For this role, additional firewall changes may be needed.  To help ensure this has not been overlooked, firewall change request '+$fwcrSubmitt+' has been submitted.'
  Send-MailMessage -to 'bwuchner@example.com' -from 'jenkins@example.com' -Subject 'Domain Controller: FSMO Role moved' -Body $friendlyMessage -SmtpServer 'mail.example.com'
} else {
  "This event is being skipped as the eventSourceHost was $eventSourceHost which is not the newOwner $($newOwnerCN)."; exit 1

Note: the New-FirewallChangeRequest function is not covered by this post, but it is an example custom function to submit an internal form


Testing this process was rather easy in a lab. From Active Directory Users and Computers, you can change the operations master, moving it from one domain controller to another. Each move should trigger two Jenkins builds with only one sending an email notification. The other build will exit as an error before a message occurs. In the case of a PDC Emulator move, the email text has a bit more detail and a firewall change request gets submitted. Here is a sample of the email notification sent for a PDC Emulator move. This text is also logged as ‘console output’ for the Jenkins build.

The role PDC Emulator - domain: enterpriseadmins.org has been moved to DR-CONTROL-21 from CORE-CONTROL-21 by LAB\bwuchner.  For this role, additional firewall changes may be needed.  To help ensure this has not been overlooked, firewall change request 123d1fd1-895a-47e9-9779-88dc33a32a16 has been submitted.
Finished: SUCCESS


This is another example of using Aria Operations for Logs to trigger a specific event to drive automation. With this webhook alert allowing us to connect the two systems, any event that occurs, whether vSphere, Windows agent, syslog stream, or anything else, can be used as the starting point to take action or send very specific notifications to any system we choose.

Posted in Lab Infrastructure, Scripting | Leave a comment

Event Driven Automation with Aria Operations for Logs, webhooks, and Jenkins

I was recently looking at the VMware Event Broker Appliance fling (https://vmweventbroker.io/). This fling enables a custom function to execute when a vSphere event happens, sort of an ‘if this then that’ approach to vSphere events (https://octo.vmware.com/vsphere-power-event-driven-automation/). As I was reading through the documentation, I began to wonder if I could build my own version of this using components already deployed to the lab. I already have Aria Operations for Logs which gets all my vSphere events via syslog. I have Jenkins configured with the generic webhook plugin. Aria Operations for Logs alert definitions can notify a webhook. These building blocks should do the trick… some assembly required*.

For demonstration purposes, I wanted to configure an alert to happen when a vSphere virtual machine is deleted. An Aria Operations for Logs alert will call Jenkins and some custom code will make sure the computer object in Active Directory is removed and the Aria Operations Config minion key is deleted.

Aria Operations for Logs – webhook payload

In Aria Operations for Logs we can define a custom payload for a webhook. This is defined under Configuration > Webhooks. I’ve used the following settings:

This will cause Aria Operations for Logs to call my webhook URL for each individual message observed. It will post the captured event as JSON to Jenkins and pass in my jobs token.

Aria Operations for Logs – alert definition

In the Alerts > Alert Definitions section, I’ve created a new alert. It will run realtime (every minute) and look for [vim.event.VmRemovedEvent]. When the count is greater than zero, it will notify my custom webhook. After some testing, I also added an additional filter where text does not contain an error message I was seeing or my VDI cluster to exclude those VMs from being in the scope of this cleanup. I’ve included a screenshot of this alert definition below:

Jenkins – New Freestyle project

In Jenkins I created a new ‘freestyle project’ to receive this webhook. I enabled the ‘Generic Webhook Trigger’ in the job properties. I defined a post content parameter to create a variable named eventmsg that had the JSONPath expression of $[0].text which parses out just the text of the first item in the event to a variable. Since we defined our webhook to send individual logs, we wouldn’t expect more items than the first item “[0]” to be included in our payload.

In the webhook trigger ‘token’ property, I used the string vmDeprovisionRequest. We included this value in our Aria Operations for Logs webhook payload above. I also checked the boxes for ‘print post content’ and ‘print contributed variables’ so that the console logs would have the details of what triggered my request. This is optional, but very helpful for troubleshooting in the future.

The next part of this is where we have a small amount of custom code. Similar to VEBA, what we do when we see this event is really only limited by our imagination. Here I’m extracting some variables, doing a bit of error checking/validation, writing some text and doing the two tasks I want to execute on VM deletion — cleaning up active directory and Aria Automation Config. This code could be better, but I’ll share it for anyone interested.

$vmname = [regex]::Match( $env:eventmsg.Split('[')[9] , "Removed (.*?) on " ).Groups[1].Value 
$username = $env:eventmsg.Split('[')[6] -Replace (']','')

if ($env:eventmsg -notmatch 'vim.event.VmRemovedEvent') { "This event message does not match expected value.  Ending."; exit 1 }

"Received request to deprovision VM $vmname after it was deleted by user: $($username)."
if ($username -match 'svc-horizon') { "This request came from user $username which has been excluded from process.  Ending."; exit 1 }

# AD Cleanup
$thisComputer = Get-ADComputer -Filter "Name -eq '$vmname'" 
if (($thisComputer | Measure-Object).Count -eq 1) {
  "Found in AD: $($thisComputer.DistinguishedName), will proceed to move/disable."
  $thisDate = (Get-Date).ToString('yyyy-MM-dd HH:mm')
  Set-ADComputer -Identity $thisComputer -Description "Disabled by Jenkins on $thisDate" -Enabled $false
  Move-ADObject -Identity $thisComputer -TargetPath "OU=_ToDelete,OU=LAB Servers,DC=lab,DC=enterpriseadmins,DC=org"

# Remove SSC minion
$cs = Connect-SscServer 'cm-config-01.lab.enterpriseadmins.org' -user 'root' -password 'VMware1!'
$sscMinionKey = Get-SscMinionKey |?{$_.minion -match "$vmname\."} 
if (($sscMinionKey | Measure-Object).Count -eq 1) {
  "Found Salt Minion $($sscMinionKey.minion) in state $($sscMinionKey.key_state), will proceed to move/disable."
  $sscMinionKey | Remove-SscMinionKey -Confirm:$false


I created a handful of VMs and deleted them… some deletes one at a time, others in batches, just to see what would happen. Aria Operations for Logs would call Jenkins, Jenkins would run the job, and I could check the build history / console output tab to see what happened.

I’ll include the console output of one specific delete attempt below as reference. We can see the event log text received by the webhook, the variables Jenkins extracted, and the text output of the script execution that moved/disabled AD computer objects & the Job ID of an Aria Automation Config minion key being deleted.


[{"text":"2024-07-08T01:02:33.381560+00:00 core-vcenter01 vpxd 7289 - -  Event [19854143] [1-1] [2024-07-08T01:02:33.380981Z] [vim.event.VmRemovedEvent] [info] [LAB\\svc-vra] [US-East-IN] [19854141] [Removed h199-linux-11 on core-esxi-33.lab.enterpriseadmins.org from US-East-IN]","timestamp":1720400553384,"fields":[{"name":"hostname","content":"core-vcenter01"},{"name":"appname","content":"vpxd"},{"name":"procid","content":"7289"},{"name":"msgid","content":"-"},{"name":"client_syslog_version","content":"1"},{"name":"__li_source_path","content":"CORE-VCENTER01.lab.enterpriseadmins.org"},{"name":"priority","content":"info"},{"name":"facility","content":"user"},{"name":"client_syslog_priority","content":"14"}]}]

Contributing variables:

    eventmsg = 2024-07-08T01:02:33.381560+00:00 core-vcenter01 vpxd 7289 - -  Event [19854143] [1-1] [2024-07-08T01:02:33.380981Z] [vim.event.VmRemovedEvent] [info] [LAB\svc-vra] [US-East-IN] [19854141] [Removed h199-linux-11 on core-esxi-33.lab.enterpriseadmins.org from US-East-IN]

[vmDeprovisionRequest] $ powershell.exe -NonInteractive -ExecutionPolicy Bypass -File C:\Users\SVC-JE~1\AppData\Local\Temp\jenkins14142109662625721398.ps1
Received request to deprovision VM h199-linux-11 after it was deleted by user: LAB\svc-vra .
Found in AD: CN=H199-LINUX-11,OU=Services,OU=LAB Servers,DC=lab,DC=enterpriseadmins,DC=org, will proceed to move/disable.
Found Salt Minion h199-linux-11.lab.enterpriseadmins.org in state pending, will proceed to move/disable.
Finished: SUCCESS


Event driven automation can be very powerful. The above example could have been tackled a variety of ways… as you can see in the example above the VM delete was actually triggered by my Aria Automation service account as I had deleted the deployment from that tool. I could have completed this same task using ABX when the VM was deleted. However, I like the idea of doing the cleanup tasks outside of Aria Automation to catch any one-off lab systems that get created manually or using other automation tools. Again, this is just one example of an event driven “if this” (VM deleted), “then that” (cleanup task) approach to automation. There are likely many better examples, this was just some cleanup I had been wanting to tackle anyway.

Posted in Lab Infrastructure, Scripting, Virtualization | 1 Comment

PowerCLI Get-TagAssignment and InaccessibleDatastore

I was recently speaking with a customer who mentioned an issue that occurred when using the PowerCLI Get-TagAssignment cmdlet. They had a tag category which applied to clusters only. If a host were offline or in maintenance mode, they were getting an error that a local datastore was not accessible. I was able to recreate this issue and observed the following error text.

Get-TagAssignment -Category h225-clusteronly-category

Tag                                      Entity
---                                      ------
h225-clusteronly-category/h225-cluste... h206-cluster-storagepath
Get-TagAssignment : 7/14/2024 10:30:04 AM       Get-TagAssignment               Datastore 'Local-h206-vesx-02' is not accessible. No
connected and accessible host is attached to this datastore.
At line:1 char:1
+ Get-TagAssignment -Category h225-clusteronly-category
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Get-TagAssignment], InaccessibleDatastore
    + FullyQualifiedErrorId : Client20_TaggingServiceCisImpl_GetVDisks_Error,VMware.VimAutomation.ViCore.Cmdlets.Comma

This is a peculiar error as the tag category only specifies ClusterComputeResource as an Associable Entity, so datastores should not be in scope and it is unexpected to see them called out in this context.

While looking into this error message, I stumbled across this blog post from a couple of years ago: https://virtuallyjason.blogspot.com/2022/02/powercli-and-get-tagassignment.html. The article doesn’t touch specifically on this error message, but discuses performance related impact on specifying the -Entity parameter of the Get-TagAssignment cmdlet. I did a bit of testing and confirmed that not only does specifying the entity improve performance, it also suppresses the error message that occurs for offline datastores.

To double check performance, I wanted run each command several times. The loop below runs each command 100 times, then outputs the min, avg, and max run times for each command. In my case, specifying -Entity (Get-Cluster) caused the execution to complete on average 5x quicker.

$myCommand = @()
1..100 | %{
  $myCommand += [pscustomobject][ordered]@{
    Iteration = $_
    RunTime1 = (measure-command {Get-TagAssignment -Category h225-clusteronly-category}).TotalSeconds
    RunTime2 = (measure-command {Get-TagAssignment -Category h225-clusteronly-category -Entity (Get-Cluster)}).TotalSeconds

$myCommand | Measure-Object -Property RunTime1 -Minimum -Average -Maximum 
$myCommand | Measure-Object -Property RunTime2 -Minimum -Average -Maximum 

When using Get-TagAssignment there are a couple of good reasons to limit the scope of the cmdlet to only those entities required.

Posted in Scripting, Virtualization | Leave a comment

Converting between guest OS serial number at VM UUID

A customer recently asked me if it was possible to find a virtual machine BIOS serial number from a virtual machine object or vmx file. While looking into this, I found an old post that provided a very useful hint: https://peppercrew.nl/2011/04/get-virtual-machine-bios-serial-number/. This post contains a PowerShell function to convert a VM UUID to the BIOS serial number. Looking at the code, and reviewing some test VMs, I realized that this is only a string replacement exercise. I’ve seen these serial numbers and UUIDs for years and never made the connection that they were so closely related.

The image below has one example value. The top line is the virtual machine serial number obtained from inside the guest OS (using wmic bios get serialnumber on Windows or dmidecode -s system-serial-number on Linux). The bottom line is the virtual machine UUID (from PowerCLI we can find this with: $vm.ExtensionData.Config.Uuid). The color coding has been added to make the pattern easier to see.

Color coded comparison of VM serial number and UUID

This actually helped explain something I’ve seen before with virtual machines created by storage array-based clones having duplicate UUIDs. The top value (minus the ‘VMware-‘ prefix) is stored in the vmx file as uuid.bios. When a direct clone of the VM file is registered into inventory, the VMs UUID (and by extension, BIOS serial number) would be a duplicate of the original/source VM.

We can see in the following command/output that two VMs have different names / IDs, but have duplicate UUIDs. I’ve confirmed with dmidecode that these two guests also have the same system-serial-number.

Get-VM h207-vm-* | Select-Object Name, ID, @{N='UUID';E={$_.extensiondata.config.uuid}}

Name       Id                        UUID
----       --                        ----
h207-vm-01 VirtualMachine-vm-3147125 42023081-331b-58e1-2730-ca560789e551
h207-vm-02 VirtualMachine-vm-3147127 42023081-331b-58e1-2730-ca560789e551

If we want to change our VM UUID, to ensure all our VMs have unique serial numbers, we can do that with PowerCLI as well. For illustration purposes, I’m going to update the UUID value for the second VM to end with the number 2. However, we could also have PowerShell generate a completely new UUID with [guid]::NewGuid().

$vm = Get-VM h207-vm-02
$spec = New-Object VMware.Vim.VirtualMachineConfigSpec
$spec.uuid = '42023081-331b-58e1-2730-ca560789e552'

The above code will update our second VM to have a UUID which ends in 552 (the string we provided). In the below code block we’ll get the same two VMs as above and note that our UUIDs have changed.

Get-VM h207-vm-* | Select-Object Name, ID, @{N='UUID';E={$_.extensiondata.config.uuid}}

Name       Id                        UUID
----       --                        ----
h207-vm-01 VirtualMachine-vm-3147125 42023081-331b-58e1-2730-ca560789e551
h207-vm-02 VirtualMachine-vm-3147127 42023081-331b-58e1-2730-ca560789e552

Knowing the BIOS serial number and UUID relationship can be understood with string manipulation, I created a quick function to reverse the original example we found. In this function we can provide a VM system-serial-number and have it select the pieces of the string required to build our UUID.

function Get-VMUuidFromSerial {
  if ($vmSerial.length -ne 54) { write-warning "The provided serial number $vmSerial does not appear to be the correct length." }

  $myResult = $vmSerial -Replace 'VMware-', '' -Replace ' ', ''
  $myResult.substring(0, 8) + '-' + $myResult.substring(8,4) + '-' + $myResult.SubString(12,4) + '-' + $myResult.Substring(17,4) + '-' + $myResult.Substring(21, $myResult.length - 21)

The original code we found expected a parameter of type [VMware.VimAutomation.ViCore.Impl.V1.Inventory.VirtualMachineImpl], but our VM objects were being passed in as [VMware.VimAutomation.ViCore.Impl.V1.VM.UniversalVirtualMachineImpl], so I updated the type definition in the function. To be able to test this using a list of UUIDs from a CSV file, I ended up removing that type definition and checked the length of the string, similar to the above function. I also adjusted the function name to match the format we used above. I’m including it below for reference/safe keeping.

function Get-VMSerialFromUUID {
  if ($vmUUID.length -ne 36) { write-warning "The provided UUID $vmUUID does not appear to be the correct length." }

  $myResult = 'VMware-'
  $serialnumtmp = $vmUUID.Replace('-','')
  for ($i = 0; $i -lt $serialnumtmp.length; $i += 2) { $myResult += $serialnumtmp.substring($i, 2); if ($myResult.Length -eq 30) {$myResult += '-' } else { $myResult += ' ' } }

Hopefully this post will help if you ever need to compare VM UUIDs with BIOS serial numbers and/or resolve duplicate serial number issues.

Posted in Scripting, Virtualization | Leave a comment

Finding the real NFS network

I was recently helping a customer who had inherited an existing vSphere deployment that used NFS storage. They were tasked with migrating the old VMs to newer infrastructure, but they first wanted to find the NFS storage array backing the datastores.

Looking at the datastores in vCenter, they couldn’t find the hostname/IP address of the storage target. Instead, they saw somewhat random values in the device backing > server field, sort of like this demo red datastore I mocked up that shows a server of network.nfs.1 where we’d normally expect to see the hostname or IP. The value observed, network.nfs.1 in this case, wasn’t a name that was resolvable using the customers DNS.

Looking at host networking, one VMkernel adapter was clearly the one used for storage access, similar to this mocked up screenshot:

It seemed logical that network.nfs.1 and the other seemingly random names were devices on this network. We wanted to try and issue a ping from this ESXi host, but the root password was unknown and we were not able to login to the console to do so. However, since we had access to vCenter, I went looking for a way to send a ping from esxcli, hoping we could then use the Get-EsxCli PowerCLI cmdlet to issue our pings. I found esxcli network diag ping and testing in a lab worked as expected, so we tried it in this environment:

$esxcli = Get-EsxCli -VMHost $thisVmHost -v2
$networkDiagPing = $esxcli.network.diag.ping.CreateArgs()
$networkDiagPing.host = 'network.nfs.1'
$networkDiagPing.interface = 'vmk1'
$pingResults = $esxcli.network.diag.ping.Invoke($networkDiagPing)

Unfortunately, this resulted in the error sendto() failed (Network is unreachable). Surprising, as the NFS datastore was online and we had specified that we wanted to use the VMkernel interface on the storage network. In this case, the host had 4 VMkernel interfaces, so we stepped through each, trying to find out if the storage traffic was using a different interface. The last interface we tried, vmk0, received a response.

As best we could tell, the vmk1 interface was unused. The portgroup named ‘storage’ had a VLAN backing that didn’t actually exist in the environment & the VMkernel IP address wasn’t a network that existed either. Once we knew which network adapter was actually in use, the ping response returned an IP address of a known NAS. We did a bit more digging and found host entries that were obfuscating the actual IP addresses of known storage targets. For reference, here is how we found the host file entries, again using esxcli.

$esxcli.network.ip.hosts.list.invoke() | Select-Object HostName, IPaddress

HostName      IPaddress
--------      ---------

After the fact I put together a quick script to help in the odd event I ever see something like this again. It finds all the unique hostnames/IPs used by NFS datastores and then for each VMkernel interface attempts to ping the NFS host, only showing the successful ping responses.

$thisVmHost = 'h197-vesx-04.lab.enterpriseadmins.org'
foreach ($thisDatastoreBacking in Get-vmhost $thisVmHost | get-datastore |?{$_.ExtensionData.info.nas.type -eq 'NFS'} | select-object @{N='RemoteHostNames';E={$_.ExtensionData.info.nas.RemoteHostNames}} -Unique) {
  foreach ($thisVmk in Get-VMHostNetworkAdapter -VMHost $thisVmHost -VMKernel) {
    $esxcli = Get-EsxCli -VMHost $thisVmHost -v2
    $networkDiagPing = $esxcli.network.diag.ping.CreateArgs()
    $networkDiagPing.host = $thisDatastoreBacking.RemoteHostNames
    $networkDiagPing.interface = $thisVmk.name
    try {$pingResults = $esxcli.network.diag.ping.Invoke($networkDiagPing); $uniqueHosts = [string]::Join(', ', ($pingResults.Trace.host | select-object -Unique))} catch { $pingResults=$null }
    if ($pingResults) { "Pinging $($thisDatastoreBacking.RemoteHostNames) from $($thisVmk.name) [IP $($thisVmk.IP)] took path $uniqueHosts" }
  } # end vmkernel loop
} # end Datastore backing loop

In a lab with a similar configuration, the script above produces output similar to:

Pinging network.nfs.1 from vmk0 [IP] took path
Pinging network.nfs.2 from vmk0 [IP] took path
Pinging network.nfs.9 from vmk0 [IP] took path,,

The final row in that output shows an NFS target that was not on the local network and took a few hops to get to the final destination, which might be helpful to see.

Posted in Scripting, Virtualization | Leave a comment