Install all the MSI installation files in a folder

All the files in a folder

All the files in a folder

If you want to install a bunch of MSI files you put them in a folder and install them with a batch file. There is a downsize: you must modify the batch file for each situation. With this PowerShell script you can install all the MSI files in the folder, including applying transform and patch files. You can add your own properties in a csv settings file.

There are some parameters:

  • Install: Use this swith to specify an installation.
  • Uninstall: Use this swith to specify an uninstall.
  • MSIPath: Specify the location where the MSI files are located.
  • Loglocation: Specify the logfile location.

The default log location is C:\Windows\system32\LogFiles.

You can add a transform file.

You can add a transform file.

You can specify a settings file for each application.

You can specify a settings file for each application.

The default settings file is applied to all installations in the folder.

The default settings file is applied to all installations in the folder.

You can also use SCCM to install all the MSI files in the folder. Create an application with one deployment type. You can add all the product codes from each MSI file to identify a successful installation.

SCCM: The 'Application Catalog' tab.

SCCM: The ‘Application Catalog’ tab.

SCCM: Program

SCCM: Program

The Install line is:

The Uninstall line is:

SCCM: Detection rule

SCCM: Detection rule

Installation on the client.

Installation on the client.

Uninstall on the client.

Uninstall on the client.

A demonstration of this script can be found on my YouTube channel: the script and AppV Repository or view it below:

The script:

<#

.SYNOPSIS
    Installs all MSI's in a folder. By default, it is the folder where the script is located, but you can specify another location.
 
.DESCRIPTION
    Installs all MSI's in a folder. By default, it is the folder where the script is located, but you can specify another location.
    If there is a MST that starts with the MSI name, then the MSI / MST combination will be installed.   
    If there is a MSP that starts with the MSI name, then the MSI / MSP combination will be installed. 

    Valid combinations:
    -> MSI file: Application_1.20.msi
    -> MST file: Application_1.20_transform.mst
    -> MSP file: Application_1.20_Update_to_1.40.msp
    -> CSV file: Application_1.20.csv

    Invalid combinations:
    -> MSI file: Application_1.20.msi
    -> MST file: Application_1.20.msi.mst     (.msi should be removed)
    -> MST file: Application_patch.msp        (not the full msi file name)
 
.EXAMPLE
     Installs all the MSIs that are in the folder where the script is located.
     ."\install_all_msi (v10).ps1"

.EXAMPLE
     Installs all the MSIs that are in the folder where the script is located.
     ."\install_all_msi (v10).ps1" -Install

.EXAMPLE
     Uninstalls all the MSIs that are in the folder \\server\share\MSIs.
     ."\install_all_msi (v10).ps1" -Uninstall -MSIPath \\server\share\MSIs

.EXAMPLE
     Uninstalls all the MSIs that are in the folder \\server\share\MSIs. Logfiles are written to C:\Logs
     ."\install_all_msi (v10).ps1" -Uninstall -MSIPath \\server\share\MSIs -LogLocation C:\Logs

.NOTES
    Author:  Willem-Jan Vroom
    Website: https://www.vroom.cc/
    Twitter: @TheStingPilot

 v0.1:
   * Initial version.

 v1.0:
   * Added: properties handler:
         - defaultproperties.csv -> will be applied to all MSI packages or MSI / MST combination in the folder.
                                    This file can be in either the location where the script is, or in the location
                                    where the MSI files are. 
         - .csv     -> will be applied only to the given MSI or MSI / MST combination.
                                    This file must be in the same directory as the MSIs.
     This file must have the following header layout:
         Property,Value
         ALLUSERS,1
         ADDLOCAL,ALL
     The content may be different. 
   * Install and uninstall switch added.
   * Check if user has admin rights. It throws up an error in case not.
   * Added the command line options MSIPath and LogLocation.
   * Added patch support. The pathname must start with the same name as the MSI.
   * Check if both install and uninstall switches are used.
   * Bugfix: error messages when there are quotes around the MSI file name.

#>

[CmdLetBinding()]

param
 (
  # Use this switch to specify an installation.
  [Parameter(Mandatory=$False)]
  [Switch] $Install,

  # Use this switch to specify an uninstall.
  [Parameter(Mandatory=$False)]
  [Switch] $Uninstall,

  # Specify the location where the MSI files are located.
  [Parameter(Mandatory=$False)]
  [String] $MSIPath = "",

  # Specify the logfile location.
  [Parameter(Mandatory=$False)]
  [String] $LogLocation = ""
 )

# ========================================================================================================================
# Function block
# ========================================================================================================================

  Function CreateLogFile
  {

   <#
   .NOTES
   ========================================================================================================================
   Created with:     Windows PowerShell ISE
   Created on:       9-January-2019
   Created by:       Willem-Jan Vroom
   Organization:     
   Functionname:     CreateLogFile
   ========================================================================================================================
   .SYNOPSIS

   This function creates the logfile 

   #>

   param
    (
     [string] $LogFile
    )

   New-Item $LogFile -Force -ItemType File | Out-Null
  }
 
Function WriteToLog
 {
  <#
  .NOTES
  ========================================================================================================================
  Created with:     Windows PowerShell ISE
  Created on:       9-January-2019
  Created by:       Willem-Jan Vroom
  Organization:     
  Functionname:     WriteToLog
  ========================================================================================================================
  .SYNOPSIS

  This function adds a line to the logfile

  #>

  param
     (
      [string] $LogFile,
      [string] $line
     )

   $timeStamp = (Get-Date).ToString('G').Replace("/","-")
   $line = $timeStamp + " - " + $line
   Add-Content -Path $LogFile -Value $line -Force
 }

 Function Import-PropertyFile
  {
   <#
   .NOTES
   ========================================================================================================================
   Created with:     Windows PowerShell ISE
   Created on:       9-January-2019
   Created by:       Willem-Jan Vroom
   Organization:     
   Functionname:     Import-PropertyFile
   ========================================================================================================================
   .SYNOPSIS

   This function imports all the properties that are mentioned in the given property-file 

   #>

   param
    (
     [string] $PropertyFile
    )

   $arrItems      = @()
   $strProperties = ""

   if(Test-Path $PropertyFile)
    {
     $arrItems = @(Import-CSV $PropertyFile)

     if($arrItems.Count -ge 1)
      {
       WriteToLog -LogFile $strLogFile -line "The property file $PropertyFile is applied."
       ForEach($objItem in $arrItems)
        {
         $strProperty    = $objItem.Property
         $strValue       = $objItem.Value
         $strLine        = $strProperty + "=" + $strValue + " "
         $strProperties += $strLine 
        }
       }
    }
    
    Return $strProperties
  }

