PowerShell and Notify My Android (NMA)
October 7, 2013 — 11:08

Here is a PowerShell function which wraps around the Notify My Android notification API call. You can use it in your environment to Notify Android devices when you like.

Please note that the line marked as 58 below, that reads as 1$webpage, should actually be [ x m l ] $webpage (without spaces) so you will need to manually correct this (the limitations of the syntax highlighter plugin prevents this from displaying correctly).

function New-NMANotification
{
	<#
	.SYNOPSIS 
	This function takes supplied parameters to makes a call to the NotifyMyAndroid (NMA) service.
	.DESCRIPTION
	This function takes supplied parameters to makes a call to the NotifyMyAndroid (NMA)  service.
	The NotifyMyAndroid service will then push these notifications to the NMA application on your
	Android device. 
	.PARAMETER application
	The application that needs notification on.
	.PARAMETER event
	The type of event (e.g. Service down, Path lost).
	.PARAMETER description
	The full description of the event (10000 max).
	.PARAMETER priority
	A priority level for this notification. This is optional and in the future will be used to change the way NMA alerts you.
	.EXAMPLE
	New-NMANotification -priority 0 -application "esxihost-01" -event "Patching complete" -description "Server finished patching. Build version now 1157734"
	.EXAMPLES
	New-NMANotification -priority 2 -application "vcenter01" -event "DOWN: VMware VirtualCenter Management Webservices" -description "Detected as down at 10:27. Impact: Prevents users logging into the vSphere Client."
	.NOTES
	Author: Robin Malik
	#>
	
	
	# Leave previous two lines blank
	param(
		[Parameter(Mandatory=$true,HelpMessage="The application that needs notification on.")]
		[String]
		$application,
		
		[Parameter(Mandatory=$true,HelpMessage="The type of event (e.g. Service down, Path lost)")]
		[String]
		$event,
		
		[Parameter(Mandatory=$true,HelpMessage="The full description of the event (10000 max).")]
		[String]
		$description,

		[Parameter(HelpMessage="A priority level for this notification. This is optional and in the future will be used to change the way NMA alerts you.")]
		[ValidateSet("-2","-1","0","1","2")]
		[String]
		$priority = "0"
	)
	
	$nmaURL = "https://www.notifymyandroid.com/publicapi/notify"	
	$apikey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

	$parameters = "?apikey=$apikey&application=$application&event=$event&description=$description&priority=$priority"
	$fullurl = $nmaURL + $parameters	
	
	
    try
    {
        $webclient = new-object System.Net.WebClient
        1$webpage = $webclient.DownloadString($fullurl)
        if($webpage.nma.success.code -eq 200)
        {
            return 1
        }
        else
        {
            return 0
        }
    }
    catch
    {
        Write-Debug $Error[0].Exception.Message
        return 0
    }

}

Example:
$result = New-NMANotification -priority 0 -application $application -event $event -description $description
if($result -eq 1)
{
    # Success
}
elseif($result -eq 0)
{
    # Failure
}
else
{
    # Unhandled
}
Generating Remote Desktop Connection Manager (RDCMan) files with PowerShell
September 1, 2013 — 14:35

When I stumbled across this article on IT Pro in June this year a knowing smile crept across my face. The author had discovered an extremely handy tool for managing multiple remote desktop connections and seemingly puzzled over why it wasn’t more well known; my thoughts exactly. I found this utility about 3 years ago when searching for an easier way to manage remote desktop connections to multiple Windows servers. It made absolutely no sense to have to open multiple unique Remote Desktop / mstsc.exe windows to every server you wanted to work on. I was very relieved to find RDCMan produced by Microsoft.

