How to enable On Demand Installations on Windows using MS PowerShell, Chocolatey, Git and Nexus.

Deeptesh Bhattacharya
11 min readJul 20, 2021

There is always a trade-off between a centralized IT function and a federated IT function where the latter gives the developers flexibility around tools and technologies. However, still as the part of the Centralized IT function how can you enable federation and still manage the primary control?

This article takes you through the approach of managing Federated IT systems while you are adopting your DevOps journey towards increased automation and efficiency.

A lot of time is wasted across when you have to enable developers on projects which are spread across different technologies. The developers always needs some tools to be installed on to their systems based on project requirements. This increases hierarchical red-tape and overall MTTF (Mean-Time-To-Fulfillment)

In general the process will involve something like this:

The process can be further made efficient and automated by enabling federation using tools like CI/CD and a repository manager. The overall process can be hooked up to Jira Service Desk, Jenkins CI/CD and Nexus Artifactory.

However, to further achieve more federation and enabling the developers to manage their own installs on-demand. This can be enabled using a package management tool.

The workflow will somewhat look similar to the diagram below:

If the software needs to be provisioned across pre-identified machines then the toolset can either use Puppet or Ansible for managing the configurations of the systems.

However, in order to achieve full federation and support on-demand installation the team can further leverage PowerShell and Chocolatey.

Chocolatey supports installations across Windows and can pull artifacts from Nexus repository for installations. These packages can be auto generated using PowerShell and converted into Nuget Packages, as chocolatey uses a Nuget Package for installation from a Nuget Repository.

Using Jenkins and Powershell, the tested packages can be converted into Nuget Packages with in the integrated pipelines during build and release and made available for self hosted proprietary private NuGet repository under as strict access control using Nexus RBACs and Security Realms.

For internal packages, the above process can used to create and push new packages whenever application teams release a new version for other teams to consume it on demand.

Here’s is an example of a Powershell Automation script which is used to automate the package conversion to Nuget Package. The package can comprise of any executable.

It collects the package variables from the variables defined during the build process and further integrates that to generate a Choco package using the chocolatey templating engine.

The entire process can be broken down into 5 pieces as key baby steps to achieve such automation.

  1. Define your own chocolatey Templates or use existing one —

For eg. if you have to write your own chocolatey package you can use the below templating framework

