No Theory Here: Adding ESXi Hosts to a Windows Domain

Here we go, round two for #Blogtober2018 – Tech Edition. The tricky thing about writing technical content for a blog is that most likely it has already been covered and covered in better detail. Today’s post is no different, so I’m going to post some links to great guides that go in depth on how to join an ESXi host to a domain. So if you want more detail or really want to know the “why” and “how”, check these out:

https://kb.vmware.com/s/article/2075361

https://www.altaro.com/vmware/how-to-join-esxi-to-active-directory-for-improved-management-and-security/

http://vcloud-lab.com/entries/esxi-installation-and-configuration/join-domain-esxi-to-an-active-directory-ou-powercli

But my goal in today’s post is function over depth. No theory, only practical application. I’m going to provide a script I wrote to join all the hosts in a specific cluster to my domain., focusing on providing something that quickly gets the job done, while avoiding theory if possible.

Part One: The Setup

There are two main things I needed to do in AD before adding ESXi hosts. First, I needed to create an AD Security group to hold accounts that will be used to log into ESXi. This is the group that users must be a part of to authenticate to the ESXi host once joined to the domain.

New-ADGroup -Name "VMware Admins" -Path "OU="VMware Admins",DC=domain,dc=root" -GroupScope Global -GroupCategory Security

Save the group name; it will be used as an argument for one of the parameters in the script we use.

The second thing I needed to do was get the canonical name where I wanted the newly created host computer accounts to land once it was created. I had previously created the OU, so all I needed now was to get the canonical name and save it:

Get-ADOrganizationalUnit -Filter "Name -eq 'ESXi Hosts OU you want to use'" -Properties canonicalname | Select-Object canonicalname

Same as before, save the canonical name since you will be using it as an argument later.

Finally, ensure the following are true before running the script to avoid any errors later on:

  • Ensure ESXi host and domain controllers share NTP source.
  • ESXi host must have an A record in the domain.
  • Proper firewall ports must be open on ESXi Hosts. If you have a restrictive setup, be sure to check that the appropriate ports are open.
  • Write down the canonical name and security group mentioned above.
  • Be sure to run this with both AD and vCenter permissions.
Part Two: Function Over Form

I used a function and mandatory parameters to help ensure we don’t forget anything. So to break it down:

  1. Connects to vCenter
  2. Loops through each host in cluster joining to domain
  3. Updates ESXi host advanced setting with the AD Security group
  4. Removes .domain.root for the Set-ADComputer cmdlet
  5. Updates AD description with the argument you passed to the $DescriptionUseQuotes parameter
function Set-JSESXiDomainJoin {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[string]$clusterName,

[Parameter(Mandatory=$true)]
[string]$domainInCanonicalNameFormat,

[Parameter(Mandatory=$true)]
[string]$user,

[Parameter(Mandatory=$true)]
[string]$password,

[Parameter(Mandatory=$true)]
[string]$descriptionUseQuotes,

[Parameter(Mandatory=$true)]
[string]$ADAdminGroup,

[Parameter(Mandatory=$true)]
[string]$VIServer

)
#Does check for required modules
#Requires -Modules ActiveDirectory
#Requires -Version 3
#Requires -Modules VMware.VimAutomation.Core

#Connecting to vCenter
Connect-VIServer -Server $VIServer

#Loop through each host in cluster
foreach ($esxiHost in (Get-Cluster $clusterName | Get-VMHost)){

#Join host to domain
Get-VMHostAuthentication -VMHost $esxiHost | Set-VMHostAuthentication -Domain $domainInCanonicalNameFormat -User $user -Password $password -JoinDomain -Confirm:$false

#Updates advanced settings with AD security group
Get-AdvancedSetting -Entity $esxiHost -Name Config.HostAgent.plugins.hostsvc.esxAdminsGroup | Set-AdvancedSetting -Value $ADAdminGroup -Confirm:$false

#Removes domain name from the host, leaving only hostname.
$esxiHostName = $esxiHost.Name.Split(".")[0]

#Updates description in Active Directory
Set-ADComputer -Identity $esxiHostName -Description $descriptionUseQuotes
}
}
Part Three: Success