Function Get-MSIFileInformation
 {

  <#
  .NOTES
  ========================================================================================================================
  Created with:     Windows PowerShell ISE
  Created on:       9-January-2019
  Created by:       Willem-Jan Vroom
  Organization:     
  Functionname:     Get-MSIFileInformation
  ========================================================================================================================
  .SYNOPSIS

  This function reads the various properties from a MSI file. 
  This function has been found on http://www.scconfigmgr.com/2014/08/22/how-to-get-msi-file-information-with-powershell/
  All credits, including the copyright go to Nickolaj Andersen.

  #>


  param
   (
    [parameter(Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [System.IO.FileInfo]$Path,
 
    [parameter(Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [ValidateSet("ProductCode", "ProductVersion", "ProductName", "Manufacturer", "ProductLanguage", "FullVersion")]
    [string]$Property
   )
  
  Process 
   {
    try 
     {
        # Read property from MSI database
        $WindowsInstaller = New-Object -ComObject WindowsInstaller.Installer
        $MSIDatabase = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $null, $WindowsInstaller, @($Path.FullName, 0))
        $Query = "SELECT Value FROM Property WHERE Property = '$($Property)'"
        $View = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, ($Query))
        $View.GetType().InvokeMember("Execute", "InvokeMethod", $null, $View, $null)
        $Record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $View, $null)
        $Value = $Record.GetType().InvokeMember("StringData", "GetProperty", $null, $Record, 1)
         
        # Commit database and close view
        $MSIDatabase.GetType().InvokeMember("Commit", "InvokeMethod", $null, $MSIDatabase, $null)
        $View.GetType().InvokeMember("Close", "InvokeMethod", $null, $View, $null)           
        $MSIDatabase = $null
        $View = $null
 
        # Return the value
        return $Value
     } 
    catch 
     {
      Write-Warning -Message $_.Exception.Message ; break
     }
  }
  End 
  {
    # Run garbage collection and release ComObject
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($WindowsInstaller) | Out-Null
    [System.GC]::Collect()
  }
}

Function Check-HasAdminRights
 {

  <#
  .NOTES
  ========================================================================================================================
  Created with:     Windows PowerShell ISE
  Created on:       11-January-2019
  Created by:       Willem-Jan Vroom
  Organization:     
  Functionname:     Check-HasAdminRights
  ========================================================================================================================
  .SYNOPSIS

  This function checks if an user has admin rights. The function returns $true or $false

  #>

  If (([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator"))
   {
    Return $True
   }
    else
   {
    Return $False
   }
 }

 Function Remove-TrailingCharacter
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       18-January-2019
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Remove-TrailingCharacter
=============================================================================================================================================
.SYNOPSIS

This function removes a trailing character from a string
#>

param
(
 [string] $RemoveCharacterFrom = "",
 [string] $Character           = "" 
)

if($RemoveCharacterFrom.Length -gt 0)
 { 
  if(($RemoveCharacterFrom.SubString($RemoveCharacterFrom.Length-1,1)) -eq $Character) 
  {
   $RemoveCharacterFrom = $RemoveCharacterFrom.Substring(0,$RemoveCharacterFrom.Length-1)
  }
 }


 Return $RemoveCharacterFrom

}

 Function Add-TrailingCharacter
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Add-TrailingCharacter
=============================================================================================================================================
.SYNOPSIS

This function adds a trailing backslash to a string
#>

param
(
 [string] $AddCharacterTo = "",
 [string] $Character      = ""
)

if($AddCharacterTo.Length -gt 0)
 { 
  $AddCharacterTo = Remove-TrailingCharacter -RemoveCharacterFrom $AddCharacterTo -Character $([char]34)
  if(($AddCharacterTo.SubString($AddCharacterTo.Length-1,1)) -ne $Character) 
  {
   $AddCharacterTo = $AddCharacterTo + $Character
  }
 }
Else
 {
  $AddCharacterTo = $Character
 }

 Return $AddCharacterTo

}

Function Get-AllFilesWithPattern
 {

  <#
  .NOTES
  ========================================================================================================================
  Created with:     Windows PowerShell ISE
  Created on:       13-January-2019
  Created by:       Willem-Jan Vroom
  Organization:     
  Functionname:     Get-AllFilesWithPattern
  ========================================================================================================================
  .SYNOPSIS

  Find all files in the given folder that matches a filter.

  #>

  param
   (
    [string] $FolderToLookIn,
    [string] $Pattern
   )

   $FolderToLookIn = Remove-TrailingCharacter -Character "\" -RemoveCharacterFrom $FolderToLookIn

   $arrItems = @()
   $arrItems = Get-ChildItem -Path $FolderToLookIn -Filter $Pattern
   $arrItems | Sort-Object -Property Name | Out-Null

   Return $arrItems
 }

Function Get-LastItemOfAnArryAndPutItInAString
 {

  <#
  .NOTES
  ========================================================================================================================
  Created with:     Windows PowerShell ISE
  Created on:       13-January-2019
  Created by:       Willem-Jan Vroom
  Organization:     
  Functionname:     Get-LastItemOfAnArryAndPutItInAString
  ========================================================================================================================
  .SYNOPSIS

  Returns the last item of string.

  #>

  param
   (
    [string] $FileName,
    [string] $OldExtension,
    [string] $NewExtension,
    [string] $WhereToLook
   )

   $arrFiles    = @()
   $strFileName = ""
   $FilePattern = $FileName -Replace($OldExtension,$NewExtension)
   $arrFiles    = Get-AllFilesWithPattern -FolderToLookIn $WhereToLook -Pattern $FilePattern
   
   if($arrFiles.Count -gt 0)
    {
     $strFileName = $arrFiles[-1].ToString()
    }
 
   Return $strFileName
}

# ========================================================================================================================
# End function block
# ========================================================================================================================

# ========================================================================================================================
# Define the variables.
# ========================================================================================================================

  $strCurrentDir          = Split-Path -parent $MyInvocation.MyCommand.Definition

  if($MSIPath.Length -eq 0)
   {
    $MSIPath = $strCurrentDir
   }
  
  if($LogLocation.Length -eq 0)
   {
    $LogLocation = $Env:Windir + "\SYSTEM32\LogFiles"
   }
      
  $LogLocation            = Add-TrailingCharacter -AddCharacterTo $LogLocation   -Character "\"
  $MSIPath                = Add-TrailingCharacter -AddCharacterTo $MSIPath       -Character "\"
  $strCurrentDir          = Add-TrailingCharacter -AddCharacterTo $strCurrentDir -Character "\"

  $strTransform           = ""
  $strDefaultPropFile     = $MSIPath + "defaultproperties.csv"

  $strDefaultProperties   = ""
  $strActivity            = ""
  $strPatch               = ""
  $strSingleOrMultipleMSI = "MultipleMSI"

  $arrDefaultProperties   = @()
  $arrMSIFiles            = @()
  $arrMSPFiles            = @()
  $arrMSIFiles            = Get-AllFilesWithPattern -FolderToLookIn $MSIPath -Pattern "*.msi"
  $numMSIFiles            = $arrMSIFiles.Count
  $numCounter             = 1

# ========================================================================================================================
# Stop the script for a non admin user
# ========================================================================================================================

  if(-not(Check-HasAdminRights))
  {
   Write-Error "The current user has no admin rights. Please rerun the script with elevated rights." -Category PermissionDenied
   Exit 999
  }

# ========================================================================================================================
# Create the log file location if not exists
# ========================================================================================================================

  if(-not (Test-Path $LogLocation))
   {
    New-Item -Path $LogLocation -ItemType Directory -Force -Confirm:$False | Out-Null
   }

# ========================================================================================================================
# Define the logfile for the install or uninstall of all the MSIs. 
# ========================================================================================================================
 
  $strLastPartOfFileName = " (" + (Get-Date).ToString('G') + ").log"
  $strLastPartOfFileName = $strLastPartOfFileName.Replace(":","-").Replace("/","-")

  if($numMSIFiles -eq 1)
   {
    $strSingleOrMultipleMSI = "SingleMSI"
   }

  if($Install -or (-not $Uninstall))
   {
    $strLogFile            = $LogLocation + $strSingleOrMultipleMSI + $strLastPartOfFileName
    $strActivity           = "Installing MSIs in the folder $MSIPath"
   }
    else
   {
    $strLogFile            = $LogLocation + "Uninstall" + $strSingleOrMultipleMSI + $strLastPartOfFileName
    $strActivity           = "Uninstalling MSIs in the folder $MSIPath"
   }
 
  CreateLogFile -LogFile $strLogFile

# ========================================================================================================================
# Give an error message if both parameters install and uninstall are used. 
# ========================================================================================================================

if($Install -and $Uninstall)
 {
  $strErrorMessage = "Both install and uninstall parameters are mentioned. That is not possible. Only one of them should be used."
  WriteToLog -line "========================================================================================================================" -LogFile $strLogFile
  WriteToLog -line "FATAL ERROR!" -LogFile $strLogFile
  WriteToLog -line $strErrorMessage -LogFile $strLogFile
  WriteToLog -line "========================================================================================================================" -LogFile $strLogFile
  Write-Error $strErrorMessage -Category InvalidArgument
  Exit 991
 }

# ========================================================================================================================
# Write default settings to the logfile
# ========================================================================================================================

  WriteToLog -line "========================================================================================================================" -LogFile $strLogFile
  WriteToLog -line "Log location: $LogLocation" -LogFile $strLogFile
  WriteToLog -line "MSI Path:     $MSIPath"     -LogFile $strLogFile
  WriteToLog -line $($strActivity + ":")        -LogFile $strLogFile

  ForEach ($objMSIFile in $arrMSIFiles)
   {
    WriteToLog -line " * $($objMSIFile.Name)" -LogFile $strLogFile
   }
  WriteToLog -line "========================================================================================================================" -LogFile $strLogFile

# ========================================================================================================================
# In case of an installation:
# Define the default properties.
# ========================================================================================================================

  if ($Install -or (-not $Uninstall))
   {
    $strDefaultProperties = Import-PropertyFile -PropertyFile $strDefaultPropFile
   }
  
# ========================================================================================================================
# Start the real installation or uninstall.
# The installation is skipped if a MSI has already been installed.
# The uninstall is only done if the product has already been installed.
# ========================================================================================================================

  ForEach ($objMSIFile in $arrMSIFiles)
   {
    Write-Progress -Activity $($strActivity + ".") -Status "Processing $objMSIFile." -PercentComplete ($numCounter / $numMSIFiles * 100)
    $strMSIFileName    = $MSIPath + $objMSIFile
    $strProductName    = Get-MSIFileInformation -Path $strMSIFileName -Property ProductName
    $strProductVersion = Get-MSIFileInformation -Path $strMSIFileName -Property ProductVersion
    $strProductCode    = Get-MSIFileInformation -Path $strMSIFileName -Property ProductCode
    $strRegPathX64     = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\"+$strProductCode
    $strRegPathX64     = $strRegPathX64 -replace(" ","")
    $strRegPathX86     = $strRegPathX64.Replace("WOW6432Node\","")
    
    if($Install -or (-not $Uninstall))
     {
           
      # ========================================================================================================================
      # The install
      # ========================================================================================================================

      $strTransform      = ""
      $strPatch          = ""
      $strMSTFileName    = Get-LastItemOfAnArryAndPutItInAString -FileName $objMSIFile -OldExtension ".msi" -NewExtension "*.mst" -WhereToLook $MSIPath
      $strMSPFileName    = Get-LastItemOfAnArryAndPutItInAString -FileName $objMSIFile -OldExtension ".msi" -NewExtension "*.msp" -WhereToLook $MSIPath
      $strPropFile       = $strMSIFileName.Replace("msi","csv")

      # ========================================================================================================================
      # Apply a patch (msp file) (if available)
      # ========================================================================================================================

      if($strMSPFileName.Length -ge 1)
       {
        $strPatch = " /update " + $([char]34) + $MSIPath + $strMSPFileName + $([char]34)
        WriteToLog -LogFile $strLogFile -line "The patch file '$strMSPFileName' has been found and is applied."
       }

      # ========================================================================================================================
      # Apply a transform file (mst) (if available)
      # ========================================================================================================================

      if($strMSTFileName.Length -ge 1)
       {
        $strMSTFileName = $MSIPath + $strMSTFileName
        $strTransform   = "TRANSFORMS=" + $([char]34) + $strMSTFileName + $([char]34)+" "
        WriteToLog -LogFile $strLogFile -line "The transform file '$strMSTFileName' has been found and is applied."
       }

      WriteToLog -line "Installing application: $strProductName"    -LogFile $strLogFile
      WriteToLog -line "ProductVersion:         $strProductVersion" -LogFile $strLogFile

      if(-not ((Test-Path $strRegPathX64) -or (Test-Path $strRegPathX86)))
       {
        $strProperties     = " "
        $strProperties     = Import-PropertyFile -PropertyFile $strPropFile
        $strMSILogFile     = "/l*v " + $([char]34) + $LogLocation + $strProductName + " " + $strProductVersion +".log" + $([char]34)
        $strArguments      = "/i "   + $([char]34) + $MSIPath + $objMSIFile + $([char]34) + $strPatch + " /qb! " + $strDefaultProperties + $strProperties + $strTransform + $strMSILogFile

        $strArguments      = $strArguments  -replace("   ","")
      
        WriteToLog -line "Command that is run:       msiexec $($strArguments)" -LogFile $strLogFile
        $StartProcess = (Start-Process -FilePath "msiexec.exe" -ArgumentList $strArguments -Wait -PassThru)
        WriteToLog -line "Result: $($StartProcess.ExitCode)" -LogFile $strLogFile
        WriteToLog -line "========================================================================================================================" -LogFile $strLogFile
       }
        else
       {
        WriteToLog -line "This application has already been installed, thus skipping." -LogFile $strLogFile
        WriteToLog -line "========================================================================================================================" -LogFile $strLogFile
       }
     }
      else
     {   
        
      # ========================================================================================================================
      # The uninstall
      # ========================================================================================================================

      WriteToLog -line "Uninstalling application: $strProductName"    -LogFile $strLogFile
      WriteToLog -line "ProductVersion:           $strProductVersion" -LogFile $strLogFile
      
      if((Test-Path $strRegPathX64) -or (Test-Path $strRegPathX86))
       {
        $strMSILogFile     = "/l*v " + $([char]34) + $LogLocation + "Uninstall_"+ $strProductName + " " + $strProductVersion +".log" + $([char]34)
        $strArguments      = "/x "   + $strProductCode + " /qb! " + $strMSILogFile

        $strArguments      = $strArguments  -replace("   ","")

        WriteToLog -line "Command that is run:         msiexec $($strArguments)" -LogFile $strLogFile
        $StartProcess = (Start-Process -FilePath "msiexec.exe" -ArgumentList $strArguments -Wait -PassThru)
        WriteToLog -line "Result: $($StartProcess.ExitCode)" -LogFile $strLogFile

        WriteToLog -line "========================================================================================================================" -LogFile $strLogFile
       }
        else
       {
        WriteToLog -line "This application has not been installed, thus skipping." -LogFile $strLogFile
        WriteToLog -line " " -LogFile $strLogFile
       }
     }
    $numCounter++
   }

# ========================================================================================================================
# Done!
# ========================================================================================================================

Current version: install_all_msi (v10).zip




All expired user accounts and the accounts that are about to expire

A colleague of mine asked me to write a script with the Active Directory users with an account about to expire. So I created this script for him:

Account with an expiry date.

Account with an expiry date.


$arrUsers = @()
$arrOU    = @("OU=Users,OU=OU1,DC=testdomain,DC=local,DC=lan","OU=Users,OU=OU2,DC=testdomain,DC=local,DC=lan")

ForEach($objOU in $arrOU)
 {
  $arrUsers += (Search-ADAccount -AccountExpiring -SearchBase $objOU -UsersOnly) | Select-Object "AccountExpirationDate","Name",@{Name="OU";Expression={($_."DistinguishedName" -split "=",3)[-1]}},"Enabled","LastLogonDate","LockedOut","ObjectClass","PasswordExpired","PasswordNeverExpires","SamAccountName","UserPrincipalName"
 }
$arrUsers | Sort-Object -Property "OU","Name" | Export-Csv "c:\temp\usersabouttoexpire.csv" -NoTypeInformation

It does what it should do, but not flexible. It can be used in one situation but needs modification for other situations. Also, you cannot specify the number of days when the account is about to expire.

So I created the script AccountsAboutToExpire (v10).ps1.

There are some parameters:

  • OUs: Specify the OUs, seperated by a comma.
  • IncludeChildOUs: Use this switch to include the child OU’s.
  • NumberOfDaysToExpirationDate: Specify the number of days in which the account expires. Default = 7

This script does not only give the accounts that are about to expire, but also the expired user accounts.

A demonstration of this script can be found on my YouTube channel: the script and AppV Repository or view it below:

The script:

<#
.SYNOPSIS
    Gives the accounts that about to expire within the given period.

.DESCRIPTION
    This script gives the accounts that are about to expire within the given period in a CSV file.

.EXAMPLE
    Reports all the accounts that are about to expire in the OUs OU=Users,OU=OU1,DC=testdomain,DC=local,DC=lan and OU=Users,OU=OU2,DC=testdomain,DC=local,DC=lan
    ."\AccountsAboutToExpire V10).ps1" -OUs "OU=Users,OU=OU1,DC=testdomain,DC=local,DC=lan","OU=Users,OU=OU2,DC=testdomain,DC=local,DC=lan"

.EXAMPLE
    Reports all the accounts that are about to expire in the OU DC=testdomain,DC=local,DC=lan, including the child OU's
    ."\AccountsAboutToExpire V10).ps1" -OUs "DC=testdomain,DC=local,DC=lan" -IncludeChildOUs

.EXAMPLE
    Reports all the accounts that are about to expire witin 60 days in the OU DC=testdomain,DC=local,DC=lan, including the child OU's
    ."\AccountsAboutToExpire V10).ps1" -OUs "DC=testdomain,DC=local,DC=lan" -IncludeChildOUs -NumberOfDaysToExpirationDate 60

.NOTES
    Author:  Willem-Jan Vroom
    Website: 
    Twitter: @TheStingPilot

v0.1:
   * Initial version. 

v1.0:
   * Included:
      - All the expired user accounts
      - Sort on expiration date. 
#>

[CmdLetBinding()]

param
(
# Specify the OUs, seperated by a comma. 
[Parameter(Mandatory=$True)]
[string[]] $OUs,

# Use this switch to include the child OU's. 
[Parameter(Mandatory=$False)]
[switch]  $IncludeChildOUs,

# Specify the number of days in which the account expires. Default = 7
[Parameter(Mandatory=$False)]
[string]  $NumberOfDaysToExpirationDate=7
)

# =============================================================================================================================================
# Function block
# =============================================================================================================================================

  Function Get-AccountsAboutToExpire
   {
  
    param
     (
      [string] $OU,
      [Switch] $InclChildOUs,
      [string] $NumDays
     )

    $arrItems       = @()
    $arrAboutToExp  = New-Object PSObject
    $arrExpired     = New-Object PSObject
    $strSearchScope = "OneLevel"

    if($InclChildOUs)
     {
      $strSearchScope = "SubTree"
     } 

    Try
     {
      $arrAboutToExp  = (Search-ADAccount -AccountExpiring -SearchBase $objOU -UsersOnly -SearchScope $strSearchScope -TimeSpan $NumDays) | Select-Object "AccountExpirationDate","Name",@{Name="OU";Expression={($_."DistinguishedName" -split "=",3)[-1]}},@{Name="ExpiredAccount";Expression={$False}},"Enabled","LastLogonDate","LockedOut","PasswordExpired","PasswordNeverExpires"
      $arrExpired     = (Search-ADAccount -AccountExpired  -SearchBase $objOU -UsersOnly -SearchScope $strSearchScope)                    | Select-Object "AccountExpirationDate","Name",@{Name="OU";Expression={($_."DistinguishedName" -split "=",3)[-1]}},@{Name="ExpiredAccount";Expression={$True}}, "Enabled","LastLogonDate","LockedOut","PasswordExpired","PasswordNeverExpires" 
      $arrItems      += $arrAboutToExp
      $arrItems      += $arrExpired
     }
      Catch
     {
      Write-Warning "The OU $OU has not been found."
     }

    Return $arrItems

   }

Function Check-HasAdminRights
 {

  <#
  .NOTES
  ========================================================================================================================
  Created with:     Windows PowerShell ISE
  Created on:       11-January-2019
  Created by:       Willem-Jan Vroom
  Organization:     
  Functionname:     Check-HasAdminRights
  ========================================================================================================================
  .SYNOPSIS

  This function checks if an user has admin rights. The function returns $true or $false

  #>

  If (([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator"))
   {
    Return $True
   }
    else
   {
    Return $False
   }
 }

# =============================================================================================================================================
# End function block
# =============================================================================================================================================

# ========================================================================================================================
# Stop the script for a non admin user
# ========================================================================================================================

  if(-not(Check-HasAdminRights))
  {
   Write-Error "The current user has no admin rights. Please rerun the script with elevated rights." -Category PermissionDenied
   Exit 999
  }

# =============================================================================================================================================
# Check if the module ActiveDirectory has been loaded.
# =============================================================================================================================================

  if(-not(Get-Module -ListAvailable ActiveDirectory))
   {
    Write-Warning "The module ActiveDirectory is not found. Thus quitting."
    Exit 9
   }

# =============================================================================================================================================
# Define the variables
# =============================================================================================================================================

  $arrUsers      = @()
  $strCurrentDir = Split-Path -parent $MyInvocation.MyCommand.Definition
  
# =============================================================================================================================================
# Define the output file
# =============================================================================================================================================

  $strOutputFile = "UserAccounts About to Expire (" + (Get-Date).ToString('G') + ").csv"
  $strOutputFile = $strOutputFile.replace(":","-").Replace("/","-")
  $strOutputFile = $strCurrentDir + "\" + $strOutputFile  

# =============================================================================================================================================
# Process the OUs.
# =============================================================================================================================================

  ForEach($objOU in $OUs)
   {
    if($IncludeChildOUs)
     {
      $arrUsers += Get-AccountsAboutToExpire -OU $objOU -NumDays $NumberOfDaysToExpirationDate -InclChildOUs
     }
      else
     {
      $arrUsers += Get-AccountsAboutToExpire -OU $objOU -NumDays $NumberOfDaysToExpirationDate
     }
   }

# =============================================================================================================================================
# Write the output to a csv file.
# =============================================================================================================================================
  
  $arrUsers | Sort-Object -Property "AccountExpirationDate","OU","Name" | Export-Csv $strOutputFile -NoTypeInformation

Get default help.

Get default help.

 

Get detailed help.

Get detailed help.

 

Output in Excel.

Output in Excel.

 

Current version: AccountsAboutToExpire (v10)

Previous versions:




Improved script to migrate users from Windows 7 to Windows 10 automatically

In my article, I wrote about a script to perform a user migration. In the meantime, that script has been improved to perform a more efficient and errorless migration.

The following improvements have been implemented:

  • Provide a mappingsfile for an automatic migration. That mappingsfile contains both the old and the new group name.
  • Perform a rollback in case of inpredicted results.
  • In case of different profile locations: copy the old desktop and the old favorites to a new location.
  • Improved help

To do this, I created a Powershell script.

The users who need to be migrated from Windows 7 to Windows 10 are in a csv file:

  • The column Userid contains the userid
  • The column NewOU contains the OU where the user should be moved to. This column can be empty.
  • The colum VDIGroup contains the new VDI group. This column can be empty.
  • The colum GroupsToAdd contains the groups the user should be added to. Split the groups with a comma. This column can be empty.
  • The colum GroupsToRemove contains the groups the user should be removed from. Split the groups with a comma. This column can be empty.

An example:

"Userid","NewOU","VDIGroup","GroupsToAdd","GroupsToRemove"
"userid1","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","Citrix VDI Windows 10","Appl_Group1,Appl_Group2,Appl_Group3","old_appl_group1,old_appl_group2,old_appl_group3,old_appl_group4"
"userid2","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","Citrix VDI Windows 10","",""
"userid3","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","","",""
"userid4","","","","old_appl_group1,old_appl_group2"

Parameters for a migration:

  • FileWithUseridsInCSVFormat: CSV Filename that contains all the userids that should be migrated. Default = the script name, with the csv extension.
  • FileForAutomaticMigration: CSV filename that contains the old and new group name for a fully automated migration. If not specified, then there is no automatic migration. Default is empty.
  • LogFilePrefix: The name the logfile starts with. So the logfiles are grouped together.
  • ProductionRun: Use this swith to update Active Directory. If not specified, the script is run in test mode.
  • CreateRollBackFile: Use this switch to create a rollback file. That makes it possible to perform a rollback in case of unpredicted behavior.
  • FullCleanUp: Use this swith to remove all unneeded groups.
  • RemoveOldADGroups: Use this switch to remove the old AD groups in case of an automated migration.
  • ClearProfilePath: Use this switch to clear the profile path.
  • ClearHomeFolder: Use this swicth to clear the drive mapping and home folder.
  • RemoveOldCitrixGroups: Use this switch to remove all the old Citrix groups from the users’ account.
  • RunDirectGroupInventory: Use this switch to display all the direct group membership in the result file.
  • RunIndirectGroupInventory: Use this switch to display all the indirect group membership in the result file.

Parameters for an user inventory, based on an OU:

  • CreateFileWithUseridsInCSVFormat: Use this switch to create a File with all the userids to migrate based on an OU.
  • srcOU: The OU (distinguised name) where all the userids are found that needs to be migrated. If the OU contains spaces, then add quotes around the OU name.
  • dstOU: The destination OU (distinguised name). If this one can is empty then the users are not moved to another OU. If the OU contains spaces, then add quotes around the OU name.
  • Windows10VDIGroup: The new Windows 10 VDI group. If the VDI group contains spaces, then add quotes around it.

Parameters to copy the desktop and internet explorer favorites to another location:

  • FileWithUseridsInCSVFormat: CSV Filename that contains all the userids that should be migrated. Default = the script name, with the csv extension.
  • ProfilePathFrom: The old profile path (exluding userid). If the profile path contains spaces, then add quotes around it.
  • ProfilePathTo: The new profile path (exluding userid). If the profile path contains spaces, then add quotes around it.
  • ProductionRun: Use this switch to update Active Directory. If not specified, the script is run in test mode.

The script:

<#
.SYNOPSIS
    Helps to migrate users from - for example - Windows 7 to Windows 10.
 
.DESCRIPTION
    This script helps to migrate users from Windows 7 to Windows 10. It offers the following options:
    * Run an inventory from all the users in an OU and al its sub OU's.
    * Move the users from one OU to another OU.
    * Add the users to a new group, based on a group mapping file.
    * Run the script is a test modus, so nothing is changed.
    * Check the users' indirect group membership.
    * Copy content from the old profile to the new profile.
 
.EXAMPLE
     Perform the actions as described in the CSV Input File 'Userids-to-migrate.csv' in test mode:
     ."\Migrate users (v10).ps1" -FileWithUseridsInCSVFormat Userids-to-migrate.csv

.EXAMPLE
     Perform the actions as described in the CSV Input File 'Userids-to-migrate.csv' and add the groups as per mappingsfile 'MappingOldGroupToNewGroup.csv' in test mode:
     ."\Migrate users (v10).ps1" -FileWithUseridsInCSVFormat Userids-to-migrate.csv -FileForAutomaticMigration MappingOldGroupToNewGroup.csv
 
.EXAMPLE
     Perform the actions as described in the CSV Input File 'Userids-to-migrate.csv' and add the groups as per mappingsfile 'MappingOldGroupToNewGroup.csv' in production:
     ."\Migrate users (v10).ps1" -FileWithUseridsInCSVFormat Userids-to-migrate.csv -FileForAutomaticMigration MappingOldGroupToNewGroup.csv -ProductionRun

.EXAMPLE
     Perform the actions as described in the CSV Input File 'Userids-to-migrate.csv', add the groups as per mappingsfile 'MappingOldGroupToNewGroup.csv' in production and create a rollback file:
     ."\Migrate users (v10).ps1" -FileWithUseridsInCSVFormat Userids-to-migrate.csv -FileForAutomaticMigration MappingOldGroupToNewGroup.csv -ProductionRun -CreateRollBackFile

.EXAMPLE
     Perform a rollback in production
     ."\Migrate users (v10).ps1" -FileWithUseridsInCSVFormat RBF_.csv -ProductionRun

.EXAMPLE
     Perform the actions as described in the CSV Input File 'Userids-to-migrate.csv' and add the groups as per mappingsfile 'MappingOldGroupToNewGroup.csv' in production.
     Also run both the direct- and indirect group membership inventory:
     ."\Migrate users (v10).ps1" -FileWithUseridsInCSVFormat Userids-to-migrate.csv -FileForAutomaticMigration MappingOldGroupToNewGroup.csv -ProductionRun -RunDirectGroupInventory -RunIndirectGroupInventory

.EXAMPLE
     Perform the actions as described in the CSV Input File 'Userids-to-migrate.csv', add the groups as per mappingsfile 'MappingOldGroupToNewGroup.csv' and remove the old group in production:
     ."\Migrate users (v10).ps1" -FileWithUseridsInCSVFormat Userids-to-migrate.csv -FileForAutomaticMigration MappingOldGroupToNewGroup.csv -ProductionRun -RemoveOldADGroups

.EXAMPLE
     Perform the actions as described in the CSV Input File 'Userids-to-migrate.csv', add the groups as per mappingsfile 'MappingOldGroupToNewGroup.csv', remove the old group in production and run the inventory for indirect group membership:
     ."\Migrate users (v10).ps1" -FileWithUseridsInCSVFormat Userids-to-migrate.csv -FileForAutomaticMigration MappingOldGroupToNewGroup.csv -ProductionRun -RemoveOldADGroups -RunIndirectGroupInventory

.EXAMPLE
     Create a csv file with all the userids to migrate from the OU testdomain.local.lan\OU1\Old OU
     ."\Migrate users (v10).ps1" -CreateFileWithUseridsInCSVFormat -srcOU "OU=Old OU,OU=OU1,DC=testdomain,DC=local,DC=lan"
     
.EXAMPLE
     Create a csv file with all the userids to migrate from the OU testdomain.local.lan\OU1\Ould OU. The new Citrix VDI group is 'Citrix10VDI'
     ."\Migrate users (v10).ps1" -CreateFileWithUseridsInCSVFormat -srcOU "OU=Old OU,OU=OU1,DC=testdomain,DC=local,DC=lan" -Windows10VDIGroup Citrix10VDI

.EXAMPLE
     Create a csv file with all the userids to migrate from the OU testdomain.local.lan\OU1\Ould OU. The new OU is testdomain.local.lan\OU1\OU2\Users. The new Citrix VDI group is 'Citrix10VDI'
     ."\Migrate users (v10).ps1" -CreateFileWithUseridsInCSVFormat -srcOU "OU=Old OU,OU=OU1,DC=testdomain,DC=local,DC=lan" -dstOU "OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan" -Windows10VDIGroup Citrix10VDI

.EXAMPLE
     Migrate the IE favorites and the desktop folder from the old profile to the new profile in test mode:
     ."\Migrate users (v10).ps1" -FileWithUseridsInCSVFormat Userids-to-migrate.csv -copyProfile -ProfilePathFrom "\\server\share\w 7" -ProfilePathTo \\server\share\w10

.EXAMPLE
     Migrate the IE favorites and the desktop folder from the old profile to the new profile in production:
     ."\Migrate users (v10).ps1" -FileWithUseridsInCSVFormat Userids-to-migrate.csv -copyProfile -ProfilePathFrom "\\server\share\w 7" -ProfilePathTo \\server\share\w10 -ProductionRun

.NOTES
    Author:  Willem-Jan Vroom
    Website: https://www.vroom.cc/
    Twitter: @TheStingPilot

 v0.1:
   * Initial version.

 v0.2:
   * The logfilename has been changed.

 v0.3:
   * The logfile mentions 'RUNNING IN TEST MODE' in case the swith -ProductionRun is not used.

 v1.0:
   * Introduction automatic migration.
   * Introduction Indirect Group Inventory
   * Automatic creation of the File with the userids to migrate in CSV format.
   * Cleanup old Citrix groups
   * The results log file has a different lay-out
   * Parametersetnames
   * Improved help.
   * Copy desktop and favorites from the old profile to the new profile.
   * Clear the homedir path in Active Directory.
   * Rollback scenario has been added.
#>

[CmdLetBinding()]

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

# CSV Filename that contains all the userids that should be migrated. Default = the script name, with the csv extension.
# The filename with the users to be modified.
#
# This file has the following layout:
#
# "Userid","NewOU","VDIGroup","GroupsToAdd","GroupsToRemove"
# "userid1","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","Citrix VDI Windows 10","Appl_Group1,Appl_Group2,Appl_Group3","old_appl_group1,old_appl_group2,old_appl_group3,old_appl_group4"
# "userid2","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","Citrix VDI Windows 10","",""
# "userid3","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","","",""
# "userid4","","","","old_appl_group1,old_appl_group2"
#
# You can add more columns, but these columns are ignored. 
[Parameter(Mandatory=$False,
           ParameterSetName="CopyProfile")]
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[String] $FileWithUseridsInCSVFormat = "",

# CSV filename that contains the old and new group name for a fully automated migration. If not specified, then there is no automatic migration. Defualt is empty.
#
# This file has the following layout:
#
# "OldGroup","NewGroup"
# "gg_appl_old1","appl_new1"
# "gg_appl_old2","appl_new2"
# "gg_appl_old1","appl_new3"
# "gg_appl_old4","appl_new4"
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[String] $FileForAutomaticMigration = "",

# The name the logfile starts with. So the logfiles are grouped together.
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[Parameter(Mandatory=$False,
           ParameterSetName="CopyProfile")]

[String] $LogFilePrefix = "ZZZ-Logfile_",

# Use this swith to update Active Directory. If not specified, the script is run in test mode.
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[Parameter(Mandatory=$False,
           ParameterSetName="CopyProfile")]
[Switch] $ProductionRun,

# Use this swith to create a rollback file. That makes it possible to perform a rollback in case of unpredicted behavior.
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[Switch] $CreateRollBackFile,

# Use this swith to remove all unneeded groups.
# If this switch is used then the following groups will be removed from the users' account:
# * All gg_appl groups
# * WM-Users
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[Switch] $FullCleanUp,

# Use this swith to remove the old AD groups in case of an automated migration.
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[Switch] $RemoveOldADGroups,

# Use this swith to clear the profile path.
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[Switch] $ClearProfilePath,

# Use this swith to clear the drive mapping and home folder.
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[Switch] $ClearHomeFolder,

# Use this swith to remove all the old Citrix groups from the users' account.
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[Switch] $RemoveOldCitrixGroups,

# Use this swith to display all the direct group membership in the result file.
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[Switch] $RunDirectGroupInventory,

# Use this swith to display all the indirect group membership in the result file.
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[Switch] $RunIndirectGroupInventory,

# Use this swith to create a File with all the userids to migrate based on an OU.
[Parameter(Mandatory=$False,
           ParameterSetName="CreateFileWithUseridsInCSVFormat")]
[Switch] $CreateFileWithUseridsInCSVFormat,

# The OU (distinguised name) where all the userids are found that needs to be migrated. If the OU contains spaces, then add quotes around the OU name.
[Parameter(Mandatory=$True,
           ParameterSetName="CreateFileWithUseridsInCSVFormat")]
[string] $srcOU,

# The destination OU (distinguised name). If this one can is empty then the users are not moved to another OU. If the OU contains spaces, then add quotes around the OU name.
[Parameter(Mandatory=$False,
           ParameterSetName="CreateFileWithUseridsInCSVFormat")]
[string] $dstOU,

# The new Windows 10 VDI group. If the VDI group contains spaces, then add quotes around it.
[Parameter(Mandatory=$False,
           ParameterSetName="CreateFileWithUseridsInCSVFormat")]
[string] $Windows10VDIGroup="",

# Use this switch if you want to copy desktop and favorites from the users' old profile to the users' new profile.
[Parameter(Mandatory=$False,
           ParameterSetName="CopyProfile")]
[switch] $copyProfile,

# The old profile path (exluding userid). If the profile path contains spaces, then add quotes around it.
[Parameter(Mandatory=$True,
           ParameterSetName="CopyProfile")]
[string] $ProfilePathFrom="",

# The new profile path (exluding userid). If the profile path contains spaces, then add quotes around it.
[Parameter(Mandatory=$True,
           ParameterSetName="CopyProfile")]
[string] $ProfilePathTo=""

)

# =============================================================================================================================================
# Function block
# =============================================================================================================================================

Function Write-EntryToResultsFile
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       03-August-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Write-EntryToResultsFile
=============================================================================================================================================
.SYNOPSIS

This function adds the success or failure information to the array that contains the log
information.

#>
param
 (
  [string] $strUserid,
  [string] $Result  = "",
  [string] $Action  = "",
  [string] $Message = ""
  
 )
 $Record                = [ordered] @{"Timestamp"="";"Username" = "";"Result"= "";"Action"= "";"Message"= ""}
 $Record."Timestamp"    = (Get-Date -UFormat "%a %e %b %Y %X").ToString()
 $Record."Username"     = $strUserid
 $Record."Result"       = $Result
 $Record."Action"       = $Action
 $Record."Information"  = $Message
 $objRecord             = New-Object PSObject -Property $Record
 $Global:arrTable      += $objRecord
}

Function Export-ResultsLogFileToCSV
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Export-ResultsLogFileToCSV
=============================================================================================================================================
.SYNOPSIS

This function writes the logfile content to a CSV file.

#>

 if($Global:arrTable.Count -gt 0)
   {
    $Global:arrTable | Export-Csv $strCSVLogFileSucces -NoTypeInformation
   } 
    Else
   {
    Write-Host "Something went wrong while writing the logfile $strCSVLogFileSucces. Maybe nothing to report..."
   } 
}

Function Export-RollBackFileToCSV
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       29-November-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Export-RollBackFileToCSV
=============================================================================================================================================
.SYNOPSIS

This function writes the rollbackfile content to a CSV file.

#>

 if($Global:arrTableWithRollBackRecords.Count -gt 0)
   {
    $Global:arrTableWithRollBackRecords | Export-Csv $strCSVRollBackFile -NoTypeInformation
   } 
    Else
   {
    Write-Host "Something went wrong while writing the file $strCSVRollBackFile. Maybe nothing to report..."
   } 
}

Function Remove-ProfilePathFromUserProfileInAD
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Remove-ProfilePathFromUserProfileInAD
=============================================================================================================================================
.SYNOPSIS

This function clears the ProfilePath from AD.

#>

  param
   (
    [string] $strUserid
   )

   Write-EntryToResultsFile -strUserid $strUserid -Message "Profilepath: $((Get-ADUser -Identity $strUserid -Properties profilePath).profilePath)" -Action "Inventory" -Result "Success"

   Try
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Set-ADUser -Identity $strUserDN -Clear profilePath -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Clear profile path" -Result "Success"
      }
       Catch
      {
       Write-EntryToResultsFile -strUserid $strUserid -Message $_.Exception.Message -Action "Clear profile path" -Result "Error"
       Continue
      }
}

