PowerShell for Windows Admins


February 6, 2015  2:17 PM

Workflow for last logon

Richard Siddaway Richard Siddaway Profile: Richard Siddaway
Active Directory, Powershell

PowerShell Workflows have been a bit of an underachiever in terms of their adoption and use.

Something I read prompted the thought that there are a number of activities when working with Active Directory where you have to interrogate a number of machines to get the result.

That’s something that workflows are good at.

A common problem is when did that user last logon. Using the Microsoft cmdlets you get this:

£> Get-ADUser -Identity Richard -Properties * -Server server02  | fl lastlogon* lastLogon          : 130677233108205070
LastLogonDate      : 30/01/2015 14:05:58
lastLogonTimestamp : 130671003582718505

£> Get-ADUser -Identity Richard -Properties * -Server W12R2SCDC01  | fl lastlogon*
lastLogon          : 130671225126188854
LastLogonDate      : 30/01/2015 14:05:58
lastLogonTimestamp : 130671003582718505

Lastlogontimestamp is the identical across the 2 DCs because it’s replicated – however its not always up to date. It can be up to 14 days out.

Now the lastlogon and lastlogontimestamp are not in a readable format – so need a bit of work on that

$dc = “server02″

Get-ADUser -Identity Richard -Properties lastLogon, LastLogonDate, lastLogonTimestamp -Server $dc  |
select Name, LastLogondate,
@{N=’LastLogon'; E={[datetime]::FromFileTime([int64]::Parse($_.lastlogon))}},
@{N=’LastLogonTimestamp'; E={[datetime]::FromFileTime([int64]::Parse($_.lastlogonTimestamp))}},
@{N= ‘Domain Controller'; E={$dc}}
Name               : Richard
LastLogondate      : 30/01/2015 14:05:58
LastLogon          : 06/02/2015 19:08:30
LastLogonTimestamp : 30/01/2015 14:05:58
Domain Controller  : server02

You can see that the lastlogondate (Introduced with Windows 2008 R2) is the lastlogontimestamp in a readable format.  Notoce the difference between the lastlogon and the lastlogontimestamp.

The calculation to get the date is relatively straightforward. Start by converting the property to a int64

[int64]::Parse($_.lastlogonTimestamp)

Then treat that as a filetime and convert to a datetime

[datetime]::FromFileTime([int64]::Parse($_.lastlogonTimestamp))

Now lets look at getting that data from multiple machines in parallel

workflow get-lastlogon {
param (
[string[]]$computername
)

foreach -parallel ($dc in $computername) {
Get-ADUser -Identity Richard -Properties lastLogon, LastLogonDate, lastLogonTimestamp -Server $dc  |
Select-Object -Property Name, LastLogondate,
@{N=’LastLogon'; E={[datetime]::FromFileTime([int64]::Parse($_.lastlogon))}},
@{N=’LastLogonTimestamp'; E={[datetime]::FromFileTime([int64]::Parse($_.lastlogonTimestamp))}} |
Add-Member -MemberType NoteProperty -Name ‘DomainController’ -Value $dc -PassThru
}
}

get-lastlogon -computername (Get-ADDomainController -Filter * | select -ExpandProperty Name)

The workflow accepts an array of computer names. A foreach –parallel  loop is use do run the Get-Aduser command.

One oddity was setting the Domain Controller name into the output object. It wouldn’t accept the value when I used a select calculated field but I could use Add-Member.

if I figure out what’s going on I post the  information

 

February 5, 2015  9:57 AM

Scripting Guy CDXML series finished

Richard Siddaway Richard Siddaway Profile: Richard Siddaway
CIM, Powershell, WMI

My CDXML series on the Scripting Guy blog finished today.  The 4 articles are:

http://blogs.technet.com/b/heyscriptingguy/archive/2015/02/02/registry-cmdlets-manage-the-registry.aspx

http://blogs.technet.com/b/heyscriptingguy/archive/2015/02/03/registry-cmdlets-first-steps-with-cdxml.aspx

http://blogs.technet.com/b/heyscriptingguy/archive/2015/02/04/registry-cmdlets-advanced-cdxml.aspx

http://blogs.technet.com/b/heyscriptingguy/archive/2015/02/05/registry-cmdlets-using-advanced-cdxml.aspx


February 2, 2015  11:41 AM

Scripting Guy CDXML series

Richard Siddaway Richard Siddaway Profile: Richard Siddaway
CIM, Powershell

Today starts a four part series I’ve written for the Scripting Guy blog on using CDXML to create a module to work with the registry.  Don’t know what CDXML is – you will when you’ve read the series

The first post is at http://blogs.technet.com/b/heyscriptingguy/archive/2015/02/02/registry-cmdlets-manage-the-registry.aspx


January 31, 2015  7:54 AM

Modifying text

Richard Siddaway Richard Siddaway Profile: Richard Siddaway
Powershell

I needed to modify some text somewhere in a file. The file looks like this

##  start file
This is some text.

I want to change something.

But not this.
##  end file

I was playing around with various options.  The simplest I found was this:

£> $txt = Get-Content .\test.txt
£> $txt = $txt.Replace(“I want”, “I need”)
£> Set-Content -Value $txt -Path C:\Test\test.txt
£> Get-Content .\test.txt
##  start file
This is some text.

I need to change something.

But not this.
##  end file

You could simplify to

£> $txt = (Get-Content .\test.txt).Replace(“I want”, “I need”)
£> Set-Content -Value $txt -Path C:\Test\test.txt -PassThru

##  start file
This is some text.

I need to change something.

But not this.
##  end file

The passthru parameter displays the file contents you’ve set.

Or if you are a fan of convoluted one liners

£> Set-Content -Value ((Get-Content .\test.txt).Replace(“I want”, “I need”)) -Path C:\Test\test.txt -PassThru
##  start file
This is some text.

I need to change something.

But not this.
##  end file

If you take this approach just make sure your text is uniquely identified otherwise you may change more than you thought.


January 30, 2015  10:07 AM

Testing for a hotfix

Richard Siddaway Richard Siddaway Profile: Richard Siddaway
CIM, Powershell, WMI

KB3000850 – the November roll up for Windows 2012 R2 contains some very useful updates.

I’ve installed it on some machines in my lab but not all. The update is huge so I’m installing it manually rather than through WSUS.

I need to test a remote machine to determine if the update  is installed.

If it is installed you get a this back

£> Get-HotFix -Id KB3000850 -ComputerName w12r2dsc

Source        Description             HotFixID         InstalledBy
——          ———–                ——–          ———–
W12R2DSC      Update           KB3000850      MANTICORE\Richard

But if its not installed you get this

£> Get-HotFix -Id KB3000850 -ComputerName w12r2od01
Get-HotFix : Cannot find the requested hotfix on the ‘w12r2od01′ computer. Verify the input and run the command again.
At line:1 char:1
+ Get-HotFix -Id KB3000850 -ComputerName w12r2od01
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : ObjectNotFound: (:) [Get-HotFix], ArgumentException
+ FullyQualifiedErrorId : GetHotFixNoEntriesFound,Microsoft.PowerShell.Commands.GetHotFixCommand

Get-Hotfix actually uses the Win32_QuickFixEngineering CIM class so you need to have DCOM open on the remote system otherwise you get a

Get-HotFix : The RPC server is unavailable. (Exception from HRESULT: 0x800706BA)

error.

You need to wrap the call to Get-Hotfix in a try catch. You only need to know if the update is installed so creating a specialised output object manages that for you

Get-VM |
where State -eq ‘Running’ |
foreach {

$props = @{
Machine = $($psitem.Name)
Present = $false
}

try {
$hf = Get-HotFix -Id KB3000850 -ComputerName $psitem.Name -ErrorAction Stop
$props.Present = $true
}
catch {
$props.Present = $false
}

New-Object -TypeName PSObject -Property $props

}

Substitute any other method of getting a list of computer names, for my call to Get-VM, to match your environment.


January 29, 2015  12:15 PM

Variable select

Richard Siddaway Richard Siddaway Profile: Richard Siddaway
Powershell

I was working on some code that  accesses a SQL database this afternoon. I only needed to pull back a single column from a single row but which column to pull back is variable depending on other data.

That’s OK

$query = “SELECT $colname FROM tablename WHERE x = ‘y’”

Invoke-SQLcmd –server <server> –database <database> –query $query

Now the problem hit me as I need to get the actual value from the object that invoke-sqlcmd returns

I normally do this:

Invoke-SQLcmd –server <server> –database <database> –query $query | select –expandproperty <columnname>

And then it dawned on me that I have the column name in $colname so this works

Invoke-SQLcmd –server <server> –database <database> –query $query | select –expandproperty $colname

I got so used to explicitly stating the properties I need that I forgot you could use a variable.  If you want an example to try on any system

Get-Service | select -First 1 | select -ExpandProperty $p1

or you could try

$p1 = ‘Status’
Get-Service | select Name, $p1

and change to

$p1 = ‘DisplayName’
Get-Service | select Name, $p1

Not something you want to do every day but a useful trick when you need it


January 27, 2015  1:47 PM

WMI errors

Richard Siddaway Richard Siddaway Profile: Richard Siddaway
Powershell, WMI

Most PowerShell users will have done something like this:

£> Get-WmiObject -ClassName Win32_ComputerSystem
Domain              : WORKGROUP
Manufacturer        : Microsoft Corporation
Model               : Surface Pro 2
Name                : RSSURFACEPRO2
PrimaryOwnerName    :
TotalPhysicalMemory : 8506093568

Or you can use a computername to access a remote system

£> $computer = $env:COMPUTERNAME
£> Get-WmiObject -ClassName Win32_ComputerSystem -ComputerName $computer

Domain              : WORKGROUP
Manufacturer        : Microsoft Corporation
Model               : Surface Pro 2
Name                : RSSURFACEPRO2
PrimaryOwnerName    :
TotalPhysicalMemory : 8506093568

But if you try to access a remote machine that doesn’t exist

£> $computer = ‘WillNotBeFound’
£> Get-WmiObject -ClassName Win32_ComputerSystem -ComputerName $computer
Get-WmiObject : The RPC server is unavailable. (Exception from HRESULT: 0x800706BA)
At line:1 char:1
+ Get-WmiObject -ClassName Win32_ComputerSystem -ComputerName $computer
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : InvalidOperation: (:) [Get-WmiObject], COMException
+ FullyQualifiedErrorId : GetWMICOMException,Microsoft.PowerShell.Commands.GetWmiObjectCommand

 

You’ve heard of try – catch  so you do something like this

$computer = ‘WillNotBeFound’

try {
Get-WmiObject -ClassName Win32_ComputerSystem -ComputerName $computer -ErrorAction Stop
}
catch {
Write-Warning “An error occurred”
}

OR your catch may do something to record the error.

$computer = ‘WillNotBeFound’

try {
Get-WmiObject -ClassName Win32_ComputerSystem -ComputerName $computer -ErrorAction Stop
}
catch {
Write-Warning “An error occurred for machine: $computer”
}

If you make the warning message specific

$computer = ‘WillNotBeFound’

try {
Get-WmiObject -ClassName Win32_ComputerSystem -ComputerName $computer -ErrorAction Stop
}
catch {
Write-Warning “The RPC server is unavailable for machine: $computer”
}

What happens if you have a different error. Lets say you copy the code and change the class. You test the code

$computer = $env:COMPUTERNAME

try {
Get-WmiObject -ClassName Win32_LogicalDsik -ComputerName $computer -ErrorAction Stop
}
catch {
Write-Warning “The RPC server is unavailable for machine: $computer”
}

And get a message

WARNING: The RPC server is unavailable for machine: RSSURFACEPRO2

What you should see is

Get-WmiObject : Invalid class “Win32_LogicalDsik”
At line:1 char:1
+ Get-WmiObject -ClassName Win32_LogicalDsik -ComputerName $computer
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : InvalidType: (:) [Get-WmiObject], ManagementException
+ FullyQualifiedErrorId : GetWMIManagementException,Microsoft.PowerShell.Commands.GetWmiObjectCommand

 

Because the class is mistyped.

You could try and create a number of catch statements – one for each error type.  Good luck with that. Its a lot of work.  A better approach, in my mind is to extract the error information you need

$computer = $env:COMPUTERNAME

try {
Get-WmiObject -ClassName Win32_LogicalDsik -ComputerName $computer -ErrorAction Stop
}
catch {
$errordata = @”
Computer = $computer
Exception = $($error[0].Exception)
ErrorId = $($error[0].FullyQualifiedErrorId)
“@

Write-Warning $errordata
}

If you look in the PowerShell automatic variable $error – you’ll find its a collection of error messages.  $error[0] contains the last error.  So now you get

£> $computer = $env:COMPUTERNAME

try {
Get-WmiObject -ClassName Win32_LogicalDsik -ComputerName $computer -ErrorAction Stop
}
catch {
$errordata = @”
Computer = $computer
Exception = $($error[0].Exception)
ErrorId = $($error[0].FullyQualifiedErrorId)
“@

Write-Warning $errordata
}
WARNING:   Computer = RSSURFACEPRO2
Exception = System.Management.ManagementException: Invalid class “Win32_LogicalDsik”
ErrorId = GetWMIManagementException,Microsoft.PowerShell.Commands.GetWmiObjectCommand

 

For a set of computers

$computers = $env:COMPUTERNAME, ‘WillNotBefound’

foreach ($computer in $computers){
try {
Get-WmiObject -ClassName Win32_computersystem -ComputerName $computer -ErrorAction Stop
}
catch {
$errordata = @”
Computer = $computer
Exception = $($error[0].Exception)
ErrorId = $($error[0].FullyQualifiedErrorId)
“@

Write-Warning $errordata
}
}

You get the data where you can contact the machine – otherwise a warning message that the server is unavailable.  Rather than outputting a warning message create an object from the error data and save to a CSV file


January 26, 2015  12:41 PM

Unravelling lists of distinguished names

Richard Siddaway Richard Siddaway Profile: Richard Siddaway
Active Directory, Powershell

There are a number of AD properties such as MemberOf and directrports that consist of a collection of distinguisednames.  Sometimes you need the  name  of the object rather than the distinguished name:

function getname {
[CmdletBinding()]
param (
[string]$dn
)

$name = Get-ADObject $dn | select -ExpandProperty Name

return $name
}
Get-ADUser -Filter * -SearchBase “OU=Testing,DC=Manticore,DC=org” -Properties MemberOf |
foreach {

$name = $psitem.Name

foreach ($member in $psitem.MemberOf) {
$props = [ordered]@{
User = $name
Group = getname -dn $member
}

New-Object -Property $props -TypeName PSObject

}
}

In this example I’m looking at the MemberOf property.  Pull back the set of users and foreach look at each member in the MemberOf list. I pass that to a function that returns the name – I can reuse the function in other scripts.

Don’t make the mistake of looking at group membership in this way though. You can get the names of group members much more simply

Get-ADGroupMember -Identity tg1 | select name

 


January 26, 2015  11:08 AM

Filtering on if an AD property exists

Richard Siddaway Richard Siddaway Profile: Richard Siddaway
Active Directory, Powershell

There are times when you want to filter the results based on whether a user has an AD property set.  You could do this:

Get-ADUser -Filter * -Properties Title | Where Title  | select Name, Title

However, that involves pulling back all of the users and then filtering. Not very efficient especially across the network in a big domain.

A better solution would be to use an LDAP filter

Get-ADUser -LDAPFilter “(Title=*)” -Properties Title  | select Name, Title

Get-ADUser does the checking to see if the user has a title set so only those objects with that property are retrieved. If you want to see the value of the property you need to include it by using the –Properties parameter.


January 21, 2015  12:21 PM

Awkward file and folder names

Richard Siddaway Richard Siddaway Profile: Richard Siddaway
Powershell

Spent some time today dealing with a situation where there were special characters – namely [ ] in folder a file names

£> Get-ChildItem -Path C:\Test
Directory: C:\Test
Mode                LastWriteTime     Length Name
—-                ————-     —— —-
d—-        21/01/2015     17:58            Awkward [One]
d—-        21/01/2015     17:59            Simple One

 

Each folder has 3 files:

File 1.txt
File 2.txt
File 3.txt

Get-ChildItem -Path ‘C:\Test\Simple One’

will work and show you the contents. When I’m typing paths like this I let Tab completion do the work. Type c:\test\ and use the Tab key to cycle round the available folders. This gives

£> Get-ChildItem -Path ‘C:\Test\Awkward `[One`]’
Get-ChildItem : Cannot find path ‘C:\Test\Awkward `[One`]’ because it does not exist.
At line:1 char:1
+ Get-ChildItem -Path ‘C:\Test\Awkward `[One`]’
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : ObjectNotFound: (C:\Test\Awkward `[One`]:String) [Get-ChildItem], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

Unfortunately, the construct produced by Tab completion doesn’t work.  You need to double up on the back ticks so that it functions as an escape character.

Get-ChildItem -Path ‘C:\Test\Awkward “[One“]’

But that only shows you the folder not the contents.

Get-ChildItem -Path ‘C:\Test\Awkward “[One“]\*’

OR

Get-ChildItem -Path ‘C:\Test\Awkward “[One“]’ -Include *

Will show you the contents of the folder.

But bizarrely

Get-Content -Path ‘C:\Test\Awkward `[One`]\File 1.txt’

Works. As does

Copy-Item -Path ‘C:\Test\Awkward `[One`]\File 1.txt’ -Destination c:\test2

By using back ticks and quotes you can get round most problems like this. Other characters that cause similar problems are commas and quote marks.

Best advice of all – don’t use those awkward characters in your file names if you can possibly avoid it.

 

 


Forgot Password

No problem! Submit your e-mail address below. We'll send you an e-mail containing your password.

Your password has been sent to: