Finalizing the config

  • by Patrick Ogenstad
  • April 25, 2018

Saving the configuration to flash makes sure that everything keeps working once devices are reloaded. Like a lot of things in this tutorial, there are many ways to solve this problem. All of the configuration commands gets applied in config mode on the Cisco device since you save the configuration in enable mode with “write mem” or “copy run start” you don’t have direct access to those commands from the config mode. There are however workarounds, so you could have this in your template:

do write mem

The downside of a solution like this is that you lose the ability to validate that the connections are correct before saving the configuration.

We can add a basic check by making some changes to /opt/ztp/app/

from app.inventory import devices
from import get_napalm_connection
from app.notifications import notify_slack

def ztp_start(host, file):
    desired = []
    if '__' in file:
        neighbor_host, neighbor_if = file.split('__')
        neighbor_if = neighbor_if.split('.')[0]
        ztp_device = False
        for interface in devices[neighbor_host]['links']:
            if neighbor_if == interface['interface_brief']:
                ztp_device = interface['neighbor']
                upstream_if = interface['interface']
                ztp_interface = interface['neighbor_if']
                ztp_platform = devices[ztp_device]['model']
                mgmt_ip = devices[ztp_device]['management_ip']
                desired = [ztp_interface, neighbor_host,
                           upstream_if, ztp_platform]

        if ztp_device:
            msg = '{}/{} connected {}, expecting {}/{} ({})'.format(
                neighbor_host, neighbor_if, host, ztp_device,
                ztp_interface, ztp_platform)
            msg = "{}/{} connected {}, that doesn't look right...".format(
                neighbor_host, neighbor_if, host)

        host = mgmt_ip

        msg = '{} downloaded {}'.format(host, file)

    dev = get_napalm_connection(host, 'ios')

    if dev:
        notify_slack('{} connection established'.format(host))
        notify_slack('{} connection failed, giving up'.format(host))

    facts = dev.get_facts()

    notify_slack('{}: {}/{}'.format(host, facts['model'],

    success = False
    lldp = dev.get_lldp_neighbors_detail()
    for interface in lldp:
        for neighbor in lldp[interface]:
            notify_slack('{}:{} -> {}: {}'.format(
                host, interface, neighbor['remote_system_name'],
            neighbor_hostname = neighbor['remote_system_name'].split('.')[0]
            actual = [interface, neighbor_hostname,
            if desired == actual:

                dev.cli(['write mem'])
                notify_slack('{} successfully provisioned'.format(ztp_device))
                success = True

    if not success:
        notify_slack('Something is wrong, very wrong')


With these changes, I first try to connect a switch with the wrong model to FastEthernet0/7 in the og-sw-01 switch and the application lets me know that I’ve made an error. When I instead plugin the correct switch it is provisioned correctly and the configuration is written to flash.

Saving the configuartion

That the device is up and running with a good configuration is one thing. You might also want to mark the device as active in your network monitoring solution. Tag the assignment of provisioning the device as done in a reporting system. Basically, tasks which are related to the provisioning but might not be on the top of your head when first envisioning the process. Any manual step needed to provision a device makes it a bit less zero touch.