Create a config export script for the vSRX 3.0 template

Some time ago I blogged about a custom template for the vSRX 3.0. One drawback was, that the config-export would no longer work, since this is a custom template but I took this drawback because the speed improvements were very good and I usually export my config manually. But I understand, that some of you use this feature in and out so I tried to generate a config-export-script. With the help of Alain from EVE-NG I was able to map the config-script to the new template 🙂 Huge thanks to Alain 🙂

Here’s what you need to do:

1.) Copy this script with the name config_vsrx30.py to /opt/unetlab/scripts/ – here’s my script, that will use the username ‘root’ and the password ‘Juniper’:

#!/usr/bin/env python3

# scripts/config_vsrx30.py
#
# Import/Export script for vsrx30.
#
# @author Andrea Dainese <andrea.dainese@gmail.com>
# @author Alain Degreffe <eczema@ecze.com>
# @copyright 2014-2016 Andrea Dainese
# @copyright 2017-2018 Alain Degreffe
# @copyright 2019-2020 Christian Scholz
# @license BSD-3-Clause https://github.com/dainok/unetlab/blob/master/LICENSE
# @link http://www.eve-ng.net/
# @version 20200422

import getopt, multiprocessing, os, pexpect, re, sys, time

username = 'root'
password = 'Juniper'
conntimeout = 3     # Maximum time for console connection
expctimeout = 3     # Maximum time for each short expect
longtimeout = 30    # Maximum time for each long expect
timeout = 60        # Maximum run time (conntimeout is included)

def node_login(handler):
    # Send an empty line, and wait for the login prompt
    i = -1
    while i == -1:
        try:
            handler.sendline('\r\n')
            i = handler.expect([
                'login:',
                'root@.*%',
                'root>',
                'root@.*>',
                'root#',
                'root@.*#'], timeout = 5)
        except:
            i = -1

    if i == 0:
        # Need to send username and password
        handler.sendline(username)
        try:
            j = handler.expect(['root@.*#', 'Password:'], timeout = longtimeout)
        except:
            print('ERROR: error waiting for ["root@.*%", "password:"] prompt.')
            node_quit(handler)
            return False

        if j == 0:
            # Nothing to do
            return True
        elif j == 1:
            handler.sendline(password)
            try:
                handler.expect('root@.*#', timeout = longtimeout)
            except:
                print('ERROR: error waiting for "root@.*%" prompt.')
                node_quit(handler)
                return False
            return True
        else:
            # Unexpected output
            node_quit(handler)
            return False
    elif i == 1:
        # Nothing to do
        return True
    elif i == 2 or i == 3:
        # Exit from CLI mode
        handler.sendline('exit')
        try:
            handler.expect('root@.*%', timeout = expctimeout)
        except:
            print('ERROR: error waiting for "root@.*%" prompt.')
            node_quit(handler)
            return False
        return True
    elif i == 4:
        # Exit from configuration mode
        handler.sendline('exit')
        try:
            handler.expect(['root>', 'root@.*>'], timeout = expctimeout)
        except:
            print('ERROR: error waiting for ["root>", "root@.*>"] prompt.')
            node_quit(handler)
            return False
        # Exit from CLI mode
        handler.sendline('exit')
        try:
            handler.expect('root.*%', timeout = expctimeout)
        except:
            print('ERROR: error waiting for "root@.*%" prompt.')
            node_quit(handler)
            return False
        return True
    elif i == 5:
        return True
    else:
        # Unexpected output
        node_quit(handler)
        return False

def node_quit(handler):
    if handler.isalive() == True:
        handler.sendline('exit\n')
    handler.close()

def config_get(handler):
    # Clearing all "expect" buffer
    while True:
        try:
            handler.expect('root@.*#', timeout = 0.1)
        except:
            break

    # Go into CLI mode
    handler.sendline('cli')
    try:
        handler.expect(['root>', 'root@.*>'], timeout = longtimeout)
    except:
        print('ERROR: error waiting for ["root>", "root@.*>"] prompt.')
        node_quit(handler)
        return False

    # Disable paging
    handler.sendline('set cli screen-length 0')
    try:
        handler.expect(['root>', 'root@.*>'], timeout = longtimeout)
    except:
        print('ERROR: error waiting for ["root>", "root@.*>"] prompt.')
        node_quit(handler)
        return False

    # Getting the config
    handler.sendline('show configuration | display set')
    try:
        handler.expect(['root>', 'root@.*>'], timeout = longtimeout)
    except:
        print('ERROR: error waiting for ["root>", "root@.*>"] prompt.')
        node_quit(handler)
        return False

    config = handler.before.decode()

    # Exit from config mode
    handler.sendline('exit')
    try:
        handler.expect('root@.*#', timeout = longtimeout)
    except:
        print('ERROR: error waiting for "root@.*%" prompt.')
        node_quit(handler)
        return False
    
    # Manipulating the config
    config = re.sub('\r', '', config, flags=re.DOTALL)                                      # Unix style
    config = re.sub('.*show configuration \| display set', '', config, flags=re.DOTALL)          # Header
    config = re.sub('\nroot.*>.*', '\n', config, flags=re.DOTALL)                        # Footer

    return config

