Mass Install of Application

by EndUser2013 at 2013-04-03 17:37:25

I am trying to automate more of my server deployments for SQL and I have a script that works serially ‘one server at a time starting with #1’ but the second I tell it to do more than a single one at once it gets stuck on the first ‘create directory command on that last server… do not understand why it does not execute the entire command before it goes to make another job for the next system… any help would be appreciated as I feel I am very close but just ‘not there yet’

Here is just the piece that just scans and if it does not detect the directory it makes it the install folder before it executes the copy-item…


$testsqlpath = "\$Hostlist\d$\installs\sqlcopydone.txt";
#write-host $testsqlpath
if ((Test-Path $testsqlpath) -eq $true)
{
Write-Debug "File Copy appears to have been done already... skipping."
}
else
{
Write-Debug "Starting SQL Install Files Copy..."
if ((Test-Path (’\$Hostlist\d$\Installs’)) -eq $false)

{
Write-Debug "Destination Path for File Copy does not exist... Creating..."
$Hostlist | %{
Invoke-Command -AsJob -JobName SQLDEPLOY -ComputerName $_ -ScriptBlock{
$setupFolder = "\$Hostlist\d$\Installs\SQLServer2008R2_Datacenter_RTM"
Write-Debug "Creating SQL Install Folder"
New-Item -Path ‘$setupFolder’ -type directory -Force
Write-Debug "Folder creation complete"
Write-Debug "Destination Path Created!"
}
$setupFolder = "\$Hostlist\d$\Installs\SQLServer2008R2_Datacenter_RTM"
$SetupRoot = "\$Hostlist\d$\Installs"
Copy-Item $SQLF -Destination $SetupFolder -Recurse
Copy-Item ‘2008R2-SQLDeployV1-uninstall.ps1’ -Destination $SetupRoot
Write-Debug "File Copy Done Successfully" | Out-File "$setupfolder\sqlcopydone.txt";
Write-Debug "File Copy Complete!"
} | out-null
}
# Write-Debug "Destination Path Created!"
}
}


Right now this just executes one at a time because the install buts take awhile to copy to the remote-host, what I really want it to do is scan the who code and execute it on each host I tell it to … why will it not skip over to the next job?
by DonJ at 2013-04-03 23:15:12
As a note, you can use the CODE button to format you code so it’s easier to read.

So, one problem is that, when you remote to a computer by using Invoke-Command, your credential only delegates (by default) across that one hop. The remote session won’t be able to execute any command that goes to a non-local resource, such as your UNC path. Look into enabling CredSSP for that - see "Secrets of PowerShell Remoting" (free) at PowerShellBooks.com.

Does $Hostlist contain more than one name? If so, what you’re trying won’t work.

When you execute an external command (like an installer) on a remote machine, it may return a result code before completing, which will end the remote session. Consider starting a session first (New-PSSession) and then invoking the command against the session (-Session). That’ll leave the session running.
by EndUser2013 at 2013-04-04 12:35:01
Answers to Questions:
Invoke-Command is passing my cred to a single hop only… Local to Remote system that system is only talking back to my maintenance box… so my issue I dont feel is tied to CredSP … if I execute the above code and set my batch to only be 1 server per run it works I just have to wait until it goes through the copy pieces before it connects to the next server…

$hostlist = contains 20

External commands are actually running as intended just in a serial fashion instead of executing in groups of 5 at a time…

I removed the SQL install stuff from this but I figure if I cannot get my file copies to work as I want I will get no where as my install piece will not have the files copied to it when it wants to launch and crash out… this copy-item piece was my workaround as network policy constraints prevent me from just doing straight invoke-commands -computername $hostlist -filepath … -arg… I exhausted that option before i went this route…

Here is the full code I put together…

Param
(
[Parameter(Mandatory=$true)]
[string] $HostFile,
[Parameter(Mandatory=$true)]
[string] $sapasswrd,
[Parameter(Mandatory=$true)]
[string] $svcpasswrd,
[Parameter(Mandatory=$false)]
[switch] $DnsLookup,
[Parameter(Mandatory=$false)]
[int] $BatchSize=10,
[Parameter(Mandatory=$false)]
[int] $CheckAfter=0,
[Parameter(Mandatory=$false)]
[string] $OutFile,
[Parameter(Mandatory=$false)]
[switch] $OutObject,
[Parameter(Mandatory=$false)]
[string] $Show="ALL",
[Parameter(Mandatory=$false)]
[switch] $Help,
[Parameter(Mandatory=$false)]
[switch] $History,
[Parameter(Mandatory=$false)]
[string] $SQLF="d:\SQLDeploy\SQLServer2008R2_Datacenter_RTM"
)
$DebugPreference = "Continue"
$StatusCodes = @{
0 = "Install Success";
1 = "Install Failed";
99999 = "CALL FAILED"}

