Monday, March 27, 2017

WFA Command to install snapdrive (windows) on a remote computer

I had a request from a customer to prep & check the iscsi connections on remote windows hosts.  The first part is checking if snapdrive (SDW) is installed.  While I was at it, it thought I'd quickly write a command to remotely install snapdrive.  Boy was I wrong when I though quickly.  First there was the challenge of figuring out the snapdrive silent install.  When I figured out that part I bumped wall after wall to get this working.  But... I did get it working.

The challenges


- Silent installing snapdrive
- Powershell Remote
- Elevated privileges
- Double hop

The answers

Silent install :Just a matter of RTFM.  After a couple of hour I was able to silently install snapdrive "locally".
Powershell remote : To execute PowerShell commands remotely, you need to start a PowerShell Remote session using "credentials".  Using that session, you can then run remote PowerShell commands.  I needed this to do basic stuff.  Create directories, remote logfiles, check if snapdrive is installed, stuff like that.
Elevated privileges : If you run the PowerShell remotely, you can't execute with elevated privileges.  And installing snapdrive requires the well known "run as admin" rights.  I tried and tried and tried.  There appears to be a way using GPO and delegations.  But that was going to make ik more complex.  Then I googled my may towards "PsExec".  The old sysinternals exe, bought by Microsoft, is apparently the recommended tool to remotely execute with elevated rights.  And you provide credentials.  That seem to work perfect.
Double hop : there was also the double hop issue.  If you remotely run a command, that command cannot pass on the credentials to a second hop.  And was hoping to run the snapdrive_install.exe from a UNC path.  First I thought about copying the exe file, but luckily the PsExec fixed this issue as well.
Batch files : However, PsExec also has a flaw.  It cannot run commands longer that a certain amount of characters and the snapdrive command just happens to be quite a long one.  So I decided to create batch files and then remove them.  Agree, that batch file contains the credentials of the snapdrive service account in clear text, but my command removes the files.  So I'd guess the risk is very low.
Copying the batch files : Creating the batch files remote seemed to be a challenge again using Ps remote, so I create the file locally on the WFA server and then copy them to the remote server.
But copying the files was again an issue as you cannot pass credentials to do the copy.  Now running the WFA service as a certain user might have fixed this issue, but I wanted to avoid that part.  Finally I managed to copy them mapping the c$ share to a drive letter and then copying them over.

Bottom line : it was quite a hassle and the steps required did made me write some nasty checking code and cleanup code.  But it was worth the while.  I now seems to work perfectly.  WFA still running under local system and all credentials are in the WFA credentials store.

Requirements