def config_put(handler, config):
    # mount drive
    handler.sendline('mount -t cd9660 /dev/cd0 /mnt')
    try:
        handler.expect(['root>', 'root@.*#'], timeout = expctimeout)
    except:
        print('ERROR: error waiting for ["root>", "root@.*%"] prompt.')
        node_quit(handler)
        return False

    # Go into CLI mode
    handler.sendline('cli')
    try:
        handler.expect(['root>', 'root@.*>'], timeout = longtimeout)
    except:
        print('ERROR: error waiting for ["root>", "root@.*>"] prompt.')
        node_quit(handler)
        return False

    # Got to configure mode
    handler.sendline('configure')
    try:
        handler.expect(['root#', 'root@.*#'], timeout = longtimeout)
    except:
        print('ERROR: error waiting for ["root#", "root@.*#"] prompt.')
        node_quit(handler)
        return False

    # Start the load mode
    handler.sendline('load set /mnt/juniper.conf')
    try:
        handler.expect('load complete', timeout = longtimeout)
    except:
        print('ERROR: error waiting for "load complete" prompt.')
        node_quit(handler)
        return False

    # Save
    handler.sendline('commit')
    try:
        handler.expect(['root#', 'root@.*#'], timeout = longtimeout)
    except:
        print('ERROR: error waiting for ["root#", "root@.*#"] prompt.')
        node_quit(handler)
        return False

    handler.sendline('exit')
    try:
        handler.expect(['root>', 'root@.*>'], timeout = longtimeout)
    except:
        print('ERROR: error waiting for ["root>", "root@.*>"] prompt.')
        node_quit(handler)
        return False

    return True

def usage():
    print('Usage: %s <standard options>' %(sys.argv[0]));
    print('Standard Options:');
    print('-a <s>    *Action can be:')
    print('           - get: get the startup-configuration and push it to a file')
    print('           - put: put the file as startup-configuration')
    print('-f <s>    *File');
    print('-p <n>    *Console port');
    print('-t <n>     Timeout (default = %i)' %(timeout));
    print('* Mandatory option')

def now():
    # Return current UNIX time in milliseconds
    return int(round(time.time() * 1000))

def main(action, fiename, port):
    try:
        # Connect to the device
        tmp = conntimeout
        while (tmp > 0):
            handler = pexpect.spawn('telnet 127.0.0.1 %i' %(port))
            time.sleep(0.1)
            tmp = tmp - 0.1
            if handler.isalive() == True:
                break

        if (handler.isalive() != True):
            print('ERROR: cannot connect to port "%i".' %(port))
            node_quit(handler)
            sys.exit(1)

        # Login to the device and get a privileged prompt
        rc = node_login(handler)
        if rc != True:
            print('ERROR: failed to login.')
            node_quit(handler)
            sys.exit(1)

        if action == 'get':
            config = config_get(handler)
            if config in [False, None]:
                print('ERROR: failed to retrieve config.')
                node_quit(handler)
                sys.exit(1)

            try:
                fd = open(filename, 'a')
                fd.write(config)
                fd.close()
            except:
                print('ERROR: cannot write config to file.')
                node_quit(handler)
                sys.exit(1)
        elif action == 'put':
            try:
                fd = open(filename, 'r')
                config = fd.read()
                fd.close()
            except:
                print('ERROR: cannot read config from file.')
                node_quit(handler)
                sys.exit(1)

            rc = config_put(handler, config)
            if rc != True:
                print('ERROR: failed to push config.')
                node_quit(handler)
                sys.exit(1)

            # Remove lock file
            lock = '%s/.lock' %(os.path.dirname(filename))

            if os.path.exists(lock):
                os.remove(lock)

            # Mark as configured
            configured = '%s/.configured' %(os.path.dirname(filename))
            if not os.path.exists(configured):
                open(configured, 'a').close()

        node_quit(handler)
        sys.exit(0)

    except Exception as e:
        print('ERROR: got an exception')
        print(type(e))  # the exception instance
        print(e.args)   # arguments stored in .args
        print(e)        # __str__ allows args to be printed directly,
        node_quit(handler)
        return False

if __name__ == "__main__":
    action = None
    filename = None
    port = None

    # Getting parameters from command line
    try:
        opts, args = getopt.getopt(sys.argv[1:], 'a:p:t:f:', ['action=', 'port=', 'timeout=', 'file='])
    except getopt.GetoptError as e:
        usage()
        sys.exit(3)

    for o, a in opts:
        if o in ('-a', '--action'):
            action = a
        elif o in ('-f', '--file'):
            filename = a
        elif o in ('-p', '--port'):
            try:
                port = int(a)
            except:
                port = -1
        elif o in ('-t', '--timeout'):
            try:
                timeout = int(a) * 1000
            except:
                timeout = -1
        else:
            print('ERROR: invalid parameter.')

    # Checking mandatory parameters
    if action == None or port == None or filename == None:
        usage()
        print('ERROR: missing mandatory parameters.')
        sys.exit(1)
    if action not in ['get', 'put']:
        usage()
        print('ERROR: invalid action.')
        sys.exit(1)
    if timeout < 0:
        usage()
        print('ERROR: timeout must be 0 or higher.')
        sys.exit(1)
    if port < 0:
        usage()
        print('ERROR: port must be 32768 or higher.')
        sys.exit(1)
    if action == 'get' and os.path.exists(filename):
        usage()
        print('ERROR: destination file already exists.')
        sys.exit(1)
    if action == 'put' and not os.path.exists(filename):
        usage()
        print('ERROR: source file does not already exist.')
        sys.exit(1)
    if action == 'put':
        try:
            fd = open(filename, 'r')
            config = fd.read()
            fd.close()
        except:
            usage()
            print('ERROR: cannot read from file.')
            sys.exit(1)

    # Backgrounding the script
    end_before = now() + timeout
    p = multiprocessing.Process(target=main, name="Main", args=(action, filename, port))
    p.start()

    while (p.is_alive() and now() < end_before):
        # Waiting for the child process to end
        time.sleep(1)

    if p.is_alive():
        # Timeout occurred
        print('ERROR: timeout occurred.')
        p.terminate()
        sys.exit(127)

    if p.exitcode != 0:
        sys.exit(127)

    sys.exit(0)

Now that we have our python script under /opt/unetlab/scripts/config_vsrx30.py we need to add a line to our vSRX 3.0 template located at /opt/unetlab/html/templates/intel/vsrx30.yml

# Copyright (c) 2016, Andrea Dainese
# Copyright (c) 2016, Alain Degreffe
# Copyright (c) 2017, Alain Degreffe
# Copyright (c) 2019, Christian Scholz
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#     * Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above copyright
#       notice, this list of conditions and the following disclaimer in the
#       documentation and/or other materials provided with the distribution.
#     * Neither the name of the UNetLab Ltd nor  the name of EVE-NG Ltd nor the
#       names of its contributors may be used to endorse or promote products
#       derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---
type: qemu
name: vSRX3-0
config_script: config_vsrx30.py
cpulimit: 1
icon: JuniperSRX.png
cpu: 2
ram: 4096
eth_name:
- fxp0/mgmt
eth_format: ge-0/0/{0}
ethernet: 6
qemu_nic: virtio-net-pci
console: telnet
qemu_arch: x86_64
qemu_options: -machine type=pc-1.0,accel=kvm -cpu qemu64,+ssse3,+sse4.1,+sse4.2,+x2apic,+aes,pclmulqdq  -serial
  mon:stdio -nographic   -nodefconfig -nodefaults -rtc base=utc
...

As you can see, the line “config_script: config_vsrx30.py” has been added, so that the server knows, what script to trigger when using the vSRX3.0 template. After I added this line and saved it, I could immediately export without having to reboot the server or wipe a device 🙂

As you can see, it’s now possible to export your vSRX3.0 configs even with the custom template 🙂 Enjoy!

2 thoughts on “Create a config export script for the vSRX 3.0 template

  1. Simon

    Hi – great tip.

    I came across an issue (of my own creation) – I made my script in windows and this causes an error when running:

    Extract from the unl_wrapper.txt log

    Apr 24 13:39:24 ERROR: Failed to export (80060).
    Apr 24 13:39:24 INFO: exporting /opt/unetlab/scripts/config_vsrx30.py -a get -p 32788 -f /tmp/unl_cfg_20_JV5zaR -t 15
    /usr/bin/env: ‘python3\r’: No such file or directory

    After a bit of a Google I found a similar issue. The python script will have used Windows style line ending characters:
    https://askubuntu.com/a/896880
    “Your file was created or edited on a Windows system and uses Windows/DOS-style line endings (CR+LF), whereas Linux systems like Ubuntu require Unix-style line endings (LF).”

    So I used the app in the post and fixed the script.

    Obviosuly if the script had have been created on box or another Linux system then this would not have been an issue!

    Cheers

    P.S. Is the EVE team going to add your custom template and script into EVE?

    Reply
  2. christianscholz Post author

    Hi SImon,

    thanks a lot for the tip – yes – sometimes WIndows messes with the formatting.
    Maybe they will – there are also custom templates for JATP, Space PE, Space LC and more 😉

    Reply

Leave a Reply to christianscholz Cancel reply

Your email address will not be published. Required fields are marked *

Captcha * Time limit is exhausted. Please reload CAPTCHA.