Diagnose a share that prompts, when the same UNC path works
Shopfloor PCs intermittently lose S: and ask for credentials while navigating the identical UNC path in Explorer works fine. Four different causes produce that same appearance - a persistent reconnect that raced the network, a stored credential whose target string does not cover the name form in use, two identities contending for one server, or a password that rotated - and each needs a different fix. From the desk they are indistinguishable. Test-ShareAccess.ps1 collects what separates them: reachability of both name forms, the mappings and the account each remembers, the exact credential target strings, Kerberos tickets, live SMB sessions and the identities holding them, and the SMBClient/Connectivity log. Then it names the causes the evidence supports rather than leaving a wall of output to interpret. Read-only by construction: it maps nothing, deletes nothing, stores no credential, and prints usernames and a password-present flag but never a password, because its log lands where techs read it. Safe on a bay while somebody is working. It reports which identity it is running as, because mappings and SMB sessions are per logon session: run as the user to explain S:, as SYSTEM to explain W: and the backup scripts. A drive missing in one is genuinely absent in the other, and mistaking that for a fault is easy. Also derives the marker backup's share root from the script's own location rather than a hardcoded UNC. GE-Enforce mounts the share as W: with SFLD credentials, and reaching the same server by a different name than the mount used costs a second authentication - or fails outright with 1219 - for reasons the error text never mentions. The script's own path is the one handle guaranteed to name the connection that already exists.
This commit is contained in:
249
playbook/shopfloor-setup/common/scripts/Test-ShareAccess.ps1
Normal file
249
playbook/shopfloor-setup/common/scripts/Test-ShareAccess.ps1
Normal file
@@ -0,0 +1,249 @@
|
||||
# Test-ShareAccess.ps1 - why can this PC not reach the share?
|
||||
#
|
||||
# READ-ONLY. It maps nothing, deletes nothing, stores no credential and prints
|
||||
# no password. Safe to run on a production bay while somebody is working.
|
||||
#
|
||||
# THE PROBLEM IT EXISTS FOR
|
||||
#
|
||||
# Shopfloor PCs intermittently lose S: - it prompts for credentials - while
|
||||
# navigating the identical UNC path in Explorer works fine. That combination has
|
||||
# several possible causes which look identical from the desk, and the fix
|
||||
# differs for each:
|
||||
#
|
||||
# 1. Persistent reconnect raced the network. The mapping is restored early in
|
||||
# logon, before wifi/802.1X is up, so it comes back dead. A manual UNC
|
||||
# visit minutes later works because by then the server is reachable.
|
||||
# 2. The stored credential does not cover the name form in use. Credential
|
||||
# Manager matches by STRING: an entry for "server" does not serve
|
||||
# "server.domain.com". One path finds the account, the other does not.
|
||||
# 3. Two identities for one server. Windows allows ONE SMB session per server
|
||||
# per logon session, so if the drive wants a shared account and something
|
||||
# already opened that server as the logged-on user (or by a second name
|
||||
# form), the loser gets a credential prompt rather than a clear error.
|
||||
# 4. Genuinely stale password, after a rotation.
|
||||
#
|
||||
# This collects the evidence that separates them, and says which it looks like.
|
||||
#
|
||||
# CONTEXT MATTERS: drive mappings and SMB sessions are PER LOGON SESSION. Run it
|
||||
# as the logged-on user to diagnose S:. Run it as SYSTEM (psexec -s, or a
|
||||
# GE-Enforce manifest entry) to diagnose W: and the backup scripts. The two see
|
||||
# different worlds, and a mapping visible in one is genuinely absent in the
|
||||
# other - that is not a bug.
|
||||
#
|
||||
# Log: C:\Logs\Shopfloor\share-access-<yyyyMMdd-HHmmss>.log
|
||||
|
||||
param(
|
||||
# Server to test. Both the short and fully-qualified forms are exercised,
|
||||
# because a mismatch between them is one of the causes.
|
||||
[string]$Server = 'tsgwp00525.wjs.geaerospace.net',
|
||||
# Share to probe on that server.
|
||||
[string]$Share = 'shared',
|
||||
# Drive letters to report on. S: is the user's shopfloor mapping, W: is the
|
||||
# one GE-Enforce makes as SYSTEM for the duration of its cycle.
|
||||
[string[]]$Drives = @('S:', 'W:')
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$logDir = 'C:\Logs\Shopfloor'
|
||||
if (-not (Test-Path $logDir)) {
|
||||
try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch { $logDir = $env:TEMP }
|
||||
}
|
||||
$logFile = Join-Path $logDir ('share-access-{0}.log' -f (Get-Date -Format 'yyyyMMdd-HHmmss'))
|
||||
|
||||
function Say {
|
||||
param([string]$Text = '')
|
||||
Write-Host $Text
|
||||
Add-Content -Path $logFile -Value $Text -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
function Section {
|
||||
param([string]$Title)
|
||||
Say ''
|
||||
Say ('=== {0} ' -f $Title).PadRight(70, '=')
|
||||
}
|
||||
|
||||
$short = ($Server -split '\.')[0]
|
||||
$forms = @($Server, $short) | Select-Object -Unique
|
||||
$findings = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
Say ('Test-ShareAccess {0}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'))
|
||||
Say ('Log: {0}' -f $logFile)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
Section 'Who is running this'
|
||||
# Everything below is scoped to this identity and this logon session. A drive
|
||||
# absent here may be perfectly healthy in another session.
|
||||
$me = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$isSystem = $me.IsSystem
|
||||
Say ('User : {0}{1}' -f $me.Name, $(if ($isSystem) { ' (SYSTEM - sees W:, NOT the user''s S:)' } else { '' }))
|
||||
Say ('Computer : {0}' -f $env:COMPUTERNAME)
|
||||
if (-not $isSystem) {
|
||||
Say 'Note : run this again as SYSTEM (psexec -s) to see what GE-Enforce sees.'
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
Section 'Name resolution and reachability'
|
||||
# A dead mapping with a healthy UNC path is often just timing: if the server is
|
||||
# unreachable at logon the persistent reconnect fails, and by the time anyone
|
||||
# looks the network is fine.
|
||||
foreach ($form in $forms) {
|
||||
$ip = ''
|
||||
try {
|
||||
$ip = ([Net.Dns]::GetHostAddresses($form) |
|
||||
Where-Object { $_.AddressFamily -eq 'InterNetwork' } |
|
||||
Select-Object -First 1).IPAddressToString
|
||||
} catch { $ip = 'DNS FAILED' }
|
||||
|
||||
$port = 'no'
|
||||
try {
|
||||
$client = New-Object Net.Sockets.TcpClient
|
||||
$async = $client.BeginConnect($form, 445, $null, $null)
|
||||
if ($async.AsyncWaitHandle.WaitOne(3000) -and $client.Connected) { $port = 'yes' }
|
||||
$client.Close()
|
||||
} catch { }
|
||||
Say ('{0,-45} -> {1,-16} SMB/445 reachable: {2}' -f $form, $ip, $port)
|
||||
if ($port -ne 'yes') {
|
||||
$findings.Add("Cannot reach $form on 445 right now. If the mapping failed at logon for the same reason, that is cause 1 - a reconnect that raced the network.")
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
Section 'Drive mappings in THIS session'
|
||||
# The 'user' column is the point: a drive remembering an account other than the
|
||||
# one running this is what makes a credential prompt possible at all.
|
||||
$netuse = & net use 2>&1 | Out-String
|
||||
Say $netuse.TrimEnd()
|
||||
foreach ($drive in $Drives) {
|
||||
$line = ($netuse -split "`r?`n" | Where-Object { $_ -match [regex]::Escape($drive) } | Select-Object -First 1)
|
||||
if (-not $line) {
|
||||
Say ('{0} not present in this session.' -f $drive)
|
||||
} elseif ($line -match 'Unavailable|Disconnected') {
|
||||
Say ('{0} present but NOT connected.' -f $drive)
|
||||
$findings.Add("$drive exists but is not connected - the classic dead persistent reconnect (cause 1).")
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
Section 'Stored credentials'
|
||||
# cmdkey matches by STRING, so the exact Target text decides whether a mapping
|
||||
# finds its account. This prints targets and usernames only - never a password,
|
||||
# and cmdkey cannot reveal one anyway.
|
||||
$cmdkey = & cmdkey /list 2>&1 | Out-String
|
||||
$targets = [regex]::Matches($cmdkey, 'Target:\s*(\S+)') | ForEach-Object { $_.Groups[1].Value }
|
||||
if ($targets) {
|
||||
Say 'Targets stored for this user:'
|
||||
foreach ($t in $targets) { Say (' {0}' -f $t) }
|
||||
} else {
|
||||
Say 'No stored credentials for this user.'
|
||||
}
|
||||
|
||||
foreach ($form in $forms) {
|
||||
$covered = @($targets | Where-Object {
|
||||
$_ -match [regex]::Escape($form) -or
|
||||
($_ -match '^\*' -and $form -like ($_ -replace '^Domain:target=', ''))
|
||||
})
|
||||
if ($covered.Count -eq 0) {
|
||||
Say ('NO stored credential matches "{0}"' -f $form)
|
||||
$findings.Add("No credential entry matches '$form'. If the mapping uses that form, it has no account to reconnect with (cause 2). Add an entry for every name form in use, or a wildcard.")
|
||||
} else {
|
||||
Say ('Credential covers "{0}": {1}' -f $form, ($covered -join ', '))
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
Section 'Kerberos tickets'
|
||||
# A cifs/ ticket for the server means this identity can reach it on its own.
|
||||
# On a shopfloor bay the logon usually CANNOT, which is why a stored account is
|
||||
# mandatory - and that is what makes two-identities-one-server possible.
|
||||
$klist = & klist 2>&1 | Out-String
|
||||
$cifs = [regex]::Matches($klist, 'cifs/[^\s@]+') | ForEach-Object { $_.Value } | Select-Object -Unique
|
||||
if ($cifs) { Say ('cifs tickets: {0}' -f ($cifs -join ', ')) } else { Say 'No cifs tickets for this identity.' }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
Section 'Live SMB sessions to this server'
|
||||
# ONE session per server per logon session. Two name forms count as two servers
|
||||
# - which is how a single share ends up wanting two identities at once.
|
||||
$connections = @()
|
||||
try {
|
||||
$connections = @(Get-SmbConnection -ErrorAction Stop |
|
||||
Where-Object { $_.ServerName -match [regex]::Escape($short) })
|
||||
} catch { }
|
||||
if ($connections.Count) {
|
||||
foreach ($c in $connections) {
|
||||
Say ('{0,-45} share {1,-14} as {2}' -f $c.ServerName, $c.ShareName, $c.UserName)
|
||||
}
|
||||
$identities = @($connections | Select-Object -ExpandProperty UserName -Unique)
|
||||
$servers = @($connections | Select-Object -ExpandProperty ServerName -Unique)
|
||||
if ($identities.Count -gt 1) {
|
||||
$findings.Add("More than one identity is connected to this server in this session ($($identities -join ', ')). That is cause 3, and whichever connected first wins - the other gets a credential prompt.")
|
||||
}
|
||||
if ($servers.Count -gt 1) {
|
||||
$findings.Add("This server is connected under more than one NAME ($($servers -join ', ')). Windows treats those as different servers, so they get separate sessions and separate credential lookups.")
|
||||
}
|
||||
} else {
|
||||
Say 'No live SMB session to this server in this logon session.'
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
Section 'Can this identity read the share right now'
|
||||
# Test-Path is deliberately not trusted on its own: a SYSTEM process denied
|
||||
# access to a UNC path gets a "not found", so a permissions problem reads as a
|
||||
# missing directory.
|
||||
foreach ($form in $forms) {
|
||||
$path = '\\{0}\{1}' -f $form, $Share
|
||||
$ok = $false
|
||||
try { $ok = Test-Path -LiteralPath $path -ErrorAction Stop } catch { }
|
||||
Say ('{0,-55} readable: {1}' -f $path, $ok)
|
||||
if (-not $ok -and $isSystem) {
|
||||
Say ' (as SYSTEM a denial reports as "not found" - do not read this as a missing path)'
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
Section 'SFLD credential provisioning (GE-Enforce)'
|
||||
# Username and presence only. The password is never printed - it is a fleet
|
||||
# credential and this log is written to a directory techs read.
|
||||
$sfldBase = 'HKLM:\SOFTWARE\GE\SFLD\Credentials'
|
||||
if (Test-Path $sfldBase) {
|
||||
foreach ($key in Get-ChildItem $sfldBase -ErrorAction SilentlyContinue) {
|
||||
$props = Get-ItemProperty -Path $key.PSPath -ErrorAction SilentlyContinue
|
||||
$names = @($props.PSObject.Properties.Name)
|
||||
$user = if ($names -contains 'Username') { $props.Username } else { '(none)' }
|
||||
$hasPassword = ($names -contains 'Password') -and $props.Password
|
||||
Say ('{0,-30} user {1,-28} password present: {2}' -f $key.PSChildName, $user, [bool]$hasPassword)
|
||||
}
|
||||
} else {
|
||||
Say 'No SFLD credentials provisioned (Azure DSC has not run, or this is not a fleet PC).'
|
||||
if ($isSystem) {
|
||||
$findings.Add('No SFLD credential in the registry, so GE-Enforce cannot mount the share at all this cycle.')
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
Section 'Recent SMB client errors'
|
||||
try {
|
||||
$events = Get-WinEvent -FilterHashtable @{
|
||||
LogName = 'Microsoft-Windows-SMBClient/Connectivity'; StartTime = (Get-Date).AddDays(-3)
|
||||
} -MaxEvents 8 -ErrorAction Stop
|
||||
foreach ($e in $events) {
|
||||
Say ('{0} id {1} {2}' -f $e.TimeCreated.ToString('MM-dd HH:mm'), $e.Id,
|
||||
(($e.Message -split "`r?`n")[0]))
|
||||
}
|
||||
} catch {
|
||||
Say 'No SMBClient/Connectivity events in the last 3 days (or the log is not enabled).'
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
Section 'What this looks like'
|
||||
if ($findings.Count -eq 0) {
|
||||
Say 'Nothing conclusive. If the drive is failing only at logon, run this again'
|
||||
Say 'IMMEDIATELY after a failure - several of these signals are transient and'
|
||||
Say 'a working session masks the state that caused the prompt.'
|
||||
} else {
|
||||
$n = 0
|
||||
foreach ($f in $findings) { $n++; Say ('{0}. {1}' -f $n, $f) }
|
||||
}
|
||||
Say ''
|
||||
Say ('Full log: {0}' -f $logFile)
|
||||
exit 0
|
||||
Reference in New Issue
Block a user