Network Automation using Ansible(Playbooks)(Part 4)
Playbooks
According to me, playbooks are by far the most powerful method that Ansible has to deploy configurations. Playbooks enable an individual to group together many tasks and have them execute all at once. They are written using the YAML(Yet Another Markup Language) syntax. It is not very difficult to understand the YAML configuration file. However, it can be annoying and picky like Python. It is extremely sensitive to spaces and tabs so beware of it! As we progress, I will assume that not all of you are knowledgeable on the Cisco IOS syntax. That is why, I will explain every command step by step.
My playbook is named playbook.yml and we will be using it to deploy the network that I mentioned in a previous article. Here is its content:
This playbook is composed of 4 plays which are each highlighted in yellow. As you have probably noticed, it is quite big therefore I couldn't fit whatever was after line 41. Later on, the contents of the 4th play shall be displayed. A play maps a group of hosts to a certain set of tasks. Think of it as a way to tell Ansible, what operations should be performed on a certain target. In each play, we have many elements that keep appearing over and over. Let us examine each one of them to better understand their purpose:
->name: Defines the name of the play. In the task section, it gives a name to our tasks.
->hosts: Target hosts on which to perform the operation. This can also refer to a group name.
->connection: In our use case, it is always set to local. This instructs Ansible to run the playbook directly on our Network Automation Appliance.
->gather_facts: This option allows you to collect information about the target host. We will set this to always be False.
->roles: Later on we will look at this in more depth, but for now think of it as a way to group a common set of configs all together into a single unit.
->tasks: This defines the set of commands/operations that will be performed on the target.
->ios_config: This is the Ansible module that we will be using to execute our commands in Global configuration mode. I think we should first define what are the different modes available in Cisco IOS. The configuration related to a technology/feature is performed in a specific mode.
If we take a close look at this picture, most of the commands that we will perform will be in router and interface configuration mode. However, to enter this mode one must access the global config mode and that is why we use this specific module.
->parents: This is used to define the config mode that we are trying to access. The yellow highlighted commands that have an arrow next to it need to be entered in order to gain entry to the appropriate mode. Please refer to the previous picture for the appropriate commands.
->lines: Once we have gained access to the appropriate mode, we can enter commands to set up protocol/services. The commands highlighted in purple are the ones used to perform those changes.
Inventory/hosts file
Once again we make use of a hosts(inventory) file, but this time it is a little bit different. This is what it looks like:
So if you refer to the 2nd article of the series, we had a group called gns3-Routers which contained all of the routers(R1 to R4). Now at the top of this file, I am going to create a group within this already existing group(Nested grouping). So I will have a new group called BGP. This group will then have 2 sub-groups called BGP-RR-Server and BGP-RR-Client. I think it's pretty clear by the group name that R1 will be our route-reflector server and that the 3 other routers will act as clients. Don't worry if you didn't understand the structure of these nested groups. Here is a picture that will hopefully away clear all of your doubts:
Play #1: Loopback0, Loopback1 creation and ....
The goal of this play is to create 2 loopback interfaces named loopback0 and loopback1. Afterwards, we will advertise the loopback0 interface into OSPF and the other created interface will be advertised into BGP. In later plays, loopback0 will be used to create our BGP adjacencies. We will be configuring iBGP adjacencies for the autonomous system(AS) 65002. This is the IP addressing scheme for the different loopback interfaces of these 4 routers:
->R1===> loopback0: 1.1.1.1/24, loopback1: 11.11.11.11/24
->R2===> loopback0: 2.2.2.2/24, loopback1: 22.22.22.22/24
->R3===> loopback0: 3.3.3.3/24, loopback1: 33.33.33.33/24
->R4===> loopback0: 4.4.4.4/24, loopback1: 44.44.44.44/24
In this play, we will perform the same set of tasks on all routers belonging to the BGP group. We will be executing a role named ip_loop. To better understand the concept of a role, there is something that has to be explained first. I had to set up my directory in this particular manner:
Just a quick little note, later on if I refer to a file, please refer to this picture to find the location of it. By looking at this picture, you can recognize a couple of familiar files such as playbook.yml and hosts. You can also see that there is a directory called roles, which has multiple nested subdirectories. One of those nested directories is named ip_loop. So when we execute line #6 of the playbook, Ansible loads the content of the ip_loop directory into the playbook and executes the tasks located in the directory named tasks. The directory named tasks contains a file named main.yml with the following content:
The file main.yml is displayed on the left hand side(LHS) of this picture. In it, we have 3 tasks to execute and they can be identified by the lines starting with "- name". In the first one, we create the interfaces and assign them an IP address. The other 2 other tasks will advertise those interfaces into their respective routing protocols.
For the first task, we have this new element named with_items on line #10 of main.yml. This is used to loop through the data structure(DS) that is specified within the double curly brackets{{}}. In our case this is interfaces and wherever you notice the word item on the LHS of the picture that is where this DS is being invoked. It is defined in 4 other files named R1.yml, R2.yml, R3.yml, R4.yml which are located under the directory labelled as host_vars. On the LHS of this picture, I have displayed the content of R1.yml. These 4 files have the same content except for the IP addresses. R1.yml contains the loopback information about R1. The 3 other YAML files contain the loopback information for the 3 other routers. In this file we have interfaces which is actually a dictionary. In Ansible, a dictionary is a complex data structure and it is composed of a key and a value. The 4 keys that are in this dictionary are: 2 x int_name, 2 x int_addr. Each of those keys refers to a value. Those values are referenced to the different loopback names and their respective IP addresses. The 2 other tasks, each have a variable which can be referenced in R1.yml and in the 3 other YAML files.
Do you remember this directive with_items: {{ interfaces }} on the LHS of the picture? It says to iterate through every single key of the dictionary and replace the placeholder item with the value being requested. The word after the period represents the value that we are asking for and in our case they are the int_name and int_addr. So after substituting all of the placeholders with the appropriate data we get the following commands which will be executed on R1:
For the 2 other variables which located in the other tasks the process is very similar. The variables ospf_address and bgp_address will be replaced with their corresponding placeholders. This will then result in the following commands to get executed on R1:
The variable interfaces, ospf_address, bgp_address can also be found in the 3 other YAML files. Therefore, the same configuration shall be applied to the 3 other routers.
There is one last thing that I would like us to pay special attention to. The before and after directive, which are located on line number 3 and 7 respectively are special directives. I commented them out, but I left in the file because I found them to be very interesting. The before directive can be used to tell Ansible to perform this command prior to executing any set of commands defined in the lines section.
I believe that prior to configuring an interface, it could be a smart idea to remove any configs on the interface. That is the purpose of the command on line 4.
The after directive executes right after the completion of the lines section. Any physical interface on a router, is by default turned off(shutdown). A loopback interface is a virtual interface so there is no need to apply this command. Therefore, it could be helpful to turn it on after having configured a physical interface(line 8).
Play #2: BGP Route-Reflector Server config
This play is definitely not as difficult to understand as the 1st one, but it requires some explanation. In this play, we only configure R1 due to the hosts directive being set to BGP-RR-Server. Remember that this group only contained R1. In terms of tasks, we configure R1 as our route-reflector server and we advertise its loopback1 interface into BGP.
I think that by now you understand the purpose of the with_items directive. Those 5 commands defined in the lines section will be executed once for each of the 3 IP addresses(2.2.2.2, 3.3.3.3, 4.4.4.4).
The parents directive, is telling R1 to enter its router config mode and to consider this router to be part of AS 65002. I will explain the purpose of the 5 commands located in the lines section for the following IP: 2.2.2.2
->neighbor 2.2.2.2 remote-as 65002: This is telling R1 that the host with this IP is located in the same AS as itself and that it should consider it as an iBGP neighbor.
->neighbor 2.2.2.2 update-source loopback0: Due to the fact that we are setting up our iBGP adjacencies using loopback interfaces, we are telling R1 to send its BGP related information using the loopback0.
->neighbor 2.2.2.2 password VERY_SAFE!: We will set up authentication between R1 and R2's loopback0 interface. The password that they will use to authenticate one another will be VERY_SAFE! . To be honest, this is not safe at all as our password is being hashed using the MD5 algorithm. This outdated and unsafe algorithm is still unfortunately being used by BGP. There is a way to use SHA1 but even that is not safe anymore. BGP has many known security flaws but it is deployed all over the internet. This makes it very difficult to substitute it.
->neighbor 2.2.2.2 route-reflector-client: This command is telling R1 that the host with this IP will be a route-reflector client(R2). So whenever R2 learns about a route. it will reflect it to R1 which will then send the same route out to all other iBGP neighbors(R3,R4). Note that on the client side, we do not need to perform any route-reflector configuration.
->network 11.11.11.0 mask https://www.dhirubhai.net/redir/invalid-link-page?url=255%2e255%2e255%2e0: We are now advertising the IP address of loopback1 into BGP.
Ansible will then repeat the same commands for the 2 other IP addresses.
Play #3: BGP Route-Reflector Client config
As we tackle the last 2 plays, it keeps getting easier, so stick with me! We can do this! This play will be performed by all hosts specified in the BGP-RR-Client group. In other words R2,R3 and R4 will all be configured to be in the same AS. They only connect to R1 and they all use that password to authenticate with it. I am aware that I had already mentioned it, but I will once again repeat it. We are creating our BGP adjacencies using our loopback0 interface.
Play #4: Verification commands
I think that the title of the plays reveals the purpose of this play. There are a few differences between this play and the previous ones. We are now using a different module and a different directive to specify the commands to run.
->ios_commands: This module enables us to run commands in Privileged Exec Mode.
->commands: This serves the same purpose as the lines directive, which was found in the ios_config module. In other words, it is used to specify what commands are to be executed in the Privileged Exec Mode.
One of the purpose of this play is to test connectivity within our network. We will do so by pinging the loopback interfaces. In other words, each host will use the PING utility to send 5 ICMP echo request to each of those 4 IP addresses that defined by the with_items directive. Finally, we are instructing Ansible to execute the command to save all of the configuration that has been performed to the devices NVRAM.
Running it all!
Alright, now is the time that we have been all waiting for. Does it work? There is only one way to know if all is functional. We are going to run our playbook with the following command:
So instead of using the old ansible command that we used when we ran ad hoc commands in part 2, we now use ansible-playbook to execute the contents of a playbook. We then specify the user(Ahnaf) and tell it to prompt for a password. After running the command, we get tons of output and it becomes very hard to decipher all of it. Ansible is telling me that all of my plays have executed properly. However, as an old school Network Engineer I like to SSH into my devices to check if everything is working as intended.
Each router holds the routes that it knows about in a routing table. This can be viewed by running the show ip route command. By adding bgp at the end of the command, I am telling Cisco IOS to only display routes learnt through BGP.
Alright, that seems good that every router knows about a path to those loopback interfaces. However, when I started to get into networking I learnt a crucial lesson. That just because you know how to get to your destination doesn't mean that you can effectively reach it.
Everything is working as planned! From our last play(#4), I performed the ping command from every host to every loopback interfaces manually on all of our hosts. If you look closely at each of those terminals, you will see 3 sets of exclamation points. Each set is produced due to me trying to ping each of those virtual interfaces. Each "!" means that we got an ICMP Echo reply back to our router which is what we wanted to see. To me this is much easier to decipher than all of that output generated by play #4.
Personal Thoughts
Finally the time has come for me to get all emotional. I can't believe that we were able to accomplish this project. I couldn't have done it all by myself. Some of my classmates who also happen to be friends of mine gave me a leg up. Tom who is a DevOps engineer, showed me some working examples of Ansible. This guided me quite a bit. Starly, taught me some very useful Linux utilities to speed up my workflow. Lastly, Ali took the time to review my work and give me some constructive feedback. So are you now convinced that Ansible can be incorporated into your toolkit? I have to admit that initially there is a learning curve involved. However, that investment will soon bear its fruit. It might not be the perfect tool for all use cases, but it can definitely help Network Engineers to take the right path towards automating their tedious tasks.
-References:
-https://www.imagination-station.org/wp-content/uploads/2015/07/pyro-power-328870.jpg
Engenheiro de Redes Sênior na Syngenta - CCNP Service Provider | CCNP Enterprise | CCNA | JNCIA | LPIC1 | ITILv3
4 年Congrats man
dsds at sdsd
6 年Hi?Ahnaf Shahriar I am building this lab but when run the main.yml, it doesn't know the "interfaces" variable in /host_vars/R1.yml.? So that the result is FAILED! => {"msg": "'interfaces' is undefined"} Pls help me to resolve this problem, thanks a lot :D