The PowerShell Morning Report

ZazuI love how easy it is to manage computers with Windows PowerShell. It is a great reporting tool, but often I find people getting locked into one approach. I’m a big believer in flexibility and re-use and using objects in the pipeline wherever I can. So I put together a PowerShell script that I can run every morning on my computer and get a quick summary about what is happening, or perhaps not happening. My script, MorningReport.ps1, relies on WMI to gather a variety of system information. By default it connects to the local host, but I’ve provided a computername parameter. I’m assuming current credentials are good enough for any remote system, but you can always add support for alternate credentials, assuming I don’t in some future version.

By default the script writes a custom object to the pipeline that contains all of the other WMI information like disk utilization, service status, and event logs. But I wanted flexibility and ease of use, so the script also supports parameters of -Text and -HTML. The former creates nicely formatted text suitable for printing or saving to a file. The latter creates HTML code from the original objects. I rely heavily on creating HTML fragments with ConvertTo-HTML and then assembling everything at the end. The script writes the HTML code to the pipeline so if you want to save results to a file, simply pipe to Out-File. I did this because there may be times when you want the “raw” HTML code. You might want to save the HTML and create an HTML mail message with Send-MailMessage. Or maybe further tweak the HTML before saving it to a file. Again, I didn’t want to lock myself in. Here’s the main part of the script.

[cmdletbinding(DefaultParameterSetName="object")]

Param(
[Parameter(Position=0,ValueFromPipeline=$True)]
[ValidateNotNullOrEmpty()]
[string]$Computername=$env:computername,
[ValidateNotNullOrEmpty()]
[alias("title")]
[string]$ReportTitle="System Report",
[ValidateScript({$_ -ge 1})]
[int]$Hours=24,
[Parameter(ParameterSetName="HTML")]
[switch]$HTML,
[Parameter(ParameterSetName="TEXT")]
[switch]$Text
)

#script internal version number used in output
[string]$reportVersion="1.0.8"

<#
define some HTML style
here's a source for HTML color codes
http://www.immigration-usa.com/html_colors.html

the code must be left justified  
#>
   
$head = @"
<style>
body { background-color:#CCFFFF;
       font-family:Tahoma;
       font-size:12pt; }
td, th { border:1px solid #000033;
         border-collapse:collapse; }
