Developer42

2017-02-04

PowerShell Suggestion: Simplify Write-Verbose in Modules

Write-Verbose is a really useful command in PowerShell; it lets you add code to see what’s going on behind the scenes, which you can easily toggle on and off with a simple parameter, rather than by amending your code each time you need to investigate some issue. Here’s a blog nicely summarising it for those unfamiliar: https://blogs.technet.microsoft.com/poshchap/2014/09/18/scripting-tips-and-tricks-write-verbose/.

However, in my opinion there’s one major flaw with this; it doesn’t work the way I’d expect it to where modules are concerned. That is, if I have Write-Verbose statements in my code, and I put my common code into a .psm1 module file then import that module into the script (.ps1 file), the behaviour will not be the same as were I to just have those functions defined in my script.

To illustrate, say I had the following code:

# MyScript.ps1
[CmdletBinding()]
param()

function Invoke-Demo {
    [CmdletBinding()]
    param ([Parameter()][string]$Message)
    process {Write-Verbose $Message}
}

Invoke-Demo 'This DOES show when I run ".\MyScript.ps1 -Verbose"'
Write-Verbose 'This DOES show when I run ".\MyScript.ps1 -Verbose"'

Running this script with the Verbose parameter outputs 2 lines:

.\MyScript.ps1 -Verbose
This DOES show when I run ".\MyScript.ps1 -Verbose
This DOES show when I run ".\MyScript.ps1 -Verbose

However, were I to put my function into a module (i.e. to simplify its re-use in other scripts) as below, the functionality would change; making it harder for me to investigate issues.

# MyModule.psm1
function Invoke-Demo {
    [CmdletBinding()]
    param ([Parameter()][string]$Message)
    process {Write-Verbose $Message}
}
# MyScript.ps1
[CmdletBinding()]
param()
Import-Module -Path '.\MyModule.psm1' -Force 
Invoke-Demo 'This does NOT show when I run ".\MyScript.ps1 -Verbose"'
Write-Verbose 'This DOES show when I run ".\MyScript.ps1 -Verbose"'
.\MyScript.ps1 -Verbose
This DOES show when I run ".\MyScript.ps1 -Verbose

I can understand that perhaps MS decided that modules contain that’s at a stage that’s ready to be packaged and shared, and therefore assumed that people may not wish to see verbose output from the packaged module, but I’d assume that there should be some way to override this for anyone who does need to look under the covers.

There is a workaround suggested by Craig on StackOverflow: http://stackoverflow.com/a/16442063/361842. That is, whenever calling a function from a module, assign the verbose parameter with the value of the caller’s Verbose parameter as so:

