Möhrenfeld

I was looking for an easy and fast way to push configuration to our Juniper devices. Preferably one that doesn’t need anything special except a ssh connection.

I started with a standard Juniper configuration snippet. Something like this:

policy-options {
replace:
    policy-statement deny-everything {
        then reject;
    }
}

How do we get this on the device? Luckily Juniper (as well as other vendors) supports a feature called NETCONF ({% include rfc.html rfc=“6241” %}) which uses a XML RPC API to talk to the device. You need to enable it together with SSH:

system {
    services {
        ssh;
        netconf {
            ssh;
        }
    }
}

In the Juniper NETCONF documentation we find the useful <load-configuration> command. This can be used to load configuration data into the candidate configuration of the JunOS device. It supports XML syntax (which is used internally by JunOS), configuration text or set commands. For our snippet we need text mode, which means the following syntax:

<rpc>
    <load-configuration action="merge" format="text">
        <configuration-text>
        <!-- configuration data -->
        </configuration-text>
    </load-configuration>
</rpc>

Combining this with our snippet we end up with:

<rpc>
    <load-configuration action="merge" format="text">
        <configuration-text>
          policy-options {
          replace:
              policy-statement deny-everything {
                  then reject;
              }
          }
        </configuration-text>
    </load-configuration>
</rpc>

This will instruct the device to merge the configuration snippet with the current configuration. The replace: tag means that the deny-everything statement will be replaced with the new version rather than merged.

Now we can send this to the device. Fortunately there is a NETCONF command for SSH. This means you can invoke a NETCONF session by simply opening a SSH session to the device and specifying the NETCONF subsystem:

ssh admin@router netconf

That’s all it takes. You now have an open NETCONF session to the system. You should get a NETCONF “hello” response, something like this:

<!-- No zombies were killed during the creation of this user interface -->
<!-- user admin, class j-super-user -->
<hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <capabilities>
    <capability>urn:ietf:params:netconf:base:1.0</capability>
    <capability>urn:ietf:params:netconf:capability:candidate:1.0</capability>
    <capability>urn:ietf:params:netconf:capability:confirmed-commit:1.0</capability>
    <capability>urn:ietf:params:netconf:capability:validate:1.0</capability>
    <capability>urn:ietf:params:netconf:capability:url:1.0?scheme=http,ftp,file</capability>
    <capability>urn:ietf:params:xml:ns:netconf:base:1.0</capability>
    <capability>urn:ietf:params:xml:ns:netconf:capability:candidate:1.0</capability>
    <capability>urn:ietf:params:xml:ns:netconf:capability:confirmed-commit:1.0</capability>
    <capability>urn:ietf:params:xml:ns:netconf:capability:validate:1.0</capability>
    <capability>urn:ietf:params:xml:ns:netconf:capability:url:1.0?protocol=http,ftp,file</capability>
    <capability>http://xml.juniper.net/netconf/junos/1.0</capability>
    <capability>http://xml.juniper.net/dmi/system/1.0</capability>
  </capabilities>
  <session-id>48653</session-id>
</hello>
]]>]]>
<!-- session end at 2017-05-11 11:25:45 CEST -->

Now we just need to push the configuration to the system. We can do this by simply using echo to pipe it into the SSH session:

$ echo '    <rpc>
>         <load-configuration action="merge" format="text">
>             <configuration-text>
>               policy-options {
>               replace:
>                   policy-statement deny-everything {
>                       then reject;
>                   }
>               }
>             </configuration-text>
>         </load-configuration>
>     </rpc>
> ' | ssh admin@router netconf

If everything worked as expected you get back a response that contains this:

<load-configuration-results>
<ok/>
</load-configuration-results>

Great! The snipped is now loaded in the candidate configuration (uncommitted):

admin@lab-mx960> configure
Entering configuration mode
The configuration has been changed but not committed

[edit]
admin@lab-mx960# show | diff
[edit policy-options]
+   policy-statement deny-everything {
+       then reject;
+   }

[edit]
admin@lab-mx960#

Do a commit and you are done.

{% include danger.html content=“There is no error checking done. If the code snippet is invalid or has other problem you must check this yourself, for example by doing a show | diff on the device.” %}

If you want you can also do the commit automatically by appending it to the RPC call:

<rpc>
    <load-configuration action="merge" format="text">
        <configuration-text>
          policy-options {
          replace:
              policy-statement deny-everything {
                  then reject;
              }
          }
        </configuration-text>
    </load-configuration>
</rpc>
<rpc>
    <commit/>
</rpc>

This will send the snippet to the device and do a commit immediately after.

{% include danger.html content=“Again, there is no error checking! This will commit whatever you send, as long as it passes the syntax check. Also it will commit anything else that was not committed before and is still in the candidate configuration! You must make sure that no one else is editing the configuration at the same time!” %}

As the XML “wrapper” around the configuration is always the same you could also put it out of the way into a separate script. I wrote one and called it netconf-merge-wrapper:

#! /bin/bash
# Use like: bgpq3 ... stuff | netconf-merge-wrapper | ssh admin@router netconf
cat <<_END
<rpc>
    <load-configuration action="merge" format="text">
        <configuration-text>
_END
cat -
cat <<_END
        </configuration-text>
    </load-configuration>
</rpc>
_END
if [ "$1" == "commit" ]; then
cat <<_END
<rpc>
    <commit/>
</rpc>
_END
fi

As the comment states you could use that script with bgpq3. bgpq3 builds automatic prefix-lists, route-filters or as-path-groups for Cisco and Juniper devices by supplying an AS number or AS-SET.

So for example if you want to generate a route-filter for all AS-APPLE prefixes, you can use it like that:

bgpq3 -J3EAl as-apple AS-APPLE | netconf-merge-wrapper | ssh admin@router netconf

If you are brave you can commit it right away as well:

bgpq3 -J3EAl as-apple AS-APPLE | netconf-merge-wrapper commit | ssh admin@router netconf

In production I would invest in a lot more error checking before rolling stuff out all over the place. For that I would use python and ncclient. Still, I hope this illustrates that it is relatively easy to push stuff to the NETCONF interface, even without skills in programming.