Centralizing the toolkit

Hello,

I was curious if any interest has been expressed in centralizing the toolkit so the bulk of code can reside in one area? I’ve somewhat done this already with Deploy-Application.ps1 in our SCCM environment where I add a few lines of code below the AppMainToolkit check to fail over to a central toolkit if a local one isn’t available. From here we can call just one instance of the exe and point it to app folders to kick off installs. The only things needed in the app folders are the Deploy-Application.ps1 and Files folder.

This got me thinking, it would be even more streamlined if all you needed in the app folders were the description variables and install/uninstall methods (including pre and post if needed). What are you thoughts? It would be neat to be dynamic enough to automatically use the local resources if they are there, but fall back to a central location if they’re not (eg. if you needed to deploy content via BITS over slower connections.)

The main reason I’ve gone this route is mainly for code maintenance. If a new version of the tools roll out, or if we need to make a minor tweak to all of our installs, it’s far easier to edit one instance than almost 200. Has anyone come up with other clever ways to maintain their PSAD packages?

Adam: I’d like to better understand your process in SCCM as I’d like to accomplish the same.
I’ve not been able to work through some roadblocks I’ve discovered.

Pre-SCCM
PSAD solved a lot of annoyances so I converted everything from batch, vbscript and some proprietary format to PoSH for use with PSAD.
Because PSAD is updated regularly - which is great - I decided to store everything in two locations:

  • Local location: C:\PSAD
  • <li>Network location: \\dfs\share\path\to\psad</li>
    

The Deploy-Application.ps1 template has been modified slightly to prefer the local but fall back on the network location.
And when a new version comes out we can quickly update:

  • The clients via GPO/GPP, Login script, our current software delivery solution etc.
  • The network by way of some dude responsible for updating the DFS share (namely me)

When I prepare an application, I:

      Use our Deploy-Application.ps1 template
      I use absolute paths (UNC mostly) in the .PS1's for all the files: the installers, files that need to be copied over like configuration files & shortcuts, .REG files that need to be merged etc.
      Create a shortcut (.LNK) that calls:
        C:\PSAD\Deploy-Application.exe "\\server\share\absolute\path\to\Deploy-App1.ps1"

When someone needs to install an application, like Visio, they run Install_Visio.lnk (e.g.: \server\share\path\to\Install_Visio.lnk) and everything runs beautifully.

Post SCCM
We just implemented SCCM - a small upgrade from MDT 2013 - and I’m now pulling in applications.
But since SCCM copies everything locally, I’m hitting a wall on how I can continue to use the centralized method above or one similar to it.

On a test machine I have the PSAD files locally and a manual test installation this way works fine:
c:\psad\deploy-application.exe "\server\share\absolute\path\to\Deploy-App1.ps1"

I then copy those install files locally to C:\Windows\ccmcache\1, simulating an SCCM deployment, and test again manually which works fine:
c:\psad\deploy-application.exe "C:\Windows\ccmcache\1\Deploy-App1.ps1"

I add the C:\PSAD path to my PATH environment variable and try once more which also works fine:
deploy-application.exe "C:\Windows\ccmcache\1\Deploy-App1.ps1"

Well, there’s no way of knowing what the SCCM directory name will be ahead of time, but since SCCM executes from that random directory within ccmcache, my thinking was:
If I execute deploy-application.exe from the same location where Deploy-App1.ps1 lives, it’ll just work itself out.
I was sorely mistaken: If I do the following from within an elevated command prompt in C:\Windows\ccmcache\1:
deploy-application.exe Deploy-App1.ps1

It assumes Deploy-App.ps1 lives in the same location as Deploy-Application.exe and fails with:

A critical component of the App Deployment Toolkit is missing. Unable to find the App Deploy Script file: C:\PSAD\Deploy-App.ps1 Please ensure you have all of the required files available to start the installation.

To be sure running the following from within an elevated command prompt in C:\Windows\ccmcache\1 also doesn’t work:
c:\psad\deploy-application.exe Deploy-App1.ps1