Invoke-Demo 'This DOES show when I run ".\MyScript.ps1 -Verbose"; but it smells' -Verbose:($PSBoundParameters['Verbose'] -eq $true)`

Suggested Improvement

Add a parameter to the Import-Module cmdlet which states whether to inherit the caller’s Verbose setting (be that from $VerbosePreference or because the calling function/script was called with the Verbose switch specified (be that explicitly or through inheritance). i.e.

# MyModule.psm1
function Invoke-Demo {
    [CmdletBinding()]
    param ([Parameter()][string]$Message)
    process {Write-Verbose $Message}
}
# MyScript.ps1
[CmdletBinding()]
param()
Import-Module -Path '.\MyModule.psm1' -Force -InheritVerbose # <-- new switch on Import-Module
Invoke-Demo 'This now DOES show when I run ".\MyScript.ps1 -Verbose" (if above suggestion implemented by MS)'
Write-Verbose 'This DOES show when I run ".\MyScript.ps1 -Verbose"'

NB: Currently MS Connect for PowerShell (https://connect.microsoft.com/PowerShell/) is not taking submissions, hence blogging this.

If you like this idea, please share; I’m hoping to get this on Microsoft’s radar so that it can be implemented; or some improved solution can be offered.

Update: Thanks to Zachary Alexander for pointing out that suggestions can be submitted on GitHub.  This is now logged here: https://github.com/PowerShell/PowerShell/issues/3106

 

2016-03-08

Update Active Directory Thumbnail Photo for All Users Without a Pic to Show Their Initials

We recently upgraded to Lync 2015 (aka Skype for Business). One feature which frustrated me was the group conversations; you can either see people’s names; in which case they’re listed in such a way as to use up a large amount of screen space, or you can see people’s photos/thumbnails, which is great except when most people don’t have their photo associated with their account.

To resolve this quickly I came up with the below script which:

  • Gets all AD users under a given OU / SearchBase
  • Filters for those without a thumbnail photo associated
  • Determines the users’ initials
  • Creates a JPG of their initials
  • Assigns this JPG as their thumbnail image

Thus giving an easier way to tell who you’re talking to until they get around to uploading a photo.

NB: The below script is currently untested as I don’t have sufficient rights to update AD / will have to wait for our infrastructure team to assist there… but those parts that I could test have been tested.

Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.IO
Add-Type -AssemblyName Microsoft.ActiveDirectory.Management
Import-Module ActiveDirectory

function Convert-InitialsToJpegImageByteArray {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Initials
        ,
        [Parameter(Mandatory = $false)]
        [int]$WidthInPixels = 96  #Ref https://technet.microsoft.com/en-gb/library/jj688150.aspx: decided not to go 648x648 just for text
        ,
        [Parameter(Mandatory = $false)]
        [int]$HeightInPixels = 96
        ,
        [Parameter(Mandatory = $false)]
        [System.Drawing.Font]$Font = (new-object System.Drawing.Font ('Proxima Nova Alt',32, [System.Drawing.FontStyle]::Bold))
        ,
        [Parameter(Mandatory = $false)]
        [System.Drawing.Color]$BackgroundColor = "#FF000434" #ARGB Colors
        ,
        [Parameter(Mandatory = $false)]
        [System.Drawing.Color]$ForegroundColor = "#FF0099D8"
    )
    process {

        [System.Drawing.Bitmap]$bmp = new-object System.Drawing.Bitmap ($widthInPixels, $heightInPixels)
        [System.Drawing.Brush]$BrushBg = (new-object System.Drawing.SolidBrush ($BackgroundColor))
        [System.Drawing.Brush]$brushFg = (new-object System.Drawing.SolidBrush ($ForegroundColor))

        [System.Drawing.Graphics]$graphics = [System.Drawing.Graphics]::FromImage($bmp) 
        $graphics.FillRectangle($brushBg,0,0,$bmp.Width,$bmp.Height) 
        $graphics.DrawString($initials,$font,$brushFg,15,18) 
        $graphics.Dispose() 

        [System.IO.MemoryStream]$stream = new-object System.IO.MemoryStream
        $bmp.Save($stream,[System.Drawing.Imaging.ImageFormat]::Jpeg)
        $stream.Close()
        $stream.ToArray()
        $stream.Dispose()

    }
}

function Get-AdUserInitials {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Microsoft.ActiveDirectory.Management.ADUser]$User
    )
    process {
        #$User = Get-ADUser $Identity #we could do parameter sets to allow passing only an identity
        $User | select-object @{Name='User';Expression={$User}}, @{Name='Initials';Expression={("{0}{1}" -f ("{0}?" -f $_.GivenName)[0],("{0}?" -f $_.Surname)[0]).ToUpperInvariant()}}
    }
}

function Get-AdUsersWithNoThumbnail {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$SearchBase
        ,
        [Parameter(Mandatory = $false)]
        [ValidateSet('Base','OneLevel','Subtree')]
        [string]$SearchScope = 'Subtree'
        ,
        [Parameter(Mandatory = $false)]
        [string]$Filter = {Enabled -eq $true}
    )
    process {
        get-aduser -filter {Enabled -eq $true} -SearchBase $SearchBase -SearchScope $SearchScope -Properties thumbnailPhoto | ?{!$_.thumbnailPhoto} 
    }
}


@('OU=UK,DC=myCompany,DC=com') `
| Get-AdUsersWithNoThumbnail `
| Get-AdUserInitials `
| %{
    $User = $_.User
    [byte[]]$Image = $_.Initials | Convert-InitialsToJpegImageByteArray 
    Set-Aduser $user -replace @{thumbnailPhoto=$Image}
}

2016-02-05

PowerShell PlayTime :: Project Oxford

I recently stumbled across Microsoft’s Project Oxford; a collection of “AI” APIs for computer vision, speech and language. More info can be found here:

  1. MSDN: https://msdn.microsoft.com/en-us/library/mt422983.aspx
  2. Demo: https://www.projectoxford.ai/demo
  3. GitHub: https://github.com/Microsoft/ProjectOxford-ClientSDK

