How to login to Cisco NXOS using ssh public key

Hi, welcome to my blog. It has been a while since my last post. Nevertheless, in this post I will be showing you how to login to Cisco NXOS device using public key.

For this post, my topology looks like this:

The CentOS7 VM is in 192.168.198.132, and the NXOS mgmt interface is in 192.168.198.131. Both can ping the other with no problem. The switch in the middle is the GNS3 built-in switch.

OK, lets get started.

First, create your private and public key pair. I am using CentOS 7, and the command to generate private and public keys is ssh-keygen

[meru@localhost ~]$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/meru/.ssh/id_rsa):
/home/meru/.ssh/id_rsa already exists.
Overwrite (y/n)? y
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/meru/.ssh/id_rsa.
Your public key has been saved in /home/meru/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:SF7Yjys4maUAP/OXBdRtZEdkuUuJmrYjW9xs8X3ymjM meru@localhost.localdomain
The key's randomart image is:
+---[RSA 2048]----+
|      .. ooo=.   |
|     . o..oo.    |
|.     + o. . o   |
| o   o + o. +    |
|  =   + Soo. .   |
|   = * +++ o..   |
|    B +.+.+ . o .|
|     o.oo.    E= |
|      .o .    o+.|
+----[SHA256]-----+
[meru@localhost ~]$

If you see the output above, all I did was type in ssh-keygen and press enter. It will ask you where you want to save the private and public keys. The default location is /home/your-username/.ssh/.

Because I already created public and private keys pair when testing this method, the system asks me if I want to overwrite the existing keys, which I answered with a resounding "y". I left the passphrase empty to keep things simple, and press enter. Once this is done, the location of your private and public key are shown. If I do an ls, then the output looks like this:

[meru@localhost .ssh]$ pwd
/home/meru/.ssh
[meru@localhost .ssh]$ ls
id_rsa  id_rsa.pub  known_hosts
[meru@localhost .ssh]$

The private key looks like this:

[meru@localhost .ssh]$ cat id_rsa
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAxD82WOeqGnHctGwg3RW87NSVT8UnzkjkaNKELZ8rWStdvaRY
Iw1BcSMlVX/D9TIl7WreqYw7rPWutboPIKUV0fNATwjkykL+OqcPc5ZnAwVJYr5e
te2hPel+S4g5MxuMlkhBCgwrSDsyKriO38/iuv8WtTsT7CSCvWDdrAdy9r/mkD6P
OfXbBPgNvcP7cEj2dNj4BhEonDWTsm8v2SLdjEnhOTd5+wxpfrQE4+XnZIsKpYAT
qDtVEY3Sh+XsJnf/UTbfurVOCR6AfZhaHypHh93eP4DcD8giTS6uKW19eemNCEy/
OVkQfMFd2VvjVE+/PbGgZLnR4XKciBlrHZG8hQIDAQABAoIBAAqxcxGnFeK4/5Cp
L6U8UP4Km1QsZv3bhO6dQeyGdsM2FLaHpPHMABvdEWS2QIQ0xlJId3d/l6TWAj4O
tFJQDH9TJNbpoiXPPrENd08SC+zjNdt2igyZtSZ9eY8+DffV8XQi3cBhZ15ZM2OW
iNpzJmSQD7myTWSZnLIa/Bkqkr4VIFLkAtup84YTPxuBz8lxCtgrHk2HIz7MTqK2
EAqgxrAzgmEdH+huH8NuDKlTQnuCeTh6YFuyUR6om7MMvUJd6aOkokaXXdTW1V0W
nhFy2CGii1fYWDkjlBwR6anGo3X+1eiht/FD12v/G5g68CaayZyFMp3FTaLndijY
2s1d8SUCgYEA68C5hI85Ur3ZJvwX8kX6eOcIau4Ep1u+mMd4t7MkB17rtOAk6hmD
xw7ZDN66ZckQti7eNgjeF95zqFDnFFGc0lnrV2f7Vki9Hd5fdsmO1zXK7QiqGNv7
dXRMPHjJW4IP9I/Ed8bpaezLDN+UnOI/ChqH3VmdhdC8fHkoz8XsFE8CgYEA1Rno
kD8AjSaXLojUKQxsuPf89twoQ79iu52Pis9D6/jgrTMKmmEjHA2DEnJJIKESnakz
ntFtGGzUvqJ/NxkXZMWGCUZZlobpAWr2a+V+E5fHT/Vo2+Z2M26RmTZ2ToNVsge4
oLNUbnEZHVXuiIDowvOA/0ObXzD22wAHEgr1aOsCgYBdoCVNEsA2LvVNeTJdYhMt
7rq2FJ6+hD32WMmFUtyNHNJI+/dNDSL7a9chOFcYUziS0ZwoI29oVUzjXHXY6Ox5
0YIYLVBwnOSWNxj8iOF0cIWNizJaOwrrQ2t/XIzDDa0ed/YTh2htS7/79dMEVwmo
Do9EhfF8EkC4wjlGAplWGwKBgQCkmMplAASxAamsK8uTmSExDXq6QfpnuZ/NQjnp
AYZ48Va48L+z1muD83vV96g8J611UYepmCEnyIfaZzDTofHLyDVrXIc+GR5IFhIX
7L6g647rV6aSk0OhHjofmU1v/0llpUkZ9um0FG9NlV4U2Vzvl51/jPypXeyN40uS
yBwd2QKBgQDRVlQKvO3MIcQLAiqBKx+270W0W+qiNS/cwj0+zkaKj04yOiNBopPX
vBrfzsSWN7h69SgnOpkSGx3bQeNlLYZ9AMkw/TFnK/ZAYPyRTI7dJ/HnF1rXxsbO
RzpxdZsP7yOtqWotixrEfSHqlLn+kDzXj/7cSjiFDrwhoSTUMpNrkg==
-----END RSA PRIVATE KEY-----
[meru@localhost .ssh]$

And the public key looks like this:

[meru@localhost .ssh]$ cat id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDEPzZY56oacdy0bCDdFbzs1JVPxSfOSORo0oQtnytZK129pFgjDUFxIyVVf8P1MiXtat6pjDus9a61ug8gpRXR80BPCOTKQv46pw9zlmcDBUlivl617aE96X5LiDkzG4yWSEEKDCtIOzIquI7fz+K6/xa1OxPsJIK9YN2sB3L2v+aQPo859dsE+A29w/twSPZ02PgGESicNZOyby/ZIt2MSeE5N3n7DGl+tATj5edkiwqlgBOoO1URjdKH5ewmd/9RNt+6tU4JHoB9mFofKkeH3d4/gNwPyCJNLq4pbX156Y0ITL85WRB8wV3ZW+NUT789saBkudHhcpyIGWsdkbyF meru@localhost.localdomain
[meru@localhost .ssh]$

OK, now we have done creating our public and private key pairs. In a production environment, you will want to keep the private key somewhere safe, and have a backup of it somewhere. Do not share this with others. The public key is the one that we want to put in to the Cisco NXOS device to allow us to login using public key.

In my previous post, I have shown you how to login to Cisco IOS device using public key, where you have to manually copy and paste the public key from your Centos machine to the IOS device. With NXOS, the copy and paste is done using copy scp command. No more manual copy-and-paste.

Here is how its done:

1. login to your NXOS device
2. use the scp command. Here is how I do it:

NXOS# 
NXOS# copy scp://meru@192.168.198.132/home/meru/.ssh/id_rsa.pub bootflash:
Enter vrf (If no input, current vrf 'default' is considered): management
meru@192.168.198.132's password: 
id_rsa.pub                                    100%  408     0.4KB/s   00:00    
Copy complete, now saving to disk (please wait)...
NXOS# 

The output above shows the full copy command. I will try and break it down for you, and give a brief explanation:

copy –> this part is where we instruct the NXOS to do a copy command

scp –> this part is telling the NXOS to use the SCP method to copy the file

://meru@192.168.198.132 –> this part is informing the NXOS to login to the Centos 7 machine with the ip address of 192.168.198.132, using username meru