Function Remove-HomeFolderPathFromUserProfileInAD
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Remove-HomeFolderPathFromUserProfileInAD
=============================================================================================================================================
.SYNOPSIS

This function clears the Home Folder Path from AD.

#>

  param
   (
    [string] $strUserid
   )

   Write-EntryToResultsFile -strUserid $strUserid -Message "Homedrive:     $((Get-ADUser -Identity $strUserid -Properties homeDrive).homeDrive)" -Action "Inventory" -Result "Success"
   Write-EntryToResultsFile -strUserid $strUserid -Message "Homedirectory: $((Get-ADUser -Identity $strUserid -Properties homeDirectory).homeDirectory)" -Action "Inventory" -Result "Success"

   Try
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Set-ADUser -Identity $strUserDN -Clear homeDirectory -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Clear homedirectory path" -Result "Success"
      }
       Catch
      {
       Write-EntryToResultsFile -strUserid $strUserid -Message $_.Exception.Message -Action "Clear homedirectory path" -Result "Error"
       Continue
      }

    Try
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Set-ADUser -Identity $strUserDN -Clear homeDrive -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Clear homedrive" -Result "Success"
      }
       Catch
      {
       Write-EntryToResultsFile -strUserid $strUserid -Message $_.Exception.Message -Action "Clear homedrive" -Result "Error"
       Continue
      }
}

Function Move-ADUserToOtherOU
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Move-ADUserToOtherOU
=============================================================================================================================================
.SYNOPSIS

This function moves the user to another OU.

#>

  param
   (
    [string] $strUserid,
    [string] $strDestinationOU
   )
   Try
     {
      if($strDestinationOU.Length -gt 0)
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Move-ADObject -Identity $strUserDN -TargetPath $strDestinationOU -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Move AD User to OU $strDestinationOU." -Result "Success"
      }
     }
      Catch
     {
      Write-EntryToResultsFile -strUserid $strUserid -Message $_.Exception.Message -Action "Move AD User to OU $strDestinationOU." -Result "Error"
      Continue
     }
}

Function Add-ADMemberToGroup
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Add-ADMemberToGroup
=============================================================================================================================================
.SYNOPSIS

This function adds a user to an AD group.

#>

  param
   (
    [string] $strUserid,
    [string] $strADGroupName
   )
   Try
     {
      if($strADGroupName.Length -gt 0)
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Add-ADGroupMember -Identity $strADGroupName -Members $strUserDN -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Add AD group $strADGroupName to user $strUserid." -Result "Success"
       if($Global:strForRollBackGroupsToRemove.Length -eq 0)
       {
        $Global:strForRollBackGroupsToRemove = $strADGroupName
       }
       else
       {
        $Global:strForRollBackGroupsToRemove = $Global:strForRollBackGroupsToRemove + "," + $strADGroupName
       }
      }
     }
      Catch
     {
      Write-EntryToResultsFile -strUserid $strUserid -Message $_.Exception.Message -Action "Add AD group $strADGroupName to user $strUserid." -Result "Error"
      Continue
     }
}

Function Remove-ADMemberFromGroup
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Remove-ADMemberFromGroup
=============================================================================================================================================
.SYNOPSIS

This function removes a user from an AD group.

#>

  param
   (
    [string] $strUserid,
    [string] $strADGroupName
   )
   Try
     {
      if($strADGroupName.Length -gt 0)
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Remove-ADGroupMember -Identity $strADGroupName -Members $strUserDN -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Remove AD group $strADGroupName from user $strUserid." -Result "Success"
       if($Global:strForRollBackGroupsToAdd.Length -eq 0)
       {
        $Global:strForRollBackGroupsToAdd = $strADGroupName
       }
       else
       {
        $Global:strForRollBackGroupsToAdd = $Global:strForRollBackGroupsToAdd + "," + $strADGroupName
       }

      }
     }
      Catch
     {
      Write-EntryToResultsFile -strUserid $strUserid -Message $_.Exception.Message -Action "Remove AD group $strADGroupName from user $strUserid." -Result "Error"
      Continue
     }
}

Function Remove-UserFromMultipleGroups
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       12-October-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Remove-UserFromMultipleGroups
=============================================================================================================================================
.SYNOPSIS

Delete multiple groups from the users' account. The group names are in an array.

#>

  param
   (
    [string] $arrGroupsToSearchFor,
    [string] $strUserid
   )

    $arrGroups = @(Get-ADUser $strUserid -Properties MemberOf).MemberOf
    forEach($objGroup in $arrGroups)
    {
    $strGroup=(Get-ADGroup $objGroup).Name
    forEach($objGroupMustContain in $arrGroupsToSearchFor)
     {
      $strGroupMustContain = $objGroupMustContain.ToString().ToLower()
      if($strGroup.ToLower().IndexOf($strGroupMustContain) -eq 0)
       {
        Remove-ADMemberFromGroup -strUserid $strUserid -strADGroupName $strGroup
       }  
     }
    }


}

Function Migrate-ADUserToNewGroup
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       11-October-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Migrate-ADUserToNewGroup
=============================================================================================================================================
.SYNOPSIS

Perform the translating from the old group name to the new group name.

#>

  param
   (
    [string] $strUserid,
    [string] $strADGroupName
   )
    
   $bolOldGroupNameHasBeenFound = $False
   
   ForEach($objGroupNameForAutomaticMigration in $arrGroupNamesForAutomaticMigration)
    {
     Try
     {
      $strOldGroup = $objGroupNameForAutomaticMigration.OldGroup
      $strNewGroup = $objGroupNameForAutomaticMigration.NewGroup
     }
      Catch
     {
      Write-Host "There is something wrong with the CSV filename $FileForAutomaticMigration while processing $strUserid."
      Exit 1 
     }
     if ($strADGroupName -eq $strOldGroup)
     {
      $bolOldGroupNameHasBeenFound = $true
      if ($strNewGroup.Length -gt 0)
       {
        Add-ADMemberToGroup -strUserid $strUserid -strADGroupName $strNewGroup
        if ($RemoveOldADGroups)
         {
          Remove-ADMemberFromGroup -strUserid $strUserid -strADGroupName $strOldGroup
         }    
       }
        Else
       {
        Write-EntryToResultsFile -strUserid $strUserid -Action "Migrate users" -Message "No new groupname specified for $strADGroupName in $FileForAutomaticMigration." -Result "Error"
       }
     }

    }
    if(-not($bolOldGroupNameHasBeenFound))
    {
     Write-EntryToResultsFile -strUserid $strUserid -Action "Migrate users" -Message "The group $strADGroupName has not been found in $FileForAutomaticMigration." -Result "Error"
    }
  }

Function Find-InArry
{
<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       17-October-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Find-InArry
=============================================================================================================================================
.SYNOPSIS

This function checks two arrays for content. I had to write my own function as the build-in functions like
Compare-Object or $arrOnlyIndirectMembers | Where {$arrDirectAndIndirectMembers -notcontains $arrDirectMembers}
did not did their work properly. 

The swith 'NotContains' can be $True or $False. If not specified on the command line, then it is $False.

#>


param
(
 [string[]] $SearchIn,
 [string[]] $LookFor,
 [switch] $NotContains
)

$Contains = -not $NotContains

$tmpArray = @()
ForEach ($objSearchIn in $SearchIn)
 {
  $bolFound = $false
  ForEach ($objLookFor in $LookFor)
  {
   if($objSearchIn -eq $objLookFor)
    {
     $bolFound = $True
     if ($bolFound -and $Contains)
     {
      $tmpArray+=$objLookFor
     }
    } 
  }
  if (-not($bolFound) -and -not $Contains)
  {
   $tmpArray+=$objSearchIn
  }
 }
 Return $tmpArray
}

Function Show-IndirectGroupsInResultsFile
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       18-October-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Show-IndirectGroupsInResultsFile
=============================================================================================================================================
.SYNOPSIS

This function shows only the indirect group membership in the results file.

#>

  param
   (
    [string] $strUserid
   )

   $dn                          = (Get-ADUser $strUserid).DistinguishedName
   $arrOnlyDirectMembers        = @()
   $arrDirectAndIndirectMembers = @()
   $arrOnlyIndirectMembers      = @()
      
   $arrDirectAndIndirectMembers = Get-ADGroup  -LDAPFilter ("(member:1.2.840.113556.1.4.1941:={0})" -f $dn)   | Select-Object sAMAccountName | Sort-Object sAMAccountName
   $arrOnlyDirectMembers        = Get-ADPrincipalGroupMembership $dn                                          | Select-Object sAMAccountName | Sort-Object sAMAccountName
   $arrOnlyIndirectMembers      = Find-InArry -SearchIn $arrDirectAndIndirectMembers -NotContains -LookFor $arrOnlyDirectMembers

   $arrOnlyIndirectMembers | ForEach($_) {
    $tmpValue = $_
    $tmpValue = $TmpValue -Replace("@{sAMAccountName=","")
    $tmpValue = $TmpValue -Replace("}","")
    Write-EntryToResultsFile -strUserid $strUserid -Result "Information" -Action "Indirect group member" -Message $TmpValue
   }

}

Function Show-DirectGroupsInResultsFile
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       15-November-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Show-DirectGroupsInResultsFile
=============================================================================================================================================
.SYNOPSIS

This function shows only the direct group membership in the results file.

#>

  param
   (
    [string] $strUserid
   )

   $arrOnlyDirectMembers        = @()
     
   $arrOnlyDirectMembers        = Get-ADPrincipalGroupMembership ((Get-ADUser $strUserid).DistinguishedName)  | Select-Object sAMAccountName | Sort-Object sAMAccountName
   
   $arrOnlyDirectMembers | ForEach($_) {
    $tmpValue = $_.sAMAccountName    
    Write-EntryToResultsFile -strUserid $strUserid -Result "Information" -Action "Direct group member" -Message $TmpValue
   }

}

Function Create-FileWithUseridsInCSVFormat
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       18-October-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Create-FileWithUseridsInCSVFormat
=============================================================================================================================================
.SYNOPSIS

This function cretes the file with all the userids to migrate, based on OU.

#>
  Param
    (
     [string] $srcOU ="",
     [string] $dstOU= "",
     [string] $Windows10VDIGroup= ""
    )

   $strBaseFileName = "UseridsToMigrate (" + (Get-Date).ToString('G') + ").csv"
   $strBaseFileName = $strBaseFileName -replace ":","-"
   $strBaseFileName = $strBaseFileName -replace "/","-"
   $strFileName     = $strCurrentPath + "\" + $strBaseFileName
      
   if($strFileName.Length -gt 260)
   {
    $valLength       = ($strCurrentPath.Length)-4
    $strBaseFileName = ($strBaseFileName.Substring(0,260-$valLength)) + ".csv"
    $strFileName     = $strCurrentPath + "\" + $strBaseFileName
   }

   $arrTableWithUserids = @()
   $arrUserids          = (Get-ADUser -Filter {Enabled -eq "True"} -SearchScope Subtree -SearchBase $srcOU)
   $valCounter          = 1

   $Users = ForEach ($objUserid in $arrUserids)
   {
    $strUserDetails = Get-ADUser $objUserid -Properties Name,sAMAccountName,department,cn, DistinguishedName, mail
    Write-Progress -Activity "Create CSV File with the userids for migration" -Status "Processing user $($strUserDetails.cn)." -PercentComplete ($valCounter / $arrUserids.Count * 100)
    $UserRecord                   = [ordered] @{"Userid" = "";"Full name"= "";"Mail"="";"Department"= "";"CurrentOU"= "";"NewOU"="";"VDIGroup"= "";"GroupsToAdd"= "";"GroupsToRemove"= ""}
    $UserRecord."Userid"          = $strUserDetails.sAMAccountName
    $UserRecord."Full name"       = $strUserDetails.cn
    $UserRecord."Mail"            = $strUserDetails.mail
    $UserRecord."Department"      = $strUserDetails.department
    $UserRecord."CurrentOU"       = "OU="+ ($strUserDetails.DistinguishedName -split "=",3)[-1]
    $UserRecord."NewOU"           = $dstOU
    $UserRecord."VDIGroup"        = $Windows10VDIGroup
    $UserRecord."GroupsToAdd"     = ""
    $UserRecord."GroupsToRemove"  = ""
    $objRecordWithUserids         = New-Object PSObject -Property $UserRecord
    $arrTableWithUserids         += $objRecordWithUserids
    $valCounter++
   }

   If($arrTableWithUserids.Count -gt 0)
   {
    $arrTableWithUserids | Export-Csv $strFileName -NoTypeInformation
   }
    Else
   {
    Write-Host "Something went wrong while writing the file $strFileName. Maybe nothing to report..."
   } 
}

Function Add-TrailingBackSlash
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Add-TrailingBackSlash
=============================================================================================================================================
.SYNOPSIS

This function adds a trailing backslash to a string
#>

param
(
 [string] $AddBackslashTo ="" 
)

if($AddBackslashTo.Length -gt 0)
 { 
  if(($AddBackslashTo.SubString($AddBackslashTo.Length-1,1)) -ne "\") 
  {
   $AddBackslashTo = $AddBackslashTo + "\"
  }
 }
Else
 {
  $AddBackslashTo = "\"
 }

 Return $AddBackslashTo

}

Function Copy-UserProfileFiles
{
<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Copy-UserProfileFiles
Source:           https://techblog.dorogin.com/powershell-how-to-recursively-copy-a-folder-structure-excluding-some-child-folders-and-files-a1de7e70f1b
=============================================================================================================================================
.SYNOPSIS

This function really copies the files.
#> 

param
 (
  [string] $OldFilesDirectory,
  [string] $NewFilesDirectory
 )

 $arrExclude      = @("desktop.ini","$RECYCLE.BIN","Google Chrome.lnk","thumbs.db")
 $arrExcludeMatch = @(".pst")
 New-Item -Path $NewFilesDirectory -ItemType Directory -Force -WhatIf:(-not($ProductionRun)) | Out-Null
  Try
   {
    Get-ChildItem -Path $OldFilesDirectory -Recurse -Exclude $arrExclude |
    where { $arrExcludeMatch -eq $null -or $_.FullName.Replace($OldFilesDirectory, "") -notmatch $arrExcludeMatch } | 
    Copy-Item -Destination {
        if ($_.PSIsContainer) 
        {
         Join-Path $NewFilesDirectory $_.Parent.FullName.Substring($OldFilesDirectory.length)
        } 
         else 
        {
         Join-Path $NewFilesDirectory $_.FullName.Substring($OldFilesDirectory.length)
        }
    } -Force -Exclude $arrExclude -WhatIf:(-not($ProductionRun)) -Confirm:$false -Erroraction SilentlyContinue
    Write-EntryToResultsFile -strUserid $Userid -Result "Success" -Action "Copy from $OldFilesDirectory to $NewFilesDirectory."
   }
    Catch
   {
    Write-EntryToResultsFile -strUserid $Userid -Result "Error" -Action "Copy from $OldFilesDirectory to $NewFilesDirectory." -Message $_.Exception.Message
    Continue 
   }

}

Function Copy-UserProfile
{
<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Copy-UserProfile
=============================================================================================================================================
.SYNOPSIS

This function copies the favorites and desktop from the old profile to the new profile

#>

param
 (
  [string] $Userid="",
  [string] $OldProfilePath="",
  [string] $NewProfilePath=""
 )

 # Perform some checking
 $bolErrorsFound = $False

 $OldProfilePath = (Add-TrailingBackSlash -AddBackslashTo $OldProfilePath) + $Userid 
 $NewProfilePath = (Add-TrailingBackSlash -AddBackslashTo $NewProfilePath) + $Userid

 if(-not(test-path($OldProfilePath)))
 {
  $bolErrorsFound = $True
  Write-EntryToResultsFile -strUserid $Userid -Result "Error" -Action "Check directory" -Message "The directory $OldProfilePath does not exists."
 }

 if(-not(test-path($NewProfilePath)))
 {
  $bolErrorsFound = $True
  Write-EntryToResultsFile -strUserid $Userid -Result "Error" -Action "Check directory" -Message "The directory $NewProfilePath does not exists."
 }

 if(test-path($NewProfilePath + "\Desktop\Oude desktop Windows 7"))
 {
  $bolErrorsFound = $True
  Write-EntryToResultsFile -strUserid $Userid -Result "Error" -Action "Check directory" -Message "The directory $($NewProfilePath + "\Desktop\Oude desktop Windows 7") exists. That means that the profile has already been copied."
 }

 If($bolErrorsFound)
 {
  Return
 }

 # Copy the Favorites and desktop

  Copy-UserProfileFiles -OldFilesDirectory ($strOldFolder = $OldProfilePath +"\Favorites") -NewFilesDirectory ($strNewFolder = $NewProfilePath +"\Favorites")
  Copy-UserProfileFiles -OldFilesDirectory ($strOldFolder = $OldProfilePath +"\Desktop")   -NewFilesDirectory ($strNewFolder = $NewProfilePath +"\Desktop\Oude desktop Windows 7")
 
}

Function Add-RollBackRecord
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Add-RollBackRecord
=============================================================================================================================================
.SYNOPSIS

This function adds an item to the rollback file

#>

  param
   (
    [string] $Userid,
    [string] $NewOU,
    [string] $VDIGroup,
    [string] $GroupsToAdd,
    [string] $GroupsToRemove
   )
  
  $RollBackRecord                       = [ordered] @{"Userid" = "";"NewOU"="";"VDIGroup"= "";"GroupsToAdd"= "";"GroupsToRemove"= ""}
  $RollBackRecord."Userid"              = $Userid
  $RollBackRecord."NewOU"               = $NewOU
  $RollBackRecord."VDIGroup"            = $VDIGroup
  $RollBackRecord."GroupsToAdd"         = $GroupsToAdd
  $RollBackRecord."GroupsToRemove"      = $GroupsToRemove
  $objRollBackRecord                    = New-Object PSObject -Property $RollBackRecord
  $Global:arrTableWithRollBackRecords  += $objRollBackRecord
  }

# =============================================================================================================================================
# End function block
# =============================================================================================================================================

# =============================================================================================================================================
# Declares the variables.
# Modify $strPrefixOldGroupName for your own enfironment.
# =============================================================================================================================================

  $valCounter                         = 1
  $Global:arrTable                    = @()
  $Global:arrTableWithRollBackRecords = @()
  $strCurrentPath                     = Split-Path -parent $MyInvocation.MyCommand.Definition
  $strCurrentFile                     = $MyInvocation.MyCommand.Name
  $strPrefixOldGroupName              = "gg_appl_"
  $strPrefixNewGroupName              = "Appl_"
  $arrGroupMustContainForFullCleanUp  = @($strPrefixOldGroupName,"WM-Users")

# =============================================================================================================================================
# check the length of the current directory. Stop the script if more that 248 characters. Otherwise the results cannot be written to the given
# folder.
# =============================================================================================================================================

  If($strCurrentPath.Length -gt 248)
  {
   Write-Host "The current directory $strCurrentPath has a length of more than 248 characters. The script will end with exit code 999 now."
   Exit 999
  }

# =============================================================================================================================================
# Check if the parameter CreateFileWithUseridsInCSVFormat is used. In that case, create the file and stop.
# =============================================================================================================================================

  if($CreateFileWithUseridsInCSVFormat)
  {
   Create-FileWithUseridsInCSVFormat -srcOU $srcOU -dstOU $dstOU -Windows10VDIGroup $Windows10VDIGroup
   Exit 0
  }
    
# =============================================================================================================================================
# Define the CSV Import File. 
# =============================================================================================================================================

  if($FileWithUseridsInCSVFormat.Length -eq 0)
   {
    $strCSVFileName = $strCurrentFile -Replace ".ps1",".csv"
   }
    else
   {
    if($FileWithUseridsInCSVFormat.ToLower().IndexOf(".csv") -eq -1)
    {
     $FileWithUseridsInCSVFormat = $FileWithUseridsInCSVFormat + ".csv"
    }
    $strCSVFileName = $FileWithUseridsInCSVFormat
   }

# =============================================================================================================================================
# Check if the string $strCSVFileName is a path. In that case, nothing has to be done.
# In case it is not a path, then the current location should be added.
# =============================================================================================================================================

  if (-not(Split-Path($strCSVFileName)))
  { 
   $strCSVFileName = $strCurrentPath + "\" + $strCSVFileName
  }

# =============================================================================================================================================
# In case of an automated migration check if the string $FileForAutomaticMigration is a path. In that case, nothing has to be done.
# In case it is not a path, then the current location should be added.
# If the file exists then read all the content, otherwise quit.
# =============================================================================================================================================

  if($FileForAutomaticMigration.Length -gt 0)
   {
    if (-not(Split-Path($FileForAutomaticMigration)))
    {
     $FileForAutomaticMigration = $strCurrentPath + "\" + $FileForAutomaticMigration
    }
   
    if(Test-Path($FileForAutomaticMigration))
     {
      $arrGroupNamesForAutomaticMigration = @(Import-Csv $FileForAutomaticMigration)
      if ($arrGroupNamesForAutomaticMigration.Count -eq 0)
      {
      Write-Host "The file $FileForAutomaticMigration seems to be empty..."
      Exit 1
      }
     }
      else
     {
      Write-Host "The file $FileForAutomaticMigration does not exists. Thus quitting."
      Exit 1
     }
   }  

