Bookmark and Share

 

(Note: These solutions were written for Advanced Event 4 of the 2010 Scripting Games.)
 

Advanced Event 4 (Windows PowerShell) 


Photo of Matt Johnson

Matt Johnson is a system administrator and founder of the Southeast Michigan PowerShell Script Club. He holds numerous certifications including MCSE and GSEC. You can find his blog at http://www.mwjcomputing.com/blog, or visit the Southeast Michigan PowerShell Script Club’s site at http://www.michiganpowershell.com. You can find him most days lurking around the Internet and trying to solve problems using Windows PowerShell.

 

When I sat down to write the solution for this event, I did what I normally do and broke it down into smaller “byte sized” pieces (nyuk nyuk). From first glance, this problem looks straightforward, and I found 4 pieces to break it into.

·         Get and add an environment variable.

·         Write information to log files.

·         Run this against remote computers.

·         Add a silent mode.


When I looked at the pieces, the easiest parts seemed to be writing the information to log files and adding the silent mode. So let’s work with the two more complicated parts.


First up, we will tackle writing and getting environment variables. The easiest way to do this is use the $env provider. However, when working on this script, it became evident that that won’t work on a remote computer, and we will need to use the [System.Environment] .NET Framework class. (You will notice this hint in the 2010 Scripting Games Study Guide.) The two methods we will concentrate on are the GetEnvironmentVariable and SetEnvironmentVariable.


To get the environment variable, you would use the command:

                [System.Environment]::GetEnvironmentVariable(Name,Type)


To set the environment variable, you would use the command:

      [System.Environment]::SetEnvironmentVariable(Name,Value,Type)


If you worked with environment variables before, you are probably used to User and System variables. However, for some reason the .NET Framework class calls system variables Machine variables. The script accepts User as well as both System and Machine as types, which is seen using the ValidateSet switch in our parameter area. (You can see more of the additional parameters by typing
Get-Help about_functions_advanced.)

[ValidateSet("User", "Machine", "System")]
      [string]$Type = "Machine",
   


Later, there are a few lines in there to convert System to Machine so that we do not have any issues when using the .NET Framework class.


With this in hand, we need to run this remotely against other computers. This is where Windows PowerShell 2.0’s Invoke-Command cmdlet comes into play. (Keep in mind that all machines should be configured to allow remoting ahead of time.) Because part of the problem is that you need to accept environment variables that the user can define and not just the default, we need Invoke-Command to accept parameters. We accomplish this by using script blocks. Here are the two script blocks I passed to Invoke-Command to perform the operations:

$getCommand = {
      param($Name, $Type)
      Invoke-Command {[System.Environment]::GetEnvironmentVariable($Name,$Type)}   
}

$setCommand = {
      param($Name, $Value, $Type)
      Invoke-Command {[System.Environment]::SetEnvironmentVariable($Name,$Value,$Type)}
}


As you can see, both script blocks accept the parameters that we need to set the information on the remote computer. Now all we need to do is run the following Invoke-Command commands so that our parameters are passed to the script blocks. The variables we pass are the ones that are defined in the main script parameter area so that we don’t lose track of the values:

Invoke-Command $setCommand -ComputerName $server -ArgumentList $Name, $Value, $Type
Invoke-Command $getCommand -ComputerName $server -ArgumentList $Name, $Type


The last part that may cause some confusion is “silent” mode. This is accomplished by adding a parameter to our script of type [Switch], as shown here.

param (
      [string]$Name = "ScriptingGuys",         
      [string]$Value = "ScriptingGuys.com",    
[ValidateSet("User", "Machine", "System")]
      [string]$Type = "Machine",
      [switch]$Silent
)


Then later in our code when we go to set the environment variable, we will check to see if $Silent is true, and act accordingly.


As you can see here, we run the script both regularly and with silent mode. I also get the content of the logs, so you can take a look at how I logged things.

Image of running script regularly and with silent mode


Here is the full script:

set-envvariable.ps1

# Script: set-envvariable.ps1
# Author: Matt Johnson - powershell@mwjcomputing.com
# Description: Microsoft 2010 Scripting Games - Advanced - Event 4
#Requires -version 2.0