- PS remoting should be enabled on the remote server (that's a service)
- Credentials to run the install (having access the the remote service c$ share, and having access to UNC install path (snapdrive_install.exe)
- PsExec.  This exe has to be on the WFA server. (path is a parameter)
- Credentials to run the snapdrive service

The dar file

The code

param(

    [Parameter(mandatory=$true,helpmessage="The computer name")]
    [string]$ComputerName,

    [Parameter(mandatory=$false,helpmessage="How long to wait before we check the status again")]
    [int]$Waitstep = 5,

    [Parameter(mandatory=$false,helpmessage="How long to wait before we flag a timeout on install start")]
    [int]$WaitStartfail = 5,

    [Parameter(mandatory=$false,helpmessage="How long to wait before we flag a timeout on install finish")]
    [int]$WaitInstallfail = 120,

    [Parameter(mandatory=$true,helpmessage="The name of credentials to find in WFA, to run the install")]
    [string]$CredentialName,

    [Parameter(mandatory=$true,helpmessage="The name of credentials to find in WFA, to run snapdrive service")]
    [string]$ServiceCredentialName,

    [Parameter(mandatory=$true,helpmessage="The repository path for snapdrive (must be (UNC) path reachable from target computer)")]
    [string]$SnapdriveSourcePath,

    [Parameter(mandatory=$false,helpmessage="The software path on the server, must be a local path (default c:\soft)")]
    [string]$SoftPath = "c:\soft",

    [Parameter(mandatory=$false,helpmessage="The path to PsExec.exe on the WFA server (default c:\soft\psexec.exe).  Needed to invoke remote install !")]
    [string]$PsExec = "c:\soft\psexec.exe",

    [Parameter(mandatory=$true,helpmessage="What to do : install,uninstall or reinstall")]
    [ValidateSet("install","uninstall","reinstall")]
    [string]$Action
)

$ErrorActionPreference = "stop"

# function to run remote commands (using psremote)
function testPath($file){
    return (Invoke-Command -Session $session -ScriptBlock {param($file) Test-Path $file} -ArgumentList $file) 
}
function removeItem($file){
    Invoke-Command -Session $session -ScriptBlock {param($file) Remove-Item -path $file} -ArgumentList $file
}
function newDir($path){
    $out = Invoke-Command -Session $session -ScriptBlock {param($path) New-Item -ItemType Directory -Path $path} -ArgumentList $path
}
function newFile($path,$text,[switch]$append){
    $out = Invoke-Command -Session $session -ScriptBlock {param($path,$text,$append) $text | Out-filele -Path $path} -ArgumentList $path
}
function tailFile($file,$lines){
    return (Invoke-Command -Session $session -ScriptBlock {param($file,$lines) Get-Content -Path $file -Tail $lines} -ArgumentList $file,$lines)
}
function changeDir($dir){
    Invoke-Command -Session $session -ScriptBlock {param($dir) Set-Location $dir} -ArgumentList $dir
}

# function to get clear password
function getPlainPass($secstring){
    return [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secstring))
}

# function to (un)install snapdrive
function doSnapdrive($logfile,$cmdfile,$keyword){

    changeDir $SoftPath
    # remove logfile
    if(testPath $logfile){
        Get-WfaLogger -info -message  "Removing $keyword logfile"
        removeItem $logfile
    }

    # invoke uninstall
    if(-not (test-path $psexec)){throw "No psexec found [$psexec]"}
    $installstring = "$psexec \\$ComputerName -u `"$username`" -p `"$password`" -d -h -w `"$SoftPath`" $cmdfile"
    try{
        #Get-WfaLogger -info -message  "executing [$installstring]"
        Invoke-Expression $installstring
    }catch{
        Get-WfaLogger -info -message  "Psexec returned $($_.exception.message)"
    }

    $waited = 0
    $errorcode = -1
    $uninstallStopped = $false


    while(-not (testPath $logfile) -and $waited -lt $waitstartfail){
        sleep $waitstep
        $waited+=$waitstep
    }
    if(testPath $logfile){
        Get-WfaLogger -info -message  "$keyword started"
        $waited = 0
        while(-not $uninstallStopped -and $waited -lt $waitInstallFail){
            $tailprogress = tailFile $logfile 1
            Get-WfaLogger -info -message  "waiting..."
            if($tailprogress -match "=== Logging stopped"){
                $uninstallStopped = $true
            }
            if($uninstallStopped){
                Get-WfaLogger -info -message  "$keyword end detected"
            }
            sleep $waitstep
            $waited+=$waitstep
        }
        Get-WfaLogger -info -message  "Getting end of logfile for status check"
        $tailStatus = tailFile $logfile 7
        foreach($l in $tailStatus -split "`n"){
            if($l -match "$keyword success or error status: (\d*)\."){
                if($Matches[1] -eq "0"){
                    Get-WfaLogger -info -message  "$keyword successful !"
                    $errorcode = 0
                }else{
                    Get-WfaLogger -error -message  "$keyword failed ! Check logfile $logfile on the server"
                    $errorcode = $Matches[1]
                    Get-WfaLogger -error -message  "Errorcode = $errorcode"
                    Get-WfaLogger -error -message  $tailStatus
                    throw "$keyword failed [error = $errorcode]"
                }
            }
        }
        if($errorcode -eq -1){
            Get-WfaLogger -info -message  "$keyword timed-out or was interupted.  Check logfile $logfile on the server"
        }
    
    }else{
        throw "$keyword Failed, the $keyword never started - no logfile"
    }
}

# get credentials to run remote
$mycreds = Get-WfaCredentials $CredentialName
$username = $mycreds.username
$password = (getPlainPass $mycreds.password)

# create paths
$cmduninstall = join-path $SoftPath "uninstall_snapdrive.cmd"
$cmdinstall = join-path $SoftPath "install_snapdrive.cmd"

# create file names
$tmpinstall = New-TemporaryFile
$tmpuninstall = New-TemporaryFile
$loginstall = "SDinstall.log"
$loguninstall = "SDuninstall.log"

# get credentials for the service
$svccreds = Get-WfaCredentials $ServiceCredentialName
$serviceUser = $svccreds.username
$servicePwd = (getPlainPass $svccreds.password)


# generate cmd files.  
# note : I know the services passwords are in here.  They are never shown in the logs and the batch files are being removed in the end.
# the general issue is that the parameters are limited in length for psexec.  So a batchfile is unfortunately needed.
# running the install with just plain psremoting is very complex and requires kerberos changes and/or delegation.  
# the issue is double hop.  You can tell the remote machine to do something, but you can't force it to run in elevated privileges.
# we could probably run the wfa service as a certain admin account, but again, that's not a best practice.
# psexec resolves these issues.  You can run it with certain credentials and you can run it with elevated privileges.

"cd $SoftPath" | Out-File -Encoding ascii -FilePath $tmpuninstall -Force
"$SnapdriveSourcePath /s /x /v`"/qn SILENT_MODE=1 /Li $loguninstall`"" | Out-File -Encoding ascii -FilePath $tmpuninstall -Append

"cd $SoftPath" | Out-File -Encoding ascii -FilePath $tmpinstall -Force
"$SnapdriveSourcePath /s /v`"/qn SILENT_MODE=1 /Li $loginstall LPSM_SERIALNUMBER=\`"\`" INSTALLDIR=\`"$installdir\`" SVCUSERNAME=\`"$serviceUser\`" SVCUSERPASSWORD=\`"$servicePwd\`" SVCCONFIRMUSERPASSWORD=\`"$servicePwd\`" SDW_WEBSRV_HTTP_PORT=4098 TRANSPORT_SETTING_ENABLE=0 ADD_WINDOWS_FIREWALL=1`"" | Out-File -Encoding ascii -FilePath $tmpinstall -Append

# set remote session
$session = New-PSSession -ComputerName $ComputerName -Credential $mycreds

# is software directory present
if(testPath $SoftPath){
    Get-WfaLogger -info -message  "Softpath found"
}else{
    Get-WfaLogger -info -message  "Creating new softpath"
    newDir $SoftPath
}

# remove old cmd files
if(testPath $cmdinstall){
    Get-WfaLogger -info -message  "cmd Install File already in the soft directory"
    Get-WfaLogger -info -message  "Overwriting cmd install file"
    removeItem $cmdinstall
}
if(testPath $cmduninstall){
    Get-WfaLogger -info -message  "cmd Uninstall File already in the soft directory"
    Get-WfaLogger -info -message  "Overwriting cmd uninstall file"
    removeItem $cmduninstall
}

try{
    # find free network drive
    $usedDrives = (get-psdrive -PSProvider FileSystem) | %{$_.Name}
    $allDrives = [char[]]([int][char]'D'..[int][char]'Z')
    $freeDrives = $allDrives | ?{$_ -notin $usedDrives}
    $networkDrive = $freeDrives | select -First 1
    $softDrive = $SoftPath[0]

    # we need to copy the files.  Again a challenge.  The WFA service might run as system, having no network acces
    # so we need to mount the remote computes c$ share to copy the batch files.  for this we search a temp free drive letter.
    New-PSDrive -Name $networkDrive -PSProvider FileSystem -Root "\\$ComputerName\$softDrive$" -Credential $mycreds -Persist

    # copy the batch files
    if(-not (testPath $cmdinstall)){
        $destPath = $networkDrive + $cmdinstall.Substring(1)
        Get-WfaLogger -info -message  "Copying file [$tmpinstall] -> [$destPath]..."
        Copy-Item -Path $tmpinstall -Destination $destPath 
    }
    if(-not (testPath $cmduninstall)){
        $destPath = $networkDrive + $cmduninstall.Substring(1)
        Get-WfaLogger -info -message  "Copying file [$tmpuninstall] -> [$destPath]..."
        Copy-Item -Path $tmpuninstall -Destination $destPath
    }

    # check if snapdrive is installed
    $snapdrive = (Invoke-Command -Session $session -ScriptBlock {gwmi win32_product}) | ?{$_.Name -match "snapdrive"}

    # remove snapdrive if needed
    if($snapdrive){
        Get-WfaLogger -info -message  "Snapdrive is installed"
        if($Action -in "uninstall","reinstall"){
            Get-WfaLogger -info -message  "Action requires uninstall [$Action]"
            doSnapdrive -logfile $loguninstall -cmdfile $cmduninstall -keyword "Removal"
        }

    }

    # check if snapdrive is not installed
    $snapdrive = (Invoke-Command -Session $session -ScriptBlock {gwmi win32_product}) | ?{$_.Name -match "snapdrive"}    

    # install snapdrive if needed
    if(-not $snapdrive){
        Get-WfaLogger -info -message  "No snapdrive is installed"
        if($Action -in "install","reinstall"){
            Get-WfaLogger -info -message  "Action requires install [$Action]"
            doSnapdrive -logfile $loginstall -cmdfile $cmdinstall -keyword "Installation"
        }
    }
}catch{
    throw $_.Exception
}finally{
    try{

        # always clean up (silently, no error catching)
        Get-WfaLogger -info -message  "Removing install files"
        removeItem -file $cmduninstall -force -ea silentlycontinue
        removeItem -file $cmdinstall -force -ea silentlycontinue
        remove-item -Path $tmpinstall -Force -ea silentlycontinue
        remove-item -path $tmpuninstall -Force -ea silentlycontinue
        Get-WfaLogger -info -message  "Unmount network drive"
        Remove-PSDrive $networkDrive -ea silentlycontinue
        Get-WfaLogger -info -message  "Clean sessions"
        $out = Disconnect-PSSession $session -ea silentlycontinue
        $out = Remove-PSSession $session -ea silentlycontinue

    }catch{
    }
}

No comments :

Post a Comment