As a systems administrator, you're well aware of how busy you are. If you're
not putting out four-alarm fires, you're playing catch-up on last month's and
maybe even last year's projects. The idea that you can squeeze anything else
into your schedule seems as preposterous as Microsoft Bob 5.0. Yet there's one
technology that's well worth making time for—Windows PowerShell, an interactive
scripting and command-shell environment that lets you automate administrative
tasks and access a wide range of information.
With PowerShell, you can run commands directly at the command prompt or run
scripts that contain those commands. PowerShell supports its own scripting language,
which leverages the Microsoft .NET object model to combine the rich features
of object-oriented programming with the ease of command-shell scripting. What
that means for you is a powerful environment that can turn complex and repetitive
tasks into simple operations. Through PowerShell, you can access a variety of
systems and technologies, such as Active Directory (AD) and Windows Management
Instrumentation (WMI) to perform such tasks as retrieving event log entries,
disabling user accounts in AD, and retrieving a computer's user-defined shares.
PowerShell runs on Windows Vista, Windows Server 2003 SP1, Windows Server 2003
Release Candidate 2 (R2), and Windows XP SP2. It will also run on Windows Server
2008 (formerly code-named Longhorn Server). You can install PowerShell on x86,
x64, and IA64 processor architectures. However, before you install PowerShell,
you must first install Microsoft .NET Framework 2.0. You can download the .NET
Framework at http://msdn2.microsoft.com/en-us/netframework/aa569263.aspx
and PowerShell at http://www.microsoft.com/technet/scriptcenter/hubs/msh.mspx.
To install either product, simply run the setup program and follow the steps
in the installation wizard.
After you've installed PowerShell, you're ready to go. Click Start, All Programs,
Windows PowerShell 1.0, then Windows PowerShell. In the PowerShell window, you
can run commands or PowerShell scripts (.ps1) files by entering the command
or filename at the command prompt. To test your installation, type
get-help
at the command prompt and press Enter. This displays information about getting
help in PowerShell—a handy command to be sure. (For more cmdlets that
are helpful when learning PowerShell, see the sidebar "PowerShell
Pointers.")
You're now ready to run commands and scripts. All you need to do is to learn
a little about the PowerShell language. To help you with that, I'll review three
sample scripts—RetrieveAppEvents.ps1, DisableUser.ps1, and FindShares.ps1—that
demonstrate many of the basic concepts in the language and show you how easy
it is to get started with PowerShell.
RetrieveAppEvents.ps1
RetrieveAppEvents.ps1 in Listing 1 retrieves
entries from the local application event log and saves them to a text file.
As callout A shows, I begin the script by defining the $date variable. A dollar
sign always precedes parameter and variable names. The variable uses the Get-Date
cmdlet to retrieve the current date and time (aka datetime). A cmdlet, which
is similar to a function, performs a specific action and usually takes the form
of verb-noun. I then use the AddDays method to obtain the datetime exactly
24 hours (i.e., 1 day) prior to the current datetime and assign that value to
the $date variable.
Next, I create the FormatEntryType function, as callout B shows. A function is a named
block of code that performs a specific action.
After you create the function, you can reference
it anywhere in your script and the block of code
will run. In this case, the FormatEntry function
retrieves the content of a text file, modifies
that content, and saves it to a second text file.
The function takes the $file parameter, which
passes the pathname of the target text file into
the function.
The first command in the function's statement block (enclosed in curly brackets)
uses the Get-Content cmdlet to retrieve content from the text file in $file.
Notice that a pipe (|) follows the cmdlet. This indicates that the content should
be passed down the pipeline to the next cmdlet. One feature that makes PowerShell
so useful is the ease with which you can create pipelines to pass information
from one statement to the next.
In this function, I pass the data retrieved by Get-Content down the pipeline
to a ForEachObject cmdlet, for which you can use the alias ForEach or %. The
ForEach cmdlet lets you iterate through objects within a collection. In this
case, the collection is made up of the content of the text file. By default,
the objects in a file collection are delineated by line breaks, which means
the collection contains one object per line. (You can override the default behavior,
but for the purposes of this example, line breaks work well.)
The ForEach cmdlet uses an expression,
enclosed in curly brackets, to process each
object in the collection. The expression begins
with the $_ symbol, which refers to the current
input object from the collection. The expression then uses the -replace operator to replace
any error object with an *** ERROR *** object.
In other words, any line that contains only the
word error is replaced with *** ERROR ***. A
second ForEach cmdlet performs a similar
operation on warning objects.
The second ForEach cmdlet pipes the content to the Out-File cmdlet, which sends the
content to the AppEvent_EntryTypes.txt file.
Each time you run the function within a script,
the content will be inserted into that file.
The code at callout C retrieves the application event entries and assigns the results to
the $events variable. To retrieve data from the
application events log, I use the Get-Eventlog
cmdlet and specify Application as a parameter.
I then send the event data down the pipeline to
the Where-Object cmdlet. The backtick (`) at
the end of the line indicates that the statement
continues to the next line. However, you don't
have to use a backtick when a line breaks at a
pipe.
The Where-Object cmdlet filters the data
based on the expression defined in the curly
brackets. As with ForEach, you use $_ to reference the current object within the collection.
In this case, the collection is made up of the
event entries. You can also use $_ to reference specific properties within the object. For
example, the expression uses $_ to reference
the TimeGenerated property. You reference
an object's properties by adding a period followed by the property name. The expression
then uses the greater than (-gt) operator to
compare the TimeGenerated property's value
to the value in the $date variable. As a result,
the $events variable includes only events generated within the last 24 hours.
Next, I use the $events variable to access the events. The statement in callout
D passes the content in $events down the pipeline to a ForEach cmdlet. The ForEach
expression consists of a Out-File cmdlet that outputs the event content to AppEvents.txt.
The Out-File cmdlet includes the -Append option to ensure that each event is
added to the file without overwriting any events. In addition, the cmdlet includes
the -InputObject option, which uses the $_ symbol and property names to specify
the types of data to save to the file. As a result, the output file includes
only the timestamp, entry type, source, and message associated with each event.
Finally, the code in callout E calls the FormatEntryType function and passes
the AppEvents.txt file's pathname to the function. As you saw earlier, this
function retrieves the data from the first text file, updates the data, and
adds it to the AppEvents_EntryTypes.txt file. Figure
1 shows a sample event from AppEvents_EntryTypes.txt.
To run RetrieveAppEvents.ps1 from the PowerShell command prompt, you simply
need to type the script's pathname and press Enter. So, the command might look
like
c:\scripts\retrieveappevents.ps1
However, PowerShell prevents you from running scripts by default. To modify
the default behavior, you must change the Power-Shell security settings. To
get started, you can change the settings by entering the command
set-executionpolicy remotesigned
Thereafter, you can run scripts that you create, but any other script must
be digitally signed.
Now that you've seen your first script, you
should be familiar with many of the basic PowerShell concepts. As you'll see, a lot of these
concepts apply to other scripts.
DisableUser.ps1
DisableUser.ps1 in Listing 2 disables user
accounts in AD. As callout A shows, I begin this script by using the Param keyword to define a parameter ($sam) that passes in the user account that's
entered on the command line when the script is run. When using this keyword,
you have several options:
-
You can use only the Param keyword and the name of the parameter in parentheses.
-
You can define a default value in case no value is specified when running
the script.
-
You can use the Throw keyword (as I've done here) to return an error message
when no value is specified. When returning an error message, you must enclose
the Throw keyword and message in parentheses and precede the parentheses
with a dollar sign.
The code at callout B locates the user account in AD. This code begins by creating
an object that searches the directory. To create the object, I use the New-Object
cmdlet with the DirectorySearcher class in the DirectoryServices namespace.
(For information about the DirectoryServices namespace, go to http://msdn2.microsoft.com/en-us/library/
system.directoryservices.aspx.) I then assign the object to the $ds variable.
Next, I create a filter on the $ds object by setting the object's Filter property
($ds.filter). The filter is based on the Active Directory Service Interfaces
(ADSI) attributes defined after the equal sign (=). The filter removes all values
except those that conform to the attribute definitions. As a result, the filter
returns a user account that is part of the person object category and the user
object class and that has a SAM name that matches the one specified in the $sam
parameter.
After creating the filter, I use the FindOne
method of the $ds object ($ds.findOne()) to
retrieve the user account. I assign the account
to the $dn variable.
The code in callout C defines two variables. The first variable, $desc, stores the user
account's description. To retrieve the description, I use the $dn variable to call the account's
properties, then call the Description property
($dn.properties.description). The second variable, $date, stores the current datetime, which
I obtain with the Get-Date cmdlet. Both these
variables are used later in the script to update
the user account's description.
The code in callout D disables the user
account. The section is encased in an If statement block that runs when the If condition
($dn.path.length –gt 0) evaluates to true. The
condition compares the length of the $dn
object's Path property to 0. The Path property
contains the LDAP location of the user account
in AD. When the Path property contains a
value, the If statement block runs.
The first command in the If statement block
uses the Path property to create an ADSI object
for the user account. The ADSI object, which is
assigned to the $user variable, is used to access
the object's properties and methods, including
the AccountDisabled property.
In the next statement, I set the AccountDisabled property to true. Because AccountDisabled is stored in a binary collection in AD,
I use the InvokeSet method on the PowerShell
base object (psBase) to update the property.
The InvokeSet method takes two arguments:
the property name (AccountDisabled) and the
new value. To set the AccountDisabled property to true, I use the built-in $true variable.
Now I'm ready to update the user account's description. To do so, I call the
$user object's Put method, which takes two arguments: the property name (Description)
and the value. In this case, the value is made up of a combination of the $desc
and $date variables and the word disabled. The value takes the original
description and appends the word disabled followed by the current datetime.
After updating AD information, you must
commit the changes, so I call the $user object's
SetInfo method. I then display two messages
by using the Write-Host cmdlet. The first message says the account has been disabled. The
second message displays the account's distinguished name (DN).
The code in callout E highlights the final
part of the script, which is an Else statement
block. The Else statement runs when the If
condition evaluates to false. For this script, the
Else statement uses the Write-Host cmdlet to
display a message that says the user account
wasn't found.
That's all there is to DisableUser.ps1. When you run this script, you must
have access to the AD store. I tested this script on a computer running Windows
2003 Enterprise Edition that was configured as a domain controller (DC). I also
tested the script on an XP machine against a Windows 2000 DC.
FindShares.ps1
FindShares.ps1, which Listing 3 shows, retrieves
a list of the user-defined shares on a computer. Like DisableUser.ps1, FindShares.ps1
begins by defining a parameter. As callout A shows, the $computer parameter
passes the computer name to the script when you run it. However, the parameter
uses a default value rather than returning an error message when a parameter
isn't provided. In this case, the default value is a period, which refers to
the local computer.
The code in callout B uses the Get-WmiObject cmdlet to create a WMI object.
The cmdlet uses the -Class option to specify the Win32_ Share class, the -Namespace
option to specify the root\CIMV2 namespace, and the -ComputerName option to
specify the computer name in $computer. Of these options, the -Class option
is the most important because it determines the type of information you can
access through the WMI object.
After accessing the WMI class information, I pass it down the pipeline to a
Where-Object cmdlet. The Where-Object expression, enclosed in curly brackets,
includes three conditions. The first condition uses the not equal (-ne) operator
to compare the Caption property's value to the phrase default share.
For the condition to evaluate to true, the property's value can't equal the
phrase. The second condition uses the -notlike operator to compare the Caption
property's value to the remote* value. Notice the use of the wildcard,
which can represent any characters. For the condition to evaluate to true, the
Caption property's value can't begin with the word remote, but it can
end with any characters. The final condition is similar to the second condition,
except that the Caption property's value can't begin with the word logon.
The Where-Object expression uses the -and logical operator to link the
three conditions, which means that they all must be true for a share to be included
in the list. I pass the filtered list down the pipeline to a Sort-Object cmdlet,
which sorts the list of shares based on the Name property (by default, in ascending,
or alphabetical, order). I assign the sorted WMI information to the $shares
variable.
The code in callout C is an If statement block. The If condition ($shares -ne
$null) uses the -ne operator and the $null system variable to specify that the
$shares variable can't contain a null value. When the $shares variable's value
isn't null (i.e., the If condition evaluates to true), the If statement block
runs.
The If statement block begins with the Write-Host cmdlet. Because the cmdlet
specifies no content, it simply returns a blank line. This provides extra spacing
to better display the information in the PowerShell window.
The next statement is a ForEach statement. The ForEach statement isn't the
same as the ForEach-Object cmdlet, even though they perform the same function.
Adding to the confusion is the fact that one of the ForEachObject cmdlet's aliases
is ForEach. Here's how you can tell them apart: When ForEach is at the beginning
of a command, it's a ForEach statement. When ForEach is within a pipeline, it's
a ForEach-Object cmdlet.
The ForEach statement iterates through the objects (i.e., shares) in the $shares
collection. The statement defines the $share variable, which refers to the current
object. The ForEach expression uses the $share variable to take action on each
object. The first command in the ForEach expression is a Write-Host cmdlet that
writes the Name property's value to the PowerShell window. The script accesses
the name property through the $share variable. The second WriteHost cmdlet writes
the Path property's value to the PowerShell window. The final Write-Host cmdlet
simply adds a line after each iteration to make it easier to read the list of
shares.
When the If condition ($shares -ne $null) evaluates to false, the Else statement
block in callout D runs. The Else statement block contains its own If and Else
statement blocks. The nested If condition specifies that the computer name must
equal a period. When the computer name is a period, Write-Host uses the COMPUTERNAME
environmental variable to return the name. Note that to access an environmental
variable, you must precede the variable name with $env:. When the computer name
isn't a period, Write-Host returns the name stored in $computer.
When you run FindShares.ps1, you'll obtain a list of shares. Figure
2 shows sample output from this script.
Only Scratching the Surface
As you can see, with PowerShell, you have a lot of flexibility in the type of
information that you can access and what you can do with that information. These
three examples of how to use PowerShell only scratch the surface. The more effort
you devote to learning PowerShell, the greater your payoff will be. And who
knows, with these new skills, you might have time to complete last year's projects.