# =============================================================================================================================================
# Define the log file. This log file contains all the results.
# =============================================================================================================================================

  $strLastPartOfFileName = " (" + (Get-Date).ToString('G') + ").csv"
  $strLastPartOfFileName = $strLastPartOfFileName -replace ":","-"
  $strLastPartOfFileName = $strLastPartOfFileName -replace "/","-"
  
  If(-not($ProductionRun))
  {
   $strLastPartOfFileName = " (RUNNING IN TEST MODE)" + $strLastPartOfFileName
  }

  $strCSVLogFileSucces   = $strCSVFileName -Replace ".csv", $strLastPartOfFileName

  $strPathName           = (Split-Path $strCSVLogFileSucces) + "\"
  $strFileName           = $strCSVLogFileSucces.Substring($strPathName.Length,($strCSVLogFileSucces.Length - $strPathName.Length)) 

  $strCSVLogFileSucces   = $strPathName + $LogFilePrefix + $strFileName
  $strCSVRollBackFile    = $strPathName + "RBF_" + $strFileName

  $valFileLengthLogFile = $strCSVLogFileSucces.Length

  if($valFileLengthLogFile -gt 260)
  {
   Write-Host "The file name $strCSVLogFileSucces is too long. The maximum file length is 260 characters. This one is $valFileLengthLogFile long.`n No log file is generated, and therefore, this application will quit."
   Exit 2
  }

# =============================================================================================================================================
# Read the CSV file.
# =============================================================================================================================================
      
  If(Test-Path $strCSVFileName)
  {
   $arrUserids = @(Import-Csv $strCSVFileName)
  }
   Else
  {
   Write-Host "The import file $strCSVFileName does not exists."
   Exit 1
  }
    
# =============================================================================================================================================
# Find all the arguments and puthem in the log file
# Source: https://ss64.com/ps/psboundparameters.html
# =============================================================================================================================================

  Write-EntryToResultsFile -strUserid "" -Result "Information" -Action "Used script" -Message $strCurrentPath + "\" + $strCurrentFile

  foreach($boundparam in $PSBoundParameters.GetEnumerator()) 
  {
   Write-EntryToResultsFile -strUserid "" -Result "Information" -Action "Key: $($boundparam.Key)" -Message "Value: $($boundparam.Value)"
  }
  
# =============================================================================================================================================
# Process the users in the CSV file.
# =============================================================================================================================================

  Clear-Host

  $strActivity                       = "Modifying users in Active Directory."

  If(-not($ProductionRun))
  {
  $strActivity = $strActivity + " (RUNNING IN TEST MODE)"
  }

  ForEach($objUser in $arrUserids)
  {
     
   $strUserid = $objUser.Userid
   Write-Progress -Activity $strActivity -Status "Processing user $strUserid" -PercentComplete ($valCounter / $arrUserids.Count * 100)

   if($CreateRollBackFile)
   {
    $Global:strForRollBackGroupsToAdd    = ""
    $Global:strForRollBackGroupsToRemove = ""
    $strUserDetails                      = Get-ADUser $strUserid -Properties cn, DistinguishedName
    $strCurrentUserOU                    = "OU=" + ($strUserDetails.DistinguishedName -split "=",3)[-1]
   }

   Try
   {
    $strNewOU          = $objUser.NewOU
    $strVDIGroup       = $objUser.VDIGroup
    $arrGroupsToAdd    = $objUser.GroupsToAdd.Split(",")
    $arrGroupsToRemove = $objUser.GroupsToRemove.Split(",")
   }
    Catch
   {
    Write-EntryToResultsFile -strUserid $strUserid -Result "Error" -Message "There is something wrong with the CSV filename $strCSVFileName while processing $strUserid."
    Export-ResultsLogFileToCSV
    Exit 1 
   }
   
   # Copy Profile
   if($copyProfile)
   {
    Copy-UserProfile -Userid $strUserid -OldProfilePath $ProfilePathFrom -NewProfilePath $ProfilePathTo
   }
     

   if(-not $copyProfile)
   {

    # Clear the profile path
    If($ClearProfilePath)
     {
      Remove-ProfilePathFromUserProfileInAD -strUserid $strUserid
     }
   
    # Clear the homedrive and home directory
    If($ClearHomeFolder)
     {
      Remove-HomeFolderPathFromUserProfileInAD -strUserid $strUserid
     }
    # Move the user to another OU:
    Move-ADUserToOtherOU -strUserid $strUserid -strDestinationOU $strNewOU
   
    # Add the user to the new VDI group:
    Add-ADMemberToGroup -strUserid $strUserid -strADGroupName $strVDIGroup
   
    # Add the user to various groups:
    ForEach($objGroupToAdd in $arrGroupsToAdd)
    {
     Add-ADMemberToGroup -strUserid $strUserid -strADGroupName $objGroupToAdd
    }
   
    # Remove the user to various groups:
    ForEach($objGroupToRemove in $arrGroupsToRemove)
    {
     Remove-ADMemberFromGroup -strUserid $strUserid -strADGroupName $objGroupToRemove
    }
   }

   # Automatic migration

   if($FileForAutomaticMigration.Length -gt 0 -and -not $copyProfile)
   {
    $arrGroups = @(Get-ADUser $strUserid -Properties MemberOf).MemberOf
    forEach($objGroup in $arrGroups)
    {
     $strGroupName = (Get-ADGroup $objGroup).Name
     if ($strGroupName.ToLower().IndexOf($strPrefixOldGroupName) -eq 0)
     {
      Migrate-ADUserToNewGroup -strUserid $strUserid -strADGroupName $strGroupName
     }
    }
   }

   # Full Clean Up
   if($FullCleanUp -and -not $copyProfile)
   {
   Remove-UserFromMultipleGroups -arrGroupsToSearchFor $arrGroupMustContainForFullCleanUp -strUserid $strUserid
   }

   # Remove old Citrix groups
   if($RemoveOldCitrixGroups -and -not $copyProfile)
   {
   $arrCitrixOU = @("OU=Citrix,OU=OU1,DC=testdomain,DC=local,DC=lan")

   ForEach($objCitrixOU in $arrCitrixOU)
     {
     $arrWithCitrixGroups = get-adgroup -filter "*" -SearchBase $objCitrixOU | Where-Object Name -Match "Citrix VDI*"
     ForEach ($objWithCitrixGroups in $arrWithCitrixGroups)
      {    
       Remove-UserFromMultipleGroups -arrGroupsToSearchFor $objWithCitrixGroups.Name -strUserid $strUserid
      }
     }

     Remove-UserFromMultipleGroups -arrGroupsToSearchFor @("CitrixTestUsers","CitrixProductionUsers") -strUserid $strUserid
   }

   # Perform the Direct Group Inventory 
   if($RunDirectGroupInventory -and -not $ProductionRun -and -not $copyProfile)
   {
    Show-DirectGroupsInResultsFile -strUserid $strUserid
   }

   # Perform the Indirect Group Inventory 
   if($RunIndirectGroupInventory -and -not $ProductionRun -and -not $copyProfile)
   {
    Show-IndirectGroupsInResultsFile -strUserid $strUserid
   }

   if($CreateRollBackFile)
   {
    Add-RollBackRecord -Userid $strUserid -NewOU $strCurrentUserOU -VDIGroup "" -GroupsToAdd $Global:strForRollBackGroupsToAdd -GroupsToRemove $Global:strForRollBackGroupsToRemove
   }

   $valCounter++
   
  }
  
  if($ProductionRun -and ($RunDirectGroupInventory -or $RunIndirectGroupInventory) -and -not $copyProfile)
  {
   $valCounter = 1
   $maxCounter = 30
   For($valCounter = 1; $valCounter -le $maxCounter;$valCounter++)
    {
     Write-Progress -Activity "Processing changes in Active Directory" -Status "Waiting $valCounter of $maxCounter seconds."  -PercentComplete ($valCounter / $maxCounter * 100)
     Sleep 1
    }
 
   # =============================================================================================================================================
   # Inventory the indirect user groups the user belongs to.
   # This can only be done after updating Active Directory, otherwise incorrect results are shown.
   # =============================================================================================================================================

     $strActivity                       = "Inventory the indirect and / or direct groups the user belongs to."

     If(-not($ProductionRun))
      {
       $strActivity = $strActivity + " (RUNNING IN TEST MODE)"
      }

     $valCounter = 1
     ForEach($objUser in $arrUserids)
      {
       $strUserid = $objUser.Userid
       Write-Progress -Activity $strActivity -Status "Processing user $strUserid" -PercentComplete ($valCounter / $arrUserids.Count * 100)
       if ($RunDirectGroupInventory)
        {
         Show-DirectGroupsInResultsFile -strUserid $strUserid
        }
       if ($RunIndirectGroupInventory) 
        {
         Show-IndirectGroupsInResultsFile -strUserid $strUserid
        }
       $valCounter++
      }
  }

# =============================================================================================================================================
# Write the results to the csv file.
# =============================================================================================================================================
  
  Export-ResultsLogFileToCSV
   
# =============================================================================================================================================
# Write the Rollback file to a csv file.
# =============================================================================================================================================

  if($CreateRollBackFile)
  {
   Export-RollBackFileToCSV
  }

Current version: Migrate users (v10)

Any feedback to improve this script is appreciated. You can download the previous versions here:

  1. Migrate users (v01)
  2. Migrate users (v02)
  3. Migrate users (v03)



Migrate users from Windows 7 to Windows 10 automatically

For a migration from Windows 7 to Windows 10 it is needed to do the following with the users’ account:

  • Move it to a different OU
  • Add it to a new VDI group
  • Add it to the new application groups
  • Remove it from the previous application groups
  • Clear the profile path, if needed.

To do this, I created a Powershell script.

The users who need to be migrated from Windows 7 to Windows 10 are in a csv file:

  • The column Userid contains the userid
  • The column NewOU contains the OU where the user should be moved to. This column can be empty.
  • The colum VDIGroup contains the new VDI group. This column can be empty.
  • The colum GroupsToAdd contains the groups the user should be added to. Split the groups with a comma. This column can be empty.
  • The colum GroupsToRemove contains the groups the user should be removed from. Split the groups with a comma. This column can be empty.

An example:

"Userid","NewOU","VDIGroup","GroupsToAdd","GroupsToRemove"
"userid1","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","Citrix VDI Windows 10","Appl_Group1,Appl_Group2,Appl_Group3","old_appl_group1,old_appl_group2,old_appl_group3,old_appl_group4"
"userid2","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","Citrix VDI Windows 10","",""
"userid3","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","","",""
"userid4","","","","old_appl_group1,old_appl_group2"

The are some parameters:

  • FileWithUseridsInCSVFormat: CSV Filename that contains all the userids that should be migrated. If not mentioned than the script name is used.
  • LogFilePrefix: The name the logfile starts with. So the logfiles are grouped together. Default = ZZZ-Logfile_
  • ProductionRun: Use the swith ProductionRun to modify. If not specified, the script is run in test mode.
  • FullCleanUp: Use the swith FullCleanUp to remove all unneeded groups.
  • ClearProfilePath: Use the swith ClearProfilePath to clear the profile path.

The script:

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Filename:         Migrate users (v03).ps1
=============================================================================================================================================
.DESCRIPTION:

This script prepares the users for the migration from Windows 7 to Windows 10.

.USAGE:

Create a CSV file with the following layout:

"Userid","NewOU","VDIGroup","GroupsToAdd","GroupsToRemove"
"userid1","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","Citrix VDI Windows 10","Appl_Group1,Appl_Group2,Appl_Group3","old_appl_group1,old_appl_group2,old_appl_group3,old_appl_group4"
"userid2","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","Citrix VDI Windows 10","",""
"userid3","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","","",""
"userid4","","","","old_appl_group1,old_appl_group2"

.PARAMETERS:

-FileWithUseridsInCSVFormat:
The filename with the users to be modified.

-LogFilePrefix
The name the logfile starts with. So the logfiles are grouped together. Default = ZZZ-Logfile_

-ProductionRun
If the switch is used then ActiveDirectory is modified. If not used then a test run is done, and Active Directory is not modified.

-FullCleanUp
If this switch is used then the following groups will be removed from the users' account:
 * All gg_appl groups
 * WM-Users

 -ClearProfilePath
 If this switch is used then the profile path will be cleared.
  
.VERSION HISTORY:
 v0.1:
   * Initial version.

 v0.2:
   * The logfilename has been changed.

 V0.3:
   * The logfile mentions 'RUNNING IN TEST MODE' in case the swith -ProductionRun is not used.
#>

param
(
[Parameter(HelpMessage="CSV Filename that contains all the userids that should be migrated. Default = the script name, with the csv extension.")]
[String] $FileWithUseridsInCSVFormat="",

[Parameter(HelpMessage="The name the logfile starts with. So the logfiles are grouped together. Default = ZZZ-Logfile_")]
[String] $LogFilePrefix = "ZZZ-Logfile_",

[Parameter(HelpMessage="Use the swith ProductionRun to modify. If not specified, the script is run in test mode.")]
[Switch] $ProductionRun,

[Parameter(HelpMessage="Use the swith FullCleanUp to remove all unneeded groups.")]
[Switch] $FullCleanUp,

[Parameter(HelpMessage="Use the swith ClearProfilePath to clear the profile path.")]
[Switch] $ClearProfilePath
)

# =============================================================================================================================================
# Function block
# =============================================================================================================================================

Function Write-EntryToResultsFile
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       03-August-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Write-EntryToResultsFile
=============================================================================================================================================
.DESCRIPTION:

This function adds the success or failure information to the array that contains the log
information.

#>
param
 (
  $strUserid,
  $ErrorMessage = "",
  $Action       = ""
 )
 $Record            = [ordered] @{"Username" = "";"Action"= "";"Testmode"="";"Error"= ""}
 $Record."Username" = $strUserid
 $Record."Action"   = $Action
 $Record."Testmode" = -not($ProductionRun)
 $Record."Error"    = $ErrorMessage
 $objRecord         = New-Object PSObject -Property $Record
 $Global:arrTable   += $objRecord
}

Function Remove-ProfilePathFromUserProfileInAD
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Remove-ProfilePathFromUserProfileInAD
=============================================================================================================================================
.DESCRIPTION:

This function clears the ProfilePath from AD.

#>

  param
   (
    $strUserid
   )
   Try
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Set-ADUser -Identity $strUserDN -Clear profilePath -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Clear profile path"
      }
       Catch
      {
       Write-EntryToResultsFile -strUserid $strUserid -ErrorMessage $_.Exception.Message -Action "Clear profile path"
       Continue
      }
}

Function Move-ADUserToOtherOU
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Move-ADUserToOtherOU
=============================================================================================================================================
.DESCRIPTION:

This function moves the user to another OU.

#>

  param
   (
    $strUserid,
    $strDestinationOU
   )
   Try
     {
      if($strDestinationOU.Length -gt 0)
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Move-ADObject -Identity $strUserDN -TargetPath $strDestinationOU -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Move AD User to OU $strDestinationOU."
      }
     }
      Catch
     {
      Write-EntryToResultsFile -strUserid $strUserid -ErrorMessage $_.Exception.Message -Action "Move AD User to OU $strDestinationOU."
      Continue
     }
}

Function Add-ADMemberToGroup
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Add-ADMemberToGroup
=============================================================================================================================================
.DESCRIPTION:

This function adds a user to an AD group.

#>

  param
   (
    $strUserid,
    $strADGroupName
   )
   Try
     {
      if($strADGroupName.Length -gt 0)
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Add-ADGroupMember -Identity $strADGroupName -Members $strUserDN -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Add AD group $strADGroupName to user."
      }
     }
      Catch
     {
      Write-EntryToResultsFile -strUserid $strUserid -ErrorMessage $_.Exception.Message -Action "Add AD group $strADGroupName to user."
      Continue
     }
}

Function Remove-ADMemberFromGroup
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Remove-ADMemberFromGroup
=============================================================================================================================================
.DESCRIPTION:

This function removes a user from an AD group.

#>

  param
   (
    $strUserid,
    $strADGroupName
   )
   Try
     {
      if($strADGroupName.Length -gt 0)
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Remove-ADGroupMember -Identity $strADGroupName -Members $strUserDN -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Remove AD group $strADGroupName from user."
      }
     }
      Catch
     {
      Write-EntryToResultsFile -strUserid $strUserid -ErrorMessage $_.Exception.Message -Action "Remove AD group $strADGroupName from user."
      Continue
     }
}

# =============================================================================================================================================
# End function block
# =============================================================================================================================================

# =============================================================================================================================================
# Declares the variables.
# =============================================================================================================================================

  $valCounter                        = 1
  $Global:arrTable                   = @()
  $strActivity                       = "Modifying users in Active Directory."
  $arrGroupMustContainForFullCleanUp = @("gg_appl","WM-Users")
  $strCurrentPath                    = Split-Path -parent $MyInvocation.MyCommand.Definition
  $strCurrentFile                    = $MyInvocation.MyCommand.Name


# =============================================================================================================================================
# Define the CSV Import File. 
# =============================================================================================================================================

  if($FileWithUseridsInCSVFormat.Length -eq 0)
   {
    $strCSVFileName = $strCurrentFile -Replace ".ps1",".csv"
   }
    else
   {
    if($FileWithUseridsInCSVFormat.ToLower().IndexOf(".csv") -eq -1)
    {
     $FileWithUseridsInCSVFormat = $FileWithUseridsInCSVFormat + ".csv"
    }
    $strCSVFileName = $FileWithUseridsInCSVFormat
   }

# =============================================================================================================================================
# Check if the string $strCSVFileName is a path. In that case, nothing has to be done.
# In case it is not a path, then the current location should be added.
# =============================================================================================================================================

  if (-not(Split-Path($strCSVFileName)))
  { 
   $strCSVFileName = $strCurrentPath + "\" + $strCSVFileName
  }

# =============================================================================================================================================
# Define the log file. This log file contains all the results.
# =============================================================================================================================================

  $strLastPartOfFileName = " (" + (Get-Date).ToString('F') + ").csv"
  $strLastPartOfFileName = $strLastPartOfFileName -replace ":","-"
  
  If(-not($ProductionRun))
  {
  $strLastPartOfFileName = " (RUNNING IN TEST MODE)" + $strLastPartOfFileName
  }

  $strCSVLogFileSucces   = $strCSVFileName -Replace ".csv", $strLastPartOfFileName

  $strPathName           = (Split-Path $strCSVLogFileSucces) + "\"
  $strFileName           = $strCSVLogFileSucces.Substring($strPathName.Length,($strCSVLogFileSucces.Length - $strPathName.Length)) 

  $strCSVLogFileSucces   = $strPathName + $LogFilePrefix + $strFileName
  
# =============================================================================================================================================
# Read the CSV file.
# =============================================================================================================================================
      
  if(Test-Path $strCSVFileName)
  {
   $arrUserids = @(Import-Csv $strCSVFileName)
  }
   Else
  {
   Write-Host "The import file $strCSVFileName does not exists."
   Exit 1
  }
    
# =============================================================================================================================================
# Process the users in the CSV file.
# =============================================================================================================================================

  Clear-Host

  If(-not($ProductionRun))
  {
  $strActivity = $strActivity + " (RUNNING IN TEST MODE)"
  }

  ForEach($objUser in $arrUserids)
  {
   $strUserid = $objUser.Userid
   Write-Progress -Activity $strActivity -Status "Processing user $strUserid" -PercentComplete ($valCounter / $arrUserids.Count * 100)
   Try
   {
    $strNewOU          = $objUser.NewOU
    $strVDIGroup       = $objUser.VDIGroup
    $arrGroupsToAdd    = $objUser.GroupsToAdd.Split(",")
    $arrGroupsToRemove = $objUser.GroupsToRemove.Split(",")
   }
    Catch
   {
    Write-Host "There is something wrong with the CSV filename $strCSVFileName while processing $strUserid."
    Exit 1 
   }
   
   # Clear the profile path
   If($ClearProfilePath)
   {
    Remove-ProfilePathFromUserProfileInAD -strUserid $strUserid
   }
   
   # Move the user to another OU:
   Move-ADUserToOtherOU -strUserid $strUserid -strDestinationOU $strNewOU
   
   # Add the user to the new VDI group:
   Add-ADMemberToGroup -strUserid $strUserid -strADGroupName $strVDIGroup

   # Add the user to various groups:
   ForEach($objGroupToAdd in $arrGroupsToAdd)
   {
    Add-ADMemberToGroup -strUserid $strUserid -strADGroupName $objGroupToAdd
   }
  
   # Remove the user to various groups:
   ForEach($objGroupToRemove in $arrGroupsToRemove)
   {
    Remove-ADMemberFromGroup -strUserid $strUserid -strADGroupName $objGroupToRemove
   }
  
   # Full Clean Up
   if($FullCleanUp)
   {
    $arrGroups = @(Get-ADUser $strUserid -Properties MemberOf).MemberOf
    forEach($objGroup in $arrGroups)
    {
    $strGroup=(Get-ADGroup $objGroup).Name
    forEach($objGroupMustContain in $arrGroupMustContainForFullCleanUp)
     {
      $strGroupMustContain = $objGroupMustContain.ToString().ToLower()
      if($strGroup.ToLower().IndexOf($strGroupMustContain) -eq 0)
       {
        Remove-ADMemberFromGroup -strUserid $strUserid -strADGroupName $strGroup
       }  
     }
    }
   }

   $valCounter++
   Sleep 2
  }

  Sleep 2

# =============================================================================================================================================
# Write the results to the csv file.
# =============================================================================================================================================
  
  If($Global:arrTable.Count -gt 0)
  {
    $Global:arrTable | Export-Csv $strCSVLogFileSucces -NoTypeInformation
  }
   Else
  {
    Write-Host "Something went wrong while writing the logfile $strCSVLogFileSucces. Maybe nothing to report..."
  } 

Any feedback to improve this script is appreciated. You can download the scripts here:

  1. Migrate users (v01)
  2. Migrate users (v02)
  3. Migrate users (v03)



Inventory the directory access rights on file servers

During the migration, it was needed to inventory the directory access on several file servers. So we could easily monitor the access rights on several directories on the file servers. Some applications are started from a UNC path. So we could add the ‘old’ and the ‘new’ group and check if that has been done properly.

For testing I created the following structure:

\\DEMOATS-SCCM\DEMO.
\---share1
    +---Appl1
    +---Appl2
    |   \---Test
    +---Appl3
    |   +---Sub1
    |   \---Sub2
    |       \---SubSub1
    \---Appl4

I had some challenges:

  • Make a difference between inherited and non-inherited rights. I only want to see the differences. But that can be changed with the parameter showInherited
  • There are some file servers where all the shares (from the root) have to be inventoried. \\server\share did not work, as the rights where inherited. And the script did not see that properly. So I had to inventory the ‘root’ shares on the server. And go through all the directories. I found the code on StackOverflow.
  • And a lot of testing.

The script that I created:

<#
.NOTES
===============================================================================================
Created with:     Windows PowerShell ISE
Created on:       03-August-2018
Created by:       Willem-Jan Vroom
Organization:     
Filename:         Inventory Permissions on Shares (v02).ps1
===============================================================================================
.DESCRIPTION:

This script writes the directory permissions of the given shares to a CSV file.

.USAGE:

1.
Run an inventory on the shares  \\server\share,\\server2\share1 and all shares on \\server3\
with the default setting of a search level of 1 and only show the directories that are not
inherited for the AD group 'Users':
.\"Inventory Permissions on Shares (v02).ps1" -ShareList \\server\share,\\server2\share1,\\server3\

2.
Run an inventory on the share \\server\share with an search level of 10 for all the 'Appl' 
groups:
.\"Inventory Permissions on Shares (v02).ps1" -ShareList \\server\share -NumberOfLevelsToSearch 10 -GroupNameToSearchFor "Appl"

3.
Run a complete inventory for one server for all groups:
.\"Inventory Permissions on Shares (v02).ps1" -ShareList \\server\ -NumberOfLevelsToSearch 10 -GroupNameToSearchFor "" -showInherited

.VERSION HISTORY:
 v0.1:
   * Initial version.

 v.0.2:
   * Option -Outputfile has been added.
   * Added help text by the options.

 v.0.3:
   * The parameter showInherited has become a switch.

#>

param
(
[Parameter(Mandatory=$true,HelpMessage="Please mention the shares you want to inventory regarding the permissions. One name each line.")]
[String[]] $ShareList,

[Parameter(HelpMessage="Give a part of the group name to search for. Leave empty for all groups. Default = Users")]
$GroupNameToSearchFor    = "Users",

[Parameter(HelpMessage="Give the search level. Default = 1")]
$NumberOfLevelsToSearch  = 1,

[Parameter(HelpMessage="Show inherited directories, if specified.")]
[switch]$showInherited,

[Parameter(HelpMessage="Mention the output file. Default is the script name, with csv as the extension.")]
$OutputFile              = ""
)

# ===============================================================================================
# Function block
# ===============================================================================================