It should look something like this when you run it:

 Set-JSESXiDomainJoin -ClusterName "ClusterName" -DomainInCanonicalNameFormat "domain.root/ou/ou" -User "jamey"-Password "secret stuff" -DescriptionUseQuotes "ESXi Host - VMware is the best" -ADAdminGroup "VMware people" -ViServer vcenter.domain.root

Set-Citrix

My last couple of posts have been about powershell. Its utility has really be obvious in my day to day job, so I have been writing about what I’m working on.

If you have ever worked in a Xendesktop environment you are familiar with machines going unregistered for whatever reason. I wrote this function to produce five possible outcomes that I needed when dealing with unregistered machines and machines in maintenance mode in a XenDesktop Environment. The parameters are setup as five switches that will kick off Citrix management actions.

The first switch is -MachineUnregistered This searches a Citrix site to find powered on and unregistered machines. It will not perform any action on the machines. It only show you the unregistered machines.

The second switch is -RestartMachineUnregisteredPrompt This searches a Citrix site to find powered on, unregistered machines and prompts you if they should be restarted.

The third switch -RestartMachineUnregistered is the same as the second switch but does not prompt asking if a reboot should be performed. It just reboots all machines that are listed as unregistered. Use this one with caution.

The fourth switch is -MachineInMaint. This searches a Citrix site to find machines in maintenance mode.

Finally, the fifth switch is -TurnOffMaintOnMachinePrompt. This searches a Citrix site to find machines in maintenance mode and prompts asking if maintenance mode should be turned off. I hard coded yes and no, with no validation. So if you type anything besides a literal ‘yes’ or ‘no’, you will not receive an error, but the action will not happen.

Update the variable $AdminAddress to match your DDC. After that you should be good to go.

Function Set-Citrix {
<# .SYNOPSIS Function that uses parameters as switches to trigger typical Citrx Xendesktop maintenance actions.
.DESCRIPTION Switch Parameters to start predefined Xendesktop actions. .PARAMETER MachineUnregistered Gets unregistered machines. 
.PARAMETER RestartMachineUnregisteredPrompt Prompts asking to restart unregisterd machines. 
.PARAMETER RestartMachineUnregistered Restarts unregisterd machines without prompt. 
.PARAMETER MachineInMaint Finds machines in maintence mode. 
.PARAMETER TurnOffMaintOnMachine Prompts asking to turn off maintenance modes on machines. .NOTES Author : Jamey .LINK https://jamey.info .EXAMPLE Set-Citrix -MachineUnregistered Searches Citrix site to find powered on and unregistered machines 
.EXAMPLE Set-Citrix -RestartMachineUnregisteredPrompt Searches Citrix site to find powered on, unregistered machines and prompts asking if they should be restarted. 
.EXAMPLE Set-Citrix -RestartMachineUnregistered Searches Citrix site to find powered on, unregistered machines and restarts them without prompt.
.EXAMPLE Set-Citrix -MachineInMaint Searches Citrix site to find machines in maintence mode
.EXAMPLE Set-Citrix -TurnOffMaintOnMachinePrompt Searches Citrix site to find machines in maintenance mode and prompts asking if maintenance mode should be turned off, #>
[CmdletBinding()]
param(

[Parameter(Mandatory=$False, ParameterSetName="MachineUnregistered")]
[Alias("MU")]
[switch]$MachineUnregistered,

[Parameter(Mandatory=$False, ParameterSetName="RestartMachineUnregisteredPrompt")]
[Alias("RSMUP")]
[switch]$RestartMachineUnregisteredPrompt,

[Parameter(Mandatory=$False, ParameterSetName="RestartMachineUnregistered")]
[Alias("RSMU")]
[switch]$RestartMachineUnregistered,

[Parameter(Mandatory=$False, ParameterSetName="MachineInMaint")]
[Alias("MIM")]
[switch]$MachineInMaint,

[Parameter(Mandatory=$False, ParameterSetName="TurnOffMaintOnMachinePrompt")]
[Alias("TOMOM")]
[switch]$TurnOffMaintOnMachinePrompt
)

##Load Citrix Modules
Add-PSSnapin Citrix.*
$AdminAddress = 'ddc'

If ($machineUnregistered) {
    Get-BrokerDesktop -adminaddress $AdminAddress -MaxRecordCount 5000 | where-object {($_.PowerState -eq 'On') -and ($_.RegistrationState -eq 'Unregistered')} | Select-Object MachineName
}

ElseIf ($RestartMachineUnregisteredPrompt) {
    $UnregisteredDesktops = (Get-BrokerDesktop -adminaddress $AdminAddress -MaxRecordCount 5000 | where-object {($_.PowerState -eq 'On') -and ($_.RegistrationState -eq 'Unregistered')} | Select-Object MachineName)
foreach ($unregisteredDesktop in $unregisteredDesktops){
    Write-host $UnregisteredDesktop.machinename
    $answer = Read-Host -prompt 'Restart Unregistered Machine?'
if ($answer -eq 'yes'){
    New-BrokerHostingPowerAction -MachineName $unregisteredDesktop.MachineName -Action Reset
    Write-Host "Unregistered machine name is: $unregisteredDesktop.MachineName"
}
elseif ($answer -eq 'no'){
    Write-Host 'Did not restart machine'
        }
    }
}

ElseIf ($RestartMachineUnregistered) {
    $unregisteredDesktops = (Get-BrokerDesktop -adminaddress $AdminAddress -MaxRecordCount 5000 | where-object {($_.PowerState -eq 'On') -and ($_.RegistrationState -eq 'Unregistered')} | select MachineName)
foreach ($unregisteredDesktop in $unregisteredDesktops){
    New-BrokerHostingPowerAction -MachineName $unregisteredDesktop.MachineName -Action Reset
    Write-Host "Unregistered machine name is: $unregisteredDesktop.MachineName"
}
}

ElseIf ($MachineInMaint) {
    Get-BrokerDesktop -AdminAddress $AdminAddress -MaxRecordCount 5000 | Where-Object {($_.InMaintenanceMode -eq $true)} | Select-Object machinename
}
ElseIf ($TurnOffMaintOnMachinePrompt) {
foreach ($desktop in (Get-BrokerDesktop -AdminAddress $adminAddress -Filter {(inmaintenancemode -eq $true) -and (desktopkind -eq 'shared' )})) {
    Write-host $desktop.machinename
$answer = Read-Host -prompt 'Disable Maint for this machine? Answer yes or no'
if ($answer -eq 'yes'){
    Set-BrokerSharedDesktop -machinename $desktop.machinename -InMaintenanceMode $false -AdminAddress $AdminAddress
    New-BrokerHostingPowerAction -MachineName $desktop.machinename -Action TurnOn
}
elseif
($answer -eq 'no'){
    Write-Host 'Did not put machine in Maintenance Mode'
}
}
}
Else {
    Return "Choose Something "
}
}

Import AD User to Learn PowerShell

I wrote this script for someone who did not use PowerShell on a daily basis. I knew they wanted to create active directory user accounts in bulk from a csv file, so I wrote this with a few goals in mind:

1. Fix their issue. I wanted to show how useful it is learn PowerShell by using it to fix a real problem they had. This allowed them to create the users as desired.

2. Show the steps as plainly as possible, skipping any fancy functions or modules. I tried to write it as a plain sequence of steps.

3. Make it so they can edit the script if their csv input changes, despite them having no scripting background.

I think this should be run from the powershell ISE instead of the command line. If you open it on the left in the ISE and then open ADUC on the right, you can observe the results right away. You can “see” what the script does.

#Imports users from HR csv and sets it to variable

$users = Import-Csv -Path 'c:\path\to\csv' 

#Loops through each user, creates variable for each property and creates the user

