Trouble copying files

I am working on a script to perform the install of PowerShell 7 remotely on machines. I am trying to copy the file from the network share to the C:\Windows\Temp directory to ensure that the application runs but am getting the below error.

Copy-Item: Access is denied
Copy-Item: Cannot find path '\\Non-Prod Server\Software\PowerShell\7.0x64\PowerShell-7.2.6-win-x64.msi' because it does not exist.

I have checked the ACL’s on the network shares and Administrators are able to access the folders in question. All the accounts I have tried running the script with are all in the local Administrators group. The servers that I am using for testing are able to read and execute from the shares also. Any help with this issue would be appreciated.

if ($Prod){
    $Environment = "Production"
    $MSIFile = "\\Prod Server\Software\PowerShell\7.0x64\PowerShell-7.2.6-win-x64.msi"
}
if ($NonProd){
    $Environment = "NonProduction"
    $MSIFile = "\\Non-Prod Server\Software\PowerShell\7.0x64\PowerShell-7.2.6-win-x64.msi"
}

function Get-TimeStamp {
    return "[{0:MM/dd/yy} {0:HH:mm:ss}]" -f (Get-Date)
}

$LogFolder = "E:\Automation\Results\Installs"
$Exists = Test-Path -Path $LogFolder
if ($Exists -eq "True") {
    Write-Host "Directory Exists!"
} else {
    New-Item -Path $LogFolder -ItemType directory
}

$logdate = (Get-Date -UFormat ".%a %m-%d-%Y %H-%M.%S %p")
$logs = new-item -path $LogFolder -Name "$Environment Install PowerShell 7 Results$logdate.log"

$ListOfServers | Foreach-Object -ThrottleLimit 25 -Parallel {
    function Get-TimeStamp {
        return "[{0:MM/dd/yy} {0:HH:mm:ss}]" -f (Get-Date)
    }
    Add-Content -Path $using:Logs -Value "$(Get-Timestamp) Connecting to server $_"
    Add-Content -Path $using:Logs -Value "$(Get-Timestamp) Installing PowerShell 7 on server $_"
    Invoke-Command -ComputerName $_ -ErrorAction SilentlyContinue -SessionOption (New-PSSessionOption -IncludePortInSPN) -ScriptBlock{
        Copy-Item -Path "$MSIFile" -Destination "C:\Windows\Temp"
	    msiexec.exe /I "C:\Windows\Temp\PowerShell-7.2.6-win-x64.msi" /quiet ADD_EXPLORER_CONTEXT_MENU_OPENPOWERSHELL=1 ADD_FILE_CONTEXT_MENU_RUNPOWERSHELL=1 ENABLE_PSREMOTING=1 USE_MU=1 ENABLE_MU=1 ADD_PATH=1
    }
    Invoke-Command -ComputerName $_ -ScriptBlock{
        Copy-Item -Path "$MSIFile" -Destination "C:\Windows\Temp"
	    msiexec.exe /I "C:\Windows\Temp\PowerShell-7.2.6-win-x64.msi" /quiet ADD_EXPLORER_CONTEXT_MENU_OPENPOWERSHELL=1 ADD_FILE_CONTEXT_MENU_RUNPOWERSHELL=1 ENABLE_PSREMOTING=1 USE_MU=1 ENABLE_MU=1 ADD_PATH=1
    }
    Start-Sleep 300
    $installed = Get-CimInstance Win32_Product -ComputerName $_ | Where-Object {$_.IdentifyingNumber -eq '{AAD8BFCE-9D62-498B-9606-030031DBE970}'} | Select-Object version -ExpandProperty version
    if ($Installed -ge '7.2.6.0'){
        Add-Content -Path $using:Logs -Value "$(Get-Timestamp) PowerShell 7 has been upgraded to 7.2.6.0 on server $_"
    } Else {
        Add-Content -Path $using:Logs -Value "$(Get-Timestamp) The upgrade of PowerShell 7.2.6.0 on server $_ has failed."
    }
}

You ran into the “double hop issue”. You cannot remote into one remote computer and remote again from there into another remote computer.

An easy and straight forward solution would be to transfer the needed instalaltion files to the servers in advance and use local paths in your script.

Your code raises some other questions to me

  • Why using 2 different source locations for the same source file?
  • In your foreach parallel loop you write from all or your parallel threads to the same log file, right? If that’s not a thread save operation I wouldn’t trust the results actually.
  • Why are you running the actual instalaltion command twice?
  • Using the WMI class Win32_Product to detect the successful installation is a bad idea. Here are some blog post explaining why:
    Win32_Product Is Evil. | Greg's Systems Management Blog
    Why Win32_Product is Bad News! - SDM Software

And just out of curiosity - if you have enough server to make it worth the effort to write a script to update some software why don’t you use a software deployment solution? You could even use WSUS to update PowerShell v 7.2.x!? :man_shrugging:t4:

Olaf,

Here are the answers to you questions.

  • Why using 2 different source locations for the same source file?
    We have 2 different environments, a production and non-production environment. These 2 environments are unable to communicate with each other by design so the installer is on 2 different file shares.
  • In your foreach parallel loop you write from all or your parallel threads to the same log file, right? If that’s not a thread save operation I wouldn’t trust the results actually.
    This is a thread save operation to show when each command was issued for that specific thread.
  • Why are you running the actual instalaltion command twice?
    We have had issues with remoting and Microsoft recommended we go this route as the SSRS servers require the SPN to be included in the connection string.
  • Using the WMI class Win32_Product to detect the successful installation is a bad idea.
    I am only using this for a verification using the actual GUID of the PowerShell 7 installation. I have the 5 minute pause built in to ensure that there is plenty of time to make sure that the installation is finished before it tries to verify that the install happened. If there is a better way to do it, I am not sure what it is.
  • And just out of curiosity - if you have enough server to make it worth the effort to write a script to update some software why don’t you use a software deployment solution?
    Once we have all the systems that have already have this installed up to version 7.2.6, we will be adding PowerShell 7 to our SCCM updates for them to be automated in the future.

I’m still sceptic if the standard cmdelts like Add-Content produce thread save actions. But since it is just for logging it might not be crucial anyway. :man_shrugging:t4:

Did you read that using the class Win32_Product even just with a Get cmdlet can change the state of your system?

A much easier and way faster option would be to query the registry uninstall key.

I don’t understand why you want to wait until that state but it seems to be reasonable for you. :wink:
For me using SCCM would solve most of your challenges at once and would be the most reliable way to go.

And I’m curious again: What solution did you choose for your double hop problem? :thinking: :wink:

To get around the double hop issue I moved the copy process outside of the invoke-command and am using UNC paths that are actually working at this point.

Ah … I see … cool.

Thanks for sharing. :+1:t4:

Here is the code that is now working.

$LogFolder = "E:\Automation\Results\Installs"
$Exists = Test-Path -Path $LogFolder
if ($Exists -eq "True") {
    Write-Host "Directory Exists!"
} else {
    New-Item -Path $LogFolder -ItemType directory
}

$logdate = (Get-Date -UFormat ".%a %m-%d-%Y %H-%M.%S %p")
$logs = new-item -path $LogFolder -Name "$Environment Install PowerShell 7 Results$logdate.log"

$ListOfServers | Foreach-Object -ThrottleLimit 25 -Parallel {
    function Get-TimeStamp {
        return "[{0:MM/dd/yy} {0:HH:mm:ss}]" -f (Get-Date)
    }
    $MSIFile = $using:MSIFile
    Add-Content -Path $using:Logs -Value "$(Get-Timestamp) Connecting to server $_"
    Add-Content -Path $using:Logs -Value "$(Get-Timestamp) Copying installation file to server $_"
    Copy-Item -Path "$MSIFile" -Destination "\\$_\C$\Windows\Temp"
    Add-Content -Path $using:Logs -Value "$(Get-Timestamp) Installing PowerShell 7 on server $_"
    Invoke-Command -ComputerName $_ -ErrorAction SilentlyContinue -SessionOption (New-PSSessionOption -IncludePortInSPN) -ScriptBlock{
	    start-process msiexec.exe -ArgumentList "/I C:\Windows\Temp\PowerShell-7.2.6-win-x64.msi /quiet ADD_EXPLORER_CONTEXT_MENU_OPENPOWERSHELL=1 ADD_FILE_CONTEXT_MENU_RUNPOWERSHELL=1 ENABLE_PSREMOTING=1 USE_MU=1 ENABLE_MU=1 ADD_PATH=1" -wait
    }
    Invoke-Command -ComputerName $_ -ScriptBlock{
	    start-process msiexec.exe -ArgumentList "/I C:\Windows\Temp\PowerShell-7.2.6-win-x64.msi /quiet ADD_EXPLORER_CONTEXT_MENU_OPENPOWERSHELL=1 ADD_FILE_CONTEXT_MENU_RUNPOWERSHELL=1 ENABLE_PSREMOTING=1 USE_MU=1 ENABLE_MU=1 ADD_PATH=1" -wait
    }
    Start-Sleep 300
    $installed = Get-CimInstance Win32_Product -ComputerName $_ | Where-Object {$_.IdentifyingNumber -eq '{AAD8BFCE-9D62-498B-9606-030031DBE970}'} | Select-Object version -ExpandProperty version
    if ($Installed -ge '7.2.6.0'){
        Add-Content -Path $using:Logs -Value "$(Get-Timestamp) PowerShell 7 has been upgraded to 7.2.6.0 on server $_"
    } Else {
        Add-Content -Path $using:Logs -Value "$(Get-Timestamp) The upgrade of PowerShell 7.2.6.0 on server $_ has failed."
    }
}