Something About Scripting

  • Pretty as a Picture

    I Told You So

    In my last post I promised to try to do something more fun.  For me that would mean some wicked algorithm, but I realize I’m in the minority on that one.  I decided instead to pull out a script I did a while back to resize an image.  I actually leveraged some .Net code I found on a blog which I can’t remember, so I acknowledge that some parts of this script I owe in large part to an unnamed donor.  Many thanks to whoever you are.  It wasn’t exactly what I was looking for, so the final product is mine—after a bit of effort.

    Extraneous Background Information

    When I wrote this script, I was working on a larger project where I had to create a boatload of very detailed performance metrics graphs for 100+ servers and make them available from a SharePoint site.  Each graph would be displayed on its own page in full resolution, but it also needed to be displayed in lower resolution on a summary page.  The images had to be clear enough that a user could spot trends on individual servers and make a decision about whether or not to look at the individual graph for further investigation.  It quickly became clear that I needed to generate thumbnail versions of all of my images.  So that’s the challenge that motivated me to write this script.

    The Script

    Param( [System.IO.FileInfo[]] $InputFile                 ,

                                  $Suffix      = "_Resized"  ,

                                  $NewWidth    = 225         ,

                                  $NewHeight   = 150            )

    If (-not $InputFile) { $ScriptInputFile = $Input }

    Else {$ScriptInputFile = $InputFile}

     

    [Reflection.Assembly]::LoadWithPartialName("System.Drawing") | Out-Null

     

    $ScriptInputFile | %{

        $FileImage = ([System.Drawing.Image]::FromFile("$($_.FullName)"))

        $Bitmap = New-Object -TypeName System.Drawing.Bitmap `

            -ArgumentList $NewWidth,$NewHeight,$FileImage.PixelFormat

        $Graphic = [System.Drawing.Graphics]::FromImage($Bitmap)

        $Graphic.SmoothingMode = `

             [System.Drawing.Drawing2D.SmoothingMode]::HighQuality

        $Graphic.InterpolationMode = `

             [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic

        $Graphic.DrawImage($FileImage, 0, 0, $Bitmap.Width, $Bitmap.Height)

        $FileType = $_.FullName.Split(".")[-1]

        $Bitmap.Save($_.FullName.Replace(".$FileType","$Suffix.$FileType"), `

             [System.Drawing.Imaging.ImageFormat]::Jpeg)

        $Graphic.Dispose()

    }

     

    Parameters

    -InputFile <Image File or files>

    You can run the script specifying a single file or a collection of FileInfo objects.  If you specify a string, it will attempt to convert the string to a valid FileInfo object.

    -Suffix <Suffix string>

    This gets appended to your newly resized file name.  Default is “_Resized.”

    -NewWidth <New width>

    Specify the new width in pixels.  Default is 225.

    -NewHeight <New height>

    Specify the new width in pixels.  Default is 150.

     

    Seeing it in Action

    Given the following sample image (object in browser are larger than the appear—the original size of this image is 1024x683):

    Tulip

    The first line here will resize the file and return the FileInfo object.  The second command will actually display the shrunken file in the default application.

     

    [PS] > Set-Image.ps1 -InputFile "C:\Users\Public\Pictures\Sample Pictures\tulip.jpg" -Suffix ".Tiny" -NewWidth 25 -NewHeight 16

     

        Directory: C:\Users\Public\Pictures\Sample Pictures

     

    Mode                LastWriteTime     Length Name

    ----                -------------     ------ ----

    -a---          6/2/2009  12:35 AM        817 tulip.Tiny.jpg

     

    [PS] > & (Set-Image.ps1 -InputFile "C:\Users\Public\Pictures\Sample Pictures\tulip.jpg" -Suffix ".Tiny" -NewWidth 25 -NewHeight 16)

    tulip.Tiny

    Challenge!

    Note that it will match your NewWidth and NewHeight parameters and skew the image if necessary.  I can imagine it wouldn’t be that difficult to modify the script to prevent skewing or to specify a percentage to use, so the new image would be 75% or 200% of the original.  The System.Drawing.Image object has Height and Width properties that would give you the dimensions of the original image.  Hint, hint!

  • Stopping Services in a Specific Order

    A lot of the work that I do in product support involves setting up labs to try and reproduce a problem in order to troubleshoot it.  In that arena, one of the most frequent sub tasks is bulk starting and stopping of services.  A lot of times the services have to be started in a particular order sequentially—which means you can’t just let them fly.  It’s a simple thing really, but what I want to do here is to use the stopping and starting of services to illustrate a really important concept—refreshing the properties of a WMI object.

    It actually took me quite a while to figure this out—although after the fact I kicked myself… hard.  This used to be simple with VBScript.  You could just get any old WMI instance object and call the Refresh_ method, which came from the SWbemObjectEx interface.  Unfortunately this much needed method isn’t present in System.Management.ManagementObject.   Before I get to that, I’ll show you the complicated script I wrote to get this done the long way—this one is an earlier version which just stops services in a particular order, but you’ll see what I mean.

    The Ugly Way

    #### Stop Services sequentially

     

    #### List your servers individually

    $Servers = ("Localhost","127.0.0.1")

    #### ...or read them in from a file

    # $Servers = (type c:\serverlist.txt)

     

    #### You must specify a timeout (in seconds) or the script could potentially never end

    $TimeOut = 10

     

    #### Use this line to get all services matching a pattern.

    # $ServiceFilters = "name like 'msex%'"

     

    #### This will stop a single service on all servers in sequence

    # $ServiceFilters = "name = 'spooler'"

     

    #### This is just a more complex filter but the sequence is not guarateed

    # $ServiceFilters = "(name = 'spooler' OR name = 'msdtc')"

     

    #### Specifying individual filters in sequence will guarantee the order or services to stop

    $ServiceFilters = "(name = 'spooler')","(name = 'msdtc')"

     

    $Servers | %{

          $Server = $_

          $Locator = new-object -com "WbemScripting.SWbemLocator"

          $WMI = $Locator.ConnectServer($Server, "root\cimv2")

          $ServiceFilters | %{

                $ThisFilter = $_

                (Get-WmiObject -Class Win32_Service -ComputerName $Server -filter "$ThisFilter AND state='running'") | %{

                      $Service = $_

                      $Refresher = new-object -comobject "WbemScripting.SWbemRefresher"

                      $FreshObject = $Refresher.Add($WMI,$Service.__RELPATH)

                      $Refresher.Refresh()

                      $Then = Get-Date

                      :Checking Do {

                            $Service.StopService() | out-null

                            $Refresher.Refresh()

                            if (($FreshObject.Object.properties_ | ?{$_.name -eq "state"}).value -eq "Stopped") {

                                  "Stopped $Service on $Server";

                                  break :Checking;

                            } Else {

                                  If (((Get-Date) - $Then).seconds -ge $TimeOut){

                                        "!!! TIMEOUT - $Service on $Server";

                                        break :Checking;

                                  }

                            }

                      } While ($True)

                }

          }

    }

    "Services are all stopped"

    I was never really a fan of the SWbemRefresher object.  It always seemed a bit counter-intuitive and require me to jump through too many hoops, like using the SWbemLocator object.  Ugh.

    As it turns out there is a method I can use from System.Management.ManagementObject which refreshes the instance data.  I was looking for something named Refresh or similar, but the actual method is called Get().  Sure it makes sense now—I can hear the condescension across space/time.  And this allows me to make the script a lot friendlier.

    The Much Nicer Version

    Aside from removed the dreadful SWbemLocator, I also cleaned the script up a bit and made it more flexible.

    Arguments
    -Operation <start|stop>

    This makes the script work for either starting or stopping services.  If you don’t provide an operation, the script will just return the current state of all services that match the filter.

    -ServiceFilters <Filter String(s)>

    This is where you specify which services and in what order to process.  We’re taking one or more filter strings here to make the script more flexible.  Some potential ServiceFilters values are:

    • "Name = 'spooler'"
    • "Name like 'msex%'"
    • "StartMode = 'Auto'"
    • "Name = 'spooler'","Name = 'msdtc'"

    Note that in order to guarantee the order of processing, you should specify each filter string separately, as it the last example.

    -Servers <ServerList>

    Specify one or more servers by any name that will resolve—Netbios, DNS, IP.  If you don’t specify a server, then the script will assume the local machine.

    -TimeOutSeconds <NumberOfSeconds>

    Specify how many seconds to wait before timing out processing the service.  Default is 10 seconds.  Once the timeout expires, it will just move on to the next service.

    #### Stop Services sequentially

     

    Param ( $Operation                    ,

            $ServiceFilters = "(name='')" ,

            $Servers        = "Localhost" ,

            $TimeOutSeconds = 10             )

     

    Switch ($Operation) {

        {$_ -like "start*"} {

            $Method       = "StartService";

            $DesiredState = "Running";

        }

        {$_ -like "stop*"}  {

            $Method       = "StopService";

            $DesiredState = "Stopped";

        }

        Default             {

            $Method       = $Null;

            $DesiredState = "*";

        }

    }

     

    $Servers | %{

          $Server = $_

          $ServiceFilters | %{

                $ThisFilter = $_

                (Get-WmiObject -Class Win32_Service `

                               -ComputerName $Server `

                               -filter "$ThisFilter AND state!='$DesiredState'") | %{

                      $Service = $_

                      $Then = Get-Date

                      :Checking Do {

                            If ($Method) {

                                  $ErrorCode = $Service.PSBase.InvokeMethod($Method,$Null)

                            }

                            $Service.Get()

                            if ($Service.State -like "$DesiredState") {

                                  "$($Service.State) on $Server - $($Service.caption) ($($Service.name))" | out-host;

                                  break :Checking;

                            } Else {

                                  If (((Get-Date) - $Then).seconds -ge $TimeOutSeconds){

                                        "!!! TIMEOUT - $Service on $Server" | out-host;

                                        break :Checking;

                                  }

                            }

                      } While ($True)

                }

          }

    }

    Here’s the code in action.  Notice what happens if I set the timeout too short.

    [PS] .\Set-BulkService.ps1 -operation start -servicefilters "(name = 'spooler')","(name = 'msdtc')" -servers 127.0.0.1 -timeout 2

    Running on 127.0.0.1 - Print Spooler (Spooler)

    Running on 127.0.0.1 - Distributed Transaction Coordinator (MSDTC)

    [PS] .\Set-BulkService.ps1 -operation start -servicefilters "(name = 'spooler')","(name = 'msdtc')" -servers 127.0.0.1 -timeout 0

    !!! TIMEOUT - \\STEPHAP1\root\cimv2:Win32_Service.Name="Spooler" on 127.0.0.1

    !!! TIMEOUT - \\STEPHAP1\root\cimv2:Win32_Service.Name="MSDTC" on 127.0.0.1

    [PS]

    So that’s that.  My greatest hope for this post is that someone is searching for the Refresh_ method and PowerShell, and they happen upon this code and save themselves some time.

    Unfortunately…

    I wrote the previous scripts on Windows 7 using PowerShell v2 where it worked like a champ, but when I tried the latter script on my Windows 2003 Exchange 2007 server, it wasn’t so happy.  I’m not sure whether it’s Windows or PowerShell, but the Get() method was not available there.  I had to go back and use reflection to get at it as well as make a couple of other changes that for whatever reason were necessary to make it work.  Here is the safer version of the script that worked in both locations:

    #### Stop Services sequentially

     

    Param ( $Operation                    ,

            $ServiceFilters = "(name='')" ,

            $Servers        = "Localhost" ,

            $TimeOutSeconds = 10             )

     

    Switch ($Operation) {

        {$_ -like "start*"} {

            $Method       = "StartService";

            $DesiredState = "Running";

        }

        {$_ -like "stop*"}  {

            $Method       = "StopService";

            $DesiredState = "Stopped";

        }

        Default             {

            $Method       = $Null;

            $DesiredState = "*";

        }

    }

    $Servers | %{

          $Server = $_

          $ServiceFilters | %{

                $ThisFilter = $_

                 $Services = `
                (Get-WmiObject -Class Win32_Service `
                               -ComputerName $Server `
                               -filter "$ThisFilter AND state!='$DesiredState'")
                $Services | %{

                      $Service = $_

                      $Then = Get-Date

                      :Checking Do {

                            If (-not $Service) { Break :Checking; }

                            If ($Method) {

                                  $ErrorCode = $Service.PSBase.InvokeMethod($Method,$Null)

                            }

                            $ObjectType = $Service.GetType()
                            $GetProperty = [reflection.bindingflags]::InvokeMethod
                            $ObjectType.InvokeMember("Get", $GetProperty,$Null,$Service,$Null)
                            if ($Service.State -like "$DesiredState") {

                                  "$($Service.State) on $Server - $($Service.caption) ($($Service.name))" | out-host;

                                  break :Checking;

                            } Else {

                                  If (((Get-Date) - $Then).seconds -ge $TimeOutSeconds){

                                        "!!! TIMEOUT - $Service on $Server" | out-host;

                                        break :Checking;

                                  }

                            }

                      } While ($True)

                }

          }

    }

     

     

    That was quite a journey you took with me.  Thanks for sticking around all the way to the end.  You must be really into scripting.  I hope you weren’t too disappointed.  Next time I’ll do something fun—I promise.

  • Identifying Defunct Organizations (Empty OUs)

    I have quite a nice little script for you today.  A customer asked me yesterday how he might identify hosted business organizations which contain no subscribed users so he can remove the empty organizations from HMC.  If you’re not familiar with HMC, that involves locating OUs that don’t contain any users other than a couple of default admin accounts.  Once I finished licking my chops I dove right in and whipped this one up. 

    The reason I was so stoked about writing this is that it involves one of my favorite things.  Forget raindrops on roses—I love writing a recursive script.  For those not educated in the ways of the geek, a recursive script calls itself.  It’s like a video of a person in standing in front of a TV playing the live video of that person—or parallel mirrors where you see 50 reflections of yourself.  (Oh, I’m getting excited just explaining the concept!)  Generally you use recursion when working with a hierarchy of some sort.  So you inspect a root object and get a list of it’s children, then you would call the same procedure (script, function, whatever) again, this time a child node becomes the root.

    A Word of Warning

    As you might guess, recursion can be somewhat dangerous.  They can lead to stack overflows or memory exhaustion.  For this reason, PowerShell limits the call depth to 1000.  So once your script is 1001 levels deep into itself you’ll get an error:

    The script failed due to call depth overflow.  The call depth reached 1001 and the maximum is 1000.
    At line:0 char:0

    Still you should be careful because if your script consumes enough memory, you just might run out of resources well before you get 1001 levels deep. 

    Arguments

    -RootNode <LDAP Path of Root Node>

    Specifies the container object where you want to begin your search.  If you don’t specify a RootNode, the script returns False, and that’s that.

    -OutputFile <Path to file>

    If you specify an output file, the DNs of the effectively empty OUs will be written to this file.  Should probably be a txt file, but it doesn't really matter.

    -Verbose

    This switch if specified will have the script echo to the console every OU that it’s currently checking so you can monitor the progress.

    -Quiet

    Using this switch means that when the script finds an empty OU, it will still write the DN to the file (if specified) but will not echo it to the console.

    * You may use –Verbose and –Quiet together if you feel you must.

    ** If you’re only interested in a single OU, but you want to know whether it contains any nested users, you can specify only the -RootNode and use the –Quiet switch with no –OutputFile.  This will return only a True or False value indicating whether that single OU is effectively empty.

    The Script

    Param ( [ADSI]               $RootNode   ,
            [system.io.fileinfo] $OutputFile ,
            [switch]             $Verbose    ,
            [switch]             $Quiet         )

     

    $UsersExist = $False;

    If ($Verbose) { write-host "CHECKING:  $($RootNode.distinguishedName)" }

    $UserObjects = (
        $RootNode.PSBase.Children | ?{
            $_.objectCategory -like "CN=Person,*" `
            -and $_.distinguishedName -notlike "CN=Admin@*" `
            -and $_.distinguishedName -notlike "CN=CSRAdmin@*";
        }
    )

    If ($UserObjects) {
        $UsersExist = $True;
    }

    $ChildNodes = ($RootNode.PSBase.Children | ?{

        $_.objectCategory -like "CN=Organizational-Unit,*"

    });
    $ChildNodes | %{
        $ChildNode = $_;
       If ($ChildNode){
            & $MyInvocation.InvocationName -RootNode $ChildNode `

                                           -out $OutputFile     `

                                           -verbose:$Verbose    `

                                           -Quiet:$Quiet         | %{
               If ($_) {
                    $UsersExist = $True;
                } Else {
                    If (-not $Quiet) {

                        write-host "*** $($ChildNode.distinguishedName) HAS NO USERS";

                    }
                    If ($OutputFile) {
                        $ChildNode.distinguishedName | `

                            out-file $Outputfile -append -encoding ascii;
                    }
                }
            }
        }
    }

    $UsersExist;


    Other Uses

    For my non-hosting folks out there, this script has a host of other uses.  For instance you want to Find all the OUs that have computer objects in them.  You can change this script up a bit to accomodate that.  You’ll want to change the filter a bit and reverse the logic, but you can do it.  Have fun!

  • Choking on (Very Large) XML Files

    You probably don't know much about what I actually do at Microsoft (yet), so now would be a good time to mention that I typically support e-mail hosters who use Microsoft Hosted Messaging and Collaboration (HMC).  These type of customer tend to have a lot (and I mean a lot) of Exchange configuration objects. They tend to have several thousand each of address lists, GALs, OABs and accepted domains for instance.

    A typical XML file created by ExchDump (http://www.microsoft.com/downloads/details.aspx?familyid=d88b807d-964e-4bf8-9344-754892e9f637&displaylang=en) for one of these hosters might be 500MB+, while the HTML file will be almost as large and completely indigestible--if somehow you have enough system resources to open it in Internet Explorer.

    I set about to break the XML into sections that I could actually use to gain some useful information.  Naturally I turned to PowerShell.  The first thing I tried was to load the file by foolishly casting the contents of the file to System.XML.XMLDocument.  After waiting several minutes I realized what a dumb move that was.  Thank you CTRL+C.   From that point the task became all about text parsing.

    Param ( $FilePath = "C:\Data" )

    $SubPath = "$FilePath\ExchDumpObjects"

    If(-not (Test-Path "$SubPath")){

       mkdir $SubPath

    }

    $ObjectString = @()

    type "$FilePath\ExchDump_*.xml" |
       %
          if($_.trim() -eq "<ADSI-Object>") {
    #### "Start new Object!!!"
             $ObjectString = @()
             $ObjectClass = ""
             $ObjectName = ""
             $ObjectString += $_
          }
          elseif($_.trim() -eq "</ADSI-Object>"){
    #### "End object"
             $ObjectString += $_
             $TickCount = (get-date).ticks
             If(-not (Test-Path ("$SubPath\" + $ObjectClass))){

                mkdir ("$SubPath\" + $ObjectClass)

             }
             $ObjectString | out-file `

               "$SubPath\$ObjectClass\$($ObjectClass)_$ObjectName.$TickCount.xml"
          } else {
    #### "Continue object"
             $ObjectString += $_
             If($_ -match "ADSI_Obj_Class"){
                $ObjectClass = $_.Trim().Replace("<ADSI_Obj_Class>","")

                $ObjectClass = $ObjectClass.Replace("</ADSI_Obj_Class>","")

                $ObjectClass = $ObjectClass.Trim()
             }
             ElseIf($_ -match "ADSI_Obj_Name"){
                $ObjectName = $_.Trim().Replace("<ADSI_Obj_Name>","")

                $ObjectName = $ObjectName.Replace("</ADSI_Obj_Name>","")

                $ObjectName = $ObjectName.Replace("CN=","").Trim()
             }
          }
       }

     

    So to run the script, you can do one of two things—either place your xml file in the folder C:\Data which is the default directory where the script will look for it, or pass in the file folder as an argument.

    When you’re done, you’ll have a subfolder in the same folder with the ExchDump output named ExchDumpObjects.  Inside that folder will be subfolders for each type of AD object that ExchDump gathered.  Inside those folders, there will be a single XML file for each object collected by ExchDump.  For instance if I wanted to find a particular OAB Object named “Demo OAL”, I would look for a file like “C:\Data\ExchDumpObjects\msExchOAB\msExchOAB_Demo OAL.*.xml”.  You might find that ExchDump outputs the object multiple times.  I’m not sure why that is, but the objects tend to be for the most part identical when that happens.

     

    Once you’ve found the object you’re looking for, you may want to use PowerShell to parse it as well.  This is sort of tricky because the XML which ExchDump uses isn’t the absolute best of formats.  Here’s a snippet you can use for getting attributes out of those files:

     

    [PS] $FP = "C:\Data\ExchDumpObjects\msExchOAB\msExchOAB_Demo OAL.*.xml"

    [PS] $OAB = [xml] (type $FP)

    [PS] $Atr = $oab."ADSI-Object".attribute | ?{$_.innertext -like "*offlineABContainers*"}

    [PS] $Atr.attrib_val

     "CN=Demo AL,CN=All Address Lists,CN=Address Lists Container,CN=hmc,CN=Microsoft Exchange,CN=Services,CN=Configuration,DC=fabrikam,DC=net"

    [PS] $Atr = $oab."ADSI-Object".attribute | ?{$_.innertext -like "*offlineABServer*"}

    [PS] $Atr.attrib_val

     "CN=OAB01,CN=Servers,CN=Exchange Administrative Group (FYDIBOHF23SPDLT),CN=Administrative Groups,CN=hmc,CN=Microsoft Exchange,CN=Services,CN=Configuration,DC=fabrikam,DC=net "

     

    (As a side note, yes, a lot of my scripts will be just as esoteric as this one.  It’s super fantastic if you find yourself in the same exact predicament as me, but that’s not very likely unless you are supporting a very large e-mail hoster.  The same sort of technique may apply to other scenarios where you have very large XML files, but for the most part this is a niche script.)

     

  • Is this thing on?

    I was under the impression that blogging was for people who kept diaries as children and always send out holiday cards and Thank You notes.  (Unfortunately despite my best intention, I'm not a member of that well bred club.)  Recently though I've had several ideas kicking around that I want to share with the IT community, so I bit the bullet and started the blog.

    My objective for the tentatively named "Something About Scripting" is to share some of my experiences and scripts with whomever might be interested in them.  I've spent almost my whole career in Product Support, so my scripts are generally of a certain flavor--building labs for reproducing proplems, diagnosing problems, collecting wide swaths of information to understand problems, etc.  You get the picture.  Every now and again I might have something a little more fun and a little less practical in the IT space.

    Hope this helps someone.

     


© 2009 Microsoft Corporation. All rights reserved. Terms of Use  |  Trademarks  |  Privacy Statement
Microsoft
Page view tracker