param (
      [string]$Name = "ScriptingGuys",          # Name of environment variable
      [string]$Value = "ScriptingGuys.com",     # Value of environment variable
      [ValidateSet("User", "Machine", "System")]
      [string]$Type = "Machine",                      # Type of environment variable
      [switch]$Silent                                       # Silent switch
)

# Get list of workstations/servers to run against.
$serverList = Get-Content "C:\fso\data\servers.txt"

# Get current time.
$time = Get-Date

# Set logging locations
# Log of variable setting actions
$scriptLog = 'c:\fso\data\set-envvariable-' + (Get-Date -format 'yyyy-MM-dd') + '.txt'
# Log of changes when -Silent switch is used.
$silentLog = 'c:\fso\data\set-envvariable-silent-log-' + (Get-Date -format 'yyyy-MM-dd') + '.txt'

# Command to get the environment variable on the machine.
$getCommand = {
      param($Name, $Type)
      Invoke-Command {[System.Environment]::GetEnvironmentVariable($Name,$Type)}   
}

# Command to set the environment variable on the machine.
$setCommand = {
      param($Name, $Value, $Type)
      Invoke-Command {[System.Environment]::SetEnvironmentVariable($Name,$Value,$Type)}
}

# Fix type of variable if System is entered.
if ($Type -eq "System") {
      $Type = "Machine"
}    

# Loop through each server
foreach ($server in $serverList)
{    
      # Get current environment variable value
      $currentValue = Invoke-Command $getCommand -ComputerName $server -ArgumentList $Name, $Type
     
      # Check to see if variable is null
      if ($currentValue -eq $null) {
            # If variable is null, set the environment variable on the remote computer.
            try {
                  # Execute value on remote server.
                  Invoke-Command $setCommand -ComputerName $server -ArgumentList $Name, $Value, $Type
                  # Write information to log file
                  "Added - $server - $time" | Out-File -FilePath $scriptLog -Append
            } catch {
                  # Display error message to user.
                  "Error has occurred while adding value to $server."  
            }
      } else {
            # Check to see if the current value is the value that we are trying to set.
            if ($Value -ne $currentValue) {
                  # Check for silent switch
                  if (!($Silent)) {
                        # Prompt user to see if user wants to change the value.
                        $userPrompt = Read-Host -Prompt "Do you want to change the old value of '$currentValue' to $Value on $server"
                       
                        # Check to see if value is either y or n. If not, prompt again for input.
                        while (($userPrompt -notlike 'y') -and ($userPrompt -notlike 'n')) {
                              $userPrompt = Read-Host -Prompt "INVALID ENTRY - Do you want to overwrite the old value of '$currentValue' on $server"
                        }
                       
                        # If Y, Overwrite value.
                        if ($userPrompt -match "[yY]") {
                              try {
                                    # Execute value on remote server.
                                    Invoke-Command $setCommand -ComputerName $server -ArgumentList $Name, $Value, $Type
                                    # Write information to log file
                                    "Updated - $server - $time" | Out-File -FilePath $scriptLog -Append
                              } catch {
                                    # Display error message to user.
                                    "Error has occurred while updating value on $server."
                              }
                        } else{
                              # If user said no to changing the value, log to file.
                              "No Change - $server - $time - $currentValue" | Out-File -FilePath $scriptLog -Append
                        }
                       
                        # Reset user prompt.
                        $userPrompt =$null
                  } else {
                        try {
                              # Execute value on remote server.
                              Invoke-Command $setCommand -ComputerName $server -ArgumentList $Name, $Value, $Type
                              # Write information to log file
                    "Updated - $server - $time" | Out-File -FilePath $scriptLog -Append
                              # Write information to silent log file.
                              "$server - $currentValue - $Value - $time" | Out-File -FilePath $silentLog -Append
                        } catch {
                                    # Display error message to user.
                                    "Error has occurred while updating value on $server."
                        }
                  }
            } else {
                  # Log to file that environment variable value was already correct on machine.
            "Correct - $server - $time" | Out-File -FilePath $scriptLog -Append
        }
      }
}

 

Advanced Event 4 (VBScript)

Photo of Clint Huffman