The only way to overcome that is to keep Deploy-Application.exe with Deploy-App.ps1.
But by virtue of doing that, one has to keep all the other required PSAD files otherwise this happens:

A critical component of the App Deployment Toolkit is missing. Unable to find the 'AppDeployToolkit' folder. Please ensure you have all of the required files available to start the installation.

So we’re back to square one: We have to add the PSAD files to every package and when a new update is released, we have to somehow update all those PSAD files for each application then update the content for all applications in SCCM and redistribute.

Short of doing that, is there another approach that’s working for others?

I spent some time on this last week and came up with the following for centralizing the PSAD in an SCCM environment.
I’m really interested in feedback and constructive criticism on this.

** WARNING **
Proceed at your own risk!

Centralize your PSAD
Put your PSAD files somewhere on the local machine.
To keep things simple, lets put everything in C:\PSAD

Updating Deploy-Application.exe
Download the source code from GitHub
Open the Deploy-Application solution (Deploy-Aplpication.sln)
In the variables section add this:

    string appDeployCWD = Environment.CurrentDirectory;
Further down look for this line:
    appDeployScriptPath = Path.Combine(currentAppFolder, appDeployScriptPath);
Replace it with this:
    appDeployScriptPath = Path.Combine(appDeployCWD, appDeployScriptPath);
In the same area, look for this line:
    appDeployScriptPath = Path.Combine(currentAppFolder, appDeployScriptPath);
Replace it with this:
    appDeployScriptPath = Path.Combine(appDeployCWD, appDeployScriptPath);
Build it BONUS: Update the version so you can distinguish your modified EXE from the stock one

Updating Deploy-Application.ps1
In the Try/Catch where the AppDeployToolkitMain.ps1 is dot-sourced, find this line:

    [string]$moduleAppDeployToolkitMain = "$scriptDirectory\AppDeployToolkit\AppDeployToolkitMain.ps1"

And replace it with this:

    [string]$moduleAppDeployToolkitMain = "$envSystemDrive\PSAD\AppDeployToolkit\AppDeployToolkitMain.ps1"

Just before the end of the variable declaration section, you’ll need to add two lines of code:

    Push-Location $scriptDirectory -Verbose [Environment]::CurrentDirectory = $scriptDirectory

Alternatively, add -WorkingDirectory $scriptDirectory to all Execute-MSI and Execute-Process functions.


I did some quick and fast testing with one of our most temperamental applications and it worked as expected. However, I suspect there will be some issues with copying files and haven’t had an opportunity to dedicate time to look at that.

Hi Julius,

We’re currently doing something very similar to your solution. I have a spot on the network that holds the centralized PsAppDeployMain.ps1. I have a clause in our psAppDeploy.ps1 that looks like:

Try { [string]$moduleAppDeployToolkitMain = "$scriptDirectory\AppDeployToolkit\AppDeployToolkitMain.ps1" If (-not (Test-Path -Path $moduleAppDeployToolkitMain -PathType Leaf)) { $moduleAppDeployToolkitMain = "$scriptNetworkDirectory\AppDeployToolkit\AppDeployToolkitMain.ps1" If (-not (Test-Path -Path $moduleAppDeployToolkitMain -PathType Leaf)) { Throw "Module does not exist at the specified location [$moduleAppDeployToolkitMain]." } } If ($DisableLogging) { . $moduleAppDeployToolkitMain -DisableLogging } Else { . $moduleAppDeployToolkitMain } } Catch { [int32]$mainExitCode = 60008 Write-Error -Message "Module [$moduleAppDeployToolkitMain] failed to load: n$($_.Exception.Message)n `

n$($_.InvocationInfo.PositionMessage)" -ErrorAction ‘Continue’
Exit $mainExitCode
}</blockquote>

(where $scriptNetworkDirectory is the alternative centralized location)

Also, while this will work if using the .ps1, this will not work if launching view the .exe. So the following had to be changed there as well.

New variable:
string appDeployConfigPath = Path.Combine(currentAppFolder, "PSADConfig.xml");

