Skip to content

Instantly share code, notes, and snippets.

@nathanverrilli
Last active August 18, 2022 21:40
Show Gist options
  • Save nathanverrilli/7d5b33729de6654d04e1e3ebf1ad1faa to your computer and use it in GitHub Desktop.
Save nathanverrilli/7d5b33729de6654d04e1e3ebf1ad1faa to your computer and use it in GitHub Desktop.
Powershell Script to Install the Backblaze Client
<#
powershell script to install backblaze backup client
-groupID <groupID>
-groupToken <groupToken>
-userEmail <userEmail>c
-password <password>
-sso
-adfs
-csvMapfile <mapfile>
-emailHeader <email_header_value>
-hostHeader <host_header_value>
-installSrc <src_directory>
<# no positional parameters accepted #>
[CmdletBinding(PositionalBinding=$false)]
param( [string]$groupID,
[string]$groupToken,
[string]$userEmail,
[string]$password = "none",
[string]$csvMapfile,
[string]$emailHeader = "Email",
[string]$hostHeader = "Hostname",
[string]$installSrc,
[switch]$sso,
[switch]$adfs
)
$LOGNAME="Backblaze Powershell Installation Script"
<# false will turn off host console messages #>
#$debug = $false
<# true will turn host console messages on and some additional stuff #>
$debug = $true
$BACKBLAZE_INSTALLER = 'install_backblaze.exe'
#############
<# #############################
# This is specific to my testing environment
# Feel free to test & develop, but these may need other values
#>
# $env:COMPUTERNAME returns the netbios name, limited to 15 characters
# This returns the full hostname.
$localMachineHostname = [system.net.dns]::gethostname()
<# ################################ #>
<# All events are logged to the Application log regardless #>
<# register this as a source in the system Application log #>
if ( -not ( [System.Diagnostics.EventLog]::SourceExists($LOGNAME) ) ) {
New-EventLog -LogName Application -Source $LOGNAME
}
<# event numbering for Application log, specify namespace as is referenced in a function #>
[int]$global:eventIx = 1
<# explicitly set this to empty string as it is tested in doCleanup #>
$tempDirName = ""
<# need to remove the temporary directory? #>
$flagNeedDirCleanup = $false
<# if and only if the author's setup vars #>
if ( ("MYTHOS" -eq $localMachineHostname) -or
("DESKTOP-3D2E2U1" -eq $localMachineHostname) -or
("MythosLotus" -eq $localMachineHostname) ) {
$groupID = "zodmo"
$groupToken = "pismo"
$csvMapFile = "c:\tmp\testfile.csv"
$installSrc = "c:\tools\gowork\bin"
$password = "none"
$BACKBLAZE_INSTALLER = 'testinstall.exe'
MyOutput " *** THIS IS THE AUTHOR'S PRIVATE DEBUG MODE *** "
}
# Either the value is passed in, or it can be hardcoded
# has to be there, though. If it's just left as a value
# it will throw an error eventually
if ( $installSrc ) {
$BACKBLAZE_INSTALL_DIR = $installSrc
} else {
$BACKBLAZE_INSTALL_DIR = join-path $localMachineHostname "\tmp\backblaze_install_dir"
MyOutput "WARNING: BACKBLAZE INSTALL DIRECTORY MAY NOT BE SET CORRECTLY (set to ${localMachineHostname})"
}
##################################################
# functions called multiple places or to clarify program logic
##################################################
function ScriptUsage {
write-host "TODO: helpful usage message here"
}
##################################################
function DebugOutput {
param( [string]$out )
if ( $debug ) {
write-host "[debug[${global:eventIx}]] $out"
}
}
##################################################
function MyOutput {
param( [string]$out )
# Everything to the applog
Write-EventLog –LogName Application –Source $LOGNAME –EntryType Information –EventID $eventIX –Message $out
DebugOutput "event ${global:eventIx}: $out"
$global:eventIx = $global:eventIx + 1
}
##################################################
function MyThrow {
param( [string]$throwMessage )
MyOutput $throwMessage
Throw $throwMessage
}
##################################################
function directoryCleanup {
# name exists and the directory exists?
if ( ( $tempDirName ) -and (test-path $tempDirName) ) {
# work around an error in Remove-Item's *recurse* functionality
# remove the install data copied from the network IN the directory
Get-ChildItem -path $tempDirName -recurse | Remove-Item -force -recurse
# and then remove the directory itself
Remove-Item $tempDirName -force -recurse
$flagNeedDirCleanup = $false
}
}
##################################################
function doCleanup {
if ($flagNeedDirCleanup) {
directoryCleanup
}
}
##################################################
function isValidEmail {
param($test)
<# ###########################################
Trying to validate an email address by form
is beyond painful. Fortunately, Microsoft will
do it for us. Cast the string into a [mailaddress]
and if the cast works, it is syntactically legal
############################################ #>
try {
$rc = [mailaddress]$test
return $true
} catch {
return $false
}
return $false
}
#####################################
function isMissing {
param ($list, $value)
if ($debug) {
write-host "looking for $value in variable list"
write-host $list
}
foreach ($val in $list) {
if ($val -eq $value) {
return $false
}
}
return $true
}
#####################################
function checkPermissions {
param($thing)
if ( $debug ) {
write-host $thing
(get-acl ${thing}).access | ft IdentityReference,FileSystemRights,AccessControlType,IsInherited,InheritanceFlags -auto
}
}
#######################################
function testFileLock {
param ( [parameter(Mandatory=$true)][string]$Path)
$oFile = New-Object System.IO.FileInfo $Path
if (-not (Test-Path -Path $Path) ) {
return $false
}
try {
$oStream = $oFile.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
if ($oStream) {
$oStream.Close()
}
return $false
} catch {
# file is locked by a process.
return $true
}
}
#############################################
function Is-Installed( $program ) {
$installed86 = ((Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall") |
Where-Object { $_.GetValue( "DisplayName" ) -like "*$program*" } ).Length -gt 0;
$installed64 = ((Get-ChildItem "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall") |
Where-Object { $_.GetValue( "DisplayName" ) -like "*$program*" } ).Length -gt 0;
return $installed86 -or $installed64;
}
#############################################
if ( Is-Installed( "Backblaze" ) ) {
MyThrow( "Backblaze is already installed on $localMachineHostname" )
#exits, does not return
}
<# test for SSO #>
if ( $sso ) {
$password = "sso"
}
<# This sets $emailAddress #>
if ( $adfs ) {
$dotNetPath=[System.IO.Path]::GetDirectoryName([Object].GetType().Assembly.Location)
[Reflection.Assembly]::LoadFile($dotNetPath + '\System.DirectoryServices.AccountManagement.dll')
$emailAddress = [System.DirectoryServices.AccountManagement.UserPrincipal]::Current.EmailAddress
MyOutput( "got this email from ADFS: $emailAddress" )
}
#if the author's system || a testsystem ;-)
if ( $debug -or
( ("MYTHOSLOTUS" -eq $localMachineHostname) -or
("MYTHOS" -eq $localMachineHostname) -or
("DESKTOP-3D2E2U1" -eq $localMachineHostname) ) ) {
# Using the $PSVersionTable.PSVersion.Major
# directly doesn't work correctly. Why?
$pmPlus = $PSVersionTable.PSVersion.Major
$pmPlusRev = $PSVersionTable.PSVersion.MajorRevision
$pmNeg = $PSVersionTable.PSVersion.Minor
$pmNegRev = $PSVersionTable.PSVersion.MinorRevision
$thisVersion = "${pmPlus}.${pmPlusRev}.${pmNeg}.${pmNegRev}"
$zod = $debug
$debug = $true
MyOutput "PowershellVersion: ${thisVersion}"
MyOutput "arg groupID: $groupID"
MyOutput "arg groupToken: $groupToken"
MyOutput "arg userEmail: $userEmail"
MyOutput "arg password: $password"
$debug = $zod
}
MyOutput "localMachineHostname: $localMachineHostname`r`n"
<# Validate assumptions #>
#Install path exists?
$ret = test-path $BACKBLAZE_INSTALL_DIR
MyOutput "BACKBLAZE_INSTALL_DIR [$BACKBLAZE_INSTALL_DIR] existence returned as ret [$ret]"
if( -not $ret ) {
$parent = Split-Path -parent $BACKBLAZE_INSTALL_DIR
Get-ChildItem $parent
MyThrow "Panic: $BACKBLAZE_INSTALL_DIR does not exist? Check sharing of network directory!"
}
$BBFUPATH = join-path $BACKBLAZE_INSTALL_DIR $BACKBLAZE_INSTALLER
#Backblaze installer is present?
if ( -not ( test-path -path "$BBFUPATH" ) ) {
Get-ChildItem $BACKBLAZE_INSTALL_DIR
MyThrow "Panic: BBUPATH [$BBUPATH] does not exist? Where is the installer?"
}
MyOutput "Full path of installer [ $BBFUPATH ] existence returned as ret [$ret]"
<# Validate parameters, throw error.#>
<# **Do not mark params as mandatory** (this may cause a user prompt) #>
# doesn't matter if they are passed in or hardcoded
if( -not $groupID ) {
ScriptUsage
MyThrow "Error: value for -groupID missing"
}
if( -not $groupToken ) {
ScriptUsage
MyThrow "Error: value for -groupToken missing"
}
if ( -not $userEmail -and -not $csvMapfile ) {
ScriptUsage
MyThrow "Error: missing a value for -userEmail [$userEmail], -csvMapfile [$csvMapfile], or -sso [$sso] (should have exactly one of these)"
}
if( (-not $userEmail) -and -not ([System.IO.File]::Exists($csvMapfile)) ) {
scriptUsage
MyThrow "File $csvMapfile does not exist or cannot be accessed"
}
if ( -not $userEmail ) {
$dbFile = import-csv $csvMapfile
$dbHeaders = $dbFile[0].psobject.Properties.name
#if ( -not $dbHeaders -contain $emailHeader ) { #works in PSVersion 6, fails in PSVersion 5
if ( isMissing $dbHeaders $hostHeader ) {
write-host "dbHeaders are: "
write-host $dbheaders
write-host $dbHeaders = $dbFile[0].psobject.Properties.name
write-host "csvfile contents are"
write-host $dbfile
MyThrow "Panic: $csvMapfile does not contain the expected host machine header [$hostHeader]"
}
#if ( -not $dbHeaders -contain $emailHeader ) { #works in PSVersion 6, fails in PSVersion 5
if ( isMissing $dbHeaders $emailHeader ) {
write-host "dbHeaders are: "
write-host $dbheaders
write-host $dbFile[0].psobject.Properties.name
write-host "csvfile contents are"
write-host $dbfile
MyThrow "Panic: $csvMapfile does not contain the expected user email header [$hostEmail]"
}
if ($dbFile.Count -le 0 ) {
MyThrow "File $csvMapfile did not import correctly (no members found)"
}
$ix=0;
# is there better way to lookup values other than O(n) search?
# -- answer appears to be 'no'
foreach ($row in $dbFile) {
$ix = $ix+1
# don't log this to the ApplicationLog in non-debug mode
if ($debug) {
$thisHost = $row.$hostHeader
MyOutput "processing row $ix looking for ${hostHeader}::${localMachineHostname} | found host $thisHost"
}
if ($row.$hostHeader -eq $localMachineHostname) {
$userEmail = $row.$emailHeader.trim()
MyOutput "found [$emailHeader]$userEmail in row $ix -- array starts at 1"
break
}
}
# possibility: we did not match a username for this host in the loop above.
# The host might not be there, email might be invalid, test for this when
# testing $userMail validity
}
$userEmail = $userEmail.trim()
if (-not($userEmail)) {
MyThrow "Panic: csv file $csvMapfile did not contain an email for this host [ $localMachineHostname ] and no valid emailUser parameter passed in"
}
# at this point, there is a nonempty string for email, but is it useful?
if (!(isValidEmail $userEmail )) {
MyThrow "Panic: email address [ $userEmail ] is invalid"
}
# find a place to copy the installation directory
$tempDir = [system.io.path]::gettemppath()
MyOutput " tempDir: [$tempDir]"
# get random names until one of them is available
do {
$tempName = [system.io.path]::getrandomfilename()
$tempDirName = join-path -path $tempDir -childpath $tempName
MyOutput "tempDirName: [ $tempDirName ]"
} while ( test-path $tempDirName )
# NEW-ITEM won't create intervening directories
# but System.IO.Directory::CreateDirectory(<dirname>) will
# new-item -itemtype directory -path $tempDirName
$ret = [system.io.directory]::createdirectory($tempDirName)
# capture return object in $ret to prevent its display
# (unless we are in debug mode, of course
DebugOutput $ret
# make certain temp dir was created
if ( !( test-path $TempDirName )) {
MyThrow "Unable to create temp directory [ $tempDirName ] - check permissions and disk space"
}
# make sure we can write to those places
checkPermissions $BACKBLAZE_INSTALL_DIR
checkPermissions $tempDirName
# created directory, remove it afterward
$flagNeedDirCleanup = $true
DebugOutput "about to copy $BACKBLAZE_INSTALL_DIR to $tempDirName"
try {
copy-item $BACKBLAZE_INSTALL_DIR -destination $tempDirName -recurse
} catch {
doCleanup
MyThrow "failed to recursively copy [$BACKBLAZE_INSTALL_DIR] to [$tempDirName] on $localMachineHostname"
#Leaves '$tempDirName' in a bad state, OK because script exits on throw
}
#put our current directory on the location stack
push-location
$final_dir = split-path -leaf $BACKBLAZE_INSTALL_DIR
$localBackblazeInstallDir = join-path -path $tempDirName -child $final_dir
MyOutput "localBackblazeInstallDir: $localBackblazeInstallDir"
CheckPermissions ${localBackblazeInstallDir}
# attempt to cd to our temporary directory
try {
set-location $localBackblazeInstallDir
} catch {
pop-location
doCleanup
MyThrow "Panic: could not change directory to [${localBackblazeInstallDir}] although it existed?"
}
$BCMD = join-path $localBackblazeInstallDir $BACKBLAZE_INSTALLER
if ( -not [System.IO.File]::Exists($BCMD) ) {
MyThrow "Panic: $BCMD doesn not exist? Did not erase directory $localBackblazeInstallDir please check permissions and disk size"
}
checkPermissions $BCMD
$installCommand = "${BCMD} -nogui -createaccount $userEmail $password $groupID $groupToken"
$thisDirectory = (Get-Item -Path ".\").FullName
MyOutput "about to execute command: [ $installCommand ] currently in directory $thisDirectory"
$ierc = Invoke-Expression $installCommand
# do not use $LastErrorCode following invoke-expression
# the installer returns 1001 on success. historical reasons.
$ret = $LASTEXITCODE
# return to our initial directory
pop-location
$thisDirectory = (Get-Item -Path ".\").FullName
MyOutput "Returned to directory ${thisDirectory}"
# For an unknown reason the system holds the
# install_backblaze.exe file LOCKED for some time, and the
# script must pause until the system unlocks the file
$sCount = 0
$tUnit = "second"
do {
start-sleep 1
$sCount = $sCount + 1
if ( $debug ) {
if (testFileLock $BCMD) {
write-host "file $BCMD has been locked for $sCount $tUnit (but should unlock shortly ...) "
} else {
write-host "`r`nfile $BCMD is unlocked after $sCount ${tUnit} `r`n"
}
$tUnit = "seconds"
}
} while ((testFileLock $BCMD) -and ($sCount -le 120) )
# do not wait longer than 120 seconds for file unlock
# cleanup & remove any downloaded files ... if the lock is open
if ( -not (testFileLock $BCMD) ) {
doCleanup
}
# did the installer return with the correct error code? If not, complain
# if there is no return code, do not bother with this at all
if ( $ret -and $ret -ne 1001 ) {
MyDebug "invoke-expression returned value [${ierc}] and rc [${ret}]"
MyThrow "Error: backblaze_install [${BCMD}] exited with error code [${ret}]"
}
@nathanverrilli
Copy link
Author

nathanverrilli commented Oct 28, 2018

Version 15: Installed workaround for install_backblaze.exe -- after an install, the system retains a lock on the temporary installation command file for a short (under several minutes) period of time.

Version 22: ! and -not behave differently between PS5 and PS6.

Version 27: Add SSO flag to support single-sign-on installation.
Add ADFS flag to get email address. Overrides all other email designation methods.

Version 28: Do not search for a CSV file in ADFS mode

Version 29: Change some text
Add missing close-parend

Version 31: Disable positional argument binding

Version 32: Some refactoring, improve debug output, correct inverted logic test for $userEmail

Version 33: $emailAddress != $userEmail

Version 34: (a) second/seconds in information strings
(b) catch case where $ret is not populated

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment