Thursday, July 18, 2013

Add Users to AD Group Using First Initial

Today at work we had a need to add users to specific Active Directory groups based on the first letter of the first name. We'll be using a plain text file as an example for the list of Active Directory identities. Save the following text into a file called names.txt:
John.Doe
Jane.Doe
Jane.Smith
Then you will need to run this in the PowerShell ISE.
Import-Module ActiveDirectory

# Each row of the text file will be consider one object. The object being the Active Directory identity (SamAccountName).
$names = Get-Content c:\names.txt

# Set this to $false when you are ready to make the changes.
$whatIf = $true

foreach ($name in $names) {
    $user = Get-ADUser -Filter { SamAccountName -eq $name } -Properties MemberOf
    if($user) {
        # The groups object will contain a list of Active Directory groups by their distinguished name. 
        # (i.e. CN=GroupName_A-C,OU=Groups,OU=Company,DC=pri,DC=company,DC=com)
        $groups = $user | Select-Object -ExpandProperty MemberOf
        if(-not ($groups -like 'CN=GroupName_*')) {
            $groupName = ''
            switch -Regex($user.SamAccountName[0]) {
                # Match the first letter as a, b, or c.
                "[a-c]" { $groupName = 'GroupName_A-C' }
                # Match the first letter as d, e, f, or g.
                "[d-g]" { $groupName = 'GroupName_D-G' }
                "[h-k]" { $groupName = 'GroupName_H-K' }
                "[l-q]" { $groupName = 'GroupName_L-Q' }
                "[r-t]" { $groupName = 'GroupName_R-T' }
                "[u-z]" { $groupName = 'GroupName_U-Z' }
            }

            if($groupName) {
                "Adding $($user.SamAccountName) to the group $($groupName)"
                Add-ADGroupMember -Identity $groupName -Members $user.SamAccountName -WhatIf:$whatIf
            }
        } else {
            "Skipping $($user.SamAccountName) because they are already in the group $($groupName)"
        }
    } else {
        "$($name) does not exist"
    }
}

Monday, July 8, 2013

PoweShell Script Module

I put together a PowerShell Script Module some time ago and thought I would make it available for others. Hope it helps give you some ideas on creating your own. Click for more details.

Thursday, June 27, 2013

CSharp String to SQL Table

I have a problem at work in which I need to convert a comma separated string into a SQL temp table. You can easily add this to a function.
DECLARE @LoginNames VARCHAR(max)
SET @LoginNames = 'Michael,Rebecca'

-- Create a temp table with a single column called LoginName
DECLARE @temp AS TABLE (LoginName NVARCHAR(255))

IF ISNULL(@LoginNames, '') <> ''
BEGIN
    DECLARE @s NVARCHAR(max)
    WHILE LEN(@LoginNames) > 0
    BEGIN
        IF CHARINDEX(',', @LoginNames) > 0
        BEGIN
            SET @s = LTRIM(RTRIM(SUBSTRING(@LoginNames, 1, CHARINDEX(',', @LoginNames) - 1)))
            -- After parsing a single value from the list, insert into the temp table
     INSERT INTO @temp (LoginName) VALUES (@s)
            SET @LoginNames = SUBSTRING(@LoginNames, CHARINDEX(',', @LoginNames) + 1, LEN(@LoginNames))
        END ELSE
        BEGIN
            SET @s = LTRIM(RTRIM(@LoginNames))
            -- After parsing a single value from the list, insert into the temp table
     INSERT INTO @temp (LoginName) VALUES (@s)
            SET @LoginNames= ''
        END
    END
END

SELECT LoginName FROM @temp

Output:
LoginName
Michael
Rebecca

Tuesday, May 7, 2013

Scripting Games 2013 Advanced Event 2 - My Submission

If you have not already, I highly recommend you work on the events for the Scripting Games. I feel like I'm learning so much within a few short days mainly due to the fact that I have very specific requirements outlined by the event, as well as knowing that tons of people will potentially see my submissions. Today I'll be talking about it during the Lunch-n-Learn I host at work, so seeing constructive and accurate criticism is welcomed (username michaellwest). Here is what I have submitted, please let me know your thoughts on how I can improve the script.
  • The begin scriptblock contains a hashtable of settings to use for the function. The keys represent the Cim class name and the values represent the properties to return. You can use strings, hashtables, and scriptblocks.
  • Use CimSessionOption with the Dcom protocol to more reliably query Windows Server 2000-2008.
  • The nested foreach loops are not that great, however the keys and properties are few so the performance is still fine. Get-CimInstance is what takes a long time.
EDIT: I made an adjustment based on a post from Mike Robbins regarding the Physical memory.
function Get-ServerInventory {
    <#
        .SYNOPSIS
            Performs a hardware inventory on the specified server(s).
 
        .DESCRIPTION
            The values returned by the inventory process may be enhanced by adding to the settings hashtable in the begin scriptblock.
            The settings key is the class name. The settings values supported include string, hashtable, and scriptblock.
 
        .PARAMETER ComputerName
            Indicates the server(s) to perform a hardware inventory. The default value is localhost.
 
        .EXAMPLE
            Perform an inventory on localhost.
 
            PS C:\> Get-ServerInventory
 
            TotalPhysicalMemory : 21356912640
            ProcessorName       : Intel(R) Core(TM) i7-2600 CPU @ 3.40GHz
            Version             : 6.1.7601
            SerialNumber        : 00371-OEM-8992671-00008
            ComputerName        : WIN7DEV01
            Cores               : 4
            Sockets             : 1
 
        .EXAMPLE
            Perform an inventory on 1-N servers with an array or using Get-Content.
 
            PS C:\> "WIN2K01","WIN2K02","WIN2008R201" | Get-ServerInventory | Format-Table -AutoSize
 
            TotalPhysicalMemory ProcessorName                            Version  SerialNumber            ComputerName Cores Sockets
            ------------------- -------------                            -------  ------------            ------------ ----- -------
                     4294148096 Intel(R) Xeon(R) CPU E5-2680 0 @ 2.70GHz 5.2.3790 69712-640-5906017-45214 WIN2K01          1       1
                     2146861056 Intel(R) Xeon(R) CPU E5-2670 0 @ 2.60GHz 5.2.3790 69712-641-5611134-45717 WIN2K02          1       1
                     4294500352 Intel(R) Xeon(R) CPU E5-2680 0 @ 2.70GHz 6.1.7601 55041-266-0135507-84842 WIN2008R201      1       1
 
        .LINK            
            Windows Server 2003 incorrectly reports the number of physical multicore processors or hyperthreading-enabled processors. Apply the below hotfix to correct the reported issue. 
            http://support.microsoft.com/kb/932370
 
        .LINK
            Example on retrieving the CPU count. 
            http://www.sql-server-pro.com/physical-cpu-count.html
    #>
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline=$true)]
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName=$env:COMPUTERNAME
    )
 
    begin {
        $settings = @{
                "Win32_OperatingSystem" = @("Version","SerialNumber")
                "Win32_ComputerSystem" = @({param($result,$output) $output["Capacity"] = $result | Measure-Object -Property Capacity -Sum | Select-Object -ExpandProperty Sum})
                "Win32_Processor" = @(@{n="ProcessorName";e={$_.Name}}, {
                            param($result,$output)
                            $processors = @($result)
                            if ($processors[0].NumberOfCores) {
                                $output["Cores"] = $processors.Count * $processors[0].NumberOfCores
                            } else {
                                $output["Cores"] = $processors.Count
                            }
                            $output["Sockets"] = @($processors | Where-Object {$_.SocketDesignation} | Select-Object -Unique).Count
                        })
        }
    }
 
    process {
        $sessions = $ComputerName | Select-Object @{n="ComputerName";e={$_}} | 
            New-CimSession -SessionOption (New-CimSessionOption -Protocol Dcom)
 
        foreach($session in $sessions) {
            $output = @{}
            foreach($key in $settings.Keys) {
                $result = Get-CimInstance -CimSession $session -ClassName $key
                $output["ComputerName"] = $result.PSComputerName
                foreach($property in $settings[$key]) {
                    if($property -is [string]) {
                        $output[$property] = $result.$property
                    } elseif ($property -is [scriptblock]) {
                        Invoke-Command -ScriptBlock $property -ArgumentList $result, $output
                    } elseif ($property -is [hashtable]) {
                        ($result | Select-Object -Property $property).PSObject.Properties | ForEach-Object {$output[$_.Name] = $_.Value }
                    }
                }
            }
 
            [PSCustomObject]$output
 
            Remove-CimSession -CimSession $session
        }
    }
}

Friday, April 26, 2013

Embedding Csharp in PowerShell Script