New discovery of the appDeployToolkitFolder:

// Verify if the App Deploy Toolkit folder exists if (!Directory.Exists(appDeployToolkitFolder)) { if (!File.Exists(appDeployConfigPath)) { throw new Exception("A critical component of the App Deployment Toolkit is missing." + Environment.NewLine + Environment.NewLine + "Unable to find the 'AppDeployToolkit' folder." + Environment.NewLine + Environment.NewLine + "Please ensure you have all of the required files available to start the installation."); } else { XmlDocument PSADConfig = new XmlDocument(); PSADConfig.Load(appDeployConfigPath); XmlNode PSADConfigNode = null; XmlElement PSADConfigRoot = PSADConfig.DocumentElement; PSADConfigNode = PSADConfigRoot.SelectSingleNode("/doc/toolkitpath"); appDeployToolkitFolder = Convert.ToString(PSADConfigNode.InnerText).Trim(); appDeployToolkitXMLPath = Path.Combine(appDeployToolkitFolder, "AppDeployToolkitConfig.xml");
                    if (!Directory.Exists(appDeployToolkitFolder))
                    {
                        throw new Exception("A critical component of the App Deployment Toolkit is missing." + Environment.NewLine + Environment.NewLine + "Unable to find the " + appDeployToolkitFolder + " folder." + Environment.NewLine + Environment.NewLine + "Please ensure you have all of the required files available to start the installation.");
                    }
                }                    
            }

PSADConfig.xml example (resides in same folder as centralized kit):

<?xml version="1.0"?> <doc> <toolkitpath>\\server\share\centralized\PSAppDeploy\AppDeployToolkit</toolkitpath> </doc>

This works great for SCCM deployments, and cuts down on our code maintenance considerably. However, if you’re set up in an environment where deployments must content based (download and execute), you’ll probably want to use an environment variable.

For example…let’s say you have a centralized PSAppDeployToolkit. You would make it a prerequisite and have a script with the toolkit that sets an environment variable (or local xml) to the location of the toolkit on the PC (basically the sms cache location including the PackageID). With that method, you wont have to worry about having to update all of the PsAppDeploy scripts and Deploy-Application.exe’s with a new PackageID path if you ever have to redo the package…they’d simply know where to look and would be future proof! :slight_smile:

One more thing with SCCM…I’m not sure if it’s been fixed with the more recent builds…there’s a bug with AppDeployToolkitMain.ps1 that will cause it to bomb out when used in OSD task sequences because the variable USERDOMAIN doesn’t exist yet. A easy fix for this (line 117 on my copy) is to put a try/catch statement in there:

Try { [string]$envUserDomain = $env:USERDOMAIN | Where-Object { $_ } | ForEach-Object { $_.ToUpper() } } catch { }

Hello all, I have really dragged my feet on leveraging PSAppDeploymentToolkit, but it is such a great tool, I had to dive in. The first issue as stated by this topic is centralizing/updating it. Obviously having to copy the toolkit and scripts to each app each time you make a change or update would not be a prudent use of time. So I found a way, that can be easily modified. The only thing I have not done is recompile the EXE as I have not modified the code for that.

Running a powershell script as an application from MDT/SCCM and then specifying the command line as the following:

powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -NoLogo -File “.\Deploy-Application.ps1” -AppDeployToolkitPath “%DEPLOYROOT%%OrgFolder%\PSAppDeploymentToolkit\AppDeployToolkit\AppDeployToolkitMain.ps1”

I added a variable that allows you to specify the Toolkit functions path (AppDeployToolkitPath)

Now you can run a “Deploy-Application.ps1” and simply specify the toolkit path via an argument! This fixes the centralization issue altogether. I also added logic to import the MDT module so that access would be granted to read/write task sequence variables. That was the only disadvantage to running the script as an application, but is now a non issue.

I hope this helps somebody! Have a blessed day folks!

Added one scripts arguments

<pre class=“brush: powershell; gutter: true; first-line: 1; highlight: []; html-script: false”>[CmdletBinding()]
Param (
[Parameter(Mandatory=$false)] <<<<<<<<
[string]$AppDeployToolkitPath, <<<<<<<<
[Parameter(Mandatory=$false)]
[ValidateSet(‘Install’,‘Uninstall’)]
[string]$DeploymentType = ‘Install’,
[Parameter(Mandatory=$false)]
[ValidateSet(‘Interactive’,‘Silent’,‘NonInteractive’)]
[string]$DeployMode = ‘Interactive’,
[Parameter(Mandatory=$false)]
[switch]$AllowRebootPassThru = $false,
[Parameter(Mandatory=$false)]
[switch]$TerminalServerMode = $false,
[Parameter(Mandatory=$false)]
[switch]$DisableLogging = $false

Added 3 lines for path

<pre class=“brush: powershell; gutter: true; first-line: 1; highlight: []; html-script: false”> ## Dot source the required App Deploy Toolkit Functions
Try {
If (!($AppDeployToolkitPath)) {If (Test-Path -Path TSEnv: -ErrorAction SilentlyContinue) {$AppDeployToolkitPath = “$($TSEnv:Deployroot)$($TSEnv:OrgFolder)\PSAppDeploymentToolkit\AppDeployToolkit\AppDeployToolkitMain.ps1”} Else {$AppDeployToolkitPath = “$($PSScriptRoot)\AppDeployToolkit\AppDeployToolkitMain.ps1”}}
$AppDeployToolkitPath = “$(If (($AppDeployToolkitPath).StartsWith(’\’)) {($AppDeployToolkitPath -Replace ‘\\’, ‘&#039;).TrimEnd(’&#039;).Insert(0, ‘&#039;)} Else {($AppDeployToolkitPath).Replace(’\’,’&#039;).TrimEnd(’&#039;)})”
$ModuleAppDeployToolkitMain = $AppDeployToolkitPath
If (-not (Test-Path -LiteralPath $moduleAppDeployToolkitMain -PathType ‘Leaf’)) { Throw “Module does not exist at the specified location [$moduleAppDeployToolkitMain].” }
If ($DisableLogging) { . $moduleAppDeployToolkitMain -DisableLogging } Else { . $moduleAppDeployToolkitMain }
}
Catch {
If ($mainExitCode -eq 0){ [int32]$mainExitCode = 60008 }
Write-Error -Message “Module [$moduleAppDeployToolkitMain] failed to load: n$($.Exception.Message)n `n$($.InvocationInfo.PositionMessage)” -ErrorAction ‘Continue’
## Exit the script, returning the exit code to SCCM
If (Test-Path -LiteralPath ‘variable:HostInvocation’) { $script:ExitCode = $mainExitCode; Exit } Else { Exit $mainExitCode }
}

Added code to import MDT module
<pre class=“brush: powershell; gutter: true; first-line: 1; highlight: []; html-script: false”> ##Import Microsoft Deployment Toolkit Module (This allows you to access MDT variables from within script during task sequence)
If (Test-Path -Path “$Env:SystemDrive\MININT\Modules\ZTIUtility”)
{
Try {Import-Module -Name “$Env:SystemDrive\MININT\Modules\ZTIUtility” -Global -Verbose -NoClobber -Force} Catch {}
}
Else
{
$NetworkDrives = Get-WmiObject -Namespace “Root\CIMv2” -Class “Win32_LogicalDisk” -Property * | Where-Object {($_.DriveType -eq 4)}
ForEach ($Item In $NetworkDrives)
{
If (Test-Path -Path “$($Item.DeviceID)\Tools\Modules\ZTIUtility”) {Try {(Import-Module -Name “$($Item.DeviceID)\Tools\Modules\ZTIUtility” -Global -Verbose -NoClobber -Force); (Break)} Catch {}}
}
}

If you want to access MDT/SCCM variables
<pre class=“brush: powershell; gutter: true; first-line: 1; highlight: []; html-script: false”>Get-ChildItem TSEnv: -ErrorAction SilentlyContinue