/home/meru/.ssh/id_rsa.pub –> this part is informing NXOS the public key’s location

bootflash: –> this part instructs the NXOS to copy the file to its bootflash directory

It takes zero seconds to copy the id_rsa.pub file from my Centos 7 machine to the NXOS. Now lets take a look if we have the file inside NXOS bootflash:

NXOS# dir | i id_rsa
        408    Aug 16 16:09:33 2020  id_rsa.pub

Yup, there it is. We have successfully copy our public key into the NXOS bootflash directory. Now to configure the NXOS to use public key everytime the user meru try to login. Here is how you do it:

1. lets create the username first, if you havent already.
NXOS(config)# username meru role network-admin password meru

2. configure that username to use sshkey file we just copied over.
NXOS(config)# username meru sshkey file bootflash:///id_rsa.pub

note: make sure you have created the username BEFORE you tell NXOS to use sshkey for that username, or NXOS will pop an error saying the username does not exists.

Lets verify the config:

NXOS# show run | sec username.meru
username meru password 5 $5$eQmmvOP3$kDQhGJqyBefA1cwoUTb5VMguZWSdjhfKXR.VqTp42E7
  role network-admin
username meru sshkey ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDEPzZY56oacdy0bCDdFbz
s1JVPxSfOSORo0oQtnytZK129pFgjDUFxIyVVf8P1MiXtat6pjDus9a61ug8gpRXR80BPCOTKQv46pw9
zlmcDBUlivl617aE96X5LiDkzG4yWSEEKDCtIOzIquI7fz+K6/xa1OxPsJIK9YN2sB3L2v+aQPo859ds
E+A29w/twSPZ02PgGESicNZOyby/ZIt2MSeE5N3n7DGl+tATj5edkiwqlgBOoO1URjdKH5ewmd/9RNt+
6tU4JHoB9mFofKkeH3d4/gNwPyCJNLq4pbX156Y0ITL85WRB8wV3ZW+NUT789saBkudHhcpyIGWsdkby
F meru@localhost.localdomain
username meru passphrase  lifetime 99999 warntime 14 gracetime 3
NXOS# 

Yep, its there. That part where is says "username meru sshkey ssh-rsa AAAAB3Nza..... is telling us that the username meru has been configured to login using sshkey.

Now lets see if it works:

[meru@localhost .ssh]$ ssh meru@192.168.198.131
User Access Verification

Cisco NX-OS Software
Copyright (c) 2002-2016, Cisco Systems, Inc. All rights reserved.
NX-OSv software ("NX-OSv Software") and related documentation,
files or other reference materials ("Documentation") are
the proprietary property and confidential information of Cisco
Systems, Inc. ("Cisco") and are protected, without limitation,
pursuant to United States and International copyright and trademark
laws in the applicable jurisdiction which provide civil and criminal
penalties for copying or distribution without Cisco's authorization.

Any use or disclosure, in whole or in part, of the NX-OSv Software
or Documentation to any third party for any purposes is expressly
prohibited except as otherwise authorized by Cisco in writing.
The copyrights to certain works contained herein are owned by other
third parties and are used and distributed under license. Some parts
of this software may be covered under the GNU Public License or the
GNU Lesser General Public License. A copy of each such license is
available at
http://www.gnu.org/licenses/gpl.html and
http://www.gnu.org/licenses/lgpl.html
***************************************************************************
*  NX-OSv is strictly limited to use for evaluation, demonstration and    *
*  NX-OS education. NX-OSv is provided as-is and is not supported by      *
*  Cisco's Technical Advisory Center. Any use or disclosure, in whole or  *
*  in part of the NX-OSv Software or Documentation to any third party for *
*  any purposes is expressly prohibited except as otherwise authorized by *
*  Cisco in writing.                                                      *
***************************************************************************
NXOS#
	

Awesome! You can see that as soon as I entered the command ssh meru@192.168.168.131", the prompt just went straight into privilege mode, not asking for a password anymore.

That is it for today. I hope this helps 🙂 See you on my next post!

Credits to NetworkEvolutions for showing me how to do this. You can find their Youtube video related to this topic in this link here: https://youtu.be/yvPMjfS_uCM