The downside to this simple tool though is that you have to add servers manually, one by one. With some 200 servers (at the time) this was a potentially painstaking task, and one that I didn’t want to undertake if there was an easier alternative. The .rdg file produced by RDCMan is actually just an XML file (discovered by dropping the file into Notepad) so I figured that it’d be quite straightforward to automate the production of this using PowerShell, grabbing a list of servers from AD (or .csv, .txt or wherever). Thankfully Jan Egil Ring had done some of the work for me, but I modified it slightly and turned it into a more suitable script for our environment. Pass the function a username, an array of computer objects and an output path and you get an RDCMan file with the computers of your choice for the user of your choice. Rather than call the script multiple times from Task Scheduler (i.e defining the parameters at the script level) I just schedule it once (on Monday morning) and within the script call the function multiple times for all the users who need a file.

Here’s the script:

<#
	.SYNOPSIS 
	This generates a Remote Desktop Manager file for computer objects within Active Directory.
	.DESCRIPTION
	This generates a Remote Desktop Manager file for computer objects within Active Directory.
	Objects are generated using Microsoft's Active Directory module.
	Pass values required to the function rather than the script.
	It is based off "New-RDCManFile.ps1" by: Jan Egil Ring.
	.PARAMETER debugScript
	Switch on Write-Debug output. Default is No.
	.EXAMPLE
	C:\PS> New-RDCManFile.ps1
	.NOTES
	Author: Jan Egil Ring | Robin Malik
#>


# Leave previous two lines blank
#*=============================================================================
#* PARAMETER DECLARATION
#*=============================================================================

#*=============================================================================
#* REVISION HISTORY
#*=============================================================================
#* Date: 	YYYY-MM-DD
#* Author:	Your Name
#* Purpose:	Why and how you modified the script in brief. Do not delete
#*			previous revision history blocks.
#*
#* Date: 	YYYY-MM-DD
#* Author:	Your Name
#* Purpose:	Why and how you modified the script in brief. Do not delete
#*			previous revision history blocks.
#*=============================================================================

#*=============================================================================
#* DEFINE GLOBAL VARIABLES
#*=============================================================================
$startDateTime = Get-Date
$EnableEmail = 1
$DebugPreference = "SilentlyContinue"

if($debugScript -eq "Yes"){
	$DebugPreference = "Continue"	# Write-Debug commands.
	$EnableEmail = Read-Host("Enable email, 0 = No, 1 = Yes [0/1]: ")
}

#*=============================================================================
#* IMPORT SNAPINS AND MODULES
#*=============================================================================
try
{
	Import-Module ActiveDirectory -ErrorAction Stop
}
catch
{
	Write-Error $($Error[0].Exception.Message)
	# Send email or whatever...
	exit
}

#*=============================================================================
#* IMPORT LIBRARIES
#*=============================================================================

#*=============================================================================
#* EXCEPTION HANDLER
#*=============================================================================

#*=============================================================================
#* FUNCTION LISTINGS
#*=============================================================================