# IMPORTANT: Before releasing this package, copy/paste the next 2 lines into PowerShell to remove all comments from this file:#   $f='c:\path\to\thisFile.ps1'#   gc $f | ? {$_ -notmatch "^\s*#"} | % {$_ -replace '(^.*?)\s*?[^``]#.*','$1'} | Out-File $f+".~" -en utf8; mv -fo $f+".~" $f# 1. See the _TODO.md that is generated top level and read through that# 2. Follow the documentation below to learn how to create a package for the package type you are creating.# 3. In Chocolatey scripts, ALWAYS use absolute paths - $toolsDir gets you to the package's tools directory.$ErrorActionPreference = 'Stop'; # stop on all errors$toolsDir   = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"# Internal packages (organizations) or software that has redistribution rights (community repo)# - Use `Install-ChocolateyInstallPackage` instead of `Install-ChocolateyPackage`#   and put the binaries directly into the tools folder (we call it embedding)#$fileLocation = Join-Path $toolsDir 'NAME_OF_EMBEDDED_INSTALLER_FILE'# If embedding binaries increase total nupkg size to over 1GB, use share location or download from urls#$fileLocation = '\\SHARE_LOCATION\to\INSTALLER_FILE'# Community Repo: Use official urls for non-redist binaries or redist where total package size is over 200MB# Internal/Organization: Download from internal location (internet sources are unreliable)$url        = '' # download url, HTTPS preferred$url64      = '' # 64bit URL here (HTTPS preferred) or remove - if installer contains both (very rare), use $url$packageArgs = @{packageName   = $env:ChocolateyPackageNameunzipLocation = $toolsDirfileType      = 'EXE_MSI_OR_MSU' #only one of these: exe, msi, msuurl           = $urlurl64bit      = $url64#file         = $fileLocationsoftwareName  = 'myapptemplate.template*' #part or all of the Display Name as you see it in Programs and Features. It should be enough to be unique# Checksums are now required as of 0.10.0.# To determine checksums, you can get that from the original site if provided.# You can also use checksum.exe (choco install checksum) and use it# e.g. checksum -t sha256 -f path\to\filechecksum      = ''checksumType  = 'sha256' #default is md5, can also be sha1, sha256 or sha512checksum64    = ''checksumType64= 'sha256' #default is checksumType# MSIsilentArgs    = "/qn /norestart /l*v `"$($env:TEMP)\$($packageName).$($env:chocolateyPackageVersion).MsiInstall.log`"" # ALLUSERS=1 DISABLEDESKTOPSHORTCUT=1 ADDDESKTOPICON=0 ADDSTARTMENU=0validExitCodes= @(0, 3010, 1641)# OTHERS# Uncomment matching EXE type (sorted by most to least common)#silentArgs   = '/S'           # NSIS#silentArgs   = '/VERYSILENT /NORESTART /SP-' # Inno Setup#silentArgs   = '/s'           # InstallShield#silentArgs   = '/s /v"/qn"'   # InstallShield with MSI#silentArgs   = '/s'           # Wise InstallMaster#silentArgs   = '-s'           # Squirrel#silentArgs   = '-q'           # Install4j#silentArgs   = '-s'           # Ghost# Note that some installers, in addition to the silentArgs above, may also need assistance of AHK to achieve silence.#silentArgs   = ''             # none; make silent with input macro script like AutoHotKey (AHK)#       https://chocolatey.org/packages/autohotkey.portable#validExitCodes= @(0) #please insert other valid exit codes here}Install-ChocolateyPackage @packageArgs # https://chocolatey.org/docs/helpers-install-chocolatey-package#Install-ChocolateyZipPackage @packageArgs # https://chocolatey.org/docs/helpers-install-chocolatey-zip-package## If you are making your own internal packages (organizations), you can embed the installer or## put on internal file share and use the following instead (you'll need to add $file to the above)#Install-ChocolateyInstallPackage @packageArgs # https://chocolatey.org/docs/helpers-install-chocolatey-install-package## Main helper functions - these have error handling tucked into them already## see https://chocolatey.org/docs/helpers-reference## Install an application, will assert administrative rights## - https://chocolatey.org/docs/helpers-install-chocolatey-package## - https://chocolatey.org/docs/helpers-install-chocolatey-install-package## add additional optional arguments as necessary##Install-ChocolateyPackage $packageName $fileType $silentArgs $url [$url64 -validExitCodes $validExitCodes -checksum $checksum -checksumType $checksumType -checksum64 $checksum64 -checksumType64 $checksumType64]## Download and unpack a zip file - https://chocolatey.org/docs/helpers-install-chocolatey-zip-package##Install-ChocolateyZipPackage $packageName $url $toolsDir [$url64 -checksum $checksum -checksumType $checksumType -checksum64 $checksum64 -checksumType64 $checksumType64]## Install Visual Studio Package - https://chocolatey.org/docs/helpers-install-chocolatey-vsix-package#Install-ChocolateyVsixPackage $packageName $url [$vsVersion] [-checksum $checksum -checksumType $checksumType]#Install-ChocolateyVsixPackage @packageArgs## see the full list at https://chocolatey.org/docs/helpers-reference## downloader that the main helpers use to download items## if removing $url64, please remove from here## - https://chocolatey.org/docs/helpers-get-chocolatey-web-file#Get-ChocolateyWebFile $packageName 'DOWNLOAD_TO_FILE_FULL_PATH' $url $url64## Installer, will assert administrative rights - used by Install-ChocolateyPackage## use this for embedding installers in the package when not going to community feed or when you have distribution rights## - https://chocolatey.org/docs/helpers-install-chocolatey-install-package#Install-ChocolateyInstallPackage $packageName $fileType $silentArgs '_FULLFILEPATH_' -validExitCodes $validExitCodes## Unzips a file to the specified location - auto overwrites existing content## - https://chocolatey.org/docs/helpers-get-chocolatey-unzip#Get-ChocolateyUnzip "FULL_LOCATION_TO_ZIP.zip" $toolsDir## Runs processes asserting UAC, will assert administrative rights - used by Install-ChocolateyInstallPackage## - https://chocolatey.org/docs/helpers-start-chocolatey-process-as-admin#Start-ChocolateyProcessAsAdmin 'STATEMENTS_TO_RUN' 'Optional_Application_If_Not_PowerShell' -validExitCodes $validExitCodes## To avoid quoting issues, you can also assemble your -Statements in another variable and pass it in#$appPath = "$env:ProgramFiles\appname"##Will resolve to C:\Program Files\appname#$statementsToRun = "/C `"$appPath\bin\installservice.bat`""#Start-ChocolateyProcessAsAdmin $statementsToRun cmd -validExitCodes $validExitCodes## add specific folders to the path - any executables found in the chocolatey package## folder will already be on the path. This is used in addition to that or for cases## when a native installer doesn't add things to the path.## - https://chocolatey.org/docs/helpers-install-chocolatey-path#Install-ChocolateyPath 'LOCATION_TO_ADD_TO_PATH' 'User_OR_Machine' # Machine will assert administrative rights## Add specific files as shortcuts to the desktop## - https://chocolatey.org/docs/helpers-install-chocolatey-shortcut#$target = Join-Path $toolsDir "$($packageName).exe"# Install-ChocolateyShortcut -shortcutFilePath "<path>" -targetPath "<path>" [-workDirectory "C:\" -arguments "C:\test.txt" -iconLocation "C:\test.ico" -description "This is the description"]## Outputs the bitness of the OS (either "32" or "64")## - https://chocolatey.org/docs/helpers-get-o-s-architecture-width#$osBitness = Get-ProcessorBits## Set persistent Environment variables## - https://chocolatey.org/docs/helpers-install-chocolatey-environment-variable#Install-ChocolateyEnvironmentVariable -variableName "SOMEVAR" -variableValue "value" [-variableType = 'Machine' #Defaults to 'User']## Set up a file association## - https://chocolatey.org/docs/helpers-install-chocolatey-file-association#Install-ChocolateyFileAssociation## Adding a shim when not automatically found - Cocolatey automatically shims exe files found in package directory.## - https://chocolatey.org/docs/helpers-install-bin-file## - https://chocolatey.org/docs/create-packages#how-do-i-exclude-executables-from-getting-shims#Install-BinFile##PORTABLE EXAMPLE#$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"# despite the name "Install-ChocolateyZipPackage" this also works with 7z archives#Install-ChocolateyZipPackage $packageName $url $toolsDir $url64## END PORTABLE EXAMPLE## [DEPRECATING] PORTABLE EXAMPLE#$binRoot = Get-BinRoot#$installDir = Join-Path $binRoot "$packageName"#Write-Host "Adding `'$installDir`' to the path and the current shell path"#Install-ChocolateyPath "$installDir"#$env:Path = "$($env:Path);$installDir"# if removing $url64, please remove from here# despite the name "Install-ChocolateyZipPackage" this also works with 7z archives#Install-ChocolateyZipPackage "$packageName" "$url" "$installDir" "$url64"## END PORTABLE EXAMPLE

