Showing posts with label Windows OS. Show all posts
Showing posts with label Windows OS. Show all posts

14 May, 2016

Find target of DFS links - OS

There are many times when people need to know the target folder of a DFS link. However, when you have multiple levels of DFS namespaces, for example, you have a Domain Based root called \\tatooine.com\root and under it there are DFS folders and links, but one link points to a standalone DFS root, e.g.: \\tatooine\root\cities\moseisley and this pointer points to another DFS root: \\c3podc1\root and then there are folders and pointers under this namespace as well.

It takes many clicks on DFS Management console to open each DFS namespace and root and walk trough them. Alternatively, you can use the DFSN module on Windows Server 2012 but the command Get-DFSNFolderTarget doesn't understand the multi level structure, it can only wok in one given DFSN root.

Let's take an example, we want to know which file server and which folder the following path points to : \\tatooine.com\root\cities\moseisley\cantina\stock
In the DFS management console, you have to
  1. add \\tatooine.com\root namespace for display
  2. drill down to the MosEisley folder to find that it actually points to \\c3podc1\moseisley
  3. so then you add \\c3podc1\moseisley DFS root to the console
  4. drill down to the Stock folder under Cantina and find that it points to \\fileserver\share1\documents folder

multi-level dfs roots
Find a target in a multi-level DFS structure on DFS Management Console



















It's not very rewarding after you get the 5th of these requests from users...

Here is a quick script which can walk through the folder structure, just specify the full path of a folder and it will tell you for each level where it points to, the script requires the DFSN PS module installed on the box - included in the DFS Management Tools Feature.
If the given folder level is just a DFS folder without target, obviously it will show and empty target, e.g.:

DFS lookup with powershell


On the above picture:

  • \\tatooine.com\root\cities is just a folder because it doesn't point anywhere
  • \\tatooine.com\root\cities\moseisley points to another DFS root
  • \\c3podc1\moseisley\Cantina is again just a folder without DFS target
  • \\c3podc1\moseisley\Cantina\stock is the pointer we wanted to look for and it points to \\fileserver\share1\documents


Here is the code:
 param([string]$folderpath = "")  
   
 $erroractionpreference = "silentlycontinue"  
 Import-module DFSN
 $arr = $folderpath.split("\\")  
 $basepath = ("\\" + $arr[2] + "\" + $arr[3])  
 $testpath = $basepath  
   
 # go from the 4th element of the path and try to get the target of each
 for($i = 4; $i -lt $arr.count; $i++){  
    $testpath = $testpath + "\" + $arr[$i]  
    $result = get-dfsnfoldertarget $testpath  
    $testpath + "->" + $result.targetpath  
   
    if($result.targetpath){  
       $testpath = $result.targetpath  
    }  
 }  



06 April, 2015

Verify forward and reverse DNS records - OS

DNS is a fairly simple and usually reliable service - when it works. People don't really think about it unless there are weird issues with an application and they spent 2 days troubleshooting it and just because they want to make sure they looked at everything, they check DNS and it turns out that those weird issues are in fact caused by e.g. missing reverse records.

These issues manifest in applications behaving strangely often providing uselessly generic errors messages or even worse, some misleading ones. After a very tedious issue over a weekend which required repeated checkouts across a globally distributed AD integrated DNS zone, I thought I wouldn't do this manually all the time with excel magic but via a script.

Steps needed to be made:
  • Take a list of server names (ideally FQDNs)
  • Perform a forward lookup to get the IP address(es) of each server
  • Take those IPs and perform a reverse lookup on them
  • Mark each host and IP with error if either the forward or reverse lookups fail or the reverse lookup provides a different hostname than the server name provided

Forward lookup (filtering on IPv4 only):
[array]$IPAddresses = [System.Net.Dns]::GetHostAddresses($obj.ComputerName) | ?{$_.AddressFamily -eq "InterNetwork"} | %{$_.IPAddressToString}


Reverse lookup:
$tmpreverse = [System.Net.Dns]::GetHostByAddress($_).HostName

Output:






The full script (simplified version):

 $hostlist = @($input)  
   
 # running through the list of hosts  
 $hostlist | %{  
      $obj = "" | Select ComputerName,Ping,IPNumber,ForwardLookup,ReverseLookup,Result  
      $obj.ComputerName = $_  
   
      # ping each host  
      if(Test-Connection $_ -quiet){  
           $obj.Ping = "OK"  
     $obj.Result = "OK"  
      }  
      else{  
           $obj.Ping = "Error"  
     $obj.Result = "Error"  
      }  
        
      # lookup IP addresses of the given host  
      [array]$IPAddresses = [System.Net.Dns]::GetHostAddresses($obj.ComputerName) | ?{$_.AddressFamily -eq "InterNetwork"} | %{$_.IPAddressToString}  
   
      # caputer count of IPs  
      $obj.IPNumber = ($IPAddresses | measure).count  
        
      # if there were IPs returned from DNS, go through each IP  
   if($IPAddresses){  
     $obj.ForwardLookup = "OK"  
   
        $IPAddresses | %{  
             $tmpreverse = $null  
                  
                # perform reverse lookup on the given IP  
             $tmpreverse = [System.Net.Dns]::GetHostByAddress($_).HostName  
             if($tmpreverse){  
                  
                     # if the returned host name is the same as the name being processed from the input, the result is OK  
                  if($tmpreverse -ieq $obj.ComputerName){  
                       $obj.ReverseLookup += "$_ : OK `n"  
                  }  
                  else{  
                       $obj.ReverseLookup += "$_ different hostname: $tmpreverse `n"  
                       $obj.Result = "Error"  
                  }  
             }  
             else{  
                  $obj.ReverseLookup = "No host found"  
                  $obj.Result = "Error"  
             }  
     }  
      }  
      else{  
           $obj.ForwardLookup = "No IP found"  
           $obj.Result = "Error"  
      }  
        
      # return the output object  
      $obj  
 }  


t


13 January, 2015

Move page file remotely - OS

With many servers in the environment, there are inevitably a bunch of them which are old, they have small disk capacity but they are needed "just for another couple of months" until the new system is up... and a couple of years later you are still the chosen one to keep them alive.

There are many challenges with these old boxes, so let me pick one: there's not enough disk space on drive C: and there's not much more to delete anymore. Last resort: move the pagefile to another drive (if there's one in the box). Let's script it to be able to make this change on many servers remotely.

Need to check a couple of things:
  • the original page file's details
  • target drive exists
  • it has sufficient space to accommodate the page file (based on MaximumSize)
  • target drive is local and is not hosted on SAN

Create the output object and get data of the current page file:
$obj = "" | Select ComputerName,OldPageFile,OldInitSize,OldMaxSize,Result
$obj.ComputerName = $srv
$pagefiledata = gwmi -ComputerName $srv -Class Win32_PageFileSetting

Get details of the volume where we want to move the page file (check whether it exists, whether there's enough space on it - here the limit is the size of the current page file twice)
$targetvolume = gwmi -ComputerName $srv -Class Win32_LogicalDisk -Filter "name='$movetodrive'"

Create new page file:
$result = Set-WmiInstance -ComputerName $srv -Class Win32_PageFileSetting -Arguments @{name="$targetfilename";InitialSize=$obj.OldInitSize;MaximumSize=$obj.OldMaxSize;}

Sometimes - especially on Windows Server 2003 - the Set-WmiInstance can't create the page file if the InitialSize and MaximumSize are set in the same command, for that the workaround is to create the page file as system managed (without additional parameters) and set the size afterwards.
$result = Set-WmiInstance -ComputerName $srv -Class Win32_PageFileSetting -Arguments @{name="$targetfilename"}

if($result.name -eq $targetfilename){
  $newPGFile = gwmi -ComputerName $srv -Query "Select * from Win32_PageFileSetting where name '$($targetfilename.replace("\","\\"))'"
  $newPGFile.InitialSize = $obj.OldInitSize
  $newPGFile.MaximumSize = $obj.OldMaxtSize
  $FinalResult = $newPGFile.Put()
}


If we do this, the script should check all the parameters we set and the existence of the new page file as part of error handling:
$checkoutPGFile = gwmi -ComputerName $srv -Query "Select * from Win32_PageFileSetting where name '$($targetfilename.replace("\","\\"))'"
if(!($checkoutPGFile -and ($checkoutPGFile.Maximumsize -eq $obj.OldMaxSize) -and ($checkoutPGFile.Maximumsize -eq $obj.OldMaxSize))){...

If all is good with the new page file, we can get rid of the old one, of course this will require a reboot but I decided to leave it to the scheduled reboots of the servers.
$oldPageFileObject = gwmi -ComputerName $srv -Query "Select * from Win32_PageFileSetting where name = '$searchString'"
$oldPageFileObject.Delete()

The full script which can take a list of hosts from the pipe, does all the checks looks like this (obviously I stripped out all the logging and fancy stuff so the essence of it is easier to see):
   
 $hostlist = @($input)  
   
 $objColl = @()  
 $movetodrive = "D:"  
 $targetfilename = $movetodrive + "\pagefile.sys"  
   
 foreach($srv in $hostlist){  
    $obj = "" | Select ComputerName,OldPageFile,OldInitSize,OldMaxSize,Result  
    $obj.ComputerName = $srv  
   
    $pagefiledata = gwmi -ComputerName $srv -Class Win32_PageFileSetting  
      
    if($pagefiledata){  
       $obj.OldPageFile = $pagefiledata.name  
       $obj.OldInitSize = $pagefiledata.Initialsize  
       $obj.OldMaxSize = $pagefiledata.Maximumsize  
         
       $targetvolume = gwmi -ComputerName $srv -Class Win32_LogicalDisk -Filter "name='$movetodrive'"  
         
       if(!$targetvolume){  
          $obj.Result = "$movetodrive does not exist"  
          $objColl += $obj  
          Continue  
       }  
         
       if(($targetvolume.Freespace / 1MB) -lt ($obj.OldMaxSize * 2)){  
          $obj.Result = "Not enough space on $movetodrive"  
          $objColl += $obj  
          Continue  
       }  
         
       $result = Set-WmiInstance -ComputerName $srv -Class Win32_PageFileSetting -Arguments @{name="$targetfilename";InitialSize=$obj.OldInitSize;MaximumSize=$obj.OldMaxSize;}  
         
       # this is needed in case the WMI instance can't be created with all parameters in one go  
       if(!($result.name -eq $targetfilename)){  
          $result = Set-WmiInstance -ComputerName $srv -Class Win32_PageFileSetting -Arguments @{name="$targetfilename"}  
         
          if($result.name -eq $targetfilename){  
             $newPGFile = gwmi -ComputerName $srv -Query "Select * from Win32_PageFileSetting where name '$($targetfilename.replace("\","\\"))'"  
             $newPGFile.InitialSize = $obj.OldInitSize  
             $newPGFile.MaximumSize = $obj.OldMaxtSize  
             $FinalResult = $newPGFile.Put()  
          }  
            
          $checkoutPGFile = gwmi -ComputerName $srv -Query "Select * from Win32_PageFileSetting where name '$($targetfilename.replace("\","\\"))'"  
          if(!($checkoutPGFile -and ($checkoutPGFile.Maximumsize -eq $obj.OldMaxSize) -and ($checkoutPGFile.Maximumsize -eq $obj.OldMaxSize))){  
             $obj.Result = "Could not create new page file"  
             $objColl += $obj  
             Continue     
          }  
       }  
         
       if($result.name -eq $targetfilename){  
          $searchString = $obj.OldPageFile.replace("\","\\")  
          $oldPageFileObject = gwmi -ComputerName $srv -Query "Select * from Win32_PageFileSetting where name = '$searchString'"  
          $oldPageFileObject.Delete()  
       }  
         
       if((gwmi -ComputerName $srv -Class Win32_PageFileSetting).name -eq $targetfilename){  
          $obj.Result = "SUCCESS - please reboot the host"  
       }  
       else{  
          $obj.Result = "Could not move page file"  
       }  
    }  
      
    $objColl += $obj  
 }  
   
 $objColl  
   



t

08 January, 2015

Determine if a drive is SAN drive - OS

If you have servers connected to SAN you would think that you don't really have to worry about the physical representation of your volumes, in other words, you don't need to know how many physical drives (spindles) are behind your e.g. D: drive.
Life is not simple. There are times when you need to know if a volume is on a local hard disk or is on a SAN LUN. An example would be when you need to move the pagefile out of drive C:, you might not want to put that on the SAN disk (there can be several reasons, one is that the volume may be in a dynamic or clustered disk group and can fail over to another node or just simply because SAN disk space is expensive therefore using it for paging is not the best use of you dollars.)

Now we have a case: we have a drive letter and we want to decide if the volume is on a SAN disk or not. Of course we want to do this remotely and on 100+ servers.
The philosophical problem with this is that engineers spent so much effort in the last decades on creating storage systems and disk manager sub-systems to hide the complex details of a storage - including multipath fiber channels, SAN switches, disk arrays...etc.) from the OS and to make sure that the OS can see a volume and does not need to worry about how it's presented and what's behind it. So any API that could be used to track down SAN drives remotely are vendor specific (it's different for EMulex, Qlogic or whatever HBA driver). I need something universal.

I was poking around the WMI classes and noticed that bits of information are in several classes, so started connecting the dots - nothing scientific, just trial and error as usual. If you look long enough you can connect the drive letter to a PNPDeviceID which can tell you if the physical drive is local or SAN, here is an example:
  • Win32_LogicalDiskToPartition: D: -> Disk #0, Partition#0
  • Win32_DiskDriveToPartition: Disk #0, Partition#0 -> PHYSICALDRIVE2
  • Win32_DiskDrive: PHYSICALDRIVE2 -> MPIO\DISK&....
    If the PNPDeviceID starts with MPIO (which stands for MultiPath I/O) then it's a drive hosted on SAN Array which the host can see on multiple fiber channels.

Just need a bit of regex matching to walk through this in Powershell:









  1. Get the disk number where the volume is hosted (and the partition number as well):
    $DriveletterToDiskNumberQuery = gwmi -ComputerName $srv -Class Win32_LogicalDiskToPartition | ?{$_.Dependent -imatch "Win32_LogicalDisk\.DeviceID=`"$driveletter\:`""} | select -First 1 | %{$_.Antecedent}
  2. Need to parse the exact Disk # from the long text which is in the WMI instance:
    $DriveletterToDiskNumber = ([regex]::Match($DriveletterToDiskNumberQuery, "Disk #\d+, Partition #\d+")).Value
  3. Take the disk number and lookup the DeviceID:
    $DiskNumberToDevideIDQuery = gwmi -ComputerName $srv -Class Win32_DiskDriveToDiskPartition | ?{$_.Dependent -imatch $DriveletterToDiskNumber} | select -First 1 | %{$_.Antecedent}
  4. Parse the DeviceID from the long text:
    $DiskNumberToDevideID = ([regex]::Match($DiskNumberToDevideIDQuery, "PHYSICALDRIVE\d+")).Value
  5. Get the PNPDeviceID of the given device to see if it's MPIO or not:
    gwmi -ComputerName $srv -Class Win32_DiskDrive | ?{$_.DeviceID -imatch $DiskNumberToDevideID} | Select -First 1 | %{$_.PNPDeviceID}
A simple script which takes the list of hosts from the pipe and outputs and object with the hostname, the PNPDeviceID and the Drive Type would look like this (without handling errors e.g. when there's no drive D or the host is not accessible...etc.):
 $hostlist = @($input)  
 $driveletter = "D"  
   
 foreach($srv in $hostlist){  
    $obj = "" | Select ComputerName,DriveType,PNPDeviceID  
    $obj.ComputerName = $srv  
   
    # Get the list Disk # for the given volume  
    $DriveletterToDiskNumberQuery = gwmi -ComputerName $srv -Class Win32_LogicalDiskToPartition | ?{$_.Dependent -imatch "Win32_LogicalDisk\.DeviceID=`"$driveletter\:`""} | select -First 1 | %{$_.Antecedent}  
   
    # parse the Disk and partition # from the text  
    $DriveletterToDiskNumber = ([regex]::Match($DriveletterToDiskNumberQuery, "Disk #\d+, Partition #\d+")).Value  
   
    # Get the DeviceID of the given Disk #  
    $DiskNumberToDevideIDQuery = gwmi -ComputerName $srv -Class Win32_DiskDriveToDiskPartition | ?{$_.Dependent -imatch $DriveletterToDiskNumber} | select -First 1 | %{$_.Antecedent}  
      
    # parse the DeviceID from the text  
    $DiskNumberToDevideID = ([regex]::Match($DiskNumberToDevideIDQuery, "PHYSICALDRIVE\d+")).Value  
   
    # get the PNPDeviceID of the given Device  
    $obj.PNPDeviceID = gwmi -ComputerName $srv -Class Win32_DiskDrive | ?{$_.DeviceID -imatch $DiskNumberToDevideID} | Select -First 1 | %{$_.PNPDeviceID}  
   
    if($obj.PNPDeviceID -imatch "^mpio"){  
       $obj.DriveType = "SAN"  
    }  
    else{  
       $obj.DriveType = "Local"  
    }  
   
    $obj  
 }  


t

17 September, 2014

Read performance counter data remotely - OS

There was a day when there were a couple of boxes with sporadically and abnormally loaded CPU and had to catch when the CPU went up for a couple of minutes on any of them.

Instead of logging into each via RDP or open Perfmon against each host manually, here is a quick command line to get the aggregate CPU load from multiple hosts in one go.

PS C:\> while (1) { (("host1,host2,host3").split(",")) | % { get-counter "\\$_\Processor(_total)\% Processor Time" -maxsamples 1 | select -exp countersamples | select path,cookedvalue }| sort CookedValue -desc; sleep 60; clear}



Some explanation and other ideas:

  •  (("host1,host2,host3").split(","))
    create an array from the comma delimited list of hosts
  • get-counter "\\$_\Processor(_total)\% Processor Time" -maxsamples 1
    read counter from each host remotely. The counter can be replaced i something else needs to be monitored, other than CPU
  • select -exp countersamples | select path,cookedvalue
    Expand what's in the countersamples property and take only the 'path' and 'cookedvalue'
  • sort CookedValue -desc
    put  the largest value to the top
  • sleep 60; clear
    wait a minute, clear the screen and do it again in a while loop

It's not quick, not sophisticated, no bells and whistles around it but it does the job and gives you a sample of the CPU load periodically.

Hope it's useful.
t

14 July, 2014

Check DNS server IPs on NICs remotely - OS

This will be a quick post. I've come across an issue - while you assume that configuration management on loads of hosts is important and usually managed well, you can never be sure. Let's say I have some DNS servers and I'm replacing them with new ones, hopefully all other servers - in one way or another - will pick up the new DNS IPs and drop the old ones on all of their network interfaces. I just had a bad feeling about this...felt tremor in The Force.

I wanted a quick report on a list of servers and see what DNS IPs they used. At the same time - if I was writing some PowerShell code already why not check if those DNS IPs were alive.

So here is what needed to be done:

Enumerate DNS IPs of all interfaces

This is the easy bit, just use WMI and take all data from all adapters which are IP enabled and have a default gateway defined.
[array]$InterfaceList = gwmi win32_networkadapterconfiguration -ComputerName $srv | ?{$_.ipenabled -ieq "true" -and $_.DefaultIPGateway}

Read DNS IPs from each interface

This will be in the DNSServerSearchOrder property and you can assume it will be an array with as many IPs as many DNS servers the interface has. In my case, I know I have 3 everywhere so I might just hard code it into a loop to look at 3 elements:
for ($i = 1; $i -le 3; $i++){
$script:obj.("DNS_" + $i) = $interface.DNSServerSearchOrder[$i]
}

Verify the given DNS IP to see if it's a live DNS server

This is the tricky bit. Not impossible though.. there are ugly solutions and some uglier ones. Why? The nice native method would be to perform a DNS lookup against the DNS server by using the System.Net.Dns class. The problem is that this class will always use the DNS server set on your local host and you can't specify one like with nslookup.

So an ugly solution would be to run nslookup against the given DNS server and parse its text output. Or you could also change your DNS server on the local host on the fly and use System.Net.Dns. Another one is to see if UDP port 53 is open, it's at least 30 lines of code because as UDP is not a handshaking protocol, you need to setup a System.Net.Sockets.Udpclient, set encoding, initiate connection, handle errors, false positives...etc.
I decided to go with an easier one. By default, all DNS servers have the TCP port 53 open as well for large queries and zone transfers.  That's much easier to test:
$Socket = New-Object System.Net.Sockets.TCPClient
$Connect = $Socket.BeginConnect($srv,53,$null,$null)
$Wait = $Connect.AsyncWaitHandle.WaitOne(3000,$false)

The below script takes a list of hosts from the pipe, enumerates 3 DNS IPs of each interface and checks if those DNS servers are alive and then lists the output in an object collection.

 param ( [string] $log = "",  
  [string] $hosts = "")  
   
   
   
 #### Function for checking if a TCP port is opened on a host  
 function TCPportCheck ([string]$fsrv, [int]$port) {  
  $Socket = New-Object System.Net.Sockets.TCPClient  
  $Connect = $Socket.BeginConnect($fsrv,$port,$null,$null)  
  Start-Sleep 1  
  if($Connect.IsCompleted){  
  $Wait = $Connect.AsyncWaitHandle.WaitOne(3000,$false)    
  if(!$Wait){  
   $Socket.Close()   
   return $false   
  }   
  else{  
   $Socket.EndConnect($Connect)  
   $Socket.Close()  
   return $true  
  }  
  }  
  else{  
  return $false  
  }  
 }  
   
   
 $script:objColl = @()  
   
 #### Collate the host list.  
 $hostlist = @($Input)  
 if ($hosts) {  
  if($hosts -imatch " "){  
  $hostsArr = @($hosts.split(" "))  
  $hostlist += $hostsArr  
  }  
  else{  
  $hostlist += $hosts  
  }  
 }  
   
 foreach ($srv in $hostlist) {  
    
  # read list of interfaces  
  [array]$InterfaceList = gwmi win32_networkadapterconfiguration -ComputerName $srv | ?{$_.ipenabled -ieq "true" -and $_.DefaultIPGateway}  
    
  # go through each interface and read DNS IPS  
  if($InterfaceList.Count -gt 0){  
  foreach ($interface in $interfaceList){  
     
   #then check the DNS accessibility and lookup the DNS server name.  
   $script:obj = "" | select ComputerName,Access,InterfaceName,DNS_1,DNS_1_check,DNS_2,DNS_2_check,DNS_3,DNS_3_check,Result  
   $script:obj.ComputerName = $srv  
   $script:obj.Access = "OK"  
   $script:obj.InterfaceName = $interface.Description  
   $tmpResult = $null  
     
   for ($i = 0; $i -le 2; $i++){  
   $tmpIP = $tmpName = $null  
   $tmpIP = $interface.DNSServerSearchOrder[$i]  
     
   $tmpName = [System.Net.Dns]::GetHostByAddress($tmpIP).HostName  
   $script:obj.("DNS_"+($i+1)) = $tmpIP + " ($tmpName)"  
     
   if((TCPportCheck $tmpName 53)){  
    $script:obj.("DNS_"+($i+1)+"_Check") = "DNS port open"  
   }  
   else{  
    $script:obj.("DNS_"+($i+1)+"_Check") = "DNS port closed"  
    $tmpResult = "One or more DNS servers are not accessible"  
   }  
   }  
   if($tmpResult) {$script:obj.Result = $tmpResult}  
   else  {$script:obj.Result = "OK"}  
   $script:objColl += $script:obj  
  }  
  }  
 }  
   
 $script:objColl  
   


t



14 May, 2014

Validate PATH environment variable entries - OS

Following on from the post where I wrote about managing the entries in the PATH environment variable to avoid OS and application misbehaviour, I thought I'd share another story on PATH which can also show you why we (IT guys) should never forget the basics and that the simplest idea is the best idea!

I've come across some hosts where the PATH was more than 2048 characters long which made applications stop working. OK, no problem (I thought), I just run my script which removes duplicates from the PATH and it should go well under 2048 characters. I'm not an overconfident guy, but I was surprised when I saw the length still well above 2000. Now what? Then it hit me, why do I take it for granted that all these entries in the PATH are needed? Maybe there are PATH entries which don't even exist on the file system anymore! Simple, isn't it, just get rid of garbage from the environment variable.

Let's run through the PATH entries one by one and check if those folders exist on the box at all - important: if you do this on a cluster node where one of the PATH entries points to one of the clustered drives, you can incorrectly identify that folder as non existent, but in reality the folder exists, it's just on a disk which is active on another node of the cluster at that time, so be careful.

Of course we want to do this remotely:
$srv = "r2d2"
$pathString = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $srv).OpenSubKey("SYSTEM\CurrentControlSet\Control\Session Manager\Environment").getvalue("path")

$pathstring.split(";") | %{
$obj = "" | Select Item,Accessible
$obj.Item = $_.trim()
$obj.Accessible = test-path ("\\$srv\" + $obj.item.replace(":","$"))
$obj}

Make it shorter by creating the output object on the fly:
$srv = "r2d2"
$pathString = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $srv).OpenSubKey("SYSTEM\CurrentControlSet\Control\Session Manager\Environment").getvalue("path")

 
$pathstring.split(";") | %{
New-Object psobject -Property @{test=(test-path $_); item=$_}
} | ft item,test -auto

Or you can do it locally on a host (which makes it simpler and it can even be a oneliner:
(Get-ItemProperty "HKLM:SYSTEM\CurrentControlSet\Control\Session Manager\Environment").path.split(";") | %{new-object psobject -Property @{test=(test-path $_); item=$_}} | ft item,test -auto

This will show you something like:


You can make this a script by adding parameters, get the list of hosts from $input...etc.

t