Christopher Cass
Christopher Cass

Reputation: 979

ForEach in XML file with PowerShell

I have an XML file at $DSConfigPath that contains information as such:

<dataSources>
  <add id="DataSource1" type="Fixed" distance="1">
    <parameters>
      <name />
      <address>10.1.1.10</address>
      <otherIdentifiers>DataSource1</otherIdentifiers>
      <activeStatus>true</activeStatus>
      <ip>10.1.1.10</ip>
      <port>952</port>
    </parameters>
  </add>
  <add id="DataSource2" type="Fixed" distance="2">
    <parameters>
      <name />
      <address>10.1.1.11</address>
      <otherIdentifiers>DataSource2</otherIdentifiers>
      <activeStatus>true</activeStatus>
      <ip>10.1.1.11</ip>
      <port>952</port>
    </parameters>
  </add>
  <add id="DataSource3" type="Fixed" distance="3">
    <parameters>
      <name />
      <address>10.1.1.12</address>
      <otherIdentifiers>DataSource1</otherIdentifiers>
      <activeStatus>false</activeStatus>
      <ip>10.1.1.12</ip>
      <port>952</port>
    </parameters>
  </add>
</dataSources>

My goal is to do a do a port connection test to any IP/port where the <activeStatus> is 'true.'

I've got the following function, which I've verified will give me the correct results when I put in a specific $hostname and $port:

function Test-Port($hostname, $port) {
    # This works no matter in which form we get $host - hostname or ip address
    try {
        $ip = [System.Net.Dns]::GetHostAddresses($hostname) | 
              Select-Object IPAddressToString -ExpandProperty IPAddressToString
        if ($ip.GetType().Name -eq "Object[]") {
            #If we have several ip's for that address, let's take first one
            $ip = $ip[0]
        }
    } catch {
        Write-Host "Possibly $hostname is wrong hostname or IP"
        return
    }
    $t = New-Object Net.Sockets.TcpClient
    # We use Try\Catch to remove exception info from console if we can't connect
    try {
        $t.Connect($ip,$port)
    } catch {}

    if ($t.Connected) {
        $t.Close()
        $msg = "Port $port is operational"
    } else {
        $msg = "Port $port on $ip is closed, "
        $msg += "You may need to contact your IT team to open it. "                                 
    }
    Write-Host $msg
}

So now when I add the following variables:

[xml]$DSConfig = gc "$DSConfigPath"

$DS = $dsconfig.datasources.add.parameters
$DSName = $DS.otherIdentifiers
$DSIP = $DS.ip
$DSPort = $DS.port
$DSActive = $DS | Where-Object {$_.activeStatus -eq 'True'}

$hostname = $DSIP   # yes I realize this is redundant
$port = $DSPORT     # and yes, I realize this is redundant as well

Then run:

foreach ($DSActive in $DSConfig) {Test-Port $hostname $port}

I get the result:

Possibly 10.1.1.10 10.1.1.11is wrong hostname or IP

Any suggestion how I can get it to test the connection to 10.1.1.10:952, give that result, then test the connection to 10.1.1.11:952 and then give that result?

Upvotes: 3

Views: 15564

Answers (3)

Ansgar Wiechers
Ansgar Wiechers

Reputation: 200493

The statement

$DS = $dsconfig.datasources.add.parameters

puts a list of all <parameter> nodes in the variable $DS. If you check the $DS.Count you'll see that it has the value 3 for your sample data, and if you echo the variable you'll see something like this:

PS C:\> Write-Output $DS

name             :
address          : 10.1.1.10
otherIdentifiers : DataSource1
activeStatus     : true
ip               : 10.1.1.10
port             : 952

name             :
address          : 10.1.1.11
otherIdentifiers : DataSource2
activeStatus     : true
ip               : 10.1.1.11
port             : 952

name             :
address          : 10.1.1.12
otherIdentifiers : DataSource1
activeStatus     : false
ip               : 10.1.1.12
port             : 952

Next the statements

$DSIP = $DS.ip
$DSPort = $DS.port

fill the variables $DSIP and $DSPort with a list of all IP addresses and all ports respectively.

PS C:\> Write-Output $DSIP
10.1.1.10
10.1.1.11
10.1.1.12

On PowerShell v3 and newer, that is. Prior to PowerShell v3 the statements would throw an error, because older versions don't support member enumeration, i.e. accessing properties and methods on the elements of an array when the array object itself doesn't have that property or method.

When you're passing this list of IP addresses to Test-Port, the statement

$ip = [System.Net.Dns]::GetHostAddresses($hostname)

fails with a MethodInvocationException, because GetHostAddresses() expects a single string, not a string array.

Also, your loop

foreach ($DSActive in $DSConfig) {Test-Port $hostname $port}

will terminate after one iteration because $DSConfig has just one element: the root node (<dataSources>).

To have your code call Test-Path for each IP address and port change the above loop into this:

foreach ($DSActive in $DSConfig.datasources.add.parameters) {
    Test-Port $DSActive.ip $DSActive.port
}

Upvotes: 1

TheMadTechnician
TheMadTechnician

Reputation: 36332

Alternative method using Select-Xml. This effectively does the same thing as Mathias's answer, but uses a cmdlet instead of the method. Not particularly useful unless you want to take advantage of some of the other options built into the cmdlet that the method doesn't make available (not real useful here, but can definitely be useful in more complex uses).

# Read XML document
[xml]$DSConfig = gc "$DSConfigPath"

# Select <parameters> nodes
$ParametersNode = Select-Xml -Xml $DSConfig -XPath '//parameters'|% {
    Test-Port $_.Node.ip $_.Node.port
}

Upvotes: 0

Mathias R. Jessen
Mathias R. Jessen

Reputation: 174920

Use SelectNodes() to grab the <parameters> nodes from the xml document:

# Read XML document
[xml]$DSConfig = gc "$DSConfigPath"

# Select <parameters> nodes
$ParametersNode = $DSConfig.SelectNodes('//parameters')

# Loop over selected nodes
foreach($Node in $ParametersNode){
  # Test if activeStatus == 'true'
  if($Node.activeStatus -eq 'true') {
    # Run the Test-Port command
    Test-Port $Node.ip -port $Node.port
  }
}

Upvotes: 4

Related Questions