Python For Network Engineers
Python For Network Engineers
Release 1.0
1 Introduction 3
About book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Who is this book for? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
OS and Python requirements . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Tasks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Book formats . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Frequently Asked Questions (FAQ) . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Will there be a printed version of the book? . . . . . . . . . . . . . . . . . . . . . 4
Why is there no topic X in the book? . . . . . . . . . . . . . . . . . . . . . . . . 4
How does this differ from the regular Python introductory book? . . . . . . . . . . 4
I’m a network engineer. What do I need this book for? . . . . . . . . . . . . . . . 5
Why is this book specifically for network engineers? . . . . . . . . . . . . . . . . 5
Why Python? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Module I want does not support Python 3 . . . . . . . . . . . . . . . . . . . . . . 6
I don’t know if I need this. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Why would a network engineer need programming? . . . . . . . . . . . . . . . . 7
Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2 Book resources 9
Preparing the working environment . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
Further reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3 I. Python basics 13
1. Preparing for work . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
Working environment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
OS and editor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Package management system Pip . . . . . . . . . . . . . . . . . . . . . . . . . . 18
i
Virtual environment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Python interpreter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Further reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2. Using Git and Github . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Git fundamentals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Displaying repository status in invitation . . . . . . . . . . . . . . . . . . . . . . 27
Working with Git . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Additional features . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
Github authentication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
Working with own repository . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Working with repository of tasks and examples . . . . . . . . . . . . . . . . . . . 41
Further reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
Tasks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
3. Getting started with Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
Python syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
Python interpreter. IPython . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
IPython special commands . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
Tasks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
4. Python data types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
Numbers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
Dictionary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
Tuple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
Set . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
Boolean values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
Types conversion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
Types checking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
Method chaining . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Sorting basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
Further reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
Tasks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
5. Basic scripts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
Executable file . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
Passing arguments to the script (sys.argv) . . . . . . . . . . . . . . . . . . . . . 104
User input . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
Tasks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
6. Control structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
if/elif/else . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
break, continue, pass . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
for/else, while/else . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
ii
Working with try/except/else/finally . . . . . . . . . . . . . . . . . . . . . . . . . 136
Further reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
Tasks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
7. Working with files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
File opening . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
File reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
File writing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
File closing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
with statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
Examples of working with files . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
Further reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
Tasks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
8. Python basic examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
Formatting lines with f-strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
Variable unpacking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
List, dict, set comprehensions . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
Further reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
iii
os . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 258
ipaddress . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 260
tabulate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265
pprint . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269
Tasks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274
13. Iterators, iterable and generators . . . . . . . . . . . . . . . . . . . . . . . . . 276
Iterable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
Iterators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
Generator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Further reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 280
iv
Work with JSON files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 359
Work with YAML files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 366
Further reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 370
Tasks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372
v
Method creation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 541
Parameter self . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 543
Method __init__ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 545
Class example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 546
Class namespace . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 548
Class variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 548
Tasks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 551
23. Special methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 560
Underscore in names . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 560
Methods __str__, __repr__ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 564
Arithmetic operator support . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 566
Protocols . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 569
Tasks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 582
24. Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 587
Inheritance basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 587
Tasks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 592
vi
Underscore in names . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 690
Underscore in name . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 691
Two underscores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 693
Two underscores before name . . . . . . . . . . . . . . . . . . . . . . . . . . . . 693
Two underscores before and after name . . . . . . . . . . . . . . . . . . . . . . . 694
Python 2.7 and Python 3.6 distinctions . . . . . . . . . . . . . . . . . . . . . . . . . 695
Unicode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 695
print function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 696
input instead of raw_input . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 696
range instead of xrange . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 697
Dictionary methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 697
Variables unpacking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 698
Iterator instead of list . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 699
subprocess.run . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 699
Jinja2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 699
Modules pexpect, telnetlib, paramiko . . . . . . . . . . . . . . . . . . . . . . . . 700
Trivia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 700
Additional information . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 700
Preparing Windows . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 701
Installing Python 3.7 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 701
Cmder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 701
Installing Mu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 701
Tips for completing tasks on windows . . . . . . . . . . . . . . . . . . . . . . . . 701
Preparing Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 704
Installing Python 3.7 on Debian 9 . . . . . . . . . . . . . . . . . . . . . . . . . . 704
Virtual environment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 704
List of modules that need to be installed to complete tasks . . . . . . . . . . . . . 705
vii
viii
Python for network engineers, Release 1.0
The book covers Python basics with examples and tasks built around networking topics.
On the one hand, this book is basic enough to be mastered by anyone, and on the other hand,
covers all the main topics that will allow you to further grow on your own. This book is not intended
to be an in-depth look at Python. The purpose of this book is to explain the basics of Python in clear
language and provide an understanding of the necessary tools for practical use. Everything in the
book is focused on network equipment and interaction with it. This immediately makes it possible
to use the knowledge gained in the daily work of network engineers. All examples are shown using
Cisco equipment as an example, but of course they apply to any other equipment.
The book was written by Natasha Samoylenko. Translated from Russian by Aidar Khairullin.
1
Python for network engineers, Release 1.0
2
Introduction
1
About book
From the one hand, book is basic enough so everyone can handle it, from the other hand, book
covers all main topics which allow you to develop skill independently in the future. Python deep
dive is not a goal of this book. The goal is to explain Python basics in plain language and provide
understanding of necessary tools for practical usage. Everything in this book is focused on network
equipment and interaction with it. It right away gives opportunity to use knowledge gained at the
course in network engineers daily work. All shown examples are based on Cisco equipment but, of
course, they could be applied to any other equipment.
For network engineers with or without programming experience. All examples and homework will be
formed with a focus on network equipment. This book will be useful for network engineers who want
to automate their daily basis routine tasks and want start coding but don’t know how to approach
this. Still haven’t decided whether it worth reading this book? Read feedbacks.
All examples and terminal outputs in the book are shown on Debian Linux. Python 3.7 is used in
this book but for the majority of examples Python 3.x will be enough. Only some examples requires
Python version higher than 3.6. It always explicitly indicated and generally concerns some additional
features.
3
Python for network engineers, Release 1.0
Examples
All examples from the book resides in repository. All examples have educational purpose. It means
they not necessarily show the best solution since they are based on information which was covered
in previous chapters. Moreover, often enough the examples in chapters are developing in tasks. In
other words, in tasks you have to create better, more universal and, in general, more proper version
of code. It’s better to write code from the book on your own or at least download examples and try
to modify them. So the information will be better remembered. If you don’t have this possibility,
for example when you read book on road, it’s better to repeat examples later on your own. In any
case, it’s necessary to do tasks manually.
Tasks
All tasks and auxiliary files can be downloaded from the same repository, where code examples are
located.
Book formats
Book is available in PDF and Epub formats. Both of them are being updated automatically.
No, there will be no printed version. The book has existed in some form since late 2015. All this time
the book has been changing. I love this opportunity to change the book, write something differently.
There are still a huge number of useful topics and it is simply impossible to fit all of them into one
book. Of course, each reader has priorities and it seems that this particular module is very necessary
for everyone, but there are a lot of such topics/modules. Globally, nothing will change in the book,
new topics will not be added.
How does this differ from the regular Python introductory book?
4 Chapter 1. Introduction
Python for network engineers, Release 1.0
• High-level thinking - it’s easier to rise above everything when you free of routine work. You’ll
have time and opportunity to think of improvements
• Trust - you won’t be afraid to make changes that are often risky because the network is the
backbone of every applications and the cost of error is high
• A coherent configuration - you will able to automatically create network configuration files,
from users and interface descriptions to security functionality and you’ll be less worried about
whether you have forgotten something
Of course, it won’t be that after reading the book you “automate everything and happiness will
come” but this is a step in this direction. I am in no way encouraging for all automation to be done
via bunch of scripts. If there is some software that solves your needs, that’s great, use it. But if
there isn’t or if you are just haven’t thought about it yet, try to start with a simple - Ansible, for
example, allows to perfrom many tasks almost “out of the box”.
Why then learn Python? The fact is that the same Ansible won’t solve everything. And you may need
to add some functionality independently. In addition, apart of equipment configuration adjustment,
there are daily routine tasks that can be automated by Python. Let’s just say that if you don’t want
to deal with Python, but want to automate setup and operation processes, please turn attention on
Ansible. Even “out of the box” it will be very useful. Later, if you get taste for it and you want to add
something that missed in Ansible, come back :-)
And yes, this course is not only about how to use Python for network equipment configuration and
connecton to it. It’s also about how to solve tasks that are not connected to the equipment. For
example, change something in multiple configuration files or parse log-file - Python will help you
solve these tasks.
• Network engineers already have experience in IT and some of concepts are familiar to them
and it is likely that some programming basics will be familiar to most. This means that it will
be much easier to deal with Python
• Network engineers have a familiar knowledge domain on which to build examples and tasks
If you tell on abstract examples “about cats and bunnies”, it is one thing. But when you have the
ability to use ideas from subject area in examples, things get easier, you get concrete ideas about
how to improve a program, a script. And when a person tries to improve it, they start to deal with
something new - it’s a very powerful way to move forward.
Why Python?
• Some equipment has Python embedded or has an API that supports Python
• Python is simple enough to learn (of course, it is relatively, and another language may seem
simpler but it is rather to be because of experience with the language than because Python is
complex)
• With Python you will not quickly reach the limits of language capabilities
• Python can be used not only to write scripts but also to develop applications. Of course, this
is not the task of this book but at least you will spend your time on a language that will allow
you to go further than simple scripts
And one more point - in the context of book, Python should not be seen as the only correct variant
nor as the “correct” language. No, Python is just a tool like a screwdriver and we learn to use it for
specific tasks. That is, there is no ideological background here, no “only Python” and no worship
especially. It is strange to worship a screwdriver :-) Everything is simple - there is a good and
convenient tool that will approach different tasks. He’s not the best language at all and he’s not
the only language at all. Start with it and then you can choose something else if you want to - that
knowledge will still be there.
• Try to find an alternative module that supports Python 3 (not necessarily the latest version of
language)
• Try to find a community version of this module for Python 3. There may not be an official version
but the community could translate it independently to version 3, especially if this module is
popular
• If you use Python 2.7, nothing terrible will happen. If you’re not going to write a huge application
but you’re just using Python to automate your problems, Python 2.7 will definitely work
6 Chapter 1. Introduction
Python for network engineers, Release 1.0
I, of course, think you need this :-) Otherwise I wouldn’t be writing this book. You don’t necessarily
want to go into all this stuff, so you might want to start with Ansible. Perhaps you’ll have enough
of it for a long time. Start with simple show commands, try to connect first to test equipment
(virtual machines), then try to execute show command on real network, on 2-3 devices, then on
more. If that’s enough for you, you can stop there. The next step is to try using Ansible to generate
configuration patterns.
In my opinion, programming is very important for a network engineer, not because everybody’s
talking about it right now or because everybody’s scaring with SDN, job loss or something like that,
but because network engineer is constantly facing with:
• Routine tasks
At present, a large amount of equipment still offers us only the command line interface and un-
structured output of commands. Software is often limited to a vendor, expensive and has reduced
possibilities - we end up doing the same thing over and over again by hand. Even banal things like
sending the same show command to 20 devices are not always easy to do. Suppose your SSH client
supports this feature. And what if you now need to analyze the output? We are limited by the means
we have been given and knowledge of programming, even the most basic, allows us to expand our
means and even create new ones. I don’t think everyone should be rushing to learn programming
but for an engineer that’s a very important skill. It’s for engineer, not everyone.
Now clearly there is a tendency that can be described by phrase “everybody is learning to code”
and it is, in general, good. But programming is not something elementary, it’s difficult, it’s time-
consuming, especially if you’ve never had relation to technology world. It might give an impression
that it’s enough to pass these courses and after 3 months you are great programmer with high
salary. No, this book is not about that :-) We don’t talk about programming as a profession in it and
we don’t set such a goal, we’re talking about programming as a tool such as knowing CLI Linux. It’s
not that engineers are anything special but, in general:
This does not mean that everybody else is “not allowed”. It will just be easier for the engineers.
Acknowledgments
Thank you to all who expressed interest in the first announcement of the course - your interest
confirmed that someone would need it.
Pavel Pasynok, thank you for agreeing to course. It’s been interesting working with you and it’s
given me an incentive to finish the course and I’m particularly glad that the knowledge that you’ve
learned from course has found practical application.
Alexey Kirillov, thank you very much :-) I have always been able to discuss with you any question on
course. You helped me maintain my motivation and not get in a muddle. Communicating with you
has inspired me to continue, especially in difficult moments. Thank you for your inspiration, positive
emotions and support!
Thanks to all those who wrote comments on book - thanks to you now book not only has fewer
typographical errors and typos, but also the contents of book have improved.
Thanks to all participants of online course - thanks to your questions book has become much better.
Slava Skorokhod, thank you so much for volunteering to be an editor - number of errors is now going
to zero :-)
8 Chapter 1. Introduction
Book resources
2
Resources that will come in handy during the learning process:
• answers
Exercises
All tasks and auxiliary files can be downloaded from the repository. Tasks are duplicated in the book
solely for a convenient overview of all tasks in the section. Since all supporting files, code, and
tests are in the repository, it is best to do tasks in a copy of the repository. How to make a copy is
described in section 2. Using Git and GitHub.
9
Python for network engineers, Release 1.0
Sometimes, a certain section can be especially difficult, in this case, you can stop at a minimum of
tasks. This will allow you to move on and not abandon your studies. Later you can come back and
finish the tasks. In general, of course, it is better to do all the tasks, since practice is the only way
to properly learn the topic, but sometimes it is better to do fewer tasks and continue studying than
to get stuck on one topic and abandon everything.
Tests
There are automated tests in the repository for checking assignments. They help to check whether
everything matches the task, and also give feedback on what does not correspond to the task. As a
rule, after the first period of adaptation to tests, it becomes easier to do tasks with tests.
Further reading
Almost every book chapter has subchapter “Further reading” which includes useful materials and
references on the subject, plus references to official documentations. Moreover, I prepared a collec-
tion of resources on “Python for network engineers” topic where you can find a lot of useful articles,
books, video courses and podcasts.
Further reading 11
Python for network engineers, Release 1.0
• Control structures
13
Python for network engineers, Release 1.0
In order to start working with Python, you need to decide on a few things:
• operating system
• editor
• Python version
This book uses Debian Linux (on other OS the output may differ slightly) and Python 3.7.
Working environment
pip install pytest pytest-clarity pyyaml tabulate jinja2 textfsm pexpect netmiko␣
,→graphviz
Cloud service
• repl.it – this service provides an online Python interpreter as well as a graphics editor.
• PythonAnywhere - a separate virtual machine. In the free version you can work only from the
command line, that is, there is no graphical text editor
Network equipment
For the 18th section of the book, you need to prepare virtual or real network equipment.
All examples and tasks in which network equipment is used use the same number of devices: three
routers with the following basic settings:
• user: cisco
• password: cisco
• IP addresses must be accessible from the virtual machine on which you perform tasks and can
be assigned on physical/logical/loopback interfaces
Basic config:
hostname R1
!
no ip domain lookup
ip domain name pyneng
!
crypto key generate rsa modulus 1024
ip ssh version 2
!
username cisco password cisco
enable secret cisco
!
line vty 0 4
logging synchronous
login local
transport input telnet ssh
interface ...
ip address 192.168.100.1 255.255.255.0
Aliases (optional)
!
alias configure sh do sh
alias exec ospf sh run | s ^router ospf
alias exec bri show ip int bri | exc unass
alias exec id show int desc
alias exec top sh proc cpu sorted | excl 0.00%__0.00%__0.00%
alias exec c conf t
alias exec diff sh archive config differences nvram:startup-config system:running-
,→config
Optionally, you can configure the EEM applet to display the commands that the user enters:
!
event manager applet COMM_ACC
event cli pattern ".*" sync no skip no occurs 1
action 1 syslog msg "User $_cli_username entered $_cli_msg on device $_cli_host "
!
OS and editor
You can choose any OS and any editor but it is better to use Python version 3.7 because book uses
this version. All examples in book were run on Debian, other operating systems may have a slightly
different output. You can use Linux, macOS or Windows to perform tasks from a book.
You can select any text editor or IDE that supports Python. Generally, working with Python requires
minimal editor settings and often the editor recognizes Python by default.
Mu editor
Mu has clean and simple user interface. It has important features such as checking code against
PEP 8 and debugger. Plus, Mu runs on different operating systems (macOS, Windows, Linux).
Note: Mu tutorials
IDE PyCharm
PyCharm — is an integrated development environment for Python. For beginners it may be difficult
option due to the large number of settings but it depends on personal preferences. Pycharm supports
a huge number of features, even in free version.
Pycharm is a great IDE but I think it’s a little difficult for beginners. I wouldn’t recommend using it if
you’re not familiar with it and you’re just starting to learn Python. You can always switch to it after
book but for now it’s better to try something else.
Geany
Geany - is a text editor that supports different programming languages, including Python. It is also
a cross-platform editor and supports Linux, macOS, and Windows.
Note: Editor variants above are given for example, they can be replaced by any text editor that
supports Python.
Pip will be used to install Python packages. It is a package management system used to install
packages from Python Package Index (Pypi). Most likely, if you already have Python installed, pip is
installed.
$ pip --version
pip 19.1.1 from /home/vagrant/venv/pyneng-py3-7/lib/python3.7/site-packages/pip␣
,→(python 3.7)
Module installation
To delete package:
pip or pip3
Depending on how Python is installed and configured in system it may be necessary to use pip3
instead of pip. To check which option is used, you should execute command pip --version.
$ pip --version
pip 9.0.1 from /usr/local/lib/python2.7/dist-packages (python 2.7)
$ pip3 --version
pip 19.1.1 from /home/vagrant/venv/pyneng-py3-7/lib/python3.7/site-packages/pip␣
,→(python 3.7)
If system uses pip3, then every time a Python module is installed in book it will be necessary to
replace pip with pip3.
Thus, it is always clear for which version of Python the package is installed.
Virtual environment
Virtual environments:
• Packages that are needed by different projects are in different places - for example, if one
project requires 1.0 package and another project requires the same package but version 3.1,
they will not interfere with each other
• Packages that are installed in virtual environments do not impact on global packages
Note: Python has several options for creating virtual environments. You can use any of them. To
start with, you can use virtualenvwrapper and then eventually you can figure out which option you
prefer.
virtualenvwrapper
After installation, in . bashrc file in current user’s home folder, you need to add several lines:
export VIRTUALENVWRAPPER_PYTHON=/usr/local/bin/python3.7
export WORKON_HOME=~/venv
. /usr/local/bin/virtualenvwrapper.sh
If you are using a command interpreter other than bash, see if it is supported in virtualenvwrapper
documentation. Environment variable VIRTUALENVWRAPPER_PYTHON points to Python command
line binary file, WORKON_HOME – points to location of virtual environments. The third line indicates
location of script installed with virtualenvwrapper package. To start virtualenvwrapper.sh script work
with virtual environments, bash must be restarted.
$ exec bash
This may not always be the right option. More on Stack Overflow.
The name of virtual environment is shown in parentheses before standard invitation. That means
you’re inside it. Virtualenvwrapper uses Tab to autocomplete name of virtual environment. This is
particularly useful when there are many virtual environments. Now “pyneng” directory was created
in directory specified in environment variable WORKON_HOME:
(pyneng)$ deactivate
$
$ workon pyneng
(pyneng)$
If you want to go from one virtual environment to another, you don’t need to do deactivate, you can
go directly through “workon”:
$ workon Test
(Test)$ workon pyneng
(pyneng)$
$ rmvirtualenv Test
Removing Test...
$
(pyneng)$ lssitepackages
ANSI.py pexpect-3.3-py2.7.egg-info
ANSI.pyc pickleshare-0.5-py2.7.egg-info
decorator-4.0.4-py2.7.egg-info pickleshare.py
decorator.py pickleshare.pyc
decorator.pyc pip-1.1-py2.7.egg
distribute-0.6.24-py2.7.egg pxssh.py
easy-install.pth pxssh.pyc
fdpexpect.py requests
fdpexpect.pyc requests-2.7.0-py2.7.egg-info
FSM.py screen.py
FSM.pyc screen.pyc
IPython setuptools.pth
(continues on next page)
ipython-4.0.0-py2.7.egg-info simplegeneric-0.8.1-py2.7.egg-info
ipython_genutils simplegeneric.py
ipython_genutils-0.1.0-py2.7.egg-info simplegeneric.pyc
path.py test_path.py
path.py-8.1.1-py2.7.egg-info test_path.pyc
path.pyc traitlets
pexpect traitlets-4.0.0-py2.7.egg-info
Starting from version 3.5, it is recommended that Python use venv to create virtual environments:
Python or python3 can be used instead of python 3.7, depending on how Python 3.7 is installed.
This command creates specified directory and all necessary subdirectories within it if they have not
been created.
$ ls -ls new/pyneng
total 16
4 drwxr-xr-x 2 vagrant vagrant 4096 Aug 21 14:50 bin
4 drwxr-xr-x 2 vagrant vagrant 4096 Aug 21 14:50 include
4 drwxr-xr-x 3 vagrant vagrant 4096 Aug 21 14:50 lib
4 -rw-r--r-- 1 vagrant vagrant 75 Aug 21 14:50 pyvenv.cfg
$ source new/pyneng/bin/activate
$ deactivate
Package installation
If you open Python interpreter and import simplejson, it is available and there are no errors:
(pyneng)$ python
>>> import simplejson
>>> simplejson
<module 'simplejson' from '/home/vagrant/venv/pyneng-py3-7/lib/python3.7/site-
,→packages/simplejson/__init__.py'>
>>>
But if you exit from virtual environment and try to do the same thing, there is no such module:
(pyneng)$ deactivate
$ python
>>> import simplejson
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'simplejson'
>>>
Python interpreter
Before you start, check that when you call Python interpreter, the output is:
$ python
Python 3.7.3 (default, May 13 2019, 15:44:23)
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
Output shows that Python 3.7 is set. Invitation >>>, this is a standard invitation from Python inter-
preter. Interpreter call is executed by python command and to exit you need to type quit(), or
press Ctrl+D.
Further reading
Documentation:
• pip
• venv
• virtualenvwrapper
• PythonEditors
• IntegratedDevelopmentEnvironments
Exercises
Task 1.1
To do that:
• Install Python 3.7. Verify that Python and pip are installed
Warning: This is a difficult section to understand. Especially because this section is at the very
beginning of the book. You can skip it, but I would highly recommend trying to understand the
basics of git/Github and use it to store tasks.
There are a lot of tasks in book and you have to store them somewhere. One option is to use Git
and Github to do this. Of course, there are other ways to do this but Github can be used for other
things in future. Tasks and examples from book are in a separate repository on Github. They can
be downloaded as a zip archive but it is better to work with repository using Git, then you can see
changes made and easily update repository. If this is the first time working with Git and especially if
this is the first version control system you work with, there are a lot of information, so this chapter
focuses on practical side of question and it says:
There will be no much theory in this subsection but references to useful resources are listed. Try
doing all basic settings for tasks and then continue reading the book. And at the end, when basic
work with Git and Github is already routine, read more about them. What could Git be useful for:
Github allows you to centrally store all above items, but it should be taken into account that these
resources will be available to others as well. Github also has private repositories, but even these
probably should not contain information such as passwords. Of course, main use of Github is to
place code of various projects. In addition, Github can be also used to:
• together with GitBook, it is also a platform for publishing books, documentation, etc
Git fundamentals
Git is a distributed version control system (Version Control System, VCS) that is widely used and
released under GNU GPL v2 license. It can:
Git stores changes as a snapshot of entire repository. This snapshot is created after each “commit”
command.
Git installation:
To start working with Git you need to specify user name and e-mail that will be used to synchronize
local repository with Github repository:
Repository initialization
[~/tools/first_repo]
$ git init
Initialized empty Git repository in /home/vagrant/tools/first_repo/.git/
After executing this command, current directory creates .git folder containing service files needed
for Git.
This is an additional functionality that is not required to work with Git but is very helpful in this
regard. When working with Git it is very convenient when you can immediately determine whether
you are in a regular directory or in a Git repository. In addition, it would be good to understand
status of current repository. To do this, you need to install a special utility that will show status of
repository. To install utility, copy its repository to user’s home directory under which you work:
cd ~
git clone https://github.com/magicmonty/bash-git-prompt.git .bash-git-prompt --
,→depth=1
GIT_PROMPT_ONLY_IN_REPO=1
source ~/.bash-git-prompt/gitprompt.sh
exec bash
In my configuration command line invitation is spread over several lines, so you will have a different
one. Please note that additional information appears when you move to repository.
[~]
vagrant@jessie-i386:
$
There are a few basic commands you need to know to work with Git.
git status
When working with Git it is important to understand current status of repository. For this purpose
Git has a git status command
Git reports that we are in master branch (this branch is auto-created and used by default) and that
it has nothing to commit. Git also offers to create or copy files and then use git add command to
start Git tracking them.
$ vi README
$ echo "test" >> README
Two files came out because I have undo-files configured for Vim. These are special files that allow
you to undo changes not only in current file session but also in the previous sessions. Note that
Git reports there are files that it does not track and tells you using which command you can start
tracking.
File .gitignore
Undo-file .README.un~ is a special file that does not need to be added to repository. Git has option
to specify which files or directories to ignore. To do this, you need to create appropriate templates
in .gitignore file in repository directory.
To make Git ignore Vim undo-files you can add such a line to .gitignore file
*.un~
This means that Git must ignore all files that end with “.un~”.
Note that there is no .README.un~ file in the output. Once a file was added to repository .gitignore,
files that are listed in it are being ignored.
git add
Or all files
git commit
After all necessary files have been added in staging, you can commit changes. Staging is a collection
of files that will be added to the next commit. Command git commit has only one mandatory
parameter - flag “-m”. It allows you to specify a message for this commit.
Phrase “nothing to commit, working directory clean” indicates that there are no changes to add to
Git or to commit.
Additional features
git diff
Command git diff allows you to see the difference between different states. For example,
README and .gitignore files have been changed in repository.
Command git status shows that both files have been changed
Command git diff command shows what changes have been made since last commit
If you add changes made to staging via git add command and run git diff again, it will show
nothing
To show the difference between staging and last commit, add parameter --staged
Commit changes
git log
Command git log command shows when last changes were made
By default, command displays all commits starting from the nearest time. With help of additional
parameters it is possible not only to look at information about commits but also what changes have
been made.
Flag -p allows you to display the differences that have been made by each commit
Github authentication
In order to start working with GitHub, you need to register on it. It is better to use SSH key authen-
tication to work safely with Github.
On all questions, just press Enter. It is more secure to use passphrase but if you press Enter when
asked then passphrase will not be requested from you permanently during operations with reposi-
tory.
$ ssh-add ~/.ssh/id_rsa
To add a key you have to copy it. For example, you can display key with cat to copy it:
$ cat ~/.ssh/id_rsa.pub
After copying, go to Github. When you are on any Github page, in upper right-hand corner click on
picture of your profile and select “Settings” in drop down list. In list on the left, select field “SSH and
GPG keys”. Then press “New SSH key” and in “Title” field write key name (for example “Home”) and
in field “Key” insert the content that was copied from file ~/. ssh/id_rsa.pub.
To check if everything has been successful, try executing command ssh -T [email protected].
$ ssh -T [email protected]
Hi username! You've successfully authenticated, but GitHub does not provide shell␣
,→access.
This chapter covers how to work with repository on your local machine.
• log in to GitHub
• In upper right corner press plus and select “New repository” to create a new repository
You can put “Initialize this repository with a README”. This will create a README.md file that only
contains repository name.
As a result, in current directory in which git clone was executed, a directory with name of repository
will appear, in my case - “online-2-natasha-samoylenko”. This directory now contains the contents
of Github repository.
The previous command not only copied repository to use it locally, but also configured Git accord-
ingly:
Now you have a complete local Git repository where you can work. Typically, sequence of steps will
be as follows:
• Before starting, synchronize local content with Github using git pull command
When working with tasks at work and at home, it is necessary to pay special attention to first and
last step:
All commands are executed inside repository directory (in example above - online-2-natasha-
samoylenko).
If contents of local repository are the same as those of remote repository, output will be:
$ git pull
Already up-to-date.
$ git pull
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 5 (delta 4), reused 5 (delta 4), pack-reused 0
Unpacking objects: 100% (5/5), done.
From ssh://github.com/pyneng/online-2-natasha-samoylenko
89c04b6..fc4c721 master -> origin/master
Updating 89c04b6..fc4c721
Fast-forward
exercises/03_data_structures/task_3_3.py | 2 ++
1 file changed, 2 insertions(+)
If you want to add a specific file (in this case, README.md), you need to enter git add README.md
command. All files of current directory are added by git add . command.
Commit
You should specify message when you are running a commit. It is better if message is with meaning,
rather than just “update” or similar. Commit could be done by a command similar to git commit
-m "Tasks 4.1-4.3 are completed".
Push on GitHub
Before executing git push you can run git log -p/origin.. - it will show what changes you are
going to add to your repository on Github.
Examples and tasks are sometimes updated, so it will be more convenient to clone this repository
to your machine and periodically update it.
If you need to update local repository to synchronize it with Github version, you need to perform
git pull from inside the created pyneng-examples-exercises directory.
$ git pull
Already up-to-date.
$ git pull
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 3 (delta 2), reused 3 (delta 2), pack-reused 0
Unpacking objects: 100% (3/3), done.
From https://github.com/natenka/pyneng-examples-exercises
49e9f1b..1eb82ad master -> origin/master
Updating 49e9f1b..1eb82ad
Fast-forward
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
View changes
If you want to see what changes have been made, you can use git log:
$ git log -p -1
commit 98e393c27e7aae4b41878d9d979c7587bfeb24b4
Author: Nataliya Samoylenko <[email protected]>
Date: Fri Aug 18 17:32:07 2017 +0300
Update task_24_4.md
In this command -p flag indicates that the output of Linux diff utility should be displayed for changes,
not just commit comment. In turn, -1 indicates that only the latest commit should be shown.
The previous version of git log relies on number of commands but this is not always convenient.
Before executing git pull you can see what changes have been made since last synchronization.
Update README.md
In this case, changes were only in one file. This command will be very useful to see what changes
have been made to tasks and which tasks. This will make it easier to navigate and to understand
whether it is related to tasks you have already done and, if so, whether they should be changed.
Note: “..origin/master” in git log -p ..origin/master means to show all commits that are
present in origin/master (in this case, it’s GitHub) but that are not in local copy of repository
If changes were in tasks you haven’t yet done, this output will tell you which files should be copied
from course repository to your personal repository (and maybe the entire section if you haven’t yet
done tasks from this section).
Further reading
Documentation:
• Authenticating to GitHub
About Git/GitHub:
• git/github guide. a minimal tutorial - minimum knowledge required to work with Git and GitHub
Tasks
Warning: Starting from section “4. Data types in Python” there are automated tests for testing
tasks. They help to check whether everything matches the task, and also give feedback on what
does not correspond to the task. As a rule, after the first period of adaptation to tests, it becomes
easier to do tasks with tests. Testing is done using the pyneng utility. Learn more about how to
work with the pyneng utility.
Task 2.1
Create your repository based on repository template with tasks and examples. To do this, press “Use
this template”.
• Python syntax
• Python variables
Python syntax
The first thing that tends to catch your eye when it comes to Python syntax is that indentation
matters:
a = 10
b = 5
if a > b:
print("A greater than B")
print(a - b)
else:
print("B is greater than or equal to A")
print(b - a)
print("End")
def open_file(filename):
print("Reading File", filename)
with open(filename) as f:
return f.read()
print("Ready")
Note: This code is shown for syntax demonstration. Although if/else statement has not yet been
covered, it is likely that the meaning of code will be clear in general.
Python understands which lines refer to “if” on indentation basis. Execution of a block if a > b
ends when another string with the same indent as string if a > b appears. Similarly to block else.
The second feature of Python is that some expressions must be followed by colon (for example, after
if a > b and after else).
• Tabs or spaces can be used as indents (it is better to use spaces or more precisely to configure
editor so that Tab is 4 spaces - then when using Tab key, 4 spaces will be placed instead of 1
tab sign).
• Number of spaces must be the same in one block (it is better to have the same number of
spaces in whole code - popular option is to use 2-4 spaces, for example, this book uses 4
spaces).
Another feature of code above is empty lines. It makes reading code easier. Other syntax features
will be shown during process of familiarization with data structures in Python.
Note: Python has a special document that describes how best to write Python code PEP 8 - Style
Guide for Python Code.
Comments
When writing code you often need to leave a comment, for example, to describe features of code.
One-line comments start with hash sign. Note that comment can be in line where code itself is or in
a separate line.
If it is necessary to write several lines with comments in order to not put hash sign before each line,
you can make a multi-line comment:
"""
Very important
and long comment
"""
a = 10
b = 5
Three double or three single quotes may be used for a multi-line comment. Comments can be used
both to comment on what happens in code and to exclude execution of a particular line or block of
code (i.e., to comment it).
Interpreter makes it possible to receive an instant response to executed actions. You can say that
interpreter works as CLI (Command Line Interface) of network devices: each command will be ex-
ecuted immediately after pressing Enter. However, there is an exception: more complex objects
(such as cycles or functions) are executed only after twice pressing Enter.
In previous section, a standard interpreter was called to verify Python installation. There is also
an improved interpreter IPython. IPython allows much more than standard interpreter called by
“python” command. Some examples (IPython features are much broader):
• Autocomplete Tab commands or hints if there are more than one command version
• You can either walk through the command execution history or watch it with %history ‘magic’
command
You can install IPython using pip (installation will be done in a virtual environment if configured):
$ ipython
Python 3.7.3 (default, May 13 2019, 15:44:23)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.5.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]:
Command quit is used to exit. The following is how IPython will be used.
In [1]: 1 + 2
Out[1]: 3
In [2]: 22*45
Out[2]: 990
In [3]: 2**3
Out[3]: 8
• Numbers after In or Out are sequential numbers of executed commands in current IPython
session
In [4]: print('Hello!')
Hello!
When a loop is created in interpreter, for example, invitation changes to ellipsis inside loop. To
complete loop and exit this shortcut, double press Enter:
help()
In IPython you can view help for an arbitrary object, function or method using help:
In [1]: help(str)
Help on class str in module builtins:
class str(object)
| str(object='') -> str
| str(bytes_or_buffer[, encoding[, errors]]) -> str
|
| Create a new string object from given object. If encoding or
| errors is specified, then object must expose a data buffer
| that will be decoded using given encoding and error handler.
...
In [2]: help(str.strip)
Help on method_descriptor:
strip(...)
S.strip([chars]) -> str
In [3]: ?str
Init signature: str(self, /, *args, **kwargs)
Docstring:
str(object='') -> str
str(bytes_or_buffer[, encoding[, errors]]) -> str
In [4]: ?str.strip
Docstring:
S.strip([chars]) -> str
Function print displays information on a standard output (current terminal screen). If you want
to get a string, you should place it in quotes(double or single). If you want to get, for example, a
computation result or just a number, quotes are not needed:
In [6]: print('Hello!')
Hello!
In [7]: print(5*5)
25
If you want to get several values in a row through a space, you have to enumerate them through a
comma:
By default, at the end of each expression passed to print, there will be a new line character. If it is
necessary that after the output of each expression there would be no new line, an additional “end”
argument should be specified as the last expression in print.
See also:
dir()
Function dir can be used to see what attributes (variables tied to object) and methods (functions
tied to object) are available.
For example, for number the output will be (pay attention on various methods that allow arithmetic
operations):
In [10]: dir(5)
Out[10]:
['__abs__',
'__add__',
'__and__',
...
'bit_length',
'conjugate',
'denominator',
'imag',
'numerator',
'real']
In [11]: dir('hello')
Out[11]:
['__add__',
'__class__',
'__contains__',
...
(continues on next page)
'startswith',
'strip',
'swapcase',
'title',
'translate',
'upper',
'zfill']
If you call dir with no value, it shows existing methods, attributes, and variables defined in current
session of interpreter:
In [12]: dir()
Out[12]:
['__builtin__',
'__builtins__',
'__doc__',
'__name__',
'_dh',
...
'_oh',
'_sh',
'exit',
'get_ipython',
'i',
'quit']
In [13]: a = 'hello'
In [15]: dir()
Out[15]:
...
'a',
'exit',
'get_ipython',
'i',
'quit',
'test']
IPython has special commands that make work with interpreter easier. All of them are started with
percent sign.
%history
For example, %history command allows to look at history of commands entered by user in current
IPython session.
In [1]: a = 10
In [2]: b = 5
In [3]: if a > b:
...: print("A is bigger")
...:
A is bigger
In [4]: %history
a = 10
b = 5
if a > b:
print("A is bigger")
%history
%time
..:
obj?, obj?? : Get help, or more help for object (also works as
?obj, ??obj).
?foo.*abc* : List names in 'foo' containing 'abc' in them.
%magic : Information about IPython's 'magic' % functions.
Magic functions are prefixed by % or %%, and typically take their arguments
without brackets, quotes or even commas for convenience. Line magics take a
single % and cell magics are prefixed with two %%.
%%timeit x=2**100
x**100 : time 'x**100' with a setup of 'x=2**100'; setup code is not
counted. This is an example of a cell magic.
System commands:
History:
_i, _ii, _iii : Previous, next previous, next next previous input
_i4, _ih[2:5] : Input history line 4, lines 2-4
exec _i81 : Execute input history line #81 again
%rep 81 : Edit input history line #81
_, __, ___ : previous, next previous, next next previous output
_dh : Directory history
_oh : Output history
%hist : Command history of current session.
%hist -g foo : Search command history of (almost) all sessions for 'foo'.
%hist -g : Command history of (almost) all sessions.
%hist 1/2-8 : Command history containing lines 2-8 of session 1.
%hist 1/ ~2/ : Command history of session 1 and 2 sessions before current.
Variables
Variables in Python do not require variable type declaration (since Python is a language with dynamic
typing) and they are references to a memory area. Variable naming rules:
In [1]: a = 3
In [2]: b = 'Hello'
In [3]: c, d = 9, 'Test'
In [4]: print(a,b,c,d)
3 Hello 9 Test
Note that Python does not need to specify that “a” is a number, and “b” is a string.
Variables are references to memory area. This can be demonstrated by using id() which shows
object ID:
In [5]: a = b = c = 33
In [6]: id(a)
Out[6]: 31671480
In [7]: id(b)
Out[7]: 31671480
In [8]: id(c)
Out[8]: 31671480
In this example you can see that all three names refer to the same identifier, so it is the same object
to which three references “a”, “b” and “c” point. Python numbers has one feature that can be
slightly misleading: numbers from -5 to 256 are pre-created and stored in an array (list). Therefore,
when you create a number from this range you actually create a reference to number in generated
array.
In [9]: a = 3
In [10]: b = 3
In [11]: id(a)
Out[11]: 4400936168
In [12]: id(b)
Out[12]: 4400936168
In [13]: id(3)
Out[13]: 4400936168
Note that a, b and number 3 have identical identifiers. They are all references to an existing number
in the list.
If you do the same with number more than 256, all identifiers will be different:
In [14]: a = 500
In [15]: b = 500
In [16]: id(a)
Out[16]: 140239990503056
In [17]: id(b)
Out[17]: 140239990503032
In [18]: id(500)
Out[18]: 140239990502960
However, if you assign variables to each other, identifiers are all the same (in this version a, b and
c are referring to the same object):
In [19]: a = b = c = 500
In [20]: id(a)
Out[20]: 140239990503080
In [21]: id(b)
Out[21]: 140239990503080
In [22]: id(c)
Out[22]: 140239990503080
Variable names
Variable names should not overlap with names of operators and modules or other reserved words.
Python has recommendations for naming functions, classes and variables:
• variable names are usually written in lowercase or in uppercase (e.g., DB_NAME, db_name)
• function names are written in lowercase, with underline between words (for example
get_names)
• class names are given with capital letters without spaces, it is called CamelCase (for example,
CiscoSwitch)
Tasks
Warning: Starting from section “4. Data types in Python” there are automated tests for testing
tasks. They help to check whether everything matches the task, and also give feedback on what
does not correspond to the task. As a rule, after the first period of adaptation to tests, it becomes
easier to do tasks with tests. Testing is done using the pyneng utility. Learn more about how to
work with the pyneng utility.
Task 3.1
Install IPython in a virtual environment or globally in OS. After installation, running ipython command
should open IPython interpreter (the output may differ slightly):
$ ipython
Python 3.7.3 (default, May 13 2019, 15:44:23)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.5.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]:
• Numbers
• Strings
• Lists
• Dictionaries
• Tuples
• Sets
• Boolean
• unordered (sets)
Numbers
In [1]: 1 + 2
Out[1]: 3
In [2]: 1.0 + 2
Out[2]: 3.0
In [3]: 10 - 4
Out[3]: 6
In [4]: 2**3
Out[4]: 8
In [5]: 10/3
Out[5]: 3.3333333333333335
In [6]: 10/3.0
Out[6]: 3.3333333333333335
In [9]: round(10/3.0, 2)
Out[9]: 3.33
In [10]: round(10/3.0, 4)
Out[10]: 3.3333
Remainder of division:
In [11]: 10 % 3
Out[11]: 1
Comparison operators
In [13]: 10 < 3
Out[13]: False
In [14]: 10 == 3
Out[14]: False
In [15]: 10 == 10
Out[15]: True
In [16]: 10 <= 10
Out[16]: True
In [17]: 10.0 == 10
Out[17]: True
The int() function allows converting to int type. The second argument can specify number system:
In [18]: a = '11'
In [19]: int(a)
Out[19]: 11
If you specify that string should be read as a binary number, the result is:
In [20]: int(a, 2)
Out[20]: 3
In [21]: int(3.333)
Out[21]: 3
In [22]: int(3.9)
Out[22]: 3
The bin() function produces a binary representation of a number (note that the result is a string):
In [23]: bin(8)
Out[23]: '0b1000'
In [24]: bin(255)
Out[24]: '0b11111111'
In [25]: hex(10)
Out[25]: '0xa'
In [29]: math.sqrt(9)
Out[29]: 3.0
In [30]: math.sqrt(10)
Out[30]: 3.1622776601683795
In [31]: math.factorial(3)
Out[31]: 6
In [32]: math.pi
Out[32]: 3.141592653589793
Strings
Examples of strings:
In [9]: 'Hello'
Out[9]: 'Hello'
In [10]: "Hello"
Out[10]: 'Hello'
In [12]: tunnel
Out[12]: '\ninterface Tunnel0\n ip address 10.10.10.1 255.255.255.0\n ip mtu␣
,→1416\n ip ospf hello-interval 5\n tunnel source FastEthernet1/0\n tunnel␣
,→protection ipsec profile DMVPN\n'
In [13]: print(tunnel)
interface Tunnel0
ip address 10.10.10.1 255.255.255.0
ip mtu 1416
ip ospf hello-interval 5
tunnel source FastEthernet1/0
tunnel protection ipsec profile DMVPN
You can multiply a string by a number. In this case, string repeats specified number of times:
In [18]: intf * 5
Out[18]: 'interfaceinterfaceinterfaceinterfaceinterface'
In [19]: '#' * 40
Out[19]: '########################################'
The fact that strings are an ordered data type allows to refer to characters in a string by a number
starting from zero:
In [21]: string1[0]
Out[21]: 'i'
All characters in a string are numbered from zero. But if you need to refer to a character from the
end, you can specify negative values (this time with 1).
In [22]: string1[1]
Out[22]: 'n'
In [23]: string1[-1]
Out[23]: '0'
In addition to referring to a specific character you can make string slices by specifying a number
range. Slicing starts with first number (included) and ends at second number (excluded):
In [24]: string1[0:9]
Out[24]: 'interface'
In [25]: string1[10:22]
Out[25]: 'FastEthernet'
In [26]: string1[10:]
Out[26]: 'FastEthernet1/0'
In [27]: string1[-3:]
Out[27]: '1/0'
You can also specify a step in slice. For example, you can get odd numbers:
In [28]: a = '0123456789'
In [29]: a[1::2]
Out[29]: '13579'
In [31]: a[::2]
Out[31]: '02468'
In [28]: a = '0123456789'
In [29]: a[::]
Out[29]: '0123456789'
In [30]: a[::-1]
Out[30]: '9876543210'
Note: Entries a[::] and a[:] give the same result but double colon makes it possible to indicate
that not every element should be taken, but for example every second element.
In [2]: len(line)
Out[2]: 15
Note: Function and method differ in that method is tied to a particular type of object and function is
generally more universal and can be applied to objects of different types. For example, len function
can be applied to strings, lists, dictionaries and so on, but startswith method only applies to strings.
String methods
When automating, very often it will be necessary to work with strings, since config file, command
output and commands sent - are strings. Knowledge of various methods (actions) that can be applied
to strings helps to work with them more efficiently.
Strings are immutable data type, so all methods that convert string returns a new string and the
original string remains unchanged.
In [26]: string1.upper()
Out[26]: 'FASTETHERNET'
In [27]: string1.lower()
Out[27]: 'fastethernet'
In [28]: string1.swapcase()
Out[28]: 'fASTeTHERNET'
In [30]: string2.capitalize()
Out[30]: 'Tunnel 0'
It is very important to pay attention to the fact that methods often return the converted string. And,
therefore, we must not forget to assign it to some variable (you can use the same).
In [32]: print(string1)
FASTETHERNET
Method count
Method count used to count how many times a character or substring occurs in a string:
In [34]: string1.count('hello')
Out[34]: 3
In [35]: string1.count('ello')
Out[35]: 4
In [36]: string1.count('l')
Out[36]: 8
Method find
You can pass a substring or character to find and it will return the lowest index where first character
of the substring is (for the first match):
In [38]: string1.find('Fast')
Out[38]: 10
In [39]: string1[string1.find('Fast')::]
Out[39]: 'FastEthernet0/1'
Checking if a string starts or ends with certain symbols (methods startswith, endswith):
In [41]: string1.startswith('Fast')
Out[41]: True
In [42]: string1.startswith('fast')
Out[42]: False
In [43]: string1.endswith('0/1')
Out[43]: True
In [44]: string1.endswith('0/2')
Out[44]: False
Method replace
Method strip
Often when a file is processed, the file is opened line by line. But at the end of each line, there are
usually some special characters (and may be at the beginning). For example, new line character.
In [48]: print(string1)
interface FastEthernet0/1
In [49]: string1
Out[49]: '\n\tinterface FastEthernet0/1\n'
In [50]: string1.strip()
Out[50]: 'interface FastEthernet0/1'
By default, strip method removes blank characters. This character set includes: \t\n\r\f\v
Method strip can be passed as an argument of any characters. Then at the beginning and at the
end of the line all characters that were specified in the line will be removed:
In [52]: ad_metric.strip('[]')
Out[52]: '110/1045'
Method strip removes special characters at the beginning and at the end of the line. If you want
to remove characters only on the left or only on the right, you can use lstrip and rstrip.
Method split
Method split splits the string using a symbol (or symbols) as separator and returns a list of strings:
In [55]: print(commands)
['switchport', 'trunk', 'allowed', 'vlan', '10,20,30,100-200']
In example above, string1.split splits the string by spaces and returns a list of strings. The list
is saved to commands variable.
By default, separator is a space symbol (spaces, tabs, new line), but you can specify any separator
in parentheses:
In [57]: print(vlans)
['10', '20', '30', '100-200']
In commands list, the last element is a string with vlans, so the index -1 is used. Then string is split
into parts using split commands[-1].split(','). Since separator is a comma, this list is received
['10', '20', '30', '100-200'].
A useful feature of split method with default separator is that the string is not only split into a list of
strings by whitespace characters, but the whitespace characters are also removed at the beginning
and at the end of the line:
In [59]: string1.split()
Out[59]: ['switchport', 'trunk', 'allowed', 'vlan', '10,20,30,100-200']
Method split has another good feature: by default, method splits a string not by one whitespace
character, but by any number. For example, this will be very useful when processing show com-
mands:
In [61]: sh_ip_int_br.split()
Out[61]: ['FastEthernet0/0', '15.0.15.1', 'YES', 'manual', 'up', 'up']
And this is the same string when one space is used as the separator:
String formatting
When working with strings, there are often situations where different data needs to be substituted
in string template. This can be done by combining string parts and data, but Python has a more
convenient way - strings formatting.
A special symbol {} indicates that the value that is passed to format method is placed here. Each
pair of curly braces represents one place for the substitution.
Values that are placed in curly braces may be of different types. For example, it can be a string,
number or list:
In [3]: print('{}'.format('10.1.1.1'))
10.1.1.1
In [4]: print('{}'.format(100))
100
You can align result in columns by formatting strings. In string formatting, you can specify how
many characters are selected for the data. If number of characters in the data is less than number
of characters selected, the missing characters are filled with blanks.
For example, you can allign data in columns of equal width of 15 characters with right side alignment:
In [7]: print(ip_template.format('10.1.1.1'))
IP address:
10.1.1.1
You can also use string formatting to change the display format of numbers.
For example, you can specify how many digits after the comma to show:
In [9]: print("{:.3f}".format(10.0/3))
3.333
You can also specify that numbers should be supplemented with zeros instead of spaces:
You can enter names in curly braces. This makes it possible to pass arguments in any order and also
makes template more understandable:
For example this can prevent repetitive transmission of the same values:
IP address:
192 100 1 1
11000000 01100100 00000001 00000001
In example above the octet address has to be passed twice - one for decimal format and other for
binary.
By specifying value indexes that are passed to format method, it is possible to avoid duplication:
IP address:
192 100 1 1
11000000 01100100 00000001 00000001
Python 3.6 added a new version of string formatting - f-strings or interpolation of strings. The f-
strings allow not only to set values to template, but also to perform calls to functions, methods,
etc.
In many situations f-strings are easier to use than format, and f-strings work faster than format and
other methods of string formatting.
Syntax
F-string is a literal line with a letter f in front of it. Inside f-string, in curly braces there are names of
variables that will be substituted:
In [1]: ip = '10.1.1.1'
In [2]: mask = 24
The same result with format method you can achieve by: "IP: {ip}, mask: {mask}".
format(ip=ip, mask=mask).
A very important difference between f-strings and format: f-strings are expressions that are pro-
cessed, not just strings. That is, in case of ipython, as soon as we wrote the expression and pressed
Enter, it was performed and instead of expressions {ip} and {mask} the values of variables were
substituted.
Therefore, for example, you cannot first write a template and then define variables that are used in
template:
In addition to substituting variable values you can write expressions in curly braces:
After colon in f-strings you can specify the same values as in format:
In [10]: print(f'''
...: IP address:
...: {oct1:<8} {oct2:<8} {oct3:<8} {oct4:<8}
...: {oct1:08b} {oct2:08b} {oct3:08b} {oct4:08b}''')
IP address:
10 1 1 1
00001010 00000001 00000001 00000001
Warning: Since for full explanation of f-strings it is necessary to show examples with loops and
work with objects that have not yet been covered, this topic is also in the section Formatting
lines with f-strings with additional examples and explanations.
In [2]: s
Out[2]: 'TestString'
(continues on next page)
In [4]: s
Out[4]: 'TestString'
You can even wrap parts of a line on different lines, but only if they are in parentheses:
In [5]: s = ('Test'
...: 'String')
In [6]: s
Out[6]: 'TestString'
regex = (
'(\S+) +(\S+) +'
'\w+ +\w+ +'
'(up|down|administratively down) +'
'(\w+)'
)
This way, the regex can be split and made easier to understand. Plus you can add explanatory
comments in strings.
regex = (
'(\S+) +(\S+) +' # interface and IP
'\w+ +\w+ +'
'(up|down|administratively down) +' # Status
'(\w+)' # Protocol
)
In [8]: message
Out[8]: 'During command execution "{}" such error occured "{}".\nExclude this␣
,→command from the list? [y/n]'
List
Examples of lists:
In [3]: print(list1)
['r', 'o', 'u', 't', 'e', 'r']
Since a list is an ordered data type just like a string, in lists you can refer to an item by number,
make slices:
In [5]: list3[1]
Out[5]: 20
In [6]: list3[1::]
Out[6]: [20, 4.0, 'word']
In [7]: list3[-1]
Out[7]: 'word'
In [8]: list3[::-1]
Out[8]: ['word', 4.0, 20, 1]
In [11]: vlans.reverse()
In [12]: vlans
Out[12]: ['100-200', '30', '20', '15', '10']
In [13]: list3
Out[13]: [1, 20, 4.0, 'word']
In [15]: list3
Out[15]: ['test', 20, 4.0, 'word']
You can also create a list of lists. As in a regular list you can refer to items in nested lists:
In [17]: interfaces[0][0]
Out[17]: 'FastEthernet0/0'
In [18]: interfaces[2][0]
Out[18]: 'FastEthernet0/2'
In [19]: interfaces[2][1]
Out[19]: '10.0.2.1'
In [2]: len(items)
Out[2]: 3
And sorted function sorts list items in ascending order and returns a new list with sorted items:
In [2]: sorted(names)
Out[2]: ['Antony', 'John', 'Michael']
List methods
List is a mutable data type, so it is important to note that most list methods change a list in place
without returning anything.
join
Method join collects a list of strings into one string with separator specified before join:
In [17]: ','.join(vlans)
Out[17]: '10,20,30'
Note: Method join actually string method but since the value must be passed to it as a list, it is
covered here.
append
In [19]: vlans.append('300')
In [20]: vlans
Out[20]: ['10', '20', '30', '100-200', '300']
Method append changes list on spot and does not return anything.
extend
If you want to combine two lists you can use one of two methods: extend method or addition
operation. These methods have an important difference: extend changes list to which method is
applied and addition returns a new list that consists of two.
Method extend:
In [23]: vlans.extend(vlans2)
In [24]: vlans
Out[24]: ['10', '20', '30', '100-200', '300', '400', '500']
Addition operation:
Note that when adding lists in IPython the ‘Out’ line appeared. This means that the result of sum-
mation can be assigned to variable:
In [31]: result
Out[31]: ['10', '20', '30', '100-200', '300', '400', '500']
pop
Method pop removes item that corresponds to specified number. Method returns this item:
In [29]: vlans.pop(-1)
Out[29]: '100-200'
In [30]: vlans
Out[30]: ['10', '20', '30']
remove
Method remove removes specified item (remove does not return deleted item):
In [32]: vlans.remove('20')
In [33]: vlans
Out[33]: ['10', '30', '100-200']
In remove you must specify item to be deleted, not its index. If item number is specified, error
occurs:
In [34]: vlans.remove(-1)
-------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-32-f4ee38810cb7> in <module>()
----> 1 vlans.remove(-1)
index
In [36]: vlans.index('30')
Out[36]: 2
insert
In [39]: vlans
Out[39]: ['10', '15', '20', '30', '100-200']
sort
In [41]: vlans.sort()
In [42]: vlans
Out[42]: [1, 10, 15, 50]
Dictionary
• since dictionaries are mutable, dictionary items can be changed, added, removed
Note: In other programming languages a similar dictionary can be called an associative array,
hash, or hash table.
Example of dictionary:
london = {
'id': 1,
'name': 'London',
'it_vlan': 320,
'user_vlan': 1010,
'mngmt_vlan': 99,
'to_name': None,
'to_id': None,
(continues on next page)
'port': 'G1/0/11'
}
In order to get a value from dictionary you have to refer to key in the same way as in lists, only key
will be used instead of number:
In [2]: london['name']
Out[2]: 'London1'
In [3]: london['location']
Out[3]: 'London Str'
In [5]: print(london)
{'vendor': 'Cisco', 'name': 'London1', 'location': 'London Str'}
Or rewritten:
In [7]: print(london)
{'vendor': 'cisco ios', 'name': 'London1', 'location': 'London Str'}
london_co = {
'r1': {
'hostname': 'london_r1',
'location': '21 New Globe Walk',
'vendor': 'Cisco',
'model': '4451',
'ios': '15.4',
'ip': '10.255.0.1'
},
'r2': {
'hostname': 'london_r2',
'location': '21 New Globe Walk',
'vendor': 'Cisco',
'model': '4451',
(continues on next page)
'ios': '15.4',
'ip': '10.255.0.2'
},
'sw1': {
'hostname': 'london_sw1',
'location': '21 New Globe Walk',
'vendor': 'Cisco',
'model': '3850',
'ios': '3.6.XE',
'ip': '10.255.0.101'
}
}
In [7]: london_co['r1']['ios']
Out[7]: '15.4'
In [8]: london_co['r1']['model']
Out[8]: '4451'
In [9]: london_co['sw1']['ip']
Out[9]: '10.255.0.101'
Function sorted sorts dictionary keys in ascending order and returns a new list with sorted keys:
In [2]: sorted(london)
Out[2]: ['location', 'name', 'vendor']
Dictionary methods
clear
In [2]: london.clear()
In [3]: london
Out[3]: {}
copy
In [6]: id(london)
Out[6]: 25489072
In [7]: id(london2)
Out[7]: 25489072
In [9]: london2['vendor']
Out[9]: 'Juniper'
In this case london2 is another name that refers to dictionary london. And when you change london
dictionary, london2 dictionary changes as well because it’s a link to the same object.
In [12]: id(london)
Out[12]: 25524512
In [13]: id(london2)
Out[13]: 25563296
In [15]: london2['vendor']
Out[15]: 'Cisco'
get
In [17]: london['ios']
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-17-b4fae8480b21> in <module>()
----> 1 london['ios']
KeyError: 'ios'
Method get queries for key and if there is no key, returns None instead.
In [19]: print(london.get('ios'))
None
Method get() also allows you to specify another value instead of None:
setdefault
Method setdefault searches for key and if there is no key, instead of error it creates a key with
None value.
In [23]: print(ios)
None
In [24]: london
Out[24]: {'name': 'London1', 'location': 'London Str', 'vendor': 'Cisco', 'ios':␣
,→None}
In [25]: london.setdefault('name')
Out[25]: 'London1'
The second argument allows to specify which value should correspond to key:
In [27]: print(model)
Cisco3580
In [28]: london
Out[28]:
{'name': 'London1',
'location': 'London Str',
'vendor': 'Cisco',
'ios': None,
'model': 'Cisco3580'}
In [25]: london.keys()
Out[25]: dict_keys(['name', 'location', 'vendor'])
In [26]: london.values()
Out[26]: dict_values(['London1', 'London Str', 'Cisco'])
In [27]: london.items()
Out[27]: dict_items([('name', 'London1'), ('location', 'London Str'), ('vendor',
,→'Cisco')])
All three methods return special view objects that contains keys, values, and key-value pairs of
dictionary, respectively.
A very important feature of view is that they change together with dictionary. And in fact, they just
give you a way to look at objects, but they don’t make a copy of them.
In [30]: print(keys)
dict_keys(['name', 'location', 'vendor'])
Now keys variable corresponds to view dict_keys, in which three keys: name, location and vendor.
But if we add another key-value pair to dictionary, keys object will also change:
In [32]: keys
Out[32]: dict_keys(['name', 'location', 'vendor', 'ip'])
If you want to get a simple list of keys that will not be changed with dictionary changes, it is enough
to convert view to list:
In [34]: list_keys
Out[34]: ['name', 'location', 'vendor', 'ip']
del
In [37]: london
Out[37]: {'location': 'London Str', 'vendor': 'Cisco'}
update
Method update allows you to add contents of one dictionary to another dictionary:
In [40]: r1
Out[40]: {'name': 'London1', 'location': 'London Str', 'vendor': 'Cisco', 'ios':
,→'15.2'}
In [42]: r1
Out[42]:
{'name': 'london-r1',
'location': 'London Str',
'vendor': 'Cisco',
'ios': '15.4'}
Literal
dict
If you use strings as keys you can use this option to create a dictionary:
In [3]: r1
Out[3]: {'model': '4451', 'ios': '15.4'}
In [5]: r1
Out[5]: {'model': '4451', 'ios': '15.4'}
dict.fromkeys
In a situation where you need to create a dictionary with known keys but so far empty values (or
identical values), fromkeys method is very convenient:
In [6]: r1 = dict.fromkeys(d_keys)
In [7]: r1
Out[7]:
{'hostname': None,
'location': None,
'vendor': None,
'model': None,
'ios': None,
'ip': None}
By default fromkeys sets None value. But you can also pass your own value:
In [10]: models_count
Out[10]: {'ISR2811': 0, 'ISR2911': 0, 'ISR2921': 0, 'ASR9002': 0}
This option of creating a dictionary is not suitable for all cases. For example, if you use a mutable
data type in value, a reference to the same object will be created:
In [12]: routers
Out[12]: {'ISR2811': [], 'ISR2911': [], 'ISR2921': [], 'ASR9002': []}
In [13]: routers['ASR9002'].append('london_r1')
In [14]: routers
Out[14]:
{'ISR2811': ['london_r1'],
'ISR2911': ['london_r1'],
'ISR2921': ['london_r1'],
'ASR9002': ['london_r1']}
In this case, each key refers to the same list. Therefore, when a value is added to one of lists, others
are updated.
Note: A dictionary comprehension is better for this task. See section List, dict, set comprehensions
Tuple
Roughly speaking, a tuple is a list that can’t be changed. We can say that the tuple has read-only
permissions. It could be a defense against accidental change.
In [2]: print(tuple1)
()
In [6]: tuple_keys
Out[6]: ('hostname', 'location', 'vendor', 'model', 'ios', 'ip')
In [7]: tuple_keys[0]
Out[7]: 'hostname'
Function sorted sorts tuple elements in ascending order and returns a new list with sorted elements:
In [3]: sorted(tuple_keys)
Out[3]: ['hostname', 'ios', 'ip', 'location', 'model', 'vendor']
Set
Set is a mutable unordered data type. Set always contains only unique elements. Set in Python is a
sequence of elements that are separated by a comma and placed in curly braces.
In [2]: set(vlans)
Out[2]: {10, 20, 30, 40, 100}
In [4]: print(set1)
{40, 100, 10, 20, 30}
Set methods
add
In [2]: set1.add(50)
In [3]: set1
Out[3]: {10, 20, 30, 40, 50}
discard
Method discard allows deleting elements without showing an error if there is no element in set:
In [3]: set1
Out[3]: {10, 20, 30, 40, 50}
In [4]: set1.discard(55)
In [5]: set1
Out[5]: {10, 20, 30, 40, 50}
In [6]: set1.discard(50)
In [7]: set1
Out[7]: {10, 20, 30, 40}
clear
In [9]: set1.clear()
In [10]: set1
Out[10]: set()
Sets are useful in performing different operations such as finding union of sets, intersection and so
on.
In [3]: vlans1.union(vlans2)
Out[3]: {10, 20, 30, 50, 100, 101, 102, 200}
In [7]: vlans1.intersection(vlans2)
Out[7]: {100}
You cannot create an empty set using a literal set (in this case it will not be a set but a dictionary):
In [1]: set1 = {}
In [2]: type(set1)
Out[2]: dict
In [4]: type(set2)
Out[4]: set
Boolean values
In Python, not only True and False are considered True and False values.
• True value:
• False value:
– 0
– None
– empty string
– empty object
Other True and False values tend to follow the condition logically.
In [3]: empty_list = []
In [4]: bool(empty_list)
Out[4]: False
In [5]: bool(items)
Out[5]: True
In [6]: bool(0)
Out[6]: False
(continues on next page)
In [7]: bool(1)
Out[7]: True
Types conversion
Python has several useful built-in features that allow data to be converted from one type to another.
int
In [1]: int("10")
Out[1]: 10
Using int function you can convert a binary number into a decimal number (binary number must
be written as a string)
In [2]: int("11111111", 2)
Out[2]: 255
bin
In [3]: bin(10)
Out[3]: '0b1010'
In [4]: bin(255)
Out[4]: '0b11111111'
hex
In [5]: hex(10)
Out[5]: '0xa'
In [6]: hex(255)
Out[6]: '0xff'
list
In [7]: list("string")
Out[7]: ['s', 't', 'r', 'i', 'n', 'g']
set
This function is very useful when you need to get unique elements in a sequence.
tuple
In [15]: tuple("string")
Out[15]: ('s', 't', 'r', 'i', 'n', 'g')
str
In [16]: str(10)
Out[16]: '10'
Types checking
In [1]: int('a')
------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-42-b3c3f4515dd4> in <module>()
----> 1 int('a')
Error is perfectly logical. We’re trying to convert string ‘a’ into decimal format. For example, this
can be useful when you want to go through a list of strings and convert to a number the strings that
contain numbers, you can get that error. To avoid error, it would be nice to be able to check what
we’re working with.
isdigit
Python has such methods. For example, isdigit method can be used to check whether a string
consists only of digits:
In [2]: "a".isdigit()
Out[2]: False
In [3]: "a10".isdigit()
Out[3]: False
In [4]: "10".isdigit()
Out[4]: True
isalpha
Method isalpha makes it possible to check whether a string consists only of letters:
In [7]: "a".isalpha()
Out[7]: True
In [8]: "a100".isalpha()
Out[8]: False
isalnum
Method isalnum makes it possible to check whether a string consists of letters or numbers:
In [11]: "a".isalnum()
Out[1]: True
In [12]: "a10".isalnum()
Out[12]: True
type
Sometimes, depending on the result, a library or function can return different types of objects. For
example, if there is one object, string is returned. If several, tuple is returned. We have to construct
the program in different ways, depending on whether a string or a tuple has been returned.
In [13]: type("string")
Out[13]: str
Out[16]: True
Method chaining
Often, you need to perform several operations with data, for example:
In [3]: words
Out[3]: ['switchport', 'trunk', 'allowed', 'vlan', '10,20,30']
In [5]: vlans_str
Out[5]: '10,20,30'
In [7]: vlans
Out[7]: ['10', '20', '30']
In the script:
In this case, variables are used to store the intermediate result and subsequent methods/actions
are performed with the variable. This is a completely normal version of the code, especially at first
when it’s hard perceive more complex expressions.
However, in Python, there are often expressions in which actions or methods are applied one after
the other in one expression. For example, the previous code could be written like this:
print(vlans)
Since there are no expressions in parentheses that would indicate the priority of execution, every-
thing is executed from left to right.
First, line.split() is executed - we get the list, then to the resulting list applies [-1] - we get the
last element of the list, the line 10,20,30. The method split(",") is applied to this line and as a
result we get the list ['10', '20', '30'].
The main nuance when writing such chains, the previous method/action should return something
what the next method/action is waiting for. And it is imperative that something is returned, otherwise
there will be an error.
Sorting basics
When sorting data like list of lists or list of tuples, sorted sorts by the first element of nested lists
(tuples), and if the first element is the same, on the second:
In [1]: data = [[1, 100, 1000], [2, 2, 2], [1, 2, 3], [4, 100, 3]]
In [2]: sorted(data)
Out[2]: [[1, 2, 3], [1, 100, 1000], [2, 2, 2], [4, 100, 3]]
If the sort is done for a list of numbers that are written as strings, the sort will be lexicographic, not
natural, and the order will be:
In [7]: vlans = ['1', '30', '11', '3', '10', '20', '30', '100']
In [8]: sorted(vlans)
Out[8]: ['1', '10', '100', '11', '20', '3', '30', '30']
In [3]: sorted(ip_list)
Out[3]: ['10.1.1.1', '10.1.10.1', '10.1.11.1', '10.1.2.1']
How to solve the problem with sorting IP addresses is discussed in section “10. Useful functions”.
Further reading
Documentation:
String formatting:
Tasks
Warning: Starting from section “4. Data types in Python” there are automated tests for testing
tasks. They help to check whether everything matches the task, and also give feedback on what
does not correspond to the task. As a rule, after the first period of adaptation to tests, it becomes
easier to do tasks with tests. Testing is done using the pyneng utility. Learn more about how to
work with the pyneng utility.
Note: In section 4, the tests can be easily “tricked” into making the correct output without getting
results from initial data using Python. This does not mean that the task was done correctly, it is just
that at this stage it is difficult otherwise test the result.
Task 4.1
Using the prepared nat string, get a new string where the FastEthernet interface is replaced with
GigabitEthernet. Print the resulting new string to the standard output (stdout) using print.
Restriction: All tasks must be done using the topics covered in this and previous chapters.
nat = "ip nat inside source list ACL interface FastEthernet0/1 overload"
Task 4.2
Convert string in mac variable from XXXX:XXXX:XXXX format to XXXX.XXXX.XXXX format. Print the
resulting new string to the standard output (stdout) using print.
Restriction: All tasks must be done using the topics covered in this and previous chapters.
mac = "AAAA:BBBB:CCCC"
Task 4.3
Get the following list of VLANs from the config string: ["1", "3", "10", "20", "30", "100"]
Write the resulting list to the result variable. (this is the variable that will be checked in the test)
Print the resulting list to the standard output (stdout) using print.
Here is a very important point that you need to get exactly the list (data type), and not, for example,
a string that looks like the list shown.
Restriction: All tasks must be done using the topics covered in this and previous chapters.
Task 4.4
Vlans list is a list of VLANs collected from all devices on the network, therefore there are duplicate
VLAN numbers in the list.
Get a new list of unique VLAN numbers from the vlans list, sorted in ascending order of numbers.
To get the final list, you cannot delete specific vlans manually.
Write the resulting list to the result variable. (this is the variable that will be checked in the test)
Print the resulting list to the standard output (stdout) using print.
Restriction: All tasks must be done using the topics covered in this and previous chapters.
Task 4.5
From the strings command1 and command2, get a list of VLANs that exist in both command1 and
command2 (intersection).
Write the resulting list to the result variable. (this is the variable that will be checked in the test)
Print the resulting list to the standard output (stdout) using print.
Restriction: All tasks must be done using the topics covered in this and previous chapters.
Task 4.6
Process the ospf_route string and print the information to the stdout as follows:
Prefix 10.0.24.0/24
AD/Metric 110/41
Next-Hop 10.0.13.3
(continues on next page)
Restriction: All tasks must be done using the topics covered in this and previous chapters.
Task 4.7
Print the resulting new string to the standard output (stdout) using print.
Restriction: All tasks must be done using the topics covered in this and previous chapters.
mac = "AAAA:BBBB:CCCC"
Task 4.8
Convert the IP address in the ip variable to binary and print output in columns to stdout:
The output should be ordered in the same way as in the example output below:
• in columns
• column width 10 characters (in binary you need to add two spaces between columns to sepa-
rate octets among themselves)
10 1 1 1
00001010 00000001 00000001 00000001
Restriction: All tasks must be done using the topics covered in this and previous chapters.
ip = "192.168.3.1"
5. Basic scripts
Generally speaking, script is a regular file. This file stores the sequence of commands that you want
to execute.
Let’s start with basic script and print several strings on standard output. To do this, you need to
create an access_template.py file with this content:
print('\n'.join(access_template).format(5))
First, items in list are combined into a string that is separated by \n and VLAN number is inserted
into string using string formatting. After this you should save file and go to command line. This is
how the script execution looks like:
$ python access_template.py
switchport mode access
switchport access vlan 5
switchport nonegotiate
spanning-tree portfast
spanning-tree bpduguard enable
All scripts that will be created in this course have an extension .py. You can say that it is a «good
manners» - to create Python scripts with .py extension.
Executable file
In order for a file to be executable and not have to write “python” every time before calling a file,
you need to:
• the first line of file should have #!/usr/bin/env python or #!/usr/bin/env python3 depend-
ing on which version of Python is used by default
#!/usr/bin/env python3
print('\n'.join(access_template).format(5))
After that:
chmod +x access_template_exec.py
$ ./access_template_exec.py
Very often script solves some common problem. For example, script processes a configuration file.
Of course, in this case you don’t want to edit name of file every time with your hands in script. It
will be much better to pass file name as script argument and then use already specified file. The
sys module allows working with script arguments via argv.
Example of access_template_argv.py:
interface = argv[1]
vlan = argv[2]
print('interface {}'.format(interface))
print('\n'.join(access_template).format(vlan))
Script output:
Arguments that have been passed to script are substituted as values in template. Several points
need to be clarified:
• argv is a list
• argv contains not only arguments that passed to script but also name of script itself
First comes the name of script itself, then arguments in the same order.
User input
Sometimes it is necessary to get information from user. For example, request a password. The
input function is used to get information from the user:
In this case, information is immediately displayed to user, but in addition, information entered by
user can be stored in a variable and can be used later in script.
In [3]: print(protocol)
OSPF
In parentheses, a question is usually written that specifies what information needs to be entered. A
script that asks the user for information using input (file access_template_input.py):
First two lines request information from user. Line print('\n' + '-' * 30) is used to visually
separate information request from the output.
Script output:
$ python access_template_input.py
Enter interface type and number: Gi0/3
Enter VLAN number: 55
------------------------------
interface Gi0/3
switchport mode access
switchport access vlan 55
switchport nonegotiate
spanning-tree portfast
spanning-tree bpduguard enable
Tasks
Warning: Starting from section “4. Data types in Python” there are automated tests for testing
tasks. They help to check whether everything matches the task, and also give feedback on what
does not correspond to the task. As a rule, after the first period of adaptation to tests, it becomes
easier to do tasks with tests. Testing is done using the pyneng utility. Learn more about how to
work with the pyneng utility.
Task 5.1
In the task you need: ask the user to enter the device name (r1, r2 or sw1). Print information about
the corresponding device to standard output (information will be in the form of a dictionary).
$ python task_5_1.py
Enter name of device: r1
{"location": "21 New Globe Walk", "vendor": "Cisco", "model": "4451", "ios": "15.4
,→", "ip": "10.255.0.1"}
All tasks must be completed using only the topics covered. That is, this task can be solved without
using the if condition. Restriction: You cannot change london_co dictionary.
london_co = {
"r1": {
"location": "21 New Globe Walk",
"vendor": "Cisco",
"model": "4451",
"ios": "15.4",
"ip": "10.255.0.1"
},
"r2": {
"location": "21 New Globe Walk",
"vendor": "Cisco",
"model": "4451",
"ios": "15.4",
"ip": "10.255.0.2"
},
(continues on next page)
"sw1": {
"location": "21 New Globe Walk",
"vendor": "Cisco",
"model": "3850",
"ios": "3.6.XE",
"ip": "10.255.0.101",
"vlans": "10,20,30",
"routing": True
}
}
Task 5.1a
Modify the script from task 5.1 so that, in addition to the device name, the script requested and then
printed the device parameter as well.
$ python task_5_1a.py
Enter device name : r1
Enter parameter name: ios
15.4
All tasks must be completed using only the topics covered. That is, this task can be solved without
using the if condition.
london_co = {
"r1": {
"location": "21 New Globe Walk",
"vendor": "Cisco",
"model": "4451",
"ios": "15.4",
"ip": "10.255.0.1"
},
"r2": {
"location": "21 New Globe Walk",
"vendor": "Cisco",
"model": "4451",
"ios": "15.4",
"ip": "10.255.0.2"
(continues on next page)
},
"sw1": {
"location": "21 New Globe Walk",
"vendor": "Cisco",
"model": "3850",
"ios": "3.6.XE",
"ip": "10.255.0.101",
"vlans": "10,20,30",
"routing": True
}
}
Task 5.1b
Modify the script from task 5.1a so that, when requesting a parameter, a list of possible parameters
was displayed. The list of parameters must be obtained from the dictionary, rather than written
manually.
$ python task_5_1b.py
Enter device name: r1
Enter parameter name (ios, model, vendor, location, ip): ip
10.255.0.1
All tasks must be completed using only the topics covered. That is, this task can be solved without
using the if condition.
london_co = {
"r1": {
"location": "21 New Globe Walk",
"vendor": "Cisco",
"model": "4451",
"ios": "15.4",
"ip": "10.255.0.1"
},
"r2": {
"location": "21 New Globe Walk",
"vendor": "Cisco",
"model": "4451",
(continues on next page)
"ios": "15.4",
"ip": "10.255.0.2"
},
"sw1": {
"location": "21 New Globe Walk",
"vendor": "Cisco",
"model": "3850",
"ios": "3.6.XE",
"ip": "10.255.0.101",
"vlans": "10,20,30",
"routing": True
}
}
Task 5.1c
Copy and modify the script from task 5.1b so that when you request a parameter that is not in the
device dictionary, the message ‘There is no such parameter’ is displayed. The assignment applies
only to the parameters of the devices, not to the devices themselves.
Note: Try typing a non-existent parameter, to see what the result will be. And then complete the
task.
$ python task_5_1c.py
Enter device name: r1
Enter parameter name (ios, model, vendor, location, ip): ips
No such parameter
All tasks must be completed using only the topics covered. That is, this task can be solved without
using the if condition.
london_co = {
"r1": {
"location": "21 New Globe Walk",
"vendor": "Cisco",
"model": "4451",
(continues on next page)
"ios": "15.4",
"ip": "10.255.0.1"
},
"r2": {
"location": "21 New Globe Walk",
"vendor": "Cisco",
"model": "4451",
"ios": "15.4",
"ip": "10.255.0.2"
},
"sw1": {
"location": "21 New Globe Walk",
"vendor": "Cisco",
"model": "3850",
"ios": "3.6.XE",
"ip": "10.255.0.101",
"vlans": "10,20,30",
"routing": True
}
}
Task 5.1d
Modify the script from task 5.1c so that, when requesting a parameter, the user could enter the
parameter name in any case.
$ python task_5_1d.py
Enter device name: r1
Enter parameter name (ios, model, vendor, location, ip): IOS
15.4
All tasks must be completed using only the topics covered. That is, this task can be solved without
using the if condition.
london_co = {
"r1": {
"location": "21 New Globe Walk",
"vendor": "Cisco",
"model": "4451",
(continues on next page)
"ios": "15.4",
"ip": "10.255.0.1"
},
"r2": {
"location": "21 New Globe Walk",
"vendor": "Cisco",
"model": "4451",
"ios": "15.4",
"ip": "10.255.0.2"
},
"sw1": {
"location": "21 New Globe Walk",
"vendor": "Cisco",
"model": "3850",
"ios": "3.6.XE",
"ip": "10.255.0.101",
"vlans": "10,20,30",
"routing": True
}
}
Task 5.2
Then print information about the network and mask in this format:
Network:
10 1 1 0
00001010 00000001 00000001 00000000
Mask:
/24
255 255 255 0
11111111 11111111 11111111 00000000
Hint: You can get the mask in binary format like this:
You can then take 8 bits of the binary mask using slices and convert them to decimal.
Restriction: All tasks must be done using the topics covered in this and previous chapters.
Task 5.2a
Copy and modify the script from task 5.2 so that, if the user entered a host address rather than a
network address, convert the host address to a network address and print the network address and
mask, as in task 5.2.
• 10.0.1.0/24
• 190.1.0.0/16
If the user entered the address 10.0.1.1/24, the output should look like this:
Network:
10 0 1 0
00001010 00000000 00000001 00000000
Mask:
/24
255 255 255 0
11111111 11111111 11111111 00000000
Check the script work on different host/mask combinations, for example: 10.0.5.195/28, 10.0.1.1/24
Hint:
The network address can be calculated from the binary host address and the netmask. If the mask
is 28, then the network address is the first 28 bits host addresses + 4 zeros. For example, the host
address 10.1.1.195/28 in binary will be:
bin_ip = "00001010000000010000000111000011"
Then the network address will be the first 28 characters from bin_ip + 0000 (4 because in total there
can be 32 bits in the address, and 32 - 28 = 4)
00001010000000010000000111000000
Restriction: All tasks must be done using the topics covered in this and previous chapters.
Task 5.3
Depending on the selected mode, print corresponding access or trunk configuration on stdout (com-
mand templates are in the lists access_template and trunk_template).
The output should first print the interface line and the interface number, and then the corresponding
template in which the VLAN number (or the list of VLANs) is inserted.
Restriction: All tasks must be done using the topics covered in this and previous chapters. This task
can be solved without using the if condition and for/while loops.
Hint: Leading up to this task was task 5.1. To make it easier to solve this task, you can look at
task 5.1 and figure out exactly how different information is displayed in the task, depending on user
input.
Below are examples of script execution to make it easier to understand the task.
$ python task_5_3.py
Enter interface mode (access/trunk): access
Enter type and interface number: Fa0/6
Enter number of vlan (vlans): 3
interface Fa0/6
switchport mode access
switchport access vlan 3
switchport nonegotiate
spanning-tree portfast
spanning-tree bpduguard enable
$ python task_5_3.py
Enter interface mode (access/trunk): trunk
Enter type and interface number: Fa0/7
Enter number of vlan (vlans): 2,3,4,5
interface Fa0/7
switchport trunk encapsulation dot1q
(continues on next page)
access_template = [
"switchport mode access", "switchport access vlan {}",
"switchport nonegotiate", "spanning-tree portfast",
"spanning-tree bpduguard enable"
]
trunk_template = [
"switchport trunk encapsulation dot1q", "switchport mode trunk",
"switchport trunk allowed vlan {}"
]
Task 5.3a
Copy and change the script from task 5.3 in such a way that, depending on the selected mode,
different questions were asked in the request for the VLAN number or VLAN list:
Restriction: All tasks must be done using the topics covered in this and previous chapters. This task
can be solved without using the if condition and for/while loops.
access_template = [
"switchport mode access", "switchport access vlan {}",
"switchport nonegotiate", "spanning-tree portfast",
"spanning-tree bpduguard enable"
]
trunk_template = [
"switchport trunk encapsulation dot1q", "switchport mode trunk",
"switchport trunk allowed vlan {}"
]
6. Control structures
So far, all code has been executed sequentially - all lines of script have been executed in order in
which they are written in file. This section covers program flow control:
if/elif/else
The if/elif/else statement allows make branches during program execution. The program goes
into branch when a certain condition is met.
• After if statement there must be some condition: if this condition is met (returns True), then
actions in block if are executed.
• elif can be used to make multiple branches, that is, to check incoming data for different
conditions.
• elif block is the same as if but it checked next. Roughly speaking, it is “otherwise if …”
Example of if statement:
In [1]: a = 9
In [2]: if a == 10:
...: print('a equal to 10')
...: elif a < 10:
...: print('a less than 10')
...: else:
...: print('a greater than 10')
...:
a less than 10
Condition
If expression is based on conditions: conditions are always written after if and elif. Blocks if/elif
are executed only when condition returns True, so the first thing to deal with is what is true and what
is false in Python.
In Python, apart from obvious True and False values, all other objects also have false or true value:
• True value:
• False value:
– 0
– None
– empty string
– empty object
For example, since an empty list is a false value, it is possible to check whether list is empty:
In [13]: if list_to_test:
....: print("The list has objects")
....:
List has objects
In [14]: if len(list_to_test) != 0:
....: print("The list has objects")
....:
List has objects
Comparison operators
In [3]: 5 > 6
Out[3]: False
In [4]: 5 > 2
Out[4]: True
In [5]: 5 < 2
Out[5]: False
In [6]: 5 == 2
Out[6]: False
In [7]: 5 == 5
Out[7]: True
In [8]: 5 >= 5
Out[8]: True
In [9]: 5 <= 10
Out[9]: True
In [10]: 8 != 10
Out[10]: True
In [1]: a = 9
In [2]: if a == 10:
...: print('a equal to 10')
...: elif a < 10:
...: print('a less than 10')
...: else:
...: print('a greater than 10')
...:
a less than 10
Operator in
Operator in allows checking for the presence of element in a sequence (for example, element in a
list or substrings in a string):
In [11]: 10 in vlan
Out[11]: True
In [12]: 50 in vlan
Out[12]: False
In [15]: r1 = {
....: 'IOS': '15.4',
....: 'IP': '10.255.0.1',
....: 'hostname': 'london_r1',
....: 'location': '21 New Globe Walk',
....: 'model': '4451',
....: 'vendor': 'Cisco'}
In [16]: 'IOS' in r1
Out[16]: True
In [17]: '4451' in r1
Out[17]: False
In [15]: r1 = {
....: 'IOS': '15.4',
....: 'IP': '10.255.0.1',
....: 'hostname': 'london_r1',
(continues on next page)
Operator and
In Python and operator returns not a boolean value but a value of one of operands.
If one of operators is a false, result of expression will be the first false value:
Operator or
In [31]: '' or [] or {}
Out[31]: {}
An important feature of or operator - operands, which are after the true operand, are not calculated:
An example of a check_password.py script that checks length of password and whether password
contains username:
if len(password) < 8:
print('Password is too short')
elif username in password:
print('Password contains username')
else:
print('Password for user {} is set'.format(username))
Script check:
$ python check_password.py
Enter username: nata
Enter password: nata1234
Password contains username
$ python check_password.py
Enter username: nata
Enter password: 123nata123
Password contains username
$ python check_password.py
Enter username: nata
Enter password: 1234
Password is too short
$ python check_password.py
Enter username: nata
Enter password: 123456789
Password for user nata is set
Ternary expression
s = [1, 2, 3, 4]
result = True if len(s) > 5 else False
It is best not to abuse it but in simple terms such a record can be useful.
for
Very often the same step should be performed for a set of the same data type. For example, convert
all strings in list to uppercase. Python uses for loop for such purposes.
For loop iterates elements of specified sequence and performs actions specified for each element.
• string
• list
• dictionary
• range
• Any Iterable
In [2]: upper_words = []
In [3]: words[0]
Out[3]: 'list'
In [6]: upper_words
Out[6]: ['LIST']
In [7]: upper_words.append(words[1].upper())
In [8]: upper_words.append(words[2].upper())
In [9]: upper_words
Out[9]: ['LIST', 'DICT', 'TUPLE']
In [11]: upper_words = []
In [13]: upper_words
Out[13]: ['LIST', 'DICT', 'TUPLE']
words list to perform actions in block for”. In this case, word is the name of the variable, which
refers to different values each iteration of the loop.
Note: The pythontutor project can be very helpful in understanding loops. The project visualize
code execution and allows you to see what happens at every stage of code execution, which is
especially useful in first steps of learning loops. The pythontutor allows you to upload your code, for
instance, see example above.
For loop can work with any sequence of elements. For example, the above code used a list and the
loop iterated over the elements of the list. The for loop works in a similar way with tuples.
When working with strings for loop iterates through string characters, for example:
s
t
r
i
n
g
Note: Loop uses a variable named letter. Although, it could be any name, it is better when name
tells you which objects go through a loop.
Sometimes it is necessary to use sequence of numbers in loop. In this case, it is best to use range
interface FastEthernet0/5
interface FastEthernet0/6
interface FastEthernet0/7
interface FastEthernet0/8
interface FastEthernet0/9
This loop uses range(10). Function range() generates numbers in range from zero to specified
number (in this example, up to 10) not including it.
In this example, loop runs through vlans list, so variable can be called vlan:
In [34]: r1 = {
...: 'ios': '15.4',
...: 'ip': '10.255.0.1',
...: 'hostname': 'london_r1',
...: 'location': '21 New Globe Walk',
...: 'model': '4451',
...: 'vendor': 'Cisco'}
...:
location
model
vendor
Or use items() method which allows you to run loop over a key-value pair:
Method items() returns a special view object that displays key-value pairs:
In [38]: r1.items()
Out[38]: dict_items([('ios', '15.4'), ('ip', '10.255.0.1'), ('hostname', 'london_
,→r1'), ('location', '21 New Globe Walk'), ('model', '4451'), ('vendor', 'Cisco
,→')])
Nested for
In this example, commands is a list of commands to execute on each interface in the fast_int list:
The first for loop passes through interfaces in the fast_int list and the second through commands
in commands list.
Generate_access_port_config.py file:
14 if command.endswith('access vlan'):
15 print(' {} {}'.format(command, vlan))
16 else:
17 print(' {}'.format(command))
• The first for loop iterates keys and values in nested fast_int[‘access’] dictionary
$ python generate_access_port_config.py
interface FastEthernet0/12
switchport mode access
switchport access vlan 10
spanning-tree portfast
spanning-tree bpduguard enable
interface FastEthernet0/14
switchport mode access
switchport access vlan 11
spanning-tree portfast
spanning-tree bpduguard enable
interface FastEthernet0/16
switchport mode access
switchport access vlan 17
spanning-tree portfast
spanning-tree bpduguard enable
interface FastEthernet0/17
switchport mode access
switchport access vlan 150
(continues on next page)
spanning-tree portfast
spanning-tree bpduguard enable
while
In the while loop, as in the if statement, you need to write a condition. If the condition is true, the
actions inside the while block are executed. In this case, unlike if, after executing the code in the
block, while returns to the beginning of the loop.
When using while loops it is necessary to pay attention to whether the result when condition of loop
is false will be reached.
Consider an example:
In [1]: a = 5
Then, in while loop the condition a > 0 is specified. That is, as long as the value of a is greater than
0, actions in the body of the loop will be executed. In this case, value of variable a will be displayed.
In addition, in the body of the loop, with each pass, the value of a is decreased by one.
Note: Record a -= 1 can be a bit unusual. Python allows this format to be used instead of a = a
- 1.
Since the value of a is decreasing, the loop will not be infinite, and at some point the expression a
> 0 will become false.
The following example is based on example about password from section which describes if state-
ment use Example of if/elif/else statement. In that example, you had to re-run the script if the
password did not meet the requirements. Using a while loop, you can make the script ask for a
password again if it does not meet the requirements (check_password_with_while.py):
password_correct = False
In this case, while loop is useful because it returns script back to the beginning of checks and allows
password to be typed again but does not require script to restart.
$ python check_password_with_while.py
Enter username: nata
Enter password: nata
Password is too short
Python has several operators that allow to change default loop behavior.
Break operator
• break breaks current loop and continues executing the next expressions
• if multiple nested loops are used, break interrupts internal loop and continues to execute ex-
pressions following the block. Break can be used in loops for and while
In [2]: i = 0
In [3]: while i < 10:
...: if i == 5:
...: break
...: else:
...: print(i)
...: i += 1
...:
0
1
2
3
4
while True:
if len(password) < 8:
print('Password is too short\n')
elif username in password:
(continues on next page)
Now it is possible not to repeat string password = input('Enter password once again: ') in
each branch, it is enough to move it to the end of loop.
And as soon as correct password is entered, break will take the program out of loop while.
Continue operator
Operator continue returns control to the beginning of loop. That is, continue allows to «jump»
remaining expressions in loop and go to the next iteration.
In [5]: i = 0
In [6]: while i < 6:
....: i += 1
....: if i == 3:
....: print("Skip 3")
....: continue
....: print("No one will see it")
....: else:
....: print("Current value: ", i)
....:
Current value: 1
Current value: 2
(continues on next page)
Skip 3
Current value: 4
Current value: 5
Current value: 6
password_correct = False
Here you can exit loop by checking password_correct flag. When correct password is entered, flag
is set to True and with continue a jump to the beginning of loop is occurred by skipping the last line
with password request.
$ python check_password_with_while_continue.py
Enter username: nata
Enter password: nata12
Password is too short
Pass operator
For example, pass can help when you need to specify a script structure. It can be set in loops,
functions, classes. And it won’t affect execution of code.
for/else, while/else
In loops for and while you may optionally use else block.
for/else
In loop for:
Example of loop for with else (block else is executed after loop for):
An example of loop for with else and break in loop (because of break, block else is not applied):
....: else:
....: print(num)
....: else:
....: print("Run out of numbers")
....:
0
1
2
Example of loop for with else and continue in loop (continue does not affect else block):
while/else
In loop while:
Example of a loop while with else (block else runs after loop while):
In [4]: i = 0
In [5]: while i < 5:
....: print(i)
....: i += 1
....: else:
....: print("The End")
....:
0
1
(continues on next page)
2
3
4
The End
An example of a loop while with else and break in loop (because of break, block else is not
applied):
In [6]: i = 0
try/except
If you repeated examples that were used before, there could be situations where a mistake was
made. It was probably a syntax error when a colon was missing, for example. Python generally
reacts quite understandably to such errors and they can easily be corrected. However, even if the
code is syntactically correct, errors can occur. In Python, these errors are called exceptions.
Examples of exceptions:
In [1]: 2/0
-----------------------------------------------------
ZeroDivisionError: division by zero
In [2]: 'test' + 2
-----------------------------------------------------
TypeError: must be str, not int
Most often, it is possible to predict what kind of exceptions will occur during execution of the program.
For example, if program expects two numbers in input and output returns their sum and user has
entered a string instead of one of numbers a TypeError error will appear as in example above.
Python allows working with exceptions. They can be intercepted and acted upon if an exception has
been occurred.
In [3]: try:
...: 2/0
...: except ZeroDivisionError:
...: print("You can't divide by zero")
...:
You can't divide by zero
• if there are no exceptions during execution of try block, block except is skipped and the
following code is executed
• if there is an exception within try block, the rest part of try block is skipped
– if except block contains an exception which has been occurred, code in except block is
executed
– if exception that has raised is not specified in except block, program execution is inter-
rupted and an error is generated
In [4]: try:
...: print("Let's divide some numbers")
...: 2/0
...: print('Cool!')
...: except ZeroDivisionError:
...: print("You can't divide by zero")
...:
Let's divide some numbers
You can't divide by zero
try/except statement may have many except if different actions are needed depending on type of
error.
try:
a = input("Enter first number: ")
b = input("Enter second number: ")
print("Result: ", int(a)/int(b))
except ValueError:
print("Please enter only numbers")
except ZeroDivisionError:
print("You can't divide by zero")
$ python divide.py
Enter first number: 3
Enter second number: 1
Result: 3
$ python divide.py
Enter first number: 5
Enter second number: 0
You can't divide by zero
$ python divide.py
Enter first number: qewr
Enter second number: 3
Please enter only numbers
In this case, ValueError exception raised when user has entered a string instead of a number. Zero-
DivisionError exception raised if second number is 0.
If you do not need to print different messages on ValueError and ZeroDivisionError, you can do this
(divide_ver2.py file):
try:
a = input("Enter first number: ")
b = input("Enter second number: ")
print("Result: ", int(a)/int(b))
except (ValueError, ZeroDivisionError):
print("Something went wrong...")
Verification:
$ python divide_ver2.py
Enter first number: wer
Enter second number: 4
Something went wrong...
$ python divide_ver2.py
Enter first number: 5
Enter second number: 0
Something went wrong...
Note: In block except you don’t have to specify a specific exception or exceptions. In that case,
all exceptions would be intercepted.
try/except/else
For example, if you need to perform any further operations with data that user entered, you can
write them in else block (divide_ver3.py file):
try:
a = input("Enter first number: ")
b = input("Enter second number: ")
result = int(a)/int(b)
except (ValueError, ZeroDivisionError):
print("Something went wrong...")
else:
print("Result is squared: ", result``2)
Example of execution:
$ python divide_ver3.py
Enter first number: 10
Enter second number: 2
Result is squared: 25
$ python divide_ver3.py
Enter first number: werq
(continues on next page)
try/except/finally
Block finally is another optional block in try statement. It is always implemented, whether an
exception has been raised or not. It’s about actions that you have to do anyway. For example, it
could be a file closing.
try:
a = input("Enter first number: ")
b = input("Enter second number: ")
result = int(a)/int(b)
except (ValueError, ZeroDivisionError):
print("Something went wrong...")
else:
print("Result is squared: ", result``2)
finally:
print("And they lived happily ever after.")
Verification:
$ python divide_ver4.py
Enter first number: 10
Enter second number: 2
Result is squared: 25
And they lived happily ever after.
$ python divide_ver4.py
Enter first number: qwerewr
Enter second number: 3
Something went wrong...
And they lived happily ever after.
$ python divide_ver4.py
Enter first number: 4
Enter second number: 0
Something went wrong...
And they lived happily ever after.
while True:
a = input("Enter first number: ")
b = input("Enter second number: ")
try:
result = int(a)/int(b)
except ValueError:
print("Only digits are supported")
except ZeroDivisionError:
print("You can't divide by zero")
else:
print(result)
break
while True:
a = input("Enter first number: ")
b = input("Enter second number: ")
if a.isdigit() and b.isdigit():
if int(b) == 0:
print("You can't divide by zero")
else:
print(int(a)/int(b))
break
else:
print("Only digits are supported")
But the same option without exceptions will not always be simple and understandable.
It is important to assess in each specific situation which version of code is more comprehensible,
compact and universal - with or without exceptions.
If you’ve used some other programming language before, it’s possible that use of exceptions was
considered a bad form. In Python this is not true. To get a little bit more into this issue, look at the
links to additional material at the end of this section.
Further reading
Documentation:
• break, continue
• Built-in Exceptions
Articles:
Stack Overflow:
• Why does python use ‘else’ after for and while loops?
Tasks
Warning: Starting from section “4. Data types in Python” there are automated tests for testing
tasks. They help to check whether everything matches the task, and also give feedback on what
does not correspond to the task. As a rule, after the first period of adaptation to tests, it becomes
easier to do tasks with tests. Testing is done using the pyneng utility. Learn more about how to
work with the pyneng utility.
Task 6.1
The mac list contains MAC addresses in the format XXXX:XXXX:XXXX However, in Cisco equipment
MAC addresses are in XXXX.XXXX.XXXX format.
Write a code that converts MAC addresses to cisco format and adds them to a new list named result.
Print the result list to the stdout using print function.
Restriction: All tasks must be done using the topics covered in this and previous chapters.
Task 6.2
Prompt the user to enter an IP address in the format 10.0.1.1. Depending on the type of address
(described below), print to the stdout:
Restriction: All tasks must be done using the topics covered in this and previous chapters.
Task 6.2a
The message “Invalid IP address” should be printed only once, even if several points above are not
met.
Restriction: All tasks must be done using the topics covered in this and previous chapters.
Task 6.2b
Add this functionality: If the address was entered incorrectly, request the address again.
Restriction: All tasks must be done using the topics covered in this and previous chapters.
Task 6.3
A configuration generator for access ports is made in the script. Make a similar configuration gen-
erator for trunk ports.
In trunks, the situation is complicated by the fact that there can be many VLANs, and you need to
understand what to do with them (add, delete, overwrite).
Therefore, in accordance with each port there is a list and the first (zero index) element of the list
specifies how to interpret VLAN numbers that follow.
The code should not be tied to specific port numbers. I.e, if there are other interface numbers in the
trunk dictionary, the code should work.
For data in the trunk_template dictionary, output to the standard output should be like this:
Restriction: All tasks must be done using the topics covered in this and previous chapters.
access_template = [
"switchport mode access",
"switchport access vlan",
"spanning-tree portfast",
"spanning-tree bpduguard enable",
]
trunk_template = [
"switchport trunk encapsulation dot1q",
"switchport mode trunk",
"switchport trunk allowed vlan",
]
In real life, in order to make full use of everything covered before this section you need to understand
how to work with files.
When working with network equipment (and not only), files can be:
• configuration templates
– section Jinja configuration temlates discusses the use of Jinja2 to create configuration tem-
plates
– usually they are structured files in some particular format: YAML, JSON, CSV
– section Modules discusses how to work with modules (other Python scripts)
This section covers simple text files. For example, Cisco configuration file.
• opening/closing
• reading
• writing
This section covers only the minimum required for working with files. More in Python documentation.
File opening
open
In open() function:
• You can specify not only the name but also the path (absolute or relative)
Function open creates a file object to which different methods can then be applied to work with it.
• a+ - open file for reading and writing. Data is added to the end of file
File reading
• readlines - reads file lines and creates a list from the lines
Let’s see how to read contents of files using the example of r1.txt:
!
service timestamps debug datetime msec localtime show-timezone year
service timestamps log datetime msec localtime show-timezone year
service password-encryption
service sequence-numbers
!
no ip domain lookup
(continues on next page)
!
ip ssh version 2
!
read
In [1]: f = open('r1.txt')
In [2]: f.read()
Out[2]: '!\nservice timestamps debug datetime msec localtime show-timezone␣
,→year\nservice timestamps log datetime msec localtime show-timezone␣
,→year\nservice password-encryption\nservice sequence-numbers\n!\nno ip domain␣
,→lookup\n!\nip ssh version 2\n!\n'
In [3]: f.read()
Out[3]: ''
When reading a file once again an empty line is displayed in line 3. This is because the whole file
is read when read method is called. And after the file has been read the cursor stays at the end of
file. The cursor position can be controlled by seek method.
readline
In [4]: f = open('r1.txt')
In [5]: f.readline()
Out[5]: '!\n'
In [6]: f.readline()
Out[6]: 'service timestamps debug datetime msec localtime show-timezone year\n'
But most often it is easier to walk through a file object in a loop without using read... methods:
In [7]: f = open('r1.txt')
service password-encryption
service sequence-numbers
no ip domain lookup
ip ssh version 2
readlines
In [9]: f = open('r1.txt')
In [10]: f.readlines()
Out[10]:
['!\n',
'service timestamps debug datetime msec localtime show-timezone year\n',
'service timestamps log datetime msec localtime show-timezone year\n',
'service password-encryption\n',
'service sequence-numbers\n',
'!\n',
'no ip domain lookup\n',
'!\n',
'ip ssh version 2\n',
'!\n']
If you want to get lines of a file but without a new line character at the end, you can use split
method and specify symbol \n as a separator:
In [11]: f = open('r1.txt')
In [12]: f.read().split('\n')
Out[12]:
['!',
'service timestamps debug datetime msec localtime show-timezone year',
'service timestamps log datetime msec localtime show-timezone year',
'service password-encryption',
'service sequence-numbers',
'!',
'no ip domain lookup',
'!',
'ip ssh version 2',
'!',
'']
If you use split before rstrip, list will be without empty string at the end:
In [13]: f = open('r1.txt')
In [14]: f.read().rstrip().split('\n')
Out[14]:
['!',
'service timestamps debug datetime msec localtime show-timezone year',
'service timestamps log datetime msec localtime show-timezone year',
'service password-encryption',
'service sequence-numbers',
'!',
'no ip domain lookup',
'!',
'ip ssh version 2',
'!']
seek
Until now, file had to be reopened to read it again. This is because after reading methods a cursor
is at the end of the file. And second reading returns an empty string.
To read information from a file again you need to use the seek method which moves the cursor to
the desired position.
In [15]: f = open('r1.txt')
In [16]: print(f.read())
!
service timestamps debug datetime msec localtime show-timezone year
service timestamps log datetime msec localtime show-timezone year
service password-encryption
service sequence-numbers
!
no ip domain lookup
!
ip ssh version 2
!
In [17]: print(f.read())
But with seek method you can go to the beginning of file (0 means the beginning of file):
In [18]: f.seek(0)
Once cursor has been set to the beginning of file you can read the content again:
In [19]: print(f.read())
!
service timestamps debug datetime msec localtime show-timezone year
service timestamps log datetime msec localtime show-timezone year
service password-encryption
service sequence-numbers
!
no ip domain lookup
!
ip ssh version 2
!
File writing
When writing information to a file, it is very important to specify the correct mode for opening the
file, so as not to accidentally delete it:
• a - open file to add data. Data is appended to the end of the file
write
In [4]: cfg_lines_as_string
Out[4]: '!\nservice timestamps debug datetime msec localtime show-timezone␣
,→year\nservice timestamps log datetime msec localtime show-timezone␣
,→year\nservice password-encryption\nservice sequence-numbers\n!\nno ip domain␣
,→lookup\n!\nip ssh version 2\n!'
In [5]: f.write(cfg_lines_as_string)
In [7]: f.close()
Since ipython supports cat command, you can easily see the content of file:
writelines
In [10]: f.writelines(cfg_lines)
In [11]: f.close()
As a result, all lines in the list were written into one line because there was no symbol \n at the end
of lines. You can add newline character in different ways. For example, you can loop through a list:
In [13]: cfg_lines2 = []
In [15]: cfg_lines2
Out[15]:
['!\n',
'service timestamps debug datetime msec localtime show-timezone year\n',
'service timestamps log datetime msec localtime show-timezone year\n',
'service password-encryption\n',
'service sequence-numbers\n',
'!\n',
'no ip domain lookup\n',
'!\n',
'ip ssh version 2\n',
If the final list is written anew to the file, then it will already contain newlines:
In [19]: f.writelines(cfg_lines2)
In [20]: f.close()
File closing
Note: In real life, the most common way to close files is use of with statement. It’s much more
convenient way than to close file explicitly. But since you can also find close method in life, this
section discusses how to use it.
After you finish working with file you have to close it. In some cases Python can close file itself. But
it’s best not to count on it and close file explicitly.
close
Method close() met in File writing section. It was there to make sure that the content of file was
written on disk.
For this, Python has a separate flush method. But since in example with file writing there was no
need to perform any more operations, file could be closed.
In [2]: print(f.read())
!
service timestamps debug datetime msec localtime show-timezone year
service timestamps log datetime msec localtime show-timezone year
service password-encryption
service sequence-numbers
!
no ip domain lookup
!
ip ssh version 2
!
The file object has a special closed attribute that lets you check whether a file is closed or not. If
file is open, it returns False:
In [3]: f.closed
Out[3]: False
In [4]: f.close()
In [5]: f.closed
Out[5]: True
In [6]: print(f.read())
------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-53-2c962247edc5> in <module>()
----> 1 print(f.read())
with statement
Python has a more convenient way of working with files than the ones used so far - statement with:
service password-encryption
service sequence-numbers
no ip domain lookup
ip ssh version 2
for line in f:
print(line)
When file needs to be run line by line, it is best to use this option.
In previous output there were extra empty lines between lines of the file because print adds another
new line character.
In [3]: f.closed
Out[3]: True
And of course, with statement can be used not only as a line-by-line reader, all methods that have
been covered before also work:
!
no ip domain lookup
!
ip ssh version 2
!
Sometimes you have to work with two files simultaneously. For example, write some lines from one
file to another.
In this case you can open two files in with block as follows:
This subsection covers working with files and brings together topics: files, loops, and conditions.
When processing output of commands or configuration, often it will be necessary to write summary
data to the dictionary. It is not always obvious how to handle the output of commands and how
to deal with the output in general. This subsection discusses several examples with increasing
complexity.
This example will deal with the output of sh ip int br command. From the output of command
we need to get interface name and IP address. Interface name is dictionary key and IP address is
value. At the same time, match must be made only for those interfaces with IP address assigned.
Working_with_dict_example_1.py file:
result = {}
with open('sh_ip_int_br.txt') as f:
for line in f:
line = line.split()
if line and line[1][0].isdigit():
interface, address, *other = line
result[interface] = address
print(result)
Command sh ip int br displays the output with columns. So desired fields are in the same line.
Script processes the output line by line and divides each line using split() method.
The resulting list contains output columns. Because we need only interfaces on which IP address is
configured, first character of second column is checked: if first character is a number the address is
assigned to interface and string has to be processed.
In interface, address, *other = line - variables are unpacked. Variable interface will have
interface name, address will have IP address and other - all other fields. Since each line has a
key-value pair, they are assigned to dictionary: result[interface] = address.
The result of script execution will be a dictionary (here it is split into key-value pairs for convenience,
in real script the dictionary output will be displayed in one line):
{'FastEthernet0/0': '15.0.15.1',
'FastEthernet0/1': '10.0.12.1',
'FastEthernet0/2': '10.0.13.1',
(continues on next page)
'Loopback0': '10.1.1.1',
'Loopback100': '100.0.0.1'}
Very often the output of commands looks like that key and value are in different lines. And you have
to figure out how to process the output to get right match. For example, from the output of sh ip int
br command you need to get match interface name – MTU (sh_ip_interface.txt file):
Interface name is in Ethernet0/0 is up, line protocol is up line and MTU in MTU is 1500
bytes line.
For example, try to remember interface each time and print its value when MTU parameter is de-
tected, together with MTU value:
Command output is organized in such a way that there is always a line with interface first and then
a line with MTU after several lines. If you remember the name of interface every time it appears
and at the time when line matches MTU, the last memorized interface is the one which matches
this MTU. Now, if you want to create a dictionary that matches interface – MTU, it’s enough to write
values when MTU was found.
Working_with_dict_example_2.py file:
result = {}
with open('sh_ip_interface.txt') as f:
for line in f:
if 'line protocol' in line:
interface = line.split()[0]
elif 'MTU is' in line:
mtu = line.split()[-2]
result[interface] = mtu
print(result)
The result of script execution will be a dictionary (here it is split into key-value pairs for convenience,
in real script the dictionary output will be displayed in one line):
{'Ethernet0/0': '1500',
'Ethernet0/1': '1500',
'Ethernet0/2': '1500',
'Ethernet0/3': '1500',
'Loopback0': '1514'}
This technique will be quite often useful because command output is generally organized in a very
similar way.
Nested dictionary
If you want to get several parameters from the output, it is very convenient to use a dictionary with
a nested dictionary. For example, from output sh ip interface you need to get two parameters:
In the first step, each value is stored in a variable and then all three values are displayed. Values
are displayed when a string has MTU because it is the last string:
It uses the same technique as in previous example but adds another nested dictionary:
result = {}
with open('sh_ip_interface.txt') as f:
for line in f:
if 'line protocol' in line:
interface = line.split()[0]
result[interface] = {}
elif 'Internet address' in line:
ip_address = line.split()[-1]
result[interface]['ip'] = ip_address
elif 'MTU' in line:
mtu = line.split()[-2]
result[interface]['mtu'] = mtu
print(result)
Each time an interface is detected, result dictionary creates a key with the name of interface that
corresponds to an empty dictionary. This blank is used so that at the time when IP address or MTU
is detected, parameter can be written into nested dictionary of the corresponding interface.
The result of script execution will be a dictionary (here it is split into key-value pairs for convenience,
in real script the dictionary output will be displayed in one line):
Sometimes, sections with empty values will be found in the output. For example, in case of output
`sh ip interface`, interfaces may look like:
Consequently, there is no MTU or IP address. And if you execute previous script for a file with such
interfaces, the result is this (output for file sh_ip_interface2.txt):
If you need to add interfaces to dictionary only when an IP address is assigned to interface, you need
to move the creation of key with interface name to a moment when line with IP address is detected
(working_with_dict_example_4.py file):
result = {}
with open('sh_ip_interface2.txt') as f:
for line in f:
if 'line protocol' in line:
interface = line.split()[0]
elif 'Internet address' in line:
ip_address = line.split()[-1]
result[interface] = {}
result[interface]['ip'] = ip_address
elif 'MTU' in line:
mtu = line.split()[-2]
result[interface]['mtu'] = mtu
print(result)
Further reading
Documentation:
Articles:
Stack Overflow:
Tasks
Warning: Starting from section “4. Data types in Python” there are automated tests for testing
tasks. They help to check whether everything matches the task, and also give feedback on what
does not correspond to the task. As a rule, after the first period of adaptation to tests, it becomes
easier to do tasks with tests. Testing is done using the pyneng utility. Learn more about how to
work with the pyneng utility.
Task 7.1
Process the lines from the ospf.txt file and print information for each line in this form to the stdout:
Prefix 10.0.24.0/24
AD/Metric 110/41
Next-Hop 10.0.13.3
Last update 3d18h
Outbound Interface FastEthernet0/0
Restriction: All tasks must be done using the topics covered in this and previous chapters.
Task 7.2
Create a script that will process the config_sw1.txt configuration file. The filename is passed as an
argument to the script.
The script should return to the stdout commands from the passed configuration file, excluding lines
that start with ‘!’.
Restriction: All tasks must be done using the topics covered in this and previous chapters.
Output example:
duplex auto
interface Ethernet0/1
switchport trunk encapsulation dot1q
switchport trunk allowed vlan 100
switchport mode trunk
duplex auto
spanning-tree portfast edge trunk
interface Ethernet0/2
duplex auto
interface Ethernet0/3
switchport trunk encapsulation dot1q
switchport trunk allowed vlan 100
duplex auto
switchport mode trunk
spanning-tree portfast edge trunk
...
Task 7.2a
Add this functionality: The script should not print to the stdout commands, which contain words from
the ignore list. The script should also not print lines that begin with !.
Check the script on the config_sw1.txt configuration file. The filename is passed as an argument to
the script.
Restriction: All tasks must be done using the topics covered in this and previous chapters.
Task 7.2b
Make a copy of the code from the task 7.2a. Add this functionality: instead of printing to stdout, the
script should write the resulting lines to a file.
In this case, the lines that are contained in the ignore list and lines that start with ! must be filtered.
Restriction: All tasks must be done using the topics covered in this and previous chapters.
Task 7.3
The script should process the lines in the CAM_table.txt file. Each line, where there is a MAC address,
must be handled in such a way that the following table was printed on the stdout:
Restriction: All tasks must be done using the topics covered in this and previous chapters.
Task 7.3a
10 01ab.c5d0.70d0 Gi0/8
10 0a1b.1c80.7000 Gi0/4
100 01bb.c580.7000 Gi0/1
200 0a4b.c380.7c00 Gi0/2
200 1a4b.c580.7000 Gi0/6
300 0a1b.5c80.70f0 Gi0/7
300 a2ab.c5a0.700e Gi0/3
500 02b1.3c80.7b00 Gi0/5
1000 0a4b.c380.7d00 Gi0/9
Pay attention to vlan 1000 - it should be displayed last. Correct sorting can be achieved if vlan is a
number, not a string.
Restriction: All tasks must be done using the topics covered in this and previous chapters.
Task 7.3b
Output example:
Restriction: All tasks must be done using the topics covered in this and previous chapters.
This section covers topics that were not included in the previous sections and also provides examples
of using Python to solve problems.
While most examples will be file-oriented the same data-processing principles can be applied to
network equipment. Only part with reading from file will be replaced to get output from hardware.
Python 3.6 added a new version of string formatting - f-strings or interpolation of strings. The f-
strings allow not only to set values to template but also to perform calls to functions, methods,
etc.
In many situations f-strings are easier to use than format and f-strings work faster than format and
other methods of string formatting.
Syntax
F-string is a string literal with a letter f in front of it. Inside f-string, in curly braces there are names
of variables that will be substituted:
In [1]: ip = '10.1.1.1'
In [2]: mask = 24
The same result with ``format`` method you can achieve by:
``"IP: {ip}, mask: {mask}".format(ip=ip, mask=mask)``.
A very important difference between f-strings and format: f-strings are expressions that are pro-
cessed, not just strings. That is, in case of ipython, as soon as we wrote the expression and pressed
Enter, it was performed and instead of expressions {ip} and {mask} the values of variables were
substituted.
Therefore, for example, you cannot first write a template and then define variables that are used in
template:
In addition to substituting variable values you can write expressions in curly braces:
In [2]: mask = 24
After colon in f-strings you can specify the same values as in format:
In [10]: print(f'''
...: IP address:
...: {oct1:<8} {oct2:<8} {oct3:<8} {oct4:<8}
...: {oct1:08b} {oct2:08b} {oct3:08b} {oct4:08b}''')
IP address:
10 1 1 1
00001010 00000001 00000001 00000001
When using f-strings you cannot first create a template and then use it as in format method.
F-string is immediately executed and contains the values of variables that were defined earlier:
In [7]: ip = '10.1.1.1'
In [8]: mask = 24
If you want to set other values you must create new variables (with the same names) and write
f-string again:
In [11]: ip = '10.2.2.2'
In [12]: mask = 24
When using f-strings in loops an f-string must be written in body of the loop to «catch» new variable
values within each iteration:
Column alignment:
In [7]: width = 10
...:
sw1 Gi0/1 r1 Gi0/2
sw1 Gi0/2 r2 Gi0/1
sw1 Gi0/3 r3 Gi0/0
sw1 Gi0/5 sw4 Gi0/2
In [2]: if session_stats['todo']:
...: print(f"Pomodoros done: {session_stats['done']}, TODO: {session_stats[
,→'todo']}")
...: else:
...: print(f"Good job! All {session_stats['done']} pomodoros done!")
...:
Pomodoros done: 10, TODO: 5
In [7]: ip = '10.1.1.1'
In many cases f-strings are more convenient to use as template looks more understandable and
compact. However, there are cases when format method is more convenient. For example:
In [6]: ip = [10, 1, 1, 1]
In [9]: template.format(*ip)
Out[9]: '00001010 00000001 00000001 00000001 '
Another situation where format is usually more convenient to use: the need to use the same tem-
plate many times in script. F-string will execute the first time and will set current values of variables
and to use template again it has to be rewritten. This means that script will contain copies of the
same line. At the same time format allows to create a template in one place and then use it again
substituting variables as needed.
This can be avoided by creating a function but creating a function to print a string based on template
is not always justified. Example of creating a function:
Variable unpacking
Variable unpacking is a special syntax that allows to assign elements of an iterable to variables.
Note: This functionality is often referred to as tuple unpacking but unpacking works on any iterable
object, not only with tuples
In [3]: intf
Out[3]: 'FastEthernet0/1'
In [4]: ip
Out[4]: '10.1.1.1'
When you unpack variables, each item in list falls into the corresponding variable. It is important to
take into account that there should be exactly as many variables on the left as there are elements
in the list.
Often only some of elements of an iterable are needed. Unpacking syntax requires that exactly as
many variables as elements in the object being iterated be specified.
If, for example, only VLAN, MAC and interface should be obtained from line, you still need to specify
a variable for “DYNAMIC”:
In [10]: vlan
Out[10]: '100'
In [11]: intf
Out[11]: 'Gi0/1'
If record type is no longer needed, you can replace item_type variable with underline character:
In [15]: mac
Out[15]: '00:09:BB:3D:D6:58'
In [16]: vlan
Out[16]: '10'
Use *
Variable unpacking supports a special syntax that allows unpacking of several elements into one.
If you put * in front of variable name, all elements except those that are explicitly assigned will be
written into it.
For example, you can get the first element in first variable and the rest in rest:
In [20]: first
Out[20]: 10
In [21]: rest
Out[21]: [11, 13, 30]
In [23]: first
Out[23]: 10
In [24]: rest
Out[24]: [11, 13, 30]
In [26]: first
Out[26]: 55
In [27]: rest
Out[27]: []
There can be only one variable with an asterisk in the unpacking expression.
In [32]: rest
Out[32]: [10, 11, 13]
In [33]: last
Out[33]: 30
In [36]: name
Out[36]: 'SW1'
In [37]: l_intf
Out[37]: 'Eth'
In [38]: r_intf
Out[38]: '0/1'
Unpacking examples
These examples show that you can unpack not only lists, tuples and strings but also any other
iterable objects.
In [40]: first
Out[40]: 1
In [41]: rest
Out[41]: [2, 3, 4, 5]
Unpacking zip:
In [42]: a = [1, 2, 3, 4, 5]
In [44]: zip(a, b)
Out[44]: <zip at 0xb4df4fac>
In [47]: first
Out[47]: (1, 100)
In [48]: rest
Out[48]: [(2, 200), (3, 300), (4, 400)]
In [49]: last
Out[49]: (5, 500)
Instead, you can run through key-value pairs and immediately unpack them into different variables:
...:
In [54]: table
Out[54]:
[['100', 'a1b2.ac10.7000', 'DYNAMIC', 'Gi0/1'],
['200', 'a0d4.cb20.7000', 'DYNAMIC', 'Gi0/2'],
['300', 'acb4.cd30.7000', 'DYNAMIC', 'Gi0/3'],
['100', 'a2bb.ec40.7000', 'DYNAMIC', 'Gi0/4'],
['500', 'aa4b.c550.7000', 'DYNAMIC', 'Gi0/5'],
['200', 'a1bb.1c60.7000', 'DYNAMIC', 'Gi0/6'],
['300', 'aa0b.cc70.7000', 'DYNAMIC', 'Gi0/7']]
Python supports special expressions that allow for compact creation of lists, dictionaries, and sets:
• list comprehensions
• dict comprehensions
• set comprehensions
These expressions not only enable more compact objects to be created but also create them faster.
Although they require a certain habit of use and understanding at first, they are very often used.
List comprehensions
In [2]: print(vlans)
['vlan 10', 'vlan 11', 'vlan 12', 'vlan 13', 'vlan 14', 'vlan 15']
In general, it is an expression that converts an iterable object into a list. That is, a sequence of
elements is converted and added to a new list.
In [3]: vlans = []
In [5]: print(vlans)
['vlan 10', 'vlan 11', 'vlan 12', 'vlan 13', 'vlan 14', 'vlan 15']
In list comprehensions you can use if. Thus, you can only add some objects to the list. For example,
a loop selects only those elements that are digits, converts them and adds them to the resulting list
only_digits:
In [7]: only_digits = []
In [9]: print(only_digits)
[10, 20, 30, 40]
In [12]: print(only_digits)
[10, 20, 30, 40]
Of course, not all loops can be rewritten as a list comprehension but when it is possible to do so
without making the expression more complex, it is better to use list comprehension.
Note: In Python, list comprehensions can also replace filter and map functions and are considered
a clearer option.
With list comprehension it is also convenient to get elements from nested dictionaries:
In [13]: london_co = {
...: 'r1' : {
...: 'hostname': 'london_r1',
...: 'location': '21 New Globe Walk',
...: 'vendor': 'Cisco',
...: 'model': '4451',
...: 'IOS': '15.4',
...: 'IP': '10.255.0.1'
...: },
...: 'r2' : {
...: 'hostname': 'london_r2',
...: 'location': '21 New Globe Walk',
...: 'vendor': 'Cisco',
...: 'model': '4451',
...: 'IOS': '15.4',
...: 'IP': '10.255.0.2'
...: },
...: 'sw1' : {
...: 'hostname': 'london_sw1',
...: 'location': '21 New Globe Walk',
...: 'vendor': 'Cisco',
...: 'model': '3850',
(continues on next page)
For example, vlans list contains several nested lists with VLANs:
It’s necessary to form only one list with VLAN numbers. The first option is to use for loop:
In [17]: result = []
In [19]: print(result)
[10, 21, 35, 101, 115, 150, 111, 40, 50]
List comprehension:
In [22]: print(result)
[10, 21, 35, 101, 115, 150, 111, 40, 50]
In [25]: result = [f'vlan {vl}\n name {name}' for vl, name in zip(vlans, names)]
In [26]: print('\n'.join(result))
vlan 100
name mngmt
vlan 110
name voice
vlan 150
name video
vlan 200
name dmz
Dict comprehensions
Dict comprehensions are similar to list comprehensions but they are used to create dictionaries. For
example, the expression:
In [27]: d = {}
In [29]: print(d)
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81, 10: 100}
In [31]: print(d)
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81, 10: 100}
Another example in which you need to change an existing dictionary and convert all keys to lower-
case. First, a solution without a dict comprehension:
In [33]: lower_r1 = {}
In [35]: lower_r1
Out[35]:
{'hostname': 'london_r1',
'ios': '15.4',
'ip': '10.255.0.1',
'location': '21 New Globe Walk',
'model': '4451',
'vendor': 'Cisco'}
In [38]: lower_r1
Out[38]:
{'hostname': 'london_r1',
'ios': '15.4',
'ip': '10.255.0.1',
'location': '21 New Globe Walk',
'model': '4451',
'vendor': 'Cisco'}
Like list comprehensions, dict comprehensions can be nested. Try to convert keys in nested dictio-
naries in the same way:
In [39]: london_co = {
...: 'r1' : {
...: 'hostname': 'london_r1',
...: 'location': '21 New Globe Walk',
...: 'vendor': 'Cisco',
...: 'model': '4451',
...: 'IOS': '15.4',
...: 'IP': '10.255.0.1'
...: },
...: 'r2' : {
...: 'hostname': 'london_r2',
...: 'location': '21 New Globe Walk',
...: 'vendor': 'Cisco',
...: 'model': '4451',
...: 'IOS': '15.4',
...: 'IP': '10.255.0.2'
...: },
...: 'sw1' : {
...: 'hostname': 'london_sw1',
...: 'location': '21 New Globe Walk',
...: 'vendor': 'Cisco',
...: 'model': '3850',
...: 'IOS': '3.6.XE',
...: 'IP': '10.255.0.101'
...: }
...: }
In [40]: lower_london_co = {}
In [42]: lower_london_co
Out[42]:
{'r1': {'hostname': 'london_r1',
'ios': '15.4',
'ip': '10.255.0.1',
'location': '21 New Globe Walk',
'model': '4451',
'vendor': 'Cisco'},
In [44]: result
Out[44]:
{'r1': {'hostname': 'london_r1',
'ios': '15.4',
'ip': '10.255.0.1',
'location': '21 New Globe Walk',
'model': '4451',
'vendor': 'Cisco'},
'r2': {'hostname': 'london_r2',
'ios': '15.4',
'ip': '10.255.0.2',
'location': '21 New Globe Walk',
'model': '4451',
'vendor': 'Cisco'},
'sw1': {'hostname': 'london_sw1',
'ios': '3.6.XE',
'ip': '10.255.0.101',
'location': '21 New Globe Walk',
'model': '3850',
'vendor': 'Cisco'}}
Set comprehensions
Set comprehensions are generally similar to list comprehensions. For example, get a set with unique
VLAN numbers:
In [47]: unique_vlans
Out[47]: {10, 30, 56}
In [51]: unique_vlans
Out[51]: {10, 30, 56}
Further reading
Documentation:
Articles:
Stack Overflow:
Instead, you create a special code block with name - function. And every time code has to be
repeated, you just call a function. Function allows not only to name a block of code but also to
make it more abstract through parameters. Parameters make it possible to pass different data for
function. And get different results depending on input parameters.
Section 9. Functions covers with creation of functions, section 10. Useful functions discusses useful
built-in functions.
Once code is divided into functions, there comes a time when you need to use function in another
script. Of course, copying a function is as inconvenient as copying a normal code. Modules are used
to reuse code from another Python script.
Section 11. Modules is dedicated to creating your own modules and section 12. Useful modules
covers useful modules from Python standard library.
The last section 13. Iterators, iterable and generators is dedicated to iterable objects, iterators and
generators.
189
Python for network engineers, Release 1.0
9. Functions
Function:
• has a name to run this code block as many times as you want
– function code will be executed taking into account the specified arguments
Typically, problems that code solves are very similar and often have something in common. For
example, when working with configuration files each time it is necessary to perform such actions:
• file opening
• deletion (or skipping) of lines starting with exclamation mark (for Cisco)
Often there’s a piece of code that repeats itself. Of course, you can copy it from one script to another.
But this is very inconvenient because when you change code you have to update it in all files in which
it is copied.
It is much easier and more accurate to put this code into a function (it can also be several functions).
And then you will call this function - in this file or another one. This section discusses when a function
is in the same file. And in 11. Modules we will see how to reuse objects that are in other scripts.
Creation of functions
Creation of function:
• after parentheses goes colon and from a new line with indent there is a block of code that
function executes
Note: Function code used in this subsection can be copied from create_func file.
Example of function:
Function configure_intf creates an interface configuration with specified name and IP address.
Function has three parameters: intf_name, ip, mask. When function is called the real data will
replace these parameters.
Note: When function is created, it does nothing yet. Actions listed in it will be executed only
when you call function. This is something like ACL in network equipment: when creating ACL in
configuration, it does nothing until it is applied.
Function call
When calling a function you must specify its name and pass arguments if necessary.
Note: Parameters are variables that are used to create a function. Arguments are the actual values
(data) that are passed to functions when called.
Function configure_intf expects three values when called because it was created with three pa-
rameters:
9. Functions 191
Python for network engineers, Release 1.0
The current version of the configure_intf function prints commands to a standard output, com-
mands can be seen but the result of function cannot be saved to a variable.
For example, sorted function does not simply print the sorting result to standard output stream but
returns it, so it can be saved to variable in this way:
In [5]: sorted(items)
Out[5]: [0, 2, 22, 40]
In [7]: sorted_items
Out[7]: [0, 2, 22, 40]
Note: Note string Out[5] in ipython: this is how ipython shows that the function/method is return-
ing something and shows what it returns.
If you try to write the result of the configure_intf function to a variable, the value of the variable
will be None:
.. code:: python
Operator return
Operator return is used to return a value, and at the same time it exits the function. Function can
return any Python object. By default, function always returns None.
In order for configure_intf function to return a value that can then be assigned to a variable, you
must use return operator:
In [12]: print(result)
interface Fa0/0
ip address 10.1.1.1 255.255.255.0
In [13]: result
Out[13]: 'interface Fa0/0\nip address 10.1.1.1 255.255.255.0'
Now the result variable contains a line with commands to configure interface. In real life, function
will almost always return some value.
Another important aspect of return operator is that after return the function closes, meaning that
the expressions that follow return are not executed.
For example, in function below the line «Configuration is ready» will not be displayed because it
stands after return:
Function can return multiple values. In this case, they are separated by a comma after return
operator. In fact, function returns tuple:
In [18]: result
Out[18]: ('interface Fa0/0\n', 'ip address 10.1.1.1 255.255.255.0')
In [19]: type(result)
Out[19]: tuple
9. Functions 193
Python for network engineers, Release 1.0
In [21]: intf
Out[21]: 'interface Fa0/0\n'
In [22]: ip_addr
Out[22]: 'ip address 10.1.1.1 255.255.255.0'
Documentation (docstring)
The first line in function definition is docstring, documentation string. This is a comment that is used
to describe a function:
In [24]: configure_intf?
Signature: configure_intf(intf_name, ip, mask)
Docstring: Fucntion generates interface configuration
File: ~/repos/pyneng-examples-exercises/examples/06_control_structures/
,→<ipython-input-23-2b2bd970db8f>
Type: function
It is best to write short comments that describe function. For example, describe what function
expects to input, what type of arguments should be and what will be the output. Besides, it is better
to write a couple of sentences about what function does. This will help when in a month or two you
will be trying to understand what function you wrote is doing.
Variables in Python have a scope. Depending on location in code where variable has been defined,
scope is also defined, it determines where variable will be available.
When using variable names in a program, Python searches, creates or changes these names in the
corresponding namespace each time. Namespace that is available at each moment depends on
area in which code is located.
Python has a LEGB rule that it uses for variables search. For example, when accessing a variable
within a function, Python searches for a variable in this order in scopes (before the first match):
• E (enclosing) - in local area of outer functions (these are functions within which our function is
located)
• local variables:
• global variables:
In [2]: intf_config
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-2-5983e972fb1c> in <module>
----> 1 intf_config
Note that intf_config variable is not available outside of function. To get the result of a function you
must call a function and assign result to a variable:
In [4]: result
Out[4]: 'interface F0/0\nip address 10.1.1.1 255.255.255.0'
9. Functions 195
Python for network engineers, Release 1.0
The purpose of creating a function is typically to take a piece of code that performs a particular
task to a separate object. This allows you to use this piece of code multiple times without having to
re-create it in program.
Typically, a function must perform some actions with input values and produce an output.
• arguments - actual values (data) that are passed to function when called.
Function checks password and returns False if checks fail and True if password passed checks:
When defining a function in this way it is necessary to pass both arguments. If only one argument
is passed, there is an error:
In [5]: check_passwd('nata')
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-5-e07773bb4cc8> in <module>
----> 1 check_passwd('nata')
When creating a function you can specify which arguments must be passed and which must not.
Accordingly, a function can be created with:
• required parameters
Required parameters
Required parameters - determine which arguments must be passed to functions. At the same
time, they need to be passed exactly as many as parameters of function are specified (you cannot
specify more or less)
Function checks password and returns False if checks fail and True if password passed all checks:
9. Functions 197
Python for network engineers, Release 1.0
When creating a function you can specify default value for parameter in this way: def
check_passwd(username, password, min_length=8). In this case, min_length option is specified
with default value and may not be passed when a function is called.
Since min_length parameter has a default value the corresponding argument can be omitted when
a function is called if default value fits you:
• as positional - passed in the same order in which they are defined at creation of function.
That is, the order in which arguments are passed determines what value each argument will
receive.
• as keyword - passed with argument name and its value. In such a case, arguments can be
stated in any order as their name is clearly indicated.
Positional and keyword arguments can be mixed when calling a function. That is, it is possible to use
both methods when passing arguments of the same function. In this process, Positional arguments
must be indicated before keyword arguments.
Positional argument
Positional arguments when calling a function must be passed in the correct order (therefore they
are called positional arguments).
9. Functions 199
Python for network engineers, Release 1.0
If you swap arguments when calling a function the error will likely occur depending on function.
Keyword arguments
Keyword arguments:
Warning: Please note that first there should always be positional arguments and then keyword
arguments.
In real life, it is often better to specify flags (parameters with True/False values) or numerical values
as a keyword argument. If you set a good name for the parameter you can immediately know by its
For example, you can add a flag that will control whether or not a username should be checked in
password:
If you specify a value equal to False the verification will not be performed:
Sometimes it is necessary to make function accept not a fixed number of arguments, but any num-
ber. For such a case, in Python it is possible to create a function with a special parameter that
accepts variable length arguments. This parameter can be both keyword and positional.
Note: Even if you don’t use it in your scripts there’s a good chance you’ll find it in someone else’s
code.
9. Functions 201
Python for network engineers, Release 1.0
Parameter that takes positional variable length arguments is created by adding an asterisk before
parameter name. Parameter can have any name but by agreement *args is the most common
name.
Example of a function:
• parameter a
In [4]: sum_arg(1)
1 ()
Out[4]: 1
In [7]: sum_arg()
()
Out[7]: 0
Parameter that accepts keyword variable length arguments is created by adding two asterisk in front
of parameter name. Name of parameter can be any but by agreement most commonly use name
**kwargs (from keyword arguments).
Example of a function:
• parameter a
9. Functions 203
Python for network engineers, Release 1.0
Unpacking arguments
In Python the expressions *args and **kwargs allow for another task - unpacking arguments.
So far we’ve called all functions manually. Hence, we passed on all relevant arguments.
In reality, it is usually necessary to pass data to function programmatically. And often data comes
in the form of a Python object.
For example, when formatting strings you often need to pass multiple arguments to format method.
And often these arguments are already in list or tuple. To pass them to format method you have to
use indexes:
• ip_address - IP address
Suppose you call a function and pass it information that has been obtained from another source,
for example from database. For example, interfaces_info list contains parameters for configuring
interfaces:
If you go through list in the loop and pass nested list as a function argument, an error will occur:
Error is quite logical: function expects three arguments and it is given 1 argument - a list. In such
a situation it is necessary to unpack arguments. Just add * before passing the list as an argument
and there is no error anymore:
9. Functions 205
Python for network engineers, Release 1.0
Python will unpack info list itself and pass list elements to function as arguments.
Error is because the function has taken dictionary as one argument and believes that it lacks only
password argument.
If you add ** before passing a dictionary to function, function will work properly:
Using variable length arguments and unpacking arguments you can pass arguments between func-
tions. Let me give you an example.
9. Functions 207
Python for network engineers, Release 1.0
Function checks password and returns True if password has passed verification and False if not.
We will create add_user_to_users_file function that requests password for specified user, checks
it and requests it again if password has not been checked or writes user and password to file if
password has been verified
In [8]: add_user_to_users_file('nata')
Enter password for user nata: natasda
Password is too short
Enter password for user nata: natasdlajsl;fjd
(continues on next page)
In this version of add_user_to_users_file() function, it is not possible to regulate the minimum pass-
word length and whether to verify the presence of a username in password. In the following version
of add_user_to_users_file() function, these features are added:
You can now specify min_length or check_username when calling a function. However, it was neces-
sary to repeat parameters of check_passwd function in defining of add_user_to_users_file func-
tion. This is not very good and when there are many parameters it is just inconvenient, especially
considering that check_passwd function can have other parameters.
This happens quite often and Python has a common solution to this problem: all arguments for
internal function (in this case it is check_passwd) will be taken in **kwargs. Then, when calling
check_passwd() function they will be unpacked into keyword arguments by the same **kwargs
syntax.
9. Functions 209
Python for network engineers, Release 1.0
...:
In this version you can add arguments to check_passwd() function without having to duplicate them
in add_user_to_users_file function.
Further reading
Documenation:
• Defining Functions
• Built-in Functions
• Sorting HOW TO
• Range function
Tasks
Warning: Starting from section “4. Data types in Python” there are automated tests for testing
tasks. They help to check whether everything matches the task, and also give feedback on what
does not correspond to the task. As a rule, after the first period of adaptation to tests, it becomes
easier to do tasks with tests. Testing is done using the pyneng utility. Learn more about how to
work with the pyneng utility.
Task 9.1
The function should return a list of all ports in access mode with configuration based on the ac-
cess_mode_template template.
In this task, the beginning of the function is written and you just need to continue writing the function
body itself.
An example of a final list (each string is written on a new line for readability):
[
"interface FastEthernet0/12",
"switchport mode access",
"switchport access vlan 10",
"switchport nonegotiate",
"spanning-tree portfast",
"spanning-tree bpduguard enable",
"interface FastEthernet0/17",
"switchport mode access",
"switchport access vlan 150",
"switchport nonegotiate",
"spanning-tree portfast",
"spanning-tree bpduguard enable",
...]
Check the operation of the function using the access_config dictionary and the list of commands
access_mode_template. If the previous check was successful, check the function again using the
9. Functions 211
Python for network engineers, Release 1.0
dictionary access_config_2 and make sure that the final list contains the correct interface numbers
and vlans.
Restriction: All tasks must be done using the topics covered in this and previous chapters.
access_mode_template = [
"switchport mode access",
"switchport access vlan",
"switchport nonegotiate",
"spanning-tree portfast",
"spanning-tree bpduguard enable",
]
access_config_2 = {
"FastEthernet0/3": 100,
"FastEthernet0/7": 101,
"FastEthernet0/9": 107,
}
Task 9.1a
Add this functionality: add an additional parameter that controls whether port-security configured
• default is None
The function should return a list of all ports in access mode with configuration based on the ac-
cess_mode_template template and the port_security_template template, if passed. There should
not be a new line character at the end of lines in the list.
Check the operation of the function using the example of the access_config dictionary, with the
generation of the configuration port-security and without.
print(generate_access_config(access_config, access_mode_template))
print(generate_access_config(access_config, access_mode_template, port_security_
,→template))
Restriction: All tasks must be done using the topics covered in this and previous chapters.
access_mode_template = [
"switchport mode access",
"switchport access vlan",
"switchport nonegotiate",
"spanning-tree portfast",
"spanning-tree bpduguard enable",
]
port_security_template = [
"switchport port-security maximum 2",
"switchport port-security violation restrict",
"switchport port-security"
]
Task 9.2
The function should return a list of commands with configuration based on the specified ports and
trunk_mode_template.
9. Functions 213
Python for network engineers, Release 1.0
Check the operation of the function using the example of the trunk_config dictionary and a list of
commands trunk_mode_template. If the previous check was successful, check the function again
on the trunk_config_2 dictionary and make sure that the final list contains the correct numbers
interfaces and vlans.
An example of a final list (each string is written on a new line for readability):
[
"interface FastEthernet0/1",
"switchport mode trunk",
"switchport trunk native vlan 999",
"switchport trunk allowed vlan 10,20,30",
"interface FastEthernet0/2",
"switchport mode trunk",
"switchport trunk native vlan 999",
"switchport trunk allowed vlan 11,30",
...]
Restriction: All tasks must be done using the topics covered in this and previous chapters.
trunk_mode_template = [
"switchport mode trunk",
"switchport trunk native vlan 999",
"switchport trunk allowed vlan",
]
trunk_config = {
"FastEthernet0/1": [10, 20, 30],
"FastEthernet0/2": [11, 30],
"FastEthernet0/4": [17],
}
trunk_config_2 = {
"FastEthernet0/11": [120, 131],
"FastEthernet0/15": [111, 130],
"FastEthernet0/14": [117],
}
Task 9.2a
Change the function so that it returns a dictionary instead of a list of commands: - keys: interface
names, like ‘FastEthernet0/1’ - values: the list of commands that you need execute on this interface
Check the operation of the function using the example of the trunk_config dictionary and the
trunk_mode_template template.
An example of a final dict (each string is written on a new line for readability):
{
"FastEthernet0/1": [
"switchport mode trunk",
"switchport trunk native vlan 999",
"switchport trunk allowed vlan 10,20,30",
],
"FastEthernet0/2": [
"switchport mode trunk",
"switchport trunk native vlan 999",
"switchport trunk allowed vlan 11,30",
],
"FastEthernet0/4": [
"switchport mode trunk",
"switchport trunk native vlan 999",
"switchport trunk allowed vlan 17",
],
}
Restriction: All tasks must be done using the topics covered in this and previous chapters.
trunk_mode_template = [
"switchport mode trunk", "switchport trunk native vlan 999",
"switchport trunk allowed vlan"
]
trunk_config = {
"FastEthernet0/1": [10, 20, 30],
"FastEthernet0/2": [11, 30],
"FastEthernet0/4": [17]
}
Task 9.3
Create a get_int_vlan_map function that handles the switch configuration file and returns a tuple of
two dictionaries:
1. A dictionary of ports in access mode, where the keys are port numbers, and the access VLAN
values (numbers):
9. Functions 215
Python for network engineers, Release 1.0
{"FastEthernet0/12": 10,
"FastEthernet0/14": 11,
"FastEthernet0/16": 17}
2. A dictionary of ports in trunk mode, where the keys are port numbers, and the values are the
list of allowed VLANs (list of numbers):
The function must have one parameter, config_filename, which expects as an argument the name
of the configuration file.
Restriction: All tasks must be done using the topics covered in this and previous chapters.
Task 9.3a
Add this functionality: add support for configuration when the port is in VLAN 1 and the access port
setting looks like this:
interface FastEthernet0/20
switchport mode access
duplex auto
In this case, information should be added to the dictionary that the port in VLAN 1 Dictionary exam-
ple:
{'FastEthernet0/12': 10,
'FastEthernet0/14': 11,
'FastEthernet0/20': 1 }
The function must have one parameter, config_filename, which expects as an argument the name
of the configuration file.
Restriction: All tasks must be done using the topics covered in this and previous chapters.
Task 9.4
Create a convert_config_to_dict function that handles the switch configuration file and returns a
dictionary:
• If the top-level team has subcommands, they must be in the value from the corresponding key,
in the form of a list (spaces at the beginning of the line must be removed).
• If the top-level command has no subcommands, then the value will be an empty list
The function must have one parameter, config_filename, which expects as an argument the name
of the configuration file.
When processing the configuration file, you should ignore the lines that begin with ‘!’, as well as
lines containing words from the ignore list.
The part of the dictionary that the function should return (the full output can be seen in
test_task_9_4.py test):
{
"version 15.0": [],
"service timestamps debug datetime msec": [],
"service timestamps log datetime msec": [],
"no service password-encryption": [],
"hostname sw1": [],
"interface FastEthernet0/0": [
"switchport mode access",
"switchport access vlan 10",
],
"interface FastEthernet0/1": [
"switchport trunk encapsulation dot1q",
"switchport trunk allowed vlan 100,200",
"switchport mode trunk",
],
"interface FastEthernet0/2": [
"switchport mode access",
"switchport access vlan 20",
],
}
Restriction: All tasks must be done using the topics covered in this and previous chapters.
9. Functions 217
Python for network engineers, Release 1.0
Returns
* True if the command contains a word from the ignore list
* False - if not
"""
ignore_status = False
for word in ignore:
if word in command:
ignore_status = True
return ignore_status
Function print has been used many times in book, but its full syntax has not yet been discussed:
Function print outputs all elements by separating them by their sep value and finishes output with
end value.
All elements that are passed as arguments are converted into strings:
In [6]: str(f)
Out[6]: '<function f at 0xb4de926c>'
In [7]: str(range(10))
Out[7]: 'range(0, 10)'
sep
In [8]: print(1, 2, 3)
1 2 3
Note: Note that all arguments that manage behavior of print function must be passed on as
keyword, not positional.
end
Parameter end controls which value will be displayed after all elements are printed. By default, new
line character is used:
In [19]: print(1, 2, 3)
1 2 3
file
Parameter file controls where values of print function are displayed. The default output is
sys.stdout.
Python allows to pass to file as an argument any object with write(string) method.
In [3]: f.close()
flush
By default, when writing to a file or print to a standard output stream, the output is buffered. Function
print allows to disable buffering. You can control it in a file.
Example script that displays a number from 0 to 10 every second (print_nums.py file):
import time
Try running a script and make sure the numbers are displayed once per second.
Now, a similar script but the numbers will appear in one line (print_nums_oneline.py file):
import time
Try running a function. Numbers does not appear one per second but all appear after 10 seconds.
This is because when output is displayed on standard output, flush is performed after new line
character.
In order to make script work properly flush should be set to True (print_nums_oneline_fixed.py file):
import time
range
Function syntax:
range(stop)
range(start, stop[, step])
Parameters of function:
• stop - on which number the sequence of numbers ends. Mentioned number is not included in
range
Function range stores only start, stop and step values and calculates values as necessary. This
means that regardless of the size of range that describes range function, it will always occupy a
fixed amount of memory.
In [1]: range(5)
Out[1]: range(0, 5)
In [2]: list(range(5))
Out[2]: [0, 1, 2, 3, 4]
If two arguments are passed, the first is used as start and the second as stop:
And in order to indicate sequence step, you have to pass three arguments:
To get a descending sequence use a negative step and specify start by a greater number and stop
by a smaller number.
The range object supports all operations that support sequences in Python, except addition and
multiplication.
In [12]: nums
Out[12]: range(0, 5)
In [13]: 3 in nums
Out[13]: True
In [14]: 7 in nums
(continues on next page)
Out[14]: False
Note: Starting with Python 3.2 this check is performed in constant time (O(1)).
In [16]: nums[0]
Out[16]: 0
In [17]: nums[-1]
Out[17]: 4
In [19]: nums[1:]
Out[19]: range(1, 5)
In [20]: nums[:3]
Out[20]: range(0, 3)
In [22]: len(nums)
Out[22]: 5
In [24]: min(nums)
Out[24]: 0
In [25]: max(nums)
Out[25]: 4
In [27]: nums.index(3)
Out[27]: 2
sorted
Function sorted returns a new sorted list that is obtained from an iterable object that has been
passed as an argument. Function also supports additional options that allow you to control sorting.
The first aspect that is important to pay attention to - sorted returns a list. If you sort a list of items,
a new list is returned:
In [2]: sorted(list_of_words)
Out[2]: ['', 'dict', 'list', 'one', 'two']
In [4]: sorted(tuple_of_words)
Out[4]: ['', 'dict', 'list', 'one', 'two']
Sorting set:
In [6]: sorted(set_of_words)
Out[6]: ['', 'dict', 'list', 'one', 'two']
Sorting string:
In [8]: sorted(string_to_sort)
Out[8]: [' ', 'g', 'g', 'i', 'l', 'n', 'n', 'o', 'r', 's', 't']
If you pass a dictionary to sorted the function will return sorted list of keys:
In [9]: dict_for_sort = {
...: 'id': 1,
...: 'name': 'London',
(continues on next page)
In [10]: sorted(dict_for_sort)
Out[10]:
['id',
'it_vlan',
'mngmt_vlan',
'name',
'port',
'to_id',
'to_name',
'user_vlan']
reverse
The reverse flag allows you to control the sorting order. By default, sorting will be in ascending
order of items:
In [12]: sorted(list_of_words)
Out[12]: ['', 'dict', 'list', 'one', 'two']
key
With key option you can specify how to perform sorting. The key parameter expects function by
which the comparison should be performed.
In [16]: dict_for_sort = {
...: 'id': 1,
...: 'name': 'London',
...: 'IT_VLAN': 320,
...: 'User_VLAN': 1010,
...: 'Mngmt_VLAN': 99,
...: 'to_name': None,
...: 'to_id': None,
...: 'port': 'G1/0/11'
...: }
The key option can accept any functions, not only embedded ones. It is also convenient to use
anonymous lambda() function.
Using key option you can sort objects by any element. However, this requires either lambda() or
special functions from operator module.
For example, in order to sort the list of tuples with two items in the second element, you should use
this technique:
enumerate
Sometimes, when iterating objects in for loop, it is necessary not only to get object itself but also
its sequence number. This can be done by creating an additional variable that will increase by one
with each iteration. However, it is much more convenient to do this with iterator enumerate.
Basic example:
enumerate can count not only from scratch but from any value that has been given to it after object:
Sometimes it is necessary to check what iterator has generated. If you want to see full content that
iterator generates you can use list() function:
This example uses Cisco EEM. In a nutshell, EEM allows you to perform some actions in response to
an event.
In EEM, in a situation where many actions need to be performed it is inconvenient to type action x
cli command each time. Plus, most often, there is already a ready piece of configuration that must
be executed by EEM.
A simple Python script can generate EEM commands based on existing command list (enumer-
ate_eem.py file):
import sys
config = sys.argv[1]
In this example, commands are read from a file and then EEM prefix is added to each line.
en
conf t
no int Gi0/0/0.300
no int Gi0/0/0.301
no int Gi0/0/0.302
int range gi0/0/0-2
channel-group 1 mode active
interface Port-channel1.300
encapsulation dot1Q 300
vrf forwarding Management
ip address 10.16.19.35 255.255.255.248
zip
zip function:
• zip returns an iterator with tuples in which n-tuple consists of n-elements of sequences that
have been passed as arguments
• for example, 10th tuple will contain 10th element of each of passed sequences
• if sequences with different lengths have been passed to input, they will all be cut by the shortest
sequence
In [1]: a = [1, 2, 3]
In [4]: a = [1, 2, 3, 4, 5]
In [9]: r1
Out[9]:
{'IOS': '15.4',
'IP': '10.255.0.1',
'hostname': 'london_r1',
'location': '21 New Globe Walk',
'model': '4451',
'vendor': 'Cisco'}
In example below there is a separate list which stores keys and a dictionary which stores information
about each device in form of list (to preserve order).
Collect them in dictionary with keys from list and information from dictionary data:
In [11]: data = {
....: 'r1': ['london_r1', '21 New Globe Walk', 'Cisco', '4451', '15.4', '10.
,→255.0.1'],
....: 'r2': ['london_r2', '21 New Globe Walk', 'Cisco', '4451', '15.4', '10.
,→255.0.2'],
....: 'sw1': ['london_sw1', '21 New Globe Walk', 'Cisco', '3850', '3.6.XE',
,→'10.255.0.101']
....: }
In [12]: london_co = {}
In [14]: london_co
Out[14]:
{'r1': {'IOS': '15.4',
'IP': '10.255.0.1',
'hostname': 'london_r1',
'location': '21 New Globe Walk',
'model': '4451',
'vendor': 'Cisco'},
'r2': {'IOS': '15.4',
'IP': '10.255.0.2',
'hostname': 'london_r2',
'location': '21 New Globe Walk',
'model': '4451',
'vendor': 'Cisco'},
'sw1': {'IOS': '3.6.XE',
'IP': '10.255.0.101',
'hostname': 'london_sw1',
'location': '21 New Globe Walk',
'model': '3850',
'vendor': 'Cisco'}}
all
Function all returns True if all elements are true (or object is empty).
In [3]: all([])
Out[3]: True
For example, it is possible to check that all octets in an IP address are numbers:
In [4]: IP = '10.0.1.1'
any
In [9]: any([])
Out[9]: False
def ignore_command(command):
'''
(continues on next page)
To this option:
def ignore_command(command):
'''
Function checks if command contains a word from ignore list.
* command is a string. Command that need to be checked returns True
* if command contains a word from ignore list, False - if not.
'''
ignore = ['duplex', 'alias', 'Current configuration']
In Python, lambda expression allows creation of anonymous functions - functions that are not tied
to a name.
Anonymous function:
Standard function:
In [2]: sum_arg(1, 2)
Out[2]: 3
In [4]: sum_arg(1, 2)
Out[4]: 3
Note that there is no return operator in lambda function definition because there can only be one
expression in this function that always returns a value and closes the function.
Function lambda is convenient to use in expressions where you need to write a small function for
data processing. For example, in sorted function you can use lambda expression to specify sorting
key:
Function lambda is also useful in map and filter functions which will be discussed in the following
sections.
map
Function map applies function to each element of sequence and returns iterator with result.
For example, map can be used to perform element transformations. Convert all strings to uppercase:
Converting to numbers:
As a rule, you can use list comprehension instead of map. The list comprehension option is often
clearer, and in some cases even faster.
But map can be more effective when you have to generate a large number of elements because map
is an iterator and list comprehension generates a list.
Converting to numbers:
String formatting:
filter
Function filter() applies function to all sequence elements and returns an iterator with those
objects for which function has returned True.
In [1]: list_of_strings = ['one', 'two', 'list', '', 'dict', '100', '1', '50']
From the list of words leave only those with more than two letters:
In [7]: list_of_strings = ['one', 'two', 'list', '', 'dict', '100', '1', '50']
Odd/even numbers:
From the list of words leave only those with more than two letters:
11. Modules
Module in Python is a plain text file with Python code and .py extention. It allows logical ordering
and grouping of the code. Division into modules can be done, for example, by this logic:
The good thing about modules is that they allow you to reuse already written code and not copy it
(for example, do not copy a previously written function).
Module import
• import module
• import module as
import module
In [1]: dir()
Out[1]:
['In',
'Out',
...
'exit',
'get_ipython',
'quit']
In [2]: import os
In [3]: dir()
Out[3]:
['In',
'Out',
...
'exit',
(continues on next page)
'get_ipython',
'os',
'quit']
After importing the os module appeared in the output dir.This means that it is now in the current
namespace.
To call some function or method from os module you should specify os. and then object name:
In [4]: os.getlogin()
Out[4]: 'natasha'
This import method is good because module objects do not enter the namespace of current program.
That is, if you create a function named getlogin it will not conflict with the same function of os
module.
Note: If file name contains a dot, the standard way of importing will not work. In such cases,
another method is used.
import module as
Construction import module as allows importing a module under a different name (usually shorter):
Option from module import object is convenient to use when only few functions are needed from
whole module:
In [2]: dir()
Out[2]:
['In',
'Out',
...
'exit',
'get_ipython',
'getcwd',
'getlogin',
'quit']
In [3]: getlogin()
Out[3]: 'natasha'
In [4]: getcwd()
Out[4]: '/Users/natasha/Desktop/Py_net_eng/code_test'
Option from module import * imports all module names into the current namespace:
In [2]: dir()
Out[2]:
['EX_CANTCREAT',
'EX_CONFIG',
...
'wait',
'wait3',
'wait4',
'waitpid',
'walk',
'write']
In [3]: len(dir())
Out[3]: 218
There are many objects in os module, so the output is shortened. At the end, length of the list of
names of current namespace is specified.
This import option is best not to use. With such code import it is not clear which function is taken,
for example from os module. This makes it much harder to understand the code.
Example of creating your own modules and importing a function from one module to another.
File check_ip_function.py:
import ipaddress
def check_ip(ip):
try:
ipaddress.ip_address(ip)
return True
except ValueError as err:
return False
ip1 = '10.1.1.1'
ip2 = '10.1.1'
print('Checking IP...')
print(ip1, check_ip(ip1))
print(ip2, check_ip(ip2))
File check_ip_function.py has created check_ip function which checks that argument is an IP address.
This is done by using ipaddress module which will be discussed in the next section.
$ python check_ip_function.py
Checking IP...
10.1.1.1 True
10.1.1 False
The second script imports check_ip function and uses it to select from address list only those that
passed the check (get_correct_ip.py file):
def return_correct_ip(ip_addresses):
correct = []
for ip in ip_addresses:
if check_ip(ip):
correct.append(ip)
return correct
$ python get_correct_ip.py
Cheking IP...
10.1.1.1 True
10.1.1 False
Cheking list of IP addresses
['10.1.1.1', '8.8.8.8']
Note that not only information from get_correct_ip.py script is displayed, but also information from
check_ip_function.py. This is because any type of import executes the entire script. That is, even
when import looks like from check_ip_function import check_ip, entire check_ip_function.py
script is executed, not just check_ip function. As a result, all messages of imported script will be
displayed.
Messages from imported script are not scary, they are just confusing. Worse when script performed
some kind of connection to equipment and when importing a function from it, we will have to wait
for connection to take place.
Python can specify that some strings should not be executed when importing. This is discussed in
the following subsection.
In [22]: return_correct_ip(ip_list)
Out[22]: ['10.1.1.1', '8.8.8.8']
if __name__ == "__main__"
Often script can be executed independently and can be imported as a module by another script.
Since importing a script runs this script, it is often necessary to specify that some strings should not
be executed when importing.
In previous example there were two scripts: check_ip_function.py and get_correct_ip.py. And when
starting get_correct_ip.py, print() from check_ip_function.py was displayed.
Python has a special technique that specifies that a code must not be executed at import: all lines
that are in if __name__ == '__main__' block are not executed at import.
Variable __name__ is a special variable that will be equal to "__main__" only if file is run as the main
program and is set equal to module name when importing the module. That is, if __name__ ==
'__main__' condition checks whether a file was run directly.
As a rule, if __name__ == '__main__' block includes all function calls and information output on
the standard output stream. That is, in check_ip_function.py script this block conytains everything
except import and return_correct_ip function:
import ipaddress
def check_ip(ip):
try:
ipaddress.ip_address(ip)
return True
except ValueError as err:
return False
if __name__ == '__main__':
ip1 = '10.1.1.1'
(continues on next page)
ip2 = '10.1.1'
print('Cheking IP...')
print(ip1, check_ip(ip1))
print(ip2, check_ip(ip2))
$ python check_ip_function.py
Cheking IP...
10.1.1.1 True
10.1.1 False
When you start check_ip_function.py script directly, all lines are executed, because __name__ vari-
able in this case is equal to '__main__'.
def return_correct_ip(ip_addresses):
correct = []
for ip in ip_addresses:
if check_ip(ip):
correct.append(ip)
return correct
$ python get_correct_ip.py
Checking list of IP addresses
['10.1.1.1', '8.8.8.8']
In general, it is better to write all code that calls functions and outputs something to the standard
output stream inside if __name__ == '__main__' block.
Warning: Starting with Section 11, for tests to work correctly you have to always write a
function call in task file within if __name__ == '__main__' block. The absence of this block
will not cause errors in all tasks, but it will still avoid problems.
Further reading
Docs:
• os
• argparse
• subprocess
• ipaddress
Video:
• David Beazley - Modules and Packages: Live and Let Die! - PyCon 2015
argparse
• Argparse docs
• PyMOTW
tabulate
• tabulate docs
Stack Overflow:
pprint
Tasks
Warning: Starting from section “4. Data types in Python” there are automated tests for testing
tasks. They help to check whether everything matches the task, and also give feedback on what
does not correspond to the task. As a rule, after the first period of adaptation to tests, it becomes
easier to do tasks with tests. Testing is done using the pyneng utility. Learn more about how to
work with the pyneng utility.
Task 11.1
Create a function parse_cdp_neighbors that handles show cdp neighbors command output.
The function must have one parameter, command_output, which expects a single string of command
output as an argument (not a filename). To do this, you need to read the entire contents of the file
into a string, and then pass the string as an argument to the function (how to pass the command
output is shown in the code below).
The function should return a dictionary that describes the connections between devices.
In the dictionary, interfaces must be written without a space between type and name. That is, so
Fa0/0, and not so Fa 0/0.
Check the operation of the function on the contents of the sh_cdp_n_sw1.txt file. In this case, the
function should work on other files (the test checks the operation of the function on the output from
sh_cdp_n_sw1.txt and sh_cdp_n_r3.txt).
Restriction: All tasks must be done using the topics covered in this and previous chapters.
def parse_cdp_neighbors(command_output):
"""
(continues on next page)
Here we pass the output of the command as single string because it is in this
form that received command output from equipment. Taking the output of
the command as an argument, instead of a filename, we make the function more
generic: it can work both with files and with output from equipment.
Plus, we learn to work with such a output.
"""
if __name__ == "__main__":
with open("sh_cdp_n_sw1.txt") as f:
print(parse_cdp_neighbors(f.read()))
Task 11.2
Create a create_network_map function that processes the show cdp neighbors command output
from multiple files and merges it into one common topology.
The function must have one parameter, filenames, which expects as an argument a list of filenames
containing the output of the show cdp neighbors command.
The function should return a dictionary that describes the connections between devices. The struc-
ture of the dictionary is the same as in task 11.1:
• sh_cdp_n_sw1.txt
• sh_cdp_n_r1.txt
• sh_cdp_n_r2.txt
• sh_cdp_n_r3.txt
Do not copy the code of the parse_cdp_neighbors and draw_topology functions. If the
parse_cdp_neighbors function cannot process the output of one of the command output files, you
need to correct the function code in task 11.1.
Restriction: All tasks must be done using the topics covered in this and previous chapters.
infiles = [
"sh_cdp_n_sw1.txt",
"sh_cdp_n_r1.txt",
"sh_cdp_n_r2.txt",
(continues on next page)
"sh_cdp_n_r3.txt",
]
Task 11.2a
Note: To complete this task, graphviz must be installed: apt-get install graphviz
Use the create_network_map function from task 11.2 to create the topology dict for files:
• sh_cdp_n_sw1.txt
• sh_cdp_n_r1.txt
• sh_cdp_n_r2.txt
• sh_cdp_n_r3.txt
Using the draw_topology function from the draw_network_graph.py file, draw schema for the
topology dict received with create_network_map function. You need to figure out how to
work with the draw_topology function on your own, by reading the function description in the
draw_network_graph.py file. The resulting scheme will be written to the svg file - it can be opened
with a browser.
With the current topology dictionary, extra connections are drawn on the diagram. They occur be-
cause one CDP file (sh_cdp_n_r1.txt) describes connection ("R1", "Eth0/0"): ("SW1", "Eth0/
1") and another (sh_cdp_n_sw1.txt) describes connection ("SW1", "Eth0/1"): ("R1", "Eth0/
0").
In this task, you need to create a new function unique_network_map, which of these two connections
will leave only one, for correct drawing of the schema. In this case, it does not matter which of the
connections to leave.
The unique_network_map function must have one topology_dict parameter, which expects a dic-
tionary as an argument. It should be a dictionary resulting from the create_network_map function
call.
Dict example:
{
("R1", "Eth0/0"): ("SW1", "Eth0/1"),
("R2", "Eth0/0"): ("SW1", "Eth0/2"),
("R2", "Eth0/1"): ("SW2", "Eth0/11"),
("R3", "Eth0/0"): ("SW1", "Eth0/3"),
(continues on next page)
The function should return a dictionary that describes the connections between devices. In the
dictionary, you need to get rid of “duplicate” connections and leave only one of them.
After creating the function, try drawing the topology again, now for the dictionary returned by the
unique_network_map function.
Wherein:
Restriction: All tasks must be done using the topics covered in this and previous chapters.
infiles = [
"sh_cdp_n_sw1.txt",
"sh_cdp_n_r1.txt",
"sh_cdp_n_r2.txt",
"sh_cdp_n_r3.txt",
]
subprocess
Subprocess module allows you to create new processes. It can then connect to standard in-
put/output/error streams and receive a return code.
Subprocess can for example execute any Linux commands from script. And depending on situation,
get the output or just check that command has been performed correctly.
Function subprocess.run()
The result variable now contains a special CompletedProcess object. From this object you can get
information about execution of process, such as return code:
In [3]: result
Out[3]: CompletedProcess(args='ls', returncode=0)
In [4]: result.returncode
Out[4]: 0
If it is necessary to call a command with arguments, it should be passed in this way (as a list):
Trying to execute a command using wildcard expressions, for example using *, will cause an error:
To call commands in which wildcard expressions are used, you add shell argument and call a com-
mand:
Another feature of run() If you try to run a ping command, for example, this aspect will be visible:
By default, run() function returns the result of a command execution to a standard output stream.
If you want to get the result of command execution, add stdout argument with value subpro-
cess.PIPE:
Now you can get the result of command executing in this way:
In [10]: print(result.stdout)
b'total 28\n4 -rw-r--r-- 1 vagrant vagrant 56 Jun 7 19:35 ipython_as_mngmt_
,→console.md\n4 -rw-r--r-- 1 vagrant vagrant 1638 Jun 7 19:35 module_search.
,→md\n4 drwxr-xr-x 2 vagrant vagrant 4096 Jun 7 19:35 naming_conventions\n4 -rw-
,→r--r-- 1 vagrant vagrant 277 Jun 7 19:35 README.md\n4 drwxr-xr-x 2 vagrant␣
,→vagrant 4096 Jun 16 05:11 useful_functions\n4 drwxr-xr-x 2 vagrant vagrant 4096␣
,→Jun 17 16:30 useful_modules\n4 -rw-r--r-- 1 vagrant vagrant 49 Jun 7 19:35␣
,→version_control.md\n'
Note letter b before line. It means that module returned the output as a byte string. There are two
options to translate it into unicode:
In [11]: print(result.stdout.decode('utf-8'))
total 28
4 -rw-r--r-- 1 vagrant vagrant 56 Jun 7 19:35 ipython_as_mngmt_console.md
4 -rw-r--r-- 1 vagrant vagrant 1638 Jun 7 19:35 module_search.md
4 drwxr-xr-x 2 vagrant vagrant 4096 Jun 7 19:35 naming_conventions
4 -rw-r--r-- 1 vagrant vagrant 277 Jun 7 19:35 README.md
4 drwxr-xr-x 2 vagrant vagrant 4096 Jun 16 05:11 useful_functions
4 drwxr-xr-x 2 vagrant vagrant 4096 Jun 17 16:30 useful_modules
4 -rw-r--r-- 1 vagrant vagrant 49 Jun 7 19:35 version_control.md
In [13]: print(result.stdout)
total 28
4 -rw-r--r-- 1 vagrant vagrant 56 Jun 7 19:35 ipython_as_mngmt_console.md
4 -rw-r--r-- 1 vagrant vagrant 1638 Jun 7 19:35 module_search.md
4 drwxr-xr-x 2 vagrant vagrant 4096 Jun 7 19:35 naming_conventions
4 -rw-r--r-- 1 vagrant vagrant 277 Jun 7 19:35 README.md
4 drwxr-xr-x 2 vagrant vagrant 4096 Jun 16 05:11 useful_functions
4 drwxr-xr-x 2 vagrant vagrant 4096 Jun 17 16:31 useful_modules
4 -rw-r--r-- 1 vagrant vagrant 49 Jun 7 19:35 version_control.md
Output disabling
Sometimes it is enough to get only return code and need to disable output of execution result on
standard output stream. This can be done by passing to run() function the stdout argument with
value subprocess.DEVNULL:
In [15]: print(result.stdout)
None
In [16]: print(result.returncode)
0
If command was executed with error or failed, the output of command will fall on standard error
stream.
This can be obtained in the same way as the standard output stream:
Now result.stdout has empty string and result.stderr has standard output stream:
In [18]: print(result.stdout)
None
In [19]: print(result.stderr)
ping: unknown host a
In [20]: print(result.returncode)
2
import subprocess
if reply.returncode == 0:
print('Alive')
else:
print('Unreachable')
$ python subprocess_run_basic.py
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=43 time=54.0 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=43 time=54.4 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=43 time=53.9 ms
That is, the result of command execution is printed to standard output stream.
Function ping_ip() checks the availability of IP address and returns True and stdout if address is
available, or False and stderr if address is not available (subprocess_ping_function.py file):
import subprocess
def ping_ip(ip_address):
"""
Ping IP address and return tuple:
On success:
* True
* command output (stdout)
On failure:
* False
* error output (stderr)
"""
reply = subprocess.run(['ping', '-c', '3', '-n', ip_address],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding='utf-8')
if reply.returncode == 0:
return True, reply.stdout
else:
(continues on next page)
print(ping_ip('8.8.8.8'))
print(ping_ip('a'))
$ python subprocess_ping_function.py
(True, 'PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.\n64 bytes from 8.8.8.8: icmp_
,→seq=1 ttl=43 time=63.8 ms\n64 bytes from 8.8.8.8: icmp_seq=2 ttl=43 time=55.6␣
,→ms\n64 bytes from 8.8.8.8: icmp_seq=3 ttl=43 time=55.9 ms\n\n--- 8.8.8.8 ping␣
,→statistics ---\n3 packets transmitted, 3 received, 0% packet loss, time␣
,→2003ms\nrtt min/avg/max/mdev = 55.643/58.492/63.852/3.802 ms\n')
(False, 'ping: unknown host a\n')
Based on this function you can make a function that will check list of IP addresses and return as a
result two lists: reachable and unreachable addresses.
If number of IP addresses to check is large, you can use threading or multiprocessing modules to
speed up verification.
os
This subsection addresses only several useful features. For a more complete description of capabil-
ities of module please refer to documentation or article on Pymotw.
In [1]: import os
In [2]: os.mkdir('test')
In [3]: ls -ls
total 0
0 drwxr-xr-x 2 nata nata 68 Jan 23 18:58 test/
In addition, module contains relevant existence checks. For example, if you try to re-create a direc-
tory, an error will occur:
In [4]: os.mkdir('test')
---------------------------------------------------------------------------
FileExistsError Traceback (most recent call last)
<ipython-input-4-cbf3b897c095> in <module>()
----> 1 os.mkdir('test')
In [5]: os.path.exists('test')
Out[5]: True
In [7]: os.listdir('.')
Out[7]: ['cover3.png', 'dir2', 'dir3', 'README.txt', 'test']
By checking os.path.isdir and os.path.isfile you can get a separate list of files and list of
directories:
In [9]: dirs
Out[9]: ['dir2', 'dir3', 'test']
In [11]: files
Out[11]: ['cover3.png', 'README.txt']
Also in module there are separate methods for working with paths:
In [13]: os.path.basename(file)
Out[13]: 'README.md'
In [14]: os.path.dirname(file)
Out[14]: 'Programming/PyNEng/book/25_additional_info'
(continues on next page)
In [15]: os.path.split(file)
Out[15]: ('Programming/PyNEng/book/25_additional_info', 'README.md')
ipaddress
Note: Since Python 3.3, ipaddress module is part of standard Python library.
ipaddress.ip_address
In [3]: ipv4
Out[3]: IPv4Address('10.0.1.1')
In [4]: print(ipv4)
10.0.1.1
In [5]: ipv4.
ipv4.compressed ipv4.is_loopback ipv4.is_unspecified ipv4.version
ipv4.exploded ipv4.is_multicast ipv4.max_prefixlen
ipv4.is_global ipv4.is_private ipv4.packed
ipv4.is_link_local ipv4.is_reserved ipv4.reverse_pointer
With is_ attributes you can check to what range the address belongs to:
In [6]: ipv4.is_loopback
Out[6]: False
In [7]: ipv4.is_multicast
Out[7]: False
In [8]: ipv4.is_reserved
Out[8]: False
In [9]: ipv4.is_private
Out[9]: True
In [16]: str(ip1)
Out[16]: '10.0.1.1'
In [17]: int(ip1)
Out[17]: 167772417
In [18]: ip1 + 5
Out[18]: IPv4Address('10.0.1.6')
In [19]: ip1 - 5
Out[19]: IPv4Address('10.0.0.252')
ipaddress.ip_network
ipaddress.ip_network function allows you to create an object that describes the network (IPv4 or
IPv6):
In [21]: subnet1.broadcast_address
Out[21]: IPv4Address('80.0.1.15')
In [22]: subnet1.with_netmask
Out[22]: '80.0.1.0/255.255.255.240'
In [23]: subnet1.with_hostmask
Out[23]: '80.0.1.0/0.0.0.15'
In [24]: subnet1.prefixlen
Out[24]: 28
In [25]: subnet1.num_addresses
Out[25]: 16
Method hosts returns generator, so to view all hosts you should apply list() function:
In [26]: list(subnet1.hosts())
Out[26]:
[IPv4Address('80.0.1.1'),
IPv4Address('80.0.1.2'),
IPv4Address('80.0.1.3'),
IPv4Address('80.0.1.4'),
IPv4Address('80.0.1.5'),
IPv4Address('80.0.1.6'),
IPv4Address('80.0.1.7'),
IPv4Address('80.0.1.8'),
IPv4Address('80.0.1.9'),
IPv4Address('80.0.1.10'),
IPv4Address('80.0.1.11'),
IPv4Address('80.0.1.12'),
IPv4Address('80.0.1.13'),
IPv4Address('80.0.1.14')]
Method subnets allows dividing network (subnetting). By default, it splits network into two subnets:
In [27]: list(subnet1.subnets())
Out[27]: [IPv4Network('80.0.1.0/29'), IPv4Network(u'80.0.1.8/29')]
Prefixlen_diff parameter allows you to specify the number of bits for subnets:
In [28]: list(subnet1.subnets(prefixlen_diff=2))
Out[28]:
(continues on next page)
[IPv4Network('80.0.1.0/30'),
IPv4Network('80.0.1.4/30'),
IPv4Network('80.0.1.8/30'),
IPv4Network('80.0.1.12/30')]
With new_prefix parameter you can specify which mask should be configured:
In [29]: list(subnet1.subnets(new_prefix=30))
Out[29]:
[IPv4Network('80.0.1.0/30'),
IPv4Network('80.0.1.4/30'),
IPv4Network('80.0.1.8/30'),
IPv4Network('80.0.1.12/30')]
In [30]: list(subnet1.subnets(new_prefix=29))
Out[30]: [IPv4Network('80.0.1.0/29'), IPv4Network(u'80.0.1.8/29')]
In [32]: subnet1[0]
Out[32]: IPv4Address('80.0.1.0')
In [33]: subnet1[5]
Out[33]: IPv4Address('80.0.1.5')
ipaddress.ip_interface
Using methods of IPv4Interface object you can get an address, mask or interface network:
In [37]: int1.ip
Out[37]: IPv4Address('10.0.1.1')
In [38]: int1.network
Out[38]: IPv4Network('10.0.1.0/24')
In [39]: int1.netmask
Out[39]: IPv4Address('255.255.255.0')
Since module has built-in address correctness checks, you can use them, for example, to check
whether an address is a network or host address:
In [43]: check_if_ip_is_network(IP1)
Out[43]: False
In [44]: check_if_ip_is_network(IP2)
Out[44]: True
tabulate
tabulate is a module that allows you to display table data beautifully. It is not part of standard
Python library, so tabulate needs to be installed:
• dictionary list (or any other iterable object with dictionaries). Keys are used as column names
In [4]: print(tabulate(sh_ip_int_br))
--------------- --------- -- --
FastEthernet0/0 15.0.15.1 up up
FastEthernet0/1 10.0.12.1 up up
FastEthernet0/2 10.0.13.1 up up
Loopback0 10.1.1.1 up up
Loopback100 100.0.0.1 up up
--------------- --------- -- --
headers
Parameter headers allows you to pass an additional argument that specifies column names:
Quite often, the first data set is headers. Then it is enough to specify headers equal to “firstrow”:
In [18]: data
Out[18]:
[('Interface', 'IP', 'Status', 'Protocol'),
('FastEthernet0/0', '15.0.15.1', 'up', 'up'),
('FastEthernet0/1', '10.0.12.1', 'up', 'up'),
('FastEthernet0/2', '10.0.13.1', 'up', 'up'),
('Loopback0', '10.1.1.1', 'up', 'up'),
('Loopback100', '100.0.0.1', 'up', 'up')]
If data is in the form of a list of dictionaries, you should specify headers equal to “keys”:
In [22]: list_of_dict
Out[22]:
[{'IP': '15.0.15.1',
'Interface': 'FastEthernet0/0',
'Protocol': 'up',
'Status': 'up'},
{'IP': '10.0.12.1',
'Interface': 'FastEthernet0/1',
(continues on next page)
'Protocol': 'up',
'Status': 'up'},
{'IP': '10.0.13.1',
'Interface': 'FastEthernet0/2',
'Protocol': 'up',
'Status': 'up'},
{'IP': '10.1.1.1',
'Interface': 'Loopback0',
'Protocol': 'up',
'Status': 'up'},
{'IP': '100.0.0.1',
'Interface': 'Loopback100',
'Protocol': 'up',
'Status': 'up'}]
In [6]: vlans = {"sw1": [10, 20, 30, 40], "sw2": [1, 2, 10], "sw3": [1, 2, 3, 4,␣
,→5, 10, 11, 12]}
Table style
</thead>
<tbody>
<tr><td>FastEthernet0/0</td><td>15.0.15.1</td><td>up </td><td>up </td>
,→</tr>
</tbody>
</table>
Alignment of columns
Note that not only columns are displayed centrally, but Markdown syntax has been changed accord-
ingly.
Additional material
• tabulate documentation
Stack Overflow:
• Printing Lists as Tabular Data. Note the answer - it contains other tabulate analogues.
pprint
Module pprint allows you to show Python objects beautifully. This saves the structure of object. You
can use the result that produces pprint to create object. Module pprint is part of standard Python
library.
The simplest use of module is ``pprint`` function. For example, a dictionary with nested dictio-
naries is displayed as follows:
In [6]: london_co = {'r1': {'hostname': 'london_r1', 'location': '21 New Globe Wal
...: k', 'vendor': 'Cisco', 'model': '4451', 'IOS': '15.4', 'IP': '10.255.0.1'}
...: , 'r2': {'hostname': 'london_r2', 'location': '21 New Globe Walk', 'vendor
...: ': 'Cisco', 'model': '4451', 'IOS': '15.4', 'IP': '10.255.0.2'}, 'sw1': {'
...: hostname': 'london_sw1', 'location': '21 New Globe Walk', 'vendor': 'Cisco
...: ', 'model': '3850', 'IOS': '3.6.XE', 'IP': '10.255.0.101'}}
...:
In [8]: pprint(london_co)
{'r1': {'IOS': '15.4',
'IP': '10.255.0.1',
'hostname': 'london_r1',
'location': '21 New Globe Walk',
'model': '4451',
'vendor': 'Cisco'},
'r2': {'IOS': '15.4',
'IP': '10.255.0.2',
'hostname': 'london_r2',
'location': '21 New Globe Walk',
'model': '4451',
'vendor': 'Cisco'},
'sw1': {'IOS': '3.6.XE',
'IP': '10.255.0.101',
'hostname': 'london_sw1',
'location': '21 New Globe Walk',
'model': '3850',
'vendor': 'Cisco'}}
List of lists:
In [14]: pprint(interfaces)
[['FastEthernet0/0', '15.0.15.1', 'YES', 'manual', 'up', 'up'],
['FastEthernet0/1', '10.0.1.1', 'YES', 'manual', 'up', 'up'],
(continues on next page)
String:
In [18]: tunnel
Out[18]: '\ninterface Tunnel0\n ip address 10.10.10.1 255.255.255.0\n ip mtu␣
,→1416\n ip ospf hello-interval 5\n tunnel source FastEthernet1/0\n tunnel␣
,→protection ipsec profile DMVPN\n'
In [19]: pprint(tunnel)
('\n'
'interface Tunnel0\n'
' ip address 10.10.10.1 255.255.255.0\n'
' ip mtu 1416\n'
' ip ospf hello-interval 5\n'
' tunnel source FastEthernet1/0\n'
' tunnel protection ipsec profile DMVPN\n')
Nesting restriction
Function pprint has an additional depth parameter that allows limiting the depth of data structure
display.
In [3]: result = {
...: 'interface Tunnel0': [' ip unnumbered Loopback0',
...: ' tunnel mode mpls traffic-eng',
...: ' tunnel destination 10.2.2.2',
...: ' tunnel mpls traffic-eng priority 7 7',
...: ' tunnel mpls traffic-eng bandwidth 5000',
...: ' tunnel mpls traffic-eng path-option 10 dynamic',
...: ' no routing dynamic'],
...: 'ip access-list standard LDP': [' deny 10.0.0.0 0.0.255.255',
...: ' permit 10.0.0.0 0.255.255.255'],
...: 'router bgp 100': {' address-family vpnv4': [' neighbor 10.2.2.2 activat
...: e',
...: ' neighbor 10.2.2.2 send-community both',
...: ' exit-address-family'],
...: ' bgp bestpath igp-metric ignore': [],
...: ' bgp log-neighbor-changes': [],
...: ' neighbor 10.2.2.2 next-hop-self': [],
...: ' neighbor 10.2.2.2 remote-as 100': [],
(continues on next page)
pformat
pformat() is a function that displays the result as a string. It is convenient to use if you want to write
a data structure into a file, for example to log.
In [17]: print(formatted_result)
{'interface Tunnel0': [' ip unnumbered Loopback0',
' tunnel mode mpls traffic-eng',
' tunnel destination 10.2.2.2',
' tunnel mpls traffic-eng priority 7 7',
' tunnel mpls traffic-eng bandwidth 5000',
' tunnel mpls traffic-eng path-option 10 dynamic',
' no routing dynamic'],
'ip access-list standard LDP': [' deny 10.0.0.0 0.0.255.255',
' permit 10.0.0.0 0.255.255.255'],
'router bgp 100': {' address-family vpnv4': [' neighbor 10.2.2.2 activate',
' neighbor 10.2.2.2 '
'send-community both',
' exit-address-family'],
' bgp bestpath igp-metric ignore': [],
' bgp log-neighbor-changes': [],
' neighbor 10.2.2.2 next-hop-self': [],
' neighbor 10.2.2.2 remote-as 100': [],
' neighbor 10.2.2.2 update-source Loopback0': [],
' neighbor 10.4.4.4 remote-as 40': []},
'router ospf 1': [' mpls ldp autoconfig area 0',
' mpls traffic-eng router-id Loopback0',
' mpls traffic-eng area 0',
' network 10.0.0.0 0.255.255.255 area 0']}
Additional material
Documentation:
Tasks
Warning: Starting from section “4. Data types in Python” there are automated tests for testing
tasks. They help to check whether everything matches the task, and also give feedback on what
does not correspond to the task. As a rule, after the first period of adaptation to tests, it becomes
easier to do tasks with tests. Testing is done using the pyneng utility. Learn more about how to
work with the pyneng utility.
Task 12.1
Restriction: All tasks must be done using the topics covered in this and previous chapters.
Task 12.2
The ping_ip_addresses function from task 12.1 only accepts a list of addresses, but it would be
convenient to be able to specify addresses using a range, for example 192.168.100.1-10.
In this task, you need to create a function convert_ranges_to_ip_list that converts a list of IP ad-
dresses in different formats into a list where each IP address is listed separately.
The function expects as an argument a list containing IP addresses and/or ranges of IP addresses.
• 10.1.1.1
• 10.1.1.1-10.1.1.10
• 10.1.1.1-10
If the address is specified as a range, the range must be expanded into individual addresses, includ-
ing the last address in the range. To simplify the task, we can assume that only the last octet of the
address changes in the range.
For example, if you pass the following list to the convert_ranges_to_ip_list function:
Task 12.3
Create a function print_ip_table that prints a table of available and unavailable IP addresses.
Reachable Unreachable
----------- -------------
10.1.1.1 10.1.1.7
10.1.1.2 10.1.1.8
10.1.1.9
• iterable
• iterators
• generator expressions
Iterable
Iteration is a generic term that describes the procedure for taking elements of something in turn. In
a more general sense, it is a sequence of instructions that is repeated a certain number of times or
before the specified condition is fulfilled.
An iterable is an object that can return elements one at a time. It is also an object from which an
iterator can be derived.
Examples of iterables:
• dicts
• files
In [2]: iter(lista)
Out[2]: <list_iterator at 0xb4ede28c>
iter function will work on any object that has __iter__ or __getitem__ method. __iter__ method
returns an iterator. If this method is not available, iter function checks if there is __getitem__
method that allows getting elements by index.
If method __getitem__ is present an iterator is returned, which iterates through the elements using
index (starting with 0). In practice, the use of __getitem__ means that all sequence elements are
iterable objects. For example, a list, a tuple, a string. Although these data types have __iter__
method.
Iterators
From Python point of view, it is any object that has __next__ method. This method returns the
next item if any, or returns StopIteration exception when items are finished. In addition, iterator
remembers which object it stopped at in the last iteration.
In Python, each iterator has __iter__ method - that is, every iterator is an iterable. This method
simply returns iterator itself.
In [4]: i = iter(numbers)
Now you can use next function that calls __next__ method to take the next element:
In [5]: next(i)
Out[5]: 1
In [6]: next(i)
Out[6]: 2
In [7]: next(i)
Out[7]: 3
In [8]: next(i)
------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-8-bed2471d02c1> in <module>()
----> 1 next(i)
StopIteration:
When we iterate over the list items, the iter function is first applied to the list to create the iterator,
and then its __next__ method is called until a StopIteration exception is raised.
Iterators are useful because they give elements one at a time. For example, when working with a
file, it is useful that memory will not contain the whole file, but only one line of a file.
File as iterator
!
service timestamps debug datetime msec localtime show-timezone year
service timestamps log datetime msec localtime show-timezone year
service password-encryption
service sequence-numbers
!
no ip domain lookup
!
ip ssh version 2
!
If you open a file with the open function, an object is returned that represents the file:
In [10]: f = open('r1.txt')
In [11]: f.__next__()
Out[11]: '!\n'
In [12]: f.__next__()
Out[12]: 'service timestamps debug datetime msec localtime show-timezone year\n'
ip ssh version 2
!
When working with files, using a file as an iterator does not simply allow iterate file line by line -
only one line is loaded into each iteration. This is very important when working with large files of
thousands and hundreds of thousands of lines, such as log files.
Therefore, when working with files in Python, the most commonly used expression is:
Generator
Generators are a special class of functions that allows you to easily create your own iterators. Unlike
regular functions, a generator doesn’t just return a value and exit, but returns an iterator that returns
the elements one at a time.
Note: Generators are not covered in this book and are only mentioned here because they are a
fairly straightforward way to create iterators. More about generators
• an exception raised
After function execution is finished the control is returned and program execution goes further. All
arguments that were passed to function like local variables, all of this is lost. Only the result that
returned a function remains.
A function can return a list of elements, multiple objects or different results depending on arguments,
but it always returns a single result.
Generator generates values. Values are then returned on demand and after return of one value
a function-generator is suspended until the next value is requested. Between requests, generator
retains its state.
• generator expression
• generator function
generator expression
Generator expression uses the same syntax as a list comprehensions, but returns iterator, not list
(notе the parentheses instead of the square brackets):
In [2]: genexpr
Out[2]: <generator object <genexpr> at 0xb571ec8c>
In [3]: next(genexpr)
Out[3]: 0
In [4]: next(genexpr)
Out[4]: 1
In [5]: next(genexpr)
Out[5]: 4
Further reading
Documentation Python:
• Sequence types
• Iterator types
Articles:
When working with network equipment, regular expressions can be used, for example, to:
• select a portion of lines from show command output that matches the template
• After processing the output of show version command, you can collect information about OS
version and uptime.
• get from log file the lines that correspond to the template.
In addition, in network equipment the regular expressions can be used to filter the output of any
show commands.
In general, use of regular expressions will involve getting part of a text out of a large output. But
that’s not the only thing they can be used for. For example, regular expressions can be used to
perform string replacements or for dividing a string.
These areas of use overlap with methods that apply to strings. And if problemi is clear and simple
to solve with string methods, it is better to use them. This code will be easier to understand and, in
addition, string methods work faster.
But string methods may not solve all problems or may make problem much harder to solve. Regular
expressions can help in this case.
281
Python for network engineers, Release 1.0
Python uses re module to work with regular expressions (regex). To get started with regular expres-
sions, you need to import re module.
This section will use search function for all examples. And in the next chapter, the rest of functions
of re module will be covered.
If a match is found, function will return special object Match. If there is no match, function will return
None.
Important distinction of search function is that it only looks for a first match. That is, if there are
several substrings in a line that correspond to a regex, search will return only the first match found.
In [1]: import re
In [2]: int_line = ' MTU 1500 bytes, BW 10000 Kbit, DLY 1000 usec,'
In this example:
• in line 3 a search pattern is passed to search function plus string int_line in which the match
is searched
In this case we are simply looking for whether there is ‘MTU’ substring in string int_line. If it exists,
match variable will contain a special Match object:
In [4]: print(match)
<_sre.SRE_Match object; span=(2, 5), match='MTU'>
Match object has several methods that allow to get different information about received match. For
example, group method shows that string matches an expression described.
In [5]: match.group()
Out[5]: 'MTU'
In [6]: int_line = ' MTU 1500 bytes, BW 10000 Kbit, DLY 1000 usec,'
In [8]: print(match)
None
The full potential of regular expressions is revealed when using special characters. For example,
symbol \d means a digit, + means repetition of previous symbol one or more times. If you combine
them \d+, you get an expression that means one or more digits.
Using this expression, you can get the part of string that describes bandwidth:
In [9]: int_line = ' MTU 1500 bytes, BW 10000 Kbit, DLY 1000 usec,'
In [11]: match.group()
Out[11]: 'BW 10000'
Regular expressions are particularly useful in getting certain substrings from a string. For example,
it is necessary to get VLAN, MAC and ports from the output of such log message:
In [13]: re.search('Host (\S+) in vlan (\d+) is flapping between port (\S+) and␣
,→port (\S+)', log2).groups()
Out[13]: ('f04d.a206.7fd6', '1', 'Gi0/5', 'Gi0/16')
Method group returns only those parts of original string that are in parentheses. Thus, by placing a
part of expression in parentheses, you can specify which parts of the line you want to remember.
Expression \d+ has been used before - it describes one or more digits. And expression \S+ describes
all characters except whitespace (space, tab, etc.).
The following subsections deal with special characters that are used in regular expressions.
Note: If you know what special characters mean in regular expressions, you can skip the following
subsection and immediately switch to subsection about module re.
Character sets
• \d - any digit
• \s - whitespace character
Note: These are not all character sets that support Python. See documentation for details.
Character sets allow you to write shorter expressions without having to list all necessary characters.
For example, get time from log file string:
Expression \w\w\w\w\.\w\w\w\w\.\w\w\w\w describes 12 letters or digits that are divided into three
groups of four characters and separated by dot.
Symbol groups are very convenient, but for now it is necessary to manually specify a character
repetition. The following subsection covers repetition symbols which will simplify description of
expressions.
Repeating characters
Plus indicates that the previous expression can be repeated as many times as you like, but at least
once. For example, here the repetition refers to letter ‘a’:
Expresson (a1)+ uses parentheses to specify that repetition is related to sequence of symbols ‘a1’.
IP address can be described by \d+\.\d+\.\d+\.\d+. Plus is used to indicate that there can be
several digits. Expression \. is required because the dot is a special symbol (it denotes any symbol).
And in order to indicate that we are interested in a dot as a symbol, you have to screen it - put a
backslash in front of a dot.
Using this expression, you can get an IP address from sh_ip_int_br string:
Another example of an expression: \d+\s+\S+ - describes a string which has digits first, then whites-
pace characters, and then non-whitespace characters (all except space, tab, and other similar char-
acters). Using it you can get VLAN and MAC address from string:
Asterisk indicates that the previous expression can be repeated 0 or more times. For example, if an
asterisk stands after a symbol, it means a repetition of that symbol.
Suppose you write a regex that describes email addresses in two formats: [email protected] and
[email protected]. That is, the left side of address can have either one word or two words
separated by a dot.
But such an expression is not suitable for an email address with a dot:
To describe both email, you have to specify that the dot is optional:
'\w+\.*\w+@\w+\.\w+'
In the last example, regex indicates that the dot is optional, but at the same time determines that
it can appear many times.
In this situation, it is more logical to use a question mark. It denotes zero or one repetition of a
preceding expression or symbol. Now regex looks like \w+\.?\w+@\w+\.\w+:
{n}
You can set how many times the previous expression should be repeated with curly braces.
For example, expression \w{4}\.\w{4}\.\w{4} describes 12 letters or digits that are divided into
three groups of four characters and separated by dot. This way you can get a MAC address:
You can specify a repetition range in curly braces. For example, try to get all VLAN numbers from
string mac_table:
Since search only looks for the first match, expression \d{1,4} will have VLAN number:
VLAN: 200
VLAN: 300
VLAN: 1100
VLAN: 500
VLAN: 1200
VLAN: 1300
Note that the output of command from equipment does not have a VLAN with number 1. Regex
got a number 1 from somewhere. Number 1 was in the output from hostname in line sw1#sh mac
address-table.
To correct this, it suffices to complete an expression and indicate that at least one space must follow
the numbers:
Special symbols
• ^ - beginning of line
• $ - end of line
• a|b - element a or b
• (regex) - expression is treated as one element. In addition, substring that matches an expres-
sion is memorized
Dot represents any symbol. Most often, a dot is used with repetition symbols + and * to indicate
that any character can be found between certain expressions.
For example, using expression Interface.+Port ID.+ you can describe a line with interfaces in the
output “sh cdp neighbors detail”:
The result was only one string as the dot represents any character except line feed character. In
addition, repetition characters + and * by default capture the longest string possible. This aspect is
addressed in subsection “Greedy qualifiers”.
Expression \S+$ describes any characters except whitespace at the end of line:
[]
Symbols that are listed in square brackets mean that any of these symbols will be a match. Thus,
different registers can be described:
Using square brackets, you can specify which characters may meet at a specific position. For exam-
ple, expression ^.+[>#] describes characters from the beginning of a line to # or > sign (including
them). This expression can be used to get the name of device:
You can specify character ranges in square brackets. For example, any number from 0 to 9:
Letters:
Another feature of square brackets is that the special symbols within square brackets lose their
special meaning and are simply a symbol. For example, a dot inside square brackets will denote a
dot, not any symbol.
2. dot or slash
If first symbol in square brackets is ^, match will be any symbol except those in brackets.
Note how | works - Fast и 0/1 are treated as an whole expression. So in the end, expression means
that we’re looking for Fast or 0/1.
()
For example, expression [0-9]([a-f]|[0-9])[0-9] describes three characters: digit, then a letter
or digit and digit:
Parentheses allow to indicate which expression is a one entity. This is particularly useful when using
repetition symbols:
Parentheses not only allow you to group expressions. String that matches expression in parentheses
is memorized. It can be obtained separately by special methods groups and group(n). This is
covered in subsection “Grouping”.
Greedy qualifiers
By default, *, +, and ? qualifiers are all greedy - they match as much text as possible.
In [1]: import re
In [4]: match.group()
Out[4]: '<text line> some text>'
In this case, expression captured maximum possible piece of symbols contained in <>. If greedy
behavior need to be disabled, just add a question mark after the repetition symbols:
In [7]: match.group()
Out[7]: '<text line>'
But greed is often useful. For example, without turning off greed of the last plus, expression
\d+\s+\S+ describes line:
Symbol \S denotes everything except whitespace characters. Therefore, expression \S+ with greedy
repetition symbol describes maximum long string until the first whitespace character. In this case
up to the first space.
Grouping
Grouping indicates that sequence of symbols should be considered as a one. However, this is not
the only advantage of grouping. In addition, by use of groups you can get only a certain portion of
string that has been described by expression.
For example, from a log file you should select strings in which “%SW_MATM-4-MACFLAP_NOTIF”
match occur and then from each such string get MAC address, VLAN and interfaces. In this case,
regex has to describe a string and all parts of string to be remembered are placed in parentheses.
For example, from the log file, you need to select the lines that contain “%SW_MATM-4-
MACFLAP_NOTIF”, and then get the MAC address, VLAN and interfaces from each such line. In this
case, the regex not only describes the string, but also indicates all parts of the string to be returned
in parentheses.
• Numbered groups
• Named groups
Numbered groups
Inside expression, group are numbered from left to right starting with 1. Groups can then be selected
by numbers to get text that corresponds to group expression.
The second group could be described as the first. Other version is just for example.
You can now access a group by number. Group 0 is a string that corresponds to the entire match:
In [10]: match.group(0)
Out[10]: 'FastEthernet0/1 10.0.12.1 YES manual up ␣
,→ up'
In [11]: match.group(1)
Out[11]: 'FastEthernet0/1'
(continues on next page)
In [12]: match.group(2)
Out[12]: '10.0.12.1'
In [13]: match.group(1, 2)
Out[13]: ('FastEthernet0/1', '10.0.12.1')
In [14]: match.group(2, 1, 2)
Out[14]: ('10.0.12.1', 'FastEthernet0/1', '10.0.12.1')
In [15]: match[0]
Out[15]: 'FastEthernet0/1 10.0.12.1 YES manual up ␣
,→ up'
In [16]: match[1]
Out[16]: 'FastEthernet0/1'
In [17]: match[2]
Out[17]: '10.0.12.1'
In [18]: match.groups()
Out[18]: ('FastEthernet0/1', '10.0.12.1')
Named groups
When expression is complex, it is not very convenient to determine number of group. Plus, when
you modify an expression the order of groups can be changed and you will need to change the code
that refers to groups.
Named groups allow you to give a name to the group. Syntax of named group (?P<name>regex):
In [21]: match.group('intf')
Out[21]: 'FastEthernet0/1'
In [22]: match.group('address')
Out[22]: '10.0.12.1'
It is also very useful that with groupdict method you can get a dictionary where keys are the names
of groups and values are the substrings that correspond to them:
In [23]: match.groupdict()
Out[23]: {'address': '10.0.12.1', 'intf': 'FastEthernet0/1'}
And then you can add groups to regex and rely on their name instead of order:
In [25]: match.groupdict()
Out[25]:
{'address': '10.0.12.1',
'intf': 'FastEthernet0/1',
'protocol': 'up',
'status': 'up'}
Consider another example of using named groups. In this example, the task is to get from the output
of ‘show ip dhcp snooping binding’ the fields: MAC address, IP address, VLAN and interface.
File dhcp_snooping.txt contains the output of command ‘show ip dhcp snooping binding’:
In regex terms, named groups are used for those parts of the output that need to be remembered:
Comments on regex:
• (?P<mac>\S+) + - group with name ‘mac’ matches any characters except whitespace charac-
ters. regex describes a sequence of any characters before space
• \d+ + - numerical sequence (one or more digits) followed by one or more spaces. Lease value
gets here
• \S+ +- sequence of any characters other than whitespace. This matches Type (in this case all
of them ‘dhcp-snooping’)
• (?P<vlan>\d+) + - named group ‘vlan’. Only numerical sequences with one or more charac-
ters are included here
• (?P<port>.\S+) - named group ‘port’. All characters except whitespace are included here
In [3]: match.groupdict()
Out[3]:
{'int': 'FastEthernet0/1',
'ip': '10.1.10.2',
'mac': '00:09:BB:3D:D6:58',
'vlan': '10'}
Since regex has worked well, you can create a script. In script all lines of dhcp_snooping.txt
file are iterated and information about devices is displayed on the standard output stream
(parse_dhcp_snooping.py):
import re
result = []
Script output:
$ python parse_dhcp_snooping.py
4 devices connected to switch
Parameters of device 1:
int: FastEthernet0/1
ip: 10.1.10.2
mac: 00:09:BB:3D:D6:58
vlan: 10
Parameters of device 2:
int: FastEthernet0/10
ip: 10.1.5.2
mac: 00:04:A3:3E:5B:69
vlan: 5
Parameters of device 3:
int: FastEthernet0/9
ip: 10.1.5.4
mac: 00:05:B3:7E:9B:60
vlan: 5
Parameters of device 4:
int: FastEthernet0/3
ip: 10.1.10.6
mac: 00:09:BC:3F:A6:50
vlan: 10
Non-capturing group
By default, everything that fell into the group is remembered. It’s called a capturing group.
Sometimes parentheses are needed to indicate a part of expression that repeats. And, in doing so,
you don’t need to remember an expression.
For example, get a MAC address, VLAN and ports from log message:
• (\w{4}\.){2} - here parentheses are used to indicate that 4 letters or digits and a dot are
repeated twice
In [3]: match.groups()
Out[3]: ('f03a.b216.7ad7', 'b216.', '10', 'Gi0/5', 'Gi0/15')
The second element is essentially superfluous. It appeared in the output because of parentheses in
expression (\w{4}\.){2}.
In this case, you need to disable capture in the group. This is done by adding ?: after the group’s
opening parenthesis.
In [5]: match.groups()
Out[5]: ('f03a.b216.7ad7', '10', 'Gi0/5', 'Gi0/15')
When working with groups, you can use the result that matched with the group, further in the same
expression.
For example, in the output of ‘sh ip bgp’ the last column describes AS Path attribute (through which
autonomous systems the route passed):
Suppose you get those prefixes where the same AS number repeats several times in the path.
This can be done by reference to a result that has been captured by the group. For example, such
an expression displays all lines in which the same number is repeated at least twice:
In this expression, \1 denotes the result that falls into the group. Number one indicates a specific
group. In this case, it’s Group 1, it’s only one group here.
Similarly, you can describe strings where the same number occurs three times:
...: if match:
...: print(line)
...:
* 192.168.66.0/24 192.168.79.7 0 500 500 500 i
* 192.168.67.0/24 192.168.79.7 0 0 700 700 700 i
* 192.168.88.0/24 192.168.79.7 0 700 700 700 i
You can reffer to the result which was captured by named group:
15. Module re
• findall - searches for all matches with template. Returns the resulting strings as a list
• compile - compiles regex. You can then apply all of listed functions to this object
In addition to functions that search matches, module has the following functions:
Match object
• search
• match
In [2]: match = re.search(r'Host (\S+) in vlan (\d+) .* port (\S+) and port (\S+)
,→', log)
In [3]: match
Out[3]: <_sre.SRE_Match object; span=(47, 124), match='Host f03a.b216.7ad7 in␣
,→vlan 10 is flapping betwee>'
The 3rd line output simply displays information about object. Therefore, it is not necessary to rely
on what is displayed in match part as displayed line is cut by a fixed number of characters.
group
In [4]: match.group()
Out[4]: 'Host f03a.b216.7ad7 in vlan 10 is flapping between port Gi0/5 and port␣
,→Gi0/15'
In [5]: match.group(0)
Out[5]: 'Host f03a.b216.7ad7 in vlan 10 is flapping between port Gi0/5 and port␣
,→Gi0/15'
In [6]: match.group(1)
Out[6]: 'f03a.b216.7ad7'
In [7]: match.group(2)
Out[7]: '10'
In [8]: match.group(3)
Out[8]: 'Gi0/5'
In [9]: match.group(4)
Out[9]: 'Gi0/15'
If you call a group method with a group number that is larger than number of existing groups, there
is an error:
In [10]: match.group(5)
-----------------------------------------------------------------
IndexError Traceback (most recent call last)
<ipython-input-18-9df93fa7b44b> in <module>()
----> 1 match.group(5)
If you call a method with multiple group numbers, the result is a tuple with strings that correspond
to matches:
In [11]: match.group(1, 2, 3)
Out[11]: ('f03a.b216.7ad7', '10', 'Gi0/5')
Group may not get anything, then it will be matched with an empty string:
In [14]: match.group(2)
Out[14]: ''
If group describes a part of template and there are more than one match, method displays the last
match:
In [17]: match.group(1)
Out[17]: 'b216.'
This is because expression in parentheses describes four letters or numbers, dot and then there is a
plus. The first and the second part of MAC address matched to expression in parentheses. But only
the last expression is remembered and returned.
If named groups are used in expression, group name can be passed to group method and the cor-
responding substring can be obtained:
In [20]: match.group('mac')
Out[20]: 'f03a.b216.7ad7'
In [21]: match.group('int2')
Out[21]: 'Gi0/15'
In [22]: match.group(3)
Out[22]: 'Gi0/5'
In [23]: match.group(4)
Out[23]: 'Gi0/15'
groups
Method group returns a tuple with strings in which the elements are those substrings that fall into
respective groups:
In [26]: match.groups()
Out[26]: ('f03a.b216.7ad7', '10', 'Gi0/5', 'Gi0/15')
Method group has an optional parameter - default. It returned when anything that comes into group
is optional.
For example, with this line the match will be in both the first group and the second:
In [28]: match.groups()
Out[28]: ('100', 'aab1')
If there is nothing in the line after space, nothing will get into the group. But the match will be
because it is stated in regex that the group is optional:
In [32]: match.groups()
Out[32]: ('100', None)
In [35]: match.groups(default=0)
Out[35]: ('100', 0)
groupdict
Method groupdict returns a dictionary in which keys are group names and values are corresponding
lines:
In [39]: match.groupdict()
Out[39]: {'int1': 'Gi0/5', 'int2': 'Gi0/15', 'mac': 'f03a.b216.7ad7', 'vlan': '10
,→'}
start, end
start and end methods return indexes of the beginning and end of the match of regex.
If methods are called without arguments, they return indexes for whole match:
In [42]: match.start()
Out[42]: 2
In [43]: match.end()
Out[43]: 42
In [45]: line[match.start():match.end()]
Out[45]: '10 aab1.a1a1.a5d3 FastEthernet0/1'
You can pass number or name of the group to methods. Then they return indexes for this group:
In [46]: match.start(2)
Out[46]: 9
In [47]: match.end(2)
Out[47]: 23
In [48]: line[match.start(2):match.end(2)]
Out[48]: 'aab1.a1a1.a5d3'
In [51]: match.start('mac')
Out[51]: 52
In [52]: match.end('mac')
Out[52]: 66
span
Method span returns a tuple with an index of the beginning and end of substring. It works in a similar
way to start and end methods, but returns a pair of numbers.
In [55]: match.span()
Out[55]: (2, 42)
In [58]: match.span(2)
Out[58]: (9, 23)
In [64]: match.span('mac')
Out[64]: (52, 66)
In [65]: match.span('vlan')
Out[65]: (75, 77)
Search function
Function search:
Function search is suitable when you need to find only one match in a string, for example when a
regex describes the entire string or part of a string.
Consider an example of using search function to parse a log file. File log.txt contains log messages
indicating that the same MAC is too often re-learned on one or another interface. One of the reasons
for these messages is loop in network.
MAC address can jump between several ports. In this case it is very important to know from which
ports MAC comes.
Try to figure out which ports and which VLAN was the problem. Check regex with one line from log
file:
In [1]: import re
Regex is divided into parts for ease of reading. It has three groups:
In [4]: match.groups()
Out[4]: ('10', 'Gi0/16', 'Gi0/24')
In the resulting script, log.txt is processed line by line and port information is collected from each
line. Since ports can be duplicated we add them immediately to the set in order to get a compilation
of unique interfaces (parse_log_search.py file):
import re
ports = set()
with open('log.txt') as f:
for line in f:
match = re.search(regex, line)
if match:
vlan = match.group(1)
ports.add(match.group(2))
ports.add(match.group(3))
$ python parse_log_search.py
Loop between ports Gi0/19, Gi0/24, Gi0/16 в VLAN 10
Try to get device parameters from ‘sh cdp neighbors detail’ output.
Version :
Cisco IOS Software, C2960 Software (C2960-LANBASEK9-M), Version 12.2(55)SE9,␣
,→RELEASE SOFTWARE (fc1)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2014 by Cisco Systems, Inc.
Compiled Mon 03-Mar-14 22:53 by prod_rel_team
advertisement version: 2
VTP Management Domain: ''
Native VLAN: 1
Duplex: full
Management address(es):
IP address: 10.1.1.2
• IOS version (Cisco IOS Software, C2960 Software (C2960-LANBASEK9-M), Version 12.2(55)SE9,
RELEASE SOFTWARE (fc1))
And for convenience you need to get data in the form of a dictionary. Example of the resulting
dictionary for SW2 switch:
import re
from pprint import pprint
def parse_cdp(filename):
result = {}
with open(filename) as f:
for line in f:
if line.startswith('Device ID'):
neighbor = re.search('Device ID: (\S+)', line).group(1)
result[neighbor] = {}
elif line.startswith(' IP address'):
ip = re.search('IP address: (\S+)', line).group(1)
result[neighbor]['ip'] = ip
elif line.startswith('Platform'):
platform = re.search('Platform: (\S+ \S+),', line).group(1)
result[neighbor]['platform'] = platform
elif line.startswith('Cisco IOS Software'):
ios = re.search('Cisco IOS Software, (.+), RELEASE',
line).group(1)
result[neighbor]['ios'] = ios
return result
pprint(parse_cdp('sh_cdp_neighbors_sw1.txt'))
The desired strings are selected using startswith() string method. And in a string, a regex takes
required part of the string. It all ends up in a dictionary.
$ python parse_sh_cdp_neighbors_detail_ver1.py
{'R1': {'ios': '3800 Software (C3825-ADVENTERPRISEK9-M), Version 12.4(24)T1',
'ip': '10.1.1.1',
'platform': 'Cisco 3825'},
'R2': {'ios': '2900 Software (C3825-ADVENTERPRISEK9-M), Version 15.2(2)T1',
'ip': '10.2.2.2',
'platform': 'Cisco 2911'},
'SW2': {'ios': 'C2960 Software (C2960-LANBASEK9-M), Version 12.2(55)SE9',
'ip': '10.1.1.2',
'platform': 'cisco WS-C2960-8TC-L'}}
import re
from pprint import pprint
def parse_cdp(filename):
regex = ('Device ID: (?P<device>\S+)'
'|IP address: (?P<ip>\S+)'
'|Platform: (?P<platform>\S+ \S+),'
'|Cisco IOS Software, (?P<ios>.+), RELEASE')
result = {}
with open(filename) as f:
for line in f:
match = re.search(regex, line)
if match:
if match.lastgroup == 'device':
device = match.group(match.lastgroup)
result[device] = {}
elif device:
result[device][match.lastgroup] = match.group(
match.lastgroup)
return result
pprint(parse_cdp('sh_cdp_neighbors_sw1.txt'))
• lastgroup method returns name of the last named group in regex for which a match has been
found
• if a match was found for device group, the value that fells into the group is written to device
variable
$ python parse_sh_cdp_neighbors_detail_ver2.py
{'R1': {'ios': '3800 Software (C3825-ADVENTERPRISEK9-M), Version 12.4(24)T1',
'ip': '10.1.1.1',
'platform': 'Cisco 3825'},
'R2': {'ios': '2900 Software (C3825-ADVENTERPRISEK9-M), Version 15.2(2)T1',
(continues on next page)
'ip': '10.2.2.2',
'platform': 'Cisco 2911'},
'SW2': {'ios': 'C2960 Software (C2960-LANBASEK9-M), Version 12.2(55)SE9',
'ip': '10.1.1.2',
'platform': 'cisco WS-C2960-8TC-L'}}
Match function
Function match:
Match function differs from search in that match always looks for a match at the beginning of the
line. For example, if you repeat the example that was used for search function, but with match:
In [2]: import re
In [6]: print(match)
None
This is because match searches for Host word at the beginning of the line. But this message is in
the middle.
Expression \S+: was added before Host word. Now match will be found:
In [11]: print(match)
<_sre.SRE_Match object; span=(0, 104), match='%SW_MATM-4-MACFLAP_NOTIF: Host 01e2.
,→4c18.0156 in >
In [12]: match.groups()
Out[12]: ('10', 'Gi0/16', 'Gi0/24')
Example is similar to one used in search function with minor changes (parse_log_match match.py
file):
import re
ports = set()
with open('log.txt') as f:
for line in f:
match = re.match(regex, line)
if match:
vlan = match.group(1)
ports.add(match.group(2))
ports.add(match.group(3))
$ python parse_log_match.py
Loop between ports Gi0/19, Gi0/24, Gi0/16 in VLAN 10
Finditer function
Function finditer:
Function finditer is well suited to handle those commands whose output is displayed by columns.
For example: ‘sh ip int br’, ‘sh mac address-table’, etc. In this case it can be applied to the entire
output of command.
In [12]: result
Out[12]: <callable_iterator at 0xb583f46c>
In [16]: groups = []
Now in groups list there are tuples with strings that fallen into groups:
In [19]: groups
Out[19]:
[('FastEthernet0/0', '15.0.15.1', 'up', 'up'),
('FastEthernet0/1', '10.0.12.1', 'up', 'up'),
('FastEthernet0/2', '10.0.13.1', 'up', 'up'),
('Loopback0', '10.1.1.1', 'up', 'up'),
('Loopback100', '100.0.0.1', 'up', 'up')]
In [22]: result
Out[22]:
[('FastEthernet0/0', '15.0.15.1', 'up', 'up'),
('FastEthernet0/1', '10.0.12.1', 'up', 'up'),
('FastEthernet0/2', '10.0.13.1', 'up', 'up'),
('Loopback0', '10.1.1.1', 'up', 'up'),
('Loopback100', '100.0.0.1', 'up', 'up')]
Now we will analyze the same log file that was used in search and match subsections.
import re
ports = set()
with open('log.txt') as f:
(continues on next page)
Warning: In real life, a log file can be very large. In that case, it’s better to process it line by
line.
$ python parse_log_finditer.py
Loop between ports Gi0/19, Gi0/24, Gi0/16 в VLAN 10
finditer can handle output of ‘sh cdp neighbors detail’ as well as in re.search subsection.
import re
from pprint import pprint
def parse_cdp(filename):
regex = (r'Device ID: (?P<device>\S+)'
r'|IP address: (?P<ip>\S+)'
r'|Platform: (?P<platform>\S+ \S+),'
r'|Cisco IOS Software, (?P<ios>.+), RELEASE')
result = {}
with open(filename) as f:
match_iter = re.finditer(regex, f.read())
for match in match_iter:
if match.lastgroup == 'device':
device = match.group(match.lastgroup)
result[device] = {}
elif device:
(continues on next page)
result[device][match.lastgroup] = match.group(match.lastgroup)
return result
pprint(parse_cdp('sh_cdp_neighbors_sw1.txt'))
Now matches are searched throughout the file, not in every line separately:
with open(filename) as f:
match_iter = re.finditer(regex, f.read())
with open(filename) as f:
match_iter = re.finditer(regex, f.read())
for match in match_iter:
$ python parse_sh_cdp_neighbors_detail_finditer.py
{'R1': {'ios': '3800 Software (C3825-ADVENTERPRISEK9-M), Version 12.4(24)T1',
'ip': '10.1.1.1',
'platform': 'Cisco 3825'},
'R2': {'ios': '2900 Software (C3825-ADVENTERPRISEK9-M), Version 15.2(2)T1',
'ip': '10.2.2.2',
'platform': 'Cisco 2911'},
'SW2': {'ios': 'C2960 Software (C2960-LANBASEK9-M), Version 12.2(55)SE9',
'ip': '10.1.1.2',
'platform': 'cisco WS-C2960-8TC-L'}}
Although the result is similar, finditer has more features, as you can specify not only what should
be in searched string but also in strings around it. For example, you can specify exactly which IP
address to take:
...
Native VLAN: 1
(continues on next page)
Duplex: full
Management address(es):
IP address: 10.1.1.2
If you want to take the first IP address you can supplement a regex like this:
Findall function
Function findall:
• returns:
– list of strings that are described by regex if there are no groups in regex
– list of strings that match with regex in the group if there is only one group in regex
– list of tuples containing strings that matches with expression in the group if there are more
than one group
Consider the work of findall with an example of ‘sh mac address-table output’:
In [3]: print(mac_address_table)
sw1#sh mac address-table
Mac Address Table
-------------------------------------------
The first example is a regex without groups. In this case findall returns a list of strings that matches
with regex.
For example, with findall you can get a list of matching strings with vlan - mac – interface
and get rid of header in the output of command:
As soon as a group appears in regex, findall behaves differently. If one group is used in the
expression, findall returns a list of strings that matches with expression in the group:
findall searches for a match of the entire string but returns a result similar to group method in
Match object. If there are several groups, findall will return the list of tuples:
If such features of findall function prevent you from getting the needed result, it is better to use
finditer function, but sometimes this behavior is appropriate and convenient to use.
import re
ports = set()
with open('log.txt') as f:
result = re.findall(regex, f.read())
for vlan, port1, port2 in result:
ports.add(port1)
ports.add(port2)
$ python parse_log_findall.py
Loop between ports Gi0/19, Gi0/16, Gi0/24 в VLAN 10
Compile function
Python has the ability to pre-compile a regular expression and then use it. This is particularly useful
when regex is used a lot in the script.
The use of a compiled expression can speed up processing and it is generally more convenient to use
this option as the program divides the creation of a regex and its use. In addition, using re.compile
function creates a RegexObject object that has several additional features that are not present in
MatchObject object.
In [53]: regex
Out[53]: re.compile(r'\d+ +\S+ +\w+ +\S+', re.UNICODE)
Note that Regex object has search, match, finditer, findall methods available. These are the
same functions that are available in module globally, but now they have to be applied to object.
Now search should be called as method of regex object. The result is a Match object:
In [69]: match
Out[69]: <_sre.SRE_Match object; span=(1, 43), match='100 a1b2.ac10.7000 ␣
,→DYNAMIC Gi0/1'>
In [70]: match.group()
Out[70]: '100 a1b2.ac10.7000 DYNAMIC Gi0/1'
An example of compiling a regex and its use based on example of a log file (parse_log_compile.py
file):
import re
ports = set()
(continues on next page)
with open('log.txt') as f:
for m in regex.finditer(f.read()):
vlan = m.group(1)
ports.add(m.group(2))
ports.add(m.group(3))
for m in regex.finditer(f.read()):
When using re.compile in search, match, findall, finditer and fullmatch methods, additional
parameters appear:
• pos - allows you to specify an index in string from where to start looking for a match
For example, this is the result without specifying pos, endpos parameters:
In [78]: match.group()
Out[78]: '100 a1b2.ac10.7000 DYNAMIC Gi0/1'
In [80]: match.group()
Out[80]: '00 a1b2.ac10.7000 DYNAMIC Gi0/1'
In [82]: match.group()
Out[82]: '00 a1b2.ac10.7000 DYNAMIC Gi0/1'
In [93]: match.group()
Out[93]: '00 a1b2.ac10.7000 DYNAMIC Gi'
In [95]: match.group()
Out[95]: '00 a1b2.ac10.7000 DYNAMIC Gi'
In match, findall, finditer and fullmatch methods pos and endpos parameters work similarly.
Flags
When using re functions or creating a compiled regex you can specify additional flags that affect the
behavior of regex.
• re.ASCII (re.A)
• re.IGNORECASE (re.I)
• re.MULTILINE (re.M)
• re.DOTALL (re.S)
• re.VERBOSE (re.X)
• re.LOCALE (re.L)
• re.DEBUG
In this subsection the re.DOTALL flag is covered. Information about other flags is available in docu-
mentation.
re.DOTALL
For example, from sh_cdp string you need to get a device name, platform and IOS:
Of course, in this case it is possible to divide a string into parts and work with each string separately,
but you can get necessary data without splitting.
In this case, there will be no match because by default a dot means any character other than a new
line character:
In [6]: match.groups()
Out[6]: ('SW2', 'WS-C2960-8TC-L', '12.2(55)SE9')
Since new line character is now included, combination .+ captures everything between data.
Now try to use this regex to get information about all neighbors from sh_cdp_neighbors_sw1.txt file.
Version :
Cisco IOS Software, C2960 Software (C2960-LANBASEK9-M), Version 12.2(55)SE9,␣
,→RELEASE SOFTWARE (fc1)
Technical Support: http://www.cisco.com/techsupport
-------------------------
Device ID: R1
Entry address(es):
IP address: 10.1.1.1
Platform: Cisco 3825, Capabilities: Router Switch IGMP
Interface: GigabitEthernet1/0/22, Port ID (outgoing port): GigabitEthernet0/0
Holdtime : 156 sec
Version :
Cisco IOS Software, 3800 Software (C3825-ADVENTERPRISEK9-M), Version 12.4(24)T1,␣
,→RELEASE SOFTWARE (fc3)
Technical Support: http://www.cisco.com/techsupport
-------------------------
(continues on next page)
Device ID: R2
Entry address(es):
IP address: 10.2.2.2
Platform: Cisco 2911, Capabilities: Router Switch IGMP
Interface: GigabitEthernet1/0/21, Port ID (outgoing port): GigabitEthernet0/0
Holdtime : 156 sec
Version :
Cisco IOS Software, 2900 Software (C3825-ADVENTERPRISEK9-M), Version 15.2(2)T1,␣
,→RELEASE SOFTWARE (fc3)
Technical Support: http://www.cisco.com/techsupport
At first glance, it seems that instead of three devices there was only one device in output. However,
if you look at the results the tuple has Device ID from the first neighbor and platform and IOS from
the last neighbor.
This is because there is a .+ combination between desired parts of the output. Without re.DOTALL
flag, such an expression would capture everything before new line character, but with a flag it
captures the longest possible piece of text because + is greedy. As a result, regex describes a string
from the first Device ID to the last place where Cisco IOS Software.+ Version match occurs.
This situation occurs very often when using re.DOTALL and in order to correct it remember to disable
greedy behavior:
Function re.split
Function split works similary to split method in strings, but in re.split function you can use
regular expressions which means dividing a string into parts using more complex conditions.
For example, ospf_route string should be split by spaces (as in str.split method):
'3d18h',
'FastEthernet0/0']
Function split has a peculiarity of working with groups (expressions in parentheses). If you specify
the same expression with parentheses, the resulting list will include separators.
To disable such behavior you should make a noncapture group. That is, disable capturing of group
elements:
Function re.sub
Function re.sub works similary to replace method in strings. But in re.sub you can use regex and
therefore make substitutions using more complex conditions. Replace commas, square brackets
and via word with space in ospf_route string:
With re.sub you can transform a string. For example, convert mac_table string to:
In a second regex these groups are used. To refer to a group a backslash and a group number are
used. To avoid backslash screening, raw string is used. As a result, the corresponding substrings
will be substituted instead of group numbers. For example, format of MAC address record was also
changed.
Further reading
• regex101
• for Python - you can specify search, match, findall methods and flags. An example of a regular
expression. Unfortunately, sometimes not all expressions are perceived.
• Another site for Python - does not support methods but works well and has worked out the
expressions which didn’t work in previous site. It’s perfect for one-line text. With the multiline,
it worth considering that Python will have a different situation.
• Many examples of the use of regular expressions from basics to more complex themes
• Regex Crossword
Tasks
Warning: Starting from section “4. Data types in Python” there are automated tests for testing
tasks. They help to check whether everything matches the task, and also give feedback on what
does not correspond to the task. As a rule, after the first period of adaptation to tests, it becomes
easier to do tasks with tests. Testing is done using the pyneng utility. Learn more about how to
work with the pyneng utility.
Task 15.1
Create a get_ip_from_cfg function that expects the name of the file containing the device configu-
ration as an argument.
The function should process the configuration and return the IP addresses and masks that are con-
figured on the interfaces as a list of tuples:
Please note that in this case, you can not check the correctness of the IP address, address ranges,
and so on, since the command output from network device is processed, not user input.
Task 15.1a
Copy the get_ip_from_cfg function from task 15.1 and redesign it so that it returns a dictionary:
– IP address
– mask
Add to the dictionary only those interfaces on which IP addresses are configured.
Check the operation of the function using the example of the config_r1.txt file.
Please note that in this case, you can not check the correctness of the IP address, address ranges,
and so on, since the command output from network device is processed, not user input.
Task 15.1b
Check the get_ip_from_cfg function from task 15.1a on the config_r2.txt configuration.
Note that there are two IP addresses assigned on the e0/1 interface:
interface Ethernet0/1
ip address 10.255.2.2 255.255.255.0
ip address 10.254.2.2 255.255.255.0 secondary
And in the dictionary returned by the get_ip_from_cfg function, only one of them (first or second)
corresponds to the Ethernet0/1 interface.
Copy the get_ip_from_cfg function from 15.1a and redesign it to return a list of tuples for each
interface in the dictionary value. If only one address is assigned on the interface, there will be one
tuple in the list. If several IP addresses are configured on the interface, then the list will contain
several tuples. The interface name remains the key.
Check the function in the config_r2.txt configuration and make sure the Ethernet0/1 interface
matches a list of two tuples.
Please note that in this case, you can not check the correctness of the IP address, address ranges,
and so on, since the command output from network device is processed, not user input.
Task 15.2
Create a function parse_sh_ip_int_br that expects as an argument the name of the file containing
the output of the show ip int br command.
The function should process the output of the show ip int br command and return the following fields:
• Interface
• IP-Address
• Status
• Protocol
Check the operation of the function using the example of the sh_ip_int_br.txt file.
Task 15.3
Create a convert_ios_nat_to_asa function that converts NAT rules from cisco IOS syntax to cisco ASA.
• the name of the file containing the Cisco IOS NAT rules
• the name of the file in which to write the NAT rules for the ASA
In all rules for ASA, the interfaces will be the same (inside, outside).
Task 15.4
Create a get_ints_without_description function that expects as an argument the name of the file
containing the device configuration.
The function should process the configuration and return a list of interface names, which do not
have a description (description command).
interface Ethernet0/2
description To P_r9 Ethernet0/2
ip address 10.0.19.1 255.255.255.0
mpls traffic-eng tunnels
ip rsvp bandwidth
interface Loopback0
ip address 10.1.1.1 255.255.255.255
Check the operation of the function using the example of the config_r1.txt file.
Task 15.5
Create a generate_description_from_cdp function that expects as an argument the name of the file
that contains the output of the show cdp neighbors command.
The function should process the show cdp neighbors command output and generate a description
for the interfaces based on the command output.
For the Eth 0/0 interface, you need to generate the following description:
The function must return a dictionary, in which the keys are the names of the interfaces, and the
values are the command specifying the description of the interface:
• command output
So far, only the simplest option has been covered - writing information to a plain text file.
This section covers data reading and writing in CSV, JSON and YAML formats:
• CSV - a tabular format of data presentation. It can be obtained, for example, by exporting data
from a table or database. Similarly, data can be written in this format for further import into
the table.
• JSON - a format that is often used in API. In addition, this format will allow you to save data
structures such as dictionaries or lists in a structured format and then read them from a JSON
file and get the same data structures in Python.
• YAML format is often used to describe playbooks. For example, it is used in Ansible. In addition,
in this format it is convenient to write manually the parameters that should be read by scripts.
Note: Python allows objects of language itself to be written into files and read through Pickle
module, but this topic is not covered in this book.
339
Python for network engineers, Release 1.0
16. Unicode
Programs we write are not isolated. They download data from the Internet, read and write data on
disk, transmit data over the network.
So it’s very important to understand the difference between how a computer stores and transmits
data and how that data is perceived by a person. We take text, computer takes bytes.
• text - an immutable sequence of unicode characters. Type string (str) is used to store these
characters
Note: It is more correct to say that text is an immutable sequence of Unicode codes (codepoints).
Unicode standard
Unicode is a standard that describes the representation and encoding of almost all languages and
other characters.
• standard also defines the encoding - the way of representing the symbol code in bytes
Each character in Unicode has a specific code. This is a number that is usually written as follows:
U+0073, where 0073 - hexadecimal digits. Apart from the code, each symbol has its own unique
name. For example, letter “s” corresponds to code U+0073 and the name “LATIN SMALL LETTER S”.
• U+1F383, “JACK-O-LANTERN” -
Encodings
• UTF-8
• UTF-16
• UTF-32
One of the most popular encoding to date is UTF-8. This encoding uses a variable number of bytes
to write Unicode characters.
• H - 48
• i - 69
• - 01 f6 c0
• - 01 f6 80
• ☃ - 26 03
Unicode in Python 3
Python 3 has:
• strings - an immutable sequence of Unicode characters. Type string (str) is used to store these
characters
Strings
Examples of strings:
In [11]: hi = 'привет'
In [12]: hi
Out[12]: 'привет'
In [15]: type(hi)
Out[15]: str
In [14]: beautiful
Out[14]: 'schön'
Since strings are a sequence of Unicode codes you can write a string in different ways.
In [4]: "\u00F6"
Out[4]: 'ö'
In [21]: hi2
Out[21]: 'привет'
In [23]: len(hi2)
Out[23]: 6
In [6]: ord('ö')
Out[6]: 246
In [7]: chr(246)
Out[7]: 'ö'
Bytes
Bytes are denoted in the same way as strings but with addition of letter b before string:
In [30]: b1 = b'\xd0\xb4\xd0\xb0'
In [31]: b2 = b"\xd0\xb4\xd0\xb0"
In [32]: b3 = b'''\xd0\xb4\xd0\xb0'''
In [36]: type(b1)
Out[36]: bytes
In [37]: len(b1)
Out[37]: 4
In Python, bytes that correspond to ASCII symbols are displayed as these symbols, not as their
corresponding bytes. This may be a bit confusing but it is always possible to recognize bytes type
by letter b:
In [39]: bytes1
Out[39]: b'hello'
In [40]: len(bytes1)
Out[40]: 5
In [41]: bytes1.hex()
Out[41]: '68656c6c6f'
In [43]: bytes2
Out[43]: b'hello'
If you try to write not an ASCII character in a byte literal, an error will occur:
You can’t avoid working with bytes. For example, when working with a network or a filesystem, most
often the result is returned in bytes. Accordingly, you need to know how to convert bytes to string
and vice versa. That’s what the encoding is for.
• how to “encrypt” a string to bytes (str -> bytes). Encode method used (similar to encrypt)
• how to “decrypt” bytes to string (bytes -> str). Decode method used (similar to decrypt)
This analogy makes it clear that string-byte and byte-string transformations must use the same
encoding.
encode, decode
In [1]: hi = 'привет'
In [2]: hi.encode('utf-8')
Out[2]: b'\xd0\xbf\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82'
In [4]: hi_bytes
Out[4]: b'\xd0\xbf\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82'
In [5]: hi_bytes.decode('utf-8')
Out[5]: 'привет'
str.encode, bytes.decode
Method encode is also present in str class (as are other methods of working with strings):
In [6]: hi
Out[6]: 'привет'
In [8]: hi_bytes
Out[8]: b'\xd0\xbf\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82'
In these methods, encoding can be used as a key argument (examples above) or as a positional
argument:
In [10]: hi_bytes
Out[10]: b'\xd0\xbf\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82'
• bytes that the program reads must be converted to Unicode (string) as early as possible
Consider a few examples of working with bytes and converting bytes to string.
subprocess
In [3]: result.stdout
Out[3]: b'PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.\n64 bytes from 8.8.8.8:␣
,→icmp_seq=1 ttl=43 time=59.4 ms\n64 bytes from 8.8.8.8: icmp_seq=2 ttl=43␣
,→time=54.4 ms\n64 bytes from 8.8.8.8: icmp_seq=3 ttl=43 time=55.1 ms\n\n--- 8.8.
,→8.8 (continuesloss,␣
ping statistics ---\n3 packets transmitted, 3 received, 0% packet on next page)
If it is necessary to work with this output further you should immediately convert it to string:
In [5]: print(output)
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=43 time=59.4 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=43 time=54.4 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=43 time=55.1 ms
Module subprocess supports another conversion option - encoding parameter. If you specify it when
you call run() function, the result will be as a string:
In [7]: result.stdout
Out[7]: 'PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.\n64 bytes from 8.8.8.8:␣
,→icmp_seq=1 ttl=43 time=55.5 ms\n64 bytes from 8.8.8.8: icmp_seq=2 ttl=43␣
,→time=54.6 ms\n64 bytes from 8.8.8.8: icmp_seq=3 ttl=43 time=53.3 ms\n\n--- 8.8.
,→8.8 ping statistics ---\n3 packets transmitted, 3 received, 0% packet loss,␣
,→time 2003ms\nrtt min/avg/max/mdev = 53.368/54.534/55.564/0.941 ms\n'
In [8]: print(result.stdout)
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=43 time=55.5 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=43 time=54.6 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=43 time=53.3 ms
telnetlib
Depending on module, conversion between strings and bytes can be performed automatically or
may be required explicitly.
For example, telnetlib module must pass bytes to read_until and write methods:
import telnetlib
import time
t = telnetlib.Telnet('192.168.100.1')
t.read_until(b'Username:')
t.write(b'cisco\n')
t.read_until(b'Password:')
t.write(b'cisco\n')
t.write(b'sh ip int br\n')
time.sleep(5)
output = t.read_very_eager().decode('utf-8')
print(output)
pexpect
In [11]: output
Out[11]: b'total 8\r\n4 drwxr-xr-x 2 vagrant vagrant 4096 Aug 28 12:16 concurrent_
,→futures\r\n4 drwxr-xr-x 2 vagrant vagrant 4096 Aug 3 07:59 iterator_
,→generator\r\n'
In [12]: output.decode('utf-8')
Out[12]: 'total 8\r\n4 drwxr-xr-x 2 vagrant vagrant 4096 Aug 28 12:16 concurrent_
,→futures\r\n4 drwxr-xr-x 2 vagrant vagrant 4096 Aug 3 07:59 iterator_
,→generator\r\n'
In [14]: output
Out[14]: 'total 8\r\n4 drwxr-xr-x 2 vagrant vagrant 4096 Aug 28 12:16 concurrent_
,→futures\r\n4 drwxr-xr-x 2 vagrant vagrant 4096 Aug 3 07:59 iterator_
,→generator\r\n'
Until now, when working with files, the following expression was used:
with open(filename) as f:
for line in f:
print(line)
But actually, when you read a file you convert bytes to a string. And default encoding was used:
In [2]: locale.getpreferredencoding()
Out[2]: 'UTF-8'
In [2]: f = open('r1.txt')
In [3]: f
Out[3]: <_io.TextIOWrapper name='r1.txt' mode='r' encoding='UTF-8'>
When working with files it is better to specify encoding explicitly because it may differ in different
operating systems:
!
ip ssh version 2
!
Conclusion
These examples are shown here to show that different modules can treat the issue of conversion
between strings and bytes differently. And different functions and methods of these modules can
expect arguments and return values of different types. However, all of these items are in documen-
tation.
Converting errors
When converting between strings and bytes it is very important to know exactly which encoding is
used as well as to know the possibilities of different encodings.
In [33]: hi_unicode.encode('ascii')
---------------------------------------------------------------------------
UnicodeEncodeError Traceback (most recent call last)
<ipython-input-33-ec69c9fd2dae> in <module>()
----> 1 hi_unicode.encode('ascii')
Similarly, if the string “привет” is converted to bytes and you try to convert it into a string with
ascii, we will also get an error:
In [36]: hi_bytes.decode('ascii')
---------------------------------------------------------------------------
UnicodeDecodeError Traceback (most recent call last)
<ipython-input-36-aa0ada5e44e9> in <module>()
----> 1 hi_bytes.decode('ascii')
(continues on next page)
In [39]: utf_16.decode('utf-8')
---------------------------------------------------------------------------
UnicodeDecodeError Traceback (most recent call last)
<ipython-input-39-4b4c731e69e4> in <module>()
----> 1 utf_16.decode('utf-8')
Having mistakes is good. They’re telling what the problem is. It’s worse when it’s like this:
In [42]: hi_bytes
Out[42]: b'\xd0\xbf\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82'
In [43]: hi_bytes.decode('utf-16')
Out[43]: ' '
Error processing
Encode and decode methods have error-processing modes that indicate how to respond to a con-
version error.
By default encode uses strict mode - UnicodeError exception is generated when encoding errors
occur. Examples of such behaviour are above.
Instead, you can use replace to substitute character with a question mark:
The decode method also uses strict mode by default and generates a UnicodeDecodeError exception.
In [52]: de_hi_utf8
Out[52]: b'gr\xc3\xbcezi'
Further reading
Python documentation:
• What’s New In Python 3: Text Vs. Data Instead Of Unicode Vs. 8-bit
• Unicode HOWTO
Articles:
• Section «Strings» of the book “Dive Into Python 3” - very well written about Unicode, encod-
ings and how all this works in Python
• The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Uni-
code and Character Sets (No Excuses!)
• Unicode (Wikipedia)
• UTF-8 (Wikipedia)
Data serialization is about storing data in some format that is often structured.
• database
In addition, Python allows you to write down objects of language itself (this aspect is not covered,
but if you are interested, look at the Pickle module).
This section covers CSV, JSON, YAML formats and chapter 25 covers databases.
• you may have data about IP address and similar information to process in tables
• software can return data in JSON format. Accordingly, by converting this data into a Python
object you can work with it and do whatever you want
– for example, it can be settings for different objects (IP addresses, VLANs, etc.)
For each of these formats, Python has a module that makes them easier to work with.
CSV (comma-separated value) - a tabular data format (for example, it may be data from a table
or data from a database).
In this format, each line of a file is a line of a table. Despite format name the separator can be not
only a comma. Formats with a different separator may have their own name, for example, TSV (tab
separated values), however, the name CSV usually means any separators).
hostname,vendor,model,location
sw1,Cisco,3750,London
sw2,Cisco,3850,Liverpool
sw3,Cisco,3650,Liverpool
sw4,Cisco,3650,London
The standard Python library has a csv module that allows working with files in CSV format.
Reading
import csv
with open('sw_data.csv') as f:
reader = csv.reader(f)
for row in reader:
print(row)
$ python csv_read.py
['hostname', 'vendor', 'model', 'location']
['sw1', 'Cisco', '3750', 'London']
['sw2', 'Cisco', '3850', 'Liverpool']
['sw3', 'Cisco', '3650', 'Liverpool']
['sw4', 'Cisco', '3650', 'London']
First list contains column names and remaining list contains the corresponding values.
Most often column headers are more convenient to get by a separate object. This can be done in
this way (csv_read_headers.py file):
import csv
with open('sw_data.csv') as f:
reader = csv.reader(f)
headers = next(reader)
print('Headers: ', headers)
for row in reader:
print(row)
Sometimes it is more convenient to get dictionaries in which keys are column names and values are
column values.
import csv
with open('sw_data.csv') as f:
reader = csv.DictReader(f)
for row in reader:
print(row)
print(row['hostname'], row['model'])
$ python csv_read_dict.py
{'hostname': 'sw1', 'vendor': 'Cisco', 'model': '3750', 'location': 'London,␣
,→Globe Str 1 '}
sw1 3750
{'hostname': 'sw2', 'vendor': 'Cisco', 'model': '3850', 'location': 'Liverpool'}
sw2 3850
{'hostname': 'sw3', 'vendor': 'Cisco', 'model': '3650', 'location': 'Liverpool'}
sw3 3650
{'hostname': 'sw4', 'vendor': 'Cisco', 'model': '3650', 'location': 'London,␣
,→Grobe Str 1'}
sw4 3650
Note: Prior to Python 3.8 OrderedDict type was returned, not dict.
Writing
Similarly, a csv module can be used to write data to file in CSV format (csv_write.py file):
import csv
with open('sw_data_new.csv') as f:
print(f.read())
In example above, strings from list are written to the file and then the content of file is displayed on
standard output stream.
$ python csv_write.py
hostname,vendor,model,location
sw1,Cisco,3750,"London, Best str"
sw2,Cisco,3850,"Liverpool, Better str"
sw3,Cisco,3650,"Liverpool, Better str"
sw4,Cisco,3650,"London, Best str"
Note the interesting thing: strings in the last column are quoted and other values are not.
This is because all strings in the last column have a comma. And quotes indicate what is an entire
string. When a comma is inside quotation marks the csv module does not perceive it as a separator.
Sometimes it’s better to have all strings quoted. Of course, in this case, example is simple enough
but when there are more values in the strings, the quotes indicate where value begins and ends.
Csv module allows you to control this. For all strings to be written in a CSV file with quotes you
should change script this way (csv_write_quoting.py file):
import csv
with open('sw_data_new.csv') as f:
print(f.read())
$ python csv_write_quoting.py
"hostname","vendor","model","location"
"sw1","Cisco","3750","London, Best str"
"sw2","Cisco","3850","Liverpool, Better str"
"sw3","Cisco","3650","Liverpool, Better str"
"sw4","Cisco","3650","London, Best str"
Now all values are quoted. And because model number is given as a string in original list, it is quoted
here as well.
Besides writerow method, writerows method is supported. It accepts any iterable object.
import csv
with open('sw_data_new.csv') as f:
print(f.read())
DictWriter
In general, DictWriter works as writer but since dictionaries are not ordered it is necessary to
specify the order of columns in file. The fieldnames option is used for this purpose (csv_write_dict.py
file):
import csv
data = [{
'hostname': 'sw1',
'location': 'London',
'model': '3750',
'vendor': 'Cisco'
}, {
'hostname': 'sw2',
'location': 'Liverpool',
'model': '3850',
'vendor': 'Cisco'
}, {
'hostname': 'sw3',
'location': 'Liverpool',
'model': '3650',
'vendor': 'Cisco'
}, {
'hostname': 'sw4',
'location': 'London',
'model': '3650',
'vendor': 'Cisco'
}]
Delimiter
Sometimes other values are used as a separator. In this case, it should be possible to tell module
which separator to use.
hostname;vendor;model;location
sw1;Cisco;3750;London
sw2;Cisco;3850;Liverpool
sw3;Cisco;3650;Liverpool
sw4;Cisco;3650;London
import csv
with open('sw_data2.csv') as f:
reader = csv.reader(f, delimiter=';')
for row in reader:
print(row)
JSON (JavaScript Object Notation) - a text format for data storage and exchange.
As for CSV, Python has a module that allows easy writing and reading of data in JSON format.
Reading
File sw_templates.json:
{
"access": [
"switchport mode access",
"switchport access vlan",
"switchport nonegotiate",
"spanning-tree portfast",
"spanning-tree bpduguard enable"
],
"trunk": [
"switchport trunk encapsulation dot1q",
"switchport mode trunk",
"switchport trunk native vlan 999",
"switchport trunk allowed vlan"
]
}
• json.loads - method reads string in JSON format and returns Python objects
json.load
import json
with open('sw_templates.json') as f:
templates = json.load(f)
print(templates)
$ python json_read_load.py
{'access': ['switchport mode access', 'switchport access vlan', 'switchport␣
,→nonegotiate', 'spanning-tree portfast', 'spanning-tree bpduguard enable'],
,→'trunk': ['switchport trunk encapsulation dot1q', 'switchport mode trunk',
,→'switchport trunk native vlan 999', 'switchport trunk allowed vlan']}
access
switchport mode access
switchport access vlan
switchport nonegotiate
spanning-tree portfast
spanning-tree bpduguard enable
trunk
switchport trunk encapsulation dot1q
switchport mode trunk
switchport trunk native vlan 999
switchport trunk allowed vlan
json.loads
import json
with open('sw_templates.json') as f:
file_content = f.read()
templates = json.loads(file_content)
print(templates)
Writing
There are also two methods for writing information in JSON format in json module:
json.dumps()
import json
trunk_template = [
'switchport trunk encapsulation dot1q', 'switchport mode trunk',
'switchport trunk native vlan 999', 'switchport trunk allowed vlan'
]
access_template = [
'switchport mode access', 'switchport access vlan',
'switchport nonegotiate', 'spanning-tree portfast',
'spanning-tree bpduguard enable'
]
f.write(json.dumps(to_json))
with open('sw_templates.json') as f:
print(f.read())
Method json.dumps is suitable for situations where you want to return a string in JSON format. For
example, to pass it to the API.
json.dump
import json
trunk_template = [
'switchport trunk encapsulation dot1q', 'switchport mode trunk',
'switchport trunk native vlan 999', 'switchport trunk allowed vlan'
]
access_template = [
'switchport mode access', 'switchport access vlan',
'switchport nonegotiate', 'spanning-tree portfast',
'spanning-tree bpduguard enable'
]
with open('sw_templates.json') as f:
print(f.read())
When you want to write information in JSON format into a file, it is better to use dump method.
Methods dump and dumps can pass additional parameters to manage the output format.
By default, these methods write information in a compact view. As a rule, when data is used by other
programs, visual presentation of data is not important. If data in file needs to be read by person,
this format is not very convenient to perceive. Fortunately, json module allows you to manage such
things.
By passing additional parameters to dump method (or dumps method) you can get a more readable
output (json_write_indent.py file):
import json
trunk_template = [
'switchport trunk encapsulation dot1q', 'switchport mode trunk',
'switchport trunk native vlan 999', 'switchport trunk allowed vlan'
]
access_template = [
'switchport mode access', 'switchport access vlan',
'switchport nonegotiate', 'spanning-tree portfast',
'spanning-tree bpduguard enable'
]
with open('sw_templates.json') as f:
print(f.read())
{
"access": [
"switchport mode access",
"switchport access vlan",
"switchport nonegotiate",
"spanning-tree portfast",
"spanning-tree bpduguard enable"
],
"trunk": [
"switchport trunk encapsulation dot1q",
"switchport mode trunk",
"switchport trunk native vlan 999",
"switchport trunk allowed vlan"
]
}
Another important aspect of data conversion to JSON format is that data will not always be the same
type as source data in Python.
In [3]: print(type(trunk_template))
<class 'tuple'>
In [7]: type(templates)
Out[7]: list
In [8]: print(templates)
['switchport trunk encapsulation dot1q', 'switchport mode trunk', 'switchport␣
,→trunk native vlan 999', 'switchport trunk allowed vlan']
This is because JSON uses different data types and does not have matches for all Python data types.
Python JSON
dict object
list, tuple array
str string
int, float number
True true
False false
None null
JSON Python
object dict
array list
string str
number (int) int
number (real) float
true True
false False
null None
It’s not possible to write a dictionary in JSON format if it has tuples as a keys.
Beside that, dictionary keys can only be strings in JSON. But if numbers are used in Python dictionary
there will be no error. But conversion from numbers to strings will take place:
In [29]: json.dumps(d)
Out[29]: '{"1": 100, "2": 200}'
YAML (YAML Ain’t Markup Language) - another text format for writing data.
YAML is more human-friendly than JSON, so it is often used to describe actions in software. Playbooks
in Ansible, for example.
YAML syntax
Like Python, YAML uses indents to specify the structure of document. But YAML can only use spaces
and cannot use tabs. Another similarity with Python is that comments start with # and continue until
the end of line.
List
When a list is written in such a block, each row must start with ‘‘- ‘‘ (minus and space) and all lines
in the list must be at the same indentation level.
Dictionary
Or a block:
vlan: 100
name: IT
Strings
Strings in YAML don’t have to be quoted. This is convenient, but sometimes quotes should be used.
For example, when a special character (special for YAML) is used in a string.
Combination of elements
A dictionary with two keys: access and trunk. Values that correspond to these keys - command lists:
access:
- switchport mode access
- switchport access vlan
- switchport nonegotiate
- spanning-tree portfast
- spanning-tree bpduguard enable
trunk:
- switchport trunk encapsulation dot1q
- switchport mode trunk
- switchport trunk native vlan 999
- switchport trunk allowed vlan
List of dictionaries:
- BS: 1550
IT: 791
id: 11
name: Liverpool
(continues on next page)
to_id: 1
to_name: LONDON
- BS: 1510
IT: 793
id: 12
name: Bristol
to_id: 1
to_name: LONDON
- BS: 1650
IT: 892
id: 14
name: Coventry
to_id: 2
to_name: Manchester
PyYAML module
Python uses a PyYAML module to work with YAML. It is not part of the standard module library, so it
needs to be installed:
- BS: 1550
IT: 791
id: 11
name: Liverpool
to_id: 1
to_name: LONDON
- BS: 1510
IT: 793
id: 12
name: Bristol
to_id: 1
to_name: LONDON
- BS: 1650
(continues on next page)
IT: 892
id: 14
name: Coventry
to_id: 2
to_name: Manchester
import yaml
from pprint import pprint
with open('info.yaml') as f:
templates = yaml.safe_load(f)
pprint(templates)
$ python yaml_read.py
[{'BS': 1550,
'IT': 791,
'id': 11,
'name': 'Liverpool',
'to_id': 1,
'to_name': 'LONDON'},
{'BS': 1510,
'IT': 793,
'id': 12,
'name': 'Bristol',
'to_id': 1,
'to_name': 'LONDON'},
{'BS': 1650,
'IT': 892,
'id': 14,
'name': 'Coventry',
'to_id': 2,
'to_name': 'Manchester'}]
YAML format is very convenient for storing different parameters, especially if they are filled manually.
Writing to YAML
import yaml
trunk_template = [
'switchport trunk encapsulation dot1q', 'switchport mode trunk',
'switchport trunk native vlan 999', 'switchport trunk allowed vlan'
]
access_template = [
'switchport mode access', 'switchport access vlan',
'switchport nonegotiate', 'spanning-tree portfast',
'spanning-tree bpduguard enable'
]
with open('sw_templates.yaml') as f:
print(f.read())
File sw_templates.yaml:
access:
- switchport mode access
- switchport access vlan
- switchport nonegotiate
- spanning-tree portfast
- spanning-tree bpduguard enable
trunk:
- switchport trunk encapsulation dot1q
- switchport mode trunk
- switchport trunk native vlan 999
- switchport trunk allowed vlan
Further reading
In this section only basic read and write operations were covered with no additional parameters.
More details can be found in the module documentation.
• CSV
• JSON
• YAML
In addition, PyMOTW has very good description of all Python modules that are part of the standard
library (installed with Python):
• CSV
• JSON
Tasks
Warning: Starting from section “4. Data types in Python” there are automated tests for testing
tasks. They help to check whether everything matches the task, and also give feedback on what
does not correspond to the task. As a rule, after the first period of adaptation to tests, it becomes
easier to do tasks with tests. Testing is done using the pyneng utility. Learn more about how to
work with the pyneng utility.
Task 17.1
Create the write_dhcp_snooping_to_csv function, which processes the output of the show dhcp
snooping binding command from different files and writes the processed data to the csv file.
Function arguments:
• filenames - list of filenames with “show dhcp snooping binding” command output
• output - the name of the csv file into which the result will be written
For example, if a list with one file sw3_dhcp_snooping.txt was passed as an argument:
switch,mac,ip,vlan,interface
sw3,00:E9:BC:3F:A6:50,100.1.1.6,3,FastEthernet0/20
sw3,00:E9:22:11:A6:50,100.1.1.7,3,FastEthernet0/21
The first column in the csv file, the name of the switch, must be obtained from the file name, the
rest - from the contents in the files.
Task 17.2
• take the contents of several files with the output of the sh version command
• parse command output using regular expressions and get device information
parse_sh_version function:
• expects the output of the sh version command as an argument in single string (not a filename)
– ios - “12.4(5)T”
– image - “flash:c2800-advipservicesk9-mz.124-5.T.bin”
write_inventory_to_csv function writes the contents to a file, in CSV format and returns nothing.
• process information from each file with sh version output: sh_version_r1.txt, sh_version_r2.txt,
sh_version_r3.txt
• using the parse_sh_version function, ios, image, uptime information should be obtained from
each output
The routers_inventory.csv file should have the following columns (in this order): hostname, ios,
image, uptime
The code below has created a list of files using the glob module. You can uncomment the
print(sh_version_files) line to see the content of the list.
In addition, a list of headers has been created, which should be written to CSV.
import glob
sh_version_files = glob.glob("sh_vers*")
#print(sh_version_files)
Task 17.3
Create a function parse_sh_cdp_neighbors that processes the output of the show cdp neighbors
command.
The function expects, as an argument, the output of the command as a single string (not a filename).
The function should return a dictionary that describes the connections between devices.
Interfaces must be written with a space. That is, so Fa 0/0, and not so Fa0/0.
Task 17.3a
Create a generate_topology_from_cdp function that processes the show cdp neighbor command
output from multiple files and writes the resulting topology to a single dictionary.
• list_of_files - list of files from which to read the output of the sh cdp neighbor command
• save_to_filename is the name of the YAML file where the topology will be saved.
The function should return a dictionary that describes the connections between devices, regardless
of whether the topology is saved to a file.
Dictionary example:
Interfaces must be written with a space. That is, so Fa 0/0, and not so Fa0/0.
• sh_cdp_n_sw1.txt
• sh_cdp_n_r1.txt
• sh_cdp_n_r2.txt
• sh_cdp_n_r3.txt
• sh_cdp_n_r4.txt
• sh_cdp_n_r5.txt
• sh_cdp_n_r6.txt
Check the operation of the save_to_filename parameter and write the resulting dictionary to the
topology.yaml file. You will need it in the next task.
Task 17.3b
Create a transform_topology function that converts the topology to a format suitable for the
draw_topology function.
The function expects a YAML filename as an argument in which the topology is stored.
The function must read data from the YAML file, transform it accordingly, so that the function returns
a dictionary of the following form:
The transform_topology function should not only change the format of the topology representa-
tion, but also remove the “duplicate” connections (they are best seen in the diagram that the
draw_topology function generates from the draw_network_graph.py file). “Duplicate” connections
are connections of this kind:
Due to the fact that the same link is described twice, there will be extra connections on the diagram.
The task is to leave only one of these links in the final dictionary, does not matter which one.
Check the operation of the function on the topology.yaml file (must be created in task 17.3a). Based
on the resulting dictionary, you need to generate a topology image using the draw_topology function.
Do not copy draw_topology function code from draw_network_graph.py file.
The result should look the same as the diagram in the task_17_3b_topology.svg file:
Note: To complete this task, graphviz must be installed: apt-get install graphviz
Task 17.4
Function arguments:
• source_log - the name of the csv file from which the data is read (mail_log.csv)
• output - the name of the csv file into which the result will be written
The write_last_log_to_csv function processes the csv file mail_log.csv. The mail_log.csv file contains
the logs of the username change. User cannot change email, only username.
The write_last_log_to_csv function should select from the mail_log.csv file only the most recent en-
tries for each user and write them to another csv file. In the output file, the first line should be the
column headers as in the source_log file.
For some users, there is only one record, and then it is necessary to write to the final file only her.
For some users, there are multiple entries with different names. For example, a user with email
[email protected] changed his username several times:
C=3PO,[email protected],16/12/2019 17:10
C3PO,[email protected],16/12/2019 17:15
C-3PO,[email protected],16/12/2019 17:24
Of these three records, only one should be written to the final file - the most recent:
C-3PO,[email protected],16/12/2019 17:24
It is convenient to use datetime objects from the datetime module for comparing dates. To make
it easier to work with dates, the convert_str_to_datetime function has been created - it converts a
date string in the format 11/10/2019 14:05 into a datetime object. The resulting datetime objects
can be compared with each other. The second function, convert_datetime_to_str, does the opposite
— it turns a datetime object into a string.
import datetime
def convert_str_to_datetime(datetime_str):
"""
Converts a date string formatted as 11/10/2019 14:05 to a datetime object.
"""
return datetime.datetime.strptime(datetime_str, "%d/%m/%Y %H:%M")
def convert_datetime_to_str(datetime_obj):
"""
Converts a datetime object to a date string in the format 11/10/2019 14:05.
"""
return datetime.datetime.strftime(datetime_obj, "%d/%m/%Y %H:%M")
379
Python for network engineers, Release 1.0
• SSH
• Telnet
Python has several modules that allow you to connect to network devices and execute commands:
– this module allows working with any interactive session: ssh, telnet, sftp, etc.
– in addition, it makes possible to execute different commands in OS (this can also be done
with other modules)
– while pexpect may be less user-friendly than other modules, it implements a more general
functionality and allows it to be used in situations where other modules do not work
– netmiko version >= 1.0 also has Telnet support, so if netmiko supports the network de-
vices you use, it is more convenient to use it
– it is more convenient to use than pexpect but with narrower functionality (only supports
SSH)
• netmiko - module that simplifies the use of paramiko for network devices
• scrapli - is a module that allows you to connect to network equipment using Telnet, SSH or
NETCONF
This section covers all five modules and describes how to connect to several devices in parallel.
Three routers are used in section examples. There are no requirements for them, only configured
SSHv2 and Telnet.
• user: cisco
• password: cisco
Password input
• Request password at start of the script and read user input. Disadvantage is that you can see
which characters user is typing
As a rule, the same user uses the same login and password to connect to devices. And usually it’s
enough to request login and password at the start of the script and then use them to connect to
different devices.
Unfortunately, if you use input the typed password will be visible. But it is better if no characters
are displayed when entering a password.
Module getpass
Module getpass allows you to request a password without displaying input characters:
In [3]: print(password)
testpass
Environment variables
$ export SSH_USER=user
$ export SSH_PASSWORD=userpass
import os
USERNAME = os.environ.get('SSH_USER')
PASSWORD = os.environ.get('SSH_PASSWORD')
Module pexpect
• telnet
• ssh
• ftp
At the same time, pexpect does not implement utilities but uses ready-made ones.
pexpect.spawn
Class spawn allows you to interact with called program by sending data and waiting for a response.
After executing this line, connection is established. Now you must specify which line to expect. In
this case, wait for password request:
In [6]: ssh.expect('[Pp]assword')
Out[6]: 0
Note how line that pexpect expects is written as [Pp]assword. This is a regex that describes a
password or Password string. That is, expect method can be used to pass a regex as an argument.
Method expect returned number 0 as a result of the work. This number indicates that a match has
been found and that this element with index zero. Index appears here because you can pass a list
of strings. For example, you can pass a list with two elements:
Note that it now returns 1. This means that Password word matched.
In [9]: ssh.sendline('cisco')
Out[9]: 6
Method sendline sends a string, automatically adds a new line character to it based on the value
of os.linesep and then returns a number indicating how many bytes were written.
Note: Pexpect has several methods for sending commands, not just sendline.
In [10]: ssh.expect('[>#]')
Out[10]: 0
In [11]: ssh.sendline('enable')
Out[11]: 7
In [12]: ssh.expect('[Pp]assword')
Out[12]: 0
In [13]: ssh.sendline('cisco')
Out[13]: 6
In [14]: ssh.expect('[>#]')
Out[14]: 0
After sending the command, pexpect must be told until what point to read the output. We specify
that it should read untill #:
In [16]: ssh.expect('#')
Out[16]: 0
In [17]: ssh.before
Out[17]: b'sh ip int br\r\nInterface IP-Address OK? Method␣
,→Status Protocol\r\nEthernet0/0 192.168.100.1 ␣
,→YES NVRAM up up \r\nEthernet0/1 192.168.
,→200.1 YES NVRAM up up \r\nEthernet0/2 ␣
,→19.1.1.1 YES NVRAM up up \r\nEthernet0/3 ␣
,→ 192.168.230.1 YES NVRAM up up \r\nEthernet0/
,→3.100 10.100.0.1 YES NVRAM up up ␣
,→\r\nEthernet0/3.200 10.200.0.1 YES NVRAM up ␣
,→up \r\nEthernet0/3.300 10.30.0.1 YES NVRAM up ␣
,→ up \r\nR1'
Since the result is displayed as a sequence of bytes you should convert it to a string:
In [19]: print(show_output)
sh ip int br
Interface IP-Address OK? Method Status ␣
,→Protocol
In [20]: ssh.close()
For example, in order make command ls -ls | grep SUMMARY work, shell must be run as follows:
In [3]: p.expect(pexpect.EOF)
Out[3]: 0
In [4]: print(p.before)
b'4 -rw-r--r-- 1 vagrant vagrant 3203 Jul 14 07:15 1_pexpect.py\r\n'
In [5]: print(p.before.decode('utf-8'))
4 -rw-r--r-- 1 vagrant vagrant 3203 Jul 14 07:15 1_pexpect.py
pexpect.EOF
This is a special value that allows you to react to the end of a command or session that has been
run in spawn.
When calling ls -ls command, pexpect does not receive an interactive session. Command is simply
executed and that ends its work.
Therefore, if you run this command and set prompt in expect, there is an error:
In [6]: p.expect('nattaur')
---------------------------------------------------------------------------
EOF Traceback (most recent call last)
<ipython-input-9-9c71777698c2> in <module>()
----> 1 p.expect('nattaur')
...
Method pexpect.expect
• regex
• compiled regex
Another very useful feature of pexpect.expect is that you can pass not a single value, but a list.
For example:
• when pexpect.expect is called with a list, you can specify different expected strings
– in this case number 2 because EOF exception is number two in the list
• with this format you can make branches in the program depending on the element which had
a match
Example of using pexpect when connecting to equipment and passing show command (file 1_pex-
pect.py):
import pexpect
import re
from pprint import pprint
ssh.expect("[Pp]assword")
ssh.sendline(password)
enable_status = ssh.expect([">", "#"])
if enable_status == 0:
ssh.sendline("enable")
ssh.expect("[Pp]assword")
ssh.sendline(enable)
ssh.expect(prompt)
(continues on next page)
result = {}
for command in commands:
ssh.sendline(command)
match = ssh.expect([prompt, pexpect.TIMEOUT, pexpect.EOF])
if match == 1:
print(
f"Symbol {prompt} is not found in output. Resulting output is␣
,→written to
dictionary")
if match == 2:
print("Connection was terminated by server")
return result
else:
output = ssh.before
result[command] = output.replace("\r\n", "\n")
return result
if __name__ == "__main__":
devices = ["192.168.100.1", "192.168.100.2", "192.168.100.3"]
commands = ["sh clock", "sh int desc"]
for ip in devices:
result = send_show_command(ip, "cisco", "cisco", "cisco", commands)
pprint(result, width=120)
If ssh.expect([">", "#"]) does not return index 0, it means that connection was not switched
to enable mode automaticaly and it should be done separately. If index 1 is returned, then we are
already in enable mode, for example, because device is configured with privilege 15.
Here commands are sent in turn and expect waits for three options: prompt, timeout or EOF. If
expect method didn’t catch #, value 1 will be returned and in this case a message is displayed, that
symbol was not found. But in both cases, when a match is found or timeout the resulting output is
written to dictionary. Thus, you can see what was received from device, even if prompt is not found.
'Lo33 up up \n'
'Lo100 up up \n'}
{'sh clock': 'sh clock\n*13:13:53.360 UTC Sun Jul 19 2020\n',
'sh int desc': 'sh int desc\n'
'Interface Status Protocol Description\n'
'Et0/0 up up \n'
'Et0/1 up up \n'
'Et0/2 admin down down \n'
'Et0/3 admin down down \n'
'Lo33 up up \n'}
Sometimes the output of a command is very large and cannot be read completely or device is not
makes it possible to disable pagination. In this case, a slightly different approach is needed.
Note: The same task will be repeated for other modules in this section.
Example of using pexpect to work with paginated output of show command (1_pexpect_more.py
file):
import pexpect
import re
from pprint import pprint
ssh.expect("[Pp]assword")
ssh.sendline(password)
enable_status = ssh.expect([">", "#"])
if enable_status == 0:
ssh.sendline("enable")
ssh.expect("[Pp]assword")
ssh.sendline(enable)
ssh.expect(prompt)
ssh.sendline(command)
output = ""
while True:
match = ssh.expect([prompt, "--More--", pexpect.TIMEOUT])
page = ssh.before.replace("\r\n", "\n")
page = re.sub(" +\x08+ +\x08+", "\n", page)
output += page
if match == 0:
break
elif match == 1:
ssh.send(" ")
else:
print("Error: timeout")
break
output = re.sub("\n +\n", "\n", output)
return output
if __name__ == "__main__":
devices = ["192.168.100.1", "192.168.100.2", "192.168.100.3"]
for ip in devices:
result = send_show_command(ip, "cisco", "cisco", "cisco", "sh run")
with open(f"{ip}_result.txt", "w") as f:
f.write(result)
Now after sending the command, expect method waits for another option --More-- - sign, that
there will be one more page further. Since it’s not known in advance how many pages will be in
the output, reading is performed in a loop while True. Loop is interrupted if prompt is met # or no
prompt appears within 10 seconds or --More--.
If --More-- is met, pages are not over yet and you have to scroll through the next one. In Cisco,
you need to press space bar to do this (without new line). Therefore, send method is used here, not
sendline - sendline automatically adds a new line character.
This string page = re.sub(" +\x08+ +\x08+", "\n", page) removes backspace symbols which
are around --More-- so they don’t end up in the final output.
Module telnetlib
Module telnetlib is part of standard Python library. This is telnet client implementation.
Note: It is also possible to connect via telnet using pexpect. The advantage of telnetlib is that this
module is part of standard Python library.
Telnetlib resembles pexpect but has several differences. The most notable difference is that telnetlib
requires a pass of a byte string, rather than normal one.
Method read_until
Method read_until specifies till which line the output should be read. However, as an argument, it
is necessary to pass bytes, not the usual string:
In [2]: telnet.read_until(b'Username')
Out[2]: b'\r\n\r\nUser Access Verification\r\n\r\nUsername'
Method write
The write method is used to transmit data. You must pass a byte string as an argument:
In [3]: telnet.write(b'cisco\n')
In [4]: telnet.read_until(b'Password')
Out[4]: b': cisco\r\nPassword'
In [5]: telnet.write(b'cisco\n')
You can now specify what should be read untill prompt and then send the command:
In [6]: telnet.read_until(b'>')
Out[6]: b': \r\nR1>'
In [8]: telnet.read_until(b'>')
Out[8]: b'sh ip int br\r\nInterface IP-Address OK? Method␣
,→Status Protocol\r\nEthernet0/0 192.168.100.1 ␣
,→YES NVRAM up up \r\nEthernet0/1 192.168.
,→200.1 YES NVRAM up up \r\nEthernet0/2 ␣
,→19.1.1.1 YES NVRAM up up \r\nEthernet0/3 ␣
(continues on next page)
,→ 192.168.230.1 YES NVRAM up up \r\nEthernet0/
,→3.100 10.100.0.1 YES NVRAM up up ␣
18. Connection to network devices 391
,→\r\nEthernet0/3.200 10.200.0.1 YES NVRAM up ␣
,→up \r\nEthernet0/3.300 10.30.0.1 YES NVRAM up ␣
,→ up \r\nR1>'
Python for network engineers, Release 1.0
Method read_very_eager
Or use another read method read_very_eager. When using read_very_eager method, you can
send multiple commands and then read all available output:
In [13]: print(all_result)
sh arp
Protocol Address Age (min) Hardware Addr Type Interface
Internet 10.30.0.1 - aabb.cc00.6530 ARPA Ethernet0/3.300
Internet 10.100.0.1 - aabb.cc00.6530 ARPA Ethernet0/3.100
Internet 10.200.0.1 - aabb.cc00.6530 ARPA Ethernet0/3.200
Internet 19.1.1.1 - aabb.cc00.6520 ARPA Ethernet0/2
Internet 192.168.100.1 - aabb.cc00.6500 ARPA Ethernet0/0
Internet 192.168.100.2 124 aabb.cc00.6600 ARPA Ethernet0/0
Internet 192.168.100.3 143 aabb.cc00.6700 ARPA Ethernet0/0
Internet 192.168.100.100 160 aabb.cc80.c900 ARPA Ethernet0/0
Internet 192.168.200.1 - 0203.e800.6510 ARPA Ethernet0/1
Internet 192.168.200.100 13 0800.27ac.16db ARPA Ethernet0/1
Internet 192.168.230.1 - aabb.cc00.6530 ARPA Ethernet0/3
R1>sh clock
*19:18:57.980 UTC Fri Nov 3 2017
R1>sh ip int br
Interface IP-Address OK? Method Status ␣
,→Protocol
With read_until will be a slightly different approach. You can execute the same three commands,
but then get the output one by one because of reading till prompt string:
In [17]: telnet.read_until(b'>')
Out[17]: b'sh arp\r\nProtocol Address Age (min) Hardware Addr Type ␣
,→Interface\r\nInternet 10.30.0.1 - aabb.cc00.6530 ARPA ␣
,→Ethernet0/3.300\r\nInternet 10.100.0.1 - aabb.cc00.6530 ARPA ␣
,→Ethernet0/3.100\r\nInternet 10.200.0.1 - aabb.cc00.6530 ARPA ␣
,→Ethernet0/3.200\r\nInternet 19.1.1.1 - aabb.cc00.6520 ARPA ␣
,→Ethernet0/2\r\nInternet 192.168.100.1 - aabb.cc00.6500 ARPA ␣
,→Ethernet0/0\r\nInternet 192.168.100.2 126 aabb.cc00.6600 ARPA ␣
,→Ethernet0/0\r\nInternet 192.168.100.3 145 aabb.cc00.6700 ARPA ␣
,→Ethernet0/0\r\nInternet 192.168.100.100 162 aabb.cc80.c900 ARPA ␣
,→Ethernet0/0\r\nInternet 192.168.200.1 - 0203.e800.6510 ARPA ␣
,→Ethernet0/1\r\nInternet 192.168.200.100 15 0800.27ac.16db ARPA ␣
,→Ethernet0/1\r\nInternet 192.168.230.1 - aabb.cc00.6530 ARPA ␣
,→Ethernet0/3\r\nR1>'
In [18]: telnet.read_until(b'>')
Out[18]: b'sh clock\r\n*19:20:39.388 UTC Fri Nov 3 2017\r\nR1>'
In [19]: telnet.read_until(b'>')
Out[19]: b'sh ip int br\r\nInterface IP-Address OK? Method␣
,→Status Protocol\r\nEthernet0/0 192.168.100.1 ␣
,→YES NVRAM up up \r\nEthernet0/1 192.168.
,→200.1 YES NVRAM up up \r\nEthernet0/2 ␣
,→19.1.1.1 YES NVRAM up up \r\nEthernet0/3 ␣
,→ 192.168.230.1 YES NVRAM up up \r\nEthernet0/
,→3.100 10.100.0.1 YES NVRAM up up ␣
,→\r\nEthernet0/3.200 10.200.0.1 YES NVRAM up ␣
,→up \r\nEthernet0/3.300 10.30.0.1 YES NVRAM up ␣
,→ up \r\nR1>'
read_until vs read_very_eager
An important difference between read_until and read_very_eager is how they react to the lack
of output.
Method read_until waits for a certain string. By default, if it does not exist, method will “freeze”.
Timeout option allows you to specify how long to wait for the desired string:
In [21]: telnet.read_very_eager()
Out[21]: b''
Method expect
Method expect allows you to specify a list with regular expressions. It works like pexpect but telnetlib
always has to pass a list of regular expressions.
In [23]: telnet.expect([b'[>#]'])
Out[23]:
(0,
<_sre.SRE_Match object; span=(46, 47), match=b'>'>,
b'sh clock\r\n*19:35:10.984 UTC Fri Nov 3 2017\r\nR1>')
• object Match
• byte string that contains everything read till regular expression including regular expression
In [26]: regex_idx
Out[26]: 0
In [27]: match.group()
Out[27]: b'>'
In [28]: match
Out[28]: <_sre.SRE_Match object; span=(46, 47), match=b'>'>
In [29]: match.group()
Out[29]: b'>'
In [30]: output
Out[30]: b'sh clock\r\n*19:37:21.577 UTC Fri Nov 3 2017\r\nR1>'
In [31]: output.decode('utf-8')
Out[31]: 'sh clock\r\n*19:37:21.577 UTC Fri Nov 3 2017\r\nR1>'
Method close
Method close closes connection but it’s better to open and close connection using context manager:
In [32]: telnet.close()
Working principle of telnetlib resembles pexpect, so the example below should be clear (2_tel-
netlib.py):
import telnetlib
import time
from pprint import pprint
def to_bytes(line):
return f"{line}\n".encode("utf-8")
result = {}
for command in commands:
telnet.write(to_bytes(command))
output = telnet.read_until(b"#", timeout=5).decode("utf-8")
result[command] = output.replace("\r\n", "\n")
return result
if __name__ == "__main__":
devices = ["192.168.100.1", "192.168.100.2", "192.168.100.3"]
commands = ["sh ip int br", "sh arp"]
for ip in devices:
result = send_show_command(ip, "cisco", "cisco", "cisco", commands)
pprint(result, width=120)
Since bytes need to be passed to write method and new line character should be added each time,
a small function to_bytes is created that does the conversion to bytes and adds a new line.
Script execution:
'Et0/3 up up \n'
'R1#',
'sh ip int br': 'sh ip int br\n'
'Interface IP-Address OK? Method Status ␣
,→ Protocol\n'
'Ethernet0/0 192.168.100.1 YES NVRAM up ␣
,→ up \n'
'Ethernet0/1 192.168.200.1 YES NVRAM up ␣
,→ up \n'
'Ethernet0/2 unassigned YES NVRAM up ␣
,→ up \n'
'Ethernet0/3 192.168.130.1 YES NVRAM up ␣
,→ up \n'
'R1#'}
{'sh int desc': 'sh int desc\n'
'Interface Status Protocol Description\n'
'Et0/0 up up \n'
'Et0/1 up up \n'
'Et0/2 admin down down \n'
'Et0/3 admin down down \n'
'R2#',
'sh ip int br': 'sh ip int br\n'
'Interface IP-Address OK? Method Status ␣
,→ Protocol\n'
'Ethernet0/0 192.168.100.2 YES NVRAM up ␣
,→ up \n'
'Ethernet0/1 unassigned YES NVRAM up ␣
,→ up \n'
'Ethernet0/2 unassigned YES NVRAM administratively␣
,→down down \n'
'Ethernet0/3 unassigned YES NVRAM administratively␣
,→down down \n'
'R2#'}
{'sh int desc': 'sh int desc\n'
'Interface Status Protocol Description\n'
'Et0/0 up up \n'
'Et0/1 up up \n'
'Et0/2 admin down down \n'
'Et0/3 admin down down \n'
'R3#',
'sh ip int br': 'sh ip int br\n'
'Interface IP-Address OK? Method Status ␣
,→ Protocol\n'
(continues on next page)
Example of using telnetlib to work with paginated output of show commands (2_telnetlib_more.py
file):
import telnetlib
import time
from pprint import pprint
import re
def to_bytes(line):
return f"{line}\n".encode("utf-8")
telnet.write(to_bytes(command))
result = ""
while True:
index, match, output = telnet.expect([b"--More--", b"#"], timeout=5)
output = output.decode("utf-8")
output = re.sub(" +--More--| +\x08+ +\x08+", "\n", output)
result += output
if index in (1, -1):
break
telnet.write(b" ")
time.sleep(1)
result.replace("\r\n", "\n")
return result
if __name__ == "__main__":
devices = ["192.168.100.1", "192.168.100.2", "192.168.100.3"]
for ip in devices:
result = send_show_command(ip, "cisco", "cisco", "cisco", "sh run")
pprint(result, width=120)
Module paramiko
Since Paramiko is not part of standard Python module library, it needs to be installed:
Connection is established in this way: first, client is created and client configuration is set, then
connection is initiated and an interactive session is returned:
In [3]: client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
SSHClient is a class that represents a connection to SSH server. It performs client authentication.
Method connect connects to SSH server and authenticates the connection. Parameters:
• look_for_keys - by default paramiko performs key authentication. To disable this, put the flag
in False
• allow_agent - paramiko can connect to a local SSH agent. This is necessary when working
with keys and since in this case authentication is done by login/password, it should be disabled.
After execution of previous command there is already a connection to server. Method invoke_shell
allows to set an interactive SSH session with server.
Method send
Method send - sends specified string to session and returns amount of sent bytes.
In [7]: ssh.send("enable\n")
Out[7]: 7
In [8]: ssh.send("cisco\n")
Out[8]: 6
Warning: In code, after send you will need to put time.sleep, especially between send and
recv. Since this is an interactive session and commands are slow to type, everything works
without pauses.
Method recv
Method recv receives data from session. In parentheses, the maximum value in bytes that can be
obtained is indicated. This method returns a received string
In [10]: ssh.recv(3000)
Out[10]: b'\r\nR1>enable\r\nPassword: \r\nR1#sh ip int br\r\nInterface ␣
,→ IP-Address OK? Method Status Protocol\r\nEthernet0/0 ␣
,→ 192.168.100.1 YES NVRAM up up ␣
,→\r\nEthernet0/1 192.168.200.1 YES NVRAM up ␣
,→up \r\nEthernet0/2 unassigned YES NVRAM up ␣
,→ up \r\nEthernet0/3 192.168.130.1 YES NVRAM up ␣
(continues on next page)
,→ up \r\nLoopback22 10.2.2.2 YES␣
,→manual up up \r\nLoopback33 unassigned ␣
400 Chapter 7. V. Working with network devices
,→ YES unset up up \r\nLoopback45 ␣
,→unassigned YES unset up up \r\nLoopback55 ␣
,→ 5.5.5.5 YES manual up up \r\nR1#'
Python for network engineers, Release 1.0
Method close
In [11]: ssh.close()
import paramiko
import time
import socket
from pprint import pprint
def send_show_command(
ip,
username,
password,
enable,
command,
max_bytes=60000,
short_pause=1,
long_pause=5,
):
cl = paramiko.SSHClient()
cl.set_missing_host_key_policy(paramiko.AutoAddPolicy())
cl.connect(
hostname=ip,
username=username,
password=password,
look_for_keys=False,
allow_agent=False,
)
with cl.invoke_shell() as ssh:
ssh.send("enable\n")
ssh.send(f"{enable}\n")
time.sleep(short_pause)
(continues on next page)
result = {}
for command in commands:
ssh.send(f"{command}\n")
ssh.settimeout(5)
output = ""
while True:
try:
part = ssh.recv(max_bytes).decode("utf-8")
output += part
time.sleep(0.5)
except socket.timeout:
break
result[command] = output
return result
if __name__ == "__main__":
devices = ["192.168.100.1", "192.168.100.2", "192.168.100.3"]
commands = ["sh clock", "sh arp"]
result = send_show_command("192.168.100.1", "cisco", "cisco", "cisco",␣
,→commands)
pprint(result, width=120)
'R1#',
'sh clock': 'sh clock\r\n*08:25:22.435 UTC Mon Jul 20 2020\r\nR1#'}
Example of using paramiko to work with paginated output of show command (3_paramiko_more.py
file):
import paramiko
import time
import socket
from pprint import pprint
import re
def send_show_command(
ip,
username,
password,
enable,
command,
max_bytes=60000,
short_pause=1,
long_pause=5,
):
cl = paramiko.SSHClient()
cl.set_missing_host_key_policy(paramiko.AutoAddPolicy())
cl.connect(
hostname=ip,
username=username,
password=password,
look_for_keys=False,
allow_agent=False,
)
(continues on next page)
result = {}
for command in commands:
ssh.send(f"{command}\n")
ssh.settimeout(5)
output = ""
while True:
try:
page = ssh.recv(max_bytes).decode("utf-8")
output += page
time.sleep(0.5)
except socket.timeout:
break
if "More" in page:
ssh.send(" ")
output = re.sub(" +--More--| +\x08+ +\x08+", "\n", output)
result[command] = output
return result
if __name__ == "__main__":
devices = ["192.168.100.1", "192.168.100.2", "192.168.100.3"]
commands = ["sh run"]
result = send_show_command("192.168.100.1", "cisco", "cisco", "cisco",␣
,→commands)
pprint(result, width=120)
Module netmiko
Netmiko is a module that makes it easier to use paramiko for network devices. Netmiko uses
paramiko but also creates interface and methods needed to work with network devices.
• Arista vEOS
• Cisco ASA
• Cisco IOS
• Cisco IOS-XR
• Cisco SG300
• HP Comware7
• HP ProCurve
• Juniper Junos
• Linux
• and other
cisco_router = {
'device_type': 'cisco_ios',
'host': '192.168.1.1',
'username': 'user',
'password': 'userpass',
'secret': 'enablepass',
'port': 20022,
}
ssh = ConnectHandler(**cisco_router)
Enable mode
ssh.enable()
ssh.exit_enable_mode()
Sending commands
• send_config_from_file - send commands from the file (uses send_config_set method in-
side)
• send_command_timing - send command and wait for the output based on timer
send_command
For example:
• sends command to device and gets the output until string with prompt or until specified string
– if your device does not determine it, you can simply specify a string till which to read the
output
1.0.0 this is how send_command works and send_command_expect method is left for
compatibility
– command_string - command
– delay_factor - option allows to increase delay before the start of string search
– max_loops - number of iterations before method gives out an error (exception). By default
500
send_config_set
Example:
result = ssh.send_config_set(commands)
• depending on device type, there may be no exit from configuration mode. For example, there
will be no exit for IOS-XR because you first have to commit changes
send_config_from_file
Example of use:
result = ssh.send_config_from_file('config_ospf.txt')
Method opens a file, reads commands and passes them to send_config_set method.
Additional methods
Besides the above methods for sending commands, netmiko supports such methods:
Telnet support
Since version 1.0.0 netmiko supports Telnet connections, so far only for Cisco IOS devices. Inside
netmiko uses telnetlib to connect via Telnet. But, at the same time, it provides the same interface
for work as for SSH connection.
In order to connect via Telnet, it is enough in the dictionary that defines connection parameters
specify device type cisco_ios_telnet:
device = {
"device_type": "cisco_ios_telnet",
"host": "192.168.100.1",
"username": "cisco",
"password": "cisco",
"secret": "cisco",
}
Otherwise, methods that apply to SSH apply to Telnet. An example similar to SSH (4_net-
miko_telnet.py file):
if __name__ == "__main__":
device = {
"device_type": "cisco_ios_telnet",
"host": "192.168.100.1",
"username": "cisco",
"password": "cisco",
"secret": "cisco",
}
result = send_show_command(device, ["sh clock", "sh ip int br"])
pprint(result, width=120)
• send_command_timing
• find_prompt
• send_config_set
• send_config_from_file
• check_enable_mode
• disconnect
if __name__ == "__main__":
with open("devices.yaml") as f:
devices = yaml.safe_load(f)
for device in devices:
result = send_show_command(device, ["sh clock", "sh ip int br"])
pprint(result, width=120)
In this example terminal length command is not passed because netmiko executes this command
by default.
Example of using netmiko with paginated output of show command (4_netmiko_more.py file):
break
return output
if __name__ == "__main__":
with open("devices.yaml") as f:
devices = yaml.safe_load(f)
print(send_show_command(devices[0], "sh run"))
Module scrapli
scrapli is a module that allows you to connect to network equipment using Telnet, SSH or NETCONF.
Just like netmiko, scrapli can use paramiko or telnetlib (and other modules) for the connection itself,
but it provides the same interface for different types of connections and different equipment.
Installing scrapli:
• channel - the next level above the transport, which is responsible for sending commands,
receiving output and other interactions with equipment
• driver is the interface for working with scrapli. There are both specific drivers, for example,
IOSXEDriver, which understands how to interact with a specific type of equipment, and the
basic Driver, which provides a minimal interface for working via SSH/Telnet.
• system - the built-in SSH client, it is assumed that the client is used on Linux/MacOS
• telnet - telnetlib
Most of the examples will be using the system transport. Since the module interface is the same for
all synchronous transport options, to use a different transport, you just need to specify it (for telnet
transport, you must also specify the port).
Note: Asynchronous transport options (asyncssh, asynctelnet) are covered in the Advanced Python
for network engineers (russian)
Supported platforms:
• Cisco IOS-XE
• Cisco NX-OS
• Juniper JunOS
• Cisco IOS-XR
• Arista EOS
In addition to these platforms, there are also scrapli community platforms. And one of the advan-
tages of scrapli is that it is relatively easy to add new platforms.
There are two connection options in scrapli: using the general Scrapli class, which selects the re-
quired driver by the platform parameter, or a specific driver, for example, IOSXEDriver. The same
parameters are passed to the specific driver and Scrapli.
Note: In addition to these options, there are also generic (base) drivers.
If the scrapli (or scrapli community) does not support the required platform, you can add the platform
to the scrapli community or use generic drivers (not covered in the book):
• Driver
• GenericDriver
• NetworkDriver
Connection parameters
The connection process is slightly different depending on whether you are using a context manager
or not. When connecting without a context manager, you first need to pass parameters to the driver
or Scrapli, and then call the open method:
r1 = {
"host": "192.168.100.1",
"auth_username": "cisco",
"auth_password": "cisco",
"auth_secondary": "cisco",
"auth_strict_key": False,
"platform": "cisco_iosxe"
}
In [3]: ssh.open()
In [4]: ssh.get_prompt()
Out[4]: 'R1#'
In [5]: ssh.close()
Available drivers
In [12]: r1_driver = {
...: "host": "192.168.100.1",
...: "auth_username": "cisco",
...: "auth_password": "cisco",
...: "auth_secondary": "cisco",
...: "auth_strict_key": False,
...: }
Sending commands
All of these methods return a Response object, not the output of the command as a string.
Response object
The send_command method and other methods for sending commands return a Response object
(not the output of the command). Response allows you to get not only the output of the command,
but also such things as the execution time of the command, whether there were errors during the
execution of the command, structured output using textfsm, and so on.
In [16]: reply
Out[16]: Response <Success: True>
You can get the output of the command by accessing the result attribute:
In [17]: reply.result
Out[17]: '*17:31:54.232 UTC Wed Mar 31 2021'
In [18]: reply.raw_result
Out[18]: b'\n*17:31:54.232 UTC Wed Mar 31 2021\nR1#'
For commands that take longer than normal show, it may be necessary to know the command
execution time:
In [19]: r.result
Out[19]: 'Type escape sequence to abort.\nSending 5, 100-byte ICMP Echos to 10.1.
,→1.1, timeout is 2 seconds:\n.....\nSuccess rate is 0 percent (0/5)'
In [20]: r.elapsed_time
Out[20]: 10.047594
In [21]: r.start_time
Out[21]: datetime.datetime(2021, 4, 1, 7, 10, 56, 63697)
In [22]: r.finish_time
Out[22]: datetime.datetime(2021, 4, 1, 7, 11, 6, 111291)
The channel_input attribute returns the command that was sent to the equipment:
In [23]: r.channel_input
Out[23]: 'ping 10.1.1.1'
send_command method
• failed_when_contains - if the output contains the specified line or one of the lines in the list,
the command will be considered as completed with an error
In [16]: reply
Out[16]: Response <Success: True>
The timeout_ops parameter specifies how long to wait for the command to execute:
If the command does not complete within the specified time, a ScrapliTimeout exception will be
raised (output is truncated):
In addition to receiving normal command output, scrapli also allows you to receive structured output,
for example using the textfsm_parse_output method:
In [22]: reply.textfsm_parse_output()
Out[22]:
[{'intf': 'Ethernet0/0',
'ipaddr': '192.168.100.1',
'status': 'up',
'proto': 'up'},
{'intf': 'Ethernet0/1',
'ipaddr': '192.168.200.1',
(continues on next page)
'status': 'up',
'proto': 'up'},
{'intf': 'Ethernet0/2',
'ipaddr': 'unassigned',
'status': 'up',
'proto': 'up'},
{'intf': 'Ethernet0/3',
'ipaddr': '192.168.130.1',
'status': 'up',
'proto': 'up'}]
Note: What is TextFSM and how to work with it is covered in chapter 21. Scrapli uses ready-made
templates in order to receive structured output and in basic cases does not require knowledge of
TextFSM.
Error detection
Methods for sending commands automatically check the output for errors. For each vendor/type of
equipment, these are different errors, plus you can specify which lines in the output will be consid-
ered an error. By default, IOSXEDriver will consider the following lines as errors:
In [21]: ssh.failed_when_contains
Out[21]:
['% Ambiguous command',
'% Incomplete command',
'% Invalid input detected',
'% Unknown command']
The failed attribute of the Response object returns False if the command finished without error
and True if it failed.
In [24]: reply.result
Out[24]: " ^\n% Invalid input detected at '^' marker."
In [25]: reply
Out[25]: Response <Success: False>
In [26]: reply.failed
Out[26]: True
send_config method
The send_config method allows you to send one configuration mode command.
Example:
Since scrapli removes the command from the output, by default, when using send_config, the result
attribute will contain an empty string (if there was no error while executing the command):
In [34]: r.result
Out[34]: ''
You can add the parameter strip_prompt=False and then the prompt will appear in the output:
In [38]: r.result
Out[38]: 'R1(config)#'
send_commands, send_configs
The send_commands, send_configs methods differ from send_command, send_config in that they
can send several commands. In addition, these methods do not return a Response, but a Mul-
tiResponse object, which can generally be thought of as a list of Response objects, one for each
command.
In [45]: reply
Out[45]: MultiResponse <Success: True; Response Elements: 2>
In [47]: reply.result
Out[47]: 'sh clock\n*08:38:20.115 UTC Thu Apr 1 2021sh ip int br\nInterface ␣
,→ IP-Address OK? Method Status Protocol\nEthernet0/
,→0 192.168.100.1 YES NVRAM up up\nEthernet0/
,→1 192.168.200.1 YES NVRAM up up\nEthernet0/
,→2 unassigned YES NVRAM up up\nEthernet0/
,→3 192.168.130.1 YES NVRAM up up'
In [48]: reply[0]
Out[48]: Response <Success: True>
In [49]: reply[1]
Out[49]: Response <Success: True>
In [50]: reply[0].result
Out[50]: '*08:38:20.115 UTC Thu Apr 1 2021'
When sending multiple commands, it is also very convenient to use the stop_on_failed parameter.
By default, it is False, so all commands are executed, but if you specify stop_on_failed=True, after
an error occurs in some command, the following commands will not be executed:
In [60]: reply
Out[60]: MultiResponse <Success: False; Response Elements: 2>
In [61]: reply.result
Out[61]: "ping 192.168.100.2\nType escape sequence to abort.\nSending 5, 100-byte␣
,→ICMP Echos to 192.168.100.2, timeout is 2 seconds:\n!!!!!\nSuccess rate is 100␣
,→percent (5/5), round-trip min/avg/max = 1/2/6 mssh clck\n ^\n% Invalid␣
,→input detected at '^' marker."
Telnet connection
To connect to equipment via Telnet, you must specify transport equal to telnet and be sure to
specify the port parameter equal to 23 (or the port that you use to connect via Telnet):
r1 = {
"host": "192.168.100.1",
"auth_username": "cisco",
"auth_password": "cisco2",
"auth_secondary": "cisco",
"auth_strict_key": False,
"transport": "telnet",
"port": 23, # must be specified when connecting telnet
}
if __name__ == "__main__":
output = send_show(r1, "sh ip int br")
print(output)
Scrapli examples
r1 = {
"host": "192.168.100.1",
"auth_username": "cisco",
"auth_password": "cisco",
"auth_secondary": "cisco",
"auth_strict_key": False,
"timeout_socket": 5, # timeout for establishing socket/initial connection
"timeout_transport": 10, # timeout for ssh|telnet transport
}
if __name__ == "__main__":
output = send_show(r1, "sh ip int br")
print(output)
r1 = {
"host": "192.168.100.1",
"auth_username": "cisco",
"auth_password": "cisco",
"auth_secondary": "cisco",
"auth_strict_key": False,
"platform": "cisco_iosxe",
(continues on next page)
if __name__ == "__main__":
print("show".center(20, "#"))
output = send_show(r1, ["sh ip int br", "sh ver | i uptime"])
pprint(output, width=120)
r1 = {
"host": "192.168.100.1",
"auth_username": "cisco",
"auth_password": "cisco",
"auth_secondary": "cisco",
"auth_strict_key": False,
"platform": "cisco_iosxe",
}
)
output = reply.result
return output
if __name__ == "__main__":
output_cfg = send_cfg(
r1, ["interfacelo11", "ip address 11.1.1.1 255.255.255.255"], strict=True
)
print(output_cfg)
Further reading
Documentation:
• pexpect
• telnetlib
• paramiko Client
• paramiko Channel
• netmiko
• scrapli
• scrapli-cfg
• time
• datetime
• getpass
Articles:
• Netmiko Library
Code examples:
• netmiko
• scrapli
Tasks
Warning: Starting from section “4. Data types in Python” there are automated tests for testing
tasks. They help to check whether everything matches the task, and also give feedback on what
does not correspond to the task. As a rule, after the first period of adaptation to tests, it becomes
easier to do tasks with tests. Testing is done using the pyneng utility. Learn more about how to
work with the pyneng utility.
Task 18.1
The function connects via SSH (using netmiko) to ONE device and executes the specified command.
Function parameters:
The script should send command command to all devices from the devices.yaml file using the
send_show_command function (this part of the code is written).
import yaml
if __name__ == "__main__":
command = "sh ip int br"
with open("devices.yaml") as f:
devices = yaml.safe_load(f)
Task 18.1a
Copy the send_show_command function from task 18.1 and rewrite it to handle the exception that
is thrown on authentication failure on the device.
Task 18.1b
Copy the send_show_command function from task 18.1a and rewrite it to handle not only the ex-
ception that is raised when authentication fails on the device, but also the exception that is raised
when the IP address of the device is not available.
Task 18.2
The function connects via SSH (using netmiko) to ONE device and executes a list of commands in
configuration mode based on the passed arguments.
Function parameters:
The function should return a string with the results of the command:
In [7]: r1
Out[7]:
{'device_type': 'cisco_ios',
'ip': '192.168.100.1',
'username': 'cisco',
'password': 'cisco',
'secret': 'cisco'}
In [8]: commands
Out[8]: ['logging 10.255.255.1', 'logging buffered 20010', 'no logging console']
In [10]: result
Out[10]: 'config term\nEnter configuration commands, one per line. End with CNTL/
,→Z.\nR1(config)#logging 10.255.255.1\nR1(config)#logging buffered␣
,→20010\nR1(config)#no logging console\nR1(config)#end\nR1#'
In [11]: print(result)
config term
Enter configuration commands, one per line. End with CNTL/Z.
R1(config)#logging 10.255.255.1
(continues on next page)
The script should send command command to all devices from the devices.yaml file using the
send_config_commands function.
commands = [
'logging 10.255.255.1', 'logging buffered 20010', 'no logging console'
]
Task 18.2a
Copy the send_config_commands function from job 18.2 and add the log parameter. The log param-
eter controls whether information is displayed about which device the connection is to:
In [15]:
The script should send command command to all devices from the devices.yaml file using the
send_config_commands function.
Task 18.2b
Copy the send_config_commands function from task 18.2a and add error checking.
When executing each command, the script should check the output for the following errors: Invalid
input detected, Incomplete command, Ambiguous command
If an error occurs while executing any of the commands, the function should print a message to
the stdout with information about what error occurred, in which command and on which device,
for example: The “logging” command was executed with the error “Incomplete command.” on the
device 192.168.100.1
Errors should always be printed, regardless of the value of the log parameter. At the same time,
the log parameter should still control whether the message “Connecting to 192.168.100.1…” will be
displayed.
• the first dict with the output of commands that were executed without error
• second dict with the output of commands that were executed with errors
In both dictionaries:
• key - command
In [16]: commands
Out[16]:
['logging 0255.255.1',
'logging',
'a',
'logging buffered 20010',
'ip http server']
"a" command was executed with error "Ambiguous command: "a"" on device 192.168.
,→100.1
'R1(config)#logging 0255.255.1\n'
' ^\n'
"% Invalid input detected at '^' marker.\n"
'\n'
'R1(config)#'})
In [20]: good.keys()
Out[20]: dict_keys(['logging buffered 20010', 'ip http server'])
In [21]: bad.keys()
Out[21]: dict_keys(['logging 0255.255.1', 'logging', 'a'])
R1(config)#logging 0255.255.1
^
% Invalid input detected at '^' marker.
R1(config)#logging
% Incomplete command.
R1(config)#a
% Ambiguous command: "a"
Task 18.2c
Copy the send_config_commands function from task 18.2b and remake it as follows: If an error
occurs while executing a command, ask the user whether to continue executing other commands.
• the first dictionary with the output of commands that were executed without error
• second dictionary with the output of commands that were executed with errors
In both dictionaries:
• key - command
In [12]: pprint(result)
({},
{'logging': 'config term\n'
'Enter configuration commands, one per line. End with CNTL/Z.\n'
'R1(config)#logging\n'
'% Incomplete command.\n'
'\n'
(continues on next page)
'R1(config)#',
'logging 0255.255.1': 'config term\n'
'Enter configuration commands, one per line. End with '
'CNTL/Z.\n'
'R1(config)#logging 0255.255.1\n'
' ^\n'
"% Invalid input detected at '^' marker.\n"
'\n'
'R1(config)#'})
Task 18.3
Function parameters:
The show and config arguments should only be passed as keyword arguments. Passing these argu-
ments as positional should raise a TypeError exception.
Depending on which argument was passed, the send_commands function calls different functions in-
ternally. When calling the send_commands function, only one of the show, config arguments should
always be passed. If both arguments are passed, a ValueError exception should be raised.
The function returns a string with the results of executing single command or multiple commands.
Commands example:
When you need to connect to a large number of devices, it will take quite a long time to complete
the connections one by one. Of course, it will be faster than manual connection, but it would be
faster to connect to equipment in parallel.
Note: All these “long” and “faster” are relative concepts, but in this section we will learn to measure
exact script execution time to compare how quick the connection is established.
There are several options for estimating execution time of the script. The simplest options are:
When measuring the execution time of script in this case, high accuracy is not important. The main
thing is to compare the execution time of script in different variants.
time
Linux time utility allows you to measure the execution time of a script. To use time utility it is enough
to write time before starting the script:
datetime
The second option is a datetime module. This module allows working with time and dates in Python.
Example:
start_time = datetime.now()
print(datetime.now() - start_time)
Output:
$ python test.py
0:00:05.004949
• process - roughly speaking, it’s a launched program. Separate resources are allocated to the
process: memory, processor time
• thread - execution unit in the process. Thread share resources of the process to which they
relate.
Python (or, more precisely, CPython - the implementation used in the book) is optimized to work in
single-threaded mode. This is good if program uses only one thread. And, at the same time, Python
has certain nuances of running in multithreaded mode. This is because CPython uses GIL (global
interpreter lock).
GIL does not allow multiple threads to execute Python code at the same time. If you don’t go into
detail, GIL can be visualized as a sort of flag that carried over from thread to thread. Whoever has
the flag can do the job. The flag is transmitted either every Python instruction or, for example, when
some type of input-output operation is performed.
Therefore, different threads will not run in parallel and the program will simply switch between them
executing them at different times. However, if in the program there is some “wait” (packages from
the network, user request, time.sleep pause), then in such program the threads will be executed as
if in parallel. This is because during such pauses the flag (GIL) can be passed to another thread.
That is, threads are well suited for tasks that involve input-output (IO) operations:
• Downloading files
Note: In the Internet it is often possible to find phrases like «In Python it is better not to use threads
at all». Unfortunately, such phrases are not always written in context, namely that it is about specific
tasks that are tied to CPU.
The next sections discuss how to use threads to connect via Telnet/SSH. Script execution time will
be checked comparing the sequential execution and execution using processes.
Processes
Processes allow to execute tasks on different computer cores. This is important for tasks that are
tied to CPU. For each process a copy of resources is created, a memory is allocated, each process
has its own GIL. This also makes processes “heavier” than threads.
In addition, the number of processes that run in parallel depends on the number of cores and CPU
and is usually estimated in dozens, while the number of threads for input-output operations can be
estimated in hundreds.
Processes and threads can be combined but this complicates the program and at the base level for
input-output operations it is better to stop at threads.
Note: Combining threads and processes, i.e., starting a process in a program and then starting
threads inside it, makes troubleshooting difficult. And I’d recomend not use that option.
Although it is usually better to use threads for input-output tasks, for some modules it is better to
use processes because they may not work correctly with threads.
Note: In addition to processes and threads, there is another version of concurrent connections to
device: asynchronous programming. This option is not covered in the book.
Number of threads
How many threads you need to use when connecting to device? There is no clear answer to this
question. The number of threads depends at least on which computer runs the script (OS, memory,
processor), on network itself (delays).
So instead of looking for the perfect number of threads, you have to measure the number on your
computer, your network, your script. For example, in the examples to this section there is a script
netmiko_count_threads.py that runs the same function with different threads and displays runtime
information. Function by default uses a small number of devices from the devices_all.yaml file and
a small number of threads, but it can be adapted to any number based on your network.
#30 threads
----------------------------------------
Execution time: 0:09:17.187867
#50 threads
----------------------------------------
Execution time: 0:09:17.604252
#70 threads
----------------------------------------
Execution time: 0:09:17.117332
#90 threads
----------------------------------------
Execution time: 0:09:16.693774
#100 threads
----------------------------------------
Execution time: 0:09:17.083294
#120 threads
----------------------------------------
Execution time: 0:09:17.945270
#140 threads
----------------------------------------
Execution time: 0:09:18.114993
#200 threads
----------------------------------------
Execution time: 0:11:12.951247
#300 threads
----------------------------------------
Execution time: 0:14:03.790432
In this case, the execution time with 30 threads and 120 threads is the same and after time only
increases. This is because switching between threads also takes a lot of time and the more streams
the more switching. And from some moment it makes no sense to increase number of threads.
For this example (this PC, code and number of devices), the optimal number is approximately 50
threads. We’re not taking 30 here in order to make a reserve.
Thread safety
When working with threads there are several recommendations and rules. If they are respected, it
is easier to work with threads and it is likely that there will be no problem with threads. Of course,
from time to time, there will be tasks that will require violations of recommendations. However,
before doing so, it is better to try to meet the task by adhering to recommendations. If this is not
possible, then we should look for ways to secure the solution so that the data is not damaged.
Very important feature of working with threads: with a small number of threads and small test tasks
“everything works”. For example, printing output when connected to 20 devices in 5 threads will
work normally. But when connected to a large number of devices with a large number of threads,
it turns out that sometimes messages overlap. This peculiarity appears very often, so do not trust
the version when “everything works” on basic examples, follow the rules of working with threads.
Before dealing with rules we have to deal with term “thread safety”. Thread safety is a concept that
describes work with multithreaded programs. Code is considered to be thread-safe if it can work
normally with multiple threads.
For example, print function is not thread-safe. This is demonstrated by the fact that when code
executes print from different threads, messages in the output can be mixed. There could be output
with a part of message from the first thread, then a part from the second thread, then a part from
the first thread, and so on. That is, print function does not work normally (as it should be) in thread.
In this case, it is said that print function is not thread-safe.
In general, there is no problem if each thread works with its own resources. For example, each thread
writes data to its own file. However, this is not always possible or can complicate the solution.
Note: print has problems because we write from different threads into one standard output stream
but print is not thread-safe.
If you have to write from different threads to the same resource, there are two options:
1. Write to the same resource after job in thread is finished. For example, a function has been
executed in threads 1, 2 and 3, its result is obtained in turn (consecutively) from each thread,
and then written into a file.
2. Use a thread-safe alternative (not always available and/or easy). For example, use a logging
module instead of print function.
1. Do not write to the same resource from different threads if resource or what you write is not
intended for multithreading. It is easy to find out by google something like “python write to
file from threads”.
• There are nuances to this recommendation. For example, you can write from different threads
to the same file if you use a Lock or a thread-safe queue. These options are often difficult to
use and are not covered in the book. It’s likely that 95 percent of problems you’ll be facing can
be solved without them.
2. If there is a possibility, avoid communication between threads in the course of their work. This
is not an easy task and it is best to avoid it.
3. Follow the KISS (Keep it simple, stupid) principle - try to make solution as simple as possible.
Note: These recommendations are generally written for those who are just beginning to program
on Python. However, they tend to be relevant to most programmers who write applications for users
rather than frameworks.
Module concurrent.futures which will be covered further, simplifies implementation of the first prin-
ciple “Do not write to the same resource from different threads… “. The module interface itself
encourages this, but of course it does not prohibit breaking it.
However, before getting to know concurrent.futures, you should read fundamentals of logging mod-
ule. It will be used instead of print function which is not thread-safe.
Module logging
Module logging - a module from Python standard library that allows you to configure logging from
the script. Module logging has a lot of features and a lot of configuration options. Only basic con-
figuration option is discussed in this section.
import logging
logging.basicConfig(
format='%(threadName)s %(name)s %(levelname)s: %(message)s',
level=logging.INFO,
)
• each message will contain thread information, log name, message level, and message itself
Now, to output a log message in this script, you need to write logging.info("test").
logging.basicConfig(
format = '%(threadName)s %(name)s %(levelname)s: %(message)s',
level=logging.INFO)
if __name__ == "__main__":
with open('devices.yaml') as f:
devices = yaml.safe_load(f)
for dev in devices:
print(send_show(dev, 'sh clock'))
$ python logging_basics.py
MainThread root INFO: ===> 12:26:12.767168 Connection: 192.168.100.1
MainThread root INFO: <=== 12:26:18.307017 Received: 192.168.100.1
(continues on next page)
Note: There are still many features in logging module. This section only uses basic configuration
option. For more information on features of the module, see Logging HOWTO
Module concurrent.futures
Module concurrent.futures provides a high-level interface for working with processes and threads.
For both threads and processes the same interface is used which makes it easy to switch between
them.
If you compare this module with threading or multiprocessing, it has fewer features but with
concurrent.futures it’s easier to work and interface easier to understand.
Concurrent.futures module allows to solve the problem of starting multiple threads/processes and
getting data from them. For this purpose, module uses two classes:
Both classes use the same interface, so it is enough to deal with one and then just switch to other
if necessary.
executor = ThreadPoolExecutor(max_workers=5)
After creating an Executor object, it has three methods: shutdown, map, and submit. Method
shutdown is responsible for the completion of threads/processes, map and submit methods are re-
sponsible for starting functions in different threads/processes.
Note: In fact, map and submit can run not only functions but any callable object. However, only
functions will be covered further.
Method shutdown indicates that Executor object must be finished. However, if to shutdown method
pass wait=True (default value), it will not return the result until all functions running in threads have
been completed. If wait=False, shutdown method returns instantly but script itself will not exit until
all functions have been completed.
Generally, shutdown is not explicitly used because when creating an Executor object in a context
manager, shutdown is automatically called at the end of a block with wait=True.
Since map and submit methods start a function in threads or processes, code must at least have a
function that performs one action and must be run in different threads with different arguments of
the function.
For example, if you need to ping multiple IP addresses in different threads you need to create a
function that pings one IP address and then run this function in different threads for different IP
addresses using concurrent.futures.
Method map
Method syntax:
Method map is similar to built-in map function: applying func function to one or more iterable objects.
Each call to a function is then started in a separate thread/process. Method map returns an iterator
with function results for each element of object being iterated. The results are arranged in the same
order as elements in iterable object.
When working with thread/process pools, a certain number of threads/processes are created and the
code is executed in these threads. For example, if the pool is created with 5 threads and function
has to be started for 10 different devices, connection will be performed first to the first five devices
and then, as they liberated, to the others.
import netmiko
import yaml
logging.getLogger('paramiko').setLevel(logging.WARNING)
(continues on next page)
logging.basicConfig(
format = '%(threadName)s %(name)s %(levelname)s: %(message)s',
level=logging.INFO)
with open('devices.yaml') as f:
devices = yaml.safe_load(f)
Since function should be passed to map method, send_show function is created which connects to
devices, passes specified show command and returns the result with command output.
logging.info(received_msg.format(datetime.now().time(), ip))
return result
Function send_show outputs log message at the beginning and at the end of work. This will determine
when function has worked for the particular device. Also within function it is specified that when
connecting to device with address 192.168.100.1, the pause for 5 seconds is required - thus router
with this address will respond longer.
Last 4 lines of code are responsible for connecting to devices in separate threads:
– elements of iterable object devices and the same command “sh clock”.
– since instead of a list of commands only one command is used, it must be repeated in
some way, so that map method will set this command to different devices. It uses repeat
function - it repeats command exactly as many times as map requests
• map method returns generator. This generator contains results of functions. Results are in
the same order as devices in the list of devices, so zip function is used to combine device IP
addresses and command output.
Execution result:
$ python netmiko_threads_map_basics.py
ThreadPoolExecutor-0_0 root INFO: ===> 08:28:55.950254 Connection: 192.168.100.1
ThreadPoolExecutor-0_1 root INFO: ===> 08:28:55.963198 Connection: 192.168.100.2
ThreadPoolExecutor-0_2 root INFO: ===> 08:28:55.970269 Connection: 192.168.100.3
ThreadPoolExecutor-0_1 root INFO: <=== 08:29:11.968796 Received: 192.168.100.2
ThreadPoolExecutor-0_2 root INFO: <=== 08:29:15.497324 Received: 192.168.100.3
ThreadPoolExecutor-0_0 root INFO: <=== 08:29:16.854344 Received: 192.168.100.1
192.168.100.1 *08:29:16.663 UTC Thu Jul 4 2019
192.168.100.2 *08:29:11.744 UTC Thu Jul 4 2019
192.168.100.3 *08:29:15.374 UTC Thu Jul 4 2019
The first three messages indicate when connection was made and to which device:
The following three messages show time of receipt of information and completion of the function:
Since sleep was added for the first device for 5 seconds, information from the first router was
actually received later. However, since map method returns values in the same order as devices in
device list, the result is:
import yaml
from netmiko import ConnectHandler, NetMikoAuthenticationException
logging.getLogger('paramiko').setLevel(logging.WARNING)
logging.basicConfig(
format = '%(threadName)s %(name)s %(levelname)s: %(message)s',
level=logging.INFO)
ip = device_dict['host']
logging.info(start_msg.format(datetime.now().time(), ip))
if ip == '192.168.100.1': time.sleep(5)
try:
with ConnectHandler(**device_dict) as ssh:
ssh.enable()
result = ssh.send_command(command)
logging.info(received_msg.format(datetime.now().time(), ip))
return result
except NetMikoAuthenticationException as err:
logging.warning(err)
if __name__ == '__main__':
with open('devices.yaml') as f:
devices = yaml.safe_load(f)
pprint(send_command_to_devices(devices, 'sh ip int br'))
Example is generally similar to the previous one but NetMikoAuthenticationException was intro-
duced in send_show function, and the code that started send_show function in threads is now in
send_command_to_devices function.
When using map method, exception handling is best done within a function that runs in threads, in
this case send_show function.
• submit can run different functions with different unrelated arguments, when map must run with
iterable objects as arguments
• submit immediately returns the result without having to wait for function execution
– submit returns Future in order that the call of submit does not block the code. Once
submit has returned Future, code can be executed further. And once all functions in
threads are running, you can start requesting Future if results are ready. Or take advan-
tage of special function as_completed, which requests the result itself and code gets it
when it’s ready
• submit can pass key arguments when map only position arguments
Method submit uses Future object - an object that represents a delayed computation. This object
can be requested for status (completed or not), and results or exceptions can be obtained from the
job. Future does not need to be created manually, these objects are created by submit.
import yaml
from netmiko import ConnectHandler, NetMikoAuthenticationException
logging.getLogger("paramiko").setLevel(logging.WARNING)
logging.basicConfig(
format = '%(threadName)s %(name)s %(levelname)s: %(message)s',
level=logging.INFO)
with open('devices.yaml') as f:
devices = yaml.safe_load(f)
for f in as_completed(future_list):
print(f.result())
The rest of the code has not changed, so you only need to understand the block which runs
send_show function in threads:
The rest of the code has not changed, so only block that runs send_show needs an attention:
• the next cycle runs through future_list using as_completed function. This function returns a
Future objects only when they have finished or been cancelled. Future is then returned as soon
as work is completed, not in the order of adding to future_list
Note: Creation of list with Future can be done with list comprehensions: future_list =
[executor.submit(send_show, device, 'sh clock') for device in devices]
$ python netmiko_threads_submit_basics.py
ThreadPoolExecutor-0_0 root INFO: ===> 17:32:59.088025 Connection: 192.168.100.1
ThreadPoolExecutor-0_1 root INFO: ===> 17:32:59.094103 Connection: 192.168.100.2
ThreadPoolExecutor-0_1 root INFO: <=== 17:33:11.639672 Received: 192.168.100.2
{'192.168.100.2': '*17:33:11.429 UTC Thu Jul 4 2019'}
ThreadPoolExecutor-0_1 root INFO: ===> 17:33:11.849132 Connection: 192.168.100.3
ThreadPoolExecutor-0_0 root INFO: <=== 17:33:17.735761 Received: 192.168.100.1
{'192.168.100.1': '*17:33:17.694 UTC Thu Jul 4 2019'}
ThreadPoolExecutor-0_1 root INFO: <=== 17:33:23.230123 Received: 192.168.100.3
{'192.168.100.3': '*17:33:23.188 UTC Thu Jul 4 2019'}
Please note that the order is not preserved and depends on which function was previously completed.
Future
An example of running send_show function with submit and displaying information about Future
(note the status of Future at different points in time):
In order to look at Future, several lines with information output are added to the script (net-
miko_threads_submit_futures.py):
import yaml
from netmiko import ConnectHandler, NetMikoAuthenticationException
logging.getLogger("paramiko").setLevel(logging.WARNING)
logging.basicConfig(
format = '%(threadName)s %(name)s %(levelname)s: %(message)s',
level=logging.INFO)
if ip == '192.168.100.1':
time.sleep(5)
if __name__ == '__main__':
with open('devices.yaml') as f:
devices = yaml.safe_load(f)
pprint(send_command_to_devices(devices, 'sh clock'))
$ python netmiko_threads_submit_futures.py
Future: <Future at 0xb5ed938c state=running> for device 192.168.100.1
ThreadPoolExecutor-0_0 root INFO: ===> 07:14:26.298007 Connection: 192.168.100.1
Future: <Future at 0xb5ed96cc state=running> for device 192.168.100.2
Future: <Future at 0xb5ed986c state=pending> for device 192.168.100.3
ThreadPoolExecutor-0_1 root INFO: ===> 07:14:26.299095 Connection: 192.168.100.2
ThreadPoolExecutor-0_1 root INFO: <=== 07:14:32.056003 Received: 192.168.100.2
ThreadPoolExecutor-0_1 root INFO: ===> 07:14:32.164774 Connection: 192.168.100.3
Future done <Future at 0xb5ed96cc state=finished returned dict>
ThreadPoolExecutor-0_0 root INFO: <=== 07:14:36.714923 Received: 192.168.100.1
Future done <Future at 0xb5ed938c state=finished returned dict>
(continues on next page)
Since two threads are used by default, only two out of three Future shows running status. The third
is in pending state and is waiting for queue to arrive.
Processing of exceptions
If there is an exception in function execution, it will be generated when the result is obtained For
example, in device.yaml file the password for device 192.168.100.2 was changed to the wrong one:
$ python netmiko_threads_submit.py
===> 06:29:40.871851 Connection to device: 192.168.100.1
===> 06:29:40.872888 Connection to device: 192.168.100.2
===> 06:29:43.571296 Connection to device: 192.168.100.3
<=== 06:29:48.921702 Received result from device: 192.168.100.3
<=== 06:29:56.269284 Received result from device: 192.168.100.1
Traceback (most recent call last):
...
File "/home/vagrant/venv/py3_convert/lib/python3.6/site-packages/netmiko/base_
,→connection.py", line 500, in establish_connection
raise NetMikoAuthenticationException(msg)
netmiko.ssh_exception.NetMikoAuthenticationException: Authentication failure:␣
,→unable to connect cisco_ios 192.168.100.2:22
Authentication failed.
Since an exception occurs when result is obtained, it is easy to add exception processing (net-
miko_threads_submit_exception.py file):
import yaml
from netmiko import ConnectHandler
from netmiko.ssh_exception import NetMikoAuthenticationException
(continues on next page)
logging.getLogger("paramiko").setLevel(logging.WARNING)
logging.basicConfig(
format = '%(threadName)s %(name)s %(levelname)s: %(message)s',
level=logging.INFO)
if __name__ == '__main__':
with open('devices.yaml') as f:
devices = yaml.safe_load(f)
pprint(send_command_to_devices(devices, 'sh clock'))
$ python netmiko_threads_submit_exception.py
ThreadPoolExecutor-0_0 root INFO: ===> 07:21:21.190544 Connection: 192.168.100.1
ThreadPoolExecutor-0_1 root INFO: ===> 07:21:21.191429 Connection: 192.168.100.2
ThreadPoolExecutor-0_1 root INFO: ===> 07:21:23.672425 Connection: 192.168.100.3
Authentication failure: unable to connect cisco_ios 192.168.100.2:22
Authentication failed.
ThreadPoolExecutor-0_1 root INFO: <=== 07:21:29.095289 Received: 192.168.100.3
ThreadPoolExecutor-0_0 root INFO: <=== 07:21:31.607635 Received: 192.168.100.1
{'192.168.100.1': '*07:21:31.436 UTC Fri Jul 26 2019',
'192.168.100.3': '*07:21:28.930 UTC Fri Jul 26 2019'}
Of course, exception handling can be performed within send_show function, but it is just an example
of how you can work with exceptions when using a Future.
Using ProcessPoolExecutor
Interface of concurrent.futures module is very convenient because migration from threads to pro-
cesses is done by replacing ThreadPoolExecutor with ProcessPoolExecutor, so all examples below
are completely similar to examples with threads.
Method map
import yaml
from netmiko import ConnectHandler, NetMikoAuthenticationException
logging.getLogger('paramiko').setLevel(logging.WARNING)
logging.basicConfig(
format = '%(threadName)s %(name)s %(levelname)s: %(message)s',
level=logging.INFO)
(continues on next page)
try:
with ConnectHandler(**device_dict) as ssh:
ssh.enable()
result = ssh.send_command(command)
logging.info(received_msg.format(datetime.now().time(), ip))
return result
except NetMikoAuthenticationException as err:
logging.warning(err)
if __name__ == '__main__':
with open('devices.yaml') as f:
devices = yaml.safe_load(f)
pprint(send_command_to_devices(devices, 'sh clock'))
Result of execution:
$ python netmiko_processes_map.py
MainThread root INFO: ===> 08:35:50.931629 Connection: 192.168.100.2
MainThread root INFO: ===> 08:35:50.931295 Connection: 192.168.100.1
MainThread root INFO: <=== 08:35:56.353774 Received: 192.168.100.2
MainThread root INFO: ===> 08:35:56.469854 Connection: 192.168.100.3
MainThread root INFO: <=== 08:36:01.410230 Received: 192.168.100.1
MainThread root INFO: <=== 08:36:02.067678 Received: 192.168.100.3
{'192.168.100.1': '*08:36:01.242 UTC Fri Jul 26 2019',
(continues on next page)
Method submit
File netmiko_processes_submit_exception.py:
import yaml
from netmiko import ConnectHandler
from netmiko.ssh_exception import NetMikoAuthenticationException
logging.getLogger("paramiko").setLevel(logging.WARNING)
logging.basicConfig(
format = '%(threadName)s %(name)s %(levelname)s: %(message)s',
level=logging.INFO)
future_ssh = [
executor.submit(send_show, device, command) for device in devices
]
for f in as_completed(future_ssh):
try:
result = f.result()
except NetMikoAuthenticationException as e:
print(e)
else:
data.update(result)
return data
if __name__ == '__main__':
with open('devices.yaml') as f:
devices = yaml.safe_load(f)
pprint(send_command_to_devices(devices, 'sh clock'))
Result of execution:
$ python netmiko_processes_submit_exception.py
MainThread root INFO: ===> 08:38:08.780267 Connection: 192.168.100.1
MainThread root INFO: ===> 08:38:08.781355 Connection: 192.168.100.2
MainThread root INFO: <=== 08:38:14.420339 Received: 192.168.100.2
MainThread root INFO: ===> 08:38:14.529405 Connection: 192.168.100.3
MainThread root INFO: <=== 08:38:19.224554 Received: 192.168.100.1
MainThread root INFO: <=== 08:38:20.162920 Received: 192.168.100.3
{'192.168.100.1': '*08:38:19.058 UTC Fri Jul 26 2019',
'192.168.100.2': '*08:38:14.250 UTC Fri Jul 26 2019',
'192.168.100.3': '*08:38:19.995 UTC Fri Jul 26 2019'}
Further reading
Python documentation:
• threading
• multiprocessing
• queue
• PEP 3148
GIL
concurrent.futures
• concurrent.futures in Python 3
Tasks
Warning: Starting from section “4. Data types in Python” there are automated tests for testing
tasks. They help to check whether everything matches the task, and also give feedback on what
does not correspond to the task. As a rule, after the first period of adaptation to tests, it becomes
easier to do tasks with tests. Testing is done using the pyneng utility. Learn more about how to
work with the pyneng utility.
Task 19.1
Create a ping_ip_addresses function that checks if IP addresses are pingable. Checking IP addresses
should be done concurrent in different threads.
You can create any additional functions to complete the task. To check the availability of an IP
address, use ping.
Note: A hint about working with concurrent.futures: If you need to ping several IP addresses in
different threads, you need to create a function that will ping one IP address, and then run this
function in different threads for different IP addresses using concurrent.futures (this last part must
be done in the ping_ip_addresses function).
Task 19.2
Create a send_show_command_to_devices function that sends the same show command to different
devices in concurrent threads and then writes the output of the commands to a file. The output from
the devices in the file can be in any order.
Function parameters:
• filename - is the name of a text file to which the output of all commands will be written
The output of the commands should be written to a plain text file in this format (before the output
of the command, you must write the hostname and the command itself):
R1#sh ip int br
Interface IP-Address OK? Method Status ␣
,→Protocol
Check the operation of the function on devices from the devices.yaml file.
Task 19.3
Create a send_command_to_devices function that sends different show commands to different de-
vices in concurrent threads and then writes the output of the commands to a file. The output from
the devices in the file can be in any order.
Function parameters:
• commands_dict - a dictionary that specifies which device to send which command. Dictionary
example - commands
• filename is the name of the file to which the output of all commands will be written
The output of the commands should be written to a plain text file in this format (before the output
of the command, you must write the hostname and the command itself):
R1#sh ip int br
Interface IP-Address OK? Method Status ␣
,→Protocol
Check the operation of the function on devices from the devices.yaml file.
Task 19.3a
Create a send_command_to_devices function that sends a list of the specified show commands to
different devices in concurrent threads, and then writes the output of the commands to a file. The
output from the devices in the file can be in any order.
Function parameters:
• commands_dict - a dictionary that specifies which device to send which commands. Dictionary
example - commands
• filename is the name of the file to which the output of all commands will be written
The output of the commands should be written to a plain text file in this format (before the output
of the command, you must write the hostname and the command itself):
R2#sh arp
Protocol Address Age (min) Hardware Addr Type Interface
Internet 192.168.100.1 87 aabb.cc00.6500 ARPA Ethernet0/0
Internet 192.168.100.2 - aabb.cc00.6600 ARPA Ethernet0/0
R1#sh ip int br
Interface IP-Address OK? Method Status ␣
,→Protocol
Commands can be written to a file in any order. To complete the task, you can create any additional
functions, as well as use the functions created in previous tasks.
Check the operation of the function on devices from the devices.yaml file and the commands dic-
tionary
Task 19.4
Function parameters:
• filename is the name of the file to which the output of all commands will be written
• show - the show command to be sent (by default, the value is None)
The show, config and limit arguments should only be passed as keyword arguments. Passing these
arguments as positional should raise a TypeError exception.
When calling the send_commands_to_devices function, only one of the show, config arguments
should always be passed. If both arguments are passed, a ValueError exception should be raised.
The output of the commands should be written to a plain text file in this format (before the output
of the command, you must write the hostname and the command itself):
R1#sh ip int br
Interface IP-Address OK? Method Status ␣
,→Protocol
In [13]: commands = ['router ospf 55', 'network 0.0.0.0 255.255.255.255 area 0']
Jinja2 is a template language used in Python. Jinja is not the only template language (template
engine) for Python and not the only template language in general.
Examples of use:
The main idea of Jinja is to separate data and template. This allows you to use the same template
but not the same data. In the simplest case, template is simply a text file that specifies locations of
Jinja variables.
hostname {{name}}
!
interface Loopback255
description Management loopback
ip address 10.255.{{id}}.1 255.255.255.255
!
interface GigabitEthernet0/0
description LAN to {{name}} sw1 {{int}}
ip address {{ip}} 255.255.255.0
!
router ospf 10
router-id 10.255.{{id}}.1
auto-cost reference-bandwidth 10000
network 10.0.0.0 0.255.255.255 area 0
Comments to template:
• When script is executed, these variables are replaced with desired values.
This template can be used to generate configuration of different devices by substituting other sets
of variables.
hostname {{name}}
!
interface Loopback10
description MPLS loopback
ip address 10.10.{{id}}.1 255.255.255.255
!
interface GigabitEthernet0/0
description WAN to {{name}} sw1 G0/1
!
interface GigabitEthernet0/0.1{{id}}1
description MPLS to {{to_name}}
encapsulation dot1Q 1{{id}}1
ip address 10.{{id}}.1.2 255.255.255.252
ip ospf network point-to-point
ip ospf hello-interval 1
ip ospf cost 10
!
interface GigabitEthernet0/1
description LAN {{name}} to sw1 G0/2 !
interface GigabitEthernet0/1.{{IT}}
description PW IT {{name}} - {{to_name}}
encapsulation dot1Q {{IT}}
xconnect 10.10.{{to_id}}.1 {{id}}11 encapsulation mpls
backup peer 10.10.{{to_id}}.2 {{id}}21
backup delay 1 1
!
interface GigabitEthernet0/1.{{BS}}
description PW BS {{name}} - {{to_name}}
encapsulation dot1Q {{BS}}
xconnect 10.10.{{to_id}}.1 {{to_id}}{{id}}11 encapsulation mpls
backup peer 10.10.{{to_id}}.2 {{to_id}}{{id}}21
backup delay 1 1
!
(continues on next page)
router ospf 10
router-id 10.10.{{id}}.1
auto-cost reference-bandwidth 10000
network 10.0.0.0 0.255.255.255 area 0
!
- id: 11
name: Liverpool
to_name: LONDON
IT: 791
BS: 1550
to_id: 1
- id: 12
name: Bristol
to_name: LONDON
IT: 793
BS: 1510
to_id: 1
- id: 14
name: Coventry
to_name: Manchester
IT: 892
BS: 1650
to_id: 2
env = Environment(loader=FileSystemLoader('templates'))
template = env.get_template('router_template.txt')
with open('routers_info.yml') as f:
routers = yaml.safe_load(f)
f.write(template.render(router))
So far, only variable substitution has been used in Jinja2 template examples. This is the simplest
and most understandable example of using templates. Syntax of Jinja templates is not limited to
this.
• variables
• conditions (if/else)
• loops (for)
In addition, Jinja supports inheritance between templates and also allows adding the contents of
one template to another. This section covers only few possibilities. More information about Jinja2
templates can be found in documentation.
Note: All files used as examples in this subsection are in 3_template_syntax/ directory
vars_file = sys.argv[2]
env = Environment(
loader=FileSystemLoader(template_dir),
trim_blocks=True,
lstrip_blocks=True)
template = env.get_template(template_file)
with open(vars_file) as f:
vars_dict = yaml.safe_load(f)
print(template.render(vars_dict))
In order to see the result, you have to call the script and give it two arguments:
• template
trim_blocks, lstrip_blocks
Parameter trim_blocks removes the first empty line after block if its value is True (default False).
env = Environment(loader=FileSystemLoader(TEMPLATE_DIR))
By default, the same behavior will be with any other Jinja blocks.
env = Environment(loader=FileSystemLoader(TEMPLATE_DIR),
trim_blocks=True)
In front of neighbor ... remote-as lines two spaces appeared. This is because there is a space in
front of for block. Once lstrip_blocks has been disabled, spaces and tabs in front of the block are
added to the first line of block.
This does not affect the next lines. Therefore, lines with neighbor ... update-source are dis-
played with one space.
Parameter lstrip_blocks controls whether spaces and tabs will be removed from the beginning of
line to the beginning of block (untill opening curly bracket).
If add lstrip_blocks=True:
env = Environment(loader=FileSystemLoader(TEMPLATE_DIR),
trim_blocks=True, lstrip_blocks=True)
For example, if lstrip_blocks is set to True in an environment, but must be disabled for the second
block in template (templates/flagenv_s2.txt file):
Plus sign after percent sign disables lstrip_blocks for the block, in this case, only in the beginning.
env = Environment(loader=FileSystemLoader(TEMPLATE_DIR))
Template templates/env_flags3.txt:
Note the minus at the beginning of second block. Minus removes all whitespace characters, in this
case, at the beginning of the block.
Try to add minus at the end of expressions describing the block and look at the result:
Variables
hostname {{ name }}
interface Loopback0
ip address 10.0.0.{{ id }} 255.255.255.255
Variable that is passed on in a dictionary may not only be a number or a string, but also for example,
a list or a dictionary. Inside template, you can refer to the item by number or key.
hostname {{ name }}
interface Loopback0
ip address 10.0.0.{{ id }} 255.255.255.255
vlan {{ vlans[0] }}
router ospf 1
router-id 10.0.0.{{ id }}
auto-cost reference-bandwidth 10000
network {{ ospf.network }} area {{ ospf['area'] }}
id: 3
name: R3
vlans:
- 10
- 20
- 30
ospf:
network: 10.0.1.0 0.0.0.255
area: 0
Note the use of vlans variable in template: since vlans variable is a list, you can specify which item
from list we need
If a dictionary is passed (as in case of ospf variable), you can refer to dictionary objects inside
template using one of the variants: ospf.network or ospf['network']
interface Loopback0
ip address 10.0.0.3 255.255.255.255
vlan 10
router ospf 1
router-id 10.0.0.3
auto-cost reference-bandwidth 10000
network 10.0.1.0 0.0.0.255 area 0
Loop for
Loop for must be written inside {% %}. Furthermore, the end of the loop must be explicitly indicated:
hostname {{ name }}
interface Loopback0
ip address 10.0.0.{{ id }} 255.255.255.255
router ospf 1
router-id 10.0.0.{{ id }}
auto-cost reference-bandwidth 10000
{% for networks in ospf %}
network {{ networks.network }} area {{ networks.area }}
{% endfor %}
id: 3
name: R3
(continues on next page)
vlans:
10: Marketing
20: Voice
30: Management
ospf:
- network: 10.0.1.0 0.0.0.255
area: 0
- network: 10.0.2.0 0.0.0.255
area: 2
- network: 10.1.1.0 0.0.0.255
area: 0
In for, it is possible to go through both the list elements (for example, ospf list) and the dictionary
(vlans dictionary). And similarly, through any sequence.
interface Loopback0
ip address 10.0.0.3 255.255.255.255
vlan 10
name Marketing
vlan 20
name Voice
vlan 30
name Management
router ospf 1
router-id 10.0.0.3
auto-cost reference-bandwidth 10000
network 10.0.1.0 0.0.0.255 area 0
network 10.0.2.0 0.0.0.255 area 2
network 10.1.1.0 0.0.0.255 area 0
if/elif/else
if allows you to add a condition to template. For example, you can use if to add parts of template
depending on the presence of variables in data dictionary.
if statement must also be within inside {% %}. End of condition must be explicitly stated:
{% if ospf %}
router ospf 1
router-id 10.0.0.{{ id }}
auto-cost reference-bandwidth 10000
{% endif %}
hostname {{ name }}
interface Loopback0
ip address 10.0.0.{{ id }} 255.255.255.255
{% if ospf %}
router ospf 1
router-id 10.0.0.{{ id }}
auto-cost reference-bandwidth 10000
{% for networks in ospf %}
network {{ networks.network }} area {{ networks.area }}
{% endfor %}
{% endif %}
if ospf expression works the same way as in Python: if variable exists and is not empty, the
result is True. If there is no variable or it is empty, the result is False. That is, in this template the
OSPF configuration is generated only if variable ospf exists and is not empty. Configuration will be
generated with two data variants.
id: 3
name: R3
vlans:
10: Marketing
20: Voice
30: Management
hostname R3
interface Loopback0
ip address 10.0.0.3 255.255.255.255
vlan 10
name Marketing
vlan 20
name Voice
vlan 30
name Management
id: 3
name: R3
vlans:
10: Marketing
20: Voice
30: Management
ospf:
- network: 10.0.1.0 0.0.0.255
area: 0
- network: 10.0.2.0 0.0.0.255
area: 2
- network: 10.1.1.0 0.0.0.255
area: 0
hostname R3
interface Loopback0
ip address 10.0.0.3 255.255.255.255
vlan 10
name Marketing
vlan 20
name Voice
vlan 30
name Management
router ospf 1
(continues on next page)
router-id 10.0.0.3
auto-cost reference-bandwidth 10000
network 10.0.1.0 0.0.0.255 area 0
network 10.0.2.0 0.0.0.255 area 2
network 10.1.1.0 0.0.0.255 area 0
trunks:
Fa0/1:
action: add
vlans: 10,20
Fa0/2:
action: only
vlans: 10,30
Fa0/3:
action: delete
vlans: 10
In this example, different commands are generated depending on value of action parameter.
In template you could also use this option to refer to nested dictionaries:
Using if you can also filter which elements of sequence will be iterated in for loop.
vlans:
10: Marketing
20: Voice
30: Management
Filters
In Jinja, variables can be changed by filters. Filters are separated from variable by a vertical line
(pipe |) and may contain additional arguments. In addition, several filters can be applied to variable.
In this case, filters are simply written consecutively and each of them is separated by a vertical line.
Jinja supports a large number of built-in filters. We will look at only a few of them. Other filters can
be found in documentation.
You can also easily create your own filters. We will not cover this possibility but it is well documented.
default
Filter default allows you to set default value for variable. If variable is defined, it will be displayed,
if variable is not defined, the value specified in default filter will be displayed.
router ospf 1
auto-cost reference-bandwidth {{ ref_bw | default(10000) }}
{% for networks in ospf %}
network {{ networks.network }} area {{ networks.area }}
{% endfor %}
If variable ref_bw is defined in dictionary, its value will be set. If there is no variable, the value of
10000 will be substituted.
ospf:
- network: 10.0.1.0 0.0.0.255
area: 0
- network: 10.0.2.0 0.0.0.255
area: 2
- network: 10.1.1.0 0.0.0.255
area: 0
By default, if variable is defined and its value is empty, it will be assumed that variable and its value
exist.
If you want default value to be set also when variable is empty (i.e., treated as False in Python), you
need to specify additional parameter boolean=true.
ref_bw: ''
ospf:
- network: 10.0.1.0 0.0.0.255
area: 0
- network: 10.0.2.0 0.0.0.255
area: 2
- network: 10.1.1.0 0.0.0.255
area: 0
If with the same data file the template will be changed as follows:
router ospf 1
auto-cost reference-bandwidth {{ ref_bw | default(10000, boolean=true) }}
{% for networks in ospf %}
network {{ networks.network }} area {{ networks.area }}
{% endfor %}
dictsort
Filter dictsort allows you to sort the dictionary. By default, sorting is done by keys but by changing
filter parameters you can sort by values.
Filter syntax:
After dictsort sorts the dictionary, it returns a list of tuples, not a dictionary.
trunks:
Fa0/2:
action: only
vlans: 10,30
Fa0/3:
action: delete
vlans: 10
Fa0/1:
action: add
vlans: 10,20
join
With join filter you can combine sequence of elements into a string with an optional separator
between elements.
trunks:
Fa0/1:
action: add
vlans:
- 10
- 20
Fa0/2:
action: only
vlans:
- 10
- 30
Fa0/3:
action: delete
vlans:
- 10
Tests
Besides filters, Jinja also supports tests. Tests allow variables to be tested for a certain condition.
Jinja supports a large number of built-in tests. We will look at only a few of them. The rest of tests
you can find in documentation.
defined
Test defined allows you to check if variable is present in the data dictionary.
router ospf 1
{% if ref_bw is defined %}
auto-cost reference-bandwidth {{ ref_bw }}
{% else %}
auto-cost reference-bandwidth 10000
{% endif %}
{% for networks in ospf %}
network {{ networks.network }} area {{ networks.area }}
{% endfor %}
This example is more cumbersome than default filter option, but this test may be useful if depend-
ing on whether a variable is defined or not, different commands need to be executed.
ospf:
- network: 10.0.1.0 0.0.0.255
area: 0
- network: 10.0.2.0 0.0.0.255
area: 2
- network: 10.1.1.0 0.0.0.255
area: 0
iterable
Test iterable checks whether the object is an iterator. Due to these checks, it is possible to make
branches in template which will take into account the type of variable.
trunks:
Fa0/1:
action: add
vlans:
- 10
- 20
Fa0/2:
action: only
vlans:
- 10
- 30
Fa0/3:
action: delete
vlans: 10
Note the last line: vlans: 10. In this case, 10 is no longer in the list and join filter does not work.
But, due to is iterable test (in this case the result will be false), in this case template goes into
else branch.
Such indents appeared because the template uses indents but does not have lstrip_blocks=True
installed (it removes spaces and tabs at the beginning of the line).
set
You can assign values to variables inside template. These can be new variables or there may be
modified values of variables that have been passed to template. In this way you can remember a
value that for example was obtained by using several filters. Then use variable name instead of
repeating all filters.
Template example templates/set.txt in which set expression is used to specify shorter parameter
names:
interface {{ intf }}
{% if vlans is iterable %}
{% if action == 'add' %}
switchport trunk allowed vlan add {{ vlans | join(',') }}
{% elif action == 'delete' %}
switchport trunk allowed vlan remove {{ vlans | join(',') }}
{% else %}
switchport trunk allowed vlan {{ vlans | join(',') }}
{% endif %}
{% else %}
{% if action == 'add' %}
switchport trunk allowed vlan add {{ vlans }}
{% elif action == 'delete' %}
switchport trunk allowed vlan remove {{ vlans }}
(continues on next page)
{% else %}
switchport trunk allowed vlan {{ vlans }}
{% endif %}
{% endif %}
{% endfor %}
In this way new variables are created and these new values are used. It makes template look clearer.
trunks:
Fa0/1:
action: add
vlans:
- 10
- 20
Fa0/2:
action: only
vlans:
- 10
- 30
Fa0/3:
action: delete
vlans: 10
interface Fa0/1
switchport trunk allowed vlan add 10,20
interface Fa0/2
switchport trunk allowed vlan 10,30
interface Fa0/3
switchport trunk allowed vlan remove 10
include
Variables that are transmitted as data must contain all data for both the master template and the
one that is added through include.
Template templates/vlans.txt:
Template templates/ospf.txt:
router ospf 1
auto-cost reference-bandwidth 10000
{% for networks in ospf %}
network {{ networks.network }} area {{ networks.area }}
{% endfor %}
Template templates/bgp.txt:
{% include 'vlans.txt' %}
{% include 'ospf.txt' %}
vlans:
10: Marketing
20: Voice
30: Management
ospf:
(continues on next page)
router ospf 1
auto-cost reference-bandwidth 10000
network 10.0.1.0 0.0.0.255 area 0
network 10.0.2.0 0.0.0.255 area 2
network 10.1.1.0 0.0.0.255 area 0
The resulting configuration is as if lines from templates ospf.txt and vlans.txt were in switch.txt
template.
Template templates/router.txt:
{% include 'ospf.txt' %}
{% include 'bgp.txt' %}
logging {{ log_server }}
In this case, in addition to include, another line in template was added to show that include ex-
pressions can be mixed with normal template.
ospf:
- network: 10.0.1.0 0.0.0.255
area: 0
- network: 10.0.2.0 0.0.0.255
area: 2
- network: 10.1.1.0 0.0.0.255
(continues on next page)
area: 0
bgp:
local_as: 100
loopback: lo100
ibgp_neighbors:
- 10.0.0.2
- 10.0.0.3
ebgp_neighbors:
90.1.1.1: 500
80.1.1.1: 600
log_server: 10.1.1.1
logging 10.1.1.1
Template inheritance
Template inheritance is a very powerful functionality that avoids repetition of the same in different
templates.
– this template may contain any ordinary expressions or text. In addition, special blocks
• child template - template that extends base template by filling in specified blocks.
!
{% block services %}
service timestamps debug datetime msec localtime show-timezone year
service timestamps log datetime msec localtime show-timezone year
service password-encryption
service sequence-numbers
{% endblock %}
!
no ip domain lookup
!
ip ssh version 2
!
{% block ospf %}
router ospf 1
auto-cost reference-bandwidth 10000
{% endblock %}
!
{% block bgp %}
{% endblock %}
!
{% block alias %}
{% endblock %}
!
line con 0
logging synchronous
history size 100
line vty 0 4
logging synchronous
history size 100
transport input ssh
!
{% block services %}
service timestamps debug datetime msec localtime show-timezone year
service timestamps log datetime msec localtime show-timezone year
(continues on next page)
service password-encryption
service sequence-numbers
{% endblock %}
!
{% block ospf %}
router ospf 1
auto-cost reference-bandwidth 10000
{% endblock %}
!
{% block bgp %}
{% endblock %}
!
{% block alias %}
{% endblock %}
These are blanks for the corresponding configuration sections. A child template that uses this base
template as a base can fill all or only some of the blocks.
{% extends "base_router.txt" %}
{% block ospf %}
{{ super() }}
{% for networks in ospf %}
network {{ networks.network }} area {{ networks.area }}
{% endfor %}
{% endblock %}
{% block alias %}
alias configure sh do sh
alias exec ospf sh run | s ^router ospf
alias exec bri show ip int bri | exc unass
alias exec id show int desc
alias exec top sh proc cpu sorted | excl 0.00%__0.00%__0.00%
alias exec c conf t
alias exec diff sh archive config differences nvram:startup-config system:running-
,→config
{% extends "base_router.txt" %}
It is said that template hq_router.txt will be constructed on the basis of template base_router.txt.
Inside child template, everything happens inside blocks. Due to the blocks that have been defined
in base template, child template can extend the parent template.
Note: Note that lines described in child template outside blocks are ignored.
There are four blocks in base template: services, ospf, bgp, alias. In child template only two of them
are filled: ospf and alias. That’s the convenience of inheritance. You don’t have to fill all blocks in
every child template.
In this way ospf and alias blocks are used differently. In base template, ospf block already has part
of configuration:
{% block ospf %}
router ospf 1
auto-cost reference-bandwidth 10000
{% endblock %}
Therefore, child template has a choice: use this configuration and supplement it or completely
rewrite everything in child template.
In this case the configuration is supplemented. That is why in child template templates/hq_router.txt
the ospf block starts with expression {{ super() }}:
{% block ospf %}
{{ super() }}
{% for networks in ospf %}
network {{ networks.network }} area {{ networks.area }}
{% endfor %}
{% endblock %}
{{ super() }} transfers content of this block from parent template to child template. Because of
this, lines from parent are moved to child template.
Note: Expression super doesn’t have to be at the beginning of the block. It could be anywhere in
the block. Content of base template are moved to where super expression is located.
alias block simply describes the alias. And even if there were some settings in parent template,
they would be substituted by content of child template.
Let’s recap the rules for working with blocks. If block is created in parent template:
• no content - in child template you can fill this block or ignore it. If block is filled, it will contain
only what was written in child template (example - alias block)
– ignore block - in this case, child template will get content from parent template (example
- services block)
– rewrite block - then child template will contain only what it has
– move content of the block from parent template and supplement it - then child template
will contain both the content of the block from parent template and the content from child
template. To pass content from parent template the expression {{ super() }} is used
(example - ospf block)
ospf:
- network: 10.0.1.0 0.0.0.255
area: 0
- network: 10.0.2.0 0.0.0.255
area: 2
- network: 10.1.1.0 0.0.0.255
area: 0
Note that in ospf block there are commands from base template and commands from child template.
Further reading
Documentation:
• Template syntax
Articles:
Tasks
Warning: Starting from section “4. Data types in Python” there are automated tests for testing
tasks. They help to check whether everything matches the task, and also give feedback on what
does not correspond to the task. As a rule, after the first period of adaptation to tests, it becomes
easier to do tasks with tests. Testing is done using the pyneng utility. Learn more about how to
work with the pyneng utility.
Task 20.1
Function parameters:
Check the operation of the function on the templates/for.txt template and data from the
data_files/for.yml file.
import yaml
Task 20.2
• templates/cisco_base.txt
• templates/alias.txt
• templates/eem_int_desc.txt
Test the template templates/cisco_router_base.txt using the generate_config function from task
20.1. Do not copy the code of the generate_config function.
Task 20.3
Create a template templates/ospf.txt based on the OSPF configuration in the cisco_ospf.txt file. A
configuration example is given to show the syntax.
The template must be created manually by copying parts of the config into the corresponding tem-
plate.
• interfaces on which to enable OSPF. The variable name is ospf_intf. In place of this variable, a
list of dictionaries with the following keys is expected:
For all interfaces in the ospf_intf list, you need to generate the following lines:
passive-interface x
For interfaces that are not passive, in interface configuration mode, you need to add the line:
ip ospf hello-interval 1
Check the resulting template templates/ospf.txt, against the data in the data_files/ospf.yml file, using
the generate_config function from task 20.1. Do not copy the code of the generate_config function.
The result should be a configuration of the following type (the commands in router ospf mode do
not have to be in this order, the main thing is that they are in the correct config section):
router ospf 10
router-id 10.0.0.1
auto-cost reference-bandwidth 20000
network 10.255.0.1 0.0.0.0 area 0
network 10.255.1.1 0.0.0.0 area 0
network 10.255.2.1 0.0.0.0 area 0
network 10.0.10.1 0.0.0.0 area 2
network 10.0.20.1 0.0.0.0 area 2
passive-interface Fa0/0.10
passive-interface Fa0/0.20
interface Fa0/1
ip ospf hello-interval 1
interface Fa0/1.100
ip ospf hello-interval 1
interface Fa0/1.200
ip ospf hello-interval 1
Задание 20.4
Create a template templates/add_vlan_to_switch.txt that will be used if you need to add a VLAN to
the switch.
The template must be created manually by copying parts of the config into the corresponding tem-
plate.
If VLAN needs to be added as access, you need to configure the interface mode and add it to VLAN:
interface Gi0/1
switchport mode access
switchport access vlan 5
For trunks, you only need to add VLANs to the allowed list:
interface Gi0/10
switchport trunk allowed vlan add 5
The variable names should be chosen based on the sample data in the
data_files/add_vlan_to_switch.yaml file.
Task 20.5
The templates/gre_ipsec_vpn_1.txt template creates the configuration for one side of the tunnel,
and templates gre_ipsec_vpn_2.txt for the other.
Examples of the final configuration that should be generated from templates in the files:
cisco_vpn_1.txt and cisco_vpn_2.txt.
Templates must be created manually by copying parts of the config into the corresponding tem-
plates.
Create a create_vpn_config function that uses these templates to generate a VPN configuration
based on the data in the data dictionary.
Function parameters:
• template1 - the name of the template file that creates the configuration for one side of the
tunnel
• template2 - the name of the template file that creates the configuration for the second side of
the tunnel
The function must return a tuple with two configurations (strings) that are derived from templates.
Examples of VPN configurations that the create_vpn_config function should return in the
cisco_vpn_1.txt and cisco_vpn_2.txt files.
data = {
'tun_num': 10,
'wan_ip_1': '192.168.100.1',
'wan_ip_2': '192.168.100.2',
'tun_ip_1': '10.0.1.1 255.255.255.252',
'tun_ip_2': '10.0.1.2 255.255.255.252'
}
Task 20.5a
Create a configure_vpn function that uses the templates from task 20.5 to configure VPN on routers
based on the data in the data dictionary.
Function parameters:
The function should configure the VPN based on templates and data on each device using netmiko.
The function returns a tuple with the output of commands from two routers (the output returned
by the netmiko send_config_set method). The first element of the tuple is the output from the first
device (string), the second element of the tuple is the output from the second device.
In this task, the data dictionary does not specify the Tunnel interface number to use. The number
must be determined independently based on information from the equipment. If the router does not
have Tunnel interfaces, take the number 0, if there is, take the nearest free number, but the same
for two routers.
For example, if the src router has the following interfaces: Tunnel1, Tunnel4. And on the dst router
are: Tunnel2, Tunnel3, Tunnel8. The first free number that is the same for two routers will be 5. And
you will need to configure the Tunnel 5 interface.
For this task, the test verifies that the function works on the first two devices from the devices.yaml
file. And it checks that the output contains commands for configuring interfaces, but does not check
the configured tunnel numbers and other commands. The tunnels must be configured, but the test
has been simplified so that there are fewer constraints on the task.
data = {
'tun_num': None,
'wan_ip_1': '192.168.100.1',
'wan_ip_2': '192.168.100.2',
'tun_ip_1': '10.0.1.1 255.255.255.252',
'tun_ip_2': '10.0.1.2 255.255.255.252'
}
On network devices that does not support any software interface, the output of show commands
is returned as a string. And although it’s partly structured, it’s still just a string. And it has to be
processed in some way to get Python objects, like a dictionary or a list.
For example, it is possible to handle the output of a command line by line using regular expressions
to get Python objects. But there is a more convenient option: TextFSM
TextFSM - a library created by Google to handle output from network devices. It allows you to create
templates to process the output of a command.
Using TextFSM is better than simple line processing, as templates give a better idea of how output
will be handled and templates are easier to share. That means it’s easier to find templates that have
already been created and use them or share your own.
To use TextFSM you should create a template to handle the output of command.
For example, you have to get hops that packet went through.
In this case TextFSM template will look like this (traceroute.template file):
Value ID (\d+)
Value Hop (\S+)
Start
^ ${ID} ${Hop} +\d+ -> Record
• Value ID (\d+) - this line defines an ID variable that describes a regular expression: (\d+) -
one or more digits, here are the hop numbers
• Value Hop (\S+) - line that defines a Hop variable that describes an IP address by such regular
expression
After Start line, the template itself begins. In this case, it’s very simple:
• first goes caret sign, then two spaces and ID and Hop variables
• word Record at the end means that lines that matches regular expression will be processed
and included in the results of TextFSM (we’ll look at this in the next section)
import textfsm
traceroute = '''
r2#traceroute 90.0.0.9 source 33.0.0.2
traceroute 90.0.0.9 source 33.0.0.2
Type escape sequence to abort.
Tracing the route to 90.0.0.9
VRF info: (vrf in name/id, vrf out name/id)
1 10.0.12.1 1 msec 0 msec 0 msec
2 15.0.0.5 0 msec 5 msec 4 msec
3 57.0.0.7 4 msec 1 msec 4 msec
4 79.0.0.9 4 msec * 1 msec
'''
print(fsm.header)
print(result)
$ python parse_traceroute.py
['ID', 'Hop']
[['1', '10.0.12.1'], ['2', '15.0.0.5'], ['3', '57.0.0.7'], ['4', '79.0.0.9']]
Lines that match the described template are returned as a list of lists. Each element is a list that
consists of two elements: hop number and IP address.
• At the end, print(fsm.header) header is displayed which contains variable names and pro-
cessing result
We can work with that output further. For example, periodically execute traceroute command and
compare whether the number of hops and their order has changed.
This section covers template syntax based on TextFSM documentation. The following section shows
examples of syntax usage. Therefore, you can move to the next section and return to this section
as necessary for those situations for which there is no example and when you need to recall the
meaning of parameter.
TextFSM template describes how data should be processed. Each template consists of two parts:
• value definitions (or variable definitions) - these variables describe which columns will be in
the table view
• state definitions
Value ID (\d+)
Value Hop (\d+(\.\d+){3})
Start
^ ${ID} ${Hop} -> Record
Value definition
Only value definitions should be used in value section. The only exception there may be comments
in this section. This section should not contain empty strings. For TextFSM, an empty string means
the end of value definition section.
Syntax of value description (for each option below we will consider examples):
• Value - keyword that indicates that a value is being created. It must be specified
• option - options that define how to work with a variable. If several options are to be specified,
they must be separated by a comma, without spaces. These options are supported:
– Filldown - value that previously matched with a regex, remembered until the next pro-
cessing line (if has not been explicitly cleared or matched again). This means that the last
column value that matches regex is stored and used in the following strings if this column
is not present.
– Required - string that is processed will only be written if this value is present.
– List - value is a list, and each match with a regex will add an item to the list. By default,
each next match overwrites the previous one.
– Fillup - works as Filldown but fills empty value up until it finds a match. Not compatible
with Required.
• name - name of value that will be used as column name. Reserved names should not be used
as value names.
State definition
• each state definition must be separated by an empty line (at least one)
• then follows lines that describe rules. Rules must start with two spaces and caret symbol ^
Initial state is always Start. Input data is compared to the current state but rule line can specify
that you want to go to a different state.
Checking is done line-by-line until EOF (end of file) is reached or the current state goes to End state.
Reserved states
• Start - state that must be specified. Without it the template won’t work.
• End - state that completes processing of incoming strings and does not execute EOF state.
• EOF - implicit state that always executes when processing reaches the end of the file. It looks
like this:
EOF
^.* -> Record
EOF writes down the current string before it is finished. If this behavior needs to be changed you
should explicitly write EOF at the end of template:
EOF
State rules
• if rule (regex) matches the string, actions in rule are executed and for the next string the
process is repeated from the beginning of state.
In rule:
• each rule must start with two spaces and caret symbol ^. Caret symbol ^ means the beginning
of a line and must always be clearly indicated
– to specify variable you can use syntax like $ValueName or ${ValueName}(preferred format)
Action in rules
Line Actions
Line Actions:
• Next - process the line, read the next line and start checking it from the beginning. This action
is used by default unless otherwise specified
• Continue - continue to process rules as if there was no match while values are assigned
Record Action
Record Action - optional action that can be specified after Line Action. They must be separated by
a dot. Types of actions:
• Record - all variables except those with Filldown option are reset.
• Clear - reset all variables except those where Filldown option is specified.
You need to split actions with a dot only if you want to specify both Line and Record actions. If you
need to specify only one of them, dot is not required.
State Transition
– then the current state changes to a new state and processing continues in new state.
If rule uses Continue action, it is not possible to change state inside this rule. This rule is needed
to avoid loops in sequence of states.
Error Action
Error stops all line processing, discards all lines that have been collected so far and returns an
exception.
Section uses parse_output.py script to process command output by template. It is not tied to a
specific template and output: template and command output will be passed as arguments:
import sys
import textfsm
from tabulate import tabulate
template = sys.argv[1]
output_file = sys.argv[2]
Note: Module tabulate is used to output data in tabular form (it must be installed if you want
to use this script). A similar output could be received with string formatting but with tabulate it is
easier to do.
Data processing by template is always done in the same way. Therefore, script will be the same only
template and data will be different.
Starting with a simple example we’ll figure out how to use TextFSM.
show clock
The first example is a review of “sh clock” command output (output/sh_clock.txt file):
• . - any character
• \d - any number
Once variables are defined, an empty line and Start state must follow, and then the rule follows
starting with space and ^ symbol (templates/sh_clock.template file):
Start
^${Time}.* ${Timezone} ${WeekDay} ${Month} ${MonthDay} ${Year} -> Record
Because in this case only one line in the output, it is not necessary to write Record action in template.
But it is better to use it in situations where you have to write values and get used to this syntax and
not make mistakes when you need to process multiple lines.
When TextFSM handles output strings it substitutes variable by its values. In the end, rule will look
like:
When this regex applies to “show clock” output, each regex group will have a corresponding value:
• 1 group: 15:10:44
• 2 group: UTC
• 3 group: Sun
• 4 group: Nov
• 5 group: 13
• 6 group: 2016
In addition to explicit Record action which specifies that record should be placed in final table, the
Next rule is also used by default. It specifies that you want to go to the next line of text. Since there
is only one line in “sh clock” command output, the processing is completed.
In case when you need to process data displayed in columns, TextFSM template is the most conve-
nient.
Start
^${INTF}\s+${ADDR}\s+\w+\s+\w+\s+${STATUS}\s+${PROTO} -> Record
In this case, the rule can be written in one line. Output command (output/sh_ip_int_br.txt file):
Now try to process output of command “show cdp neighbors detail”. Peculiarity of this command is
that the data are not in the same line but in different lines.
Version :
Cisco IOS Software, C2960 Software (C2960-LANBASEK9-M), Version 12.2(55)SE9,␣
,→RELEASE SOFTWARE (fc1)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2014 by Cisco Systems, Inc.
Compiled Mon 03-Mar-14 22:53 by prod_rel_team
advertisement version: 2
VTP Management Domain: ''
Native VLAN: 1
Duplex: full
Management address(es):
IP address: 10.1.1.2
-------------------------
Device ID: R1
Entry address(es):
IP address: 10.1.1.1
Platform: Cisco 3825, Capabilities: Router Switch IGMP
Interface: GigabitEthernet1/0/22, Port ID (outgoing port): GigabitEthernet0/0
Holdtime : 156 sec
Version :
Cisco IOS Software, 3800 Software (C3825-ADVENTERPRISEK9-M), Version 12.4(24)T1,␣
,→RELEASE SOFTWARE (fc3)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2009 by Cisco Systems, Inc.
Compiled Fri 19-Jun-09 18:40 by prod_rel_team
advertisement version: 2
VTP Management Domain: ''
Duplex: full
Management address(es):
-------------------------
Device ID: R2
Entry address(es):
IP address: 10.2.2.2
Platform: Cisco 2911, Capabilities: Router Switch IGMP
Interface: GigabitEthernet1/0/21, Port ID (outgoing port): GigabitEthernet0/0
Holdtime : 156 sec
(continues on next page)
Version :
Cisco IOS Software, 2900 Software (C3825-ADVENTERPRISEK9-M), Version 15.2(2)T1,␣
,→RELEASE SOFTWARE (fc3)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2009 by Cisco Systems, Inc.
Compiled Fri 19-Jun-09 18:40 by prod_rel_team
advertisement version: 2
VTP Management Domain: ''
Duplex: full
Management address(es):
Start
^${LOCAL_HOST}[>#].
^Device ID: ${DEST_HOST}
^.*IP address: ${MGMNT_IP}
^Platform: ${PLATFORM},
^Interface: ${LOCAL_PORT}, Port ID \(outgoing port\): ${REMOTE_PORT}
^.*Version ${IOS_VERSION},
Although rules with variables are described in different lines and accordingly work with different
lines, TextFSM collects them into one line of the table. That is, variables that are defined at the
beginning of template determine the string of resulting table.
Note that sh_cdp_n_det.txt file has three neighbors, but table has only one neighbor, the last one.
Record
This is because Record action is not specified in template. And only the last line left in final table.
Corrected template:
Start
^${LOCAL_HOST}[>#].
^Device ID: ${DEST_HOST}
^.*IP address: ${MGMNT_IP}
^Platform: ${PLATFORM},
^Interface: ${LOCAL_PORT}, Port ID \(outgoing port\): ${REMOTE_PORT}
^.*Version ${IOS_VERSION}, -> Record
Output from all three devices. But LOCAL_HOST variable is not displayed in every line, only in the
first one.
Filldown
This is because the prompt from which variable value is taken appears only once. And in order to
make it appear in the next lines, use Filldown action for LOCAL_HOST variable:
Start
^${LOCAL_HOST}[>#].
^Device ID: ${DEST_HOST}
^.*IP address: ${MGMNT_IP}
^Platform: ${PLATFORM},
^Interface: ${LOCAL_PORT}, Port ID \(outgoing port\): ${REMOTE_PORT}
^.*Version ${IOS_VERSION}, -> Record
LOCAL_HOST now appears in all three lines. But there was another strange effect - the last line in
which only LOCAL_HOST column is filled.
Required
The thing is, all variables we’ve determined are optional. Also, one variable with Filldown param-
eter. And to get rid of the last line, you have to make at least one variable mandatory by using
Required option:
Start
^${LOCAL_HOST}[>#].
^Device ID: ${DEST_HOST}
^.*IP address: ${MGMNT_IP}
^Platform: ${PLATFORM},
^Interface: ${LOCAL_PORT}, Port ID \(outgoing port\): ${REMOTE_PORT}
^.*Version ${IOS_VERSION}, -> Record
Consider the case where we need to process output of “show ip route ospf” command and in routing
table there are several routes to the same network.
For routes to the same network, instead of multiple lines where network is repeated, one record will
be created in which all available next-hop addresses are in list.
For this example we simplify the task and assume that routes can only be OSPF and only with “O”
designation (i.e., only intra-zone routes).
Start
^O +${network}/${mask}\s\[${distance}/${metric}\]\svia\s${nexthop}, -> Record
All right, but we’ve lost path options for routes 10.4.4.4/32 and 10.5.5.5/32. This is logical, because
there is no rule that would be appropriate for such a line.
Start
^O +${network}/${mask}\s\[${distance}/${metric}\]\svia\s${nexthop}, -> Record
^\s+\[${distance}/${metric}\]\svia\s${nexthop}, -> Record
Partial entries are missing networks and masks, but in previous examples we have already covered
Filldown and, if desired, it can be applied here. But for this example we will use another option - List.
List
Start
^O +${network}/${mask}\s\[${distance}/${metric}\]\svia\s${nexthop}, -> Record
^\s+\[${distance}/${metric}\]\svia\s${nexthop}, -> Record
Now nexthop column displays a list but so far with one element. When using List the value is a list,
and each match with a regex will add an item to the list. By default, each next match overwrites
the previous one. If, for example, leave Record action for full lines only:
Start
^O +${network}/${mask}\s\[${distance}/${metric}\]\svia\s${nexthop}, -> Record
^\s+\[${distance}/${metric}\]\svia\s${nexthop},
Now the result is not quite correct, address hops are assigned to wrong routes. This happens because
writing is done on full route entry, then hops of incomplete route entries are collected in the list (other
variables are overwritten) and when the next full route entry appears, the list is written to it.
In fact, incomplete route entry should really be written when the next full route entry appears, but
at the same time they should be written to appropriate route. The following should be done: once
full route entry is met, the previous values should be written down and then continue to process the
same full route entry to get its information. In TextFSM, you can do this with Continue.Record:
^O -> Continue.Record
Here, Record action tells you to write down the current value of variables. Since there are no vari-
ables in this rule, what was in the previous values is written.
Continue action says to continue working with the current line as if there was no match. So, the
next line of template will work. The resulting template (templates/sh_ip_route_ospf.template):
Start
^O -> Continue.Record
^O +${network}/${mask}\s\[${distance}/${metric}\]\svia\s${nexthop},
^\s+\[${distance}/${metric}\]\svia\s${nexthop},
TextFSM is convenient to use to parse output that is displayed by columns or to process output
that is in different lines. Templates are less convenient when it is necessary to get several identical
elements from one line.
• list of all the ports in it. For example, ['Fa0/1', 'Fa0/2', 'Fa0/3']
The difficulty is that ports are in the same line and TextFSM cannot specify the same variable multiple
times in line. But it is possible to search multiple times for a match in a line.
Start
^\d+ +${CHANNEL}\(\S+ +[\w-]+ +[\w ]+ +${MEMBERS}\( -> Record
• MEMBERS - list of ports that are included in an aggregated port. List – type which is specified
for this variable.
CHANNEL MEMBERS
--------- ----------
Po1 ['Fa0/1']
Po3 ['Fa0/11']
So far, only the first port is in output but we need all ports to hit. In this case after match is found,
you should continue processing string with ports. That is, use Continue action and describe the
following expression.
The only line in template describes the first port. Add a line that describes the next port.
Start
^\d+ +${CHANNEL}\(\S+ +[\w-]+ +[\w ]+ +${MEMBERS}\( -> Continue
^\d+ +${CHANNEL}\(\S+ +[\w-]+ +[\w ]+ +\S+ +${MEMBERS}\( -> Record
The second line describes the same expression, but MEMBERS variable is moved to the next port.
CHANNEL MEMBERS
--------- --------------------
Po1 ['Fa0/1', 'Fa0/2']
Po3 ['Fa0/11', 'Fa0/12']
Similarly, lines that describe the third and fourth ports should be written to template. But, because
the output can have a different number of ports, you have to move Record rule to separate line so
that it is not tied to a specific number of ports in string.
For example, if Record is located after the line that describes four ports, for a situation with fewer
ports in the line the entry will not be executed.
Start
^\d+.* -> Continue.Record
^\d+ +${CHANNEL}\(\S+ +[\w-]+ +[\w ]+ +\S+ +${MEMBERS}\( -> Continue
^\d+ +${CHANNEL}\(\S+ +[\w-]+ +[\w ]+ +(\S+ +){2} +${MEMBERS}\( -> Continue
^\d+ +${CHANNEL}\(\S+ +[\w-]+ +[\w ]+ +(\S+ +){3} +${MEMBERS}\( -> Continue
CHANNEL MEMBERS
--------- ----------------------------------------
Po1 ['Fa0/1', 'Fa0/2', 'Fa0/3']
Po3 ['Fa0/11', 'Fa0/12', 'Fa0/13', 'Fa0/14']
The template assumes a maximum of four ports in line. If there are more ports, add the correspond-
ing lines to template.
Start
^\d+.* -> Continue.Record
^\d+ +${CHANNEL}\(\S+ +[\w-]+ +[\w ]+ +${MEMBERS}\( -> Continue
^\d+ +${CHANNEL}\(\S+ +[\w-]+ +[\w ]+ +\S+ +${MEMBERS}\( -> Continue
^\d+ +${CHANNEL}\(\S+ +[\w-]+ +[\w ]+ +(\S+ +){2} +${MEMBERS}\( -> Continue
^\d+ +${CHANNEL}\(\S+ +[\w-]+ +[\w ]+ +(\S+ +){3} +${MEMBERS}\( -> Continue
^ +${MEMBERS} -> Continue
^ +\S+ +${MEMBERS} -> Continue
^ +(\S+ +){2} +${MEMBERS} -> Continue
^ +(\S+ +){3} +${MEMBERS} -> Continue
CHANNEL MEMBERS
--------- ------------------------------------------------------------
Po1 ['Fa0/1', 'Fa0/2', 'Fa0/3']
Po3 ['Fa0/11', 'Fa0/12', 'Fa0/13', 'Fa0/14', 'Fa0/15', 'Fa0/16']
Examples of templates for Cisco and other vendors can be seen in project ntc-ansible.
With TextFSM it is possible to process output of commands and get a structured result. However, it
is still necessary to manually specify which template will handle show commands each time TextFSM
is used.
It would be much more convenient to have some mapping between command and template so that
you can write a common script that performs connections to devices, sends commands, chooses
template and parse output according to template.
TextFSM has such feature. To use it, you should create a file that describes mapping between com-
mands and templates. In TextFSM it is called index.
This file should be in a directory with templates and should have this format:
• mandatory columns with fixed position (mandatory first and last, respectively):
– last column - the corresponding command. This column uses a special format to describe
that a command may not be fully written
– in example below there are columns Hostname, Vendor. They allow you to refine your
device information to determine which template to use. For example, show version com-
mand may be in Cisco and HP devices. Of course, having only commands are not sufficient
to determine which template to use. In this case, you can pass information about the type
of equipment used with command and then you can define the correct template.
• all columns except the first column support regular expressions. Regular expressions are not
supported inside [[]]
Note how commands are written: sh[[ow]] ip int[[erface]] br[[ief]]. Record will be con-
verted to sh((ow)?)? ip int((erface)?)? br((ief)?)?. This means that TextFSM will be able to
determine which template to use even if command is not fully written. For example, such command
variants will work:
• sh ip int br
Let’s see how to use clitable class and index file. templates directory contains such templates
and index file:
sh_cdp_n_det.template
sh_clock.template
sh_ip_int_br.template
(continues on next page)
sh_ip_route_ospf.template
index
First we try to work with CLI Table in ipython to see what features this class has and then we look at
the final script.
Warning: There are different ways to import clitable depending on textfsm version:
We will check clitable on the last example from previous section - “show ip route ospf” command.
Read the output that is stored in output/sh_ip_route_ospf.txt file to string:
First, you should initialize a class by giving it name of file in which mapping between templates and
commands is stored, and specify name of directory in which templates are stored:
Specify which command should be passed and specify additional attributes that will help to identify
template. To do this, you should create a dictionary in which keys are names of columns that are
defined in index file. In this case, it is not necessary to specify vendor name, since “sh ip route ospf”
command corresponds to only one template.
Command output and dictionary with parameters should be passed to ParseCmd method:
As a result we have processed output of “sh ip route ospf” command in cli_table object.
In [6]: cli_table.
cli_table.AddColumn cli_table.NewRow cli_table.index ␣
,→cli_table.size
In [7]: print(cli_table)
Network, Mask, Distance, Metric, NextHop
10.0.24.0, /24, 110, 20, ['10.0.12.2']
10.0.34.0, /24, 110, 20, ['10.0.13.3']
10.2.2.2, /32, 110, 11, ['10.0.12.2']
10.3.3.3, /32, 110, 11, ['10.0.13.3']
10.4.4.4, /32, 110, 21, ['10.0.13.3', '10.0.12.2', '10.0.14.4']
10.5.35.0, /24, 110, 20, ['10.0.13.3']
In [8]: print(cli_table.FormattedTable())
Network Mask Distance Metric NextHop
====================================================================
10.0.24.0 /24 110 20 10.0.12.2
10.0.34.0 /24 110 20 10.0.13.3
10.2.2.2 /32 110 11 10.0.12.2
10.3.3.3 /32 110 11 10.0.13.3
10.4.4.4 /32 110 21 10.0.13.3, 10.0.12.2, 10.0.14.4
10.5.35.0 /24 110 20 10.0.13.3
To get a structured output from cli_table object, such as a list of lists, you have to refer to object in
this way:
In [11]: data_rows
Out[11]:
[['10.0.24.0', '/24', '110', '20', ['10.0.12.2']],
['10.0.34.0', '/24', '110', '20', ['10.0.13.3']],
['10.2.2.2', '/32', '110', '11', ['10.0.12.2']],
['10.3.3.3', '/32', '110', '11', ['10.0.13.3']],
['10.4.4.4', '/32', '110', '21', ['10.0.13.3', '10.0.12.2', '10.0.14.4']],
['10.5.35.0', '/24', '110', '20', ['10.0.13.3']]]
In [14]: header
Out[14]: ['Network', 'Mask', 'Distance', 'Metric', 'NextHop']
import clitable
output_sh_ip_route_ospf = open('output/sh_ip_route_ospf.txt').read()
cli_table.ParseCmd(output_sh_ip_route_ospf, attributes)
print('CLI Table output:\n', cli_table)
print(header)
for row in data_rows:
print(row)
In exercises to this section there will be a task to combine described procedure into a function and
task to get a list of dictionaries.
$ python textfsm_clitable.py
CLI Table output:
Network, Mask, Distance, Metric, NextHop
10.0.24.0, /24, 110, 20, ['10.0.12.2']
10.0.34.0, /24, 110, 20, ['10.0.13.3']
10.2.2.2, /32, 110, 11, ['10.0.12.2']
10.3.3.3, /32, 110, 11, ['10.0.13.3']
10.4.4.4, /32, 110, 21, ['10.0.13.3', '10.0.12.2', '10.0.14.4']
10.5.35.0, /24, 110, 20, ['10.0.13.3']
Formatted Table:
Network Mask Distance Metric NextHop
====================================================================
10.0.24.0 /24 110 20 10.0.12.2
10.0.34.0 /24 110 20 10.0.13.3
10.2.2.2 /32 110 11 10.0.12.2
10.3.3.3 /32 110 11 10.0.13.3
10.4.4.4 /32 110 21 10.0.13.3, 10.0.12.2, 10.0.14.4
10.5.35.0 /24 110 20 10.0.13.3
Now with TextFSM it is possible not only to get a structured output, but also to automatically deter-
mine which template to use by command and optional arguments.
Further reading
Documentation:
• TextFSM
Articles:
• Programmatic Access to CLI Devices with TextFSM. Jason Edelman (26.02.2015) - TextFSM ba-
sics and development ideas that formed the basis of ntc-ansible module
• Parse CLI outputs with TextFSM. Henry Ölsner (24.08.2015) - an example of using TextFSM to
parse a large file with sh inventory output. Explains TextFSM syntax in more detail
• TextFSM and Structured Data. Kirk Byers (22.10.2015) - an introductory article on TextFSM.
This does not describe syntax but gives an overview of what TextFSM is and an example of its
use
• Module ntc-ansible
• ntc-templates
Tasks
Warning: Starting from section “4. Data types in Python” there are automated tests for testing
tasks. They help to check whether everything matches the task, and also give feedback on what
does not correspond to the task. As a rule, after the first period of adaptation to tests, it becomes
easier to do tasks with tests. Testing is done using the pyneng utility. Learn more about how to
work with the pyneng utility.
Task 21.1
• template - name of the file containing the TextFSM template. For example tem-
plates/sh_ip_int_br.template
• the rest of the items are lists, which contain the results of processing the output of the show
command
Check the operation of the function on the output of the sh ip int br command from the equipment
and on the templates/sh_ip_int_br.template template.
Task 21.1a
Function parameters:
• template is the name of the file containing the TextFSM template. For example tem-
plates/sh_ip_int_br.template
Check the operation of the function on the output of the command output/sh_ip_int_br.txt and the
template templates/sh_ip_int_br.template.
Task 21.2
Create a TextFSM template to parse the output of the sh ip dhcp snooping binding command and
write it to templates/sh_ip_dhcp_snooping.template
The template should process and return the values of such columns:
• mac - 00:04:A3:3E:5B:69
• ip - 10.1.10.6
• vlan - 10
• intf - FastEthernet0/10
Check the work of the template using the parse_command_output function from task 21.1.
Task 21.3
Function parameters:
– ‘Command’: command
– ‘Vendor’: vendor
• index_file is the name of the file where the correspondence between commands and templates
is stored. The default is “index”
The function should return a list of dicts with the results of parsing the command output (as in task
21.1a):
Task 21.4
Function parameters:
The function should connect to one device, send a show command using netmiko, and then parse
the command output using TextFSM.
The function should return a list of dictionaries with the results of parsing the command output (as
in task 21.1a):
Check the operation of the function using the output of the sh ip int br command and devices from
devices.yaml.
Task 21.5
• command - command
• keys - the IP address of the device from which the output was received
Dictionary example:
Check the operation of the function using the output of the sh ip int br command and devices from
devices.yaml.
It is possible to write code without using OOP, but at a minimum learning of OOP basics will help to
better understand what an object, class, method, variable are. These are things that are used in
Python all the time. In addition, knowledge of OOP will be useful in reading someone else’s code.
For example, it will be easier to understand netmiko code.
537
Python for network engineers, Release 1.0
OOP basics
• Class - an element of a program that describes some data type. Class describes a template
for creating objects, typically specifies variables of object and actions that can be performed
on object.
• Method - a function that is defined within a class and describes an action that class supports
• Class variable - data that refer to class and shared by all class instances
• Instance attribute - variables and methods that refer to objects (instances) created on the basis
of a class. Every object has its own copy of attributes.
• Features such as color of house, number of windows - instance variables (of this particular
house)
For example, when working with netmiko, the first thing to do was create connection:
device = {
"device_type": "cisco_ios",
"host": "192.168.100.1",
"username": "cisco",
"password": "cisco",
"secret": "cisco",
}
ssh = ConnectHandler(**device)
The ssh variable is an object that represents the real connection to equipment. Thanks to the type
function, you can find out by an instance what class is the ssh object:
In [3]: type(ssh)
Out[3]: netmiko.cisco.cisco_ios.CiscoIosSSH
ssh has its own methods and variables that depend on the state the current object. For example, the
instance variable ssh.host is available for every instance of the class netmiko.cisco.cisco_ios.
CiscoIosSSH and returns IP address or hostname, whichever is specified in the device dictionary:
In [4]: ssh.host
Out[4]: '192.168.100.1'
The enable method goes into enable mode and the ssh object saves state: before and after the
transition, a different prompt is visible:
In [6]: ssh.find_prompt()
Out[6]: 'R1>'
In [7]: ssh.enable()
Out[7]: 'enable\r\nPassword: \r\nR1#'
In [8]: ssh.find_prompt()
Out[8]: 'R1#'
This example illustrates important aspects of OOP: data integration, data handling and state preser-
vation.
Until now, when writing code, data and actions have been separated. Most often, actions are de-
scribed as functions, and data is passed as arguments to these functions. When a class is created,
data and actions are combined. Of course, these data and actions are linked. That is, those ac-
tions that are characteristic of an object of this type, and not some arbitrary actions, become class
methods.
For example, in an class instance str, all methods refer to working with this string:
In [10]: s = 'string'
In [11]: s.upper()
Out[11]: 'STRING'
Note: Class does not have to store a state - string is immutable data type and all methods return
new strings and do not change the original string.
Above, the following syntax is used when referring to instance attributes (variables and methods):
objectname.attribute. This entry s.lower() means: call lower method on s object. Calling
methods and variables is the same, but to call a method you have to add parentheses and pass all
necessary arguments.
Everything described has been used repeatedly in the book but now we will deal with formal termi-
nology.
Class creation
Note: Note that the basis is explained here given that the reader has no experience with OOP.
Some examples are not very correct from Python’s ideology point of view, but they help to better
understand how it works. At the end, an explanation is given of how this should be done in proper
way.
Keyword class is used in python to create classes. The easiest class you can create in Python:
Note: Class names: usually class names in Python are written in CamelCase format.
In [3]: print(sw1)
<__main__.Switch object at 0xb44963ac>
Using dot notation, it is possible to get values of instance variables, create new variables and assign
a new value to existing ones:
You can see value of instance variables using the same dot notation:
In [10]: sw1.model
Out[10]: 'Cisco 3850'
In [11]: sw2.model
Out[11]: 'Cisco 3750'
Method creation
Before we start dealing with class methods, let’s see an example of a function that waits as an argu-
ment an instance variable of Switch class and displays information about it using instance variables
hostname and model:
In [5]: info(sw1)
Hostname: sw1
Model: Cisco 3850
In info function, sw_obj awaits an instance of Switch class. Most likely, there is nothing new
about this example, because in the same way earlier we wrote functions that wait for a string as an
argument and then call some methods in this string.
This example will help you to understand info method that we will add to Switch class.
If you look closely, info method looks exactly like info function, only instead of sw_obj name the
self is used. Why there is a strange self name here will be explained later and in the meantime
we will see how to call info method:
In [19]: sw1.info()
Hostname: sw1
Model: Cisco 3850
In example above, first an instance of Switch class is created, then hostname and model variables
are added to instance and then info method is called. Method info outputs information about
switch using values that are stored in instance variables.
Method call is different from the function call: we do not pass a link to an instance of Switch class.
We don’t need that because we call method from instance itself. Another unclear thing - why we
wrote self then?
In [39]: sw1.info()
Hostname: sw1
Model: Cisco 3850
To this one:
In [38]: Switch.info(sw1)
Hostname: sw1
Model: Cisco 3850
In the second case, self parameter already makes more sense, it actually accepts the reference to
instance and displays information on this basis.
From objects usage point of view, it is more convenient to call methods using the first syntax version,
so it is almost always used.
Note: When a class instance method is called the instance reference is passed by the first argu-
ment. In this case, instance is passed implicitly but parameter must be stated explicitly.
This conversion is not a feature of user classes and works for embedded data types in the same way.
For example, standard way to call append method in the list is:
In [4]: a = [1, 2, 3]
In [5]: a.append(5)
In [6]: a
Out[6]: [1, 2, 3, 5]
The same can be done using the second option, calling through a class:
In [7]: a = [1, 2, 3]
In [8]: list.append(a, 5)
In [9]: a
Out[9]: [1, 2, 3, 5]
Parameter self
Parameter self was specified before in method definition, as well as when using instance variables
in method. Parameter self is a reference to a particular instance of class. Parameter self is not a
special name but an arrangement. Instead of self you can use a different name but you shouldn’t
do that.
...:
In [19]: sw1.info()
Hostname: sw1
Model: Cisco 3850
Warning: Although technically you can use another name but always use self.
In all “usual” methods of class the first parameter will always be self. Furthermore, creating an
instance variable within a class is also done via self.
...:
In [8]: sw1.interfaces
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-8-e6b457e4e23e> in <module>()
----> 1 sw1.interfaces
This variable does not exist because it exists only within method and visibility area of method is the
same as function. Even other methods of the same class do not see variables in other methods.
For list with interfaces to be available as a variable in instances, you have to assign value in
self.interfaces:
...:
...: def generate_interfaces(self, intf_type, number_of_intf):
...: interfaces = [f'{intf_type}{number}' for number in range(1,␣
,→number_of_intf + 1)]
...: self.interfaces = interfaces
...:
In [12]: sw1.interfaces
Out[12]: ['Fa1', 'Fa2', 'Fa3', 'Fa4', 'Fa5', 'Fa6', 'Fa7', 'Fa8', 'Fa9', 'Fa10']
Method __init__
For info method to work correctly the instance should have hostname and model variables. If these
variables are not available, an error will occur:
In [60]: sw2.info()
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-60-5a006dd8aae1> in <module>()
----> 1 sw2.info()
<ipython-input-57-30b05739380d> in info(self)
1 class Switch:
2 def info(self):
----> 3 print(f'Hostname: {self.hostname}\nModel: {self.model}')
Almost always, when an object is created it has some initial data. For example, to create a connection
to device with netmiko you have to pass connection parameters.
In Python these initial object data are specified in __init__. Method __init__ is executed after
Python has created a new instance and __init__ method is passed arguments with which instance
was created:
Note that each instance created from this class will have variables: self.model and self.hostname.
Now, when creating an instance of Switch class you have to specify hostname and model:
In [36]: sw1.info()
Hostname: sw1
Model: Cisco 3850
Note: __init__ method is sometimes called a class constructor, although technically in Python
__new__ method is executed first and then __init__. In most cases there is no necessety to create
__new__ method.
An important feature of __init__ method is that it should not return anything. Python will generate
an exception if it tries to do this.
Class example
class Network:
def __init__(self, network):
self.network = network
self.hosts = tuple(str(ip) for ip in ipaddress.ip_network(network).
,→hosts())
self.allocated = []
else:
raise ValueError(f"IP address {ip} does not belong to {self.network}")
In [3]: net1.allocate("10.1.1.1")
In [4]: net1.allocate("10.1.1.2")
In [5]: net1.allocated
Out[5]: ['10.1.1.1', '10.1.1.2']
In [6]: net1.allocate("10.1.1.100")
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-6-9a4157e02c78> in <module>
----> 1 net1.allocate("10.1.1.100")
15
In [7]: net1.hosts
Out[7]: ('10.1.1.1', '10.1.1.2', '10.1.1.3', '10.1.1.4', '10.1.1.5', '10.1.1.6')
Class namespace
Each method in class has its own local visibility area. This means that one class method does not see
variables of another class method. For variables to be available, you have to assign their instance
through self.name. Basically, method is a function tied to an object. Therefore, all nuances that
concern function apply to methods.
Variable instances are available in another method because instance itself is passed as a first argu-
ment to each method. In example below in __init__ method, hostname and model variables are
assigned to an instance and then used in info due to instance being passed as a first argument:
class Switch:
def __init__(self, hostname, model):
self.hostname = hostname
self.model = model
def info(self):
print(f'Hostname: {self.hostname}\nModel: {self.model}')
Class variables
Besides instance variables, there are also class variables. They are are created by specifying vari-
ables inside the class itself, not a method:
class Network:
all_allocated_ip = []
else:
raise ValueError(f"IP address {ip} does not belong to {self.network}")
• self.all_allocated_ip
• Network.all_allocated_ip
• type(self).all_allocated_ip
The self.all_allocated_ip option allows you to access the value of class variable or add an
element if the class variable is a mutable data type. The disadvantage of this option is that if in
the method you write self.all_allocated_ip = ..., instead of changing the class variable, an
instance variable will be created.
The option Network.all_allocated_ip will work correctly, but a small drawback this option is that
the class name is written manually. You can use the third option type(self).all_allocated_ip
instead, since type(self) returns a class.
Now the class has a variable all_allocated_ip which is written all IP addresses that are allocated on
the networks:
In [4]: net1.allocate("10.1.1.1")
...: net1.allocate("10.1.1.2")
...: net1.allocate("10.1.1.3")
...:
In [5]: net1.allocated
Out[5]: ['10.1.1.1', '10.1.1.2', '10.1.1.3']
In [7]: net2.allocate("10.2.2.1")
...: net2.allocate("10.2.2.2")
...:
In [9]: net2.allocated
Out[9]: ['10.2.2.1', '10.2.2.2']
In [10]: Network.all_allocated_ip
Out[10]: ['10.1.1.1', '10.1.1.2', '10.1.1.3', '10.2.2.1', '10.2.2.2']
The variable is accessible not only through the class, but also through the instances:
In [40]: Network.all_allocated_ip
Out[40]: ['10.1.1.1', '10.1.1.2', '10.1.1.3', '10.2.2.1', '10.2.2.2']
In [41]: net1.all_allocated_ip
Out[41]: ['10.1.1.1', '10.1.1.2', '10.1.1.3', '10.2.2.1', '10.2.2.2']
In [42]: net2.all_allocated_ip
Out[42]: ['10.1.1.1', '10.1.1.2', '10.1.1.3', '10.2.2.1', '10.2.2.2']
Tasks
Warning: Starting from section “4. Data types in Python” there are automated tests for testing
tasks. They help to check whether everything matches the task, and also give feedback on what
does not correspond to the task. As a rule, after the first period of adaptation to tests, it becomes
easier to do tasks with tests. Testing is done using the pyneng utility. Learn more about how to
work with the pyneng utility.
Task 22.1
When creating an instance of a class, a dictionary that describes the topology is passed as an argu-
ment. The dictionary may contain “duplicate” connections. “Duplicate” connections are a situation
when there are two connections in the dictionary:
The task is to leave only one of these links in the final dictionary, no matter which one.
In each instance, a topology instance variable must be created, which contains the topology dictio-
nary, but already without “duplicates”. The topology instance variable should contain a dict without
“duplicates” immediately after instance creation.
In [3]: top.topology
Out[3]:
{('R1', 'Eth0/0'): ('SW1', 'Eth0/1'),
('R2', 'Eth0/0'): ('SW1', 'Eth0/2'),
('R2', 'Eth0/1'): ('SW2', 'Eth0/11'),
('R3', 'Eth0/0'): ('SW1', 'Eth0/3'),
('R3', 'Eth0/1'): ('R4', 'Eth0/0'),
('R3', 'Eth0/2'): ('R5', 'Eth0/0')}
Task 22.1a
Copy the Topology class from task 22.1 and modify it.
Transfer the functionality of removing “duplicates” to the _normalize method. In this case, the
__init__ method should look like this:
class Topology:
def __init__(self, topology_dict):
self.topology = self._normalize(topology_dict)
Task 22.1b
Copy the Topology class from either task 22.1a or 22.1 and modify it.
Add a delete_link method that deletes the specified connection. The method should also remove
the “reverse” connection, if any (an example is given below).
If there is no such link, the message “There is no such link” should be printed.
Topology creation:
In [7]: t = Topology(topology_example)
In [8]: t.topology
Out[8]:
{('R1', 'Eth0/0'): ('SW1', 'Eth0/1'),
('R2', 'Eth0/0'): ('SW1', 'Eth0/2'),
('R2', 'Eth0/1'): ('SW2', 'Eth0/11'),
('R3', 'Eth0/0'): ('SW1', 'Eth0/3'),
('R3', 'Eth0/1'): ('R4', 'Eth0/0'),
('R3', 'Eth0/2'): ('R5', 'Eth0/0')}
Removing a link:
In [10]: t.topology
Out[10]:
{('R1', 'Eth0/0'): ('SW1', 'Eth0/1'),
('R2', 'Eth0/0'): ('SW1', 'Eth0/2'),
('R2', 'Eth0/1'): ('SW2', 'Eth0/11'),
('R3', 'Eth0/0'): ('SW1', 'Eth0/3'),
('R3', 'Eth0/2'): ('R5', 'Eth0/0')}
Deleting the “reverse” link: the dictionary contains an entry ('R3', 'Eth0/2'): ('R5', 'Eth0/
0'), but calling the delete_link method specifying the key and value in reverse order ('R5', 'Eth0/
0'): ('R3', 'Eth0/2') should delete the connection:
In [12]: t.topology
Out[12]:
{('R1', 'Eth0/0'): ('SW1', 'Eth0/1'),
('R2', 'Eth0/0'): ('SW1', 'Eth0/2'),
('R2', 'Eth0/1'): ('SW2', 'Eth0/11'),
('R3', 'Eth0/0'): ('SW1', 'Eth0/3')}
Task 22.1c
Copy the Topology class from task 22.1b and modify it.
Add a delete_node method that deletes all connections to the specified device. If there is no such
device, the message “There is no such device” is printed.
Topology creation:
In [1]: t = Topology(topology_example)
In [2]: t.topology
Out[2]:
{('R1', 'Eth0/0'): ('SW1', 'Eth0/1'),
('R2', 'Eth0/0'): ('SW1', 'Eth0/2'),
('R2', 'Eth0/1'): ('SW2', 'Eth0/11'),
(continues on next page)
Removing a device:
In [3]: t.delete_node('SW1')
In [4]: t.topology
Out[4]:
{('R2', 'Eth0/1'): ('SW2', 'Eth0/11'),
('R3', 'Eth0/1'): ('R4', 'Eth0/0'),
('R3', 'Eth0/2'): ('R5', 'Eth0/0')}
In [5]: t.delete_node('SW1')
There is no such device
Task 22.1d
Copy the Topology class from task 22.1c and modify it.
Add the add_link method, which adds the specified link if it is not already in the topology. If the
connection exists, print the message “Such a connection already exists”, If one of the sides is in the
topology, display the message “A link to one of the ports exists”.
Topology creation:
In [7]: t = Topology(topology_example)
In [8]: t.topology
Out[8]:
{('R1', 'Eth0/0'): ('SW1', 'Eth0/1'),
('R2', 'Eth0/0'): ('SW1', 'Eth0/2'),
('R2', 'Eth0/1'): ('SW2', 'Eth0/11'),
('R3', 'Eth0/0'): ('SW1', 'Eth0/3'),
('R3', 'Eth0/1'): ('R4', 'Eth0/0'),
('R3', 'Eth0/2'): ('R5', 'Eth0/0')}
In [10]: t.topology
(continues on next page)
Out[10]:
{('R1', 'Eth0/0'): ('SW1', 'Eth0/1'),
('R1', 'Eth0/4'): ('R7', 'Eth0/0'),
('R2', 'Eth0/0'): ('SW1', 'Eth0/2'),
('R2', 'Eth0/1'): ('SW2', 'Eth0/11'),
('R3', 'Eth0/0'): ('SW1', 'Eth0/3'),
('R3', 'Eth0/1'): ('R4', 'Eth0/0'),
('R3', 'Eth0/2'): ('R5', 'Eth0/0')}
Task 22.2
When instantiating the class, a Telnet connection should be created, as well as the transition to
enable mode. The class must use the telnetlib module to connect via Telnet.
The CiscoTelnet class, in addition to __init__, must have at least two methods:
• _write_line - takes a string as an argument and sends the string converted to bytes to the
hardware and adds a line end character at the end. The _write_line method must be used
inside the class.
• send_show_command - takes the show command as an argument and returns the output re-
ceived from the device
• ip - IP address
• username - username
• password - password
In [3]: r1_params = {
...: 'ip': '192.168.100.1',
(continues on next page)
In [4]: r1 = CiscoTelnet(**r1_params)
Note: The _write_line method is needed in order to be able to shorten a line: self.telnet.
write(line.encode("ascii") + b"\n")
to this: self._write_line(line)
Task 22.2a
Copy the CiscoTelnet class from job 22.2 and modify the send_show_command method by adding
three parameters:
• parse - controls what will be returned: normal command output or a list of dicts received after
parsing command output using TextFSM. If parse=True, a list of dicts should be returned, and
parse=False normal output. The default is True.
• index is the name of the file where the correspondence between commands and templates is
stored. The default is “index”
In [1]: r1_params = {
...: 'ip': '192.168.100.1',
...: 'username': 'cisco',
...: 'password': 'cisco',
...: 'secret': 'cisco'}
(continues on next page)
In [3]: r1 = CiscoTelnet(**r1_params)
Task 22.2b
Copy the CiscoTelnet class from task 22.2a and add the send_config_commands method.
The send_config_commands method must be able to send one configuration mode command and
a list of commands. The method should return output similar to the send_config_set method of
netmiko (example output below).
In [2]: r1_params = {
...: 'ip': '192.168.100.1',
...: 'username': 'cisco',
(continues on next page)
In [3]: r1 = CiscoTelnet(**r1_params)
Out[6]: 'conf t\r\nEnter configuration commands, one per line. End with CNTL/Z.
,→\r\nR1(config)#interface loop55\r\nR1(config-if)#ip address 5.5.5.5 255.255.255.
,→255\r\nR1(config-if)#end\r\nR1#'
Task 22.2c
Copy the CiscoTelnet class from task 22.2b and modify the send_config_commands method to check
for errors.
• strict=True means that when an error is encountered, a ValueError must be raised (default)
• strict=False means that when an error is found, you only need to print the error message to
the stdout
The method should return output similar to the send_config_set method of netmiko (example output
below). The text of the exception and error in the example below.
In [2]: r1_params = {
...: 'ip': '192.168.100.1',
...: 'username': 'cisco',
...: 'password': 'cisco',
...: 'secret': 'cisco'}
In [3]: r1 = CiscoTelnet(**r1_params)
R1(config)#logging
% Incomplete command.
R1(config)#a
% Ambiguous command: "a"
R1(config)#logging buffered 20010
R1(config)#ip http server
R1(config)#end
R1#
...
Special methods in Python - methods that are responsible for “standard” possibilities of objects and
are called automatically when these possibilities are used. For example, expression a + b where a
and b are numbers that is converted to such a call a.__add__(b). That is, special method __add__
is responsible for the addition operation. All special methods start and end with double underscore,
therefore in English they are often called dunder methods, shortened from “double underscore”.
Special methods are responsible for such features as working in context managers, creating iterators
and iterable objects, addition operations, multiplication and others. By adding special methods to
objects that are created by user, we make them look like built in Python objects.
Underscore in names
In Python, underscore at the beginning or at the end of a name indicates special names. Most often
it’s just an arrangement, but sometimes it actually affects object behavior.
One underscore before method name indicates that method is an internal feature of implementation
and it should not be used directly.
import time
import paramiko
class CiscoSSH:
def __init__(self, ip, username, password, enable, disable_paging=True):
self.client = paramiko.SSHClient()
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.client.connect(
hostname=ip,
username=username,
password=password,
look_for_keys=False,
allow_agent=False)
self.ssh = self.client.invoke_shell()
self.ssh.send('enable\n')
self.ssh.send(enable + '\n')
if disable_paging:
self.ssh.send('terminal length 0\n')
time.sleep(1)
self.ssh.recv(1000)
After creating an instance of the class, not only send_show_command method is available but also
client and ssh attributes (3rd line is tab tips in ipython):
In [3]: r1.
client
send_show_command()
ssh
If you want to specify that client and ssh are internal attributes that are needed for class operation
but are not intended for user, you need to underscore name below:
class CiscoSSH:
def __init__(self, ip, username, password, enable, disable_paging=True):
self._client = paramiko.SSHClient()
self._client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self._client.connect(
hostname=ip,
username=username,
password=password,
look_for_keys=False,
allow_agent=False)
self._ssh = self._client.invoke_shell()
self._ssh.send('enable\n')
self._ssh.send(enable + '\n')
if disable_paging:
(continues on next page)
Note: Often such methods and attributes are called private but this does not mean that methods
and variables are not available to user.
Two underscores before method name are not used simply as an agreement. Such names are trans-
formed into format “name of class + name of method”. This allows the creation of unique methods
and attributes of classes.
This conversion is only performed if less than two underscores endings or no underscores.
In [15]: dir(Switch)
Out[15]:
['_Switch__configure', '_Switch__quantity', ...]
If you create a subclass then __configure method will not rewrite parent class method Switch:
In [17]: dir(CiscoSwitch)
Out[17]:
['_CiscoSwitch__configure', '_CiscoSwitch__quantity', '_Switch__configure', '_
,→Switch__quantity', ...]
• __name__ - this variable is equal to __main__ when the script runs directly and is equal to
module name when imported
• __file__ - this variable is equal to name of the script that was run directly and equals to
complete path to module when it is imported
Variable __name__ is most commonly used to indicate that a certain part of code must be executed
only when module is called directly:
return a * b
if __name__ == '__main__':
print(multiply(3, 5))
And __file__ variable can be useful in determining the current path to script file:
import os
print('__file__', __file__)
print(os.path.abspath(__file__))
__file__ example2.py
/home/vagrant/repos/tests/example2.py
Python also denotes special methods. These methods are called when using Python functions and
operators and allow to implement a certain functionality.
As a rule, such methods need not be called directly. But for example, when creating your own class
it may be necessary to describe such method in order to object can support some operations in
Python.
For example, in order to get length of an object it must support __len__ method.
Special methods __str__ and __repr__ are responsible for string representation of the object. They
are used in different places.
Consider example of IPAddress class that is responsible for representing IPv4 address:
class IPAddress:
def __init__(self, ip):
self.ip = ip
After creating class instances, they have a default string view that looks like this (the same output
is displayed when print is used):
In [4]: str(ip1)
Out[4]: '<__main__.IPAddress object at 0xb4e4e76c>'
In [5]: str(ip2)
Out[5]: '<__main__.IPAddress object at 0xb1bd376c>'
Unfortunately, this presentation is not very informative. It would be better to show information
about which address this instance represents. Special method __str__ is responsible for displaying
information when using str function. As an argument this method expects only instance and must
return string.
class IPAddress:
def __init__(self, ip):
self.ip = ip
def __str__(self):
return f"IPAddress: {self.ip}"
In [9]: str(ip1)
(continues on next page)
In [10]: str(ip2)
Out[10]: 'IPAddress: 10.2.2.2'
A second string view which is used in Python objects is displayed when using repr function and
when adding objects to containers such as lists:
In [12]: ip_addresses
Out[12]: [<__main__.IPAddress at 0xb4e40c8c>, <__main__.IPAddress at 0xb1bc46ac>]
In [13]: repr(ip1)
Out[13]: '<__main__.IPAddress object at 0xb4e40c8c>'
Method __repr__ is responsible for this output and it should also return a string, but it would return
a string by copying which you can get an instance of a class:
class IPAddress:
def __init__(self, ip):
self.ip = ip
def __str__(self):
return f"IPAddress: {self.ip}"
def __repr__(self):
return f"IPAddress('{self.ip}')"
In [18]: ip_addresses
Out[18]: [IPAddress('10.1.1.1'), IPAddress('10.2.2.2')]
In [19]: repr(ip1)
Out[19]: "IPAddress('10.1.1.1')"
Special methods are also responsible for arithmetic operations support, for example, __add__
method is responsible for addition operation:
__add__(self, other)
Let’s add to IPAddress class the support of summing with numbers, but in order not to complicate
method implementation we will take an advantage of ipaddress module possibilities.
In [3]: int(ipaddress1)
Out[3]: 167837953
In [4]: ipaddress.ip_address(167837953)
Out[4]: IPv4Address('10.1.1.1')
class IPAddress:
def __init__(self, ip):
self.ip = ip
def __str__(self):
return f"IPAddress: {self.ip}"
def __repr__(self):
return f"IPAddress('{self.ip}')"
ip_int variable refers to source address value in decimal format. And sum_ip_str is a string with IP
address obtained by adding two numbers. In general, it is desirable that the summation operation
returns an instance of the same class, so in the last line of method an instance of IPAddress class is
created and a string with resulting address is passed to it as an argument.
Now IPAddress class instances support addition with number. As a result we get a new instance of
IPAddress class.
In [7]: ip1 + 5
Out[7]: IPAddress('10.1.1.6')
Since ipaddress module is used within method and it supports creating IP address only from a decimal
number, it is necessary to limit method to work only with int data type. If the second element was
an object of another type, an exception must be generated. The exception and error message are
taken from a similar error in the ipaddress.ip_address function:
In [8]: a1 = ipaddress.ip_address('10.1.1.1')
In [9]: a1 + 4
Out[9]: IPv4Address('10.1.1.5')
In [10]: a1 + 4.0
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-10-a0a045adedc5> in <module>
----> 1 a1 + 4.0
class IPAddress:
def __init__(self, ip):
self.ip = ip
def __str__(self):
return f"IPAddress: {self.ip}"
def __repr__(self):
return f"IPAddress('{self.ip}')"
ip_int = int(ipaddress.ip_address(self.ip))
sum_ip_str = str(ipaddress.ip_address(ip_int + other))
return IPAddress(sum_ip_str)
If the second operand is not an instanse of int class, a TypeError exception is generated. In excep-
tion, information is displayed that summation is not supported between IPAddress class instances
and operand class instance. Class name is derived from class itself, after calling type: type(other).
__name__.
In [13]: ip1 + 5
Out[13]: IPAddress('10.1.1.6')
See also:
Protocols
Special methods are responsible not only for support of operations like addition and comparison,
but also for protocol support. Protocol - set of methods that must be implemented in object to make
object support a certain behavior. For example, Python has protocols like iteration, context manager,
containers and others. After creating certain methods in the object, it will behave as built-in and
use an interface understood by all who write on Python.
Note: A table with abstract classes describing which methods an object should have to make it
support a certain protocol
Iteration protocol
iterable - object that can return elements one at a time. For Python, it is any object that has
__iter__ or __getitem__ method. If an object has __iter__ method, the iterable becomes an
iterator by calling iter(name) where name - name of iterable. If __iter__ method is not present,
Python iterates elements using __getitem__ (also by calling iter function).
class Items:
def __init__(self, items):
self.items = items
In [3]: iterable_1[0]
Calling __getitem__
Out[3]: 1
Calling __getitem__
>>>> 4
Calling __getitem__
If object has __iter__ method (which must return iterator), it is used for values iteration:
class Items:
def __init__(self, items):
self.items = items
def __iter__(self):
print('Вызываю __iter__')
return iter(self.items)
In [2]: iter(lista)
Out[2]: <list_iterator at 0xb4ede28c>
iter function will work on any object that has __iter__ or __getitem__ method. Method __iter__
returns an iterator. If this method is not available, iter function checks availability of __getitem__
method that can get elements by index. If __getitem__ method exists, elements will be iterated
through index (starting with 0).
iterator - object that returns its elements one at a time. From Python point of view, it is any object
that has __next__ method. This method returns the next item if any or raises Stopiteration
exception when items are ended. In addition, iterator remembers which object it stopped at in the
last iteration. Each iterator also has __iter__ method - that is, every iterator is an iterable object.
This method returns iterator itself.
In [4]: i = iter(lista)
Now you can use next function that calls __next__ method to take the next element:
In [5]: next(i)
Out[5]: 1
In [6]: next(i)
Out[6]: 2
In [7]: next(i)
Out[7]: 3
In [8]: next(i)
------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-8-bed2471d02c1> in <module>()
----> 1 next(i)
StopIteration:
After elements are ended, Stopiteration exception is raised. In order for iterator to return elements
again, it has to be re-created. Similar steps are performed when for loop iterates items in the list:
...: print(item)
...:
1
2
3
When we iterate list items, iter function is first applied to the list to create an iterator and then
__next__ method is called until Stopiteration exception raised.
An example of my_for function that works with any iterable and loosely imitates built-in function
for (actually gititem are iterated over by iter function):
def my_for(iterable):
if getattr(iterable, "__iter__", None):
print('Есть __iter__')
iterator = iter(iterable)
while True:
try:
print(next(iterator))
except StopIteration:
break
elif getattr(iterable, "__getitem__", None):
print('Нет __iter__, но есть __getitem__')
index = 0
while True:
try:
print(iterable[index])
index += 1
except IndexError:
break
Check function on object that does not have __iter__ but has __getitem__:
class Items:
def __init__(self, items):
(continues on next page)
self.items = items
In [21]: my_for(iterable_1)
Нет __iter__, но есть __getitem__
Calling __getitem__
1
Calling __getitem__
2
Calling __getitem__
3
Calling __getitem__
4
Calling __getitem__
5
Calling __getitem__
Iterator creation
In [15]: net1
Out[15]: <__main__.Network at 0xb3124a6c>
In [16]: net1.addresses
Out[16]: ['10.1.1.193', '10.1.1.194']
In [17]: net1.network
Out[17]: '10.1.1.192/30'
class Network:
def __init__(self, network):
self.network = network
subnet = ipaddress.ip_network(self.network)
self.addresses = [str(ip) for ip in subnet.hosts()]
self._index = 0
def __iter__(self):
print('Вызываю __iter__')
return self
def __next__(self):
print('Вызываю __next__')
if self._index < len(self.addresses):
current_address = self.addresses[self._index]
self._index += 1
return current_address
else:
raise StopIteration
Method __iter__ in iterator must return object itself, therefore return self is specified in method
and __next__ method returns elements one at a time and raises StopIteration exception when
elements have run out.
Most of the time, iterator is a disposable object and once we’ve iterated elements, we can’t do it
again:
Creation of iterable
Very often it is sufficient for class to be an iterable and not necessarily an iterator. If an object is
iterable, it can be used in for loop, map functions, filter, sorted, enumerate and others. It is also
generally easier to make an iterable than an iterator.
In order for Network class to be iterable, class must have __iter__ (__next__ is not needed) and
method must return iterator. Since in this case, Network iterates addresses that are in self.
addresses list, the easiest option to return iterator is to return iter(self.addresses):
class Network:
def __init__(self, network):
self.network = network
subnet = ipaddress.ip_network(self.network)
self.addresses = [str(ip) for ip in subnet.hosts()]
def __iter__(self):
return iter(self.addresses)
Sequence protocol
In the most basic version, sequence protocol (sequence) includes two methods: __len__ and
__getitem__. In more complete version also methods: __contains__, __iter__, __reversed__,
index and count. If sequence is mutable, several other methods are added.
In [3]: len(net1)
Out[3]: 2
In [4]: net1[0]
Out[4]: '10.1.1.193'
In [5]: net1[1]
Out[5]: '10.1.1.194'
In [6]: net1[-1]
Out[6]: '10.1.1.194'
__getitem__ method is responsible not only for access by index, but also for slices:
In [8]: net1[0]
Out[8]: '10.1.1.193'
In [9]: net1[3:7]
Out[9]: ['10.1.1.196', '10.1.1.197', '10.1.1.198', '10.1.1.199']
(continues on next page)
In [10]: net1[3:]
Out[10]:
['10.1.1.196',
'10.1.1.197',
'10.1.1.198',
'10.1.1.199',
'10.1.1.200',
'10.1.1.201',
'10.1.1.202',
'10.1.1.203',
'10.1.1.204',
'10.1.1.205',
'10.1.1.206']
In this case, because __getitem__ method uses a list, errors are processed correctly automatically:
In [11]: net1[100]
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
<ipython-input-11-09ca84e34cb6> in <module>
----> 1 net1[100]
In [12]: net1['a']
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-12-facd90673864> in <module>
----> 1 net1['a']
• __contains__ - this method is responsible for checking the presence of element in sequence
'10.1.1.198' in net1. If object does not define this method, the presence of element is
checked by iteration of elements using __iter__ and if this method is also unavailable, then
by index iteration with __getitem__.
• __reversed__ - is used by built-in reversed function. This method is usually best not to cre-
ate and rely on the fact that reversed function in absence of __reversed__ method will use
methods __len__ and __getitem__.
• index - returns index of element. Works exactly the same as index method in lists and tuples.
• count - returns number of values. Works exactly the same as count method in lists and tuples.
Context manager
Context manager allows specified actions to be performed at the beginning and end of with block.
Two methods are responsible for context manager:
• __enter__(self) - indicates what should be done at the beginning of with block. Value that
returns method is assigned to variable after as.
• file opening/closing
class CiscoSSH:
def __init__(self, ip, username, password, enable, disable_paging=True):
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(
hostname=ip,
username=username,
(continues on next page)
password=password,
look_for_keys=False,
allow_agent=False)
self.ssh = client.invoke_shell()
self.ssh.send('enable\n')
self.ssh.send(enable + '\n')
if disable_paging:
self.ssh.send('terminal length 0\n')
time.sleep(1)
self.ssh.recv(1000)
In order for the class to support work in context manager, it is necessary to add methods __enter__
and __exit__:
class CiscoSSH:
def __init__(self, ip, username, password, enable, disable_paging=True):
print('Метод __init__')
client = paramiko.SSHClient()
(continues on next page)
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(
hostname=ip,
username=username,
password=password,
look_for_keys=False,
allow_agent=False)
self.ssh = client.invoke_shell()
self.ssh.send('enable\n')
self.ssh.send(enable + '\n')
if disable_paging:
self.ssh.send('terminal length 0\n')
time.sleep(1)
self.ssh.recv(1000)
def __enter__(self):
print('Метод __enter__')
return self
Tasks
Warning: Starting from section “4. Data types in Python” there are automated tests for testing
tasks. They help to check whether everything matches the task, and also give feedback on what
does not correspond to the task. As a rule, after the first period of adaptation to tests, it becomes
easier to do tasks with tests. Testing is done using the pyneng utility. Learn more about how to
work with the pyneng utility.
Task 23.1
When creating an instance of a class, the IP address and mask are passed as an argument, and the
correctness of the address and mask must be checked:
• the mask is considered correct if the mask is a number and a number in the range from 8 to
32 inclusiveA
If the mask or address fails validation, you must raise a ValueError with the appropriate text (output
below).
Also, when creating a class, two instance variables must be created: ip and mask, which contain
the address and mask, respectively.
In [3]: ip1.ip
Out[3]: '10.1.1.1'
In [4]: ip1.mask
Out[4]: 24
Task 23.1a
Add two string views for instances of the IPAddress class. How string representations should look
like should be determined from the output below.
In [6]: str(ip1)
Out[6]: 'IP address 10.1.1.1/24'
In [7]: print(ip1)
IP address 10.1.1.1/24
In [8]: ip1
Out[8]: IPAddress('10.1.1.1/24')
In [9]: ip_list = []
In [10]: ip_list.append(ip1)
In [11]: ip_list
Out[11]: [IPAddress('10.1.1.1/24')]
In [12]: print(ip_list)
[IPAddress('10.1.1.1/24')]
Task 23.2
Copy the CiscoTelnet class from any 22.2x task and add context manager support to the class. When
exiting the context manager block, the connection should be closed.
Example of work:
In [14]: r1_params = {
...: 'ip': '192.168.100.1',
...: 'username': 'cisco',
...: 'password': 'cisco',
...: 'secret': 'cisco'}
Task 23.3
In this task, you need to add a method that will allow you to add two instances of the Topology class.
The addition should return a new instance of the Topology class.
In [1]: t1 = Topology(topology_example)
In [2]: t1.topology
Out[2]:
{('R1', 'Eth0/0'): ('SW1', 'Eth0/1'),
('R2', 'Eth0/0'): ('SW1', 'Eth0/2'),
('R2', 'Eth0/1'): ('SW2', 'Eth0/11'),
('R3', 'Eth0/0'): ('SW1', 'Eth0/3'),
('R3', 'Eth0/1'): ('R4', 'Eth0/0'),
('R3', 'Eth0/2'): ('R5', 'Eth0/0')}
In [4]: t2 = Topology(topology_example2)
In [5]: t2.topology
Out[5]: {('R1', 'Eth0/4'): ('R7', 'Eth0/0'), ('R1', 'Eth0/6'): ('R9', 'Eth0/0')}
In [6]: t3 = t1 + t2
In [7]: t3.topology
Out[7]:
{('R1', 'Eth0/0'): ('SW1', 'Eth0/1'),
('R1', 'Eth0/4'): ('R7', 'Eth0/0'),
('R1', 'Eth0/6'): ('R9', 'Eth0/0'),
('R2', 'Eth0/0'): ('SW1', 'Eth0/2'),
('R2', 'Eth0/1'): ('SW2', 'Eth0/11'),
('R3', 'Eth0/0'): ('SW1', 'Eth0/3'),
('R3', 'Eth0/1'): ('R4', 'Eth0/0'),
('R3', 'Eth0/2'): ('R5', 'Eth0/0')}
In [9]: t1.topology
Out[9]:
{('R1', 'Eth0/0'): ('SW1', 'Eth0/1'),
('R2', 'Eth0/0'): ('SW1', 'Eth0/2'),
('R2', 'Eth0/1'): ('SW2', 'Eth0/11'),
('R3', 'Eth0/0'): ('SW1', 'Eth0/3'),
('R3', 'Eth0/1'): ('R4', 'Eth0/0'),
('R3', 'Eth0/2'): ('R5', 'Eth0/0')}
In [10]: t2.topology
Out[10]: {('R1', 'Eth0/4'): ('R7', 'Eth0/0'), ('R1', 'Eth0/6'): ('R9', 'Eth0/0')}
Task 23.3a
In this task, you need to make sure that instances of the Topology class are iterables. The base of
the Topology class can be taken from either task 22.1x or task 23.3.
After creating an instance of a class, the instance should act like an iterable object. Each iteration
should return a tuple that describes one connection. The order of output of connections can be any.
24. Inheritance
Inheritance basics
Inheritance allows creation of new classes based on existing ones. There are child and parents
classes: child class inherits parent class. In inheritance, child class inherits all methods and at-
tributes of parent class.
import paramiko
import time
class ConnectSSH:
def __init__(self, ip, username, password):
self.ip = ip
self.username = username
self.password = password
self._MAX_READ = 10000
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(
hostname=ip,
username=username,
password=password,
look_for_keys=False,
allow_agent=False)
self._ssh = client.invoke_shell()
time.sleep(1)
self._ssh.recv(self._MAX_READ)
def __enter__(self):
return self
def close(self):
self._ssh.close()
This class will be used as the basis for classes that are responsible for connecting to devices of
different vendors. For example, CiscoSSH class will be responsible for connecting to Cisco devices
and will inherit ConnectSSH class.
Inheritance syntax:
class CiscoSSH(ConnectSSH):
pass
After that, all ConnectSSH methods and attributes are available in CiscoSSH class:
In [4]: r1.ip
Out[4]: '192.168.100.1'
In [5]: r1._MAX_READ
Out[5]: 10000
In [7]: r1.send_show_command('enable')
Out[7]: 'enable\r\nPassword: '
In [8]: r1.send_show_command('cisco')
Out[8]: '\r\nR1#'
• supplement method
In CiscoSSH class you have to create __init__ method and add parameters to it:
Method __init__ can be created entirely from scratch but basic SSH connection logic is the same in
ConnectSSH and CiscoSSH, so it is better to add necessary parameters and call __init__ method of
ConnectSSH class for connection. There are several options for calling parent method, for example,
all of these options will call send_show_command method of parent class from child class CiscoSSH:
The first version of ConnectSSH.send_show_command explicitly specifies the name of parent class -
this is the most understandable version for perception, but its disadvantage is that when a parent
class name is changed the name will have to be changed in all places where parent class methods
were called. This option also has disadvantages when using multiple inheritance. The second and
third options are essentially equivalent but the third option is shorter, so we will use it.
class CiscoSSH(ConnectSSH):
def __init__(self, ip, username, password, enable_password,
disable_paging=True):
super().__init__(ip, username, password)
self._ssh.send('enable\n')
self._ssh.send(enable_password + '\n')
if disable_paging:
self._ssh.send('terminal length 0\n')
time.sleep(1)
self._ssh.recv(self._MAX_READ)
Method __init__ in CiscoSSH class added enable_password and disable_paging parameters and
uses them accordingly to enter enable mode and disable paging. Example:
Now when connecting, switch enters enable mode and paging is disabled by default, so you can try
to run a long command like sh run.
Another method that should be further developed is send_config_commands method: since Cis-
coSSH class is designed to work with Cisco, you can add switching to configuration mode before
commands and exit after.
class CiscoSSH(ConnectSSH):
def __init__(self, ip, username, password, enable_password,
disable_paging=True):
super().__init__(ip, username, password)
self._ssh.send('enable\n')
self._ssh.send(enable_password + '\n')
if disable_paging:
self._ssh.send('terminal length 0\n')
time.sleep(1)
self._ssh.recv(self._MAX_READ)
def config_mode(self):
self._ssh.send('conf t\n')
time.sleep(0.5)
result = self._ssh.recv(self._MAX_READ).decode('ascii')
return result
def exit_config_mode(self):
(continues on next page)
self._ssh.send('end\n')
time.sleep(0.5)
result = self._ssh.recv(self._MAX_READ).decode('ascii')
return result
Tasks
Warning: Starting from section “4. Data types in Python” there are automated tests for testing
tasks. They help to check whether everything matches the task, and also give feedback on what
does not correspond to the task. As a rule, after the first period of adaptation to tests, it becomes
easier to do tasks with tests. Testing is done using the pyneng utility. Learn more about how to
work with the pyneng utility.
Task 24.1
Create a CiscoSSH class that inherits the BaseSSH class from the base_connect_class.py file.
Create an __init__ method in the CiscoSSH class so that after connecting via SSH, it switches to
enable mode.
To do this, the __init__ method must first call the __init__ method of the BaseSSH class, and then
switch to enable mode.
In [3]: r1 = CiscoSSH(**device_params)
Task 24.1a
Before connecting via SSH, you need to check if the dictionary with the connection parameters
contains the following parameters: username, password, secret. If any parameter is missing, ask
the user for a value and then connect. If all parameters are present, connect.
In [2]: device_params = {
...: 'device_type': 'cisco_ios',
...: 'host': '192.168.100.1',
...: }
In [3]: r1 = CiscoSSH(**device_params)
Enter username: cisco
Enter password: cisco
Enter enable passwod: cisco
Task 24.2
Create a MyNetmiko class that inherits the CiscoIosSSH class from netmiko. Write the __init__ method
in the MyNetmiko class so that after connecting via SSH, it switches to enable mode.
To do this, the __init__ method must first call the __init__ method of the CiscoIosSSH class, and then
switch to enable mode.
Check that the send_command and send_config_set methods are available in the MyNetmiko class
(they are inherited automatically, this is just for checking).
In [3]: r1 = MyNetmiko(**device_params)
Task 24.2a
Add the _check_error_in_command method that checks for such errors: Invalid input detected, In-
complete command, Ambiguous command
The method expects a command and command output as an argument. If no error is found in the
output, the method returns nothing. If an error is found in the output, the method should raise an
ErrorInCommand exception with a message about which error was detected, on which device, and
in which command.
In [3]: r1 = MyNetmiko(**device_params)
Task 24.2b
Add error checking to the send_config_set method using the _check_error_in_command method.
The send_config_set method should send commands one at a time and check each for errors. If
no errors are encountered while executing the commands, the send_config_set method returns the
output of the commands.
In [3]: r1 = MyNetmiko(**device_params)
In [4]: r1.send_config_set('lo')
---------------------------------------------------------------------------
ErrorInCommand Traceback (most recent call last)
<ipython-input-2-8e491f78b235> in <module>()
----> 1 r1.send_config_set('lo')
...
ErrorInCommand: When executing the command "lo" on device 192.168.100.1, an error␣
,→occurred "Incomplete command."
Task 24.2c
Copy the class MyNetmiko from task 24.2b. Check that the send_command method, in addition to
a command, also accepts additional arguments, for example, strip_command.
If an error occurs, rewrite the method to accept any arguments that netmiko supports.
In [3]: r1 = MyNetmiko(**device_params)
Task 24.2d
In [3]: r1 = MyNetmiko(**device_params)
In [6]: r1.send_config_set('lo')
Out[6]: 'config term\nEnter configuration commands, one per line. End with CNTL/
,→Z.\nR1(config)#lo\n% Incomplete command.\n\nR1(config)#end\nR1#'
...
ErrorInCommand: When executing the command "lo" on device 192.168.100.1, an error␣
,→occurred "Incomplete command."
597
Python for network engineers, Release 1.0
The use of databases is another way of storing information. Databases are useful not only in storing
information. Using DBMS it is possible to make information slices according to different parameters.
Database (DB) - data stored according to a certain scheme. This scheme describes relationships
between data. DB language (language tools) - used to describe database structure, manage
data (add, edit, delete, receive), manage access rights to database and its objects, and manage
transactions.
Database Management System (DBMS) - a software tool that enables management of DB. DBMS
must support appropriate language(s) for DB management.
SQL
SQL (structured query language) - used to describe database structure, manage data (add, edit,
delete, receive), manage access rights to database and its objects, and manage transactions.
Each category has its own operators (not all operators are listed):
• DDL
• DML
• DCL
• TCL
• work with a library that corresponds to a specific database and use SQL language to work with
database. For example, sqlite uses sqlite3 module
• work with ORM which uses an object-oriented approach to work with database. For example,
Sqlalchemy
SQLite
SQLite — a built-in SQL machine implementation. Sqlite is often used as an embedded DBMS in
applications.
Note: The word SQL server is not used here because server is not needed there - all functionality
that is embedded in SQL server is implemented inside a library (and therefore within program that
uses it).
SQLite CLI
SQLite package also includes a command line utility for working with SQLite. Utility is presented
as a sqlite3 executable file (sqlite3.exe for Windows) and can be used to execute SQL commands
manually.
With this utility it is very convenient to check the correctness of SQL commands as well as to get
acquainted with SQL language in general.
Let’s try to use this utility to figure out basic SQL commands that will be needed to work with
database.
Note: If you are using Linux or Mac OS, it is likely that sqlite3 is installed. If you are using Windows
you can download sqlite3 here.
To create a database (or open an already created database), you simply call sqlite3:
$ sqlite3 testDB.db
SQLite version 3.8.7.1 2014-10-29 13:59:56
Enter ".help" for usage hints.
sqlite>
Inside sqlite3 you can execute SQL commands or so-called metacommands (or dot commands).
Metacommands include several special commands to work with SQLite. They refer only to sqlite3
utility, not to SQL language. There is no need to put ; at the end of command.
Examples of metacommands:
Examples:
sqlite> .help
.backup ?DB? FILE Backup DB (default "main") to FILE
.bail ON|OFF Stop after hitting an error. Default OFF
.databases List names and files of attached databases
...
sqlite> .databases
seq name file
--- -------- ----------------------------------
0 main /home/nata/py_for_ne/db/db_article/testDB.db
litecli
• no autocomplete commands
• no tips
All these deficiencies are fixed in litecli. So it’s best to use it.
Installation of litecli:
$ litecli example.db
Version: 1.0.0
Mail: https://groups.google.com/forum/#!forum/litecli-users
Github: https://github.com/dbcli/litecli
example.db>
If you are familiar with basic SQL syntax you can skip this section and move to section Sqlite3 module
CREATE
$ litecli new_db.db
Version: 1.0.0
Mail: https://groups.google.com/forum/#!forum/litecli-users
Github: https://github.com/dbcli/litecli
new_db.db>
new_db.db> create table switch (mac text not NULL primary key, hostname text,␣
,→model text, location text);
Query OK, 0 rows affected
Time: 0.010s
In this example, we described switch table: we defined which fields would be in the table and which
types of values would be in them.
• field cannot have null value (in SQLite this must be stated explicitly)
There are no entries in the table at the moment, only a definition. You can view definition with this
command:
| sql ␣
,→ |
+---------------------------------------------------------------------------------
,→--------------+
| CREATE TABLE switch (mac text not NULL primary key, hostname text, model text,␣
,→location text) |
+---------------------------------------------------------------------------------
,→--------------+
Time: 0.037s
DROP
DROP operator removes table along with schema and all data.
INSERT
new_db.db> create table switch (mac text not NULL primary key, hostname text,␣
,→model text, location text);
Query OK, 0 rows affected
Time: 0.010s
There are several options for adding entries, depending on whether all fields are filled and whether
or not they follow the field order.
If values for all fields are specified you can add an entry in this way (the order of fields must be
respected):
If you want to specify not all fields or specify them randomly, this entry is used:
new_db.db> INSERT into switch (mac, model, location, hostname) values ('0020.A2AA.
,→C2CC', 'Cisco 3850', 'London, Green Str', 'sw2');
Query OK, 1 row affected
Time: 0.009s
SELECT
For example:
SELECT * means that all fields in the table must be displayed. Then indicates from which table data
is requested: from switch.
WHERE
WHERE operator is used to specify a query. With the help of this operator it is possible to spec-
ify certain conditions under which data is selected. If condition is met the corresponding value is
returned from table, if not - it is not returned.
To create more entries in table you need to create more rows. Litecli has a source command that
lets you upload SQL commands from a file.
INSERT into switch values ('0030.A3AA.C1CC', 'sw3', 'Cisco 3750', 'London, Green␣
,→Str');
INSERT into switch values ('0040.A4AA.C2CC', 'sw4', 'Cisco 3850', 'London, Green␣
,→Str');
INSERT into switch values ('0050.A5AA.C3CC', 'sw5', 'Cisco 3850', 'London, Green␣
,→Str');
INSERT into switch values ('0060.A6AA.C4CC', 'sw6', 'C3750', 'London, Green Str');
INSERT into switch values ('0070.A7AA.C5CC', 'sw7', 'Cisco 3650', 'London, Green␣
,→Str');
Time: 0.002s
Using the WHERE clause, you can show only those switches whose model is 3850:
WHERE operator allows you to specify more than a specific field value. If you add LIKE operator to
it you can specify a field template.
Like with characters _ and % indicates what the value should look like:
For example, if model field is written in different formats the previous query will not be able to extract
needed switches. For sw6 switch the model field is written in this format: C3750, but for sw1 and
ALTER
ALTER TABKE statement allows you to change an existing table: add new columns or rename the
table.
Now table looks like this (new fields are set to NULL):
7 rows in set
Time: 0.034s
UPDATE
Usually, UPDATE is used with WHERE operator to specify which entry to change.
7 rows in set
Time: 0.035s
+----------------+----------+------------+-------------------+------------+-------
,→----+
7 rows in set
Time: 0.037s
7 rows in set
Time: 0.033s
To avoid filling fields mngmt_ip and mngmt_vid manually, fill in the rest from up-
date_fields_in_testdb.txt file (command source update_fields_in_testdb.txt):
UPDATE switch set mngmt_ip = '10.255.1.3', mngmt_vid = 255 WHERE hostname = 'sw3';
UPDATE switch set mngmt_ip = '10.255.1.4', mngmt_vid = 255 WHERE hostname = 'sw4';
UPDATE switch set mngmt_ip = '10.255.1.5', mngmt_vid = 255 WHERE hostname = 'sw5';
UPDATE switch set mngmt_ip = '10.255.1.6', mngmt_vid = 255 WHERE hostname = 'sw6';
UPDATE switch set mngmt_ip = '10.255.1.7', mngmt_vid = 255 WHERE hostname = 'sw7';
+----------------+----------+------------+-------------------+------------+-------
,→----+
7 rows in set
Time: 0.038s
Now suppose that sw1 was replaced from 3750 model to 3850. Accordingly, not only model field
but also MAC address field was changed.
Making changes:
new_db.db> UPDATE switch set model = 'Cisco 3850', mac = '0010.D1DD.E1EE' WHERE␣
,→hostname = 'sw1';
Query OK, 1 row affected
Time: 0.009s
7 rows in set
Time: 0.049s
REPLACE
Or a shorter version:
In this case, MAC address in new entry is the same as in existing one, so the replacement occurs.
Note: If not all fields have been specified, the new entry will contain only those fields that have
been specified. This is because REPLACE first removes an existing entry.
For entry which was added without uniqueness violation, REPLACE functions as a normal INSERT:
8 rows in set
Time: 0.034s
DELETE
DELETE operator is used to delete enties. It is commonly used together with WHERE operator.
8 rows in set
Time: 0.033s
7 rows in set
Time: 0.039s
ORDER BY
ORDER BY operator is used to sort the output by a certain field, ascending or descending. To do this
it should be added to SELECT operator.
7 rows in set
Time: 0.039s
With help of ORDER BY operator you can get entries from switch table by sorting them by switch
name:
7 rows in set
Time: 0.034s
7 rows in set
(continues on next page)
Time: 0.034s
7 rows in set
Time: 0.034s
AND
new_db.db> select * from switch where model = 'Cisco 3850' and mngmt_ip LIKE '10.
,→255.%';
+----------------+----------+------------+-------------------+------------+-------
,→----+
5 rows in set
Time: 0.034s
OR
Operator OR:
new_db.db> select * from switch where model LIKE '%3750' or model LIKE '%3850';
+----------------+----------+------------+-------------------+------------+-------
,→----+
6 rows in set
Time: 0.046s
IN
Operator IN:
NOT
Operator NOT:
new_db.db> select * from switch where model not in ('Cisco 3750', 'C3750');
+----------------+----------+------------+-------------------+------------+-------
,→----+
6 rows in set
Time: 0.037s
Sqlite3 module
import sqlite3
connection = sqlite3.connect('dhcp_snooping.db')
Once you have created a connection you should create a Cursor object which is the main way to
work with database.
connection = sqlite3.connect('dhcp_snooping.db')
cursor = connection.cursor()
• executemany - method allows to execute one SQL expression for a sequence of parameters (or
for iterator)
Method execute
Method execute allows one SQL command to be executed. First, create connection and cursor:
In [4]: cursor.execute("create table switch (mac text not NULL primary key,␣
,→hostname text, model text, location text)")
Out[4]: <sqlite3.Cursor at 0x1085be880>
SQL expressions can be parameterized - data can be substituted by special values. Due to this you
can use the same SQL command to pass different data.
For example, switch table needs to be filled with data from data list:
In [5]: data = [
...: ('0000.AAAA.CCCC', 'sw1', 'Cisco 3750', 'London, Green Str'),
...: ('0000.BBBB.CCCC', 'sw2', 'Cisco 3780', 'London, Green Str'),
...: ('0000.AAAA.DDDD', 'sw3', 'Cisco 2960', 'London, Green Str'),
...: ('0011.AAAA.CCCC', 'sw4', 'Cisco 3750', 'London, Green Str')]
Question marks in command are used to fill in the data that will be passed to execute.
The second argument that is passed to execute must be a tuple. If you want to pass a tuple with
one element, (value, ) entry is used.
For changes to be applied, commit must be executed (note that commit method is called at the
connection):
In [8]: connection.commit()
Now, when querying from sqlite3 command line you can see these rows in switch table:
$ litecli sw_inventory.db
Version: 1.0.0
Mail: https://groups.google.com/forum/#!forum/litecli-users
Github: https://github.com/dbcli/litecli
sw_inventory.db> SELECT * from switch;
+----------------+----------+------------+-------------------+
| mac | hostname | model | location |
+----------------+----------+------------+-------------------+
| 0000.AAAA.CCCC | sw1 | Cisco 3750 | London, Green Str |
| 0000.BBBB.CCCC | sw2 | Cisco 3780 | London, Green Str |
| 0000.AAAA.DDDD | sw3 | Cisco 2960 | London, Green Str |
| 0011.AAAA.CCCC | sw4 | Cisco 3750 | London, Green Str |
+----------------+----------+------------+-------------------+
4 rows in set
(continues on next page)
Time: 0.039s
sw_inventory.db>
Method executemany
Method executemany allows one SQL command to be executed for parameter sequence (or for it-
eratoAr). Using executemany method you can add a similar data list to switch table by a single
command.
For example, you should add data from data2 list to switch table:
In [9]: data2 = [
...: ('0000.1111.0001', 'sw5', 'Cisco 3750', 'London, Green Str'),
...: ('0000.1111.0002', 'sw6', 'Cisco 3750', 'London, Green Str'),
...: ('0000.1111.0003', 'sw7', 'Cisco 3750', 'London, Green Str'),
...: ('0000.1111.0004', 'sw8', 'Cisco 3750', 'London, Green Str')]
In [12]: connection.commit()
$ litecli sw_inventory.db
Version: 1.0.0
Mail: https://groups.google.com/forum/#!forum/litecli-users
Github: https://github.com/dbcli/litecli
sw_inventory.db> SELECT * from switch;
+----------------+----------+------------+-------------------+
| mac | hostname | model | location |
+----------------+----------+------------+-------------------+
| 0000.AAAA.CCCC | sw1 | Cisco 3750 | London, Green Str |
| 0000.BBBB.CCCC | sw2 | Cisco 3780 | London, Green Str |
| 0000.AAAA.DDDD | sw3 | Cisco 2960 | London, Green Str |
| 0011.AAAA.CCCC | sw4 | Cisco 3750 | London, Green Str |
| 0000.1111.0001 | sw5 | Cisco 3750 | London, Green Str |
(continues on next page)
Method executemany placed corresponding tuples to SQL command and all data was added to the
table.
Method executescript
In [15]: cursor.executescript('''
...: create table switches(
...: hostname text not NULL primary key,
...: location text
...: );
...:
...: create table dhcp(
...: mac text not NULL primary key,
...: ip text,
...: vlan text,
...: interface text,
...: switch text not null references switches(hostname)
...: );
...: ''')
Out[15]: <sqlite3.Cursor at 0x10efd67a0>
• using fetch... - depending on the method one, more or all rows are returned
Method fetchone
Method fetchone returns one data row. Example of fetching information from sw_inventory.db
database:
In [20]: cursor.fetchone()
Out[20]: ('0000.AAAA.CCCC', 'sw1', 'Cisco 3750', 'London, Green Str')
Note that while the SQL query requests all table content, fetchone returned only one row. If you
re-call method, it returns the next row:
In [21]: print(cursor.fetchone())
('0000.BBBB.CCCC', 'sw2', 'Cisco 3780', 'London, Green Str')
Similarly, method will return the next rows. After processing all rows, method starts returning None.
Method fetchmany
Method syntax:
cursor.fetchmany([size=cursor.arraysize])
Size parameter allows you to specify how many rows are returned. By default the size parameter is
cursor.arraysize:
In [24]: print(cursor.arraysize)
1
For example, you can return three rows at a time from query:
Method displays required number of rows and if amount of rows are less than the size parameter, it
returns remaining rows.
Method fetchall
In [29]: cursor.fetchall()
Out[29]:
[('0000.AAAA.CCCC', 'sw1', 'Cisco 3750', 'London, Green Str'),
('0000.BBBB.CCCC', 'sw2', 'Cisco 3780', 'London, Green Str'),
('0000.AAAA.DDDD', 'sw3', 'Cisco 2960', 'London, Green Str'),
('0011.AAAA.CCCC', 'sw4', 'Cisco 3750', 'London, Green Str'),
('0000.1111.0001', 'sw5', 'Cisco 3750', 'London, Green Str'),
('0000.1111.0002', 'sw6', 'Cisco 3750', 'London, Green Str'),
('0000.1111.0003', 'sw7', 'Cisco 3750', 'London, Green Str'),
('0000.1111.0004', 'sw8', 'Cisco 3750', 'London, Green Str')]
That is, if fetchone method was used before fetchall, then fetchall would return remaining query
rows:
In [31]: cursor.fetchone()
Out[31]: ('0000.AAAA.CCCC', 'sw1', 'Cisco 3750', 'London, Green Str')
In [32]: cursor.fetchone()
Out[32]: ('0000.BBBB.CCCC', 'sw2', 'Cisco 3780', 'London, Green Str')
In [33]: cursor.fetchall()
Out[33]:
[('0000.AAAA.DDDD', 'sw3', 'Cisco 2960', 'London, Green Str'),
('0011.AAAA.CCCC', 'sw4', 'Cisco 3750', 'London, Green Str'),
('0000.1111.0001', 'sw5', 'Cisco 3750', 'London, Green Str'),
('0000.1111.0002', 'sw6', 'Cisco 3750', 'London, Green Str'),
('0000.1111.0003', 'sw7', 'Cisco 3750', 'London, Green Str'),
('0000.1111.0004', 'sw8', 'Cisco 3750', 'London, Green Str')]
Cursor as iterator
If you want to process the resulting strings, use cursor as an iterator. It is not necessary to use fetch
methods.
If you use execute methods, the cursor is returned. Since cursor can be used as an iterator you can
use it, for example, in for loop:
Execute methods are available in Connection object and in Cursor object but fetch methods are
only available in Cursor object.
When using execute methods with Connection object, cursor is returned as a result of execute
method. It can be used as an iterator and receive data without fetch methods. This allows you not
to create cursor when working with sqlite3 module.
import sqlite3
con = sqlite3.connect('sw_inventory2.db')
con.close()
$ python create_sw_inventory_ver1.py
('0000.AAAA.CCCC', 'sw1', 'Cisco 3750', 'London, Green Str')
('0000.BBBB.CCCC', 'sw2', 'Cisco 3780', 'London, Green Str')
('0000.AAAA.DDDD', 'sw3', 'Cisco 2960', 'London, Green Str')
('0011.AAAA.CCCC', 'sw4', 'Cisco 3750', 'London, Green Str')
Exception Handling
Let’s see an example of how to use execute method when an error occurs.
In switch table the mac field must be unique. If you try to write an overlapping MAC address, there
is an error:
In [38]: query = "INSERT into switch values ('0000.AAAA.DDDD', 'sw7', 'Cisco 2960
,→', 'London, Green Str')"
In [39]: con.execute(query)
------------------------------------------------------------
IntegrityError Traceback (most recent call last)
<ipython-input-56-ad34d83a8a84> in <module>()
----> 1 con.execute(query)
In [40]: try:
...: con.execute(query)
...: except sqlite3.IntegrityError as e:
...: print("Error occurred: ", e)
...:
Error occurred: UNIQUE constraint failed: switch.mac
After operations are completed the changes must be saved (apply commit), and then you can close
connection if it is no longer needed.
Python allows you to use Connection object as a context manager. In that case, you don’t have to
explicitly commit.
import sqlite3
con = sqlite3.connect('sw_inventory3.db')
(continues on next page)
try:
with con:
query = 'INSERT into switch values (?, ?, ?, ?)'
con.executemany(query, data)
except sqlite3.IntegrityError as e:
print('Error occured: ', e)
con.close()
Note that although a transaction will be rolled back when an exception occurs, the exception itself
must still be intercepted.
To check this functionality you should write to the table the data in which MAC address is repeated.
But before, in order to not repeat parts of the code, it is better to split the code by functions in
create_sw_inventory_ver2.py file:
def create_connection(db_name):
'''
Function creates a connection to db_name database and returns it
'''
connection = sqlite3.connect(db_name)
return connection
If an error occurs during the writing process, transaction rolls back and␣
,→function returns False.
'''
try:
with connection:
connection.executemany(query, data)
except sqlite3.IntegrityError as e:
print('Error occured: ', e)
return False
else:
print('Data writing was successful')
return True
if __name__ == '__main__':
con = create_connection('sw_inventory3.db')
print('DB creation...')
schema = '''create table switch
(mac text primary key, hostname text, model text, location text)''
,→' (continues on next page)
con.execute(schema)
con.close()
$ python create_sw_inventory_ver2_functions.py
Table creation...
Data writing to DB:
[('0000.AAAA.CCCC', 'sw1', 'Cisco 3750', 'London, Green Str'),
('0000.BBBB.CCCC', 'sw2', 'Cisco 3780', 'London, Green Str'),
('0000.AAAA.DDDD', 'sw3', 'Cisco 2960', 'London, Green Str'),
('0011.AAAA.CCCC', 'sw4', 'Cisco 3750', 'London, Green Str')]
Data writing was successful
Checking DB content
[('0000.AAAA.CCCC', 'sw1', 'Cisco 3750', 'London, Green Str'),
('0000.BBBB.CCCC', 'sw2', 'Cisco 3780', 'London, Green Str'),
('0000.AAAA.DDDD', 'sw3', 'Cisco 2960', 'London, Green Str'),
('0011.AAAA.CCCC', 'sw4', 'Cisco 3750', 'London, Green Str')]
Now let’s check how write_data_to_db function will work when there are identical MAC addresses
in the data.
con = dbf.create_connection('sw_inventory3.db')
print('-' * 60)
print("Trying to write data with a duplicate MAC address:")
pprint(data2)
dbf.write_data_to_db(con, query_insert, data2)
print("\nChecking DB content")
pprint(dbf.get_all_from_db(con, query_get_all))
con.close()
In data2 list, sw7 switch has the same MAC address as sw3 switch already existing in database.
$ python create_sw_inventory_ver3.py
Cheking DB content
[('0000.AAAA.CCCC', 'sw1', 'Cisco 3750', 'London, Green Str'),
('0000.BBBB.CCCC', 'sw2', 'Cisco 3780', 'London, Green Str'),
('0000.AAAA.DDDD', 'sw3', 'Cisco 2960', 'London, Green Str'),
('0011.AAAA.CCCC', 'sw4', 'Cisco 3750', 'London, Green Str')]
Note that the content of switch table before and after adding of information is the same. This means
that no line from data2 list has been written. This is because executemany method is used and within
the same transaction we try to write all four lines. If an error occurs with one of them, all changes
are reversed.
Sometimes it’s exactly the kind of behavior you need. If you want to ignore only row with errors you
should use execute method and write each row separately.
File create_sw_inventory_ver4.py has write_rows_to_db function which writes data in turn and if
there is an error, only changes for specific data are rolled back:
writing attempt.
'''
for row in data:
try:
with connection:
connection.execute(query, row)
except sqlite3.IntegrityError as e:
if verbose:
(continues on next page)
con = dbf.create_connection('sw_inventory3.db')
print('-' * 60)
print('Trying to write data with a duplicate MAC address:')
pprint(data2)
write_rows_to_db(con, query_insert, data2, verbose=True)
print('\nChecking DB content')
pprint(dbf.get_all_from_db(con, query_get_all))
con.close()
$ python create_sw_inventory_ver4.py
Data "0088.AAAA.CCCC, sw8, Cisco 3750, London, Green Str" writing was successful
Cheking DB content
[('0000.AAAA.CCCC', 'sw1', 'Cisco 3750', 'London, Green Str'),
('0000.BBBB.CCCC', 'sw2', 'Cisco 3780', 'London, Green Str'),
('0000.AAAA.DDDD', 'sw3', 'Cisco 2960', 'London, Green Str'),
('0011.AAAA.CCCC', 'sw4', 'Cisco 3750', 'London, Green Str'),
('0055.AAAA.CCCC', 'sw5', 'Cisco 3750', 'London, Green Str'),
('0066.BBBB.CCCC', 'sw6', 'Cisco 3780', 'London, Green Str'),
('0088.AAAA.CCCC', 'sw8', 'Cisco 3750', 'London, Green Str')]
In section 15 there was an example of reviewing the output of command “show ip dhcp snooping
binding”. In the output we received information about parameters of connected devices (interface,
IP, MAC, VLAN).
In this version you can only see all devices connected to switch. If you want to find out others based
on one of the parameters, it’s not convenient in this way.
For example, if you want to get information based on IP address about to which interface the host
is connected, which MAC address it has and in which VLAN it is, then script is not very simple and
more importantly, not convenient.
Let’s write information obtained from the output “sh ip dhcp snooping binding” to SQLite. This will
allow do queries based on any parameter and get missing ones. For this example, it is sufficient to
create a single table where information will be stored.
MAC address is the primary key of our table which is logical because MAC address must be unique.
Additionally, by using expression create table if not exists - SQLite will only create a table if
it does not exist.
Now you have to create a database file, connect to database and create a table (cre-
ate_sqlite_ver1.py file):
import sqlite3
conn = sqlite3.connect('dhcp_snooping.db')
print('Creating schema...')
with open('dhcp_snooping_schema.sql', 'r') as f:
schema = f.read()
conn.executescript(schema)
print("Done")
conn.close()
Comments to file:
• table is created in database (if it does not exist) based on commands specified in
dhcp_snooping_schema.sql file:
Execution of script:
$ python create_sqlite_ver1.py
Creating schema...
Done
You can check that table has been created with sqlite3 utility which allows you to execute queries
directly in command line.
Now it is necessary to write information from the output of “sh ip dhcp snooping binding” command
to the table (dhcp_snooping.txt file):
In the second version of the script, the output in dhcp_snooping.txt file is processed with regular
expressions and then entries are added to database (create_sqlite_ver2.py file):
import sqlite3
import re
result = []
conn = sqlite3.connect('dhcp_snooping.db')
print('Creating schema...')
with open('dhcp_snooping_schema.sql', 'r') as f:
schema = f.read()
conn.executescript(schema)
print('Done')
conn.execute(query, row)
except sqlite3.IntegrityError as e:
print('Error occured: ', e)
conn.close()
Note: For now, you should delete database file every time because script tries to create it every
time you start.
• in regular expression that processes the output of “sh ip dhcp snooping binding”,
numbered groups are used instead of named groups as it was in example of section
‘https://pyneng.readthedocs.io/en/latest/book/14_regex/4a_group_example.html>‘__
• result - a list that stores the result of processing the command output
– query string containes a query. But instead of values, question marks are given. This
query type allows dynamicly substite field values.
– then execute method is passed a query string and row tuple where values are
$ python create_sqlite_ver2.py
Creating schema...
Done
Inserting DHCP Snooping data
That is, it is now possible to get others parameters based on one parameter.
Let’s modify the script to make it check for the presence of dhcp_snooping.db. If you have a database
file you don’t need to create a table, we believe it has already been created.
File create_sqlite_ver3.py:
import os
import sqlite3
import re
data_filename = 'dhcp_snooping.txt'
db_filename = 'dhcp_snooping.db'
schema_filename = 'dhcp_snooping_schema.sql'
result = []
db_exists = os.path.exists(db_filename)
conn = sqlite3.connect(db_filename)
if not db_exists:
print('Creating schema...')
(continues on next page)
conn.close()
Now there is a verification of the presence of database file and dhcp_snooping.db file will only be
created if it does not exist. Data is also written only if dhcp_snooping.db file is not created.
Note: Separating the process of creating a table and completing it with the data is specified in
tasks to the section.
$ rm dhcp_snooping.db
$ python create_sqlite_ver3.py
Creating schema...
Done
Inserting DHCP Snooping data
Let’s check. In case the file already exists but the data is not written:
$ rm dhcp_snooping.db
$ python create_sqlite_ver1.py
Creating schema...
Done
$ python create_sqlite_ver3.py
(continues on next page)
$ python create_sqlite_ver3.py
Database exists, assume dhcp table does, too.
Inserting DHCP Snooping data
Error occurred: UNIQUE constraint failed: dhcp.mac
Error occurred: UNIQUE constraint failed: dhcp.mac
Error occurred: UNIQUE constraint failed: dhcp.mac
Error occurred: UNIQUE constraint failed: dhcp.mac
Now we make a separate script that sends queries to database and displays results. It should:
– parameter name
– parameter value
File get_data_ver1.py:
import sqlite3
import sys
db_filename = 'dhcp_snooping.db'
conn = sqlite3.connect(db_filename)
for k in keys:
print('{:12}: {}'.format(k, row[k]))
print('-' * 40)
– selected key is removed from keys list. Thus, only parameters that you want to display
are left in the list
• connecting to DB
– in SQL the values can be set by a question mark but you cannot give a column name.
Therefore, the column name is substituted by row formatting and the value by SQL tool.
– iterate over the results obtained and show only those fields that are in keys list
----------------------------------------
mac : 00:07:BC:3F:A6:50
ip : 10.1.10.6
interface : FastEthernet0/3
----------------------------------------
The second version of the script to get data with minor improvements:
• Instead of rows formatting, a dictionary that contains queries corresponding to each key is
used.
• Method keys is used to get all columns that match the query
File get_data_ver2.py:
import sqlite3
import sys
db_filename = 'dhcp_snooping.db'
query_dict = {
'vlan': 'select mac, ip, interface from dhcp where vlan = ?',
'mac': 'select vlan, ip, interface from dhcp where mac = ?',
'ip': 'select vlan, mac, interface from dhcp where ip = ?',
'interface': 'select vlan, mac, ip from dhcp where interface = ?'
}
query = query_dict[key]
result = conn.execute(query, (value, ))
• does not check number of arguments that are passed to the script
• It would be good to collect information from different switches. To do this, you should add a
field that indicates on which switch the entry was found
In addition, a lot of work needs to be done in the script that creates database and writes the data.
Further reading
Documentation:
Articles:
Tasks
Warning: Starting from section “4. Data types in Python” there are automated tests for testing
tasks. They help to check whether everything matches the task, and also give feedback on what
does not correspond to the task. As a rule, after the first period of adaptation to tests, it becomes
easier to do tasks with tests. Testing is done using the pyneng utility. Learn more about how to
work with the pyneng utility.
Task 25.1
1. create_db.py
2. add_data.py
The code in scripts should be broken down into functions. It is up to you to decide which functions
and how to split the code. Some of the code can be global.
create_db.py - this script should contain the functionality for creating a database:
• if the file does not exist, according to the description of the database schema in the
dhcp_snooping_schema.sql file, the database must be created
The database should contain two tables (the schema is described in the dhcp_snooping_schema.sql
file):
• dhcp - information obtained from the output of sh ip dhcp snooping binding is stored here
$ python create_db.py
Database creation...
$ python create_db.py
Database exists
add_data.py script adds data to the database. This script should add data from sh ip dhcp snooping
binding output and switch information
• information about switches is added to the switches table. Switch data are in the switches.yml
file
• information based on the output of sh ip dhcp snooping binding is added to the dhcp table
– since the dhcp table has changed, and now there is a switch field, it must also be filled.
The switch name is derived from the data file name
An example of script execution when the database has not yet been created:
$ python add_data.py
The database does not exist. Before adding data, you need to create it
An example of executing the script for the first time after creating the database:
$ python add_data.py
Adding data to the switches table...
Add data to dhcp table...
An example of script execution after the data has been added to the table (the order of adding data
can be arbitrary, but messages must output similarly to the output below):
$ python add_data.py
Adding data to the switches table...
While adding data: ('sw1', 'London, 21 New Globe Walk') An error occurred: UNIQUE␣
,→constraint failed: switches.hostname
While adding data: ('sw2', 'London, 21 New Globe Walk') An error occurred: UNIQUE␣
,→constraint failed: switches.hostname
While adding data: ('sw3', 'London, 21 New Globe Walk') An error occurred: UNIQUE␣
,→constraint failed: switches.hostname
Adding data to the dhcp table...
While adding data: ('00:09:BB:3D:D6:58', '10.1.10.2', '10', 'FastEthernet0/1',
,→'sw1') An error occurred: UNIQUE constraint failed: dhcp.mac
While adding data: ('00:04:A3:3E:5B:69', '10.1.5.2', '5', 'FastEthernet0/10', 'sw1
,→') An error occurred: UNIQUE constraint failed: dhcp.mac
While adding data: ('00:05:B3:7E:9B:60', '10.1.5.4', '5', 'FastEthernet0/9', 'sw1
,→') An error occurred: UNIQUE constraint failed: dhcp.mac
While adding data: ('00:07:BC:3F:A6:50', '10.1.10.6', '10', 'FastEthernet0/3',
,→'sw1') An error occurred: UNIQUE constraint failed: dhcp.mac
While adding data: ('00:09:BC:3F:A6:50', '192.168.100.100', '1', 'FastEthernet0/7
,→', 'sw1') An error occurred: UNIQUE constraint failed: dhcp.mac (continues on next page)
The code in scripts should be broken down into functions. It is up to you to decide which functions
and how to split the code. Some of the code can be global.
Task 25.2
The code in the script should be broken down into functions. It is up to you to decide which functions
and how to split the code. Some of the code can be global.
The script can be passed arguments and, depending on the arguments, you need to display different
information. If the script is called:
• with two arguments, display information from the dhcp table that matches the field and value
• with any other number of arguments, print a message that the script only supports two or zero
arguments
$ python get_data.py
The dhcp table has the following entries:
----------------- --------------- -- ---------------- ---
00:09:BB:3D:D6:58 10.1.10.2 10 FastEthernet0/1 sw1
00:04:A3:3E:5B:69 10.1.5.2 5 FastEthernet0/10 sw1
00:05:B3:7E:9B:60 10.1.5.4 5 FastEthernet0/9 sw1
00:07:BC:3F:A6:50 10.1.10.6 10 FastEthernet0/3 sw1
00:09:BC:3F:A6:50 192.168.100.100 1 FastEthernet0/7 sw1
00:E9:BC:3F:A6:50 100.1.1.6 3 FastEthernet0/20 sw3
00:E9:22:11:A6:50 100.1.1.7 3 FastEthernet0/21 sw3
00:A9:BB:3D:D6:58 10.1.10.20 10 FastEthernet0/7 sw2
00:B4:A3:3E:5B:69 10.1.5.20 5 FastEthernet0/5 sw2
00:C5:B3:7E:9B:60 10.1.5.40 5 FastEthernet0/9 sw2
00:A9:BC:3F:A6:50 10.1.10.60 20 FastEthernet0/2 sw2
----------------- --------------- -- ---------------- ---
(continues on next page)
Task 25.3
In previous tasks, information was added to an empty database. In this task, the script should work
correctly even in a situation where the database already contains information.
Copy the add_data.py script from task 25.1 and try running it again on the existing database. The
output should be like this:
$ python add_data.py
Adding data to the switches table...
While adding data: ('sw1', 'London, 21 New Globe Walk') An error occurred: UNIQUE␣
,→constraint failed: switches.hostname
While adding data: ('sw2', 'London, 21 New Globe Walk') An error occurred: UNIQUE␣
,→constraint failed: switches.hostname
While adding data: ('sw3', 'London, 21 New Globe Walk') An error occurred: UNIQUE␣
,→constraint failed: switches.hostname
Adding data to the dhcp table...
(continues on next page)
When creating the database schema, it was explicitly stated that the MAC address field must be
unique. Therefore, when adding an entry with the same MAC address, an exception (error) is raised.
Task 25.1 handles the exception and writes a message to stdout.
In this task, it is assumed that information is periodically read from the switches and written to files.
After that, the information from the files must be transferred to the database. At the same time,
there may be changes in the new data: MAC disappeared, MAC switched to another port/vlan, a new
MAC appeared, etc.
In this task, in the dhcp table, you need to create a new field, active, which will indicate whether the
record is up-to-date. The new database schema is located in the dhcp_snooping_schema.sql file.
Every time information from files with DHCP snooping output is added again, all existing entries (for
this switch) must be marked as inactive (active = 0). You can then update the information and mark
the new records as active (active = 1).
Thus, the old records will remain in the database for MAC addresses that are currently inactive, and
updated information for active addresses will appear.
And you need to add the following information from the file:
After adding the data, the table should look like this:
• MAC 00:04:A3:3E:5B:69 went to a different port and got into a different interface and got a
different IP address
If some MAC address is not in the new file, it must be left in the database with the value active = 0:
MAC addresses 00:09:BC:3F:A6:50 not in new information (turned off the computer)
Modify the add_data.py script so that the new conditions are met and the active field is populated.
The code in the script should be broken down into functions. It is up to you to decide which functions
and how to split the code. Some of the code can be global.
To check task and operation of a new field, first add information to the database from files
sw*_dhcp_snooping.txt, and then add information from files new_data/sw*_dhcp_snooping.txt.
The data should look like this (the lines can be in any order)
Task 25.4
Copy file get_data.py from task 25.2. Add to the script support for the active column, which we
added in task 25.3.
Now, when requesting information, active records should be displayed first, and then, inactive ones.
If there are no inactive records, do not display the “Inactive records” header.
$ python get_data.py
The dhcp table has the following entries:
Active entries:
----------------- ---------- -- ---------------- --- -
00:09:BB:3D:D6:58 10.1.10.2 10 FastEthernet0/1 sw1 1
00:04:A3:3E:5B:69 10.1.15.2 15 FastEthernet0/15 sw1 1
00:05:B3:7E:9B:60 10.1.5.4 5 FastEthernet0/9 sw1 1
00:07:BC:3F:A6:50 10.1.10.6 10 FastEthernet0/5 sw1 1
00:E9:BC:3F:A6:50 100.1.1.6 3 FastEthernet0/20 sw3 1
00:E9:22:11:A6:50 100.1.1.7 3 FastEthernet0/21 sw3 1
00:A9:BB:3D:D6:58 10.1.10.20 10 FastEthernet0/7 sw2 1
00:B4:A3:3E:5B:69 10.1.5.20 5 FastEthernet0/5 sw2 1
00:A9:BC:3F:A6:50 10.1.10.65 20 FastEthernet0/2 sw2 1
00:A9:33:44:A6:50 10.1.10.77 10 FastEthernet0/4 sw2 1
----------------- ---------- -- ---------------- --- -
Inactive entries:
(continues on next page)
Active entries:
----------------- --------- - --------------- --- -
00:05:B3:7E:9B:60 10.1.5.4 5 FastEthernet0/9 sw1 1
00:B4:A3:3E:5B:69 10.1.5.20 5 FastEthernet0/5 sw2 1
----------------- --------- - --------------- --- -
Inactive entries:
----------------- --------- - --------------- --- -
00:C5:B3:7E:9B:60 10.1.5.40 5 FastEthernet0/9 sw2 0
----------------- --------- - --------------- --- -
Active entries:
----------------- ---------- -- --------------- --- -
00:09:BB:3D:D6:58 10.1.10.2 10 FastEthernet0/1 sw1 1
00:07:BC:3F:A6:50 10.1.10.6 10 FastEthernet0/5 sw1 1
00:A9:BB:3D:D6:58 10.1.10.20 10 FastEthernet0/7 sw2 1
00:A9:33:44:A6:50 10.1.10.77 10 FastEthernet0/4 sw2 1
----------------- ---------- -- --------------- --- -
Task 25.5
After completing tasks 25.1 - 25.5, information about inactive records remains in the database. And,
if some MAC address did not appear in new records, the record with it may remain in the database
forever.
And while it can be useful to see where the MAC address was last located, it is not very useful to
keep this information permanently. Instead, you can delete a record if, for example, it has been
In order to be able to delete records by date, you need to enter a new field in which the last time
the record was added will be recorded.
The new field is called last_active and must contain a string in the format: YYYY-MM-DD HH:MM:SS.
• change the dhcp table accordingly and add a new field. The table can be changed from cli
sqlite, but the dhcp_snooping_schema.sql file must also be changed
You can get a string with time and date in the specified format using the datetime function in an
SQL query. The syntax is as follows:
sqlite> insert into dhcp (mac, ip, vlan, interface, switch, active, last_active)
...> values ('00:09:BC:3F:A6:50', '192.168.100.100', '1', 'FastEthernet0/7',
,→'sw1', '0', datetime('now'));
That is, instead of the value that is written to the database, you must specify datetime(‘now’).
After this command, the following entry will appear in the database:
Task 25.5a
After completing task 25.5, the dhcp table has a new last_active field.
Update the add_data.py script so that it removes all records that were active more than 7 days ago.
In order to get such records, you can manually update the last_active field in some records and set
the time to 7 or more days.
The task file shows an example of working with objects of the datetime module. Shows how to get
the date 7 days ago. It will be necessary to compare the last_active time with this date.
Please note that date strings that are written to the database can be compared with each other.
now = datetime.today().replace(microsecond=0)
week_ago = now - timedelta(days=7)
#print(now)
#print(week_ago)
#print(now > week_ago)
#print(str(now) > str(week_ago))
Task 25.6
This task contains the parse_dhcp_snooping.py file. You cannot change anything in the
parse_dhcp_snooping.py file.
The file creates several functions and describes the command line arguments that the file takes.
There is support for arguments to perform all the actions that, in the previous tasks, were performed
in the files create_db.py, add_data.py and get_data.py.
And the goal of this task is to create all the necessary functions in the
parse_dhcp_snooping_functions.py file based on the information in the parse_dhcp_snooping.py
file.
It is necessary to create the corresponding functions and transfer to them the functionality that is
described in the previous tasks. All the necessary information is present in the create, add, get
functions, in the parse_dhcp_snooping.py file.
In principle, to complete the task, it is not necessary to understand the argparse module, but, you
can read about it in this section
You can create any helper functions in parse_dhcp_snooping_functions.py, not just those called from
parse_dhcp_snooping.py.
• creating a database
• adding information based on the output of sh ip dhcp snooping binding from files
• selection of information from the database (by parameter and all information)
To make it easier to understand what the script call will look like, here are some examples. The
examples show the option when the database has active and last_active fields, but you can also use
the option without these fields.
optional arguments:
-h, --help show this help message and exit
--db DB_FILE database name
-k {mac,ip,vlan,interface,switch}
parameter for searching records
-v VALUE parameter value
-a show all database content
positional arguments:
filename file(s) to add
optional arguments:
-h, --help show this help message and exit
--db DB_FILE database name
-s if the flag is set, add switch data, otherwise add DHCP
records
Active entries:
Active entries:
(continues on next page)
661
Python for network engineers, Release 1.0
Starting with “4. Python Data Types”, automated tests are used to validate tasks. They help to
check whether everything matches the task, and also give feedback on what does not correspond
to the task. As a rule, after the first period of adaptation to tests, it becomes easier to do tasks with
tests.
In addition to the positive points listed above, in the tests you can also see what the final result is
needed: to clarify the data structure and little things that can affect the result.
To run the tests, pyneng.py is used - a script that is located in the job repository.
Tasks must be done in prepared files. For example, section 04_data_structures contains task 4.3.
Open the exercises/04_data_structures/task_4_3.py file and write code for the task directly in this
file after the task description.
This is important because tests are tied to the fact that solution for the tasks is written in specific
files and in a specific directory structure. In addition to the fact that the tasks must be solved in
the prepared files, be sure to copy the entire exercises directory (or even better, the entire pyneng-
examples-exercises-en repository), since tests depend on files in the exercises directory, not only
on files in the specific tasks directory.
First, you need to install it so that you don’t have to write python pyneng.py every time.
To install the script, the pyneng.py and setup.py files must be in the repository. These files are
located in the root of the repository.
You need to go to your repository, for example (write the name of your repository):
cd my_repo/
pip install .
This will install the module and make it possible to call it in any directory using the word pyneng.
pyneng utility
1. Completion of tasks
2. Checking that the task is working as needed python task_4_2.py or running the script in the
editor/IDE
Note: The second step is very important because it is much easier to find syntax errors and similar
problems with the script at this stage than when running the code through a test in step 3.
The script makes it easier to run tests, since you do not need to specify any parameters, by default
the output is set to verbose and runs with the pytest-clarity plugin, which improves diff when the
solution is different from correct result. Also, some things are hidden, for example, the warning that
pytest shows, so as not to distract from the task.
Tests can still be run with pytest if you are used to it or have used it before. The pyneng script is
just a wrapper around running pytest.
The second part of the script’s work is copying the answers to the tasks. This part is done for
convenience, so that you do not have to look for answers and is made in such a way that first the
task must pass the test and only after that pyneng -a will work and show the answers (copy them
to the current directory). To copy responses, the script clones the asnwers repository into the user’s
home directory, copies the task(s) answer, and deletes the repository with answers.
After completing the task, it must be checked using tests. To run the tests, you need to call pyneng
in the task directory. For example, if you are doing section 4 of the tasks, you need to run pyneng
from the exercises/04_data_structures directory
pyneng
pyneng 1
pyneng 1-3
If there are tasks with letters, for example, in section 7, you can run it in such a way to start checking
for tasks 7.2a, 7.2b (you must be in the 07_files directory):
pyneng 2a-b
pyneng 2*
If the tasks pass the tests, you can see the answers to the tasks.
To do this, add -a to the previous versions of the command. Such a call means running tests for
tasks 1 and 2 and copying the answers if the tests passed:
pyneng 1-2 -a
For the specified tasks, tests will run, and for those tasks that passed the tests, the answers will be
copied to the answer_task_x.py files in the current directory.
pyneng output
Warning
At the end of the test output, “1 warning” is often written. This can be ignored, warnings are mainly
related to the operation of some modules and are hidden so as not to distract from the tasks.
Tests passed
Test failed
When some tests fail, the output shows the difference between what the output should look like and
what output was received.
The differences are shown as Left and Right, unfortunately there is no such thing that the correct
option is highlighted in green, and the wrong one is highlighted in red, you need to look at the
situation. Every time you display differences, there is a line in front of them like this:
In this case, Left is the correct output, right is the output of the task:
another example:
return_value == correct_return_value
In this case, Right is the correct output, Left is the output of the task:
Starting with section “9. Functions” automatic tests are used to check tasks. They help to check
that everything conforms to the task and also provide feedback on what is not up to task. Usually,
after the first period of adaptation it becomes easier to do tasks with tests.
In addition to above-mentioned positive features, tests can also show what result is expected: clarify
structure of data and details that may affect the result.
Pytest basics
Although you don’t have to write tests code but to understand it you should look at an example of
a test. For example, there is the following code with check_ip function:
import ipaddress
def check_ip(ip):
try:
ipaddress.ip_address(ip)
return True
except ValueError as err:
return False
if __name__ == "__main__":
result = check_ip('10.1.1.1')
print('Function result:', result)
Function check_ip checks whether the argument given to it is an IP address. An example of calling
a function with different arguments:
...:
...: def check_ip(ip):
...: try:
...: ipaddress.ip_address(ip)
...: return True
...: except ValueError as err:
...: return False
...:
In [2]: check_ip('10.1.1.1')
Out[2]: True
In [3]: check_ip('10.1.')
Out[3]: False
In [4]: check_ip('a.a.a.a')
Out[4]: False
In [5]: check_ip('500.1.1.1')
Out[5]: False
Now it is necessary to write a test for check_ip function. Test must check that function returns True
when correct address is passed and False when wrong argument is passed.
To simplify task, test can be written in the same file. In pytest, test can be a normal function with
a name that starts with test_. Inside function you have to write conditions that are checked. In
pytest this is done with assert.
assert
assert does nothing if expression is True and generates an exception if expression is False:
In [7]: a = 4
AssertionError:
AssertionError:
After assert and expression you can write a message. If there is a message, it is displayed in
exception:
Test example
pytest uses assert to specify which conditions must be met in order for test to be considered passed.
In pytest, you can write test as a normal function but function name must start with test_. Below
is test_check_ip test which verify check_ip function by passing two values to it: correct address
and wrong one, and after each check the message is written:
import ipaddress
def check_ip(ip):
try:
ipaddress.ip_address(ip)
return True
except ValueError as err:
return False
def test_check_ip():
assert check_ip('10.1.1.1') == True, 'If IP is correct, the fucntion returns␣
,→True'
(continues on next page)
if __name__ == "__main__":
result = check_ip('10.1.1.1')
print('Function result:', result)
Code is written in check_ip_functions.py. Now you have to figure out how to call tests. The easiest
option is to write pytest word. In this case, pytest will automatically detect tests in the current
directory. However, pytest has certain rules, not only by name of function but also by name of test
files - file names should also start with test_. If rules are respected, pytest will automatically find
tests, if not - you have to specify a test file.
$ pytest check_ip_functions.py
========================= test session starts ==========================
platform linux -- Python 3.7.3, pytest-4.6.2, py-1.5.2, pluggy-0.12.0
rootdir: /home/vagrant/repos/general/pyneng.github.io/code_examples/pytest
collected 1 item
check_ip_functions.py . [100%]
By default if tests pass, each test (test_check_ip function) is marked with a dot. Since in this case
there is only one test - test_check_ip function, there is a dot after name check_ip_functions.py and
it is also written below that 1 test has passed.
Now, suppose the function does not work correctly and always returns False (write return False at
the beginning of function). In this case, test execution will look like:
$ pytest check_ip_functions.py
========================= test session starts ==========================
platform linux -- Python 3.6.3, pytest-4.6.2, py-1.5.2, pluggy-0.12.0
rootdir: /home/vagrant/repos/general/pyneng.github.io/code_examples/pytest
collected 1 item
check_ip_functions.py F [100%]
def test_check_ip():
> assert check_ip('10.1.1.1') == True, 'If IP is correct, the fucntion␣
,→returns True'
E AssertionError: If IP is correct, the fucntion returns True
E assert False == True
E + where False = check_ip('10.1.1.1')
check_ip_functions.py:14: AssertionError
======================= 1 failed in 0.06 seconds =======================
If test fails, pytest displays more information and shows where things went wrong. In this case,
after execution of assert check_ip('10.1.1.1') == True string, the expression did not return
True result, so an exception was generated.
Below, pytest shows what it has compared: assert False == True and specifies that False is
check_ip('10.1.1.1'). Looking at the output, one suspects that something is wrong with check_ip
function because it returns False to correct address.
Most tests are written in separate files. For this example, test is only one but it is still in a separate
file.
File test_check_ip_function.py:
def test_check_ip():
assert check_ip('10.1.1.1') == True, 'If IP is correct, the fucntion returns␣
,→True'
File check_ip_functions.py:
import ipaddress
def check_ip(ip):
#return False
try:
ipaddress.ip_address(ip)
return True
except ValueError as err:
return False
if __name__ == "__main__":
result = check_ip('10.1.1.1')
print('Function result:', result)
$ pytest
================= test session starts ========================
platform linux -- Python 3.6.3, pytest-4.6.2, py-1.5.2, pluggy-0.12.0
rootdir: /home/vagrant/repos/general/pyneng.github.io/code_examples/pytest
collected 1 item
test_check_ip_function.py . [100%]
Pytest in course is primarily used for self-tests of tasks. However, this test is not optional - task
is considered done when it complies with all specified points and passes tests. For my part, I also
check tasks with automatic tests and then look at the code, write comments if necessary and show
a solution option.
At first, tests require effort but through a couple of sections they will help solve tasks.
Warning: Tests that are written for course are not a benchmark or best practice of test writing.
Tests are written with maximum emphasis on clarity and many things are done differently.
When solving tasks especially when there are doubts about the final format of data to be ob-
tained, it is better to look into test. For example, if task_9_1.py the corresponding test will be in
test/test_task_9_1.py.
import pytest
import task_9_1
import sys
sys.path.append('..')
def test_function_return_value():
access_vlans_mapping = {
'FastEthernet0/12': 10,
'FastEthernet0/14': 11,
'FastEthernet0/16': 17
}
template_access_mode = [
'switchport mode access', 'switchport access vlan',
'switchport nonegotiate', 'spanning-tree portfast',
'spanning-tree bpduguard enable'
]
correct_return_value = ['interface FastEthernet0/12',
'switchport mode access',
'switchport access vlan 10',
'switchport nonegotiate',
'spanning-tree portfast',
'spanning-tree bpduguard enable',
'interface FastEthernet0/14',
'switchport mode access',
'switchport access vlan 11',
'switchport nonegotiate',
'spanning-tree portfast',
'spanning-tree bpduguard enable',
'interface FastEthernet0/16',
'switchport mode access',
'switchport access vlan 17',
'switchport nonegotiate',
'spanning-tree portfast',
'spanning-tree bpduguard enable']
Note correct_return_value variable - this variable contains the resulting list that should return gener-
ate_access_config function. Therefore for example, if question has arisen of whether to add spaces
before commands or a new line at the end, you can look at what the result requires. Also check your
output against the output in variable_return_value.
The most important thing is where to run tests: all tests must be run from a directory with section
tasks, not from a test directory. For example, in section 09_functions such a directory structure with
tasks:
[~/repos/pyneng-7/pyneng-online-may-aug-2019/exercises/09_functions]
vagrant: [master|✓ ]
$ tree
.
├── config_r1.txt
├── config_sw1.txt
├── config_sw2.txt
├── conftest.py
├── task_9_1a.py
├── task_9_1.py
├── task_9_2a.py
├── task_9_2.py
├── task_9_3a.py
├── task_9_3.py
├── task_9_4.py
└── tests
├── test_task_9_1a.py
├── test_task_9_1.py
├── test_task_9_2a.py
├── test_task_9_2.py
├── test_task_9_3a.py
├── test_task_9_3.py
└── test_task_9_4.py
[~/repos/pyneng-7/pyneng-online-may-aug-2019/exercises/09_functions]
vagrant: [master|✓ ]
$ pytest tests/test_task_9_1.py
========================= test session starts ==========================
platform linux -- Python 3.7.3, pytest-4.6.2, py-1.5.2, pluggy-0.12.0
rootdir: /home/vagrant/repos/pyneng-7/pyneng-online-may-aug-2019/exercises/09_
,→functions
collected 3 items
conftest.py
In addition to test directory there is a conftest.py file - special file in which you can write functions
(more precisely fixtures) common to different tests. For example, this file contains functions that
connect via SSH/Telnet to equipment.
Useful commands
$ pytest tests/test_task_9_1.py
Run one test with more detailed output (shows diff between data in test and what is received from
function):
[~/repos/pyneng-7/pyneng-online-may-aug-2019/exercises/09_functions]
vagrant: [master|✓ ]
$ pytest
======================= test session starts ========================
platform linux -- Python 3.6.3, pytest-4.6.2, py-1.5.2, pluggy-0.12.0
rootdir: /home/vagrant/repos/pyneng-7/pyneng-online-may-aug-2019/exercises/09_
,→functions
collected 21 items
...
Starts all tests of the same section with error messages displayed in one line:
$ pytest --tb=line
argparse
argparse is a module for handling command line arguments. Examples of what a module does:
argparse is not the only module for handling command line arguments. And not even the only one
in standard library.
This book covers only argparse, but in addition it is worth looking at modules that are not part of
standard Python library. For example, click.
Note: A good article, compares different command line argument processing modules (covered
argparse, click and docopt).
import subprocess
import argparse
'''
Ping IP address and return tuple:
On success: (return code = 0, command output)
On failure: (return code, error output (stderr))
'''
reply = subprocess.run(
'ping -c {count} -n {ip}'.format(count=count, ip=ip_address),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding='utf-8'
)
if reply.returncode == 0:
return True, reply.stdout
else:
return False, reply.stdout+reply.stderr
args = parser.parse_args()
print(args)
Creation of a parser:
Adding arguments:
– argument that is passed after -c option will be saved to variable count, but will be con-
verted to a number first. If no argument was specified, the default is 2
String args = parser.parse_args() is specified after all arguments have been defined. After run-
ning it, variable args contains all arguments that were passed to the script. They can be accessed
argparse 677
Python for network engineers, Release 1.0
Let’s try a script with different arguments. If both arguments are passed:
$ python ping_function.py
Namespace(count=2, ip=None)
Traceback (most recent call last):
File "ping_function.py", line 31, in <module>
rc, message = ping_ip( args.ip, args.count )
File "ping_function.py", line 16, in ping_ip
stderr=temp)
File "/usr/local/lib/python3.6/subprocess.py", line 336, in check_output
````kwargs).stdout
File "/usr/local/lib/python3.6/subprocess.py", line 403, in run
with Popen(*popenargs, ````kwargs) as process:
File "/usr/local/lib/python3.6/subprocess.py", line 707, in __init__
(continues on next page)
restore_signals, start_new_session)
File "/usr/local/lib/python3.6/subprocess.py", line 1260, in _execute_child
restore_signals, start_new_session, preexec_fn)
TypeError: expected str, bytes or os.PathLike object, not NoneType
If function was called without arguments when argparse is not used, an error would occur that not
all arguments are specified.
Because of argparse the argument is actually passed, but it has None value. You can see this in
Namespace(count=2, ip=None) string.
In such a script the IP address must be specified at all times. And in argparse you can specify that
argument is mandatory. To do this, change -a option: add required=True at the end:
$ python ping_function.py
usage: ping_function.py [-h] -a IP [-c COUNT]
ping_function.py: error: the following arguments are required: -a
Now you see a clear message that you need to specify a mandatory argument and a usage hint.
$ python ping_function.py -h
usage: ping_function.py [-h] -a IP [-c COUNT]
Ping script
optional arguments:
-h, --help show this help message and exit
-a IP
-c COUNT
Note that in message all options are in optional arguments section. argparse itself determines
that options are specified because they start with - and only one letter in name.
import subprocess
from tempfile import TemporaryFile
import argparse
argparse 679
Python for network engineers, Release 1.0
args = parser.parse_args()
print(args)
Now instead of giving -a option you can simply pass IP address. It will be automatically saved in
host variable. And it’s automatically considered as a mandatory. Тhat is, it is no longer necessary
to specify required=True and dest="ip".
In addition, script specifies messages that will be displayed when you call help. Now script call looks
like this:
help message:
$ python ping_function_ver2.py -h
usage: ping_function_ver2.py [-h] [-c COUNT] host
Ping script
positional arguments:
host IP or name to ping
optional arguments:
-h, --help show this help message and exit
-c COUNT Number of packets
Nested parsers
Note: This example will show more features of argparse but they are not limited to that, so if you
use argparse you should check module documentation or article on PyMOTW.
File parse_dhcp_snooping.py:
# Default values:
DFLT_DB_NAME = 'dhcp_snooping.db'
DFLT_DB_SCHEMA = 'dhcp_snooping_schema.sql'
def create(args):
print("Creating DB {} with DB schema {}".format((args.name, args.schema)))
argparse 681
Python for network engineers, Release 1.0
def add(args):
if args.sw_true:
print("Adding switch data to database")
else:
print("Reading info from file(s) \n{}".format(', '.join(args.filename)))
print("\nAdding data to db {}".format(args.db_file))
def get(args):
if args.key and args.value:
print("Geting data from DB: {}".format(args.db_file))
print("Request data for host(s) with {} {}".format((args.key, args.
,→value)))
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(title='subcommands',
description='valid subcommands',
help='description')
get_parser.add_argument('-k', dest="key",
choices=['mac', 'ip', 'vlan', 'interface', 'switch'],
help='host key (parameter) to search')
get_parser.add_argument('-v', dest="value", help='value of key')
get_parser.add_argument('-a', action='store_true', help='show db content')
get_parser.set_defaults(func=get)
if __name__ == '__main__':
args = parser.parse_args()
if not vars(args):
parser.print_usage()
else:
args.func(args)
Now not only a parser is created as in previous example, but also nested parsers. Nested parsers
will be displayed as commands. In fact, they will be used as mandatory arguments.
With help of nested parsers a hierarchy of arguments and options is created. Arguments that are
added to nested parser will be available as arguments for this parser. For example, this part creates
a nested create_db parser and adds -n option:
Syntax for creating nested parsers and adding arguments to them is the same:
Method add_argument adds an argument. Here, syntax is exactly the same as without nested
parsers.
argparse 683
Python for network engineers, Release 1.0
Function create receives as an argument all arguments that have been passed. And within function
you can access to necessary arguments:
def create(args):
print("Creating DB {} with DB schema {}".format(args.name, args.schema))
$ python parse_dhcp_snooping.py -h
usage: parse_dhcp_snooping.py [-h] {create_db,add,get} ...
optional arguments:
-h, --help show this help message and exit
subcommands:
valid subcommands
{create_db,add,get} description
create_db create new db
add add data to db
get get data from db
Note that each nested parser that is created in the script is displayed as a command in usage hint:
optional arguments:
-h, --help show this help message and exit
-n db-filename db filename
-s SCHEMA db schema filename
In addition to nested parsers, there are also several new features of argparse in this example.
metavar
Argument metavar allows you to specify argument name to show it in usage message and help:
optional arguments:
-h, --help show this help message and exit
-n db-filename db filename
-s SCHEMA db schema filename
• after -n option in both usage and help the name is specified in the metavar parameter
nargs
Parameter nargs allows to specify a certain number of elements that must be entered into this
argument. In this case, all arguments that have been passed to the script after filename argument
will be included in nargs list, but at least one argument must be passed.
positional arguments:
filename file(s) to add to db
optional arguments:
-h, --help show this help message and exit
--db DB_FILE db name
-s add switch data if set, else add normal data
If you pass several files, they’ll be in the list. And since add function simply displays file names, the
output is:
argparse 685
Python for network engineers, Release 1.0
• N - - number of arguments should be specified. Arguments will be in list (even if only one is
specified)
• ? - 0 or 1 argument
• + - all arguments will be in list, but at least one argument has to be passed
choices
get_parser.add_argument('-k', dest="key",
choices=['mac', 'ip', 'vlan', 'interface', 'switch'],
help='host key (parameter) to search')
For some arguments it is important that the value is selected only from certain options. In such
cases you can specify choices.
optional arguments:
-h, --help show this help message and exit
--db DB_FILE db name
-k {mac,ip,vlan,interface,switch}
host key (parameter) to search
-v VALUE value of key
-a show db content
In this example it is important to specify allowed options that could be chosen because based on
chosen option the SQL-query is generated. And thanks to choices there is no pissibility to specify
parameter that is not allowed.
Parser import
In parse_dhcp_snooping.py, the last two lines will only be executed if script has been called as a
main script.
if __name__ == '__main__':
args = parser.parse_args()
args.func(args)
args = parser.parse_args()
args.func(args)
$ python call_pds.py -h
usage: call_pds.py [-h] {create_db,add,get} ...
optional arguments:
-h, --help show this help message and exit
subcommands:
valid subcommands
{create_db,add,get} description
create_db create new db
add add data to db
get get data from db
argparse 687
Python for network engineers, Release 1.0
Arguments can be passed as a list when calling parse_args method (call_pds2.py file):
It is necessary to use split method since parse_args method expects list of arguments.
The result will be the same as if script was called with arguments:
$ python call_pds2.py
Reading info from file(s)
test.txt, test2.txt
• %d - integer
• %f - float
Output data columns of equal width of 15 characters with right side alignment:
You can also use string formatting to influence the appearance of numbers.
For example, you can specify how many digits to show after comma:
Note: String formatting still has many possibilities. Good examples and explanations of two string
formatting options can be found here.
Naming convention
In general, it is better to adhere to this convention. However, if a particular library or module uses
different convention, it is worth following the style used in them.
Not all rules are described in this section. More information can be found in PEP8 in English
or Russian.
Variable names
Variable names should not overlap with operators and names of modules or other reserved values.
Variable names are usually written entirely in large or small letters. It is better to stick to one of
option within a script/module/package.
If variables are constants for module, it is better to use names written in capital letters:
DB_NAME = 'dhcp_snooping.db'
TESTING = True
db_name = 'dhcp_snooping.db'
testing = True
Modules can use underscores to make names more understandable. For packages it is better to
select short names.
Function names
Function names are given in small letters with underscores between words.
ignore_command = False
Class names
class CiscoSwitch:
Underscore in names
In Python, underscores at the beginning or at the end of a name indicates special names. Most often
it’s just an arrangement but sometimes it actually affects object behavior.
Underscore in name
For example, if you want to get MAC address, IP address, VLAN and interface from line string and
discard the rest of fields, you can use this option:
This record indicates that we do not need the third and fourth elements.
But then it may be unclear why lease and entry_type variables are not used any further. It is better
to call variable names like ignored.
Underscore in interpreter
In the python and ipython interpreter undesrcore is used to get result of the last experision.
In [7]: _
Out[7]: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
In [8]: a = _
In [9]: a
Out[9]: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Single underscore
One underscore before name indicates that the name is used as an internal name.
For example, if one underscore is specified in name of function or method, this means that the object
is an internal implementation and should not be used directly.
But also, when importing from module import * the objects that start with underscore will not be
imported.
db_name = 'dhcp_snooping.db'
_path = '/home/nata/pyneng/'
def func1(arg):
print arg
def _func2(arg):
print arg
If you import all objects from module, those that start with underscore will not be imported:
In [8]: db_name
Out[8]: 'dhcp_snooping.db'
In [9]: _path
...
NameError: name '_path' is not defined
In [10]: func1(1)
1
In [11]: _func2(1)
...
NameError: name '_func2' is not defined
One underscore after name is used when the name of object or parameter overlaps with the em-
bedded names.
Example:
Two underscores
Two underscores before method name are not used simply as an agreement. Such names are trans-
formed into format “class name + method name”. This allows the creation of unique methods and
attributes of classes.
Note: This transformation is only performed if less than two underscore endings or no underscores.
In [15]: dir(Switch)
Out[15]:
['_Switch__configure', '_Switch__quantity', ...]
If you create a subclass, then __configure method will not rewrite method of parent Switch class:
In [17]: dir(CiscoSwitch)
Out[17]:
['_CiscoSwitch__configure', '_CiscoSwitch__quantity', '_Switch__configure', '_
,→Switch__quantity', ...]
• __name__ - this variable is equal to __main__ when script runs directly, and it is equal to module
name when imported
• __file__ - this variable is equal to script name that was run directly, and equals to complete
path to the module when it is imported
__name__ variable is most commonly used to indicate that a certain part of the code must be exe-
cuted only when module is executed directly:
return a * b
if __name__ == '__main__':
print(multiply(3, 5))
__file__ variable can be useful in determining the current path to script file:
import os
print('__file__', __file__)
print(os.path.abspath(__file__))
__file__ example2.py
/home/vagrant/repos/tests/example2.py
Python also denotes special methods in this way. These methods are called when using Python
functions and operators and allow for implementation of a certain functionality.
As a rule, such methods need not be called directly. But for example, when creating your own class
it may be necessary to describe such method in order to make object support some operations in
Python.
For example, in order to get object length, it must support __len__ method.
Another special method __str__ is called when print operator is used or str function is called. If
it is necessary to get a certain output, you have to create this method in the class:
In [12]: sw1.set_name('sw1')
In [14]: str(sw1)
Out[14]: 'Switch sw1'
There are many such special methods in Python. Some useful links where you can read about a
particular method:
• documentation
Unicode
In Python 3, string is str type but in addition bytes type appeared in Python 3:
In [4]: line.encode('utf-8')
Out[4]: b'\xd1\x82\xd0\xb5\xd1\x81\xd1\x82'
print function
In Python 2.7 it is possible to put arguments in parentheses, but it doesn’t make print a function
and print returns another result (tuple):
In Python 2.7, raw_input function was used to get information from user as a string:
In [11]: number
Out[11]: '55'
In [13]: number
Out[13]: '55'
In [14]: range(5)
Out[14]: [0, 1, 2, 3, 4]
In [15]: xrange(5)
Out[15]: xrange(5)
In [16]: list(xrange(5))
Out[16]: [0, 1, 2, 3, 4]
In [17]: range(5)
Out[17]: range(0, 5)
In [18]: list(range(5))
Out[18]: [0, 1, 2, 3, 4]
Dictionary methods
Methods keys, values, items in Python 3 return views instead of lists. The peculiarity of view is
that they change with the change of dictionary. And in fact, they just give you a way to look at
corresponding objects but they don’t make a copy of them.
In [20]: d.
d.clear d.get d.iteritems d.keys d.setdefault d.viewitems
d.copy d.has_key d.iterkeys d.pop d.update d.viewkeys
d.fromkeys d.items d.itervalues d.popitem d.values d.viewvalues
And in Python 3:
In [22]: d.
clear() get() pop() update()
copy() items() popitem() values()
fromkeys() keys() setdefault()
Variables unpacking
In [24]: a
Out[24]: 1
In [25]: b
Out[25]: [2, 3, 4]
In [26]: c
Out[26]: 5
^
SyntaxError: invalid syntax
subprocess.run
Python 3.5 introduced the new run function in subprocess module. It provides a more user-friendly
interface for working with module and getting output of commands.
Accordingly, run function is used instead of call and check_output functions. But call and
check_output functions remain.
Jinja2
In Jinja2 module it is no longer necessary to use such code, since the default encoding is utf-8:
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
In the templates themselves as in Python, dictionary methods have changed. Here, you should use
items instead of iteritems.
Modules pexpect, telnetlib, paramiko send and receive bytes, so you have to make encode/decode
accordingly.
Trivia
• Starting from Python 3.6, csv.DictReader returns OrderedDict instead of a regular dictionary.
Additional information
Documentation:
Articles:
• The key differences between Python 2.7.x and Python 3.x with examples
Preparing Windows
Download and install Python 3.7. Be sure to check the “Add Python 3.7 to PATH” checkbox.
python --version
Note: If you are not going to use the Mu editor, you can install 3.8 as well, but it is better to look at
Mu first. For the basic topics covered in the course, there are practically no changes in 3.8, so you
can safely use Python 3.7.
Cmder
Since we work with git on the course, you need to install Cmder to work on the course. To do this,
select “Download Full”.
After installing Cmder, git is immediately available in it and you need to figure out how it works and
configure it to work with Github.
Installing Mu
The only caveat with installing Mu on windows is that it is better to install it via pip so that the
modules that are installed in pip are visible. That is, you need to install it like this:
The pexpect module does not work on Windows, and since it is not needed to perform tasks, this
only affects the fact that it will not be possible to repeat the examples from the book.
All other modules work, but with some there are nuances.
graphviz
To complete the tasks in sections 11 and 17, you will need graphviz. And you will need to install the
Python module:
csv
When working with csv on Windows, you always need to specify newline ="" when opening a file:
textfsm
Some of the modules that textfsm uses are not available for Windows. And at the same time, they
are not needed for our use of textfsm. For textfsm to work correctly on Windows, you need to
comment out some lines in the terminal.py file in the textfsm directory.
How to find the directory textfsm. First, we look at where the site-packages directory is located:
In [3]: sys.path
Out[3]:
...
'c:\\users\\nata\\appdata\\local\\programs\\python\\python37\\lib\\site-packages
,→',
...
Then we go to this directory and inside it we look for the textfsm directory. In the textfsm directory,
open the terminal.py file and comment out the lines in this way:
# import fcntl
import getopt
import os
(continues on next page)
import re
import struct
import sys
# import termios
import time
# import tty
In the last third of the course, you will need network equipment to complete the assignments. You
can use real or virtual network equipment and any equipment control system: GNS3, UNL or other.
• Click Next
• select “Install the hardware that i manually select from a list”. Click Next
• select Microsoft in the left column and Microsoft KM-TEST Loopback in the right. Click Next
• click Next
• click Finish
Then in GNS3 select this loopback interface for connecting the “cloud”.
Preparing Linux
wget https://www.python.org/ftp/python/3.7.3/Python-3.7.3.tgz
tar xvf Python-3.7.3.tgz
cd Python-3.7.3
./configure --enable-optimizations --enable-loadable-sqlite-extensions
sudo make altinstall
Virtual environment
After installation, in ~/.bashrc file in current user’s home folder, you need to add several lines:
export VIRTUALENVWRAPPER_PYTHON=/usr/local/bin/python3.7
export WORKON_HOME=~/venv
. /usr/local/bin/virtualenvwrapper.sh
exec bash
Create a virtual environment using Python 3.7 (the same command will take you to a virtual envi-
ronment):
pip install pytest pytest-clarity pyyaml tabulate jinja2 textfsm pexpect netmiko
Information is usually hard to grasp from the first time. Especially new information.
If you do your homework and make notes during your study, you learn a lot more information than
if you just read a book. But most likely, in some way you’ll have to read about the same information
several times.
Book provides only basics of Python and therefore it is necessary to continue to learn and to repeat
already completed topics and to learn new ones. And there are a lot of options:
These resources are listed selectively, considering you’ve already read the book. But in addition,
I’ve made a compilation of resources where other materials can be found.
Most likely, after reading the book there will be ideas what you can automate at work. It’s a great
option, because it’s always easier to learn on a real problem. But it is better to go beyond work
tasks and study Python further.
Python allows you to do quite a lot with only basic knowledge. Therefore, with work tasks it is not
always possible to increase level of knowledge, but knowing Python better you can usually solve the
same problems much more easily. So it’s best not to stop and learn.
The following resources are connected to network equipment and generally Python. Depending on
from what materials you learn best you can select a book or video course from list
707
Python for network engineers, Release 1.0
Books:
• Network Programmability and Automation: Skills for the Next-Generation Network Engineer
• Mastering Python Networking (Eric Chou) - is partly similar to what was discussed in this book
but there are many new themes. Plus, examples are considered not only on Cisco equipment
but on Juniper and Arista as well.
• Kirk Byers
• Jason Edelman
• Matt Oswalt
• Michael Kashin
• Henry Ölsner
• Mat Wood
• Show 198 – Kirk Byers On Network Automation With Python & Ansible
• Show 270: Design & Build 9: Automation With Python And Netmiko
Projects:
• CiscoConfParse - library that parses Cisco IOS configurations. It can: check existing
router/switch configurations, get a certain part of configuration, change configuration
• NAPALM - NAPALM (Network Automation and Programmability Abstraction Layer with Multiven-
dor support) - library that allows working with network equipment of different vendors using a
unified API
• NOC Project - NOC is scalable, high-performance and open-source OSS system for ISP, service
and content providers
• netdev
• Nornir
• eNMS
General Python
Books
Basic level:
• Think Python - good book on Python basics. There are tasks in the book.
• Automate the Boring Stuff with Python - in this book you can find many ideas on automation
of daily work. These topics are: working with PDF, Excel, Word, sending letters, working with
pictures, working with the web
Medium/advanced level:
• Python Tricks - excellent for 2-3 book on Python. Book describes various aspects of Python and
how to use it correctly. The book is fairly new (late 2017) and covers Python 3.
• Effective Python: 59 Specific Ways to Write Better Python (Effective Software Development
Series) - book of useful advice on how best to write code
• Dive Into Python 3 - briefly covers fundamentals of Python and then more advanced topics:
closure, generators, tests and so on. Book written in 2009 but covers Python 3 and 99% of
topics remained unchanged.
• Problem Solving with Algorithms and Data Structures using Python - excellent book on data
structures and algorithms. Many examples and homework.
• Fluent Python - excellent book on more advanced topics. Even topics that are obsolete in the
current version of Python (asyncio) are worth reading for a perfect explanation of topic.
• Python Cookbook - great recipe book. A huge number of scenarios are considered with solutions
and explanations.
Courses
• MITx - 6.00.1x Introduction to Computer Science and Programming Using Python - a very good
course in Python. It’s a great way to continue your study after
book. In it you will repeat material on Python basics but from a different angle and
learn a lot of new things. There’s a lot of practical tasks and it’s pretty intense.
• Python от Computer Science Center - an excellent video lecture on Python. There are some
basics and more advanced topics
Coding challenges
• Bites of Py
• HackerRank - on this resource tasks are broken down by fields: algorithms, regular expressions,
databases and others. But there are basic tasks as well
Podcasts
Podcasts will generally broaden the horizon and give an idea of various Python projects, modules
and libraries:
• Talk Python To Me
Documentation
711
Python for network engineers, Release 1.0
713