Or Write your own installation script. The script below is used to install Nexus Repository.

# Nexus Download Link: http://www.sonatype.org/downloads/nexus-latest-bundle.zipClear-Host# URL Parameter$WebURL = "http://www.sonatype.org/downloads/nexus-latest-bundle.zip"# Directory Parameter$FileDirectory = "$($env:USERPROFILE)$("\downloads\")"#Write-Output $FileDirectory# If directory doesn't exist create the directoryif((Test-Path $FileDirectory) -eq 0){mkdir $FileDirectory;}# We assume the file you download is named what you want it to be on your computer$FileName = [System.IO.Path]::GetFileName($WebURL)# Concatenate the two values to prepare the download$FullFilePath = "$($FileDirectory)$($FileName)"#Write-Output $FullFilePathfunction Get-FileDownload([String] $WebURL, [String] $FullFilePath){# Give a basic message to the user to let them know what we are doingWrite-Output "Downloading '$WebURL' to '$FullFilePath'"$uri = New-Object "System.Uri" "$WebURL"$request = [System.Net.HttpWebRequest]::Create($uri)$request.set_Timeout(30000) #15 second timeout$response = $request.GetResponse()$totalLength = [System.Math]::Floor($response.get_ContentLength()/1024)$responseStream = $response.GetResponseStream()$targetStream = New-Object -TypeName System.IO.FileStream -ArgumentList $FullFilePath, Create$buffer = new-object byte[] 10KB$count = $responseStream.Read($buffer,0,$buffer.length)$downloadedBytes = $countwhile ($count -gt 0){[System.Console]::Write("`r`nDownloaded {0}K of {1}K", [System.Math]::Floor($downloadedBytes/1024), $totalLength)$targetStream.Write($buffer, 0, $count)$count = $responseStream.Read($buffer,0,$buffer.length)$downloadedBytes = $downloadedBytes + $count}$targetStream.Flush()$targetStream.Close()$targetStream.Dispose()$responseStream.Dispose()# Give a basic message to the user to let them know we are doneWrite-Output "`r`nDownload complete"}function Expand-ZipFile([string]$File, [string]$Destination) #The targets to run.{# If directory doesn't exist create the directoryif((Test-Path $Destination) -eq 0){mkdir $Destination;}$Shell = new-object -com shell.application# Get the name of the Zip file$Zip = $Shell.NameSpace($File)#Expand/Extract each file from the zip fileforeach($Item in $Zip.items()){$Shell.Namespace($Destination).copyhere($Item)}}Get-FileDownload $WebURL  $FullFilePathExpand-ZipFile $FullFilePath c:\NexusSet-Location C:\Nexus$NexusFolder = (Get-ChildItem nexus* | Select-Object Name).Name# Create System Variable[Environment]::SetEnvironmentVariable("NEXUS_HOME", "C:\Nexus\$NexusFolder", "Machine")Set-Location "C:\Nexus\$NexusFolder"# Configure C:\Nexus\nexus-2.12.0-01\conf\nexus.properties#     Set Port Number if you want something other than 8081Set-Location bin# Nexus Download Link: http://www.sonatype.org/downloads/nexus-latest-bundle.zipClear-Host# URL Parameter$WebURL = "http://www.sonatype.org/downloads/nexus-latest-bundle.zip"# Directory Parameter$FileDirectory = "$($env:USERPROFILE)$("\downloads\")"#Write-Output $FileDirectory# If directory doesn't exist create the directoryif((Test-Path $FileDirectory) -eq 0){mkdir $FileDirectory;}# We assume the file you download is named what you want it to be on your computer$FileName = [System.IO.Path]::GetFileName($WebURL)# Concatenate the two values to prepare the download$FullFilePath = "$($FileDirectory)$($FileName)"#Write-Output $FullFilePathfunction Get-FileDownload([String] $WebURL, [String] $FullFilePath){# Give a basic message to the user to let them know what we are doingWrite-Output "Downloading '$WebURL' to '$FullFilePath'"$uri = New-Object "System.Uri" "$WebURL"$request = [System.Net.HttpWebRequest]::Create($uri)$request.set_Timeout(30000) #15 second timeout$response = $request.GetResponse()$totalLength = [System.Math]::Floor($response.get_ContentLength()/1024)$responseStream = $response.GetResponseStream()$targetStream = New-Object -TypeName System.IO.FileStream -ArgumentList $FullFilePath, Create$buffer = new-object byte[] 10KB$count = $responseStream.Read($buffer,0,$buffer.length)$downloadedBytes = $countwhile ($count -gt 0){[System.Console]::Write("`r`nDownloaded {0}K of {1}K", [System.Math]::Floor($downloadedBytes/1024), $totalLength)$targetStream.Write($buffer, 0, $count)$count = $responseStream.Read($buffer,0,$buffer.length)$downloadedBytes = $downloadedBytes + $count}$targetStream.Flush()$targetStream.Close()$targetStream.Dispose()$responseStream.Dispose()# Give a basic message to the user to let them know we are doneWrite-Output "`r`nDownload complete"}function Expand-ZipFile([string]$File, [string]$Destination) #The targets to run.{# If directory doesn't exist create the directoryif((Test-Path $Destination) -eq 0){mkdir $Destination;}$Shell = new-object -com shell.application# Get the name of the Zip file$Zip = $Shell.NameSpace($File)#Expand/Extract each file from the zip fileforeach($Item in $Zip.items()){$Shell.Namespace($Destination).copyhere($Item)}}Get-FileDownload $WebURL  $FullFilePathExpand-ZipFile $FullFilePath c:\NexusSet-Location C:\Nexus$NexusFolder = (Get-ChildItem nexus* | Select-Object Name).Name# Create System Variable[Environment]::SetEnvironmentVariable("NEXUS_HOME", "C:\Nexus\$NexusFolder", "Machine")Set-Location "C:\Nexus\$NexusFolder"# Configure C:\Nexus\nexus-2.12.0-01\conf\nexus.properties#     Set Port Number if you want something other than 8081Set-Location binStart-Process nexus.bat install -WaitStart-Process nexus.bat start -WaitStart-Process 'http://localhost:8081/nexus'

Some of these features are part of Chocolatey Enterprise so might not be available for open source. Please have a look at Chocolatey Features set at

Built-In Functions

https://chocolatey.org/docs/helpers-reference

2. Write a Nuget Package conversion tool using PowerShell for auto generation of packages you can use the code reference from example above.

3. Embed this tool into your existing CI/CD pipeline.

4. Enable download of chocolatey from the Self Service Portal and run initial scripts for configuring chocolatey repos.

5. Optional — Enable all of this as part of your IT/DevOps self service portal.

I hope this article helps bring your Developers and Operations team closer and can support your OKR’s for the year.

I am sincerely thankful to Rob Reynolds (Rob Reynold) for his contribution towards automation in the Windows ecosystem. Rob has rich experience in infrastructure automation and modern automation approaches (something mostly all of us call “CI/CD” and “DevOps” nowadays).

--

--

Deeptesh Bhattacharya

A DevOps practitioner looking for Sponsored Visa Opportunities in Europe.