`

Ansible – How to login to Cisco devices using ssh key

Hi, my name is Meru, and it has been a very long time since my last post.

I have just upgraded my GNS3 to version 2.2.3. And the Ansible controller that I used throughout my previous posts, somehow, after many moons of abandonment, decided to broke, and I have to rebuild a new one. But I try to keep the topology similar to the previous one I use.

The Ansible machine is still running on CENTOS 7, but I am using Ansible 2.9.1 (the latest version at the time of this writing). The two routers are still on IOSv15.5(3)M, and the switch in the middle is still using GNS3’s built-in switch, all interface are vlan 1.

ANSIBLE VM eth1 is 192.168.172.10 /24

R1 g0/0 is 192.168.172.11 /24

R2 g0/0 is 192.168.172.12 /24

Alright,

So, on my previous post, we hard-coded the credentials use to login to the router in ‘routers.yml’ file inside the ansible groups_vars directory.

[root@localhost ~]# cd /etc/ansible/group_vars
[root@localhost ~]# vi routers.yml
---

ansible_ssh_user: meru
ansible_ssh_pass: meru

To avoid hard-coding any username and password, lets now create an ssh key, and use it to login to the router

We will first generate the private and public key in the Ansible controller:

[root@localhost ~]# ssh-keygen 
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa): 
/root/.ssh/id_rsa already exists.
Overwrite (y/n)? y
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /root/.ssh/id_rsa.
Your public key has been saved in /root/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:MV564yizP9etqRqPCWs2GRjn8WDJgQhX4ZINexAxyBA root@localhost.localdomain
The key's randomart image is:
+---[RSA 2048]----+
|E+B=oo           |
|.o.O. .          |
|  + +. oo .      |
|   o. B. =       |
|     * +S o      |
|    . o .+ .     |
|      +oo .. .   |
|      =*.=. ...  |
|     ooo=+o.o.   |
+----[SHA256]-----+
[root@localhost ~]#

The output above shows that we run the command ssh-keygen in Ansible controller. Then, it says that there is an existing key already in the directory, and it asks we we want to overwrite.

Before I write this blog, I have already created the rsa key as a test, but lets overwrite the existing anyway.

It then asks for a passphrase, which we will leave it as blank (just press enter twice). The key has been generated. Lets take a look at the content of the id_rsa directory:

[root@localhost ~]# cd /root/.ssh/ 
[root@localhost .ssh]# ll
total 12
-rw-------. 1 root root 1679 Dec 17 23:23 id_rsa
-rw-r--r--. 1 root root  408 Dec 17 23:23 id_rsa.pub
-rw-r--r--. 1 root root  229 Dec  9 10:18 known_hosts
[root@localhost .ssh]#

The file called id_rsa is our private key. This key needs to be present in our Ansible machine, and shall not be shared anywhere. Lets take a look at the content of the private key:

[root@localhost ~]# cat /root/.ssh/id_rsa
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAqRIVL3pl3wstRL9J+/RC0syeFqN/84KPiTM9RXrUXTJThih9
TX6saLugggMpa6l3O4ybvRcI/zdIdi0J88jwHgyVd2pdMyfk5TLYvjPei/FGkWPe
tHlcKlRdTo12Kd++35VG2xz/Ql1NG6jybdLiQXbdxaFP0n9zLfxNFldrMvedE9c/
hqk1tPsuxdrc2fymEaf8rsfa6bBkCTYsJCxjBvZeYuxjSx9nTtIenQ8I6Bd9Djxj
hlvKo6hVXMGK8xv52YM7CLTXuNrk1UKHcT1GVxMxrTKxfSq1UkxURXAFVFSZkPKc
igD9JMxUw3a6VnhRL38CnJiAh9FYOzNzbj4SEQIDAQABAoIBAQCjtwvmtqoQqj0C
lgzpOiS/BT6MA3Sx8xpUq9ZIAmHDgSkZ/vke4mvG2vDZFIC2bRo1AroIB1dB82Fq
dcBuXYQORPcy9D8deyMNwgfZXlbAwjkoLkIIFoBlyN21ZAwpDi1BScacBF80/y3c
e+OM7ykCZTzo04R8+8cnn2lyGeKHLWTWMFYwYmBBp2qBRMFRJl9Te0AGA6tfL4Kv
0fSi87PadgN76awqktJOolkDIaFl425+39QpZsbi4Yz7X42ld9apgsIBhDkKv5rx
mZh6JN2OiKFsGb4MmLPj1kwlwUugflAPutDC/GKZIkoHvIa9urK8X/Jxr/+q0wTQ
AB0ApCH1AoGBAOAxifVaSsM+gUd/c0/9v/+nMlDmdYuonwpFaZa8PTejhmvzpbLE
W57xgWZVXiHMiz6Y9xl/+k80h9J+89YkVV+enlK4gRwd2HozSqCQZVFFPI7ClpPq
rD9Q/JgNpMwVzws1HMEk7fmw86XsD1UBEtDOrIbJLjZMFI23ghq4GTHnAoGBAMEO
irOQ58byOUMrks/u/fr4+kUNYV7yaOiXIu3Rp7+vSNuntfZdmR6ugBu9YS5l3tWB
RTp/SpSDwk5t2b1Sxw3B8y12ajzVftNVadll1plcFkS6cIH12QSF8qCMeB04l42u
TGGZDSp4l0BSkiqHgTaHKIh6VMeGdsyPQBnMc41HAoGBAJ1d6IqSMHxP+YroSVbS
tNyMzeK/ga7gU5JwGqe1xfqyC/7mbV7IOc+dkcj1DzgGTY9I69m4XUkPhG8asamU
o2CVBGjoHahQws00B6Qg9x1Ozi0fJXb6eKGRBVzt5sliXYxuYZqpg2mK7dt/kVuH
f1+WUr/ho1B1os+/CdIbe4PtAoGAP0ct7Ud8wPfaDws0NtWGCAIdvg5xsPZRrpMD
TuIeUrT4c47YFKV/L9BQi0camGOpk1+oulDIuD8rrBtTeDuupPLCa09Z6RCtXkWr
yScZHPFTFzno8KANfu8MpNUF9cX73uOXg5Hv/9DA+sNVx3zcvGu2vG0kZrXLMKdv
gkVCRrcCgYEAuHqA8IqCuIqSaD5ecsgS+0C9Gu481Daf8A1Dxb2GTx5Px5hoj/m5
pSY2tUvr5Pfe4SRpppRKvSXOBEfY4VF0S3z6nAn1Du8EojR44T0kEkOws4Flyqbw
PqU020I+ZZHm4AFN5sMxULVnRz83PnKn/sjFOoXWDErf1v/E1XBYstg=
-----END RSA PRIVATE KEY-----
[root@localhost ~]#

The id_rsa.pub is the public key. This is the key that we need to enter in our router. Lets take a look at the content of the public key:

[root@localhost .ssh]# cat /root/.ssh/id_rsa.pub 
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCpEhUvemXfCy1Ev0n79ELSzJ4Wo3/zgo+JMz1FetRdMlOGKH1Nfqxou6CCAylrqXc7jJu9Fwj/N0h2LQnzyPAeDJV3al0zJ+TlMti+M96L8UaRY960eVwqVF1OjXYp377flUbbHP9CXU0bqPJt0uJBdt3FoU/Sf3Mt/E0WV2sy950T1z+GqTW0+y7F2tzZ/KYRp/yux9rpsGQJNiwkLGMG9l5i7GNLH2dO0h6dDwjoF30OPGOGW8qjqFVcwYrzG/nZgzsItNe42uTVQodxPUZXEzGtMrF9KrVSTFRFcAVUVJmQ8pyKAP0kzFTDdrpWeFEvfwKcmICH0Vg7M3NuPhIR root@localhost.localdomain
[root@localhost .ssh]#

Now lets jump to router R1. We will configure the router to accept username ‘meru’ to login using certificate. If the certificate is not present, then the router will fallback to use local password.

First, we need to set the domain name on the router, otherwise we cannot generate an rsa key on the router:

R1(config)#ip domain name meru.com

Next, we configure a local username:

R1(config)#username meru privilege 15 password meru

Next, we configure the line vty 0 4 to accept ssh, and use login local:

R1(config)#line vty 0 4
R1(config-line)#transport input ssh
R1(config-line)#login local

Next, we create an rsa key on the router. When asked for how many bits in the modulus, lets put in 1024:

R1(config)#crypto key generate rsa
The name for the keys will be: R1.meru.com
Choose the size of the key modulus in the range of 360 to 4096 for your
  General Purpose Keys. Choosing a key modulus greater than 512 may take
  a few minutes.

How many bits in the modulus [512]: 1024
% Generating 1024 bit RSA keys, keys will be non-exportable...
[OK] (elapsed time was 1 seconds)

R1(config)#
*Dec 18 23:57:01.249: %SSH-5-ENABLED: SSH 1.99 has been enabled
R1(config)#

Next, just to be sure, lets set the ssh version to version 2:

R1(config)#ip ssh ver 2

We are now going to enter the public key into the router. The public key, however, is generated as one very long string. But Cisco IOS only supports 254 characters on a single line at a time. This means we cannot just highlight the public key and paste it all in to the router (if you do this, the router will stop paste-ing at the 254th character).

The trick is, as I found out from networklessons.com, is to use this command:

[root@localhost ansible]# fold -b -w 72 /root/.ssh/id_rsa.pub       
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCpEhUvemXfCy1Ev0n79ELSzJ4Wo3/zgo+J
Mz1FetRdMlOGKH1Nfqxou6CCAylrqXc7jJu9Fwj/N0h2LQnzyPAeDJV3al0zJ+TlMti+M96L
8UaRY960eVwqVF1OjXYp377flUbbHP9CXU0bqPJt0uJBdt3FoU/Sf3Mt/E0WV2sy950T1z+G
qTW0+y7F2tzZ/KYRp/yux9rpsGQJNiwkLGMG9l5i7GNLH2dO0h6dDwjoF30OPGOGW8qjqFVc
wYrzG/nZgzsItNe42uTVQodxPUZXEzGtMrF9KrVSTFRFcAVUVJmQ8pyKAP0kzFTDdrpWeFEv
fwKcmICH0Vg7M3NuPhIR root@localhost.localdomain
[root@localhost ansible]#

The fold command lets you wrap the lines into shorter lines. The option -w sets the line’s width. In this case, we set the width as 72 characters per line.

Now that we get that one sorted out, lets copy the public key into the router, one line at a time:

R1(config)#ip ssh pubkey-chain 
R1(conf-ssh-pubkey)#username meru
R1(conf-ssh-pubkey-user)#key-string 
R1(conf-ssh-pubkey-data)#$2EAAAADAQABAAABAQCpEhUvemXfCy1Ev0n79ELSzJ4Wo3/zgo+J
R1(conf-ssh-pubkey-data)#$6CCAylrqXc7jJu9Fwj/N0h2LQnzyPAeDJV3al0zJ+TlMti+M96L
R1(conf-ssh-pubkey-data)#$77flUbbHP9CXU0bqPJt0uJBdt3FoU/Sf3Mt/E0WV2sy950T1z+G
R1(conf-ssh-pubkey-data)#$9rpsGQJNiwkLGMG9l5i7GNLH2dO0h6dDwjoF30OPGOGW8qjqFVc
R1(conf-ssh-pubkey-data)#$odxPUZXEzGtMrF9KrVSTFRFcAVUVJmQ8pyKAP0kzFTDdrpWeFEv
R1(conf-ssh-pubkey-data)#fwKcmICH0Vg7M3NuPhIR root@localhost.localdomain      
R1(conf-ssh-pubkey-data)#exit
R1(conf-ssh-pubkey-user)#exit
R1(conf-ssh-pubkey)#exit
R1(config)#end
R1#

OK. Now, lets check if we can login from the Ansible machine to the router without having to type in our local password:

[root@localhost ansible]# ssh meru@192.168.172.11
The authenticity of host '192.168.172.11 (192.168.172.11)' can't be established.
RSA key fingerprint is SHA256:13DbaAF5Sb13rBvkHQQjhW80GnltZxW89X6sFEKVoCc.
RSA key fingerprint is MD5:6f:de:da:fc:48:b1:48:3c:46:bd:02:b7:c2:f4:e8:62.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '192.168.172.11' (RSA) to the list of known hosts.

**************************************************************************
* IOSv is strictly limited to use for evaluation, demonstration and IOS  *
* education. IOSv is provided as-is and is not supported by Cisco's      *
* Technical Advisory Center. Any use or disclosure, in whole or in part, *
* of the IOSv Software or Documentation to any third party for any       *
* purposes is expressly prohibited except as otherwise authorized by     *
* Cisco in writing.                                                      *
**************************************************************************
**************************************************************************
* IOSv is strictly limited to use for evaluation, demonstration and IOS  *
* education. IOSv is provided as-is and is not supported by Cisco's      *
* Technical Advisory Center. Any use or disclosure, in whole or in part, *
* of the IOSv Software or Documentation to any third party for any       *
* purposes is expressly prohibited except as otherwise authorized by     *
* Cisco in writing.                                                      *
**************************************************************************
R1#

Fantastic, it works! We went straight into privileged mode (because we set privilege 15 for username meru), without being prompted for password.

Because this is the first time we login from the Ansible machine to R1, it will display a warning message that the authenticiy of R1 cannot be established. When asked id we want to continue, just hit enter.

The second time you login to R1, you will not see that warning message again, because we have accepted R1 into our known_hosts.

Now that Ansible can login to the router without password, we can remove the hard-coded password on routers.yml group_vars 🙂

See you on the next post!

Note: Special thanks to networklessons.com for the information about the fold command. Very useful little trick. If you want to see the detail, please visit their page here:

https://networklessons.com/uncategorized/ssh-public-key-authentication-cisco-ios

Ansible with Cisco (GNS3) – Part 4 – Templating

On my last post, we configured ntp and local users on two Cisco routers. The first part on my previous post, the actual commands to configure ntp and local users are hard-coded on the playbook. On the second part on my previous post, we moved the hard-coded cli commands from the playbook to group_vars.yml, and in the playbook we have tasks to call them as needed.

Now we are going to look at templating. For this post, we are going to configure some interfaces (int g0/1, and int lo0), on both routers. But instead of hard-coding the commands on the group_vars.yml file, we are going to use jinja2 plus yaml to generate the interface configurations for us BEFORE we push them to the routers. Lets get started.

We want to create a host_vars file, one for each router. a host_vars file contain all the information that is unique for each router. The host var file for R1 will contain the information about R1, like its interface g0/1 and lo0 (ip address, description, etc. same thing with host var file for R2.

We created a new directory called ‘host_vars’, and inside that directory, we created a new file called ‘R1.yml’ and ‘R2.yml’. The structure looks like this:

 .
 ├── ansible.cfg
 ├── group_vars
 │   └── routers.yml
 ├── hosts
 ├── host_vars <--- new directory called host_vars
 │   ├── R1.yml <--- new file called R1.yml
 │   └── R2.yml <--- new file called R2.yml
 ├── playbook
 │   ├── common_cfg.yml
 │   └── show_ver.yml
 └── roles 

And the R1 and R2 yml files looks like this:

[root@localhost host_vars]# cat R1.yml
---
 
interface:
  Loopback0:
    ip: 1.1.1.1
    mask: 255.255.255.255
  GigabitEthernet0/1:
    ip: 10.0.1.1
    mask: 255.255.255.252
    desc: TO_ISP_A

[root@localhost host_vars]# 
[root@localhost host_vars]# cat R2.yml
---

interface:
  Loopback0:
    ip: 2.2.2.2
    mask: 255.255.255.255
  GigabitEthernet0/1:
    ip: 10.0.2.1
    mask: 255.255.255.252
    desc: TO_ISP_B

Now, we are going to create the template to generate the desired commands. Lets start with creating the playbook to run the task for generating commands using templates.

[root@localhost ansible]# cd /etc/ansible/playbook/
[root@localhost playbook]# vi configure_interface.yml
[root@localhost playbook]# cat configure_interface.yml
---

- name: INTERFACE
  hosts: routers
  gather_facts: false
  connection: network_cli
 
  tasks:
    - name: generate interface config
      template: 
        src="/etc/ansible/jinja2-template/interface.j2" 
        dest=/etc/ansible/config/{{ inventory_hostname }}_interface.txt

[root@localhost playbook]#

We created a new playbook called “configure_interface.yml”. At the moment, it only contains one task, named ‘generate interface config’. The task will use the ‘template’ module. It will look for the template file named ‘interface.j2’, located in ‘/etc/ansible/jinja2-template/’ . The generated file will be stored in ‘/etc/ansible/config/’, ‘named R1_interface.txt’ for R1, and similarly for R2.

We have not created the the ‘config’ and the ‘jinja2-template’ directory, so lets do that now.

[root@localhost ansible]# mkdir config
[root@localhost ansible]# mkdir jinja2-template
[root@localhost ansible]# tree
.
├── ansible.cfg
├── config <--- new directory called 'config'
├── group_vars
│   └── routers.yml
├── hosts
├── host_vars
│   ├── R1.yml
│   └── R2.yml
├── jinja2-template <--- new directory called 'jinja2-template'
├── playbook
│   ├── common_cfg.yml
│   ├── configure_interface.yml
│   └── show_ver.yml
└── roles

Then lets create the jinja2 template file, and name it ‘interface.j2’.

[root@localhost ansible]# cd jinja2-template/
[root@localhost jinja2-template]# vi interface.j2
[root@localhost jinja2-template]# cat interface.j2
{% if interface is defined %}
{% for i in interface %}
{%   set _i = interface[i] %}
!
interface {{ i }}
{% if _i['desc'] is defined %}
 description {{ _i['desc'] }}
{% endif %}
 ip address {{ _i['ip'] }} {{ _i['mask'] }}
{% endfor %}
{% endif %}
[root@localhost jinja2-template]#

OK. Looks good. Now lets run the playbook.

[root@localhost ansible]# ansible-playbook playbook/configure_interface.yml

PLAY [INTERFACE] *****************************************************************************

TASK [generate interface config] ****************************************************************
changed: [R2]
changed: [R1]

PLAY RECAP ***********************************************************************************
R1               : ok=1     changed=1     unreachable=0     failed=0
R2               : ok=1     changed=1     unreachable=0     failed=0

[root@localhost ansible]#

OK. The playbook run without problem. Lets take a look at what has been generated by this playbook.

[root@localhost ansible]# cd /etc/ansible/config/
[root@localhost config]#
[root@localhost config]# tree
.
├── R1_interface.txt
└── R2_interface.txt
[root@localhost config]# cat R1_interface.txt
!
interface GigabitEthernet0/1
 description TO_ISP_A
 ip address 10.0.1.1 255.255.255.252
!
interface Loopback0
 ip address 1.1.1.1 255.255.255.255
[root@localhost config]# cat R2_interface.txt
!
interface GigabitEthernet0/1
 description TO_ISP_B
 ip address 10.0.2.1 255.255.255.252
!
interface Loopback0
 ip address 2.2.2.2 255.255.255.255

Okay, the generated files looks good. Lets add a second task on our ‘configure_interface.yml’ playbook. The second task will be for pushing the generated config to the routers.

[root@localhost ansible]# cd /etc/ansible/playbook/
[root@localhost playbook]# vi configure_interface.yml

---

- name: INTERFACE
  hosts: routers
  gather_facts: false
  connection: network_cli

  tasks:
    - name: generate interface config
      template: 
        src="/etc/ansible/jinja2-template/interface.j2" 
        dest=/etc/ansible/config/{{ inventory_hostname }}_interface.txt
      register: interface

    - name: push configuration
      ios_config:
        src: /etc/ansible/config/{{ inventory_hostname }}_interface.txt
      notify: do wr
      when: interface.changed

  handlers:
    - name: do wr
      ios_command:
        commands: wr

Notice the first task we added a new parameter called ‘register’. We register the result of this task as a variable called ‘interface’, which we can call upon later.

We then added a second task called ‘push configuration’. This task uses the ‘ios_config’ module, and will look for the auto-generated interface configuration file stored in /etc/ansible/config/{{ inventory_hostname }}_interface.txt, and apply the commands to the routers. The {{ inventory_hostname }} is a varibale, referring to the inventory hostname specified in our ansible hosts file.

This second task has a condition that it will only be executed WHEN the task registered as ‘interface’ has a status of ‘changed’. And, like in my previous post, this task will notify handler called ‘do wr’.

Before we run the modified playbook, we need to delete the generated configuration files we previously created.

[root@localhost playbook]# cd ../config/
[root@localhost config]# rm R1_interface.txt
rm: remove regular file ‘R1_interface.txt’? y
[root@localhost config]# rm R2_interface.txt
rm: remove regular file ‘R2_interface.txt’? y
[root@localhost config]#
[root@localhost config]# ls -ll
total 0
[root@localhost config]#

Now lets run the playbook again..

[root@localhost playbook]# ansible-playbook configure_interface.yml

PLAY [INTERFACE] ****************************************************************************************************************************************************************************************************************************

TASK [generate interface config] ************************************************************************************************************************************************************************************************************
changed: [R1]
changed: [R2]

TASK [push configuration] *******************************************************************************************************************************************************************************************************************
changed: [R1]
changed: [R2]

RUNNING HANDLER [do wr] *********************************************************************************************************************************************************************************************************************
ok: [R2]
ok: [R1]

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
R1                         : ok=3    changed=2    unreachable=0    failed=0
R2                         : ok=3    changed=2    unreachable=0    failed=0

Cool, it works! Notice that the first task named ‘generate interface config’ has a status of changed for R1 and R2.

The second task named ‘push configuration’ will look at the status of the first task, and use it to evaluate the specified condition (on the playbook, its the part that says ‘when: interface.changed’). Since we instructed Ansible to run second task if the first task has a status of ‘changed’ (which is true), Ansible will run the second task.

Lastly, the second task will notify the handler ‘do wr’, and the handler is successfully run.

Just to make sure the configuration has been correctly pushed to both routers, lets check on the routers:

R1:

R1#sh run int g0/1
Building configuration...

Current configuration : 147 bytes
!
interface GigabitEthernet0/1
 description TO_ISP_A
 ip address 10.0.1.1 255.255.255.252
 shutdown <--- notice it is still shutdown
end

R1#sh run int lo0
Building configuration...

Current configuration : 63 bytes
!
interface Loopback0
 ip address 1.1.1.1 255.255.255.255
end

R2:

R2#sh run int g0/1
Building configuration...

Current configuration : 147 bytes
!
interface GigabitEthernet0/1
 description TO_ISP_B
 ip address 10.0.2.1 255.255.255.252
 shutdown <--- notice it is still shutdown
end

R2#
R2#sh run int lo0
Building configuration...

Current configuration : 63 bytes
!
interface Loopback0
 ip address 2.2.2.2 255.255.255.255
end

Nice! R1 and R2 now have both interface GigabitEthernet0/1 and Loopback0 correctly configured as we wanted.

Now let’s run the same playbook again, without changing anything on R1.yml and R2.yml host vars file.

[root@localhost playbook]# ansible-playbook configure_interface.yml

PLAY [INTERFACE] ****************************************************************************************************************************************************************************************************************************

TASK [generate interface config] ************************************************************************************************************************************************************************************************************
ok: [R1]
ok: [R2]

TASK [push configuration] *******************************************************************************************************************************************************************************************************************
skipping: [R1]
skipping: [R2]

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
R1                         : ok=1    changed=0    unreachable=0    failed=0
R2                         : ok=1    changed=0    unreachable=0    failed=0

[root@localhost playbook]#

Notice that now the first task ‘generate interface config’ has a status of ‘OK’ for R1 and R2, which means Ansible know that if it generates the interface configuration file for R1 and R2, the result will be the same. Whereas previously it has status of ‘changed’ for both routers, because we havent generated the files before.

We specified only to run the second task when status for first task is ‘changed’.
Now, Ansible uses this information, evaluates the specified condition, and resulted in False. So it skips the second tasks, indicated by ‘skipping R1’ and ‘skipping: R2’. And since the second tasks is skipped, no other tasks will call the handler, so the handler task ‘do wr’ is not called upon.

Pretty neat, right?

Ansible with Cisco (GNS3) – Part 3 – ios-config, making simple changes on our router

Hi,

My name is Meru, and welcome to my blog.

On my previous post, we created a playbook to run a ‘show version’ command, and print the output. But thats just a ‘show’ command. Lets do something a bit more interesting.

In this post, we will make a configuration change on our routers. We will split the configuration into two:

1) configuration that will exists on both routers (common config),

2) configuration that will be unique for each routers.

We are still using the same topology as before:

topology1

Lets configure the common config first. This is what the configuration looks like

username test_user1 privilege 15 password test_password1
username test_user2 privilege 15 password test_password2
ntp server 1.1.1.1
ntp server 2.2.2.2

We will make sure that they exists in R1 and R2.

To accomplish this task, we will be using the Ansible module called ‘ios_config’.

Ansible module: ios-config

Lets create a new playbook called ‘ntp’:

[root@localhost playbook]# vi ntp.yml
[root@localhost playbook]#
[root@localhost playbook]# cat ntp.yml
---

- name: CONFIGURE NTP
  hosts: routers
  gather_facts: false
  connection: network_cli

  tasks:
    - name: configuring ntp servers
      ios_config:
        lines:
          - ntp server 1.1.1.1
          - ntp server 2.2.2.2
      notify: do wr

  handlers:
    - name: do wr
      ios_command:
        commands: wr
[root@localhost playbook]#

OK, we are using a new module called ‘ios_config’. This module is used if you want to make configuration changes on IOS devices. we can insert multiple commands using this module. In this example, we are configuring two ntp servers: 1.1.1.1 and 2.2.2.2

Notice the ‘notify’ after the ‘ios_config’ module. This is used to notify a handler. A handler is something that Ansible will execute after being notified by some tasks. In this example, the handler will execute a ‘wr’ (similar to copy run start) using the ios_command module after the task ‘configuring ntp servers’ is executed. This essentially saves the configuration we just made.

Lets run the playbook:

[root@localhost playbook]# ansible-playbook ntp.yml

PLAY [CONFIGURE NTP] ***********************************************************************************************************************

TASK [configuring ntp servers] *************************************************************************************************************
changed: [R2]
changed: [R1]

RUNNING HANDLER [do wr] ********************************************************************************************************************
ok: [R2]
ok: [R1]

PLAY RECAP *********************************************************************************************************************************
R1 :           ok=2   changed=1   unreachable=0   failed=0
R2 :           ok=2   changed=1   unreachable=0   failed=0

[root@localhost playbook]#

Nice! Notice on the output the section called ‘RUNNING HANDLERS’. This is where Ansible execute the handler specified on the playbook.

Lets check on the routers if the ntp configuration exists:

R1#show run | s ntp
ntp server 1.1.1.1
ntp server 2.2.2.2
R1#

R2#show run | s ntp
ntp server 1.1.1.1
ntp server 2.2.2.2
R2#

OK. That works. Now, for configuring the users, instead of creating a new playbook specifically to do this task, we are going to add a second task on the ntp.yml playbook. Now, the ntp.yml playbook looks like this:

[root@localhost playbook]# cat ntp.yml
---

- name: CONFIGURE NTP
  hosts: routers
  gather_facts: false
  connection: network_cli

  tasks:
    - name: configuring ntp servers
      ios_config:
        lines:
          - ntp server 1.1.1.1
          - ntp server 2.2.2.2
      notify: do wr

    - name: configuring local users
      ios_config:
        lines:
          - username test_user1 privilege 15 password test_password1
          - username test_user2 privilege 15 password test_password2
      notify: do wr

  handlers:
    - name: do wr
      ios_command:
        commands: wr

.. and lets rename the ntp.yml file to something that really reflects on what it actually does:

[root@localhost playbook]# cp ntp.yml common_cfg.yml
[root@localhost playbook]# ls -ll
total 12
-rw-r--r--. 1 root root 558 Nov 23 16:26 common_cfg.yml
-rw-r--r--. 1 root root 558 Nov 23 16:19 ntp.yml
-rw-r--r--. 1 root root 410 Nov 4 03:19 show_ver.yml
[root@localhost playbook]#

here, using the ‘cp’ command, we are copying the existing ntp.yml and name the copied file common_cfg.yml.

‘ls -ll’ lists down all the files inside the playbook directory. Notice that now we have the ntp.yml file and the common_cfg.yml file. Lets remove the ntp.yml file

[root@localhost playbook]# rm ntp.yml
rm: remove regular file ‘ntp.yml’? y
[root@localhost playbook]#
[root@localhost playbook]# ls -ll
total 8
-rw-r--r--. 1 root root 558 Nov 23 16:26 common_cfg.yml
-rw-r--r--. 1 root root 410 Nov 4 03:19 show_ver.yml
[root@localhost playbook]#

We delete the ntp.yml by using the ‘rm‘ command.

Lets run the common_cfg.yml:

[root@localhost playbook]# ansible-playbook common_cfg.yml

PLAY [CONFIGURE NTP] ***********************************************************************************************************************

TASK [configuring ntp servers] *************************************************************************************************************
ok: [R1]
ok: [R2]

TASK [configuring local users] *************************************************************************************************************
changed: [R1]
changed: [R2]

RUNNING HANDLER [do wr] ********************************************************************************************************************
ok: [R1]
ok: [R2]

PLAY RECAP *********************************************************************************************************************************
R1 :                         ok=3    changed=1    unreachable=0    failed=0
R2 :                         ok=3    changed=1    unreachable=0    failed=0

[root@localhost playbook]#

Looks like it works. But before we check on the routers, there are a couple of things we need to explain:

  • The playbook still says: “CONFIGURING NTP”, we forgot to change the playbook’s name to something like “CONFIGURING COMMON CONFIG”. It is best practice to name your playbook with something that tells us what the playbook is doing.
  • Now, the playbook executes two tasks: 1) configuring ntp servers, and 2) configuring local users.
  • Notice that first task shows ‘OK: [R1]’ and ‘OK: [R2]’. This means that there are no errors in running this tasks, but since the command specified in this task already configured on R1 and R2, Ansible did not execute the command. Hence it shows ‘OK’ for both routers.
  • On the second task however, it shows ‘changed: [R1]’ and ‘changed: [R2]’. This means that there are no errors in running this task, AND Ansible successfully executed the command for configuring local users on both routers. It knows that these are new configs, and therefore run the command.
  • Ansible run the handler after all tasks has been executed. It is not running the handler each time a task related to the handler has been executed.
  • On the PLAY RECAP section, we can see for each router, there are ‘ok=3’ (1 for ntp task, 1 for local user task, 1 for handler), and ‘changed=1’. this is because only one successful task actually make changes to the router (the local user task).

Alright, finally lets verify the local users on both routers:

R1#sh run | s user
username meru privilege 15 secret 5 $1$BblC$mYe.poguxsA/MLwnPKHy/.
username test_user1 privilege 15 password 0 test_password1
username test_user2 privilege 15 password 0 test_password2
R1#

R2#sh run | s user
username meru privilege 15 secret 5 $1$h0Gu$0DOCxcX3.N0TYCVfrgZI1/
username test_user1 privilege 15 password 0 test_password1
username test_user2 privilege 15 password 0 test_password2
R2#

The first username ‘username meru privilege 15 secret …’ is the username configured on each router that is being used by Ansible to connect to the routers.

The second and third username are the ones configured using Ansible.

Using Variables

OK, so we manage to deploy same configuration across two routers. The actual commands are written directly on the playbook, using ios-config, under ‘lines’. We can also separate the commands we want to execute, put them on another file, and have the playbook call them. Lets do that.

For this to work, we need to do two things: 1) add commands we want to call in ‘routers’ group_vars, and 2) modify our playbook.

First, lets modify the ‘routers’ group_vars:

cd /etc/ansible/group_vars
vi routers

--- 

ansible_ssh_user: meru 
ansible_ssh_pass: meru
ansible_network_os: ios

ntp:
  - ntp server 1.1.1.1 
  - ntp server 2.2.2.2

users:
  - username test_user1 privilege 15 password test_password1 
  - username test_user2 privilege 15 password test_password2

Above, we have two new sections: ‘ntp’ and ‘users’. Under each section, we listed down the actual commands we want Ansible to execute. Each command is an item, represented by a dash sign. If we want, we can have more than two commands, but for now lets stick with two.

OK, now lets modify the playbook. Oh, this time don’t forget to modify the playbook’s name.


[root@localhost group_vars]# cd /etc/ansible/playbook
[root@localhost playbook]# vi common_cfg.yml
[root@localhost playbook]# cat common_cfg.yml

---

- name: CONFIGURE COMMON CONFIGURATION
  hosts: routers
  gather_facts: false
  connection: network_cli

  tasks:
    - name: configuring ntp servers
      ios_config:
        lines: "{{ ntp }}"
      notify: do wr

    - name: configuring local users
      ios_config:
        lines: "{{ users }}"
      notify: do wr

  handlers:
    - name: do wr
      ios_command:
        commands: wr

Here, the playbook is now using a variable. A variable is a value that can change. If something is enclosed with this special sets of character, “{{ }}”, Ansible knows that it is a variable.

On the first task, we see the variables “{{ ntp }}” is specified. When running this playbook, Ansible will look up the routers.yml file in the group_vars directory, and search for ‘ntp’. Then, Ansible will loop through the ‘ntp’ items.

Same process will happen with “{{ users }}”.

Now, lets run the playbook again.

[root@localhost playbook]# ansible-playbook common_cfg.yml

PLAY [CONFIGURE COMMON CONFIGURATION] ******************************************

TASK [configuring ntp servers] *************************************************
ok: [R2]
ok: [R1]

TASK [configuring local users] *************************************************
changed: [R1]
changed: [R2]

RUNNING HANDLER [do wr] ********************************************************
ok: [R2]
ok: [R1]

PLAY RECAP *********************************************************************
R1 :               ok=3     changed=1     unreachable=0     failed=0
R2 :               ok=3     changed=1     unreachable=0     failed=0

[root@localhost playbook]#

As you can see, now we have separated the commands from the playbook. We also have shorten the playbook.

On my next post, we will be configuring the routers with commands unique to each router.

Ansible with Cisco (GNS3) – Part 2 – Running the first Playbook

Hi,

My name is Meru, and welcome to my blog. This post continues my previous post. We are now going to create our first playbook. As a refreshment, here is the topology we are working with:

topology1

Ansible VM: 192.168.71.128

R1: 192.168.71.11

R2: 192.168.71.12

A brief explanation of ‘playbook’ is required. Playbook is like a script. Playbook contains one or more ‘tasks’. Tasks are, well, tasks. Tasks is run sequentially, and each tasks can contain one or more instructions. Lets create our first Playbook.

Go into /etc/ansible directory, and create a new directory called ‘playbook’.

cd /etc/ansible

mkdir playbook

Once inside the ‘playbook’ directory, lets create our first playbook, called ‘show_ver.yml’.

vi show_ver.yml

Here is what the playbook looks like:

---

- name: SHOW VERSION
  hosts: routers
  gather_facts: false
  connection: network_cli

  tasks:
    - name: run the command 'show version'
      ios_command:
        commands: show version

A couple of things to note here:

  • If you haven’t noticed already, the playbook is written in yml format, indicated by the three dashes at the top.
  • In yml file, indentation is extremely important. You may notice that the ‘name’, ‘hosts’, ‘gather_facts’, and ‘connection’ has the same level of indentation. Industry best practice uses two blank spaces, which is what we used here. If the indentation is not uniform, Ansible will spit an error when trying to run the playbook.
  • There are a few new items that we have not discussed before, but lets go through them all:
    • name: this is the name we give to our playbook
    • hosts: this specify the hosts that we will run the playbook against.
    • gather_facts. This is used to gather certain facts from the hosts. We set this to ‘false’ so Ansible can run the playbook without having to wait until it finishes gathering facts about the two routers.
    • connection: this is the connection type Ansible uses to connect to the hosts. Since we are dealing with network devices, we specify the value to ‘network-cli’, which is the recommended type for Ansible 2.7. There are many connection type that Ansible supports, like ‘ssh’, ‘httpapi’, ‘winrm’. For a complete lists of connection type supported by Ansible 2.7, check out their documentation here: https://docs.ansible.com/ansible/latest/plugins/connection.html#plugin-list.
    • tasks: playbook contains one or more tasks. This playbook have only one task, which will execute the command: ‘show version’, using the ‘ios_command’ module. More on this later..

OK, now lets run the playbook:

ansible-playbook /etc/ansible/playbook/show_ver.yml
[root@localhost playbook]# ansible-playbook show_ver.yml

PLAY [SHOW VERSION] **********************************************************************************************************************

TASK [run the command 'show version'] ****************************************************************************************************
fatal: [R2]: FAILED! => {"msg": "Unable to automatically determine host network os. Please manually configure ansible_network_os value for this host"}
fatal: [R1]: FAILED! => {"msg": "Unable to automatically determine host network os. Please manually configure ansible_network_os value for this host"}
to retry, use: --limit @/etc/ansible/playbook/show_ver.retry

PLAY RECAP *******************************************************************************************************************************
R1 : ok=0 changed=0 unreachable=0 failed=1
R2 : ok=0 changed=0 unreachable=0 failed=1

[root@localhost playbook]#

Whoops, looks like something went wrong here.

“Unable to automatically determine host network os. Please manually configure ansible_network_os value for this host”

Ansible cannot determine R1 and R2’s OS. Lets fix this by adding new parameter into the group_vars/routers.yml. Changes made are indicated in blue.

vi /etc/ansible/group_vars/routers.yml
---

ansible_ssh_user: meru
ansible_ssh_pass: meru
ansible_network_os: ios

We added ‘ansible_network_os: ios’, to tell Ansible that the routers are running on IOS. Don’t forget to save the file after you made the change.

Now lets run the playbook again.

[root@localhost ~]# ansible-playbook /etc/ansible/playbook/show_ver.yml

PLAY [SHOW VERSION] **********************************************************************************************************************

TASK [run the command 'show version'] ****************************************************************************************************
ok: [R2]
ok: [R1]

PLAY RECAP *******************************************************************************************************************************
R1                         : ok=1    changed=0    unreachable=0    failed=0
R2                         : ok=1    changed=0    unreachable=0    failed=0

[root@localhost ~]#

The playbook successfully executed without any errors. Fromt he output, we can see:

  • the play’s title called “SHOW VERSION”,
  • the task being executed: “run the command ‘show version'”, and
  • the playbook summary, which tells us that on each hosts we have successfully ran the task (ok=1), no changes are made (changed=0), and no failed task (failed=0).

But the playbook result did not show us the output of the command being executed i.e. we do not see the output of the command ‘show version’.

In order to do so, we need to modify our playbook. Lets do that.

vi /etc/ansible/playbook/show_version.yml

The changes made are in blue

tasks:
  - name: run the command 'show version'
    ios_command:
      commands: show version
    register: output1

  - name: print result
    debug:
      var: output1.stdout_lines

Alright, we have made some changes on our playbook.

  • We added ‘register’ on the last line of the first task. This tells Ansible to register the output of the command ‘show version’ as a variable called ‘output1’. The word ‘output1’ is a string, and we can name it anything we like.
  • We also added a second task, and give it a name “print result”. Then, using the ‘debug’ module, we ask Ansible to print out the variable called ‘output1’. We added “.stdout_lines” after “output1” so that Ansible will print the variable in a nice format. If we omit the “.stdout_lines”, Ansible will print ‘output1’ with no formatting, not very human readable.
  • Again, indentation is really important. If the first task and the second task is not using the same level of indentation, Ansible will spit an error.

Cool, lets execute the playbook again.

[root@localhost ~]# ansible-playbook /etc/ansible/playbook/show_ver.yml

PLAY [SHOW VERSION] **********************************************************************************************************************

TASK [run the command 'show version'] ****************************************************************************************************
ok: [R2]
ok: [R1]

TASK [print result] **********************************************************************************************************************
ok: [R1] => {
    "output1.stdout_lines": [
        [
            "Cisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.5(3)M, RELEASE SOFTWARE (fc1)",
            "Technical Support: http://www.cisco.com/techsupport",
            "Copyright (c) 1986-2015 by Cisco Systems, Inc.",
            "Compiled Wed 22-Jul-15 23:04 by prod_rel_team",
            "",
            "",
            "ROM: Bootstrap program is IOSv",
            "",
            "R1 uptime is 5 hours, 44 minutes",
            "System returned to ROM by reload",
            "System image file is \"flash0:/vios-adventerprisek9-m\"",
            "Last reload reason: Unknown reason",
            "",
            "",
            "",
            ----- output omitted -----
            "",
            "",
            "",
            "Configuration register is 0x0"
        ]
    ]
}
ok: [R2] => {
    "output1.stdout_lines": [
        [
            "Cisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.5(3)M, RELEASE SOFTWARE (fc1)",
            "Technical Support: http://www.cisco.com/techsupport",
            "Copyright (c) 1986-2015 by Cisco Systems, Inc.",
            "Compiled Wed 22-Jul-15 23:04 by prod_rel_team",
            "",
            "",
            "ROM: Bootstrap program is IOSv",
            "",
            "R2 uptime is 5 hours, 44 minutes",
            "System returned to ROM by reload",
            "System image file is \"flash0:/vios-adventerprisek9-m\"",
            "Last reload reason: Unknown reason",
            "",
            "",
            "",
            ----- output omitted -----
            "",
            "",
            "",
            "Configuration register is 0x0"
        ]
    ]
}

PLAY RECAP *******************************************************************************************************************************
R1                         : ok=2    changed=0    unreachable=0    failed=0
R2                         : ok=2    changed=0    unreachable=0    failed=0

[root@localhost ~]#

Alright, that works! You can see that the playbook summary now tells us that on each host, we have successfully executed two tasks (ok=2), no changes are made (changed=0), and failed tasks (failed=0)

We also can see that Ansible prints the show version on a json format.

See you on my next post!

Meru

Ansible with Cisco (GNS3) – Part 1 – How to setup Ansible

Hi,

My name is Meru, and welcome to my blog. Here, I am going to show you how a non-Linux guy like me sets up Ansible for network automation. For this lab, we are using GNS3 network simulator, and VMWare workstation.

The topology is very simple.

Ansible will be installed on Centos7 running on VMWare, and will manage two virtual routers running on Cisco IOSv. The switch in the middle is just a layer 2 switch to connect everything together, all eight ports are in the same VLAN. It will not be managed by Ansible.

topology1

Before we install Ansible, there are a couple of things that we need to do. First, make sure our Centos7 VM can connect to the Internet.

[root@localhost ~]# ping google.com
PING google.com (172.217.194.138) 56(84) bytes of data.
64 bytes from 172.217.194.138 (172.217.194.138): icmp_seq=1 ttl=128 time=23.3 ms
64 bytes from 172.217.194.138 (172.217.194.138): icmp_seq=2 ttl=128 time=26.5 ms
64 bytes from 172.217.194.138 (172.217.194.138): icmp_seq=3 ttl=128 time=32.9 ms
64 bytes from 172.217.194.138 (172.217.194.138): icmp_seq=4 ttl=128 time=25.3 ms
^C
--- google.com ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3050ms
rtt min/avg/max/mdev = 23.314/27.057/32.991/3.624 ms
[root@localhost ~]#

Internet connectivity looks good. Then update the OS, by running the command:

yum -y update

Next, make sure we installed the epel repository:

yum -y install epel-release

Then, finally, lets install Ansible:

yum -y install ansible

Once the installation completes, lets check its version. As of the time of this writing, Ansible is up to version 2.7.0

[root@localhost ~]# ansible --version
ansible 2.7.0
config file = /etc/ansible/ansible.cfg
configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python2.7/site-packages/ansible
executable location = /usr/bin/ansible
python version = 2.7.5 (default, Jul 13 2018, 13:06:57) [GCC 4.8.5 20150623 (Red Hat 4.8.5-28)]
[root@localhost ~]#

Alright, looks good. Now lets configure Centos7 so it can talk to the routers:

vi /etc/sysconfig/network-scripts/ifcfg-[yourinterfacename]

We are using the built-in text editor in Centos7 called Vi, hence the command above starts with ‘vi’. Once you are in Vi text editor, to edit the ifcfg file, Press ‘SHIFT’ + ‘;’ and then press ‘i’, to go into editing mode.

TYPE=Ethernet
BOOTPROTO=STATIC
IPADDR=192.168.71.128
PREFIX=24
GATEWAY=192.168.71.2
DEFROUTE=no
PEERDNS=yes
PEERROUTES=yes
IPV4_FAILURE_FATAL=no
NAME=ens33
UUID=00f6ec1a-131d-42ab-9435-50858552371f
DEVICE=ens33
ONBOOT=yes

A couple of things to point out: the LAN segment that connects the Centos7 VM and the two Cisco routers are 192.168.71.0/24. In our case, the Centos7 VM is connected to the switch using interface ens33, so we set the interface ens33 on Centos to have a static ip address of 192.168.71.128, with a /24 prefix, and a default gateway of 192.168.71.2.

We also set the ‘ONBOOT’ parameter to ‘yes’, and the ‘DEFROUTE’ to ‘no’. The ONBOOT is set to ‘YES’ so that we do not have to manually turn on the interface every time we start the VM. And since we are using a different interface to connect the Centos7 VM to the Internet, we set the DEFROUTE to ‘no’ so it will not install a default route on the VM’s routing table pointing to this interface (otherwise our Centos7 VM can’t connect to the Internet)

When we have finished editing the ifcfg file, press ‘Esc’, then press ‘SHIFT’ + ‘;’ again, but this time type ‘wq!’ and press enter. This will save the changes we made into that file. If we want to exit the Vi editor WITHOUT saving any changes, type ‘q!’.

Alright, lets do some verification.

ip addr
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:0c:29:e2:de:f7 brd ff:ff:ff:ff:ff:ff
inet 192.168.71.128/24 brd 192.168.71.255 scope global ens33
valid_lft forever preferred_lft forever
inet6 fe80::2ee8:ab57:e3c:72b/64 scope link
valid_lft forever preferred_lft forever

Looks good. We can see that interface ens33 have an ip address of 192.168.71.128, and the state is ‘UP’.

We already have the two Cisco routers preconfigured. R1 and R2 are connected to interface g0/0 with ip address of 192.168.71.11, and .12 respectively. Both are configured to enable ssh version 2, with a configured local username: ‘meru’ and password: ‘meru’. Both are also configured to accept ssh connection with ‘login local’ authentication on their line vty 0 4.

Lets check if Ansible can reach both routers:

[root@localhost ~]# ping 192.168.71.11
PING 192.168.71.11 (192.168.71.11) 56(84) bytes of data.
64 bytes from 192.168.71.11: icmp_seq=1 ttl=255 time=1.94 ms
64 bytes from 192.168.71.11: icmp_seq=2 ttl=255 time=1.38 ms
64 bytes from 192.168.71.11: icmp_seq=3 ttl=255 time=1.62 ms
^C
--- 192.168.71.11 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 1.383/1.651/1.944/0.234 ms
[root@localhost ~]# ping 192.168.71.12
PING 192.168.71.12 (192.168.71.12) 56(84) bytes of data.
64 bytes from 192.168.71.12: icmp_seq=1 ttl=255 time=1.29 ms
64 bytes from 192.168.71.12: icmp_seq=2 ttl=255 time=1.77 ms
64 bytes from 192.168.71.12: icmp_seq=3 ttl=255 time=1.47 ms
^C
--- 192.168.71.12 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 1.298/1.513/1.772/0.200 ms
[root@localhost ~]#

Alright, ping looks good. Lets see if we can ssh to the routers:

[root@localhost ~]# ssh meru@192.168.71.11

**************************************************************************
* IOSv is strictly limited to use for evaluation, demonstration and IOS *
* education. IOSv is provided as-is and is not supported by Cisco's *
* Technical Advisory Center. Any use or disclosure, in whole or in part, *
* of the IOSv Software or Documentation to any third party for any *
* purposes is expressly prohibited except as otherwise authorized by *
* Cisco in writing. *
**************************************************************************Password:

**************************************************************************
* IOSv is strictly limited to use for evaluation, demonstration and IOS *
* education. IOSv is provided as-is and is not supported by Cisco's *
* Technical Advisory Center. Any use or disclosure, in whole or in part, *
* of the IOSv Software or Documentation to any third party for any *
* purposes is expressly prohibited except as otherwise authorized by *
* Cisco in writing. *
**************************************************************************
R1#
R1#exit
Connection to 192.168.71.11 closed.
[root@localhost ~]#
[root@localhost ~]# ssh meru@192.168.71.12

**************************************************************************
* IOSv is strictly limited to use for evaluation, demonstration and IOS *
* education. IOSv is provided as-is and is not supported by Cisco's *
* Technical Advisory Center. Any use or disclosure, in whole or in part, *
* of the IOSv Software or Documentation to any third party for any *
* purposes is expressly prohibited except as otherwise authorized by *
* Cisco in writing. *
**************************************************************************Password:

**************************************************************************
* IOSv is strictly limited to use for evaluation, demonstration and IOS *
* education. IOSv is provided as-is and is not supported by Cisco's *
* Technical Advisory Center. Any use or disclosure, in whole or in part, *
* of the IOSv Software or Documentation to any third party for any *
* purposes is expressly prohibited except as otherwise authorized by *
* Cisco in writing. *
**************************************************************************
R2#
R2#exit
Connection to 192.168.71.12 closed.
[root@localhost ~]#

Sweet!

Now, before Ansible can connect to these devices, we need to edit the Ansible inventory file.

vi /etc/ansible/hosts
[routers]
R1 ansible_host=192.168.71.11
R2 ansible_host=192.168.71.12

Basically, we are creating a group called ‘routers’, and that group contains two members: R1 and R2. The ip address for each router is also specified.

We also need to specify the username and password that will be used to connect to the routers. They are specified in a yml file that corresponds to the group name, and is located inside group_vars folder.

Lets make the group_vars folder first;

mkdir /etc/ansible/group_vars/

Then, lets go into group_vars

cd /etc/ansible/group_vars

Once we are inside the ‘group_vars’ directory, lets create a yml file called ‘routers’. Note: since we are already in group_vars directory, we can just type:

vi routers
---

ansible_ssh_user: meru
ansible_ssh_pass: meru

The three dash at the top is a unique way of telling the system that the file is a yml file. the next line is just to specify the username that Ansible will use to ssh to the cisco routers.

In my next post, we will create our first Ansible playbook, and we will run the playbook to execute simple tasks on the two routers.

Meru