PowerShell

Add a Domain User to the Local Administrators Group

When building out a workstation for an AD Domain user, in some environments the user is added to the local Administrators group to allow the user to install and configure applications. Now there are some of us who think that's a Bad Idea and a Security Risk, but the reality is that it's policy in some organizations. Doing this with the GUI is easy, but who wants to have to use the GUI for anything? Especially for a highly repetitive task that you're going to have to do on every user's workstation. So, let's use PowerShell and [ADSI] to do the heavy lifting.

The first step is to define the target we want to add the user to:

$ComputerName = "workstation01"
$Group = "Administrators"
$target=[ADSI]"WinNT://$ComputerName/$Group,group"

Next, we invoke the Add method on that target to add the user to the group.

$Domain = 'TreyResearch'
$UserName = 'Charlie.Russel'
$target.psbase.Invoke("Add",([ADSI]"WinNT://$Domain/$UserName").path)

And that's really all there is to it.

(Note, by the way, that this is one of the only places in PowerShell where CASE MATTERS. the WinNT commands are case sensitive so don't change that to winnt or WINNT. It won't work. )

Finally, let's pull all that together into a script that accepts the user name, the target computer, and the AD Domain as parameters:

<#
.Synopsis
Adds a user to the Local Administrators group
.Description
Add-myLocalAdmin adds a user to the local Administrators group on a computer. 
.Example
Add-myLocalAdmin Charlie.Russel 
Adds the TreyResearch user Charlie.Russel to the Administrators local group on the localhost.
.Example
Add-myLocalAdmin Charlie.Russel -ComputerName ws-crussel-01
Adds the TreyResearch user Charlie.Russel to the Administrators local group on ws-crussel-01.
.Example
Add-myLocalAdmin -UserName Charlie.Russel -ComputerName ws-crussel-01 -Domain Contoso
Adds the Contoso user Charlie.Russel to the Administrators local group on ws-crussel-01.
.Parameter UserName
The username to add to the Administrators local group. This should be in the format first.last. 
.Parameter ComputerName
[Optional] The computer on which to modify the Administrators group. The default is localhost
.Parameter Domain
[Optional] The user's Active Directory Domain. The default is TreyResearch.
.Inputs
[string]
[string]
[string]
.Notes
    Author: Charlie Russel
 Copyright: 2017 by Charlie Russel
          : Permission to use is granted but attribution is appreciated
   Initial: 21 June, 2017 (cpr)
   ModHist:
          :
#>
[CmdletBinding()]
Param(
     [Parameter(Mandatory=$True,Position=0)]
     [alias("user","name")]
     [string]
     $UserName,
     [Parameter(Mandatory=$False,Position=1)]
     [string]
     $ComputerName = 'localhost',
     [Parameter(Mandatory=$False)]
     [string]
     $Domain = 'TreyResearch'
     )

$Group = 'Administrators'

# Please be warned. The syntax of [ADSI] is CASE SENSITIVE!
$target=[ADSI]"WinNT://$ComputerName/$Group,group"
$target.psbase.Invoke("Add",([ADSI]"WinNT://$Domain/$UserName").path)

 

Copying AD User Group Permissions with PowerShell

One of the tasks that I'm often asked to perform as an Active Directory domain administrator is to assign a user the same set of permissions as an existing user. This is something you can do fairly easily in the GUI (Active Directory Users and Computers, dsa.msc) when you're first creating the user, but which is a pain if the target user already exists. Turns out PowerShell can help with this, of course.

First, you need to get the list of groups that the template or source user ($TemplateUser) is a member of. That's fairly simple:

$UserGroups =@()
$UserGroups = (Get-ADUser -Identity $TemplateUser -Properties MemberOf).MemberOf

A couple of important points in the above:

  • First, you should create the empty array first. That tells PowerShell that you're going to be creating a list of groups, not a single one. You can often get away without doing this at the command line because of PowerShell's command line magic, but in a script, you need to be explicit.
  • Second, you need to include the MemberOf property in the Get-ADUser query. By default, that isn't returned and you'll end up with an empty $UserGroups variable.

So, you've got a list of groups. If you're just doing an "additive" group membership change, all you need to do is add the target user ($TargetUser) to the all the groups. However, if you want to exactly match the group memberships, you need to first remove the target user from any groups s/he is part of before adding groups back. To do that, we need to first find out what groups the target user is currently in with much the same command as above:

$CurrentGroups = @()
$CurrentGroups = (Get-ADUser -Identity $TargetUser -Properties MemberOf).MemberOf

Now, we can remove the user from all current groups with:

foreach ($Group in $CurrentGroups) {
    Remove-ADGroupMember -Identity $Group -Members $TargetUser
}

Notice in the above that -Identity is the identity of the group, not the user. This is because we're acting on the groups, not acting on the user(s).

Finally, we can now add $TargetUser back in to the groups that $TemplateUser had with:

foreach ($Group in $UserGroups) {
    Add-ADGroupMember -Identity $Group -Members $TargetUser
}

All of this, of course, happens quietly with no confirmation. So, just to verify that everything went as expected, use:

(Get-ADUser -Identity $TargetUser -Properties MemberOf).MemberOf

And you should get back a list of user groups the target user is now a member of.

Note: If you're including this code in a new user script, you won't need to remove the user from current groups, merely add them to the same groups as the template user.

PowerShell Syntax Highlighting

With Windows 10 / Server 2016, PowerShell got command-line syntax highlighting. And what a difference that makes! With syntax highlighting, it's easier to see a mistyped line of code as you make the mistake. Combined with intelligent tab-completion, my errors/command ratio is way, way down.

 