###
#Help with file syntax
###
if ($help -or $History -or (!$HostFile)){
write-host $HelpText
if ($History){write-host $HistoryText}
exit
}
###
#IP Address to String
###
function isIPAddress(){
param ($object)
($object -as [System.Net.IPAddress]).IPAddressToString -eq $object -and $object -ne $null
}
######################################################
## MAIN
######################################################
$elapsedTime = [system.diagnostics.stopwatch]::StartNew()
$result = @()
$itemCount = 0
$callFailure = test-connection -computername localhost -count 1
$callFailure.statuscode = 99999
$callFailure.address = "notset"
###
# Clean-up Old Jobs
###
if (get-job|? {$.name -like "SQLInstall*"}){
write-host "ERROR: There are pending background jobs in this session:" -back red -fore white
get-job |? {$
.name -like "SQLInstall*"} | out-host
write-host "REQUIRED ACTION: Remove the jobs and restart this script" -back black -fore yellow
$yn = read-host "Automatically remove jobs now?"
if ($yn -eq "y"){
get-job|? {$.name -like "SQLInstall*"}|% {remove-job $}
write-host "jobs have been removed; please restart the script" -back black -fore green
}
exit
}
###
# Test if Serverlist is Given If Not Exit
###
if (!(test-path $HostFile)){
write-host "ERROR: "$HostFile" is not a valid file" -back black -fore red
write-host "REQUIRED ACTION: Re-run this script using a valid filename" -back red -fore white
exit
}
###
# Test if SA Password was Given If Not Exit
###
if ((test-path $sapasswrd)){
write-host "ERROR: "$sapasswrd" was not entered" -back black -fore red
write-host "REQUIRED ACTION: Re-run this script and provide the necessary password" -back red -fore white
exit
}
###
# Test if Service Password was Given If Not Exit
###
if ((test-path $svcpasswrd)){
write-host "ERROR: "$svcpasswrd" was not entered" -back black -fore red
write-host "REQUIRED ACTION: Re-run this script and provide the necessary password" -back red -fore white
exit
}
###
# Bring up Static UI
###
$offset = 0
$itemCount = gc $HostFile |sort |get-unique | ? {((!$.startswith("#")) -and ($ -ne ""))} | measure-object -line |% {$.lines}
write-host " SQLDeploy started at $(get-date) ".padright(60) -back darkgreen -fore white
write-host " -HostFile : $HostFile" -back black -fore green
write-host " (contains $itemCount unique entries)" -back black -fore green
if ($DnsLookup){$temp="Selected"}else{$temp="Not Selected"}
write-host " -DnsLookup : $temp" -back black -fore green
write-host " -BatchSize : $BatchSize" -back black -fore green
if ($CheckAfter){$temp="Selected"}else{$temp="Not Selected"}
write-host " -CheckAfter : $temp" -back black -fore green
write-host " -Show : $Show" -back black -fore green
if ($OutFile){$temp=$OutFile}else{$temp="Not Selected"}
write-host " -OutFile : $temp" -back black -fore green
if ($OutObject){$temp="Selected"}else{$temp="Not Selected"}
write-host " -OutObject : $temp" -back black -fore green
write-host ""
$activeJobCount = 0
$totalJobCount = 0
write-host "Submitting background Install jobs..." -back black -fore yellow
for ($offset=0; $offset -lt $itemCount;$offset += $batchSize){
$activeJobCount += 1; $totalJobCount += 1; $HostList = @()
$HostList += gc $HostFile |sort |get-unique |? {((!$
.startswith("#")) -and ($_ -ne ""))} | select -skip $offset -first $batchsize
###Job Execution
# Job Commands to execute at remote host
###
#
# Copy Install Bits Locally

$testsqlpath = "\$Hostlist\d$\installs\sqlcopydone.txt";
#write-host $testsqlpath
if ((Test-Path $testsqlpath) -eq $true)
{
Write-Debug "File Copy appears to have been done already... skipInstall."
}
else
{
Write-Debug "Starting SQL Install Files Copy..."
if ((Test-Path (‘\$Hostlist\d$\Installs’)) -eq $false)

{
Write-Debug "Destination Path for File Copy does not exist... Creating..."
$Hostlist | %{
$j = Invoke-Command -AsJob -ComputerName $_ -ScriptBlock {
$setupFolder = "\$\d$\Installs\SQLServer2008R2_Datacenter_RTM"
Write-Debug "Creating SQL Install Folder"
New-Item -Path ‘$setupFolder’ -type directory -Force
Write-Debug "Folder creation complete"
Write-Debug "Destination Path Created!"
}
$setupFolder = "\$
\d$\Installs\SQLServer2008R2_Datacenter_RTM"
$SetupRoot = "\$\d$\Installs"
Copy-Item $SQLF -Destination $SetupFolder -Recurse
Copy-Item ‘2008R2-SQLDeployV1-uninstall.ps1’ -Destination $SetupRoot
Write-Debug "File Copy Done Successfully" | Out-File "$setupRoot\sqlcopydone.txt"
Write-Debug "File Copy Complete!"
}
write-host "+" -back black -fore cyan -nonewline
if (($checkAfter) -and ($activeJobCount -ge $checkAfter)){
write-host "n$totaljobCount jobs submitted; checking for completed jobs&#46;&#46;&#46;&quot; -back black -fore yellow<br> foreach ($j in get-job | ? {$_&#46;name -like &quot;SQLDEPLOY*&quot; -and $_&#46;state -eq &quot;completed&quot;}){<br> $result += receive-job $j<br> remove-job $j<br> $activeJobcount -= 1<br> write-host &quot;-&quot; -back black -fore cyan -nonewline<br> }<br> }<br> } <br># Write-Debug &quot;Destination Path Created!&quot;<br> }<br>}<br>write-host &quot;n$totaljobCount jobs submitted, checking for completed jobs..." -back black -fore yellow
$recCnt = 0
while (get-job |? {$
.name -like "SQLDEPLOY*"}){
foreach ($j in get-job | ? {$.name -like "SQLDEPLOY*"}){
#write-host "Job: $($j.name) State: $($j.state) " -back black -fore cyan -nonewline
$temp = @()
if ($j.state -eq "completed"){
$temp = @()
$temp += receive-job $j
$result += $temp
#write-host " (read $($temp.count) Lines - result count : $($result.count))" -back black -fore green
remove-job $j
$ActiveJobCount -= 1
write-host "-" -back black -fore cyan -nonewline
}
elseif ($j.state -eq "failed"){
$temp = $j.name.split(":")
if ($temp[1] -eq "R"){
#
# This is a single-entry recovery Job failure
# extract hostname from the JobName and update our callFailure record
# force-feed callFailure record into the results array
# delete the job
#
write-host " "
write-host "Call Failure on Host: $($temp[2]) " -back red -fore white
remove-job $j
$callFailure.address = $temp[2]
$result += $callFailure
write-host "resuming check for completed jobs..." -back black -fore yellow
}
else{
#
# The original background job failed, so need to resubmit each hostname from that job
# to determine which host failed and gather accurate Install results for the others
#
# Recovery Job Name Format: SQLDeploy:R:
# where "R" indicates a Recovery job and is the hostname or IP Address to be Installed
# Recovery jobs will only have ONE hostname specified
#
write-host "nFailure detected in job&#58; $($j&#46;name); recovering now&#46;&#46;&#46;&quot; -back black -fore red<br> remove-job $j<br> $ActiveJobCount -= 1<br> $HostList = gc $HostFile |sort|get-unique|? {((!$_&#46;startswith(&quot;#&quot;)) -and ($_ -ne &quot;&quot;))} | select -skip $($temp&#91;2&#93;-1) -first $temp&#91;3&#93;<br> foreach ($x in $HostList){<br> $j = test-connection -computername $x -count 4 -throttlelimit 32 -erroraction silentlycontinue -asjob<br> $j&#46;name = &quot;SQLDEPLOY&#58;R&#58;$x&quot;<br> write-host &quot;&#46;&quot; -back black -fore cyan -nonewline<br> }<br> write-host &quot;nresuming check for completed jobs..." -back black -fore yellow
}
}
}
if ($result.count -lt $itemCount){
sleep 5
}
}
write-host " "
write-host " SQLDEPLOY finished Installing at $(get-date) ".padright(60) -back darkgreen -fore white
write-host (" Hosts Installed : {0}" -f $($result.count)) -back black -fore green
write-host (" Elapsed Time : {0}" -f $($ElapsedTime.Elapsed.ToString())) -back black -fore green
$result | add-member -membertype NoteProperty -Name StatusDescr -value "" -Force
foreach ($r in $result){
if ($r.statusCode -eq $null){
$r.statusCode = 99999
}
$r.StatusDescr = $statusCodes.item([int]$r.statusCode)
}
if ($DnsLookup){
write-host "DNS Hostname lookup started..." -back black -fore yellow
write-host "BE PATIENT - this could take a while!" -back black -fore yellow
$result | add-member -membertype noteproperty -name DnsHostName -value "" -Force
$result | add-member -membertype noteproperty -name DnsIpAddress -value "" -Force
$DnsLookupFailures = 0
foreach ($r in $result){
try{
# gethostentry only thows errors on invalid IP Addresses
# if returned hostname is IpAddress, then the lookup failed
$x = [system.net.dns]::gethostentry($r.address)
if (isIPAddress $x.hostname){
throw "no such host is known"
}
else{
$r.DnsHostName = $x.hostname
$r.DnsIpAddress = $x.addresslist[0].ipaddresstostring
}
write-host "." -back black -fore cyan -nonewline
}
catch{
write-host "." -back red -fore black -nonewline
#write-host "nDNS Lookup Failed&#58; $($r&#46;address)&quot; -back red -fore black -nonewline<br> $DnsLookupFailures += 1<br> }<br> }<br> write-host &quot;&quot;<br> write-host &quot; SQLDEPLOY finished DNS Lookup at $(get-date) &quot;&#46;padright(60) -back darkgreen -fore white<br> write-host (&quot; Lookup Failures &#58; {0}&quot; -f $DnsLookupFailures) -back black -fore green<br> write-host (&quot; Lookup Success &#58; {0}&quot; -f ($itemCount - $DnsLookupFailures)) -back black -fore green<br> write-host (&quot; Elapsed Time &#58; {0}&quot; -f $($ElapsedTime&#46;Elapsed&#46;ToString())) -back black -fore green<br> }<br>write-host &quot;nSUMMARY:" -back black -fore cyan
$result | group statuscode | sort count | <br> ft -auto
@{Label="Status"; Alignment="left"; Expression={"$($
.name)"}}, <br> @{Label=&quot;Description&quot;; Alignment=&quot;left&quot;; Expression={&quot;{0}&quot; -f $statuscodes&#46;item(&#91;int&#93;$_&#46;name)}},
count `
| out-host
switch ($show){
SUCCESS{
write-host "extracting Successful Install results..." -back black -fore yellow
$result = $result|? {$.statuscode -eq 0}
write-host "done" -back black -fore yellow
write-host (" Elapsed Time : {0}" -f $($ElapsedTime.Elapsed.ToString())) -back black -fore green
}
FAIL{
write-host "extracting UnSuccessful Install results..." -back black -fore yellow
$result = $result|? {$
.statuscode -ne 0}
write-host "done" -back black -fore yellow
write-host (" Elapsed Time : {0}" -f $($ElapsedTime.Elapsed.ToString())) -back black -fore green
}
}
if ($OutFile){
write-host "writing results to $outfile... " -back black -fore yellow
if ($DnsLookup){
$result | select address,statuscode,statusdescr,dnshostname,dnsipaddress | export-csv -notypeinfo -path $OutFile
}
else{
$result | select address,statuscode,statusdescr | export-csv -notypeinfo -path $OutFile
}
write-host "done" -back black -fore yellow
}
if ($OutObject){
$result
}
write-host " SQLDEPLOY completed all requested operations at $(get-date) ".padright(60) -back darkgreen -fore white
write-host (" Elapsed Time : {0}" -f $($ElapsedTime.Elapsed.ToString())) -back black -fore green
by DonJ at 2013-04-04 23:01:15
Couple notes - first, you’re working really hard to do the help text. Read about_comment_based_help in PowerShell. It’ll do that help screen for you.

Second, consider adding [CmdletBinding()] right before your Param() block, and look into using Write-Verbose and Write-Warning and Write-Error instead of all those Write-Host commands. You’ve got some fundamental structural issues that you could resolve to make this more maintainable over the long haul.

Sorry. Can’t help myself.

Anyway:

So, if I’m reading this correctly, you list computer names in $HostFile and then read this into $HostList.

I see where you’re enumerating that list:

$Hostlist | %{

I’d suggest you rewrite that as a script construct. Using ForEach-Object as you’re doing makes this a lot harder, syntactically.

ForEach ($Host in $HostList) { # in here, use $host for one host, not $_ }

You’re using Invoke-Command a bit awkwardly, I think. It’s designed to accept multiple computer names, contact them individually, queue them if it’s a large list, and so on. I’d really suggest using it that way. Also, this:


$j = Invoke-Command -AsJob -ComputerName $_ -ScriptBlock {
$setupFolder = "\$\d$\Installs\SQLServer2008R2_Datacenter_RTM"
Write-Debug "Creating SQL Install Folder"
New-Item -Path ‘$setupFolder’ -type directory -Force
Write-Debug "Folder creation complete"
Write-Debug "Destination Path Created!"
}


I worry a bit because variables like $
won’t have any meaning on the remote machine, which is where this is being executed. There’s a specific technique for passing local variables to the remote machine:


$j = Invoke-Command -AsJob -ComputerName $_ -ScriptBlock -Arg $,$setupfolder {
param($comp,$setup)
$setupFolder = "\$comp\d$\Installs\SQLServer2008R2_Datacenter_RTM"
Write-Debug "Creating SQL Install Folder"
New-Item -Path "$setup" -type directory -Force
Write-Debug "Folder creation complete"
Write-Debug "Destination Path Created!"
}


Also noticed that I changed to double quotes in the New-Item command. Single quotes won’t replace variables - you’d have been creating a folder literally named $setupFolder.

The corrections I made assume you’re running Invoke-Command once per computer, since that’s the structure you have in place. Visually scanning your script, that’s the first thing I’d consider fixing. If you’ve been testing that Invoke-Command, I wonder if you’ve been doing so using variables - like your script uses - or hardcoded "test" values. The latter would work; the former wouldn’t - it’s important to test exactly as you’re doing in-script to catch stuff like that.
by EndUser2013 at 2013-04-05 11:14:51
Well alright… I managed to get this going here is the code to get the designated folders to copy over to the servers in the list…

# Loop through the server list
Get-Content "D:\SQLDeploy\serverlist.txt"| %{

# Define what each job does

$ScriptBlock = {
param($Server)
# Write-Output $Server;

$testsqlpath = "\$Server\d$\installs\sqlcopydone.txt";

if ((Test-Path "$testsqlpath") -eq $true)
{
Write-Host "File Copy appears to have been done already... skipping Copy on $Server"
}
else
{

if ((Test-Path "$testsqlpath") -eq $False)
{
$SQLF="\‘servername with files on it’\SQLDeploy\SQLServer2008R2_Datacenter_RTM"
$setupFolder = "\$Server\d$\Installs\SQLServer2008R2_Datacenter_RTM"
New-Item -Path $setupFolder -type directory -Force
$SetupRoot = "\$Server\d$\Installs"
Out-File "$setupRoot\sqlcopydone.txt"
write-host "+" -back black -fore cyan -nonewline
}}

}

# Execute the jobs in parallel

Start-Job -Name ‘SqlDeploy’ $ScriptBlock -ArgumentList $
| Out-Null
}

# Wait for it all to complete

While (Get-Job -State "Running")
{
Start-Sleep 1
}

# Getting the information back from the jobs

Get-Job | Receive-Job|Write-Host
Remove-Job *


Right now I am resorting to hacking up my script so it runs in Sections… I have some registry things that need to be changed and also pagefile location and size as well… . I appreciate the tips you pushed to me with the invoke-command and I am hopeful I can get the remaining registry , pagefile, installer launch to work properly…

If I hardcode pieces I hit my original problem of the script working in a serial fashion which I want this to run in batches of 5 else I might as well goto each machine and just run it manually which is what I want to avoid…

…But…

I think the expressed points have given me some good pointers I will keep this thread going as I progress… the end product will be posted in here as well as I believe in ‘sharing what I make’ , I am a novice in powershell so I am sure there will be plenty of pieces that are bloated which will be cleaned up later once I get the whole thing running solo.

Stay tuned…