foreach ($user in $users){

#These match the column names in the csv

$SamAccountName = $user.Username
$Name = "$($user."Last Name"), $($user."First Name") $($user.Int)."
$GivenName = $user."First Name"
$Surname = $user."Last Name"
$Initials = $user.Int
$DisplayName = "$($user."Last Name"), $($user."First Name") $($user.Int)."
$UserPrincipalName = "$($user.Username)@company.org"
$Description = "$($user."Position") - $($user."Department")"
$EmployID = $user."EE #"
$OU = "ou=ou,dc=company,dc=org"


#Creates the user

$NewUser = New-ADUser -SamAccountName $SamAccountName -Name $Name -GivenName $GivenName -Surname $Surname -Initials $Initials -DisplayName $DisplayName -UserPrincipalName $UserPrincipalName -Description $Description -OtherAttributes @{'EmployeeID' = $EmployID} -AccountPassword $newpwd -CannotChangePassword $false -ChangePasswordAtLogon $true -Enabled $true -Path $OU -PassThru

#Adds new user to group one

Add-ADGroupMember -Identity 'group one' -Members $SamAccountName

#Adds new user to group two

Add-ADGroupMember -Identity 'group two' -Members $SamAccountName

}

VMware PowerCLI to enable management services

I wrote this once when I needed to pull as much information as possible about the vmware environment I found myself in that day. It enables ssh and sets it to start at boot. It also enables SNMP, sets it to star at boot and sets the community string you define. I split it up into multiple for loops so I could easily track what I was doing with on screen commentary via write-host. I used it in a vSphere 5.5 environment. Beware results might vary by version.

<#####################################################################
I begin by letting you define your vCenter server, your datacenter object and your snmp community string
Next this script starts SSH, makes SSH start on boot, hides SSH warnings, starts SNMP, makes SNMP start on boot,
and sets the SNMP community string on each host in your defined data center.
######################################################################>

#Your Datacenter specific variables
$vCenter = "FQDN of vcenter server"
$datacenter = "datacenter object name"
$community = "SNMP Community String"

#Load VMware Cmdlets and connect to vCenter
Add-PSSnapin vmware*
Connect-VIServer -Server $vCenter

#Set VMHosts variable
$VMHosts = Get-VMhost -Location $datacenter | Sort-Object
  
# Start SSH on each host in the datacenter
ForEach ($HostMachine in $VMHosts) {
    Write-Host "Starting the SSH Service on $HostMachine " 
    $HostMachine | Get-VMHostService | Where-Object {($_.Key -eq "TSM-SSH") -and ($_.Running -eq $False)} | Start-VMHostService
}
  
# Change SSH to start on boot
ForEach ($HostMachine in $VMHosts) {
    Write-Host "Changing SSH to start on boot for $HostMachine " 
    $HostMachine | Get-VMHostService | where { $_.key -eq "TSM-SSH" } | Set-VMHostService -Policy "On" -Confirm:$false -ea 1
}
  
# Supress SSH warning
ForEach ($HostMachine in $VMHosts) {
    Write-Host "Hiding SSH Warning on $HostMachine "
    $HostMachine | Get-AdvancedSetting | Where-Object {$_.Name -eq "UserVars.SuppressShellWarning"} | Set-AdvancedSetting -Value "1" -Confirm:$false
}

#Start SNMP service on hosts
ForEach ($HostMachine in $VMHosts) {
    Write-Host "Starting SNMP on $HostMachine " 
    $HostMachine | Get-VMHostService | Where-Object {($_.Key -eq "snmpd") -and ($_.Running -eq $false)} | Start-VMHostService | Out-Null
}

#Change SNMP to start on boot
ForEach ($HostMachine in $VMHosts) {
    Write-Host "Changing SNMP to start with $HostMachine " 
    Get-VMHostService $HostMachine  | Where-Object { $_.key -eq "snmpd" } | Set-VMHostService -Policy "On" -Confirm:$false -ea 1 | Out-Null
}

#Set snmp community string and opens firewall for snmp
ForEach ($HostMachine in $VMHosts) {
    Write-Host "Setting snmp community string and opening firewall for $HostMachine"
    $esxcli = Get-EsxCli -VMHost $HostMachine
    $esxcli.system.snmp.set($null,$community,$true,$null,$null,$null,$null,$null,$null,$null,$null,$null,$null,$null,$null,$null,$null)
    $esxcli.network.firewall.ruleset.set($true, $true, "snmp")
}

Write-Host "Good Job"

Powershell Get-Person