#*=============================================================================
#* Function:	New-LURDCMFile
#* ============================================================================
function New-RDCManFile
{
	<#
	.SYNOPSIS 
	This generates a Remote Desktop Manager file for computer objects within Active Directory.
	.DESCRIPTION
	This generates a Remote Desktop Manager file for computer objects within Active Directory.
	.PARAMETER username
	This username that you wish to be present in the RDG file by default.
	.PARAMETER outputPath
	The output path for the file (e.g. D:\).
	.PARAMETER computerArray
	Array of computer objects from Active Directory.
	.EXAMPLE
	Verb-LUServiceNoun -param1 "foo" -param2 "bar"
	.NOTES
	Author: Your Name
	#>
	
	
	# Leave previous two lines blank
	param(
		[Parameter(Mandatory=$true,HelpMessage="Admin account.")]
		[String]
		$username,

		[Parameter(Mandatory=$true,HelpMessage="Output Path for file.")]
		[String]
		$outputPath,
		
		[Parameter(Mandatory=$true,HelpMessage="Array of computers.")]
		[Array]
		$computerArray
	)
	
	
# Create a template XML. This needs to be indented to the margin so that the output XML file has no indent.
$template = @' 
<?xml version="1.0" encoding="utf-8"?> 
<RDCMan schemaVersion="1"> 
    <version>2.2</version> 
    <file> 
        <properties> 
            <name></name> 
            <expanded>True</expanded> 
            <comment /> 
            <logonCredentials inherit="FromParent" /> 
            <connectionSettings inherit="FromParent" /> 
            <gatewaySettings inherit="FromParent" /> 
            <remoteDesktop inherit="FromParent" /> 
            <localResources inherit="FromParent" /> 
            <securitySettings inherit="FromParent" /> 
            <displaySettings inherit="FromParent" /> 
        </properties> 
        <group> 
            <properties> 
                <name></name> 
                <expanded>True</expanded> 
                <comment /> 
                <logonCredentials inherit="None"> 
                    <userName></userName> 
                    <domain></domain> 
                    <password storeAsClearText="False"></password> 
                </logonCredentials> 
                <connectionSettings inherit="FromParent" /> 
                <gatewaySettings inherit="None"> 
                    <userName></userName> 
                    <domain></domain> 
                    <password storeAsClearText="False" />
                    <enabled>False</enabled> 
                    <hostName /> 
                    <logonMethod>4</logonMethod> 
                    <localBypass>False</localBypass> 
                    <credSharing>False</credSharing> 
                </gatewaySettings> 
                <remoteDesktop inherit="FromParent" /> 
                <localResources inherit="FromParent" /> 
                <securitySettings inherit="FromParent" /> 
                <displaySettings inherit="FromParent" /> 
            </properties> 
            <server> 
                <name></name> 
                <displayName></displayName> 
                <comment /> 
                <logonCredentials inherit="FromParent" /> 
                <connectionSettings inherit="FromParent" /> 
                <gatewaySettings inherit="FromParent" /> 
                <remoteDesktop inherit="FromParent" /> 
                <localResources inherit="FromParent" /> 
                <securitySettings inherit="FromParent" /> 
                <displaySettings inherit="FromParent" /> 
            </server> 
        </group> 
    </file> 
</RDCMan> 
'@ 
	
	$outputFile = $outputPath + "-$username" + ".rdg"
	
	# Output $template to a temporary XML file:
	$template | Out-File $home\RDCMan-template.xml -encoding UTF8 
	 
	# Load the XML template into XML object: 
	$xml = New-Object xml 
	$xml.Load("$home\RDCMan-template.xml") 
	 
	# Set the file properties:
	$file = (@($xml.RDCMan.file.properties)[0]).Clone() 
	$file.name = $domain 
	$xml.RDCMan.file.properties | Where-Object { $_.Name -eq "" } | ForEach-Object  { [void]$xml.RDCMan.file.ReplaceChild($file,$_) } 
	 
	# Set the group properties 
	$group = (@($xml.RDCMan.file.group.properties)[0]).Clone() 
	$group.name = $env:userdomain 
	$group.logonCredentials.Username = "$username"
	$group.logonCredentials.Domain = $domain

	$xml.RDCMan.file.group.properties | Where-Object { $_.Name -eq "" } | ForEach-Object  { [void]$xml.RDCMan.file.group.ReplaceChild($group,$_) } 
	 
	# Use template to add servers from Active Directory to the XML  
	$server = (@($xml.RDCMan.file.group.server)[0]).Clone()
	
	$computerArray | ForEach-Object {
	$server = $server.clone()
	[string]$server.DisplayName = $_.Name
	[string]$server.Name = $_.DNSHostName

	$xml.RDCMan.file.group.AppendChild($server) > $null} 
	# Remove template server 
	$xml.RDCMan.file.group.server | Where-Object { $_.Name -eq "" } | ForEach-Object { [void]$xml.RDCMan.file.group.RemoveChild($_) } 
	 
	# Save the XML object to a file 
	$xml.Save($outputFile) 
	 
	# Remove the temporary XML file:
	Remove-Item $home\RDCMan-template.xml -Force	
}