The project has links to a C# SDK, but that felt a bit heavy-weight for what are essentially just web services; so I thought I’d have a quick play with these functions using PowerShell.

Before coding, an API key is required. This can be obtained by logging into the following site using your MS Account (aka .net passport / live account) and registering for the free API keys: https://www.projectoxford.ai/Subscription.

I began by looking at the Emotion API, trawling through the client SDK code found here: https://github.com/Microsoft/ProjectOxford-ClientSDK/blob/master/Emotion/Windows/ClientLibrary/EmotionServiceClient.cs

Based on that code I was able to find all the info needed to call the rest services through PowerShell, resulting in this short script:

Clear-Host
$imageFilePath = 'c:\SomeFolder\ImageOfMeLookingAwesome.jpg'
$emotionApiKeyFree = '**my emotion aki key goes here**' #api keys: https://www.projectoxford.ai/Subscription

$RecogniseUri = ("https://api.projectoxford.ai/emotion/v1.0/recognize?subscription-key={0}" -f $emotionApiKeyFree)
$ContentType = 'application/octet-stream'
$Body = [System.IO.MemoryStream][System.Convert]::FromBase64String([convert]::ToBase64String((get-content $imageFilePath -Encoding Byte)))
$Result = Invoke-RestMethod -Method Post -Uri $RecogniseUri -ContentType $ContentType -Body $Body 

#show the results
$Result | format-list #information saying I don't look awesome; just angry, contemptable, and sad... until you spot the Es

#open the image file so we can compare the results with the picture
invoke-item $imageFilePath

To make it easier to see how the results map to the image, I then appended this to the end of my script, generating an HTML page with an image map. Hovering your mouse over the image’s faces gives you the emotions.

$htmlResultPath = 'c:\SomeFolder\EmotiomApiMap.html'

$html = @"
<html>
<head><title>really simple html page for demoing project oxford's emotion api</title></head>
<body>
<img src="$imageFilePath" usemap="#emotionMap">
<map name="emotionMap">
{0}
</map>
</body>
</html>
"@

$html = $html -f ($Result | %{
        $top = $_.faceRectangle.top
        $left = $_.faceRectangle.left
        $bottom = ($_.faceRectangle.top + $_.faceRectangle.height)
        $right = ($_.faceRectangle.left + $_.faceRectangle.width)

        "<area shape='rect' coords='{0},{1},{2},{3}' title='{4}'>" -f $left, $top, $right, $bottom, ($_.scores | out-string) 
    } | Join-String)

$html | out-file $htmlResultPath -Force
invoke-item $htmlResultPath 
My emotional state

PowerShell Project Oxford Emotion API PoC

I plan to play with these APIs more over the next few months / hope to find time to knock up a PowerShell module for them which I could share on GibHub.

2016-02-03

PowerShell & SQL :: A Quick Fix for the SQLPS Problems

Filed under: Microsoft, powershell, SQL Server, Technology — Tags: , , , — Developer42 @ 18:19

There are a couple of issues with the SQLPS PowerShell module.

  1. It changes the current directory to PS SQLSERVER:\>.
  2. It may cause warnings (see end of this post for examples).

Calling the Import-Module-SQLPS function (the definition of which you’ll find below) instead of running Import-Module SQLPS will prevent such issues.

cls
function Import-Module-SQLPS {
    #pushd and popd to avoid import from changing the current directory (ref: http://stackoverflow.com/questions/12915299/sql-server-2012-sqlps-module-changing-current-location-automatically)
    #3>&1 puts warning stream to standard output stream (see https://connect.microsoft.com/PowerShell/feedback/details/297055/capture-warning-verbose-debug-and-host-output-via-alternate-streams)
    #out-null blocks that output, so we don't see the annoying warnings described here: https://www.codykonior.com/2015/05/30/whats-wrong-with-sqlps/
    push-location
    import-module sqlps 3>&1 | out-null
    pop-location
}

"Is SQLPS Loaded?"
if(get-module sqlps){"yes"}else{"no"}

Import-Module-SQLPS

"Is SQLPS Loaded Now?"
if(get-module sqlps){"yes"}else{"no"}