th { color:white;
     background-color:#000033; }
table, tr, td, th { padding: 2px; margin: 0px }
table { margin-left:50px; }
</style>
<Title>$ReportTitle</Title>
"
@

If ($computername -eq $env:computername) {
 #local computer so no ping test is necessary
 $OK=$True
}
elseIf (($computername -ne $env:computername) -AND (Test-Connection -ComputerName $computername -quiet -Count 2)) {
 #not local computer and it can be pinged so proceed
 $OK=$True
}

If ($OK) {

    Try {
        $os=Get-WmiObject Win32_operatingSystem -ComputerName $computername -ErrorAction Stop
        #set a variable to indicate WMI can be reached
        $wmi=$True
    }
    Catch {
        Write-Warning "WMI failed to connect to $($computername.ToUpper())"
    }
   
    if ($wmi) {
        Write-Host "Preparing morning report for $($os.CSname)" -ForegroundColor Cyan

        #OS Summary
        Write-Host "...Operating System" -ForegroundColor Cyan
        $osdata=$os | Select @{Name="Computername";Expression={$_.CSName}},
        @{Name="OS";Expression={$_.Caption}},
        @{Name="ServicePack";Expression={$_.CSDVersion}},
        free*memory,totalv*,NumberOfProcesses,
        @{Name="LastBoot";Expression={$_.ConvertToDateTime($_.LastBootupTime)}},
        @{Name="Uptime";Expression={(Get-Date) - ($_.ConvertToDateTime($_.LastBootupTime))}}
       
        #Computer system
        Write-Host "...Computer System" -ForegroundColor Cyan
        $cs=Get-WmiObject -Class Win32_Computersystem -ComputerName $computername
        $csdata=$cs | Select Status,Manufacturer,Model,SystemType,Number*
       
        #services
        Write-Host "...Services" -ForegroundColor Cyan
        #get services via WMI and group into a hash table
        $wmiservices=Get-WmiObject -class Win32_Service -ComputerName $computername    
        $services=$wmiservices | Group State -AsHashTable
       
        #get services set to auto start that are not running
        $failedAutoStart=$wmiservices | Where { ($_.startmode -eq "Auto") -AND ($_.state -ne "Running")}
       
        #Disk Utilization
        Write-Host "...Logical Disks" -ForegroundColor Cyan
        $disks=Get-WmiObject -Class Win32_logicaldisk -Filter "Drivetype=3" -ComputerName $computername
        $diskData=$disks | Select DeviceID,
        @{Name="SizeGB";Expression={$_.size/1GB -as [int]}},
        @{Name="FreeGB";Expression={"{0:N2}" -f ($_.Freespace/1GB)}},
        @{Name="PercentFree";Expression={"{0:P2}"  -f ($_.Freespace/$_.Size)}}
       
        #NetworkAdapters
        Write-Host "...Network Adapters" -ForegroundColor Cyan
        #get NICS that have a MAC address only
        $nics=Get-WmiObject -Class Win32_NetworkAdapter -filter "MACAddress Like '%'" -ComputerName $Computername
        $nicdata=$nics | Foreach {
         $tempHash=@{Name=$_.Name;DeviceID=$_.DeviceID;AdapterType=$_.AdapterType;MACAddress=$_.MACAddress}
         #get related configuation information
         $config=$_.GetRelated() | where {$_.__CLASS -eq "Win32_NetworkadapterConfiguration"}
         #add to temporary hash
         $tempHash.Add("IPAddress",$config.IPAddress)
         $tempHash.Add("IPSubnet",$config.IPSubnet)
         $tempHash.Add("DefaultGateway",$config.DefaultIPGateway)
         $tempHash.Add("DHCP",$config.DHCPEnabled)
         #convert lease information if found
         if ($config.DHCPEnabled -AND $config.DHCPLeaseObtained) {
            $tempHash.Add("DHCPLeaseExpires",($config.ConvertToDateTime($config.DHCPLeaseExpires)))
            $tempHash.Add("DHCPLeaseObtained",($config.ConvertToDateTime($config.DHCPLeaseObtained)))
            $tempHash.Add("DHCPServer",$config.DHCPServer)
         }
         
         New-Object -TypeName PSObject -Property $tempHash
         
         }

        #Event log errors and warnings in the last $Hours hours
        $last=(Get-Date).AddHours(-$Hours)
        #System Log
        Write-Host "...System Event Log Error/Warning since $last" -ForegroundColor Cyan
        $syslog=Get-EventLog -LogName System -ComputerName $computername -EntryType Error,Warning -After $last
        $syslogdata=$syslog | Select TimeGenerated,EventID,Source,Message
       
        #Application Log
        Write-Host "...Application Event Log Error/Warning since $last" -ForegroundColor Cyan
        $applog=Get-EventLog -LogName Application -ComputerName $computername -EntryType Error,Warning -After $last
        $applogdata=$applog | Select TimeGenerated,EventID,Source,Message
       
    } #if wmi is ok
   
    #write results depending on parameter set
    $footer="Report v{3} run {0} by {1}\{2}" -f (Get-Date),$env:USERDOMAIN,$env:USERNAME,$reportVersion
   
    if ($HTML) {
        #prepare HTML code
        $fragments=@()
        #insert a graphic header with one of the two following lines
        #$fragments+="<center><img src='http://jdhitsolutions.com/blog/wp-content/uploads/2011/03/jdhit-blog-header-2.png' /></Center>"
        $fragments+="<Img src='http://www.lionking.org/imgarchive/Clip_Art/zazu03.gif' Alt='Zazu' style='float:left' width='120' height='100' hspace=10><H1> The Morning Report</H1>"
       
        #insert navigation bookmarks
$nav=@"
<a href='#Services'>Services</a>
<a href='#NoAutoStart'>Failed Auto Start</a>
<a href='#Disks'>Disks</a>
<a href='#Network'>Network</a>
<a href='#SysLog'>System Log</a>
<a href='#AppLog'>Application Log</a>
"
@
        $fragments+=$nav
        $fragments+="<br clear='All'>"
       
        #add a link to the document top
        $nav+="`n<a href='#' target='_top'>Top</a>"
       
        $fragments+="<H2>System Summary</H2>"
        $fragments+=$osdata | ConvertTo-Html -as List -Fragment
        $fragments+=$csdata | ConvertTo-Html -as List -Fragment
        $fragments+=ConvertTo-Html -Fragment -PreContent "<H2><a name='Services'>Services</a></H2>"
        $services.keys | foreach {
         $fragments+= ConvertTo-Html -Fragment -PreContent "<H3>$_</H3>"
         $fragments+=$services.$_ | Select Name,Displayname,StartMode| ConvertTo-HTML -Fragment
         #insert navigation link after each section
         $fragments+=$nav
        }
               
        $fragments+=$failedAutoStart | Select Name,Displayname,StartMode,State |
         ConvertTo-Html -Fragment -PreContent "<h3><a name='NoAutoStart'>Failed Auto Start</a></h3>"
         $fragments+=$nav
       
        $fragments+=$diskdata | ConvertTo-HTML -Fragment -PreContent "<H2><a name='Disks'>Disk Utilization</a></H2>"
        $fragments+=$nav
       
        #convert nested object array properties to strings
        $fragments+=$nicdata | Select Name,DeviceID,DHCP*,AdapterType,MACAddress,
         @{Name="IPAddress";Expression={$_.IPAddress | Out-String}},
         @{Name="IPSubnet";Expression={$_.IPSubnet | Out-String}},
         @{Name="IPGateway";Expression={$_.DefaultGateway | Out-String}}  |
          ConvertTo-HTML -Fragment -PreContent "<H2><a name='Network'>Network Adapters</a></H2>"
        $fragments+=$nav
       
        $fragments+=$syslogData | ConvertTo-HTML -Fragment -PreContent "<H2><a name='SysLog'>System Event Log Summary</a></H2>"
        $fragments+=$nav
       
        $fragments+=$applogData | ConvertTo-HTML -Fragment -PreContent "<H2><a name='AppLog'>Application Event Log Summary</a></H2>"
        $fragments+=$nav
               
        Write $fragments | clip
        ConvertTo-Html -Head $head -Title $ReportTitle -PreContent ($fragments | out-String) -PostContent "<br><I>$footer</I>"
    }
    elseif ($TEXT) {
        #prepare formatted text
        $ReportTitle
        "-"*($ReportTitle.Length)
        "System Summary"
        $osdata | Out-String
        $csdata | format-List | Out-String
        Write "Services"
        $services.keys | foreach {
         $services.$_ | Select Name,Displayname,StartMode,State
        } | Format-List | Out-String
        Write "Failed Autostart Services"
        $failedAutoStart | Select Name,Displayname,StartMode,State
        Write "Disk Utilization"
        $diskdata | Format-table -AutoSize | Out-String
        Write "Network Adapters"
        $nicdata | Format-List | Out-String
        Write "System Event Log Summary"
        $syslogdata | Format-List | Out-String
        Write "Application Event Log Summary"
        $applogdata | Format-List | Out-String
        Write $Footer
    }
    else {
        #Write data to the pipeline as part of a custom object
               
        New-Object -TypeName PSObject -Property @{
         OperatingSystem=$osdata
         ComputerSystem=$csdata
         Services=$services.keys | foreach {$services.$_ | Select Name,Displayname,StartMode,State}
         FailedAutoStart=$failedAutoStart | Select Name,Displayname,StartMode,State
         Disks=$diskData
         Network=$nicData
         SystemLog=$syslogdata
         ApplicationLog=$applogdata
         ReportVersion=$reportVersion
         RunDate=Get-Date
         RunBy="$env:USERDOMAIN\$env:USERNAME"
        }
    }
   
} #if OK

else {
    #can't ping computer so fail
    Write-Warning "Failed to ping $computername"
}

As you can see, it is a lengthy script, but I’ve tried to include a fair amount of internal comments and documentation, so I won’t repeat it here, but I will touch on a few key points.

First, I’m embedding a style sheet directly in any HTML output so any files I create can stand alone. You could certainly modify the relevant sections and use the -CSSUri parameter with ConvertTo-HTML. Next, I use a combination of Test-Connection and Try/Catch to handle computers that are offline or I can’t access. This speeds up the script and makes it nicer for the script user. Assuming all is good, I create a number of variables that hold the WMI information I am interested in.

...
#OS Summary
        Write-Host "...Operating System" -ForegroundColor Cyan
        $osdata=$os | Select @{Name="Computername";Expression={$_.CSName}},
        @{Name="OS";Expression={$_.Caption}},
        @{Name="ServicePack";Expression={$_.CSDVersion}},
        free*memory,totalv*,NumberOfProcesses,
        @{Name="LastBoot";Expression={$_.ConvertToDateTime($_.LastBootupTime)}},
        @{Name="Uptime";Expression={(Get-Date) - ($_.ConvertToDateTime($_.LastBootupTime))}}
       
        #Computer system
        Write-Host "...Computer System" -ForegroundColor Cyan
        $cs=Get-WmiObject -Class Win32_Computersystem -ComputerName $computername
        $csdata=$cs | Select Status,Manufacturer,Model,SystemType,Number*
...

After everything is collected, then I can write output using an If/ElseIf statement depending on what parameters were passed. If no parameters were specified then a custom object is assembled and written to the pipeline.

 else {
        #Write data to the pipeline as part of a custom object
               
        New-Object -TypeName PSObject -Property @{
         OperatingSystem=$osdata
         ComputerSystem=$csdata
         Services=$services.keys | foreach {$services.$_ | Select Name,Displayname,StartMode,State}
         FailedAutoStart=$failedAutoStart | Select Name,Displayname,StartMode,State
         Disks=$diskData
         Network=$nicData
         SystemLog=$syslogdata
         ApplicationLog=$applogdata
         ReportVersion=$reportVersion
         RunDate=Get-Date
         RunBy="$env:USERDOMAIN\$env:USERNAME"
        }

But perhaps you’d like to see this in action. Here’s a sample HTML SampleReport. In this report, there are no recent errors or warnings in the Application event log.

Or if you have a few minutes, here’s a short clip of the script in action.

I hope you find this a useful jumping off point for your own script, although I think I’ve already worked out information you are most likely interested in. Download MorningReport.ps1 and try it out for yourself.

Post to Twitter Post to Plurk Post to Yahoo Buzz Post to Delicious Post to Digg Post to Facebook Post to FriendFeed Post to Google Buzz Post to Ping.fm Post to Reddit Post to Slashdot Post to StumbleUpon Post to Technorati

Posted in PowerShell, Scripting, Windows 7, Windows Server, WMI | Tagged , , , , , | 3 Comments

Friday Fun What’s My Variable

I use scriptblocks quite a bit in my PowerShell work, often saved as variables. These are handy for commands you want to run again, but don’t necessarily need to turn into permanent functions.

$freec={(get-wmiobject win32_logicaldisk -filter "deviceid='c:'" -property Freespace).FreeSpace/1mb}

Now in PowerShell I can invoke the scriptblock.

PS S:\> &$freec
94079.72265625

Ok then. I have a number of these defined. I decided I wanted an easy way to identify them when I run Get-Variable. For example, if I remembered all the variable names I could just do this:

PS S:\> get-variable freec,dirt

Name                           Value
----                           -----
freec                          (gwmi win32_logicaldisk -filter "deviceid='c:...
dirt                           Param([string]$Path=$env:temp) Get-ChildItem ...

But needless to say that’s asking too much. When I first looked at this problem I went down the path of trying to parse values I saw with Get-Variable to identify potential script blocks. Then I realized this was a rookie mistake. PowerShell is all about the objects. Now a variable is also an object with a value property. This value could be a string, and integer or a pscredential. So my task then was to identify each value type.

Every object in PowerShell has a built in method called GetType().

PS S:\> $s=get-service spooler
PS S:\> $s.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    ServiceController                        System.ComponentM...

This is actually another object with a Name property.

PS S:\> $s.GetType().name
ServiceController

Aha! Let’s look at this with my variable.

PS S:\> (get-variable freec).value.GetType().Name
ScriptBlock

This is a one-line shortcut that gets the Value property of the Freec variable and then runs the GetType() method followed by retrieving just the Name property. This is promising. Here’s one way I can use this:

get-variable | Where {$_.value.GetType().Name -eq "ScriptBlock"}

As you can see there is still an issue with variables with no values.

I’ll just add another condition to my Where expression.

get-variable | Where {$_.value -AND $_.value.GetType().Name -eq "ScriptBlock"}

Success!

These are in fact all of the scriptblocks in my current session. But now I can take this a step further and look at my other variables and their type.

get-variable | select Name,@{Name="Type";Expression={$_.value.GetType().Name}}

Or I might try grouping.

get-variable | select Name,@{Name="Type";Expression={$_.value.GetType().Name}} | where {$_.type} | Group Type | Sort Count -Descending

I wanted to filter out empty values so I’m only keeping objects that have a defined type in my grouped output.

The bottom line is never forget about the object!

Post to Twitter Post to Plurk Post to Yahoo Buzz Post to Delicious Post to Digg Post to Facebook Post to FriendFeed Post to Google Buzz Post to Ping.fm Post to Reddit Post to Slashdot Post to StumbleUpon Post to Technorati

Posted in Friday Fun, PowerShell | Tagged , , , , , | Leave a comment

Using Start-Job as a Scheduled Task

Here’s a technique you might want to use for ad hoc troubleshooting or reporting. Even though it is possible to set up scheduled tasks to run PowerShell commands or scripts, it is cumbersome and time consuming. PowerShell v3 offers a great alternative, but I’ll cover that another day. Suppose I want to do something every 15 minutes such as check the status of a service. Instead of going through the effort of creating a scheduled task, I’ll create a PowerShell job.

If I setup a job, it will run in my PowerShell session for as long as it needs to run or until I close my PowerShell session. Thus the trick is to keep the job running which easy to accomplish with a simple While loop.

While ($True) {
  #do something
}

This will loop indefinitely because True is always True. In an interactive session, I can break out of this using Ctrl+C or insert code to break out if some condition is met. If I use this loop in my Start-Job command, the command will run indefinitely until the job terminates. I can always kill the job with the Stop-Job cmdlet. Although I suppose you should be careful with the code you put in the While loop as part of job because if you use Stop-Job, there’s no way of knowing what code might be executing at the time. But for my use of this technique I’m keeping it simple and quick.

The other part is the timing interval. This is easily accomplished using the Start-Sleep cmdlet and specifying a value in milliseconds, or more typically, seconds. Depending on your task you could also take advantage of eventing, but that too is a bit complicated to setup and tear down. So, now the main part of my While loop looks like this:

While ($True) {
  #do something
 Start-Sleep -seconds 900
}

My code will run every 900 seconds (15 minutes). Here’s a longer example that I put in a script.

$file="c:\work\mylog.txt"
#the number of seconds to pause between checks
$seconds=600
#the service to check
$service="Spooler"
#the computer to check
$computer=$env:computername

while ($True) {
    $s=get-service $service -ComputerName $computer
    $t="{0} Service {1} on {3} has a status of {2}" -f (Get-Date), $s.name,$s.status,$computer
    $t | Out-File -FilePath $file -Append
    Start-Sleep -seconds $seconds
}

I could have put all of that into a script block and created the job, or I could use the script.

PS C:\> start-job -FilePath C:\scripts\PollService.ps1

The job will write the service status information to a local text file every 10 minutes. When I’m done I can simply my PowerShell session or run Stop-Job. Here’s where it can really get interesting: how about running this “scheduled task” ON a remote computer? There are two approaches. It depends on where you want the job to live. I could keep the job on my computer:

PS C:\> invoke-command -FilePath C:\scripts\PollService.ps1 -ComputerName quark -asjob
PS C:\> get-job 3 | format-list


HasMoreData   : True
StatusMessage :
Location      : quark
Command       : #requires -version 2.0

                $file="c:\work\mylog.txt"
                #the number of seconds to pause between checks
                $seconds=600
                #the service to check
                $service="Spooler"
                $computer=$env:computername

                while ($True) {
                $s=get-service $service -ComputerName $computer
                $t="{0} Service {1} on {3} has a status of {2}" -f (Get-Date),
                $s.name,$s.status,$computer
                $t | Out-File -FilePath $file -Append
                Start-Sleep -seconds $seconds
                }
JobStateInfo  : Running
Finished      : System.Threading.ManualResetEvent
InstanceId    : 03a48cd0-f21a-4b7d-9a78-f148ec784ff8
Id            : 3
Name          : Job3
ChildJobs     : {Job4}
Output        : {}
Error         : {}
Progress      : {}
Verbose       : {}
Debug         : {}
Warning       : {}
State         : Running

The job object is on my (local) computer, even though the task is running on the remote computer. The other approach is to put the job ON the remote computer. This is a little trickier since I have to get the code I want to run ON the remote computer. I could copy the script over. Or I might try something like this:

First I want to convert my script into a scriptblock by stripping out the comments and inserting a semi colon at the end of each line. Then I can create a scriptblock from this text on the remote computer.

PS C:\> $text=get-content C:\scripts\PollService.ps1 | where {$_ -notmatch "^#" -AND $_} | foreach {"$_;"}

I can pass this text as a parameter with invoke-command to setup a job on the remote computer. I recommend using a PSSession in case you want to go back later and stop the job.

PS C:\> $quark=new-pssession -comp quark
PS C:\> invoke-command -scriptblock {param ($txt) $sb=$executioncontext.invokecommand.newscriptblock($txt) ; Start-job -scriptblock $sb } -session $quark -ArgumentList ($text | out-string)

The job is created on the remote session and runs indefinitely.

PS C:\> invoke-command {get-job -State Running} -Session $quark

WARNING: 2 columns do not fit into the display and were removed.

Id              Name            State      HasMoreData     Location
--              ----            -----      -----------     --------
17              Job17           Running    True            localhost
PS C:\> invoke-command {get-content C:\work\mylog.txt} -Session $quark
1/5/2012 9:37:41 AM Service Spooler on QUARK has a status of Running
1/5/2012 9:38:46 AM Service Spooler on QUARK has a status of Running
1/5/2012 10:44:54 AM Service Spooler on QUARK has a status of Running

As I mentioned there are probably several ways you could do this. When I am finished I can either terminate the PSSession or stop the remote job.

PS C:\> invoke-command {stop-job -State Running -PassThru} -Session $quark

WARNING: 2 columns do not fit into the display and were removed.

Id              Name            State      HasMoreData     Location
--              ----            -----      -----------     --------
17              Job17           Stopped    False           localhost

So the next time you need some scheduled PowerShell, at least on a temporary basis, take a look at Start-Job.

Post to Twitter Post to Plurk Post to Yahoo Buzz Post to Delicious Post to Digg Post to Facebook Post to FriendFeed Post to Google Buzz Post to Ping.fm Post to Reddit Post to Slashdot Post to StumbleUpon Post to Technorati

Posted in PowerShell v2.0, Scripting | Tagged , , , , | 1 Comment

Format Leading Zeros in PowerShell

I’ve been working on a question in the forums at ScriptingAnswers.com and the need arose to create a folder name with a 4 digit number. But the number needed to have enough leading zeros so that the number was always 4 digits. For example, Test_0005 or Test_0456. The solution is to use the -f Format operator.

To use this operator on the left side of the statement is a formatting directive and on the right side of -f operator is the value to be formatted. To get the leading zeros I’m going to use a formatting string like this:

"{0:0000}"

This basically says, “take a value and make pad it with 4 0s. Here are some examples:

PS C:\> "{0:0000}" -f 4
0004
PS C:\> "{0:0000}" -f 45
0045
PS C:\> "{0:0000}" -f 456
0456
PS C:\> "{0:0000}" -f 4567
4567

Technically, these are string objects, but that’s ok. Let’s see how we might use this.

PS C:\> 1..10 | foreach {
>>  $i="{0:0000}" -f $_
>>  $dir="c:\test\Target_$i"
>>  $file="file_$i.txt"
>>  $target=Join-Path -Path $dir -ChildPath $file
>>  Write-Host "Updating $target"
>>
>>  }
>>
Updating c:\test\Target_0001\file_0001.txt
Updating c:\test\Target_0002\file_0002.txt
Updating c:\test\Target_0003\file_0003.txt
Updating c:\test\Target_0004\file_0004.txt
Updating c:\test\Target_0005\file_0005.txt
Updating c:\test\Target_0006\file_0006.txt
Updating c:\test\Target_0007\file_0007.txt
Updating c:\test\Target_0008\file_0008.txt
Updating c:\test\Target_0009\file_0009.txt
Updating c:\test\Target_0010\file_0010.txt
PS C:\>

Within the For loop I’m taking the piped in number and formatting to 4 places with leading zeros. This value is then inserted into variables for paths and files, using Join-Path to combine them. This is a much better technique than trying to concatenate.

I hope you find this useful.

Post to Twitter Post to Plurk Post to Yahoo Buzz Post to Delicious Post to Digg Post to Facebook Post to FriendFeed Post to Google Buzz Post to Ping.fm Post to Reddit Post to Slashdot Post to StumbleUpon Post to Technorati

Posted in PowerShell, Scripting | Tagged , , | 5 Comments

Where’s Jeff?

I hope that 2011 was a good a year for you as it was for me. In 2012 I hope to keep up the pace. Here’s a brief look at where you can find me this year and a few of my projects. Of course, this blog is my primary content site. According to my WordPress stats I only published 42 articles in 2011 so I will have to do much better this year.

I still write the Prof. PowerShell column. As PowerShell v3.0 gets closer to release, you can expect to see more v3 content in these columns. I also will continue to contribute the the SMB IT Simplified web site. There is a wealth of great content here provided by the likes of Greg Shields, Don Jones and Jeremy Moskowitz. Late in 2011 I started contributing content to the Petri Knowledgebase. This site has been around for quite awhile and I believe has always had high quality content. I’m pleased to be offering my thoughts to the mix.

Publicly and in person, I hope to be at a few conferences this year such as Microsoft TechEd, TechMentor and The Experts Conference. I also hope to offer a few public PowerShell training classes. Thanks to every one who took the time to fill out my end of year traning survey. As those things fall into place, I’ll be posting about it here.

And in case you didn’t know, I am available for private training classes as well on just about any PowerShell related topic. Or perhaps you need someone to come in and offer some mentoring or advice on automating and improving efficiency. I’d love to help out and that type of work might not even entail any travel.

In addition, I am a Microsoft Certified Trainer (over 10 years now). If you have a training center, like New Horizons, and would like me to teach a course there, perhaps the 10325 Automating Administration with Windows PowerShell 2.0, give them my contact information and hopefully we can work something out.

The last place to look for me is on the book shelf. I am co-writing a PowerShell v3.0 book with Don Jones and Richard Siddaway for Manning Press. Because we don’t know when PowerShell v3.0 will ship and have yet to see the final bits, don’t expect to see the title until after RTM. But hopefully not too far after. There may even be another book project or two in the mix for 2012. Again, as these things come to fruition, I’ll post information here.

Finally, I continue to be as active as I can on Twitter. I am also on Google Plus. I tend to use Twitter for quick information and G+ for slightly longer material. Although you can expect to find a tweet linking back to Google Plus.

With the impending arrival of Windows 8 and PowerShell v3.0 there will be much to keep us all busy in 2012. I hope I can continue to be a part of your little corner of the IT world. If there are topics or ideas you’d like me to pursue in terms of writing or training projects, I hope you’ll let me know. If you have more private questions, use the contact me by email link off to the right.

Post to Twitter Post to Plurk Post to Yahoo Buzz Post to Delicious Post to Digg Post to Facebook Post to FriendFeed Post to Google Buzz Post to Ping.fm Post to Reddit Post to Slashdot Post to StumbleUpon Post to Technorati

Posted in Books, Conferences, PowerShell, Professional, Training, Writing | Tagged , , | Leave a comment