Immybot

Desktop management like no other

Metascripts

In release 0.27.0 we introduced a new Execution Context called “Metascript”. Why did we call it “Metascript”? Because it’s a script that runs scripts. We initially called it “Server-Side” scripts, but we felt that would lead users to believe that the script would be running on a customer’s server, when in actuality the script is running in-process on the ImmyBot backend.

If you’re a PowerShell nerd, Metascript will be remniscent of PowerShell Workflows introduced in PowerShell 4.0.

Metascripts are useful in scenarios like the following:

  • You need to continue certain logic after a reboot
  • Your script contains sensitive information like an API key that you aren’t comfortable sending to a client machine

Example: Configuring 3CX Phone for Windows

The problem: 3CX sends a “Welcome to 3CX Email” email to each user requiring them to Open an attached .3cxconfig file (which is just XML) containing the server and extension information for the user.

Your end user would have to double click that file to configure the application. We all know that the 3CX Phone will remain unconfigured if left to the end user to do this.

The file is stored on the 3CX server and you can get the link via the API, but you don’t want to send a script containing API credentials to the end users’ computer.

The solution:

Write-Host "Getting information about selected computer..."
$Computer = Get-ImmyComputer
$UPN = $Computer.PrimaryPerson.EmailAddress
Write-Host "Primary user is $($Computer.PrimaryPerson)"
[System.Uri]$Uri = "https://$HostName/api"
[System.Uri]$LoginUri = "https://$HostName/api/login"
$webSession = $null
Write-Host "Authenticating to 3CX server"
$AuthResponse = Invoke-RestMethod -Method POST -SessionVariable webSession -Uri $LoginUri -Body (ConvertTo-Json @{"Username"=$Username;"Password"=$Password}) -ContentType "application/json; charset=utf-8"
Write-Host "Getting extension list"
$Extensions = Invoke-RestMethod "https://$HostName/api/ExtensionList" -WebSession $webSession
Write-Host "Finding extension for user with email address: $UPN"
$DesiredExtension = $Extensions.List | Where-Object {$_.Email -like $UPN}
Write-Host "Found extension $DesiredExtension"
$SetResponse = Invoke-Restmethod -WebSession $webSession -Method POST -Uri "https://$HostName/api/ExtensionList/set" -Body (ConvertTo-Json @{Id="$($DesiredExtension.Id)"}) -ContentType "application/json; charset=utf-8" #-Headers $Headers 
$ProvisioningLink = [Uri]"https://$Hostname$($SetResponse.ActiveObject.MyPhoneProvLink._value)"
Write-Host "Provisioning file URI: $ProvisioningLink"
Write-Host "Switching from Server to User context"
Invoke-ImmyCommand -ScriptBlock {
    $ProvisioningFilePath = [IO.Path]::ChangeExtension([IO.path]::GetTempFileName(),".3cxconfig")
    Invoke-RestMethod $using:ProvisioningLink -OutFile $ProvisioningFilePath    
    Start-Process $ProvisioningFilePath
} -Context User
Write-Host "Done."

The juicy bit is here:

Invoke-ImmyCommand -ScriptBlock {
    $ProvisioningFilePath = [IO.Path]::ChangeExtension([IO.path]::GetTempFileName(),".3cxconfig")
    Invoke-RestMethod $using:ProvisioningLink -OutFile $ProvisioningFilePath    
    Start-Process $ProvisioningFilePath
} -Context User

Invoke-ImmyCommand works just like PowerShell’s native Invoke-Command, with an additional -Context parameter.

Here’s its definition

Invoke-ImmyCommand [-ScriptBlock] <Object> [-Computer <Computer[]>] [-Context <string> { User | System }] [-ArgumentList <array>]

This is very powerful for MSPs because the native Invoke-Command doesn’t work for us since we aren’t on the same network and domain as the client machines. Even if we were, client machines would have had to be configured to accept inbound connections, typically via Group Policy. Fancy scripts like Adam’s Get-InstalledSoftware wouldn’t work for us, but now it can by simply changing line 97 by replacing Invoke-Command with Invoke-ImmyCommand

For those who dabble in the details, Invoke-ImmyCommand serializes all input and output objects just like the regular Invoke-Command does. This means the results you get back are real objects, not string representations. It took a lot of time to get that right.

Passing Parameters

We support both the PowerShell 2.0 -ArgumentList (with or without a param block in your script block) and the more concise PowerShell 3.0 $using Scope Modifier.

In this example, we chose the $using technique. Variables like $ProvisioningLink are available in your script block as $using:ProvisioningLink.

Note: The $using scope modifier works even if the target machine only has PowerShell 2.0.

If you are more comfortable with the 2.0 syntax, I could have done it like this

Invoke-ImmyCommand -ScriptBlock {
    param($ProvisioningLink)
    $ProvisioningFilePath = [IO.Path]::ChangeExtension([IO.path]::GetTempFileName(),".3cxconfig")
    Invoke-RestMethod $ProvisioningLink -OutFile $ProvisioningFilePath    
    Start-Process $ProvisioningFilePath
} -Context User -ArgumentList @($ProvisioningLink)

or like this

Invoke-ImmyCommand -ScriptBlock {
    $ProvisioningFilePath = [IO.Path]::ChangeExtension([IO.path]::GetTempFileName(),".3cxconfig")
    Invoke-RestMethod $args[0] -OutFile $ProvisioningFilePath    
    Start-Process $ProvisioningFilePath
} -Context User -ArgumentList @($ProvioningLink)

How does the Context parameter work?

We combine the techniques in murrayju/CreateProcessAsUser and this stackoverflow comment to run your script as the user and also suppress the PowerShell window. It’s a boat-load of code, but creates a completely seamless experience.

What’s Next?

Customer and People Targets

In the current implementation, a computer needs to be selected before the Metascript can be run, and that computer is provided as a default parameter to Invoke-ImmyCommand. But what if your script is checking if legacy protocols are enabled

Scheduling