#*=============================================================================
#* END OF FUNCTION LISTINGS
#*=============================================================================

#*=============================================================================
#* SCRIPT BODY
#*=============================================================================
$domain = $Env:USERDOMAIN
# Base output path:
$outputPath = "C:\RDCMan"

# Example to get a list of MemberServers and Domain Controllers:
$computerObjects1 = Get-ADComputer -SearchBase "OU=MemberServers,DC=lunet,DC=lboro,DC=ac,DC=uk" -LDAPFilter "(operatingsystem=*Windows server*)"  | Select-Object -property name,dnshostname
$computerObjects2 = Get-ADComputer -SearchBase "OU=Domain Controllers,DC=lunet,DC=lboro,DC=ac,DC=uk" -LDAPFilter "(operatingsystem=*Windows server*)" | Select-Object -property name,dnshostname
$allComputers = $computerObjects1 + $computerObjects2 | Sort-Object

# Call the function to generate the file:
$filePrefix = "allservers"
New-RDCManFile -username "useraccount-admin" -outputPath "$outputPath\$filePrefix" -computerArray $allComputers
New-RDCManFile -username "useraccount2-admin" -outputPath "$outputPath\$filePrefix" -computerArray $allComputers


# Example to output a list of all SQL servers (from an AD security group):
$filePrefix = "sqlservers"
$sqlservers = Get-ADGroupMember -Identity "sql-servers" | Get-ADComputer | Select-Object -property name,dnshostname | Sort-Object -Property name
# Call the function to generate the file:
New-RDCManFile -username "useraccount-admin" -outputPath "$outputPath\$filePrefix" -computerArray $sqlservers

# Optional block to output script execution time:
#$endDateTime = Get-Date
#$scriptExecutionMin = ($endDateTime.Subtract($startDateTime).Minutes)
#$scriptExecutionSec = ($endDateTime.Subtract($startDateTime).Seconds)	
#Write-Output "Script execution time: $scriptExecutionMin min $scriptExecutionSec sec."
#*=============================================================================
#* END SCRIPT BODY
#*=============================================================================

#*=============================================================================
#* END OF SCRIPT
#*=============================================================================
Yet Another Invoke-SSH PowerShell Function (thanks PS Fab)
March 7, 2013 — 12:54

As part of a writing a decommission server PowerShell script at work, I had a requirement for a quick and easy SSH function to connect to our NetBackup server at work and remove the server from the backup system (Symantec if you’re reading this, please can we have a PowerShell module? *wishful thinking*…). Not quite needing the entire functionality provided by this module (based on the SSH.NET Library), I came across a function on PS Fab. One fantastic thing about this function is that supports the automatic acceptance of an SSH key when you connect to a host for the first time.

I made a few changes to the code, amended comments that referenced plist rather than plink and put it into a more standard function form with examples (that you might be able to add to an existing library). This is the result:

