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-07-28

PowerShell :: Display AD Group Membership Hierarchy

Filed under: Microsoft, powershell, Uncategorized — Developer42 @ 14:26
#requires –Modules ActiveDirectory -version 3.0

clear-host

#NB: currently assumes that all users & groups are in the same domain
function Get-AdGroupHierarchy {
    [cmdletbinding()]
    param(
        [parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$AdGroupSamAccountName
        ,
        [parameter(Mandatory = $false)]
        [int]$Level = 0
        ,
        [parameter(Mandatory = $false)]
        [string]$DisplayField = 'SamAccountName' #allows us to use names instead of samAccountNames if desired; though faster if we just use the sAmAccountName as avoids a lookup per call
        ,
        [parameter(Mandatory = $false)]
        [switch]$IncludeManagedBy 
    )
    begin {
        [string[]]$DefaultDisplayProperties = @('Directory')
        if($IncludeManagedBy){
            $DefaultDisplayProperties += 'ManagedBy'
        }
    }
    process {
        [string]$managedBy = ''
        [string]$displayName = $AdGroupSamAccountName
        if(($DisplayField -ne 'SamAccountName') -or ($IncludeManagedBy)) { #save an ad lookup if not required
            $group = Get-AdGroup $AdGroupSamAccountName -Properties ManagedBy, $DisplayField 
            $displayName = $group | Select-Object -ExpandProperty $DisplayField
            if (($IncludeManagedBy) -and ($group.ManagedBy)) {
                $managedBy = $group.ManagedBy | Get-AdUser -Properties DisplayName | Select-Object -ExpandProperty DisplayName
            }
        }
        (new-object -TypeName PSObject -Property ([ordered]@{
            Level = $Level
            DisplayName = $DisplayName
        })) `
        | %{if($IncludeManagedBy){$_ | Add-Member -MemberType NoteProperty -Name ManagedBy -Value $managedBy -PassThru}else{$_}} `
        | Add-Member -MemberType ScriptProperty -Name DirectorySpacer -Value {("`t" * $this.Level) + ('|- ' * ($this.Level -gt 0))} -PassThru `
        | Add-Member -MemberType ScriptProperty -Name Directory -Value {$this.DirectorySpacer + $this.DisplayName} -PassThru `
        | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value ([System.Management.Automation.PSMemberInfo[]](New-Object System.Management.Automation.PSPropertySet DefaultDisplayPropertySet, $DefaultDisplayProperties)) -PassThru
        Get-AdGroupMember $AdGroupSamAccountName | ?{$_.objectClass -eq 'group'} | Select-Object -ExpandProperty SamAccountName | Sort-Object | Get-AdGroupHierarchy -Level ($level + 1) -DisplayField $DisplayField -IncludeManagedBy:$IncludeManagedBy
    }
}

Get-AdGroupHierarchy 'MyAdGroupSamAccountName' #see just the directory structure
Get-AdGroupHierarchy 'MyAdGroupSamAccountName' -IncludeManagedBy | ft -AutoSize #see directory structure and manager
Get-AdGroupHierarchy 'MyAdGroupSamAccountName' -IncludeManagedBy | select * #see all properties being returned

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…)

Create a free website or blog at WordPress.com.

%d bloggers like this: