Juniper ZTP/Homer
Goal
In this Tutorial, we are going to setup a ZTP environment to be able to run the basic configuration on a new Juniper switch. Once the basic configuration is done, we will run homer to deploy the switch configuration. If you want to know more about homer please check: https://wikitech.wikimedia.org/wiki/Homer
Prerequisites
For this tutorial I will be using :
- EVE-NG 2.0.3-112
- VQFX model: vqfx-10000 running JUNOS 19.4R1.10
- DHPC/TFTP VM (IP:10.192.16.5/install1001.dfw.labnet)
- Homer VM(IP:10.192.32.17/homer1001.dfw.labnet)
- Netbox VM(IP:10.192.32.3/netbox1001.dfw.labnet)
- DNS server(IP:10.192.16.2/ns0.dfw.labnet)
All the VM's are running on Proxmox 7.3.4
Diagram
Setup and Configuration
The section is the just the section needed to make ztp work.
Note: We will generate the juniper_ztp file using later on a python script
DHCP configuration
####################################################################### # ZTP Sample Config begins here: ####################################################################### option option-150 code 150 = ip-address; set vendor-string = option vendor-class-identifier; option space ZTP; option ZTP.image-file-name code 0 = text; option ZTP.config-file-name code 1 = text; option ZTP.image-file-type code 2 = text; option ZTP.transfer-mode code 3 = text; option ZTP.alt-imagefile-name code 4 = text; option ZTP-encapsulation code 43 = encapsulate ZTP; # mgmt nework subnet 10.193.0.0 netmask 255.255.255.0 { authoritative; option subnet-mask 255.255.255.0; option option-150 10.192.16.5; #TFTP server option routers 10.193.0.1; option broadcast-address 10.193.0.255; option domain-name "mgmt.dfw.labnet"; default-lease-time 600; max-lease-time 7200; } group { vendor-option-space ZTP; include "/etc/dhcp/juniper_ztp"; }
DNS entries for the 4 switches
$ORIGIN 0.193.10.in-addr.arpa. --- 101 1H IN PTR lsw1-a1-dfw.mgmt.dfw.labnet. 102 1H IN PTR lsw1-a2-dfw.mgmt.dfw.labnet. 103 1H IN PTR lsw1-a3-dfw.mgmt.dfw.labnet. 104 1H IN PTR lsw1-a4-dfw.mgmt.dfw.labnet. $ORIGIN mgmt.dfw.labnet. --- lsw1-a1-dfw 1H IN A 10.193.0.101 lsw1-a2-dfw 1H IN A 10.193.0.102 lsw1-a3-dfw 1H IN A 10.193.0.103 lsw1-a4-dfw 1H IN A 10.193.0.104
Script
This Python scrip will read data from a csv file and generate the dhcp_hosts = juniper_ztp file using a template. I am running the script on my homer VM in a NFS directory (see below). The reason i am running it in a NFS directory is to create a symbolic link from the directory to the "/etc/dhcp/juniper_ztp" file mentioned in the dhcpd.conf file.
ppaul@homer1001:/nfs/general/ztp$ ls -l total 12 -rw-r--r-- 1 nobody nogroup 281 Feb 5 21:34 dhcp_template -rw-r--r-- 1 nobody nogroup 738 Feb 5 21:36 generate_host.py -rw-r--r-- 1 nobody nogroup 224 Feb 5 23:31 ztp_host.csv ppaul@homer1001:/nfs/general/ztp$
ppaul@install1001:/etc/dhcp$ ls -la
total 72
drwxr-xr-x 5 root root 4096 Feb 5 23:13 .
drwxr-xr-x 82 root root 4096 Feb 5 23:05 ..
-rw-r--r-- 1 root root 1426 May 26 2021 debug
-rw-r--r-- 1 root root 1735 May 26 2021 dhclient.conf
drwxr-xr-x 2 root root 4096 Nov 3 23:31 dhclient-enter-hooks.d
drwxr-xr-x 2 root root 4096 Nov 3 23:32 dhclient-exit-hooks.d
-rw-r--r-- 1 root root 3331 Oct 4 15:52 dhcpd6.conf
-rw-r--r-- 1 root root 6088 Feb 5 01:07 dhcpd.conf
lrwxrwxrwx 1 root root 27 Feb 5 23:13 juniper_ztp -> /nfs/general/ztp/dhcp_hosts
-rw-r--r-- 1 root root 18869 Jan 28 14:29 linux-host-entries
-rw-r--r-- 1 root root 794 Nov 3 23:38 linux-vm-entries
- cvs file :ztp_host.csv
In a real world, working with real switches you can get the mgmt interface MAC directly from the sticker on the box the switch came in. You don't have to power on and connect to the switch before getting the information like I did with the VQFX. For the mgmt IP address and the host name you can get those information once the switch is already in Netbox and has an IP.
hostname,mgmt_ip,cidr_bits,mac_address lsw1-a1-dfw,10.193.0.101,24,50:00:00:0e:00:00 lsw1-a2-dfw,10.193.0.102,24,50:00:00:16:00:00 lsw1-a3-dfw,10.193.0.103,24,50:00:00:17:00:00 lsw1-a4-dfw,10.193.0.104,24,50:00:00:18:00:00
- template: dhcp_template
host {{ hostname }} { hardware ethernet {{ mac_address }}; fixed-address {{ mgmt_ip }}; option option-150 10.192.16.5; option host-name "{{ hostname }}"; option ZTP.config-file-name "junos_script.sh"; }
- python script
#!/usr/bin/python3 # Import modules import csv import sys from jinja2 import Template # Set variable to your host file path. dhcp_file="/nfs/general/ztp/dhcp_hosts" # Set this variable to the configuration folder conf_path="/nfs/general/ztp/" # csv file name csv_file="/nfs/general/ztp/ztp_host.csv" # Read the cvs file and stores the colum headings as a keys device_data = csv.DictReader(open(csv_file)) # Loops through the device_data csv so we can perform actions for each row for row in device_data: data = row with open("dhcp_template") as t_dhcp: t_format = t_dhcp.read() template = Template(t_format) open_t = open(dhcp_file, 'a') print (open_t) open_t.write((template.render(data))) open_t.close()
Running the script
In the ztp directory on the homer VM we have just three(3) files. Now we are going to run the "generate_host.py" script; this should have a fourth file called "dhch_hosts"
ppaul@homer1001:/nfs/general/ztp$ sudo python3 generate_host.py <_io.TextIOWrapper name='/nfs/general/ztp/dhcp_hosts' mode='a' encoding='UTF-8'> <_io.TextIOWrapper name='/nfs/general/ztp/dhcp_hosts' mode='a' encoding='UTF-8'> <_io.TextIOWrapper name='/nfs/general/ztp/dhcp_hosts' mode='a' encoding='UTF-8'> <_io.TextIOWrapper name='/nfs/general/ztp/dhcp_hosts' mode='a' encoding='UTF-8'> ppaul@homer1001:/nfs/general/ztp$
dhcp_hosts contents
# Host lsw1-a1-dfw added by ztp script host lsw1-a1-dfw { hardware ethernet 50:00:00:0e:00:00; fixed-address 10.193.0.101; option option-150 "10.192.16.5"; option host-name "lsw1-a1-dfw"; option ZTP.config-file-name "junos_script.sh"; } # End lsw1-a1-dfw section# Host lsw1-a2-dfw added by ztp script host lsw1-a2-dfw { hardware ethernet 50:00:00:16:00:00; fixed-address 10.193.0.102; option option-150 "10.192.16.5"; option host-name "lsw1-a2-dfw"; option ZTP.config-file-name "junos_script.sh"; } # End lsw1-a2-dfw section# Host lsw1-a3-dfw added by ztp script host lsw1-a3-dfw { hardware ethernet 50:00:00:17:00:00; fixed-address 10.193.0.103; option option-150 "10.192.16.5"; option host-name "lsw1-a3-dfw"; option ZTP.config-file-name "junos_script.sh"; } # End lsw1-a3-dfw section# Host lsw1-a4-dfw added by ztp script host lsw1-a4-dfw { hardware ethernet 50:00:00:18:00:00; fixed-address 10.193.0.104; option option-150 "10.192.16.5"; option host-name "lsw1-a4-dfw"; option ZTP.config-file-name "junos_script.sh"; } # End lsw1-a4-dfw section
Switch script
We can see in the DHCP file we have the "option ZTP.config-file-name" pointing to the script "junos_script.sh". You can fin the scirpt below
#!/bin/sh # Define some variables KEY='"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC4xpjWJoQhCf176i77ni9//mcYO3bBWu7necWZwJNVkFsvvT6XuWfkKVMUFnTjTMr1erv8WRDze7le9Jl2a/xMIgo9Cf71SU9faPbd /ukvaLl5VUeGvHKFg9d+7GUGx1z9K1qKY2VOBO5EQCht8+4o4mMaizoXoxHvkNolswAa5Jv/EPwnfCeDyV7TsG+Se1k7/1h1VFOwW7Dbxno1aCnMDYbcfiBnzGnLSZQGjehok6cqYTjsNIIdAiZYSpH77pnAGglFhxNUSlqj0qRIJZdG3nhPlvIRPjn7fouq3BJEmiWPP8ru67H1J2mdSkix4xOxdUWfGB9eJlENfnobJjBr ppaul@U18"' root_passwd='"$6$qspxq1Js$f0UFEnzRo7oFEOZ3URiSBVBinJvwYSv4qlZpQ9eKlhc1YlRG3RCCVyqtXN.92mB8P3ilTUK1h.7WOSRjY1z9i."' em0_IP=$(cli -c "show interfaces em0 brief | match inet | trim 10 ") # Mgmt interface cli -c "configure; \ set interfaces em0 unit 0 family inet address $em0_IP ; \ delete interfaces em0 unit 0 family inet dhcp ; \ commit and-quit " # Routing options cli -c "configure; \ set routing-options static route 0.0.0.0/0 next-hop 10.193.0.1 ; \ commit and-quit " # Edit system cli -c "configure; \ set system root-authentication encrypted-password $root_passwd ; \ set system login user homer uid 2002 ; \ set system login user homer class super-user ; \ set system login user homer authentication ssh-rsa $KEY ; \ set system services ssh ; \ set system services netconf ; \ commit and-quit "
How it works?
Part 1
Steps 1 is not needed if you are working on a real switch. On the VQFX, you need to manually setup ztp
1- setup ztp (set chassis auto-image-upgrade) commit
2- switch starts the ztp process
3- switch send a dhcp request to the dhcp server
4- switch receive an IP address from the dhcp server
5- switch check if there is a configuration file and an image file
6- if file(s) exist switch download the file(s)
In our case, the switch will only download the script. We are using a script here and not a configuration file because a configuration file will over write the existing switch configuration which we don't want to.
7- Execution of the script
Part 2
1- The script setup the mgmt interface(em0) IP address and delete the dhcp option on the interface
2- The script setup the net-hop for the mgmt netowrk
3- The script setup the root password
4- The script setup the homer user and add the the ssh-key (needed to run homer)
5- The script setup the ssh and netconf service
6- END
At this point we can use now homer to perform the other switch configuration.
ZTP in action
- Set up ztp on the swith
{master:0}[edit] root@vqfx-re# set chassis auto-image-upgrade
root@vqfx-re# show | compare [edit] + chassis { + auto-image-upgrade; + }
{master:0}[edit] root@vqfx-re# show interfaces Auto Image Upgrade: DHCP Client Bound interfaces: Auto Image Upgrade: DHCP Client Unbound interfaces: xe-0/0/0.0 xe-0/0/1.0 xe-0/0/2.0 xe-0/0/3.0 xe-0/0/4.0 xe-0/0/5.0 xe-0/0/6.0 xe-0/0/7.0 xe-0/0/8.0 xe-0/0/9.0 xe-0/0/10.0 xe-0/0/11.0 em0.0 Auto Image Upgrade: To stop, on CLI apply "delete chassis auto-image-upgrade" and commit Auto Image Upgrade: No DHCP Client in bound state, reset all DHCP clients Auto Image Upgrade: DHCP Client State Reset: xe-0/0/0.0 xe-0/0/1.0 xe-0/0/2.0 xe-0/0/3.0 xe-0/0/4.0 xe-0/0/5.0 xe-0/0/6.0 xe-0/0/7.0 xe-0/0/8.0 xe-0/0/9.0 xe-0/0/10.0 xe-0/0/11.0 em0.0
Auto Image Upgrade: DHCP Options for client interface em0.0 ConfigFile: junos_script.sh Gateway: 10.193.0.1 DHCP Server: 10.192.16.5 File Server: 10.192.16.5 Options state: Partial Options::Config File set,Image File not set,File Server set Auto Image Upgrade: Active on client interface: em0.0 error: remote side unexpectedly closed connection root@vqfx-re:RE:0%
Auto Image Upgrade: Interface:: "em0" Auto Image Upgrade: Server:: "10.192.16.5" Auto Image Upgrade: Image File:: "NOT SPECIFIED" Auto Image Upgrade: Config File:: "junos_script.sh" Auto Image Upgrade: Gateway:: "10.193.0.1" Auto Image Upgrade: Protocol:: "tftp" Auto Image Upgrade: Start fetching junos_script.sh file from server 10.192.16.5 through em0 using tftp Auto Image Upgrade: File junos_script.sh fetched from server 10.192.16.5 throug h em0 Auto Image Upgrade: Executing script junos_script.sh Auto Image Upgrade: junos_script.sh executed successfully. See /var/log/script_ output Broadcast Message from root@lsw1-a2-dfw (no tty) at 16:19 UTC... Auto image Upgrade: Stopped
Please see below for the script log
root@vqfx-re:RE:0% cat /var/log/script_output Entering configuration mode configuration check succeeds commit complete Exiting configuration mode Entering configuration mode configuration check succeeds commit complete Exiting configuration mode Entering configuration mode configuration check succeeds commit complete Exiting configuration mode
We see in the output below that the homer user, the mgmt interface and the ssh and netconf services are set.
{master:0}[edit] root@lsw1-a2-dfw# show system login user homer uid 2002; class super-user; authentication { ssh-rsa "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC4xpjWJoQhCf176i77ni9//mcYO3bBWu7necWZwJNVkFsvvT6XuWfkKVMUFnTjTMr1erv8WRDze7le9Jl2a/xMIgo9Cf71SU9faPbd /ukvaLl5VUeGvHKFg9d+7GUGx1z9K1qKY2VOBO5EQCht8+4o4mMaizoXoxHvkNolswAa5Jv/EPwnfCeDyV7TsG+Se1k7/1h1VFOwW7Dbxno1aCnMDYbcfiBnzGnLSZQGjehok6cqYTjsNIIdAiZYSpH77pnAGglFhxNUSlqj0qRIJZdG3nhPlvIRPjn7fouq3BJEmiWPP8ru67H1J2mdSkix4xOxdUWfGB9eJlENfnobJjBr papaul@U18"; ## SECRET-DATA }
{master:0}[edit] root@lsw1-a2-dfw# show interfaces em0 unit 0 { family inet { address 10.193.0.102/24; } }
{master:0}[edit] root@lsw1-a2-dfw# show system services ssh { root-login allow; } netconf { ssh; }
Now the switch is ready.We can run homer on it
Running homer
Before running homer make sure that the switch is in Netbox and that it has it's DNS Name set.
Below is the output of the changes after running homer with diff
ppaul@homer1001:~$ homer lsw1-a2* diff /usr/local/lib/python3.9/dist-packages/paramiko/transport.py:219: CryptographyDeprecationWarning: Blowfish has been deprecated "class": algorithms.Blowfish, INFO:homer.devices:Initialized 46 devices INFO:homer:Generating diff for query lsw1-a2* INFO:homer:Gathering global Netbox data INFO:homer.devices:Matched 1 device(s) for query 'lsw1-a2*' INFO:homer:Generating configuration for lsw1-a2-dfw.mgmt.dfw.labnet INFO:homer.transports.junos:Running commit check on lsw1-a2-dfw.mgmt.dfw.labnet Changes for 1 devices: ['lsw1-a2-dfw.mgmt.dfw.labnet']
[edit system root-authentication] - encrypted-password "$6$qspxq1Js$f0UFEnzRo7oFEOZ3URiSBVBinJvwYSv4qlZpQ9eKlhc1YlRG3RCCVyqtXN.92mB8P3ilTUK1h.7WOSRjY1z9i."; ## SECRET-DATA + encrypted-password "$5$bSgF2gnxBS/rA$sYP/f1pWJhl5d1VN0hHzjxd0jZhmnwGLCiwVm3hE8Z."; ## SECRET-DATA [edit system root-authentication] - ssh-rsa "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key"; ## SECRET-DATA [edit system login] + class operations { + permissions [ admin-control clear configure field interface-control maintenance network rollback secret shell trace-control view view-configuration ]; + } + class rancid { + permissions [ view view-configuration ]; + } [edit system login] + user ppaul { + uid 2001; + class super-user; + authentication { + ssh-rsa "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCry+y+fh4Yjz/yu+wfUtWl80z0Cd2bHcjEbGSg3kf0bkjmZK9uDRrHrxP /bmMjhqHNQCPwSIi6eRrvddPJxi8yP5Qmr9T2rnE8UxacmjXhGPTL+2GpoDYp5ybGuRhZdJG07EyiXiAFu6jSe9okZdhapRaAbhy/wMr1weZ/rrJwOGar7AKk+XCi4fwahh6vw9b++EEtGqRNzIDMw10nBl/kCc0141hHQ8ZhmvFdzNh2LQt2tvYwu0T/YmENcDs3tb7Us2hd4fw6KXIsqNLqxWLRjWgWj5J5zQpOzHgP/TM1X25ZxUlRh6jttcBsGSfaddzFwISK2qNbP+XvqJkR0g5J tpapaul@Macbooks-MacBook-Air.local"; ## SECRET-DATA + } + } - user vagrant { - uid 2000; - class super-user; - authentication { - ssh-rsa "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key"; ## SECRET-DATA - } - } [edit system] + domain-name mgmt.dfw.labnet; + backup-router 10.193.0.1; + time-zone UTC; + /* Prefer the primary (loopback) address for locally generated packets causing issue on vqfx between fpc and RE */ + no-redirects; [edit interfaces] - et-0/0/0 { - unit 0 { - family inet { - dhcp { - vendor-id Juniper-qfx10002-72q; - } - } - } - } - xe-0/0/0 { - unit 0 { - family inet { - dhcp { - vendor-id Juniper-qfx10002-72q; - } ----------- ----------- [edit] + snmp { + location dfw; + filter-interfaces { + interfaces { + "^.*\.0$"; + } + } + community passwordnotGood { + authorization read-only; + } [edit routing-options static route 0.0.0.0/0] + no-readvertise; [edit protocols] + lldp { + port-id-subtype interface-name; + interface all; + } [edit vlans] - default { - vlan-id 1; - } + private1-a-dfw { + vlan-id 20; + } + private1-b-dfw { + vlan-id 30; + } + private1-c-dfw { + vlan-id 40; + } + private1-d-dfw { + vlan-id 50; + }
tcpdump on dhcp server
ppaul@install1001:~$ sudo tcpdump -vvv 'udp and (src port 67 or src port 68 or src port 69)' tcpdump: listening on ens18, link-type EN10MB (Ethernet), snapshot length 262144 bytes 19:59:25.102231 IP (tos 0x0, ttl 64, id 30167, offset 0, flags [none], proto UDP (17), length 293) 10.193.0.1.bootps > install1001.dfw.labnet.bootps: [udp sum ok] BOOTP/DHCP, Request from 50:00:00:17:00:00 (oui Unknown), length 265, hops 1, xid 0x2d895b7c, Flags [Broadcast] (0x8000) Gateway-IP 10.193.0.1 Client-Ethernet-Address 50:00:00:17:00:00 (oui Unknown) Vendor-rfc1048 Extensions Magic Cookie 0x63825363 DHCP-Message (53), length 1: Discover Lease-Time (51), length 4: 86400 Hostname (12), length 12: "VM5F3D5FF6E7" END (255), length 0 PAD (0), length 0 19:59:25.122924 IP (tos 0x0, ttl 64, id 14907, offset 0, flags [DF], proto UDP (17), length 367) install1001.dfw.labnet.bootps > 10.193.0.1.bootps: [bad udp cksum 0x26f3 -> 0xb12a!] BOOTP/DHCP, Reply, length 339, hops 1, xid 0x2d895b7c, Flags [Broadcast] (0x8000) Your-IP lsw1-a3-dfw.mgmt.dfw.labnet Gateway-IP 10.193.0.1 Client-Ethernet-Address 50:00:00:17:00:00 (oui Unknown) file "nonexistent-file" Vendor-rfc1048 Extensions Magic Cookie 0x63825363 DHCP-Message (53), length 1: Offer Server-ID (54), length 4: install1001.dfw.labnet Lease-Time (51), length 4: 7200 Subnet-Mask (1), length 4: 255.255.255.0 Default-Gateway (3), length 4: 10.193.0.1 Domain-Name-Server (6), length 8: ns0.dfw.labnet,ns1.tx.labnet Hostname (12), length 11: "lsw1-a3-dfw" TFTP-Server-Address (150), length 4: install1001.dfw.labnet BR (28), length 4: 10.193.0.255 Domain-Name (15), length 15: "mgmt.dfw.labnet" Vendor-Option (43), length 17: 1.15.106.117.110.111.115.95.115.99.114.105.112.116.46.115.104 END (255), length 0