Clint Huffman is a senior premier field engineer (PFE) at Microsoft who specializes in Windows performance analysis, Microsoft BizTalk Server, and Microsoft Internet Information Services. He has been with Microsoft more than 10 years going onsite with customers every week, and solving performance problems and teaching Windows architecture. Clint strives to make performance analysis less of an art form and more of a science, and he is probably best known for creating the Performance Analysis of Logs (PAL) tool, which simplifies the analysis of performance monitor logs (*.blg files). He is an author or contributor to many of the performance guides on MSDN and spoke at Tech·Ed 2008 about BizTalk Server performance analysis.

More about Clint: http://bit.ly/aboutclinthuffman.


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


I am back. It seems VBScripters are going extinct in favor of Windows PowerShell scripting. I guess I am one of the few old guys still doing this stuff. Windows PowerShell is very cool, as a matter of fact. My VBScripting skills have been getting rusty; I’ve been power coding in Windows PowerShell a lot. Therefore, this is refreshing to get back into VBScripting again for the 2010 Scripting Games. Let’s bring it!


To remotely modify computers via VBScript, it almost always means you need to use Windows Management Instrumentation (WMI). Without going into too much detail, WMI is basically the database of the operating system that allows us to make SQL-like statements to select the data in which we are interested, modify it, and then send it back. You will see this pattern many times in my script.


Going with the “Hey, Scripting Guys” theme of make it useful and reusable, I broke this script into multiple functions, each of which can be copied and reused in other scripts. Hey, that is what scripting is really about now, isn’t it? The only problem is that for simplicity’s sake, I didn’t add any error handling in it, so use this script at your own risk.


First, you point it to a text file that contains a list of computer names. For simplicity, I hard-coded the path to the text file in the script. In the GetComputerNamesFromTextFileAsDictionary() function, I read the text file and put it into a dictionary object. I did this because I only want unique values and it is easy to add new items to it, which avoids the dreaded ReDim Preserve statement.


Next, in the GetSystemEnvirommentVariables() function, I use the Win32_Environment class, use the SELECT statement to get the SYSTEM environment variables from a target computer, and place the data again into a dictionary object for easy retrieval later. We are dealing with environment variables that are name value pairs and dictionary objects are perfect for this. Now, normally when you want to modify the instances of a class, you use the method calls that the class supports. Unfortunately, in this case the Win32_Environment class does not have any methods, so what are we to do? Well, WMI supports a set of methods that all WMI classes will always have. One of them is the Put_ method. Yes, you heard right. It is Put with an underscore after it. The Put_ method allows us to save the modifications we made and “put” them back to commit our changes.


Finally, in the SetEnvironmentVariable() and UpdateEnvironmentVariable() subroutines, I call the SpawnInstance_ method (another method that all WMI classes support) and that returns a new instance of that class. Before we can Put (commit) the new instance back to the server, we have to populate the values of its required properties—in this case, Name, VariableValue, UserName, and SystemVariable must have valid values assigned to them. Again, the syntax is a bit odd, but I hope you get the idea. Yes, I originally stole…er, I mean learned this syntax from other people’s code just like everyone else.


I wrote this while in the air flying to Phoenix, so I hope this was helpful.
Here is the full script.


AdvancedEvent4.vbs

Option Explicit
Dim sSystemEnvironmentVariable, sNewValue, sFilePath

sSystemEnvironmentVariable = "ScriptingGuys"
sNewValue = "ScriptingGuys.com"
sFilePath = ".\ComputerNames.txt"

Dim dctComputerNames, dctSystemEnvVariables
Dim sComputerName, sKey

Set dctComputerNames = CreateObject("Scripting.Dictionary")
Set dctSystemEnvVariables = CreateObject("Scripting.Dictionary")

Set dctComputerNames = GetComputerNamesFromTextFileAsDictionary(sFilePath)

