The vCAC user interface is functional but has quite some limitations when you want to do selection based on different criteria the user enters. So for example, it is not possible to have cascading dropdown lists based on two parent lists or values. Furthermore you cannot select a physicl machine based on its location. And there are plenty more examples where vCAC just does not let you perform a proper selection and you might end up with a placement on a resource or reservation where you just do not want that machine to be provisioned.

Now you would naturally just destroy the newly requested machine (preferrably in the ‘Requested’ state) and create a new one behind the scenes. This is usually accomplished with the following (or via the SOAP VMPS service):

PS > $m.GetType().FullName
PS > $Machine.GetType().FullName

PS > $shimIdTkn = New-Object DynamicOps.ManagementModel.Common.Shim.IdentityToken;
PS > $shimIdTkn.ImpersonatingUser =
PS > $shimIdTkn.User =
PS > [DynamicOps.ManagementModel.Shim.OperationHelpers]::FireVirtualMachineEvent(
  $m, $Machine.VirtualMachineID, 'Destroy', shimIdTkn, $null);

However, in the ‘Requested’ state (or ‘AwaitingApproval’) this is not possbible. And to wait for the ‘BuildingMachine’ state does not help when you have a machine that needs approval to be built. So there must be a better solution (if you want to get it work at all).

So here is the approach (and yes, it is a little bit of a hassle):
1. Create an approval group with a mail address that can receive mails but does not bother anyone.
2. Create a new blueprint (eg vCenter default) and have it assigned the approval group from the previous step. Make sure the service account is admin and user in this blueprint.
3. Hook into the ‘Requested’ state and rewrite the the blueprint property (‘VirtualMachineTemplate’) to the blueprint from the previous step.
4. Change the owner of the newly requested machine to the service account.
5. Delete the UserLogs for the real user requesting the machine and move them to the service account.
6. As our machine will transition to the ‘AwaitingApproval’ state after we changed the Blueprint we will start a script asynchronously that waits for a machine request to be associated with our newly requested machine and issue a ‘Reject’ for that request (essentially issuing a ‘Destroy’ of the machine).
Note: We have to start this async as in the ‘AwaitingApproval’ state there is not yet a machine request.

Tricky? Hm, no. Laborious? Yes, a bit. In code this might look like this:

PS > $Machine.GetType().FullName

# Current Username of service account
$CurrentUsername =

### Change to new blueprint
# $VirtualMachineTemplateName is the new blueprint
$null = $m.LoadProperty($Machine, 'VirtualMachineTemplate');
$VirtualMachineTemplate = $m.VirtualMachineTemplates |?
  VirtualMachineTemplateName -eq $VirtualMachineTemplateName;
$m.SetLink($Machine, 'VirtualMachineTemplate', $VirtualMachineTemplate);
$r = $m.SaveChanges();

### Adjust property of blueprint
$null = $m.LoadProperty($Machine, 'VirtualMachineProperties');
$p = $Machine.VirtualMachineProperties |? PropertyName -eq '__approvalprofile';
$p.PropertyValue = $VirtualMachineTemplateName;
$r = $m.SaveChanges();

### Changing owner
$null = $m.LoadProperty($Machine, 'Owner');
$OwnerOriginal = $Machine.Owner;

# Change ownership of machine
$u = $m.Users |? Username -eq $CurrentUsername
$m.SetLink($Machine, 'Owner', $u);
$null = $m.LoadProperty($Machine, 'Owner')

### Remove messages from original requester
$aul = $m.UserLogs |? { $_.UserName -eq $OwnerOriginal.UserName -And $_.Message -match $Machine.VirtualMachineName };
foreach($ul in $aul) {
  $ul.Message.Replace($Machine.VirtualMachineName, ('new-{0}' -f $Machine.VirtualMachineName));
  $ul.UserName = $u.UserName;
} # foreach

And here is how to reject the waiting approval (this would be executed asynchronously as described in Child Processes in vCAC Stubs are killed upon Exit of the Parent Process):

# Creating VMPS connection
[Uri] $Uri = ('{0}/VMPS' -f '');
Log-Debug $fn ("Creating a VMPS SOAP connection to '{0}' with DefaultCrendetials ..." -f $Uri.AbsoluteUri);
$VMPS = New-WebServiceProxy -Uri $Uri -Class VMPS -Namespace VMPS;
$vmpsIdTkn = New-Object VMPS.IdentityToken;
$vmpsIdTkn.User = $Username;
if($PSBoundParameters.ContainsKey('ImpersonatingUser')) { $vmpsIdTkn.ImpersonatingUser = $ImpersonatingUser; }
Log-Debug $fn ("Creating a VMPS SOAP connection to '{0}' COMPLETED." -f $Uri.AbsoluteUri);

if($WaitTimeoutMillisecond -eq -1) { $WaitTimeoutMillisecond = [double]::MaxValue; }
$SpinTimeoutMillisecond = [Math]::Min($SpinTimeoutMillisecond, $WaitTimeoutMillisecond);
$sw = [System.Diagnostics.Stopwatch]::StartNew();
do {
  $msg = ("Waiting for Machine Requests of '{0}' [{1}] [{2}/{3}] ..." -f
    $Machine.VirtualMachineName, $Machine.VirtualMachineID, $sw.ElapsedMilliseconds, $WaitTimeoutMillisecond)
  Log-Debug $fn $msg;
  Start-Sleep -Milliseconds $SpinTimeoutMillisecond;

  Log-Debug $fn ("{0}: Loading Machine Requests ..." -f $Machine.VirtualMachineID);
  $null = $m.LoadProperty($Machine, 'Requests');
  $aReq = $Machine.Requests |? RequestTypeID -eq ([DynamicOps.ManagementModel.Common.Shim.RequestType]::CreateVM.value__ -as [int]);
  if($aReq) {
    Log-Debug $fn ("{0}: Loading Machine Requests SUCCEEDED. Found '{1}' requests." -f $Machine.VirtualMachineID, $aReq.Count);
  } # if
} while($WaitTimeoutMillisecond -gt $sw.ElapsedMilliseconds);

foreach($req in $aReq) {
  Log-Debug $fn ("{0}: Processing Machine RequestID '{1}' [RequestTypeID '{2}', RequestDate '{3}', WorkflowID '{4}'] with Action '{5}' ..." -f
    $Machine.VirtualMachineID, $Req.RequestID, $req.RequestTypeID, $req.RequestDate.ToString('yyyy-MM-dd HH:MM:ss.fffzzz'), $req.WorkflowID, $Action);
  $ru = New-Object VMPS.RequestUpdate
  $ru.RequestID = $req.RequestID;
  switch($Action) {
  'Accept' { $VMPS.AcceptVirtualMachineRequest($vmpsIdTkn, $ru); break; }
  'Reject' { $VMPS.RejectVirtualMachineRequest($vmpsIdTkn, $ru); break; }
  'AcceptLeaseExtension' { $VMPS.AcceptLeaseExtensionRequests($vmpsIdTkn, $ru); break; }
  'RejectLeaseExtension' { $VMPS.RejectLeaseExtensionRequests($vmpsIdTkn, $ru); break; }
  default {
    $msg = "Action: Parameter validation FAILED '{0}'." -f $Action;
    Log-Critical $fn $msg;
    $e = New-CustomErrorRecord -m $msg -cat InvalidData -o $Action;
  } # default
  } # switch
  Log-Debug $fn ("{0}: Processing Machine RequestID '{1}' [RequestTypeID '{2}', RequestDate '{3}', WorkflowID '{4}'] with Action '{5}' COMPLETED." -f
    $Machine.VirtualMachineID, $Req.RequestID, $req.RequestTypeID, $req.RequestDate.ToString('yyyy-MM-dd HH:MM:ss.fffzzz'), $req.WorkflowID, $Action);
} # foreach

So all in all doable, but it would have been much better, when vCAC provided an easier and better way to do this out of the box … Good news is, that in all other state the first approach via ‘FireVirtualMachineEvent’ is sufficient and will work.

As a last note: you might wonder why to go through all the hassle instead of just failing the ‘Requested’ oder any other stub? If you did that you would receive some exceptions about a failed provisioning in the logs.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.