NB: Though from PowerShell 3.0 onwards you don’t need to import modules; for SQLPS I’d recommend that you do import it; this ensures that you can control when in your script this happens, rather than the first command from that library (e.g. Invoke-SqlCmd statement) causing the script to load and thus your directory being changed and warnings being spewed.

(more…)

2015-06-16

FTP Traffic Simulator / Test Script (PowerShell)

Filed under: Microsoft, Technology — Tags: , , , , , , , , , — Developer42 @ 01:38

We recently had an intermittent communication issue between an FTP server and the file share on which it stored its files.
In order to ensure frequent activity (with some variety to make it realistic) between the servers whilst investigating I created the below script.

The script connects to an FTP server, then loops (a given number of times) creating a dummy (Hunka.Junk) file of a random size, then uploading this file before pausing a random amount of time, then repeating for the next iteration.

cls
 
$fnTemplate = "hunka.junk.{0}.jpg"
$fnTempPath = "c:\temp\ftpTestScript\"
$user = "myFtpUser"
$pass = "myFtpPassword"
$ftp = "ftp://myFtpDns.myCompany.com/{0}"; 
$minFileSizeBytes =  20 * 1024 #min file size is 20kb
$maxFileSizeBytes = 100 * 1024 #max file size is 100kb
$minWaitSecs = 0 
$maxWaitSecs = 30
$stopConditionCounter = 1000000 #just in case we forget about the script have a stopping condition so it doesn't fill the server with junk
 
$webclient = New-Object -TypeName System.Net.WebClient;
$webclient.Credentials = New-Object System.Net.NetworkCredential($user,$pass) 
#$webclient.ContentType = "application/octet-stream";
#$webclient.Proxy = $null;
 
0..$stopConditionCounter | %{
    #setup variables for this iteration
    $filesize = Get-Random -Minimum $minFileSizeBytes -Maximum $maxFileSizeBytes #filesize in bytes
    $sleepTimeSecs = Get-Random -Minimum $minWaitSecs -Maximum ($maxWaitSecs-1)  #time to wait between uploads in seconds
    $sleepTimeSecs = Get-Random -Minimum $minWaitSecs -Maximum ($sleepTimeSecs+1) #increase liklihood of getting smaller wait times
    $fn = ($fnTemplate -f [string]([System.Guid]::NewGuid())) #get a unique filename
    write-output ("{0:0,000,000}: Generating {1:000} kb hunka.junk file ({2}), uploading to ftp, then sleeping {3} seconds" -f $_,($filesize/1024),$fn,$sleepTimeSecs)
    #create file
    $fnFullPath = (join-path $fnTempPath $fn)
    fsutil file createnew $fnFullPath $filesize
    #upload file
    $uri = New-Object -TypeName System.Uri -ArgumentList ($ftp -f $fn);
    $webclient.UploadFile($uri, $fnFullPath);
    #clean-up
    remove-item $fnFullPath
    #sleep
    Start-Sleep -seconds $sleepTimeSecs
}

2015-05-18

PowerShell Script :: Combine-Paths

Filed under: Microsoft, Technology — Tags: , , , , , , , — Developer42 @ 18:34

When you wish to create paths based on various combinations of strings, the following accepts a root plus an array of arrays and returns all combinations:

cls
function combine-paths([string]$root,[array]$paths)
{
    if(($paths -ne $null) -and ($paths.length -gt 0))
    {
        [array]$roots = $paths | select -first 1
        [array]$tail = @($paths | select -skip 1) #@() used to prevent final array from becoming an array of strings instead of a single-element array containing an array of strings
        $roots | %{combine-paths -root (join-path $root $_) -paths $tail}
    }
    else
    {
        $root
    }
}

$x = ('PROD','UAT','SIT'),('SystemA','SystemB','SystemC'),'Interfaces',('In','Out')
combine-paths -root '\\myFileShare\' -paths $x

NB: If you had multiple roots you could do the following:

'c:\','\\somewhere' | %{combine-paths -root $_ -paths $x}

Or amend the function to become a cmdlet taking root from the pipeline to simplify the above further / or add logic to allow the root to be blank/unspecified and to just pass `$_` instead of `(join-path $root $_)` where root is blank/null.
I’ve not done that here in order to keep the code short & simple.

2014-10-13

PowerShell Script :: Get HotFix Info from Local Machine & Web

Filed under: Microsoft, Technology — Tags: , , , , , , , , — Developer42 @ 22:02

In answering a question on Stack Overflow[1], I discovered that the Get-Hotfix cmdlet doesn’t list all hotfixes on your machine. Rather, if the fix is included in a CU, details of the installed CU will be presented, with no apparent way to drill down into the information and check for a specific fix.
So far as I can tell, there are no services / open databases which would allow you to programmatically retrieve this information. The only way currently is to manually head to the CU or fix’s related knowledge base site and read the contents.
As a very rough hack-around, I created a scraping script which will interrogate the MS knowledge-base for information on each hotfix, finding whether it’s a CU or not (based on the hotfix’s description in the page’s title) and if so try to find all related fixes. I only go down one level – to avoid the risk of an infinite loop & save time; should anyone want to go deeper, a caching mechanism could be used to store previous results and thus prevent the risk of a loop and also potentially improve performance.
As with most of the scripts on this site, this is more for me to play with PowerShell than to be of much practical use; but hopefully it could be to someone…

function get-hotfixInfo()
{
	process 
	{
		#$url = "http://support2.microsoft.com/kb/{0}" -f ($_.HotFixId -replace "KB(\d*)",'$1')
		$url = $_.Caption
		if($url -like "*.microsot.com")
		{
			$url = "{0}/{1}" -f $url,($_.HotFixId -replace "(KB)(\d*)",'$1/$2') 
		}
		try
		{
			$response = (Invoke-Webrequest $url -ea stop)
		} catch {
			$response = @{
				ParsedHTML = @{
					Title = "{0}`n`nURL: {1}" -f $error[0].Exception,$url 
				}
			}
		}
		$html = $response.ParsedHTML
		$isCU = $html.title -like "*cumulative*update*"
		$kblets = $null
		if($isCU) #this bit can be even slower than the above, hence only run if we believe we have a CU
		{
			$baseUri = $response.BaseResponse.ResponseURI
			$kblets = $html.getElementsByTagName('a') `
				| ? { ($_.parentNode.tagname -eq 'TD') -and ($_.parentNode.nextsibling.tagname = 'TD') } `
				| ? { $_.className -eq 'KBlink' } `
				| % { New-Object -TypeName PSObject -Prop @{
					Id = "KB{0}" -f $_.innerText
					Uri = (new-object System.URIBuilder($baseUri.scheme,$baseUri.dnssafehost,$baseUri.port,($_.href -replace "about:/(.*),'$1'"))).ToString()
					Title = $_.parentNode.nextsibling.innerText
				}}
		}
		New-Object -TypeName PSObject -Prop @{
			Id = $_.HotFixId
			IsCU = $isCU 
			Title = $html.title
			Source = $_.source
			Description = $_.description
			InstalledBy = $_.installedby
			InstalledOn = $_.installedon
			Uri = "http://support2.microsoft.com/kb/{0}" -f ($_.HotFixId -replace "KB(\d*)",'$1')
			KBlets = $kblets
		}
	}
}

#demos

#get the hotfix id for IE11 CU Feb 2014, and it's component hotfixes
get-hotfix -id kb2909921 | get-hotfixInfo | %{ $_; $_.KBlets | %{ $_; }} | ft -property id,title -autosize

#get the first 2 cumulative update hotfixes
get-hotfix | select -first 2 | get-hotfixInfo | ?{$_.isCU} 


2014-10-12

PowerShell Script: Demo of Calling a (StackExchange’s) Rest Web Service

Filed under: Microsoft, Technology — Tags: , , , , , , — Developer42 @ 01:25

Warning: this post is not useful unless you just like playing with scripts & APIs.

Reading a few posts on StackExchange’s Meta site, I saw that there was demand for, but no intent to, create some kind of synonym sub-type feature for tags.
e.g. Users looking for items tagged PowerShell would also see those tagged only PowerShell-1.0, PowerShell-2.0, PowerShell-ISE, etc.
Users looking for PowerShell-1.0 are being more specific though, so shouldn’t see those tagged simply PowerShell, and definitely not those tagged PowerShell-2.0.

Since SE have no intention of creating this, I figured it would be useful to have a site which could present a view over Stack Exchange sites, giving users these additional features.
Since I don’t have time to do that though, I satisfied my craving to build something by knocking up a quick script to play with the SE API.
…And since implementing synonyms would require a huge dictionary, which I’d have to create, I sufficed myself with simply playing with the API without the synonymn hierarchy feature.
So this is just me playing about with my new favourite scripting language and an API I hope one day to play more with (though then it would be with some other language).
Here’s what I came up with…

Code:

$tagList='c#','.net'
$tags = $taglist -join ';'
[Reflection.Assembly]::LoadWithPartialName("System.Web") | Out-Null
$tags = [System.Web.HttpUtility]::UrlEncode($tags)
$url = "http://api.stackexchange.com/2.2/search/advanced?page=1&amp;pagesize=10&amp;order=desc&amp;sort=creation&amp;ans
wers=0&amp;closed=False&amp;tagged={0}&amp;site=stackoverflow" -f $tags
$json = Invoke-RestMethod $url
$json.items | select title,link | %{ "{0}`n{1}`n" -f [System.Web.HttpUtility]::HtmlDecode($_.title), $_.link; }

Output:

ASP.NET Web Forms and MySql Entity Framework: "Nested transactions are not supported"
http://stackoverflow.com/questions/26320679/asp-net-web-forms-and-mysql-entity-framework-nested-transactions-are-not-su
ppo

.NET C# - Both Bitmap and Image Loads Images Smaller than Expected
http://stackoverflow.com/questions/26320637/net-c-both-bitmap-and-image-loads-images-smaller-than-expected

Mono TypeLoadException in an executable
http://stackoverflow.com/questions/26320146/mono-typeloadexception-in-an-executable

Developing a music player in C# without using com component
http://stackoverflow.com/questions/26319978/developing-a-music-player-in-c-without-using-com-component

Fastest way to get directory data
http://stackoverflow.com/questions/26319973/fastest-way-to-get-directory-data

c# datagridview multi-page print
http://stackoverflow.com/questions/26319852/c-datagridview-multi-page-print

How to access an object in c sharp from outside my method?
http://stackoverflow.com/questions/26319550/how-to-access-an-object-in-c-sharp-from-outside-my-method

.NET &amp; jQuery Jcrop Initialization Causing Double Page Load
http://stackoverflow.com/questions/26319428/net-jquery-jcrop-initialization-causing-double-page-load

Why i'm getting exception InvalidOperationException when using backgroundworker?
http://stackoverflow.com/questions/26319024/why-im-getting-exception-invalidoperationexception-when-using-backgroundwor
ker

Index was out of range error when I add a UserControl to a panel, Microsoft VS C#
http://stackoverflow.com/questions/26318864/index-was-out-of-range-error-when-i-add-a-usercontrol-to-a-panel-microsoft-
vs-c

So not that useful, but a fun play project.

2014-09-26

PowerShell Script :: Get AD Users by Email (Advanced)

Filed under: Microsoft, Technology — Tags: , , , , , , , — Developer42 @ 22:19

Here’s a script I knocked up today.

Auto.ps1 allows me to host the script on a server (or wherever), whilst others can use it by dropping input files into a queue folder, without needing to touch powershell (which may be scary to non-programmers, or may require additional setup or permissions).
ADGetUsersByEmailAdvanced.ps1 gets AD info based on email addresses; without requiring exchange modules, and includes workarounds to cope with missing information.

Auto.ps1

Monitors a folder for new text files. Once found, passes that file to a script to be processed. On completion moves the source file to the same directory & renames to begin with the same timestamp as the generated output file.

$infile = "\\myServer\myShare\Scripts\powershell\ADGetUsersByEmail\in\*.txt"

while (1 -eq 1) {
	#wait for a new file
	while(!(Test-Path $infile)) {Start-Sleep -s 30;}
	write-host "new input file found"
	Get-ChildItem $infile | %{
		$fileTimestamp = "{0:yyyy-MM-dd_HHmmss}" -f (get-date).ToUniversalTime()
		$inputFile = $_.fullname
		$exportFile = "{0}\out\{1}_output.csv" -f $PSScriptRoot,$fileTimestamp
		$inputFileMoved = "{0}\out\{1}_{2}" -f $PSScriptRoot,$fileTimestamp,$_.name
		write-host ("source: {0}" -f $inputFile)
		write-host ("output: {0}" -f $exportFile) 
		write-host ("archive: {0}" -f $inputFileMoved) 
		.\ADGetUsersByEmailAdvanced.ps1 -sourceFile $_.fullname -exportFile $exportFile 
		write-host "processed"
		Move-Item $inputFile $inputFileMoved
		write-host "archived"
	}
}

ADGetUsersByEmailAdvanced.ps1

Given a text file containing a list of email addresses, attempts to resolve those to corresponding AD users, taking advantage of email information in AD where available, then gracefully degrading to more hacky methods. Works its way through a list of domains in case the users are in the same company but on a different domain.

Param(
  [string]$sourceFile
  ,[string]$exportFile 
)
#$sourceFile = '.\sourceEmails.txt'
#$exportFile = ".\output_{0:yyyy-MM-dd_HHmmss}.csv" -f (get-date).ToUniversalTime()
$domains = 'eu','usa','myDomain','anotherDomain' # domain points to the GC; could equally list GC server names here, though this version's more user friendly

#create dummy; allows us to put in values for any unfound items (currently just using null, but we can easily amend if desired)
$adDummy = New-Object –TypeName PSObject –Prop @{
	emailSearched	= $null;
	notFound		= $true;
	sAmAccountName 	= $null;
	fullname		= $null;
	firstname		= $null;
	lastname		= $null;
	cn				= $null;
	countryCode		= $null;
	country			= $null;
	#title			= $null;
	title			= $null;
	department		= $null;
	company			= $null;
	email			= $null;	
	adEmail			= $null;
	proxyEmail		= $null;
}

function RemoveEmailDomain($email) {
  return $email -replace "(\S*)@\S*", '$1'
}
function IsFirstDotLast($name) {
	return $name -like '*.*'
}
function GetFirstName($name) {
	return $name -replace "(\S*?)\.\S*", '$1'
}
function GetLastName($name) {
	return $name -replace "\S*?\.(\S*)", '$1'
}
function GetFirstNamePartial($name) {
	return $name.substring(0,[system.math]::min(3,$name.length))
}
function GetLastNamePartial($name) {
	return $name.substring([system.math]::max($name.length-3,0),[system.math]::min(3,$name.length))
}
function GetADUserByIdentity($id, $domain) {
	#trycatch since erroraction not recognised on this type of command & I don't want error messages polluting my output
	write-host "searching for id '$id' on domain '$domain'"
	try { 
		Get-ADUser -Identity $id -Server $domain -Properties sAmAccountName, displayName, givenName, surname, distinguishedName, countryCode, c, title, department, company, emailAddress, proxyAddresses
	} catch {}
}
function GetADUserFiltered($filter, $domain) {
	Get-ADUser -Filter $filter -Server $domain -Properties sAmAccountName, displayName, givenName, surname, distinguishedName, countryCode, c, title, department, company, emailAddress, proxyAddresses
}
function GetADUserByEmailAddress($email, $domain) {
	write-host "searching for email '$email' on domain '$domain'"
	$filter = {emailAddress -eq $email} 
	GetADUserFiltered $filter $domain
}
function GetADUserByProxyAddress($email, $domain) {
	write-host "searching for proxy '$email' on domain '$domain'"
	$psBugFixSearchUser = "*:$_*"
	$filter = {proxyAddresses -like $psBugFixSearchUser}
	GetADUserFiltered $filter $domain
}
function GetADUserByFullName($name, $domain) {
	$fn = GetFirstName $name
	$ln = GetLastName $name
	write-host "searching for name '$fn', '$ln' on domain '$domain'"
	$filter = {(givenname -eq $fn) -and (surname -eq $ln)}
	GetADUserFiltered $filter $domain
}
function GetADUserByPartialName($name, $domain) {
	$fn = "{0}*" -f (GetFirstNamePartial $name)
	$ln = "*{0}" -f (GetLastNamePartial $name)
	write-host "searching for partial name '$fn', '$ln' on domain '$domain'"
	$filter = {(givenname -like $fn) -and (surname -like $ln)}
	GetADUserFiltered $filter $domain | where { ($_.givenname + $_.surname) -eq $name }
}
function FindBestMatch($email) {
	$result = $null
	#$domains | %{ $result=GetADUserByEmailAddress $email $_; if($result) {return $result;} } #doesn't play as expected
	foreach($domain in $domains) { $result=GetADUserByEmailAddress $email $domain; if($result) {return $result;} }
	foreach($domain in $domains) { $result=GetADUserByProxyAddress $email $domain; if($result) {return $result;} }
	$name = RemoveEmailDomain $email
	foreach($domain in $domains) { $result=GetADUserByIdentity $name $domain; if($result) {return $result;} }
	if(IsFirstDotLast($name)) {
		foreach($domain in $domains) { $result=GetADUserByFullName $name $domain; if($result) {return $result;} }
	} else {
		foreach($domain in $domains) { $result=GetADUserByPartialName $name $domain; if($result) {return $result;} }
	}	
	return $adDummy;
}

#define a function for later use
#get list of emails (ignore blanks)
$emails = (Get-Content $sourceFile) | where{ $_ -gt '' }

#get data from ad and stick it in an csv (or error to console if not found)
$emails | %{ 
	#get ad user by email address
	$adUser = FindBestMatch($_);
	if($adUser.notFound) {
		write-host ":(" -ForegroundColor Red
	} else {
		write-host ":)" -ForegroundColor Green
	}
	#return object replresenting results.
	New-Object –TypeName PSObject –Prop @{
		emailSearched	= $_;
		found			= if($adUser.notFound){$false} else {$true};
		sAmAccountName 	= $adUser.sAmAccountName;
		fullname		= $adUser.displayName;
		firstname		= $adUser.givenname;
		lastname		= $adUser.surname;
		cn				= $adUser.distinguishedName;
		countryCode		= $adUser.countryCode;
		country			= $adUser.c;
		#title			= $adUser.personalTitle;
		title			= $adUser.title;
		department		= $adUser.department;
		company			= $adUser.company;
		adEmail			= $adUser.emailAddress;
		proxyEmail		= [string]$adUser.proxyAddresses; #string joins the array down to a single string value
	}
} | export-csv $exportFile -notype #stick output to file

Script could be improved by allowing auto to kick off jobs so multiple instances of the worker script can be run simultaneously. Also changing the main script to make use of workflows and take advantage of the parallel foreach method should significantly improve it’s performance. However I’m still pretty new to PowerShell, so those steps will have to come later.

2014-09-10

Powershell Script: Monitor Connection Status

Filed under: Technology, Uncategorized — Tags: , , , , , , — Developer42 @ 20:59

This script monitors a connection, reporting when the connection drops and when it’s recovered.
The script monitors by pinging an IP, or by running an HTTP Get against a defined IP/URL.
The URL option’s provided in case run from behind a firewall blocking ICMP.

function Run-ConnectionTest($site, $waitSecs, $firewall)
{
	$state = 'unknown'
	$previousState = 'unknown'

	while($true)
	{
		#get current connection state
		if(Test-MyConnection $site $firewall)
		{
			$state = 'up'
		}
		else
		{
			$state = 'down'
		}
		#report change in state
		if ($state -ne $previousState) 
		{
			$previousState = $state
			$now = Get-Date
			if($state -eq 'up')
			{
				Write-Host -ForegroundColor Green ("{0:yyyy-MM-dd hh:mm:ss}: Connection up!" -f $now)
			}
			else
			{
				Write-Host -ForegroundColor Red ("{0:yyyy-MM-dd hh:mm:ss}: Connection down..." -f $now)
			}
		}
		#wait before checking again
		Start-Sleep -Seconds $waitSecs
	}
}
function Test-MyConnection($site, $firewall)
{
	$result = $false
	if($firewall) #if there's a firewall blocking pings, pinging won't work
	{
		try 
		{ 
			$response = (Invoke-WebRequest -Uri $site)
			#write-host('OK')
			#write-host($response.StatusCode)
			if(($response.StatusCode -ge 200) -and ($response.StatusCode -lt 400)) #treat everything from 200 to 399 as connection successful
			{
				$result = $true
			}
			else #everything outside of 200-399 is treated as a connection issue
			{
				$result = $false
			}
		} 
		catch #all exceptions are treated as connection issues, regardless of http response status code
		{
			#write-host('KO')
			#write-host($_.Exception)
			#$_.Exception.Response.StatusCode.Value__
			$result = $false
		}
	}
	else
	{
		$result = Test-Connection -computer $site -count 1 -quiet
	}
	return $result
}

Clear
#Run-ConnectionTest '8.8.8.8' 5 $false
Run-ConnectionTest 'http://www.google.com' 30 $true
Older Posts »

Blog at WordPress.com.