Many of the same advantages accrued to readers of this blog -- by having syntax highlighting turned on, it was easier to follow the logic of the scripts and commands I posted here. Unfortunately, the tool we were using for that has 'issues', and hasn't been actively developed or updated in several years. Reluctantly, the overall Site Admins for msmvps.com (who are GODDESSES and loved beyond belief!) have had to remove that plugin. It just had too many problems and with no active development, little chance they'd get fixed.  The result? All the code on this site is now plain text ugly. :(

 

ETA: Fixed. Mivhak to the rescue. It's not perfect, but a huge improvement over nothing. And it looks like it's easily customizable, so I might do some tweaking to improve contrast without going to a dark theme.

 

But wait! There's hope! We think we've identified a solution that we can use, and that is still being actively maintained. It supports PowerShell, and it supports WordPress Multi-site. Both absolute requirements. The plug in is being actively tested while they continue the work of cleaning up the mess that the previous plugin created. Unfortunately, however, it will require me to go in and edit every single post that includes PowerShell code to enable the new plugin for that code. That process will take time. I'll start with some of the most recent, and the most popular, and slowly work my way through them.

 

ETA: Whew! Mivhak is smart enough to recognize all my posts that had PRE tags and automatically syntax highlight them as PowerShell. That saves a BUNCH of work.

 

Until we have a confirmed solution, and I've had time to go in and edit each post, I'm afraid you'll have to do it the hard way. Copy and paste the code into the syntax highlighting editor of your choice. And, speaking of which, have you tried Visual Studio Code? This is a slick, new, FREE, editor that supports easy customization, has full IntelliSense support(!!), and even has a plugin to enable Vi mode editing! How cool is that?! I've been playing around with it a lot lately, and I'm almost ready to switch from my beloved gVim.

Guest Post — Get-myFreeSpace Revisited

Today's post comes by way of a co-worker, Robert Carlson, who took my previous post on getting the free disk space of remote computers and offered a very useful suggestion -- instead of outputting strings, which is only useful for a display or report, he suggests creating a PSCustomObject and outputting that. Slick! I like it.

 

So, why a PSCustomObject? Because now he can use it to drive automation, rather than simply reporting. A very handy change, and a good reminder for all of us that we should put off formatting until the last possible moment, because once you pipe something to Format-*, you're done. All your precious objects and their properties are gone, and you're left with a simple string.

 

The other thing Robert has done is change this from a script to a function. This makes it easier to call from other scripts and allows it to be added to your "toolbox" module. (More on Toolbox Modules soon. )  A worthy change. So, without further ado, here's Robert's revised Get-myFreeSpace function.

function Get-myFreeSpace {
<#
.Synopsis
Gets the disk utilization of one or more computers
 
.Description
Get-myFreeSpace queries an array of remote computers and returns a nicely formatted display of 
their current disk utilization and free space. The output can be redirected to a file or other 
output option using standard redirection, or can be piped to further commands.

.Parameter ComputerName
An array of computer names from which you want the disk utilization

.Example
(Get-VM -Name “*server*” | Where-Object {$_.State -eq ‘Running’).Name } | Get-myFreeSpace
Gets the free disk space of the running virtual machines whose name includes 'server'

.Inputs
[string[]]

.Notes
 Original Author: Charlie Russel
Secondary Author: Robert Carlson
Copyright: 2017 by Charlie Russel
         : Permission to use is granted but attribution is appreciated
  Initial: 26 Nov, 2014 (cpr)
  ModHist: 29 Sep, 2016 — Changed default to array of localhost (cpr)
         : 18 Apr, 2017 — Changed to use Write-Output,accept Pipeline,added man page, (cpr)
         : 20 Apr, 2017 — Changed output to pscustomobject rather than string, etc.(RC)
#>

[CmdletBinding()]
Param(
[Parameter(Mandatory=$False,Position=0,`
           ValueFromPipeline=$True,`
           ValueFromPipelineByPropertyName=$True,`
           ValueFromRemainingArguments=$True)]
           [alias(“Name”,”Computer”)]
           [string[]]
           $ComputerName = @(“localhost”)
           )

Begin {
   if ($Input) {
      $ComputerName = @($Input) 
   }
}

Process {
   ForEach ( $Computer in $ComputerName ) {
      $volumes = Get-WmiObject -ComputerName $Computer -Class Win32_Volume -ErrorAction SilentlyContinue 
      foreach ($volume in $volumes) {
         $volumeData = [pscustomobject]@{
            ComputerName=$Computer 
            Drive=$volume.DriveLetter
            VolumeLabel=$volume.Label
            VolumeSize=”{0:N0}” -f ($volume.Capacity / 1GB)
            FreeSpace=”{0:N0}” -f ($volume.FreeSpace/1GB)
            }
         if ($volume.Capacity) {
            $percentage = “{0:P0}” -f ($volume.FreeSpace / $volume.Capacity)
            $volumeData | Add-Member -NotePropertyName “PercentageFree” -NotePropertyValue $percentage
         } else {
            $volumeData | Add-Member -NotePropertyName “PercentageFree” -NotePropertyValue “n/a”
         }
         Write-Output $volumeData
      }
   }
 }
}

I really appreciate Robert's contribution, and I thank him profoundly for his suggestion. I learned something, and I hope you have too.  I hope you found this useful, and I'd love to hear comments, suggestions for improvements, or bug reports as appropriate. As always, if you use this script as the basis for your own work, please respect my copyright and provide appropriate attribution.

Resizing the PowerShell Console

Windows 10's support for high DPI displays is much better than previous iterations of Windows, but there are still some times it gets a bit confused. One such problem occurs when you have multiple high DPI displays or two displays of different sizes. If you move PowerShell console windows between displays or log back in after being logged out for a while, you can end up with a scrunched up PowerShell window. Nothing I had to deal with when all I had was a pair of standard FullHD monitors, but ever since I got my Surface Book, and connected it to a 28 inch 4k monitor, I've had periodic problems. Very annoying when your PowerShell window changes to 37 characters wide and 7 lines long!

 

The fix is to reset the window size. Now I can do this graphically (right click on the title bar, select Properties, and then the Layout tab), but that's a nuisance at best, and besides, the whole idea of using the GUI to fix a console just isn't right. The answer is to leverage the built-in $host variable:

$host | Get-Member

   TypeName: System.Management.Automation.Internal.Host.InternalHost

Name                   MemberType Definition
----                   ---------- ----------
EnterNestedPrompt      Method     void EnterNestedPrompt()
Equals                 Method     bool Equals(System.Object obj)
ExitNestedPrompt       Method     void ExitNestedPrompt()
GetHashCode            Method     int GetHashCode()
GetType                Method     type GetType()
NotifyBeginApplication Method     void NotifyBeginApplication()
NotifyEndApplication   Method     void NotifyEndApplication()
PopRunspace            Method     void PopRunspace(), void IHostSupportsInteractiveSession.PopRunspace()
PushRunspace           Method     void PushRunspace(runspace runspace), void IHostSupportsInteractiveSession.PushRunspace(runspace runspace)
SetShouldExit          Method     void SetShouldExit(int exitCode)
ToString               Method     string ToString()
CurrentCulture         Property   cultureinfo CurrentCulture {get;}
CurrentUICulture       Property   cultureinfo CurrentUICulture {get;}
DebuggerEnabled        Property   bool DebuggerEnabled {get;set;}
InstanceId             Property   guid InstanceId {get;}
IsRunspacePushed       Property   bool IsRunspacePushed {get;}
Name                   Property   string Name {get;}
PrivateData            Property   psobject PrivateData {get;}
Runspace               Property   runspace Runspace {get;}
UI                     Property   System.Management.Automation.Host.PSHostUserInterface UI {get;}
Version                Property   version Version {get;}
  

OK, there's some interesting bits there, but the one that looks most promising is UI. So:

 $host.UI | Get-Member


   TypeName: System.Management.Automation.Internal.Host.InternalHostUserInterface

Name                    MemberType Definition
----                    ---------- ----------
Equals                  Method     bool Equals(System.Object obj)
GetHashCode             Method     int GetHashCode()
GetType                 Method     type GetType()
Prompt                  Method     System.Collections.Generic.Dictionary[string,psobject] Prompt(string caption, string message, System.Collection...
PromptForChoice         Method     int PromptForChoice(string caption, string message, System.Collections.ObjectModel.Collection[System.Management...
PromptForCredential     Method     pscredential PromptForCredential(string caption, string message, string userName, string targetName), pscredent...
ReadLine                Method     string ReadLine()
ReadLineAsSecureString  Method     securestring ReadLineAsSecureString()
ToString                Method     string ToString()
Write                   Method     void Write(string value), void Write(System.ConsoleColor foregroundColor, System.ConsoleColor backgroundColor, ...
WriteDebugLine          Method     void WriteDebugLine(string message)
WriteErrorLine          Method     void WriteErrorLine(string value)
WriteInformation        Method     void WriteInformation(System.Management.Automation.InformationRecord record)
WriteLine               Method     void WriteLine(), void WriteLine(string value), void WriteLine(System.ConsoleColor foregroundColor, System.Cons...
WriteProgress           Method     void WriteProgress(long sourceId, System.Management.Automation.ProgressRecord record)
WriteVerboseLine        Method     void WriteVerboseLine(string message)
WriteWarningLine        Method     void WriteWarningLine(string message)
RawUI                   Property   System.Management.Automation.Host.PSHostRawUserInterface RawUI {get;}
SupportsVirtualTerminal Property   bool SupportsVirtualTerminal {get;}
  

Hmmm. Even more interesting stuff. I can tell I'm going to be doing some poking around in here! But, for our purposes, let's take a look at RawUI.

That looks the most promising:

$host.UI.RawUI | Get-Member


   TypeName: System.Management.Automation.Internal.Host.InternalHostRawUserInterface

Name                  MemberType Definition
----                  ---------- ----------
Equals                Method     bool Equals(System.Object obj)
FlushInputBuffer      Method     void FlushInputBuffer()
GetBufferContents     Method     System.Management.Automation.Host.BufferCell[,] GetBufferContents(System.Management.Automation.Host.Rectangle r)
GetHashCode           Method     int GetHashCode()
GetType               Method     type GetType()
LengthInBufferCells   Method     int LengthInBufferCells(string str), int LengthInBufferCells(string str, int offset), int LengthInBufferCells(cha...
NewBufferCellArray    Method     System.Management.Automation.Host.BufferCell[,] NewBufferCellArray(string[] contents, System.ConsoleColor foregro...
ReadKey               Method     System.Management.Automation.Host.KeyInfo ReadKey(System.Management.Automation.Host.ReadKeyOptions options), Syst...
ScrollBufferContents  Method     void ScrollBufferContents(System.Management.Automation.Host.Rectangle source, System.Management.Automation.Host.C...
SetBufferContents     Method     void SetBufferContents(System.Management.Automation.Host.Coordinates origin, System.Management.Automation.Host.Bu...
ToString              Method     string ToString()
BackgroundColor       Property   System.ConsoleColor BackgroundColor {get;set;}
BufferSize            Property   System.Management.Automation.Host.Size BufferSize {get;set;}
CursorPosition        Property   System.Management.Automation.Host.Coordinates CursorPosition {get;set;}
CursorSize            Property   int CursorSize {get;set;}
ForegroundColor       Property   System.ConsoleColor ForegroundColor {get;set;}
KeyAvailable          Property   bool KeyAvailable {get;}
MaxPhysicalWindowSize Property   System.Management.Automation.Host.Size MaxPhysicalWindowSize {get;}
MaxWindowSize         Property   System.Management.Automation.Host.Size MaxWindowSize {get;}
WindowPosition        Property   System.Management.Automation.Host.Coordinates WindowPosition {get;set;}
WindowSize            Property   System.Management.Automation.Host.Size WindowSize {get;set;}
WindowTitle           Property   string WindowTitle {get;set;}
  

BINGO! I see BufferSize and WindowSize, and I know from the GUI Properties page that those are the relevant settings, but just to verify:

$host.UI.RawUI.BufferSize | Get-Member


   TypeName: System.Management.Automation.Host.Size

Name        MemberType Definition
----        ---------- ----------
Equals      Method     bool Equals(System.Object obj)
GetHashCode Method     int GetHashCode()
GetType     Method     type GetType()
ToString    Method     string ToString()
Height      Property   int Height {get;set;}
Width       Property   int Width {get;set;}


$host.UI.RawUI.WindowSize | Get-Member


   TypeName: System.Management.Automation.Host.Size

Name        MemberType Definition
----        ---------- ----------
Equals      Method     bool Equals(System.Object obj)
GetHashCode Method     int GetHashCode()
GetType     Method     type GetType()
ToString    Method     string ToString()
Height      Property   int Height {get;set;}
Width       Property   int Width {get;set;}
  

And there we have it.  Both of them can be retrieved and set.  So, I came up with a little script, Set-myConSize, that lets me restore the window to its default size, or set it to a new size if I'm doing something that needs a bit of window size tweaking.

<#
.Synopsis
Resets the size of the current console window
.Description
Set-myConSize resets the size of the current console window. By default, it
sets the windows to a height of 40 lines, with a 3000 line buffer, and sets the 
the width and width buffer to 120 characters. 
.Example
Set-myConSize
Restores the console window to 120x40
.Example
Set-myConSize -Height 30 -Width 180
Changes the current console to a height of 30 lines and a width of 180 characters. 
.Parameter Height
The number of lines to which to set the current console. The default is 40 lines. 
.Parameter Width
The number of characters to which to set the current console. Default is 120. Also sets the buffer to the same value
.Inputs
[int]
[int]
.Notes
    Author: Charlie Russel
 Copyright: 2017 by Charlie Russel
          : Permission to use is granted but attribution is appreciated
   Initial: 28 April, 2017 (cpr)
   ModHist:
          :
#>
[CmdletBinding()]
Param(
     [Parameter(Mandatory=$False,Position=0)]
     [int]
     $Height = 40,
     [Parameter(Mandatory=$False,Position=1)]
     [int]
     $Width = 120
     )
$Console = $host.ui.rawui
$Buffer  = $Console.BufferSize
$ConSize = $Console.WindowSize

# If the Buffer is wider than the new console setting, first reduce the buffer, then do the resize
If ($Buffer.Width -gt $Width ) {
   $ConSize.Width = $Width
   $Console.WindowSize = $ConSize
}
$Buffer.Width = $Width
$ConSize.Width = $Width
$Buffer.Height = 3000
$Console.BufferSize = $Buffer
$ConSize = $Console.WindowSize
$ConSize.Width = $Width
$ConSize.Height = $Height
$Console.WindowSize = $ConSize
  

One quick comment on this script -- you can't set the BufferSize to smaller than the current WindowSize. With a Height buffer set to 3,000, that's not likely to be a problem, but if you don't want scroll bars on the bottom of your console windows (and you do NOT, trust me!), then you need the console WindowSize.Width to be the same as the BufferSize.Width. So if your reducing, you need to change the WindowSize first, then you can reduce the BufferSize. If you're increasing width, you need to do the buffer first.

 

Finally, I set an alias in my $Profile:

Set-Alias -Name Resize -Value Set-myConSize

 

Getting the Free Disk Space of Remote Computers Revisited

Several years ago, I wrote a fairly simplistic script to get the free disk space of remote computers. It wasn't all that sophisticated, but it got the job that I needed done, so I shared it here on my blog, since I thought others might find it useful. Which, based on the number of hits here, and the comments, they did. However, based on some of those comments, it had a problem for some users.

 

The problem was that I used Write-Host in it. That was fine for me, because I only used it to write to my screen. But it's a bad practice to be using Write-Host unless you really need to manipulate screen colours. The reason it's a bad practice is that it prevents any sort of redirection! This meant that those users who wanted to capture the result of the script in a file were horked, because Write-Host will ALWAYS write to ( ... wait for it...  )

 

The Host. You can't redirect it. The fix, of course, is easy -- use Write-Object instead, which is what I should have done in the first place.

 

While I was in the process of making that change, I thought it would be nice to add in a basic Get-Help page for it, which was trivial. But then it occurred to me that I really should let it handle pipeline input, allowing me to use other PowerShell commands to select the group of machines I wanted the free disk space on, and then pipe that result directly to Get-myFreeSpace.

 

Seemed like a good idea, but it turned out I had to almost completely rewrite the script to use the Begin{}/Process{}/End{} syntax. Accepting pipeline input is not as simple as just saying you do in the Parameter statement, you need to actually process that input. The result is the new, improved version of Get-myFreeSpace.ps1 shown below. (If you care about how I got to this script in the first place, do check out the original post, here. There's some useful information there about the whole process. )

 

<#
.Synopsis
Gets the disk utilization of one or more computers

.Description
Get-myFreeSpace queries an array of remote computers and returns a nicely formatted display of 
their current disk utilization and free space. The output can be redirected to a file or other 
output option using standard redirection. 

.Example
Get-myFreeSpace 
Gets the disk utilization and free space of all drives on the local host. 

.Example
Get-myFreeSpace -ComputerName Server1,Server2
Gets the disk utilization and free space of all drives on the Server1 and Server2

.Example
(Get-VM -Name "*server*" | Where State -eq 'Running' ).Name | Get-myFreeSpace
PS C:\>(Get-VM -Name "*server*" | Where-Object {$_.State -eq 'Running').Name | Get-myFreeSpace

Gets a list of running VMs with Server in their name, and passes it to Get-myFreeSpace to process for 
their current disk utilization. The first version of this example uses PowerShell v5 syntax, while 
the second version uses the older syntax that works on earlier versions. 
.Parameter ComputerName
An array of computer names from which you want the disk utilization

.Inputs
[string[]]

.Notes
    Author: Charlie Russel
 Copyright: 2017 by Charlie Russel
          : Permission to use is granted but attribution is appreciated
   Initial: 26 Nov, 2014 (cpr)
   ModHist: 29 Sep, 2016 -- Changed default to array of localhost (cpr)
          : 18 Apr, 2017 -- Changed to use Write-Output,accept Pipeline,added man page,  (cpr)
          :
#>
[CmdletBinding()]
Param(
     [Parameter(Mandatory=$False,Position=0,`
                ValueFromPipeline=$True,`
                ValueFromPipelineByPropertyName=$True,`
                ValueFromRemainingArguments=$True)]
     [alias("Name","Computer")]
     [string[]]
     $ComputerName = @("localhost")
     )

Begin {
   if ($Input) {
      $ComputerName = @($Input)
   } 
   Write-Output ""
   # Save ErrorActionPreference so we can reset it when we're done
   $eap = $ErrorActionPreference
}

Process {
   $ErrorActionPreference = 'SilentlyContinue'
   ForEach ( $Computer in $ComputerName ) {
      Write-Output "Disk Utilization for Computer $Computer is: " 
      Get-WmiObject  -ComputerName $Computer -Class Win32_Volume `
         | Format-Table  -auto `
            @{Label="Drive";`
               Expression={$_.DriveLetter};`
               Align="Right"},`
            @{Label="Free(GB)";`
               Expression={"{0:N0}" -f ($_.FreeSpace/1GB)};`
               Align="Right"},`
            @{Label="% Free";`
               Expression={"{0:P0}" -f ($_.FreeSpace / $_.Capacity)};`
               Align="Right"},`
            @{Label="Size(GB)";`
               Expression={"{0:N0}" -f ($_.Capacity / 1GB)};`
               Align="Right"},`
            @{Label="Volume Label";`
               Expression={$_.Label};`
               Width=25}
      } #EndForEach
} #EndProcessBlock

End {
   # Reset ErrorActionPreference to original value
   $ErrorActionPreference = $eap
}

And there you have it. A new and improved version of one of the most popular scripts I've ever posted here. You can use it to get the disk utilization on your current machine, or any list of remote computers to which you have the rights to run WMI against.

 

I hope you find this script useful, and I'd love to hear comments, suggestions for improvements, or bug reports as appropriate. As always, if you use this script as the basis for your own work, please respect my copyright and provide appropriate attribution.

How to tell if you’re running on Windows Server Core

I have a bunch of scripts I use when I'm building a lab to install "stuff" (that's the Technical Term we IT Professionals use) that I need to manage and work with a virtual machine. Now, when I build from a SysPrep'd image, that's not an issue, but if I have to build from an ISO, I want to automate the process as much as possible. So I have a couple of Setup scripts I run that install gVim, HyperSnap (my screen capture tool), and various other things.

As I was building a new lab this week, I realized that those scripts were all designed to deal with full GUI installations, and had no provisions for not installing applications that make no sense and can't work when there's only a Server Core installation. So, time to find out how I can tell if I'm running as Server Core, obviously. A bit of poking around, and I came up with the following:

$regKey = "hklm:/software/microsoft/windows nt/currentversion"
$Core = (Get-ItemProperty $regKey).InstallationType -eq "Server Core"

(You could do that as a single line, obviously, but I broke it up to make it easier to see on the page. )

The result is stored as a Boolean value in $Core, and I can now branch my installation decisions based on the value of $Core. (Note there ARE other ways to determine whether you're running on Server Core, but they appear to all be programmatic ones not well suited to the avowedly non-programmer IT system administrator types like me. )

Building a Lab in Hyper-V with PowerShell, Part 5

Deploying a DHCP Server

Now that you have your forest and domain installed, including DNS, the next step to setting up a lab is the DHCP server.  Start by creating a new VM for the DHCP server, trey-dhcp-03. (For details on how to create a VM with PowerShell, see Building a Lab in Hyper-V Part 2 and Part 3. ) There's no particular need to make this a GUI installation, so build it as a Server Core installation. We'll do the configuration all in PowerShell anyway.

 

Next, Install the DHCP role, add the local groups required, and authorize it in Active Directory. (Do note the slightly different server name when you go to do that, please, and you don't need or want to promote this server to a domain controller. )

 

Now, as you'll remember from earlier posts in this series, I configure all my VMs with known MAC addresses by first defining the range and then requiring a MAC address final pair parameter to New-myVM.ps1This allows me to now configure a set of reservations for each VM in the lab, simplifying connections and making it a lot easier for me to keep track what is where.

 

Assuming by now you have installed the DHCP role and authorized it in Active Directory, the next step is to set up your IPv4 and IPv6 ranges. We do that by first adding a scope, then setting exclusion ranges and finally scope options. For IPv4, this is three commands:

Add-DhcpServerv4Scope -Name "Trey-Default" `
                      -ComputerName "trey-dhcp-03" `
                      -Description "Default IPv4 Scope for Lab" `
                      -StartRange "192.168.10.1" `
                      -EndRange   "192.168.10.220" `
                      -SubNetMask "255.255.255.0" `
                      -State Active `
                      -Type DHCP `
                      -PassThru

Add-DhcpServerv4ExclusionRange -ScopeID "192.168.10.0" `
                               -ComputerName "trey-dhcp-03" `
                               -StartRange "192.168.10.1" `
                               -EndRange   "192.168.10.20" `
                               -PassThru

Set-DhcpServerv4OptionValue -ScopeID 192.168.10.0 `
                            -ComputerName "trey-dhcp-03" `
                            -DnsDomain "TreyResearch.net" `
                            -DnsServer "192.168.10.2" `
                            -Router "192.168.10.1" `
                            -PassThru

Now,  the same process for IPv6, though I usually do NOT create IPv6 reservations, but do want to set some default values.

Add-DhcpServerv6Scope -Name "Trey-IPv6-Default" `
                      -ComputerName "trey-dhcp-03" `
                      -Description "Default IPv6 Scope for Lab" `
                      -Prefix 2001:db8:0:10:: `
                      -State Active `
                      -PassThru

Add-DhcpServerv6ExclusionRange –ComputerName trey-dhcp-03 `
                               -Prefix 2001:db8:0:10:: `
                               -StartRange 2001:db8:0:10::1 `
                               -EndRange   2001:db8:0:10::20 `
                               -PassThru

Set-DhcpServerv6OptionValue -Prefix 2001:db8:0:10:: `
                            -ComputerName "trey-dhcp-03" `
                            -DnsServer 2001:db8:0:10::2 `
                            -DomainSearchList "TreyResearch.net" `
                            -PassThru

Now, create a CSV file with Names,MAC addresses(ClientID), and IPv4 Addresses. You can use your favourite plain text editor (mine is gVim), or Excel to create the CSV file. My lab has the following for the 192.168.10.xxx range of IP addresses:

Name,ClientID,IPAddress
trey-edge-01,00-15-5D-32-0A-01,192.168.10.1
trey-dc-02,00-15-5D-32-0A-02,192.168.10.2
trey-dhcp-03,00-15-5D-32-0A-03,192.168.10.3
trey-dc-04,00-15-5D-32-0A-04,192.168.10.4
trey-srv-05,00-15-5D-32-0A-05,192.168.10.5
trey-wds-11,00-15-5D-32-0A-0B,192.168.10.11
Trey-Srv-12,00-15-5D-32-0A-0C,192.168.10.12
Trey-Srv-13,00-15-5D-32-0A-0D,192.168.10.13
Trey-Srv-14,00-15-5D-32-0A-0E,192.168.10.14
Trey-Srv-15,00-15-5D-32-0A-0F,192.168.10.15
Trey-Srv-16,00-15-5D-32-0A-10,192.168.10.16
Trey-client-21,00-15-5D-32-0A-15,192.168.10.21
Trey-client-22,00-15-5D-32-0A-16,192.168.10.22
Trey-client-23,00-15-5D-32-0A-17,192.168.10.23
Trey-client-24,00-15-5D-32-0A-18,192.168.10.24
Trey-client-25,00-15-5D-32-0A-19,192.168.10.25

Save the CSV file as "TreyDHCP.csv". Now, to create the reservations, first read in the CSV file with:

$TreyDHCP = Import-CSV TreyDHCP.csv

Then, create the IPv4 reservations with a simple ForEach loop:

ForEach ($addr in $TreyDHCP ) {
   $ErrorActionPreference = "Continue"
   Add-DhcpServerv4Reservation -ScopeID   192.168.10.0 `
                               -Name      $addr.Name `
                               -ClientID  $addr.ClientID `
                               -IPAddress $addr.IPAddress `
                               -PassThru
}

If you run multiple NICs on your lab environment, you'll want to repeat all of the above for the second range of IP addresses.

So, here's the whole thing in a script that supports running remotely.

<#
.Synopsis
Install and configure DHCP for the TreyResearch.net lab environment
.Description
The New-TreyDHCP script installs and configures the DHCP environment for the TreyResearch.net
lab environment. It assumes a DHCP server "trey-dhcp-03" has already been created, but accepts
a parameter to change the server name. 
The script reads a CSV file with the machine names, MAC addresses (ClientIDs), and IPv4
addresses that the that the network will use and then creates IPv4 DHCP reservations for those
machines.
.Example
New-TreyDHCP.ps1
Reads in a list of DHCP addresses from TreyDHCP.csv and configures trey-dhcp-03 as a DHCP
server with those addresses. 
.Example
New-TreyDHCP.ps1 -ComputerName Trey-core-03 -Path c:\temp\dhcp.csv
Reads in a list of DHCP addresses from c:\temp\dhcp.csv and configures the server
Trey-core-03 as a DHCP server with those address reservations. 
.Parameter ComputerName
The server to install and configure DHCP on. Default value is trey-dhcp-03
.Parameter Path
The path to a CSV file with the machine names, client IDs, and IPv4 addresses to configure
DHCP reservations for. The default value is .\TreyDHCP.csv. 
.Inputs
[string]
[string]
.Notes
    Author: Charlie Russel
 Copyright: 2017 by Charlie Russel
          : Permission to use is granted but attribution is appreciated
   Initial: 25 March, 2014 (cpr)
   ModHist: 14 March, 2017 (cpr) Added ComputerName parameter and man page
          : 
#>
[CmdletBinding()]
Param(
     [Parameter(Mandatory=$False)]
     [alias("server")]
     [string]
     $ComputerName = 'trey-dhcp-03',
     [Parameter(Mandatory=$False)]
     [Alias("filename")]
     [string]
     $Path = '.\TreyDHCP.csv'
     )

if ( (Get-WindowsFeature -Name DHCP -ComputerName $ComputerName) -ne "Installed" ) {
  Install-WindowsFeature -Name DHCP -ComputerName $ComputerName -IncludeManagementTools 
}

if (Test-Path $Path ) { 
   $TreyDHCP = Import-CSV $Path 
} else {
   Throw "This script requires an input CSV file with the DHCP Reservations in it."
}

# Find out if the DHCP Server is already authorized. If it is, 
# we assume all the rest of this is done. 
If ( (Get-DhcpServerInDC).DnsName -match $ComputerName ) {
   $IsAuth = $True 
} else {
   $IsAuth = $False 
   $DnsName = $ComputerName + ".TreyResearch.net"
}

# If the server isn't authorized, then nothing is set yet, so set up 
# our DHCP server. 
if (! $IsAuth) {
   Add-DhcpServerInDC -DnsName $DnsName -PassThru
   # Create local groups for DHCP
   # The WinNT in the following IS CASE SENSITIVE
   $connection = [ADSI]"WinNT://$ComputerName"
   $lGroup = $connection.Create("Group","DHCP Administrators")
   $lGroup.SetInfo()
   $lGroup = $connection.Create("Group","DHCP Users")
   $lGroup.SetInfo()
   Add-DhcpServerv4Scope -Name "Trey-Default" `
                         -Description "Default IPv4 Scope for TreyResearch Lab" `
                         -StartRange "192.168.10.1" `
                         -EndRange   "192.168.10.220" `
                         -SubNetMask "255.255.255.0" `
                         -State Active `
                         -Type DHCP `
                         -ComputerName $ComputerName `
                         -PassThru
   Add-DhcpServerv4ExclusionRange -ScopeID "192.168.10.0" `
                                  -StartRange "192.168.10.1" `
                                  -EndRange   "192.168.10.20" `
                                  -ComputerName $ComputerName `
                                  -PassThru
   Set-DhcpServerv4OptionValue -ScopeID 192.168.10.0 `
                               -DnsDomain "TreyResearch.net" `
                               -DnsServer "192.168.10.2" `
                               -Router "192.168.10.1" `
                               -ComputerName $ComputerName `
                               -PassThru
   Add-DhcpServerv6Scope -Name "Trey-IPv6-Default" `
                         -Description "Default IPv6 Scope for TreyResearch Lab" `
                         -Prefix 2001:db8:0:10:: `
                         -State Active `
                         -ComputerName $ComputerName `
                         -PassThru
   Add-DhcpServerv6ExclusionRange -Prefix 2001:db8:0:10:: `
                                  -StartRange 2001:db8:0:10::1 `
                                  -EndRange   2001:db8:0:10::20 `
                                  -ComputerName $ComputerName `
                                  -PassThru
   Set-DhcpServerv6OptionValue -Prefix 2001:db8:0:10:: `
                               -DnsServer 2001:db8:0:10::2 `
                               -DomainSearchList "TreyResearch.net" `
                               -ComputerName $ComputerName `
                               -PassThru
}


ForEach ($addr in $TreyDHCP ) {
   $ErrorActionPreference = "Continue"
   Add-DhcpServerv4Reservation -ScopeID 192.168.10.0 `
                               -Name $addr.Name `
                               -ClientID $addr.ClientID `
                               -IPAddress $addr.IPAddress `
                               -ComputerName $ComputerName `
                               -PassThru
}

I hope you find this script useful, and I'd love to hear comments, suggestions for improvements, or bug reports as appropriate. As always, if you use this script as the basis for your own work, please respect my copyright and provide appropriate attribution.

PowerShell: Rename an Active Directory User

This came up at work the other day. Another admin had attempted to rename an AD User account and it had only partially gotten renamed -- the SAM Account, Name and Display name were all correct, but the old user name was still showing up in a couple of places, including the login screen. The user was not happy, so I was asked to fix it, and provide a script that would handle it correctly. I poked around a bit and found the issue - even if you set all of the obvious properties correctly (and the other admin had missed UPN), it still won't show correctly on that logon screen -- you need to actually rename the AD object itself. So, after I fixed the problem user's account, I wrote up a script to solve the problem for the next time. I chose to use a CSV file as the input, but you could easily re-work this to work off either a CSV file or a set of command-line parameters. But honestly, I don't ever want to have to enter that many command-line parameters for a simple script. Especially if I have more than one to change.

 

The script uses Get-ADUser with the old name, then pipes it to Set-ADUser, and finally pipes it to Rename-ADObject to finish the process.  I even gave it basic help. :)

<#
.Synopsis
Renames the Active Directory users
.Description
Rename-myADUser reads a CSV file to identify an array of users. The users are then 
renamed to the new name in Active Directory.
.Example
Rename-myADUser
Renames the AD Accounts of the users in the default "ADUsers.csv" source file
.Example
Rename-myADUser -Path "C:\temp\ChangedUsers.txt"
Renames the AD accounts of the users listed in the file C:\temp\ChangedUsers.txt"
.Parameter Path
The path to the input CSV file of format:
OldSam,NewName,GivenName,Surname,DisplayName,SAMAccountName,UserPrincipalName,EmailAddress

The default value is ".\ADUsers.csv".  
.Inputs
[string]
.Notes
    Author: Charlie Russel
 Copyright: 2017 by Charlie Russel
          : Permission to use is granted but attribution is appreciated
   Initial: 03/09/2017 (cpr)
   ModHist: 
          :
#>
[CmdletBinding()]
Param(
     [Parameter(Mandatory=$False,Position=0)]
     [string]
     $Path = ".\ADUsers.csv" 
     )

$ADUsers = @()
If (Test-Path $Path ) {
   $ADUsers = Import-CSV $Path
} else { 
   Throw  "This script requires a CSV file with user names and properties."
}
$PDC = (Get-ADDomain).PDCEmulator
Write-Verbose "The PDC Emulator has been identified as $PDC"
Write-Verbose " "

ForEach ($User in $ADUsers ) {
   Write-Verbose "Modifying $user.OldSam to $user.NewName" 
   Sleep 3
   Get-ADUser -Identity $User.OldSam -Properties * | `
   Set-ADUser -Server $PDC `
              -DisplayName $user.DisplayName `
              -EmailAddress $User.EmailAddress `
              -SamAccountName $User.SamAccountName `
              -GivenName $User.GivenName `
              -Surname $User.Surname `
              -UserPrincipalName $user.UserPrincipalName `
              -PassThru | `
   Rename-ADObject -NewName $user.NewName -Server $PDC -PassThru
}

 

Building a Lab in Hyper-V with PowerShell, Part 4

Creating a new forest

In the previous sections of this series, I've covered how to build VMs using PowerShell, but labs aren't much good if they don't actually have any structure. So, let's create a new forest and domain to manage our labs. I'm going to assume for this post that you've gotten started already and created a new Windows Server 2012R2 or Windows Server 2016 virtual machine. For this, it can be a graphical install or a Server Core installation and either Server Standard or Datacenter. Since we're going to be using only PowerShell to create the forest, there's no need for a GUI.

The things we'll need to have identified before we start are:

  • Server IP address
  • Server name
  • DNS namespace for the root domain of the forest
  • Domain name for the root domain of the forest
  • DNS Server type (AD-integrated or standalone)

Set Server IP Address

We need set our server to a fixed IP address. While not absolutely required, I think it's a really bad idea to not do this. And, since our lab doesn't yet have DHCP in it, you need to anyway. (We'll add a DHCP server in the next installment. )

To configure the network adapter for a static IP address, I need to know either the interface alias (name) or the interface index. To get those, use Get-NetAdapter from a PowerShell window. (Note: if you're doing this on a new Windows Server Core installation, you can open a PowerShell window with Start PowerShell.exe at the command prompt. To start a PowerShell window automatically for this user, at logon, see my May post. )

Get-NetAdapter | Format-Table -AutoSize Name,Status,IFIndex,MacAddress

Name       Status ifIndex MacAddress
----       ------ ------- ----------
Ethernet 2 Up           3 00-15-5D-32-0A-02
Ethernet   Up           5 00-15-5D-32-CE-02

Which tells us that the DC has two network adapters, and the one that is on the Local-10 switch (from New-myVM.ps1) is at an ifIndex of 3, while the one on the "199 Network" switch has an ifIndex of 5. Now, we'll set the static IP addresses for these two adapters. First, the NIC on Local-10:

# Set IPv4
$NIC2 = Get-NetAdapter -ifIndex 3
$NIC2 | Set-NetIPInterface -DHCP Disabled
$NIC2 | New-NetIPAddress -AddressFamily  IPv4 `
                         -IPAddress      192.168.10.2 `
                         -PrefixLength   24 `
                         -Type Unicast `
                         -DefaultGateway 192.168.10.1
# Set IPv6
$NIC2 | New-NetIPAddress -AddressFamily  IPv6 `
                         -IPAddress      2001:db8:0:10::2 `
                         -PrefixLength   64 `
                         -Type Unicast `
                         -DefaultGateway 2001:db8:0:10::1

# Set DNS Server Addresses to self
Set-DnsClientServerAddress -InterfaceIndex  $NIC2.ifIndex `
                           -ServerAddresses 192.168.10.2,2001:db8:0:10::2

#Now, for the 199 Network, which I use for internal communications between lab hosts, I want to set a pure IPv4 address with no IPv6, so instead of setting an IPv6 address for the NIC, I'll disable it with Disable-NetAdapterBinding.

$NIC = Get-NetAdapter -ifIndex 5

# Disable IPv6
Disable-NetAdapterBinding -Name $NIC.Name -ComponentID ms_tcpip6

# Set IPv4 to 192.168.199.2
$NIC | Set-NetIPInterface -Dhcp Disabled
$NIC | New-NetIPAddress -AddressFamily IPv4 `
                        -IPAddress     192.168.199.2 `
                        -PrefixLength  24 `
                        -Type Unicast
# Set DNS to self
Set-DnsClientServerAddress -InterfaceIndex  $NIC.ifIndex `
                           -ServerAddresses 192.168.199.2

(Note: Set-NetAdapterBinding is not available on Windows 7/Server 2008 R2)

 

Set Server Name

Next, let's set the name of the server to match our naming conventions for this lab. We do this now, knowing it will force a reboot before we go any further.

Rename-Computer -NewName trey-dc-02 -Restart -Force

This will give the computer a new name and restart it.

 

Create Forest and Install AD-integrated DNS

Now that we have static IP addresses for our network adapters, and we've set the name of the server, we can go ahead and create our AD forest. First, we install Active Directory and update the PowerShell Help files with:

Install-WindowsFeature -Name AD-Domain-Services -IncludeManagementTools
Update-Help -SourcePath \\labhost\PSHelp

This installs the ActiveDirectory and ADDSDeployment modules that we'll need to create the forest. Now, we promote the server to be the first domain controller in the new forest. Before we do the actual install, we test to make sure we don't have any issues with Test-ADDSForestInstallation:

Test-ADDSForestInstallation `
         -DomainName 'TreyResearch.net' `
         -DomainNetBiosName 'TREYRESEARCH' `
         -DomainMode 6 `
         -ForestMode 6 `
         -NoDnsOnNetwork `
         -SafeModeAdministratorPassword (ConvertTo-SecureString `
                                                  -String 'P@ssw0rd' `
                                                  -AsPlainText `
                                                  -Force) `
         -NoRebootOnCompletion

Even though this is a brand new forest in an isolated lab setting, it's still a good practice to test before you actually deploy. And it doesn't cost all that much time or annoyance. I've included the SafeModeAdministratorPassword parameter to avoid the prompts for it. This is a lab, not real life. :) Also note that we're setting the forest and domain modes to Server2012R2. If you need earlier versions of domain controllers in your lab, you can set the mode accordingly.

The results of the test are as expected:

WARNING: Windows Server 2016 domain controllers have a default for the security setting named "Allow cryptography
algorithms compatible with Windows NT 4.0" that prevents weaker cryptography algorithms when establishing security
channel sessions.

For more information about this setting, see Knowledge Base article 942564
(http://go.microsoft.com/fwlink/?LinkId=104751).

WARNING: A delegation for this DNS server cannot be created because the authoritative parent zone cannot be found or it
 does not run Windows DNS server. If you are integrating with an existing DNS infrastructure, you should manually
create a delegation to this DNS server in the parent zone to ensure reliable name resolution from outside the domain
"TreyResearch.net". Otherwise, no action is required.


Message                          Context                                  RebootRequired  Status
-------                          -------                                  --------------  ------
Operation completed successfully Test.VerifyDcPromoCore.DCPromo.General.3          False Success

With that confirmation, we can go ahead and finish creating the forest and configuring DNS with the command:

Install-ADDSForest `
    -DomainName 'TreyResearch.net' `
    -DomainNetBiosName 'TREYRESEARCH' `
    -DomainMode 6 `
    -ForestMode 6 `
    -NoDnsOnNetwork `
    -SkipPreChecks `
    -SafeModeAdministratorPassword (ConvertTo-SecureString `
                                                  -String 'P@ssw0rd' `
                                                  -AsPlainText `
                                                  -Force) `
    -Force

You'll notice that the options here match our test pass, except I chose to bypass a second test. If you want to keep your SafeMode Administrator password private you can eliminate that parameter and you'll be prompted at the command line. When this finishes and the server has rebooted, you can log in with the TREYRESEARCH\Administrator account and the local Administrator password you had before you promoted the VM to be a domain controller.  This may or may not be the same as the SafeModeAdministratorPassword you set during the installation.