Function Invoke-SSH 
{
	<#
	.SYNOPSIS 
	Uses Plink.exe to SSH to a host and execute a list of commands.
	.DESCRIPTION
	Uses Plink.exe to SSH to a host and execute a list of commands.
	.PARAMETER hostname
	The host you wish to connect to.
	.PARAMETER username
	Username to connect with.
	.PARAMETER password
	Password for the specified user account.
	.PARAMETER commandArray
	A single, or list of commands stored in an array object.
	.PARAMETER plinkAndPath
	The location of the plink.exe including the executable (e.g. F:\tools\plink.exe)
	.PARAMETER connectOnceToAcceptHostKey
	If set to true, it will accept the remote host key (use when connecting for the first time)
	.EXAMPLE
	Invoke-SSH -username root -hostname centos-server -password Abzy4321! -plinkAndPath "F:\tools\plink.exe" -commandArray $commands -connectOnceToAcceptHostKey $true
	.EXAMPLE
	Invoke-SSH -username root -hostname centos-server -password Abzy4321! -plinkAndPath "F:\tools\plink.exe" -commandArray ifconfig -connectOnceToAcceptHostKey $true
	.NOTES
	Author: Robin Malik
	Source: Modified from: http://www.zerrouki.com/invoke-ssh/
	#>
	
	Param(
		[Parameter(Mandatory=$true,HelpMessage="Enter a host to connect to.")]
		[string]
		$hostname,
		
		[Parameter(Mandatory=$true,HelpMessage="Enter a username.")]
		[string]
		$username,
		
		[Parameter(Mandatory=$true,HelpMessage="Enter the password.")]
		[string]
		$password,
		
		[Parameter(Mandatory=$true,HelpMessage="Provide a command or comma separated list of commands")]
		[array]
		$commandArray,
		
		[Parameter(Mandatory=$true,HelpMessage="Path to plink (e.g. F:\tools\plink.exe).")]
		[string]
		$plinkAndPath,
		
		[Parameter(HelpMessage="Accept host key if connecting for the first time (the default is `$false)")]
		[string]
		$connectOnceToAcceptHostKey = $false
	)
	
	$target = $username + '@' + $hostname
	$plinkoptions = "-ssh $target -pw $password"
	 
	# On first connect to a host, plink will prompt you to accept the remote host key. 
	# This section will login and accept the host key then logout:
	if($ConnectOnceToAcceptHostKey)
	{
		$plinkCommand  = [string]::Format('echo y | & "{0}" {1} exit', $plinkAndPath, $plinkoptions )
		$msg = Invoke-Expression $plinkCommand
	}
	
	# Build the SSH Command by looping through the passed value(s). Append exit in order to logout:
	$commandArray += "exit"
	$commandArray | % { $remoteCommand += [string]::Format('{0}; ', $_) }
	
	# Format the command to pass to plink:
	$plinkCommand = [string]::Format('& "{0}" {1} "{2}"', $plinkAndPath, $plinkoptions , $remoteCommand)
	 
	# Execute the command and display the output:
	$msg = Invoke-Expression $plinkCommand
	Write-Output $msg
}

Copy and paste this function into a PowerShell window, and then test it with the below code (changing where appropriate of course):

$plinkAndPath = "F:\tools\plink\plink.exe"
$username = "root"
$password = "Adk3453#5341!"
$hostname = "centos-server"
# Commands to execute:
$Commands = @()
$Commands += "ifconfig"
$Commands += "ls"
Invoke-SSH -username $username -hostname $hostname -password $password -plinkAndPath $plinkAndPath -commandArray $Commands -connectOnceToAcceptHostKey $true
Catching Virtual Machine questions with PowerCLI
October 19, 2012 — 11:34

As part of our VM-template, template-VM conversion process at work (which I’ve automated via a webpage as discussed in my previous post “Executing Powershell using PHP and IIS“), I had to find a way to handle the VM question “This VM has questions that must be answered before the operation can continue” when attempting to power the VM on via Start-VM.

I found that handling the question was not possible using a simple try/catch block (the explanation can be found in this excellent post by Clint Bergman). I was however able to catch it using the following block:

try
{
	try
	{
		Start-VM -VM $serverName -ErrorAction Stop -ErrorVariable custErr
	}
	catch [System.Management.Automation.ActionPreferenceStopException]
	{
		throw $_.Exception
	}
}
catch [VMware.VimAutomation.ViCore.Types.V1.ErrorHandling.VMBlockedByQuestionException]
{
	Write-Output "Power on operation triggered a VMBlockedByQuestionException. Answering question with `"I moved it`". <br />"
	Get-VMQuestion -VM $serverName | Set-VMQuestion –Option "I moved it" -Confirm:$false	
}

It may seem like a lot of work to convert between a VM and template (and vice versa) but in our environment there are a couple of critical steps to the conversation process that must be followed to ensure deploys from these templates don’t break. As there are users outside of the team who update various templates I wanted to provide an easy way for everyone to ensure consistency when doing this, and the best way to do this is of course, automation :)