Saturday, 11 November 2017

Hyper-V Auditing With PowerShell

Wanted to share some handy PowerShell constructions for extracting information from Hyper-V servers - particularly useful if you have multiple hosts and are trying to work out how all the VMs are configured across them all and different storage devices

Bring up an administrative PowerShell prompt (note run on Windows 10 or Server 2016, some of this doesn't work on 2012 R2), and first set up some credentials and a list of the hosts you want to query:

$cred = Get-Credential domain\adminaccount
$hosts = ('host001','host002','host003','host004')

Then grab the relevant command from below:

# get all vmswitches
$hosts | % { invoke-command -computername $_ -credential $cred -ScriptBlock {get-vmswitch}}

#show location of all hard disks
$hosts | % { invoke-command -computername $_ -credential $cred -scriptblock {(Get-VM).HardDrives }} | select VMName, ComputerName, Path | ft -autosize

# show location of all other (non-vhd) files
$hosts | % { invoke-command -computername $_ -credential $cred -scriptblock {Get-VM}} | select VMName, ComputerName, Path, CheckpointFileLocation, ConfigurationLocation, SmartPagingFilePath, SnapshotFileLocation | ft -autosize

# show all network adpaters
$hosts | % { invoke-command -computername $_ -credential $cred -scriptblock {( Get-VM).NetworkAdapters }} | select VMName, ComputerName, SwitchName, Status, IPAddresses | ft -autosize

# show all VM cpu and RAM
$hosts | % { invoke-command -computername $_ -credential $cred -scriptblock {(Get-VM) }} | ft VMName, ComputerName, @{Expression={$_.ProcessorCount};Label="Cores"}, @{Expression={[math]::Round($_.MemoryStartup/1024/1024/1024)};Label="RAM"}, @{Expression={$_.DynamicMemoryEnabled};Label="DynamicRAM"}

# show all dynamic disks
$hosts | % { invoke-command -computername $_ -credential $cred -scriptblock {(Get-VM).HardDrives } | % {get-vhd -Path $_.path}} | ft Path, VhdType, @{Expression={[math]::Round($_.FileSize/1024/1024/1024,2)};Label="FileSize (GB)"}, @{Expression={$_.Size/1024/1024/1024};Label="Size (GB)"}

If you have a lot of host servers that you are querying then this will all take some time - you can speed this up by using a workflow to query each host in parallel rather than seqeuntially:

# using a workflow to query each host in parallel
workflow getVHDLocations {
  ForEach -parallel ($vmhost in $computers) {
    InlineScript {invoke-command -computername $using:vmhost -scriptblock {(Get-VM).HardDrives } }

getVHDLocations $hosts | select VMName, ComputerName, Path | ft -autosize

Monday, 24 September 2012

Threat Management Gateway Monitoring with PowerShell

I knocked up this quick and dirty script to help find the cause of a memory issue for a client but it could easily be extended to automate and monitor any number of TMG tasks.

As is, the script lists non-paged memory pool usage and the number of active firewall sessions, but the main purpose in posting it here is to demonstrate how to access Microsoft Threat Management Gateway COM objects through PowerShell.

$smtpserver = "EXCHANGE001"
$sender = "monitoring@clientdomain.example"
$client = "Client Name"
$recipient = "support@mydomain.example"
$subject = "TMG Monitoring"

$FPC = New-Object -ComObject FPC.root
$TMGArray = $FPC.GetContainingArray()
$SessionMonitor = $TMGArray.SessionsMonitors.SessionsMonitorFirewall
$TMGFilter = New-Object -ComObject FPC.FPCFilterExpressions
$count = 0
$SessionMonitor | forEach-Object {$count++}

$message = $message + "<p>TMG Firewall Session Count: " + $count + "</p>"
$message = $message + "<p>TMG Non-Paged Pool Memory Usage (bytes)</p>"
$message = $message + "<table>"
$message = $message + "<tr><th>Process</th><th>PID</th><th>Non-Paged Memory (bytes)</th></tr>"
$procs = get-process | ? {$_.NPM -gt 1000000} | select Name, Id, NPM | sort "NPM" -Descending
Foreach ($proc in $procs) {
         $message = $message + "<tr><td>" + $proc.Name + "</td><td>" + $proc.Id + "</td><td>" + [math]::round($proc.NPM/1024) + "</td></tr>"
$message = $message + "</table>"

write $message

$smtp = new-object Net.Mail.SmtpClient($smtpserver)
$msg = new-object Net.Mail.MailMessage
$msg.From = $sender
$msg.Subject = $subject
$msg.Body = $message
$msg.IsBodyHTML = $true

Monday, 19 December 2011

How To Get Full IPv6 Net Sessions Addresses

Net Sessions is a tool I use frequently when working on client systems to find IP addresses of machines connected to servers.  However, with IPv6 addresses the net sessions command truncates the address rendering it useless.

Below is a powershell command I found to get the full IPv6 address:

gwmi win32_serversession | ft –Property ComputerName,UserName

Thursday, 15 December 2011

Automated Notifications For Machines Not Checking In To WSUS

Recently had a problem with an anti-virus update preventing computers from checking in with the WSUS server. Not too big a deal except for the fact that we didn't actually notice until our client pointed out that all the machines were showing errors connecting to the server.

Looking into this, the email notifications in WSUS (which we had set up and were working) do not list machines that have not checked in for a long time so it was not at all obvious that there was a problem. Further more, there is no way within the WSUS administration console to set up email notifications for this.

After a bit of research, I managed to put together the following PowerShell script which others may find useful (be sure to change the variables at the top, in red, to appropriate values for your environment)

# find stale computers in WSUS
# based on code:

$smtpserver = "myExchangeServer"
$sender = "
$recipient = "
$maxAge =

$smtp = new-object Net.Mail.SmtpClient($smtpserver)
$msg = new-object Net.Mail.MailMessage
$msg.From = $sender
$msg.Subject = "WSUS Machines Not Checking-In Report"
$msg.Body = "<p>WSUS Machines that have not checked-in in the last $maxAge days</p>"
$msg.Body += "<table>"

$lastValidContactDate = $(Get-Date).Adddays(-$maxAge)
[reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration") | out-null
$wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer()
$computerScope = new-object Microsoft.UpdateServices.Administration.ComputerTargetScope

$computerScope.ToLastSyncTime = $lastValidContactDate
$wsus.GetComputerTargets($computerScope) | foreach {
$msg.Body += "<tr><td>" + $_.FullDomainName + "</td><td>" + $_.LastSyncTime + "</td></tr>"
$msg.Body += "</table>"
$msg.IsBodyHTML = $true

Thursday, 27 October 2011

How To Automatically Connect To Network Shares On Login To A Mac - Part 2

Since writing my previous post on this subject I have found a much better way of doing this, and as a bonus it works as a far more generalised login script than just mapping drives.

Instead of using launchd this approach uses an OSX feature to automatically run applications at login and logout. I found the basic technique at so please check out the original article describing how to to set up the base system and then see below for how to set up drive mappings

An entry is added to the AutoLaunchedApplicationDictionary of the global loginwindow preference file. This calls an application launcher during logon for all users which then executes all scripts in a given directory.
On logout, the loginwindow LogoutHook is used to call a similar launcher to remove any drive mappings or other clean up that should be performed during logoff.

The launcher applications are just scripts that are bundled into two OS X “apps”, one for running the login scripts and one to run the logout ones. These applications and scripts are located in the following directories

- place scripts to be run at login here
- place scripts to be run at logout here

The two basic scripts that should be added to the relevant folders for connecting to network shares are:

/usr/bin/osascript <<-EOT
mount volume “smb://servername/share”
end try

/usr/bin/osascript <<-EOT
tell application “Finder” to eject “sharename”

Windows and Macs use different file formats, if these files are edited on a Windows machine they will not work – all editing or creation of new scripts must be done on a Mac.

These instructions assume that the relevant files (the Vuzzlevuzz directory and below) have been copied to the desktop of the machine you are installing on and that you are logged in as a user with administrative privileges.

Open a terminal (Finder... Utilities... Terminal) and enter the following commands (Note: each number represents one line to enter at the terminal, ignore any other line breaks)

1. sudo mkdir /Library/Vuzzlevuzz
2. sudo cp –r ~/Desktop/Vuzzlevuzz/* /Library/Vuzzlevuzz
3. sudo chmod a+x /Library/Vuzzlevuzz/
4. sudo chmod a+x /Library/Vuzzlevuzz/
5. sudo chmod a+x /Library/Vuzzlevuzz/LoginItems/*
6. sudo chmod a+x /Library/Vuzzlevuzz/LogoutItems/*
7. sudo defaults write /Library/Preferences/loginwindow '{ AutoLaunchedApplicationDictionary = ({Hide = 1; Path = "/Library/Vuzzlevuzz/"; });}'
8. sudo defaults write LogoutHook /Library/Vuzzlevuzz/


Wednesday, 26 October 2011

Active Directory Login Problems with OSX 10.7.2 Lion and .local domains

Recently one of our clients had two Macs decide to drop off the domain - all of a sudden they could no longer log in to them with active directory accounts. Both machines are running OSX Lion 10.7.2 and the problem seemed to start after installing the latest updates from Apple.

Poking about on the net, this seems to be a known problem with no real solution but I managed to piece together a process to get this working.

The problem is related to the way that OSX uses the .local domain for Bonjour and uses multicast DNS queries to try to locate network resources instead of directly querying the DNS server. If you have your Active Directory domain set up as mydomain.local (which is Microsoft best practice and the default in Small Business Server) then conflicts arise. This has been an ongoing issue with using Macs in Windows domains for some time and the reliability of domain logins seems to go up and down with each new release.

My first thought was to just disable mDNS but it seems that this is not possible as OSX uses this for normal DNS queries as well as for Bonjour.

Googling reveals lots of potential solutions for different versions of OSX but none of these worked for me. Eventually by piecing different bits of information together I stumbled on a working configuration which I repeated successfully on two machines.

I'm not sure if all of this is strictly necessary - the machines in question I was working on remotely and the client needed them up and running as soon as possible so I was not able to isolate exactly which steps are required but there is nothing in here that will cause problems for small environments. For larger installations with multiple domain controllers this isn't really the best way of setting up machines but it will at least get them logging on to the network until Apple can come up with a proper fix.

Before you start, if you have tried increasing the mDNS timeout settings as recommended by lots of articles on the net, then change them back. Once the machine has been set up as below it works correctly with the default and I suspect that increasing the timeout would actually be counterproductive as we want the mDNS query to timeout quickly so that the machine fails over to normal DNS queries

Open a terminal and type

sudo nano /System/Library/SystemConfiguration/IPMonitor.bundle/Contents/Info.plist

Ensure the mdns timeout is set to 2


First off, ensure name resolution is set up correctly by adding mydomain.local and mydomain to the DNS Search Domains

System Preferences, Network, Ethernet Connection, Advanced button, DNS, Search Domains


Ensure the naked domain resolves by adding a record for it in the machine's hosts file
Open a terminal and type

sudo nano /etc/hosts

And add the following line, substituting the correct values for your environment:

mydomain ipaddress_of_domain_controller

If the machine is already bound to the domain, then un-bind it before re-joining the domain as below:

System Preferences, Users & Groups, click Login options,
if already bound to a domain, click Edit and then Unbind. Else click Join

Do not enter the details in the first window that pops up but instead click on Open Directory Utility

Go to Services, Active Directory, and click the pencil icon to edit
Enter the domain name,


Click Show Advanced Options and click on the Administrative tab
Tick Use Preferred Server and enter the IP address (not the name) of the domain controller
Untick Allow Authentication from any domain in the forest
Click Bind, enter credentials when prompted and wait for 10 minutes...

Once this finishes, stay in the Directory Utility and go to the Search Policy tab
Remove /Active Directory/MYDOMAIN/All Domains
Add /Active Directory/MYDOMAIN/mydomain.local
Move this entry up to immediately under /Local/Default
Your Search Policy list should now look like this:

/Active Directory/MYDOMAIN/mydomain.local
/Active Directory/MYDOMAIN

Reboot and you can now login with Active Directory accounts

Friday, 30 September 2011

Sending Email through Exchange 2010 EWS with VBScript

At work we need to monitor the backups for all of our clients.  To make life easier a colleague of mine put some scripts together to read all the backup emails from the various servers and put them into a report, to easily show which have failed.

This worked great until a couple of weeks ago when we migrated from Exchange 2003 to 2010 and it all stopped working.

Of course, the guy who put this all together has since left the company so it was down to me to work out what was going on and put it right.

The mailbox monitoring script was written in well commented VBScript so it was fairly easy to follow.  I turned on some 'debugging' (wscript.echo !) and found that the script was crashing when trying to parse long email subject lines (it breaks the subject line into fields to find the client name, job name, success or failure, etc.) because there were not as many fields as it was expecting.

Now there is no native way to read a mailbox in VBScript so this script uses a 3rd party ActiveX control called FreePop to connect to the mailbox and download the messages.

A little experimentation found that the Exchange server was line-wrapping the subject at 72 characters which effectively truncated it as far as FreePop was concerned, causing the problem.  At first I looked for some way to change this line-wrapping, but it seems to be hard-coded and immutable, so I then looked about for an alternative way to access a mailbox from VBScript.

Of course, I could have used a different language (Powershell sprung to mind) but then I would have had to re-write the whole thing.  Far preferable would be to simply replace the section of code that pulls the emails from the mailbox.

After a bit of Googling I discovered that Exchange 2010 exposes a web service (imaginatively called Exchange Web Services or EWS) that can be used to do this and much more.  The only problem is that all the examples I could find were written in other languages than VBScript.

Still, not one to be deterred, I fiddled around until I got some test code successfully reading a mailbox inbox.
This code manually creates a SOAP xml message, submits it to the web service and parses the response - don't worry if you have no idea of what these things are, copy and paste works a treat :)

Watch out for those pesky line-breaks...

' set some variables
servername = "myExchangeServer"
username = "DOMAIN\User"
password = "UserPassword"

' enumerate items in inbox
smsoapmessage  = "<?xml version='1.0' encoding='utf-8'?>" _
& "<soap:Envelope xmlns:soap='' xmlns:t=''>" _
& "  <soap:Body>"_
& "    <FindItem xmlns='' xmlns:t='' Traversal='Shallow'>" _
& "      <ItemShape>" _
& "        <t:BaseShape>AllProperties</t:BaseShape>" _
& "      </ItemShape>" _
& "      <ParentFolderIds>" _
& "        <t:DistinguishedFolderId Id='inbox'/>" _
& "      </ParentFolderIds>" _
& "    </FindItem>" _
& "  </soap:Body>" _
& "</soap:Envelope>"

set req = createobject("microsoft.xmlhttp")
req.Open "post", "https://" & servername & "/ews/Exchange.asmx", False,username,password 
req.setRequestHeader "Content-Type", "text/xml"
req.setRequestHeader "translate", "F"
req.send smSoapMessage

if req.status = 200 then
  set oXMLDoc = req.responseXML
  set ResponseCode = oXMLDoc.getElementsByTagName("m:ResponseCode")
  if ResponseCode(0).childNodes(0).text = "NoError" then
    Set oXMLSubject = oXMLDoc.getElementsByTagName("t:Subject")
      For i = 0 To (oXMLSubject.length -1)
        set oSubject = oXMLSubject.nextNode
        WScript.echo i & vbTab & oSubject.text
    WScript.echo "Exchange server returned error: " & ResponseCode(0).childNodes(0).text
  end if
  WScript.echo "Failed to connect to Exchange server, error code: " & req.status
end if

When I first ran this I was getting a 200 for the request object which is a success but the Exchange server was returning "ErrorServerBusy".  This threw me for a while, but I then discovered another new feature in Exchange 2010 called Throttling Policies.

Throttling policies are designed to prevent malicious or unintentional large requests from affecting the performance of the server and are enabled by default in Exchange 2010

MSDN blog on Understanding Throttling Policies

Using the Exchange shell to run the get-ThrottlingPolicy shows you all the default settings.  In my case the problem was caused by the default EWSFindCountLimit being 1000 - the inbox I was querying had over 2000 items in it.

I could have changed the EWSFindCountLimit setting in either the default throttling policy or created a custom one just for this user, but as the only reason there was so many emails in the inbox was because the script had not been running for a while and this normally clears them out when it finishes, I just deleted all the old emails to get below a thousand.
Everything then worked beautifully.

Deleting the emails from the inbox after they have been processed is achieved by calling the EmptyFolder method with another SOAP request.  Here is the xml for the request, I'll leave the actual VBScript to do this as an exercise for the reader :)

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="" xmlns:m="" xmlns:t="" xmlns:soap="">
    <t:RequestServerVersion Version="Exchange2010_SP1" />
    <m:EmptyFolder DeleteType="HardDelete" DeleteSubFolders="false">
        <t:DistinguishedFolderId Id='inbox'/>