Easy network automation with Ansible & Expect

These days I’ve been testing different things at work related to network automation. Most of the development that I’ve done is related to Junos platforms, netconf and ansible. It’s been really fun to learn different things such as Jinja2/Yaml/Netconf/XML(Path)/Networkx….
I see all of these things like lego blocks that are just waiting to be assembled into something bigger and actually pretty powerful.

But well, as an introduction to a series of posts that I’ll try to do soon related to network automation, let me introduce a really humble approach to a well-known tool (expect) and its integration with Ansible.

The task is basically to log into several hosts and execute a series of ‘show’ commands to retrieve some information.
(I’ve seen documentation related with an expect module integrated already on Ansible but what I’m trying to do here is to show how to run a script via Ansible and by doing so, make it multi-threading).

First, we have a simple Expect script.
We pass 3 variables via command line:
1.- Ip address of the host
2.- Command list file
3.- Host name (for log file naming purposes)

I named the expect script ‘get_commands.expect’ (it will be referenced by Ansible below).

set USER xxxx
set PASS yyyy
set HOST [lindex $argv 0]

set COMMAND_LIST [open [lindex $argv 1]]
set commands [split [read $COMMAND_LIST] "\n"]

set HOSTNAME [lindex $argv 2]
set timeout 60

set FILE_DIR [lindex $argv 3]

log_file -noappend ${FILE_DIR}/${HOSTNAME}_results.log

spawn telnet $HOST
expect {
	"ogin:" { send "$USER\r" }
	"sername" { send  "$USER\r" }
expect "assword:"
send "$PASS\r"

foreach cmd $commands {
	expect "$USER@"
	send "$cmd\r"
send "\r"
close $spawn_id

The idea for passing commands via the for loop came from here

We could have a nested ‘for’ referencing each target host pulled from another file (like in the link). But if we do this, we would be having the telnet session built sequentially for each of the hosts, by this I mean that we would have to wait for the commands to be executed at one host before being able to move to the next host and get the info.

Instead of that, we use ansible to do the multi-threading work for us, so we can log into all the target hosts at once and extract the necessary information given by the commands defined by a command list file:

- name: Fetching Juniper Commands
  hosts: target_hosts
  gather_facts: no
  serial: 100%

  - set_fact:
      script_dir: "scripts"
  - name: Executing the expect script
    script: ./{{ script_dir }}/get_commands.expect {{ ansible_ssh_host }} {{ commands }} {{ inventory_hostname }} {{ log_dir }}

For this to work we also need the hosts file to have the group named [target_hosts] and the necessary login information for each target.
We need to have a .txt including the show commands as well.
For example, the command_list.txt (referenced inside the ansible playbook and passed as an argument to the expect script as {{ commands }}) could have the next commands:

cat scripts/command_list.txt

show bgp summary
show bgp neighbor
show route summary
show bgp summary

{{ log_dir }} is defined inside the group_vars yml file. This is useful to define the same directory to keep all the log files for each router.
cat target_hosts.yml

log_dir: /xxxx/logs

When we pass this as an argument to the expect script and the other necessary info to make the telnet sessions, then the magic occurs.

I’ll be publishing more and more regarding these topics, especially regarding netconf and pyez (a library written in python released by Juniper to make automation easier), so keep in touch.

I hope this is useful to somebody 🙂


Junos configuration parser

Hola, después de tanto tiempo….

Por cierta cuestión en el trabajo por fin me di el tiempo de crear una herramienta para traducir formatos de configuración Juniper (JUNOS).

Para probar los resultados de esto, pongo el siguiente ejemplo de configuración que se quiere traducir de formato jerárquico a formato ‘set command’ de Junos.

protocols {
    bgp {
        path-selection always-compare-med;
        group USER-A {
            type external;
            local-address x.x.x.x;
            export policy-A;
            peer-as XXX;
            neighbor y.y.y.y {
                description USER-A;
        group USER-A-v6 {
            type internal;
            local-address x:x:x::x;
            export policy-B;
            peer-as XXX;
            neighbor Y:Y:Y::Y {
                description USER-A-v6;

El script:

#Code by @aldogoliath --- 26-Feb-2016
import string

target ='junos_config.txt'

def main():
    buff = []
    for line in open(target,'r'):
        if '{\n' in line[-2:]:
        if ';' in line:
            if len(buff) == 0:
                print 'set ' + line[:line.index(';')].strip()
                print 'set ' + string.join(buff) + ' ' + line[:line.index(';')].strip()
        if '}\n' in line:
            buff = buff[:-1]

if __name__ == '__main__': main()

Ejecutando el código nos queda lo siguiente:

python blog_set-command.py
set protocols bgp path-selection always-compare-med
set protocols bgp advertise-inactive
set protocols bgp log-updown
set protocols bgp group USER-A type external
set protocols bgp group USER-A local-address x.x.x.x
set protocols bgp group USER-A export policy-A
set protocols bgp group USER-A peer-as XXX
set protocols bgp group USER-A neighbor y.y.y.y description USER-A
set protocols bgp group USER-A-v6 type internal
set protocols bgp group USER-A-v6 local-address x:x:x::x
set protocols bgp group USER-A-v6 export policy-B
set protocols bgp group USER-A-v6 peer-as XXX
set protocols bgp group USER-A-v6 neighbor Y:Y:Y::Y description USER-A-v6

Nota: Cuando se tiene acceso directo al equipo el script es innecesario debido a que se pueden obtener los mismos resultados ejecutando ‘show configuration | display set’

Ojalá le sea útil a alguien.

Saludos y nos vemos pronto para contar los últimos acontecimientos después de 6 meses.