For Each sComputerName in dctComputerNames.Keys
    dctSystemEnvVariables.RemoveAll   
    Set dctSystemEnvVariables = GetSystemEnvirommentVariables(sComputerName)
    If dctSystemEnvVariables.Exists(sSystemEnvironmentVariable) = False Then   
        '// Add the system environment variable.
        SetEnvironmentVariable sComputerName, sSystemEnvironmentVariable, sNewValue
        WScript.Echo sComputerName & ": Environment variable created!"
    Else
        If dctSystemEnvVariables(sSystemEnvironmentVariable) <> sNewValue Then
            UpdateEnvironmentVariable sComputerName, sSystemEnvironmentVariable, sNewValue
            WScript.Echo sComputerName & ": Environment variable updated!"
        Else
            WScript.Echo sComputerName & ": Environment variable already exists."
        End If       
    End If
Next

WScript.Echo "Done!"

Sub UpdateEnvironmentVariable(sComputerName, sSysEnvVariable, sValue)
    Dim sWmiConnectionString, sWQL
    Dim oCollectionOfEnvVariables, oInstance
    Dim dctSystemEnvVariables
    Set dctSystemEnvVariables = CreateObject("Scripting.Dictionary")
    sWmiConnectionString = "winmgmts://" & sComputerName & "/root/cimv2:Win32_Environment.Name=" & chr(34) & sSysEnvVariable & chr(34) & ",UserName='<SYSTEM>'"
    Set oInstance = GetObject(sWmiConnectionString)
    oInstance.Properties_.Item("VariableValue") = sValue
    oInstance.Put_
End Sub

Sub SetEnvironmentVariable(sComputerName, sSysEnvVariable, sValue)
    Dim sWmiConnectionString
    Dim oWin32Environment, oNewInstance
    sWmiConnectionString = "winmgmts://" & sComputerName & "/root/cimv2:Win32_Environment"
    Set oWin32Environment = GetObject(sWmiConnectionString)
    Set oNewInstance = oWin32Environment.SpawnInstance_
    oNewInstance.Properties_.Item("Name") = sSysEnvVariable
    oNewInstance.Properties_.Item("VariableValue") = sValue
    oNewInstance.Properties_.Item("UserName") = "<SYSTEM>"
    oNewInstance.Properties_.Item("SystemVariable") = True
    oNewInstance.Put_
End Sub

Function GetSystemEnvirommentVariables(sComputerName)
    Dim sWmiConnectionString, sWQL
    Dim oCollectionOfEnvVariables, oInstance
    Dim dctSystemEnvVariables
    Set dctSystemEnvVariables = CreateObject("Scripting.Dictionary")
    sWmiConnectionString = "winmgmts://" & sComputerName & "/root/cimv2"
    sWQL = "SELECT Name, VariableValue FROM Win32_Environment WHERE UserName = '<SYSTEM>'"
    Set oCollectionOfEnvVariables = GetObject(sWmiConnectionString).ExecQuery(sWQL)
    For Each oInstance in oCollectionOfEnvVariables
        dctSystemEnvVariables.Add oInstance.Name, oInstance.VariableValue
    Next
    Set GetSystemEnvirommentVariables = dctSystemEnvVariables
End Function

Function GetComputerNamesFromTextFileAsDictionary(sFilePathToComputerNames)
    Dim dctComputerNames '// A dictionary object is very good as a list of unique items.
    Dim oFSO, oFile, sLine
    Const ForReading = 1, ForWriting = 2, ForAppending = 8
    Set dctComputerNames = CreateObject("Scripting.Dictionary")
    Set oFSO = CreateObject("Scripting.FileSystemObject")
    Set oFile = oFSO.OpenTextFile(sFilePathToComputerNames,ForReading)
    Do Until oFile.AtEndOfStream = True
        sLine = oFile.Readline
        sLine = Trim(sLine) '// Trim beginning and ending spaces from the line of text.
        '// If the computer name doesn't exist in the dictionary yet, add it as a key.
        '// This operation removes duplicate entries.
        '// Dictionary objects require a value for each key. Each key must be unique.
        If dctComputerNames.Exists(sLine) = False Then
            dctComputerNames.Add sLine,""
        End If
    Loop
    Set GetComputerNamesFromTextFileAsDictionary = dctComputerNames
End Function

  


If you want to know exactly what we will be looking at tomorrow, follow us on Twitter or Facebook. If you have any questions, send e-mail to us at scripter@microsoft.com or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
 

Ed Wilson and Craig Liebendorfer, Scripting Guys