Search code examples
xmlstarlet

xmlstarlet - need to selectively replace attribute value based on value of another attribute


Just wondering if anyone can help me. I'm currently building a pfsense firewall, which uses VPN connections to secure the traffic. The VPN provider does provide a port forwarding mechanism, but the incoming port number changes every hour. I have a script which allows me to discover the new port, but I need a scripted way to modify the port forward settings in the firewall to match.

A snippet of the firewall config file that controls this is as follows: input.xml:

<?xml version="1.0"?>
<pfsense>
    <nat>
        <rule>
            <source>
                <any/>
            </source>
            <destination>
                <any/>
                <port>53400</port>
            </destination>
            <protocol>tcp/udp</protocol>
            <target>192.168.0.15</target>
            <local-port>53400</local-port>
            <interface>opt2</interface>
            <descr><![CDATA[Torrent]]></descr>
            <associated-rule-id>nat_52d81d2dc904f5.77023355</associated-rule-id>
            <created>
                <time>1389894957</time>
                <username>[email protected]</username>
            </created>
            <updated>
                <time>1389980696</time>
                <username>[email protected]</username>
            </updated>
        </rule>
    </nat>

    <filter>
        <rule>
            <id/>
            <type>pass</type>
            <interface>opt2</interface>
            <ipprotocol>inet</ipprotocol>
            <tag/>
            <tagged/>
            <max/>
            <max-src-nodes/>
            <max-src-conn/>
            <max-src-states/>
            <statetimeout/>
            <statetype>keep state</statetype>
            <os/>
            <protocol>tcp/udp</protocol>
            <source>
                <any/>
            </source>
            <destination>
                <address>192.168.0.15</address>
                <port>53400</port>
            </destination>
            <log/>
            <descr><![CDATA[NAT Torrent]]></descr>
            <associated-rule-id>nat_52d81d2dc904f5.77023355</associated-rule-id>
            <created>
                <time>1389894957</time>
                <username>NAT Port Forward</username>
            </created>
            <updated>
                <time>1389899075</time>
                <username>[email protected]</username>
            </updated>
        </rule>
    </filter>
</pfsense>

In the XML above, we have the two parts which comprise a port forward rule for pfsense. The part enclosed in the <nat> section is the port forward. The section in the <rule> is an interface specific incoming firewall rule. Both have to be modified for the new port forward setting to be effective.

I was thinking to use xmlstarlet to modify the config file, using the <descr> as my key for identifying which sections to change.

I'm aware that you can have data like:

<username><![CDATA[name]]></username>
<password><![CDATA[password]]></password>
<dbname><![CDATA[name]]></dbname>

and modify it with:

xml ed -P -O -L \
    -u '//username/text()' -v 'something' \
    -u '//password/text()' -v 'somethingelse' \
    -u '//dbname/text()'   -v 'somethingdifferent' \
    file.xml

and also that you can have something like: sample.xml:

<objects>
    <object>
        <name>Foo</name>
        <constant1>10</constant1>
        <constant2>20</constant2>
    </object>
    <object>
        <name>Bar</name>
        <constant1>15</constant1>
        <constant2>40</constant2>
    </object>
</objects>

and update attributes with:

xmlstarlet ed -u '//object[name="Foo"]/const1' -v 18 sample.xml

However, I'm struggling to merge the two, so that I have a single statement which matches <descr>="Torrent" and then updates the relevant <port> and <local-port> attributes.

Any help with a suitable xmlstarlet command would be much appreciated.


Solution

  • $ xmlstarlet ed \
        -u '//rule[descr="Torrent"]/destination/port' -v 1111 \
        -u '//rule[descr="Torrent"]/local-port'       -v 2222 \
        input.xml |
        xmlstarlet format --indent-spaces 4 > output.xml
    
    $ diff input.xml output.xml
    10c10
    <                 <port>53400</port>
    ---
    >                 <port>1111</port>
    14c14
    <             <local-port>53400</local-port>
    ---
    >             <local-port>2222</local-port>
    28d27
    <