Function Get-NetShares
{

<#
.NOTES
===============================================================================================
Created with:     Windows PowerShell ISE
Created on:       03-August-2018
Created by:       https://stackoverflow.com/users/2102693/bill-stewart
Organization:     
Functionname:     Get-NetShares
===============================================================================================
.DESCRIPTION:

This function finds all the shares that are on a server.

I have found this script here:
https://stackoverflow.com/questions/45089582/using-get-childitem-at-root-of-unc-path-servername
(C) by https://stackoverflow.com/users/2102693/bill-stewart

#>

param
(
  [String] $ComputerName = "."
)

Add-Type @"
using System;
using System.Runtime.InteropServices;
using System.Text;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct SHARE_INFO_1
{
  [MarshalAs(UnmanagedType.LPWStr)]
  public string shi1_netname;
  public uint shi1_type;
  [MarshalAs(UnmanagedType.LPWStr)]
  public string shi1_remark;
}
public static class NetApi32
{
  [DllImport("netapi32.dll", SetLastError = true)]
  public static extern int NetApiBufferFree(IntPtr Buffer);
  [DllImport("netapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
  public static extern int NetShareEnum(
    StringBuilder servername,
    int level,
    ref IntPtr bufptr,
    uint prefmaxlen,
    ref int entriesread,
    ref int totalentries,
    ref int resume_handle);
}
"@

$pBuffer = [IntPtr]::Zero
$entriesRead = $totalEntries = $resumeHandle = 0
$result = [NetApi32]::NetShareEnum(
  $ComputerName,        # servername
  1,                    # level
  [Ref] $pBuffer,       # bufptr
  [UInt32]::MaxValue,   # prefmaxlen
  [Ref] $entriesRead,   # entriesread
  [Ref] $totalEntries,  # totalentries
  [Ref] $resumeHandle   # resumehandle
)
if ( ($result -eq 0) -and ($pBuffer -ne [IntPtr]::Zero) -and ($entriesRead -eq $totalEntries) ) {
  $offset = $pBuffer.ToInt64()
  for ( $i = 0; $i -lt $totalEntries; $i++ ) {
    $pEntry = New-Object IntPtr($offset)
    $shareInfo = [Runtime.InteropServices.Marshal]::PtrToStructure($pEntry, [Type] [SHARE_INFO_1])
    $shareInfo
    $offset += [Runtime.InteropServices.Marshal]::SizeOf($shareInfo)
  }
  [Void] [NetApi32]::NetApiBufferFree($pBuffer)
}
if ( $result -ne 0 ) {
  Write-Error -Exception (New-Object ComponentModel.Win32Exception($result))
}
}

Function Add-EntryToReport
{

<#
.NOTES
===============================================================================================
Created with:     Windows PowerShell ISE
Created on:       03-August-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Add-EntryToReport
===============================================================================================
.DESCRIPTION:

This function adds an entry to the report. When the shares have been searched, the report is
exported to a CSV file.

#>

param
 (
  $FolderNameToAdd,
  $ErrorMessage = "",
  $ADGroup      = "",
  $Permissions  = "",
  $Inherited    = ""
 )
  $Record = [ordered] @{"FolderName" = "";"AD Group" = "";"Permissions" = "";"Inherited" = "";"Error" = ""}
  $Record."FolderName"  = $FolderNameToAdd
  $Record."Error"       = $ErrorMessage
  $Record."AD Group"    = $ADGroup
  $Record."Permissions" = $Permissions
  $Record."Inherited"   = $Inherited
  $Global:Report += New-Object -TypeName PSObject -Property $Record
}


Function Search-InTheFolder
{

<#
.NOTES
===============================================================================================
Created with:     Windows PowerShell ISE
Created on:       03-August-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Search-InTheFolder
===============================================================================================
.DESCRIPTION:

This function goes through all the folders in the given location.

The variable $numLevels gives the number of levels that the search goes. The less the number, 
the quicklier the script is.

#>

param
(
  $RootOfTheShare,
  $numLevels = 1
)

$valNumberOfDirectories = 0 
$valCounterOfDirectores = 0

Try
  {
$FolderPath = Get-ChildItem -Path $RootOfTheShare -Directory -Recurse -Force -Depth $numLevels -ErrorAction SilentlyContinue
Write-Progress -Activity "Counting the number of folders in the share." -Id 2 -ParentId 1
Foreach ($Folder in $FolderPath)
     {
      $valNumberOfDirectories++
     }
  
   Foreach ($Folder in $FolderPath)
     {
      $FolderFullName = $Folder.FullName
      Write-Progress -Activity "Going through all the shares." -Status "Processing share $FolderFullName." -Id 2 -PercentComplete ($valCounterOfDirectores / $valNumberOfDirectories * 100) -ParentId 1
      Get-FolderRights -FolderNameToInvestigate $Folder.FullName
      $valCounterOfDirectores++
     }
  }
  Catch
  {
  Add-EntryToReport -FolderNameToAdd $RootOfTheShare -ErrorMessage $_.Exception.Message
  }

}


Function Get-FolderRights
{

<#
.NOTES
===============================================================================================
Created with:     Windows PowerShell ISE
Created on:       03-August-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Get-FolderRights
===============================================================================================
.DESCRIPTION:

This function puts the access information in an array.
If the parameter GroupNameToSearchFor is empty or "" then all the groups are shown.

#>
Param($FolderNameToInvestigate)
$GroupNameToSearchFor = $GroupNameToSearchFor.ToLower()
Try
    {
    $Acl = Get-Acl -Path $FolderNameToInvestigate -ErrorAction SilentlyContinue
    foreach ($Access in $acl.Access)
        {           
              $Group=$Access.IdentityReference
              if ($GroupNameToSearchFor.Length -ge 1)
               {
                $Position = $Group.ToString().ToLower().IndexOf($GroupNameToSearchFor)
               }
                Else
               {
                $Position = 2
               }
              if($Position -ge 1)
              {
               [bool]$blnIsInheriated = $Access.IsInherited
               if([bool]$showInherited)
                {
                 Add-EntryToReport -FolderNameToAdd $FolderNameToInvestigate -ADGroup $Access.IdentityReference -Permissions $Access.FileSystemRights -Inherited [bool]$blnIsInheriated
                }
               if((-not[bool]$showInherited) -and (-not[bool]$blnIsInheriated))
                {
                 Add-EntryToReport -FolderNameToAdd $FolderNameToInvestigate -ADGroup $Access.IdentityReference -Permissions $Access.FileSystemRights -Inherited [bool]$blnIsInheriated
                }
              }
        }
        }
        Catch
        {
        Add-EntryToReport -FolderNameToAdd $FolderNameToInvestigate -ErrorMessage $_.Exception.Message
        Continue
        }
}

# ===============================================================================================
# End function block
# ===============================================================================================

# ===============================================================================================
# Define the CSV Export File. 
# ===============================================================================================

  $currentPath                    = Split-Path -parent $MyInvocation.MyCommand.Definition
  $strCurrentFile                 = $MyInvocation.MyCommand.Name
  
  if($OutputFile.Length -eq 0)
   {
    $strCSVFileName                 = $strCurrentFile -Replace ".ps1",".csv"
   }
    else
   {
    if($OutputFile.ToLower().IndexOf(".csv") -eq -1)
    {
     $OutputFile = $OutputFile + ".csv"
    }
    $strCSVFileName = $OutputFile
   }

# ===============================================================================================
# Check if the string $strCSVFileName is a path. In that case, nothing has to be done.
# In case it is not a path, then the current location should be added.
# ===============================================================================================


  if (Split-Path($strCSVFileName))
  {
   $CSVExportFile = $strCSVFileName
  }
   else
  { 
   $CSVExportFile = $currentPath + "\" + $strCSVFileName
  }

# ===============================================================================================
# Create the folder as a part of $CSVExportFile if not exists.
# ===============================================================================================

  $PathFromCSVExportFile = Split-Path $CSVExportFile
  if(-not(Test-Path(Split-Path $PathFrom$CSVExportFile)))
  {
   New-Item -Path $PathFromCSVExportFile -ItemType Directory
  }

# ===============================================================================================
# Define variables.
# ===============================================================================================

  $arrShares                      = @($ShareList)
  $Global:Report                  = @()
    
# ===============================================================================================
# Start the job.
# ===============================================================================================

  Clear-Host
  Try
     {
      Import-Module ActiveDirectory
     }
     Catch
     {
     Write-Host The module ActiveDirectory could not be loaded.
     Exit 1
     }
  Write-Host (Get-Date).ToString('T') " Starting..."

# ===============================================================================================
# Deletes the CSV file if exists.
# ===============================================================================================

  If(Test-Path $CSVExportFile)
  {
  Remove-Item $CSVExportFile
  }

# ===============================================================================================
# Go through all the shares as defined in the array $arrShares
# ===============================================================================================
  
  $valCounter       = 1 
  $valNumerOfShares = $arrShares.Count 
  ForEach ($shareName in $arrShares)
  {
   Write-Progress -Id 1 -Activity "Going through the shares" -Status "Checking share $shareName ($valCounter of $valNumerOfShares)" -PercentComplete ($valCounter / $valNumerOfShares*100)
   
   $LastCharacter = $shareName.SubString($shareName.Length-1,1)

   # ===============================================================================================
   # If the last character is not a '\' then it is a regular share. Then it is simple: call the
   # function 'Search-InTheFolder'
   #
   # If the last character is a '\' then only the servername is given. So first find all the
   # shares on that server. After that, process all the subshares.
   # ===============================================================================================
   
   if ($LastCharacter -ne "\")
   {
   Search-InTheFolder -RootOfTheShare $shareName -numLevels $NumberOfLevelsToSearch
   }
   Else
   {
     $arrShares = Get-NetShares -ComputerName $shareName
     ForEach($objShare in $arrShares)
     {
      $LastCharacter = ($objShare.shi1_netname).SubString(($objShare.shi1_netname).Length-1,1)
      if($LastCharacter -ne "$")
      {
       
       # ===============================================================================================
       # Ignore C$, D$, IPC$, NETADMIN$ etc.
       # This means that all the hidden shares are ignored.
       # ===============================================================================================
             
       $ServerShareName = $shareName + $objShare.shi1_netname
       Get-FolderRights -FolderNameToInvestigate $ServerShareName
       Search-InTheFolder -RootOfTheShare $ServerShareName -numLevels $NumberOfLevelsToSearch
      }
     }  
   }
   $valCounter++
  }

# ===============================================================================================
# Output naar een CSV file
# ===============================================================================================

  $Global:Report | Sort-Object -Property FolderName,"AD Group" | Export-Csv -path $CSVExportFile -NoTypeInformation -Encoding ASCII
  Write-Host You can open the file $CSVExportFile now. 
  Write-Host (Get-Date).ToString('T')  " Ended..." 

Any feedback to improve this script is appreciated. You can download the scripts here:

  1. Link to Inventory Permissions on Shares (v0.1)
  2. Link to Inventory Permissions on Shares (v0.2)
  3. Link to Inventory Permissions on Shares (v0.3)



Modify the users’ profile path in Active Directory

Profile path in AD.

Profile path in AD.

The customer I am working for asked me to write a script that removes the profile path from the users’ profile in Active Directory. One of the requirements was to do it on a batch-by-batch basis. Thus not a big bang.

I decided to write a PowerShell script and use a .csv file as the input. The results should be written to a log file.

I created the following script:

<#
.NOTES
===============================================================================================
Created with:     Windows PowerShell ISE
Created on:       03-August-2018
Created by:       Willem-Jan Vroom
Organization:     
Filename:         RemoveProfilePathFromUser (v02).ps1
===============================================================================================
.DESCRIPTION:

This script removes the profile path from the users' profile in Active Directory.

.USAGE:

Create a CSV file with the following layout:

Userid
test1
test2

And mention the CSV file as a parameter -FileWithUseridsInCSVFormat.

.VERSION HISTORY:
 v0.1:
   * Initial version.

 v.0.2:
   * Option -FileWithUseridsInCSVFormat has been added.
   * Added help text by the options.

 v.0.3:
   * Minor cosmetic changes.

#>

param
(
[Parameter(HelpMessage="CSV Filename that contains all the userids that should be modified. If not mentioned than the script name is used.")]
[String] $FileWithUseridsInCSVFormat=""
)

# ===============================================================================================
# Function block
# ===============================================================================================

Function Write-EntryToResultsFile
{

<#
.NOTES
===============================================================================================
Created with:     Windows PowerShell ISE
Created on:       03-August-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Write-EntryToResultsFile
===============================================================================================
.DESCRIPTION:

This function adds the success or failure information to the array that contains the log
information.

#>
param
 (
  $strUserid,
  $ErrorMessage = ""
 )
 $Record            = [ordered] @{"Username" = "";"Error"= ""}
 $Record."Username" = $strUserid
 $Record."Error"    = $ErrorMessage
 $objRecord         = New-Object PSObject -Property $Record
 $Global:arrTable   += $objRecord
}

Function Process-User

<#
.NOTES
===============================================================================================
Created with:     Windows PowerShell ISE
Created on:       03-August-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Process-User
===============================================================================================
.DESCRIPTION:

Determine if the user can be modified.

#>
{
param
(
 $strUserid
)
    Try
       {
        $UN = Get-ADUser -Identity $strUserid
        Remove-ProfilePathFromUserProfileInAD -strUserid $strUserid
       }
        Catch
       {
        Write-EntryToResultsFile -strUserid $strUserid -ErrorMessage $_.Exception.Message 
        Continue
       }

}


Function Remove-ProfilePathFromUserProfileInAD
{

<#
.NOTES
===============================================================================================
Created with:     Windows PowerShell ISE
Created on:       03-August-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Write-EntryToResultsFile
===============================================================================================
.DESCRIPTION:

This function adds the success or failure information to the array that contains the log
information.

#>

  param
   (
    $strUserid
   )
  
   Try
      {
       Set-ADUser -Identity $strUserid -Clear profilePath
       Write-EntryToResultsFile -strUserid $strUserid
      }
       Catch
      {
       Write-EntryToResultsFile -strUserid $strUserid -ErrorMessage $_.Exception.Message
       Continue
      }
}

# ===============================================================================================
# End function block
# ===============================================================================================

# ===============================================================================================
# Define the CSV Import File. 
# ===============================================================================================

  $currentPath                    = Split-Path -parent $MyInvocation.MyCommand.Definition
  $strCurrentFile                 = $MyInvocation.MyCommand.Name
  
  if($FileWithUseridsInCSVFormat.Length -eq 0)
   {
    $strCSVFileName                 = $strCurrentFile -Replace ".ps1",".csv"
   }
    else
   {
    if($FileWithUseridsInCSVFormat.ToLower().IndexOf(".csv") -eq -1)
    {
     $FileWithUseridsInCSVFormat =+ ".csv"
    }
    $strCSVFileName = $FileWithUseridsInCSVFormat
   }

# ===============================================================================================
# Check if the string $strCSVFileName is a path. In that case, nothing has to be done.
# In case it is not a path, then the current location should be added.
# ===============================================================================================


  if (-not(Split-Path($strCSVFileName)))
  { 
   $strCSVFileName = $currentPath + "\" + $strCSVFileName
  }

# ===============================================================================================
# Declares the variables.
# ===============================================================================================

  $valCounter                     = 1
  $Global:arrTable                = @()
  $Record                         = [ordered] @{"Username" = "";"Error"= ""}

# ===============================================================================================
# Define the log file. This log file contains all the results.
# ===============================================================================================

  $strCSVLogFileSucces = $strCSVFileName -Replace ".csv","_(Results).csv"
  If(Test-Path $strCSVLogFileSucces)
  {
  Remove-Item $strCSVLogFileSucces
  }
 
# ===============================================================================================
# Read the CSV file.
# ===============================================================================================

      
  if(Test-Path -LiteralPath $strCSVFileName)
  {
   $arrUserids = Import-Csv $strCSVFileName
  }
   Else
  {
   Write-Host "The import file $strCSVFileName does not exists."
   Exit 1
  }

# ===============================================================================================
# Modify the users' profile path.
# Write the success or failure to the array with the results.
# ===============================================================================================

  Import-Module ActiveDirectory
  Clear-Host
  
  Write-Host (Get-Date).ToString('T')  "Started."
  ForEach ($objUserid in $arrUserids)
   {
     $strUserid = $objUserid.Userid
     if ($strUserid.Length -gt 0)
     {
       Write-Progress -Activity "Removing profile path from user in Active Directory" -Status "Processing user $strUserid" -PercentComplete ($valCounter / $arrUserids.Count * 100)
       Process-User -strUserid $strUserid
       Start-Sleep -s 1
       $valCounter ++
     }
       else
     {
       Write-Host "The input file $strCSVFileName has an invalid layout. The column header should be named 'Userid'."
       Exit 1
     }
   }
  Start-Sleep -s 1

# ===============================================================================================
# Write the results to the csv file.
# ===============================================================================================

  If($Global:arrTable.Count -gt 0)
  {
    $Global:arrTable | Export-Csv $strCSVLogFileSucces -NoTypeInformation
  }
   Else
  {
    Write-Host "Something went wrong while writing the logfile $strCSVLogFileSucces. Maybe nothing to report..."
  } 
Write-Host (Get-Date).ToString('T')  "Finished." 

After running the script with the correct permissions, the profile path looks like:

Profile path after running the script.

Profile path after running the script.


The scripts can be downloaded in ZIP format:

  1. Link to RemoveProfilePathFromUser (v0.1)
  2. Link to RemoveProfilePathFromUser (v0.2)
  3. Link to RemoveProfilePathFromUser (v0.3)



Check if there is a working internet connection when a firewall blocks ping requests

Sometimes you need to check if there is a working internet connection. You can use the
pingcommand for that. But this command does not work if ICMP echo request have disabled on the firewall. If this is the case we have to check the status of a website. If the status is 200 then there is a working internet connection.

In this example I will use both vbscript and powershell to check if there is a working internet connection.

The basics

  1. The command Invoke-WebRequest -Uri www.google.com -TimeoutSec 1 -UseBasicParsing gives a lot of internet about the webpage. Pay attention to the statuscode.

    Invoke-WebRequest -Uri www.google.com -TimeoutSec 1 -UseBasicParsing

  2. The command (Invoke-WebRequest -Uri www.google.com -TimeoutSec 1 -UseBasicParsing).StatusCode returns only the statuscode.

    (Invoke-WebRequest -Uri www.google.com -TimeoutSec 1 -UseBasicParsing).StatusCode

  3. There is an error message if there is no working internet connection.

    Invoke-WebRequest -Uri www.google.com -TimeoutSec 1 -UseBasicParsing
    With no working internet connection.

  4. If there is a working internet connection then the script returns 200.

    Try {(Invoke-WebRequest -Uri www.google.com -TimeoutSec 1 -UseBasicParsing).StatusCode} Catch {Write-Host “Error!!!”}
    Working internet connection.

  5. In this case there is no working internet connection, so an error message is shown.

    Try {(Invoke-WebRequest -Uri www.google.com -TimeoutSec 1 -UseBasicParsing).StatusCode} Catch {Write-Host “Error!!!”}
    No working internet connection.

Further testing with PowerShell

<#
.NOTES
===========================================================================
Created with:     Windows PowerShell ISE
Created on:       21-October-2017
Created by:       Willem-Jan Vroom
Organization:     
Filename:         CheckIfWebsiteIsReachable.ps1
===========================================================================
.DESCRIPTION:

It checks if there is a working internet connection by reading the webpage.
This is very usefull in case the ping command does not work when ICMP echo
requests are disabled on the firewall.

#>
param ($URL)
Function Get-StatusCodeFromWebsite
{
  param($Website)
  Try
   {
    (Invoke-WebRequest -Uri $Website -TimeoutSec 1 -UseBasicParsing).StatusCode
   }
  Catch
   {
   }
}

$URL = "www.google.com"
$Result = Get-StatusCodeFromWebsite -Website $URL
if($Result -eq 200)
 {
  Write-Host "The website $URL is reachable."
 }
 else
 {
  Write-Host "The website $URL is not reachable."
 }

Result if there is a working internet connection.

Result if there is no working internet connection.

The next step: use vbscript and powershell to detect if there is a working internet connection

As far as I know vbscript has not the option to detect the status from a website. So we use the powershell function with vbscript to detect if there is a working internet connection.

The powershell script has changed a little bit:

<#
.NOTES
===========================================================================
Created with:     Windows PowerShell ISE
Created on:       21-October-2017
Created by:       Willem-Jan Vroom
Organization:     
Filename:         CheckIfWebsiteIsReachable_with_vbscript.ps1
===========================================================================
.DESCRIPTION:

It checks if there is a working internet connection by reading the webpage.
This is very usefull in case the ping command does not work when ICMP echo
requests are disabled on the firewall.

#>
Param ($URL)
Function Get-StatusCodeFromWebsite
{
  param($Website)
  Try
   {
    (Invoke-WebRequest -Uri $Website -TimeoutSec 1 -UseBasicParsing).StatusCode
   }
  Catch
   {
   }
}

$Result = Get-StatusCodeFromWebsite -Website $URL
if($Result -eq 200)
 {
  Exit 0
 }
 else
 {
  Exit 1
 }

It exits with an exit code:
0 = success
1 = failure

And we use vbscript to check:

' ================================================================================================
' Check if a there is a working network connection. Do not use ping, as the ping command cannot
' bu used in case ICMP echo requests have been disabled on the firewall.
' Created by Willem-Jan Vroom
' Version history:
'
' 0.0.1
'  - Initial version
'
' 1.0.0
'  - Final version
' ================================================================================================

' ------------------------------------------------------------------------------------------------
' Declare the most variables. 
' ------------------------------------------------------------------------------------------------
 
  Option Explicit

  Dim objShell      : set objShell   = WScript.CreateObject("WScript.Shell")
  Dim strWebsite    : strWebsite     = "http://www.vroom.cc"
  Dim strCurrentDir : strCurrentDir  = Left(Wscript.ScriptFullname, InstrRev(Wscript.ScriptFullname, "\"))
  Dim strCommand    : strCommand     = "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File " & chr(34) & strCurrentDir & "CheckIfWebsiteIsReachable_with_vbscript.ps1" & chr(34) & " -URL "& strWebsite
  Dim strText       : strText        = ""
  Dim valResult     : valResult      = 0

' ------------------------------------------------------------------------------------------------
' Check the website that is mentioned in the string strWebsite. 
' Use the exitcode from the powershell script:
'  0 = Success
'  1 = Failure
' ------------------------------------------------------------------------------------------------

  valResult = objShell.Run(strCommand, 6, True)
  if valResult = 0 Then 
     strText = "The website '" & strWebSite & "' could be reached"
       else
     strText = "The website '" & strWebSite & "' could not be reached"
  end if

' ------------------------------------------------------------------------------------------------
' Display a message box with the result. 
' ------------------------------------------------------------------------------------------------

  msgbox strText,0,"The result."

In this example we try the webpage http://www.vroom.cc/.

Result if there is a working internet connection.

Result if there is no working internet connection.

You can download the scripts in a ZIP file.




Export lines that contain a specific value to a CSV file with PowerShell

For a client I needed to find all the hostnames that where defined in each IBM iSeries Client Access Workstation file (.ws file). All the .ws files where in subfolders. The country, the filename and the hostname needs to be in a .csv file.

The directory structure:

Folder PATH listing for volume Windows
Volume serial number is 000000BE 
C:.
|   
\---Sessions
    +---CAT Austria
    |       CAT_01.WS
    |       CAT_02.WS
    |       
    +---CBE Belgium
    |       CBE_01.ws
    |       CBE_02.ws
    |       
    +---CCH Switzerland
    |       CCH_01.ws
    |       CCH_02.ws
    |       CCH_03.ws
    |       
    \---Generic sessions
            GEN_01_xx.ws
            GEN_02_xx.WS
            GEN_03_xx.ws
            GEN_TEST_xx.WS
            GEN_UAT.WS

And the content of a .ws file:

[Profile]
ID=WS
[Communication]
HostName=HOST653.company.local

As I want to learn PowerShell, I decided to use PowerShell.

Here is my script:

###########################################################
# FILE:     GetHostnamesFromWSFiles_v100.ps1   
#
# AUTHOR:   Willem-Jan Vroom
# DATE:     17-March-2017
# COMMENT:  This script reads all the hostnames as defined
#           in the .ws files
###########################################################

# ---------------------------------------------------------
# Define the variables
# ---------------------------------------------------------

$Directory        = "$PSScriptRoot\*"
$CSVFile          = "$PSScriptRoot\Results.csv"
$arrWithWSFiles   = Get-ChildItem -Path $Directory -Include *.ws -Recurse
$arrResults       = @()

# ---------------------------------------------------------
# Process each .ws file and search for hostname
# If found, add to the array $arrResults:
#    Country:  The foldername that contains the .ws file
#    Filename: The filename from the .ws file
#    Hostname: The entry that begins with hostname= and
#              contains the AS400 host name to connect to.
# ---------------------------------------------------------

ForEach ($WSFileName in $arrWithWSFiles)
{
 $content = Get-Content $WSFileName
 ForEach ($line in $content)
  {
  $line=$line.tolower()
  $Position = $line.IndexOf("hostname")
  if ($Position -eq 0)
     {
     $DetailedLine               = "" | Select Country,FileName,AS400Hostname
     $CountryCode                = ""
     $CountryCode                = $WSFileName.DirectoryName
     $LastPosition               = $CountryCode.LastIndexOfAny("\")
     $DetailedLine.Country       = $CountryCode.SubString($LastPosition+1, $CountryCode.Length - ($LastPosition + 1))
     $DetailedLine.FileName      = $WSFileName.Name
     $DetailedLine.AS400Hostname = $line -replace("hostname=","")
     $arrResults += $DetailedLine     
     } 
  } 
}

# ---------------------------------------------------------
# Export the array with the results to a CSV File.
# ---------------------------------------------------------
 
$arrResults | Export-CSV -Path $CSVFile

And in this case the output looks like:

#TYPE Selected.System.String
"Country","FileName","AS400Hostname"
"CAT Austria","CAT_01.WS","host653.company.local"
"CAT Austria","CAT_02.WS","host007.company.local"
"CBE Belgium","CBE_01.ws","host123.company.local"
"CBE Belgium","CBE_02.ws","host481.company.local"
"CCH Switzerland","CCH_01.ws","host053.company.local"
"CCH Switzerland","CCH_02.ws","host613.company.local"
"CCH Switzerland","CCH_03.ws","host653.company.local"
"Generic sessions","GEN_01_xx.ws","host001.company.local"
"Generic sessions","GEN_02_xx.WS","host002.company.local"
"Generic sessions","GEN_03_xx.ws","host003.company.local"
"Generic sessions","GEN_TEST_xx.WS","hosttest.company.local"
"Generic sessions","GEN_UAT.WS","hostuat.company.local"

The script can be found here.




Installing a MSI package during App-V 5.0 Package deployment

Driver packages cannot be virtualized as they have to communicate directly with the hardware. So packages using a driver have never been virtualized. Until now.

With AppV 5.0 it is possible to install the driver while adding an AppV 5.0 package and to uninstall it during the removal of the AppV 5.0 package.

In this article I will show how this is done. I will use an existing package that I made earlier.

For this example I made a empty MSI package (it only contains one feature and one component) and that MSI package is installed when the AppV package is added. The details are:

The properties of the empty MSI file edited with Orca.

The properties of the empty MSI file edited with Orca.

1. Add the MSI file to the virtual application

Ths MSI file is added to the ‘Scripts’ section of the virtual application.

  • Copy the virtual package to a local drive, for example C:\AppV50Demo.
  • Open the AppV 5.0 Package.
    Open AppV Package

    Open AppV Package

  • Select ‘Edit’ to modify the virtual package.
    Select Edit

    Select Edit

  • Click [Edit]
    Click Edit.

    Click Edit.

  • Click on the tab [Package Files].
    Tab Package Files

    Tab Package Files

  • Find and select the folder ‘Scripts’.
    Click [Add].
    Add a script to the AppV 5.0 package.

    Add a script to the AppV 5.0 package.

  • Click [Browse].
    Browse

    Browse

  • Find and select the MSI file. If needed, add all the needed files, like transform and cab files.
    Select the MSI that has to be added to the AppV 5.0 package.

    Select the MSI that has to be added to the AppV 5.0 package.

  • Click [Open].
    Click Open.

    Click Open.

  • Click [Ok].
    Click Ok

    Click Ok

  • The result.
    The result.

    The result.

  • Save the package.
    Save the package

    Save the package

  • Exit the sequencer.
    Exit the sequencer.

    Exit the sequencer.

2. Edit the deployment xml file

  • Make a copy of the file ‘IgorPavlov_7Zip_9.20_ENG_2_3_4_DeploymentConfig.xml’
  • Rename that copy to ‘IgorPavlov_7Zip_9.20_ENG_2_3_4_DeploymentConfig with MSI.xml’. All the changes will be in this file.
  • Open the file ‘IgorPavlov_7Zip_9.20_ENG_2_3_4_DeploymentConfig with MSI.xml’.
  • Search for the following text:
        
        
  • And replace it by:
        
        
          
    
          
            msiexec.exe
            /i TestMSI.MSI /qb! /l*v c:\windows\system32\LogFiles\Install_TestMSI.log
            
          
          
            msiexec.exe
            /x TestMSI.MSI /qb! /l*v c:\windows\system32\LogFiles\Uninstall_TestMSI.log
            
          
        
    
  • Save the modified ‘IgorPavlov_7Zip_9.20_ENG_2_3_4_DeploymentConfig with MSI.xml’.

3. Add and publish the virtual application.

  • Copy the AppV 5.0 to a local drive, for example: C:\IgorPavlov_7Zip_9.20_ENG\1.00.00
  • Start PowerShell 3.0 as an administrator.
  • Change the security policy:
    Set-ExecutionPolicy -ExecutionPolicy Unrestricted
  • Import all the AppV client cmdlets:
    Import-Module AppVClient
  • check if it is possible to run scripts. It is needed to change the policy ‘EnablePackageScripts’ to 1.
    Get-AppvclientConfiguration
  • And change the policy if needed:
    Set-AppvclientConfiguration -EnablePackageScripts 1
  • Add and publish the virtual application:
    Add-AppvclientPackage -Path C:\IgorPavlov_7Zip_9.20_ENG\1.00.00\IgorPavlov_7Zip_9.20_ENG_2_3_4.appv -DynamicDeploymentConfiguration "C:\IgorPavlov_7Zip_9.20_ENG\1.00.00\IgorPavlov_7Zip_9.20_ENG_2_3_4_DeploymentConfig with MSI.xml" | Publish-AppVClientPackage -Global
  • Check the logfile in C:\Windows\System32\LogFiles.
  • You will not find the application within ‘Programs and features’.
    Not in Programs and features.

    Not in Programs and features.

  • However, you can find it in the registry.
    You will find the application via regedit.

    You will find the application via regedit.

4. Remove the virtual application.

  • To uninstall the virtual application:
    Remove-AppVClientPackage -Name *Pavlov*
  • Check the logfile in C:\Windows\System32\LogFiles.



Using connection groups to interact locally installed applications with virtual applications

Let’s take the following scnenario: both Microsoft Visio 2010 and Microsoft Project 2010 have been virtualized using Application Virtualization v5.0 techniques. Microsoft Word 2010 has been installed locally. With Microsoft Application Virtualization prior to v5.0 it was not possible to use a virtualized application within a locally installed one. Unless you started the locally installed application within the same bubble as the virtualized application… Too much of a hassle.

In this example I will demonstrate how to use virtualized applications within a locally installed one.

Preparation: sequencing

  • Create a sequencing machine with Word 2010.
  • Start Word so there will be no more MSI Self Repairs
  • Install AppV 5.0 sequencer
  • Sequence both Visio 2010 and Project 2010.
    Make sure that the machine is reverted to a clean state before sequencing the second application.

Actions on the client

  • Make sure that the Application Virtualization Client 5.0 has been installed.
  • Start Powershell (x64) as an administator.
    Enter the command:

    Set-ExecutionPolicy -ExecutionPolicy Unrestricted

    Set-ExecutionPolicy -ExecutionPolicy Unrestricted

    Set-ExecutionPolicy -ExecutionPolicy Unrestricted

    Press ‘y’ to continue

  • Enter the command:
    Import-Module AppVClient

    Import-Module AppVClient

    Import-Module AppVClient

  • Add the virtualized Project 2010 with the command:
    Add-AppVClientPackage -Path "C:\AppV50\Microsoft_Project-AppV50_2010STD_MLI\Microsoft_Project-AppV50_2010STD_MLI_2.appv" | Publish-AppVClientPackage -Global

    Add-AppVClientPackage -Path "C:\AppV50\Microsoft_Project-AppV50_2010STD_MLI\Microsoft_Project-AppV50_2010STD_MLI_2.appv" | Publish-AppVClientPackage -Global

    Add-AppVClientPackage -Path “C:\AppV50\Microsoft_Project-AppV50_2010STD_MLI\Microsoft_Project-AppV50_2010STD_MLI_2.appv” | Publish-AppVClientPackage -Global

    Add-AppVClientPackage -Path "C:\AppV50\Microsoft_Project-AppV50_2010STD_MLI\Microsoft_Project-AppV50_2010STD_MLI_2.appv" | Publish-AppVClientPackage -Global  (Done)

    Add-AppVClientPackage -Path “C:\AppV50\Microsoft_Project-AppV50_2010STD_MLI\Microsoft_Project-AppV50_2010STD_MLI_2.appv” | Publish-AppVClientPackage -Global (Done)

  • Add the virtualized Visio 2010 with the command:
    Add-AppVClientPackage -Path "C:\AppV50\Microsoft_Visio-AppV50_2010STD_MLI\Microsoft_Visio-AppV50_2010STD_MLI_2_3.appv" | Publish-AppVClientPackage -Global

    Add-AppVClientPackage -Path "C:\AppV50\Microsoft_Visio-AppV50_2010STD_MLI\Microsoft_Visio-AppV50_2010STD_MLI_2_3.appv" | Publish-AppVClientPackage -Global

    Add-AppVClientPackage -Path “C:\AppV50\Microsoft_Visio-AppV50_2010STD_MLI\Microsoft_Visio-AppV50_2010STD_MLI_2_3.appv” | Publish-AppVClientPackage -Global

  • You will also find the results in the Event Viewer:

    Eventviewer

    Eventviewer

  • If you start the locally installed Word, you will not see the virtualized Visio 2010 and Project 2010:

    No Visio 2010 and Project 2010 in Word 2010.

    No Visio 2010 and Project 2010 in Word 2010.

  • To enable interaction between the locally installed application and the virtualized application the registry needs to be modified:
  • Start regedit and go to HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\AppV\Client\RunVirtual
  • Create the key ‘winword’.
  • Under the (Default) value, add both the PackageID and VerionId of Visio. There is an underscore between the PackageId and VersionId.

    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\AppV\Client\RunVirtual

    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\AppV\Client\RunVirtual

  • Use the following command to find both the PackageId and VersionId:
    Get-AppVClientPackage -Name *Visio*

    Get-AppVClientPackage -Name *Visio*

    Get-AppVClientPackage -Name *Visio*

  • If Word 2010 is started, then go to ‘Insert’ and select ‘Object’ from the ribbon. Then you have the option to include a Visio 2010 drawing:

    Virtualized Visio 2010 within locally installed Word 2010.

    Virtualized Visio 2010 within locally installed Word 2010.

  • An example:

    An example.

    An example.

Actions on the client: create the connection group.
Now only Visio 2010 is made available on the locally installed Word 2010. To include Project 2010, a connection group has to be used.

  • Create the connection group XML File ‘ProjectandVisio.xml’.
    
    
       
         
         
       
    
  • The AppConnectionGroupId and VersionId have been created with the command:
    [Guid]::NewGuid()

    [Guid]::NewGuid()

    [Guid]::NewGuid()

  • The PackageId and VersionId have been found with the command:
    Get-AppVClientPackage -Name *Microsoft*

    Get-AppVClientPackage -Name *Microsoft*

    Get-AppVClientPackage -Name *Microsoft*

    The PackageId and VerionId have been marked.

  • To enable the connectiongroup run the command:
    Add-AppvClientConnectionGroup -Path "C:\AppV50\ProjAndVisio.xml" | Enable-AppVClientConnectionGroup -Global

    Add-AppvClientConnectionGroup -Path "C:\AppV50\ProjAndVisio.xml" | Enable-AppVClientConnectionGroup -Global

    Add-AppvClientConnectionGroup -Path “C:\AppV50\ProjAndVisio.xml” | Enable-AppVClientConnectionGroup -Global

  • Also check the event viewer:

    Check the event viewer.

    Check the event viewer.

  • If you start the locally installed Word 2010, you will see both Project 2010 and Visio 2010:

    Virtualized Project 2010 and Visio 2010 in a locally installed Word 2010.

    Virtualized Project 2010 and Visio 2010 in a locally installed Word 2010.

  • This happens if a Project 2010 document is added:

    Project 2010 document.

    Project 2010 document.

  • If you see this warning please wait a short while and try again:

    Warning.

    Warning.

Cleaning up.

First remove the connection group:

  • Run the command:
    Get-AppVClientConnectionGroup | Remove-AppVClientConnectionGroup

    Then all the connection groups are removed.

    Get-AppVClientConnectionGroup | Remove-AppVClientConnectionGroup

    Get-AppVClientConnectionGroup | Remove-AppVClientConnectionGroup

  • And check the event viewer for the results:

    Event Viewer - Remove ConnectionGroup

    Event Viewer – Remove ConnectionGroup

Remove the virtual Microsoft applications
remove all the Microsoft applications. As only visio 2010 and Project 2010 have been added as a virtual Microsoft application, a wildcard can be used.

  • Run the command:
    Get-AppVClientPackage -Name *Microsoft* | Remove-AppVClientPackage

    Get-AppVClientPackage -Name *Microsoft* | Remove-AppVClientPackage

    Get-AppVClientPackage -Name *Microsoft* | Remove-AppVClientPackage

Known errors

  • Error:

    Add-AppvClientConnectionGroup : Application Virtualization Service failed to  complete requested operation.  Operation attempted: Add AppV Connection Group.  AppV Error Code: 0600000008.  Error module: Catalog. Internal error detail: 8A90160600000008.  Please consult AppV Client Event Log for more details.

    Add-AppvClientConnectionGroup : Application Virtualization Service failed to
    complete requested operation.
    Operation attempted: Add AppV Connection Group.
    AppV Error Code: 0600000008.
    Error module: Catalog. Internal error detail: 8A90160600000008.
    Please consult AppV Client Event Log for more details.

    Event Viewer

    Event Viewer

    In this case the connection group is installled, but one of the packages mentioned in the connection group XML File has not been added on the client.

  • In the runvirtual registry key it has no sense to add the AppConnectionGroupId and VersionId of the connection group XML file. It is completely ignored. Just add one of the PackageID / VersionId of one of the virtual applicatoins mentioned in the connection group XML file.

Example: linked objects in Excel

As an example both Visio 2010 and Project 2010 will be made available in a locally installed Excel 2010. A Visio 2010 object and a Project 2010 object will be inserted into Excel 2010.

  • Allow the locally installed Excel 2010 to run in a virtual environment:
    • Start regedit and go to HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\AppV\Client\RunVirtual
    • Create the key ‘excel.exe’.
    • Under the (Default) value, add both the PackageID and VersionID of Visio. There is a underscore between both the PackageID and VersionID.

    Allow the locally installed Excel 2010 to run in a virtual environment

    Allow the locally installed Excel 2010 to run in a virtual environment

  • Create a Visio 2010 object in Excel 2010:
    • Start Excel and go to ‘Insert’and select ‘Object’ from the ribbon.
    • Select Visio Drawing from the list.

    Create a Visio 2010 object in Excel 2010.

    Create a Visio 2010 object in Excel 2010.

    • Click [Ok]

    Visio 2010: Change drawing type.

    Visio 2010: Change drawing type.

    • Click [Ok].
  • Drag and drog some figures in Visio.
    Drag and drop some figures in Visio 2010.

    Drag and drop some figures in Visio 2010.

  • When ready click somewhere in the Excel sheet.
    When ready click somehwere in the sheet.

    When ready click somehwere in the sheet.

  • Do the same but then for Project.
    Insert Project 2010 object  in Excel.

    Insert Project 2010 object in Excel.

  • Dubble click on the Visio object. It is opened with Visio.
    Dubble click on the Visio 2010 object.

    Dubble click on the Visio 2010 object.


    Save the Excel sheet and quit Excel.
  • Remove the locally installed Excel 2010 to run in a virtual environment:Use regedit to modify (Default) for Excel: just put an underscore in front of the data.
    Disallow the locally installed Excel 2010 to run in a virtual environment

    Disallow the locally installed Excel 2010 to run in a virtual environment

  • Start Excel and open the saved sheet.
    Click on the visio 2010 object.
    Now there is a failure: “Cannot start the source application for this object.”

    Now there is a failure: “Cannot start the source application for this object.”


    Click [Ok].
    Now there is a failure: “Cannot start the source application for this object.”
  • The same when there is a double click on the Project object.
    Project 2010: Now there is a failure: “Cannot start the source application for this object.”

    Project 2010: Now there is a failure: “Cannot start the source application for this object.”


    Quit Excel without saving the sheet.
  • Allow the locally installed Excel 2010 to run in a virtual environment by removing the underscore.
    Allow the locally installed Excel 2010 to run in a virtual environment

    Allow the locally installed Excel 2010 to run in a virtual environment

  • And the Visio 2010 object works again.
    Visio 2010 works again.

    Visio 2010 works again.

More information