I wanted to use a "using block" found in C# to dispose of objects in PowerShell such as Streams or other object types that require the calling of Dispose. After seeing different examples here is where I stopped. I made some minor tweaks.
  1. Begin by creating a class in C#. You can also save the code in a separate file such as Code.cs. See help Add-Type -Examples for additional examples. The class must also inherit from System.IDisposable, otherwise the using-block function will complain with something like 'using-block : Cannot process argument transformation on parameter 'InputObject'. Cannot convert the "Code" value of type "Code" to type "System.IDisposable".'
  2. Create your using block with the instantiation of a new object in parentheses.
  3. Finally, in the script block place your needed logic.

Add-Type @"
using System;

public class Code : IDisposable
{
    public Code()
    {
        Name = "Michael";
    }
    
    public string Name { get; set; }

    public bool IsDisposed { get; set; }
    public void Dispose() 
    {
        Dispose(true);
        GC.SuppressFinalize(this);      
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!IsDisposed)
        {
            IsDisposed = true;   
        }
    }
}
"@

function using-block {
    param (
        [System.IDisposable]$InputObject = $(throw "The parameter -inputObject is required."),
        [ScriptBlock]$ScriptBlock = $(throw "The parameter -scriptBlock is required.")
    )
    try { & $ScriptBlock }
    finally {
        if ($InputObject) {
            if ($InputObject.PSBase) {
                $InputObject.PSBase.Dispose()
            } else {
                $InputObject.Dispose()
            }
        }
    }
}

using-block($c = New-Object Code) {
    $name = $c.Name
    $name # Michael
    $c.IsDisposed # False
}
$name # Not in this scope so no value
$c.IsDisposed # True
This may not be the most elegant approach but it served it's purpose.

Sunday, April 7, 2013

PowerShell Module In Session

I found it a little difficult to bring in functions into a session so I thought this would help others with what helps me get the job done.
The video goes from creating a script module to importing that module into a session.

Tuesday, April 2, 2013

Run with PowerShell Context Menu

Today I was working on our automated build process at work, and found myself running a batch file in a console window that I wanted to remain open. I setup a context menu item associated with .bat files which launches with PowerShell.
Here are the steps:
# We need to create our keys under HKEY_CLASSES_ROOT, which by default has not associated PSDrive.
New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT

# Set the current location to the new drive.
cd HKCR:

# Set the current location to the shell key for .bat files.
cd '.\batfile\shell'

# Create a new key called "Run with PowerShell"
New-Item -Path 'Run with PowerShell'

# Set the current location to the new key.
cd '.\Run with PowerShell'

# Create a new key called "command", which will contain the reference to PowerShell.
New-Item -Path 'command'
cd '.\command'

# Create a new "(default)" string with the command to execute PowerShell. The "%1" contains the path to the .bat file. 
New-ItemProperty -Path '.' -Name '(default)' -Value 'C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe "-nologo" "-noexit" "-command" "& {%1}"'

Here is an example of the output:
PS C:\> New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT

Name           Used (GB)     Free (GB) Provider      Root                  CurrentLocation
----           ---------     --------- --------      ----                  ---------------
HKCR                                   Registry      HKEY_CLASSES_ROOT

PS C:\> cd HKCR:\batfile\shell
PS HKCR:\batfile\shell> New-Item -Path 'Run with PowerShell'

    Hive: HKEY_CLASSES_ROOT\batfile\shell

Name                           Property
----                           --------
Run with PowerShell

PS HKCR:\batfile\shell> cd '.\Run with PowerShell'
PS HKCR:\batfile\shell\Run with PowerShell> New-Item -Path 'command'

    Hive: HKEY_CLASSES_ROOT\batfile\shell\Run with PowerShell

Name                           Property
----                           --------
command

PS HKCR:\batfile\shell\Run with PowerShell> cd '.\command'
PS HKCR:\batfile\shell\Run with PowerShell\command> New-ItemProperty -Path '.' -Name '(default)' -Value 'C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe "-nologo" "-noexit" "-command" "& {%1}"'

(default)    : C:\WINDOWS\SysWow64\WindowsPowerShell\v1.0\powershell.exe "-nologo" "-noexit" "-command" "& {%1}"
PSPath       : Microsoft.PowerShell.Core\Registry::HKEY_CLASSES_ROOT\batfile\shell\Run with PowerShell\command
PSParentPath : Microsoft.PowerShell.Core\Registry::HKEY_CLASSES_ROOT\batfile\shell\Run with PowerShell
PSChildName  : command
PSDrive      : HKCR
PSProvider   : Microsoft.PowerShell.Core\Registry