Where I work I do a lot of querying AD for users. I work for a large organization with many departments. When I first started, someone would say something like, “Hey Jamey, pull up Mr. Jolgue or Mr. Joluge or was it Mr. Joleguge,” and I would have no idea where to start my search. So I wanted to write something that would let me type in almost any criteria and get something besides an error back if there were no matches.

You can invoke this with no parameters if you want any result that is close to your search, or you can invoke this with the -FirstName, -LastName, or -UserName parameters if you want specific matches.

Remember to download RSAT tools to query AD remotely. They can be found here RSAT Tools.

Copy and paste this to a file and save with a .psm1 extension. Look up your module path, which can be found by typing $Env:PSModulePath into your shell. Create a folder of the same name in your module folder, then copy the file to that folder. After that you can call it as any other cmdlet.

Function Get-Person { 
.SYNOPSIS This script is used to search AD for users with more liberal filtering that I could get with Get-Aduser -Filter
.DESCRIPTION I use LDAP queries and wildcards to allow liberal search criteria and attempt to return anything close to what the user was thinking. If they want to be more specific, -FirstName -LastName or -UserName parameters can be used which require exact matches for input.
.PARAMETER LastName Accepts last name as search criteria. 
.PARAMETER FirstName Accepts first name as search criteria. 
.PARAMETER UserName Accepts SamAccountName as search criteria. 
.NOTES Author : Jamey Email : jamey@jamey.info 
.INPUTS Strings 
.OUTPUTS New custom user object with predefined attributes 
.LINK Script posted over: https://jamey.info 
.EXAMPLE Get-Person Jon Searches Active Directory for users that Last Name, First Name or Sam Account Name contain the string Jon 
.EXAMPLE Get-Person -LastName Jon Searches Active Directory for users with the last name Jon 
.EXAMPLE Get-Person -FirstName Jon Searches Active Directory for users with the first name Jon 
.EXAMPLE Get-Person -UserName Searches Active Directory for users with the SamAccountName Jon #>

#Requires –Modules ActiveDirectory
#Requires –Version 3

[CmdletBinding(DefaultParameterSetName = "All")] 
param( 

    [Parameter(Mandatory=$False,  Position=0,  ParameterSetName="All")]
    [ValidateNotNullOrEmpty()]  
    [Alias("AnyCriteria")]  
    [string]$All,
    
    [Parameter(Mandatory=$False,  ParameterSetName="LastName")]
    [ValidateNotNullOrEmpty()]  
    [Alias("Surname")]  
    [string]$LastName, 

    [Parameter(Mandatory=$False,  ParameterSetName="FirstName")]  
    [Alias("GivenName")]
    [ValidateNotNullOrEmpty()]  
    [string]$FirstName,
    
    [Parameter(Mandatory=$False,  ParameterSetName="UserName")]  
    [Alias("SamAccountName")]
    [ValidateNotNullOrEmpty()]  
    [string]$UserName
) 

If ($All) {
    $UserInfo = Get-ADUser -LDAPFilter "(|(Sn=*$All*)(givenName=*$All*)(sAMAccountName=*$All*))" -Properties *
}
ElseIf ($LastName) {
    $UserInfo = Get-ADUser -LDAPFilter "(Sn=$LastName)" -Properties *
}
ElseIf ($FirstName) {
    $UserInfo = Get-ADUser -LDAPFilter "(GivenName=$FirstName)" -Properties *
}
ElseIf ($UserName) {
    $UserInfo = Get-ADUser -LDAPFilter "(samaccountname=$UserName)" -Properties *
}
Else {
    Return "Please enter some value to search on"
}

foreach ($User in $UserInfo) {
    $EachUser = Get-ADUser $User.samaccountname -Properties *
    $Properties = [PSCustomObject]@{
        "First Name" = $EachUser.Givenname
        "Last Name" = $EachUser.Surname
        "Display Name" = $EachUser.Displayname
        "User Name" = $EachUser.samaccountname
        "Employee ID" = $EachUser.employeeid
        "Account Created Date" = $EachUser.Created
        "Department" = $EachUser.Department
        "Description" = $EachUser.Description
        "OU Info" = $EachUser.Distinguishedname
        "Email Address" = $EachUser.emailaddress
        }
    $Properties
    }
}