Hello my friend,
In the previous blog post we briefly touched on the conditionals, when we talked about looking for presence of some element in Python list or Go slice. So I thought, it would make sense to introduce now the key concept of the code flow control, which are conditionals and loops. These items are essential for any production code, so let’s see how it works.
Does Automation Come Last?
Surfing through the LinkedIn today I’ve found an interesting picture, which was attributed to Elon Musk and Twitter (or X, how is that called now):
I don’t if that is really related to Mr Musk and Twitter in any capacity, but thoughts it contains are quite important: your first remove all unnecessary steps and optimize everything you can, before you start any automation. That’s very true and in our network automation trainings we talk about how to optimize network operations processes to ensure that they are viable for automation. Join our network trainings to learn how to build viable automation:
We offer the following training programs in network automation for you:
- Zero-to-Hero Network Automation Training
- High-scale automation with Nornir
- Ansible Automation Orchestration with Ansble Tower / AWX
- Expert-level training: Closed-loop Automation and Next-generation Monitoring
During these trainings you will learn the following topics:
- Success and failure strategies to build the automation tools.
- Principles of software developments and the most useful and convenient tools.
- Data encoding (free-text, XML, JSON, YAML, Protobuf).
- Model-driven network automation with YANG, NETCONF, RESTCONF, GNMI.
- Full configuration templating with Jinja2 based on the source of truth (NetBox).
- Best programming languages (Python, Bash) for developing automation
- The most rock-solid and functional tools for configuration management (Ansible) and Python-based automation frameworks (Nornir).
- Network automation infrastructure (Linux, Linux networking, KVM, Docker).
- Orchestration of automation workflows with AWX and its integration with NetBox, GitHub, as well as custom execution environments for better scalability.
- Collection network data via SNMP and streaming telemetry with Prometheus
- Building API gateways with Python leveraging Fast API
- Integration of alerting with Slack and your own APIs
- … and many more
Moreover, we put all mentions technologies in the context of real use cases, which our team has solved and are solving in various projects in the service providers, enterprise and data centre networks and systems across the Europe and USA. That gives you opportunity to ask questions to understand the solutions in-depth and have discussions about your own projects. And on top of that, each technology is provided with online demos and labs to master your skills thoroughly. Such a mixture creates a unique learning environment, which all students value so much. Join us and unleash your potential.
What Are We Going To Talk Today About?
Code flow controls are instructions, which allow your code to work in multiple different scenarios. For example, you have an input list, and you want to modify each its element in some way, depending on what is the current value of each element. Or to put it in the context of the network / IT infrastructure automation, you fetch from some external source a list of network devices, which you want to poll. This list of devices contains IP addresses and hostnames and you want to connect to remote sources only, if you have IP addresses available. So you want to loop through all the elements of collected inventory and start polling job in case the IP address is provided.
Summing that up, in today blog post we are going to cover:
- What are the loops, which types they have and how to use them in Python and Go (Golang)?
- What are the conditionals, which types they have and how to use them in Python and Go (Golang)?
Once we complete this overview, you shall be savvy in how to use these powerful tools to make your applications more robust and repeatable.
Explanation
As it comes from the name, code flow control means that there are certain structures, which controls how your code is being executed. For example, some parts of your code are to be executed only if certain conditions are met. That is achieved using conditionals.
Conditionals
If – else if – else Condition
This is possibly the most popular condition, which majority of people, who even doesn’t have massive software development background, already know. The idea here is that only part of code, where condition is met, is executed, whilst others aren’t. In a pseudo code, which though looks very Pythonic) the example could be:
1
2
3
4
5
6 if network_operating_system == "cisco-nxos":
parse_nxos(...)
else if network_operating_system == "arista-eos":
parse_eos(...)
else:
do_nothing(...)
The conditions are evaluated from top to bottom, so the first matching condition will trigger execution of the associated code, whilst further conditions aren’t check. So sequence matters.
In case, if you have only two choices, “else if” statement isn’t needed and your code will be having just “if” and “else” statements:
1
2
3
4 if network_operating_system == "cisco-nxos":
parse_nxos(...)
else:
do_nothing(...)
Or in certain cases, even “if” alone is good enough:
1
2 if network_operating_system not in ["cisco-nxos", "arista-eos"]:
exit(...)
The “if – else if – else” condition exists both in Python and Go (Golang), but their syntax is slightly different:
Python | Go / Golang |
---|---|
if network_operating_system == “cisco-nxos”: parse_nxos(…) elif network_operating_system == “arista-eos”: parse_eos(…) else: do_nothing(…) | if network_operating_system == “cisco-nxos” { parseNxos(…) } else if network_operating_system == “arista-eos” { parseEos(…) } else { doNothing(…) } |
Switch – case Condition
This is another approach, which can be used to implement conditional statements. In a pseudo code that will look like:
1
2
3
4
5
6
7 switch network_operating_system:
case "cisco-nxos":
parse_nxos(...)
case "arista-eos":
parse_eos(...)
case no_match:
do_nothing(...)
In the same way as in the previous example for “if-else“, the top down approach for conditionals matching is taken with the first matching “case” statement being executed. Which condition to use, “if-else” or “switch-case” often depends on the use or style of code:
- “switch-case” is often used when your key may have different value, like a selector or so, and you want to execute specific action based on this value
- “if-else” is often used, when certain parts of code shall be executed if some complex conditions are met.
From my experience, in almost all the scenarios you can replace switch-case with if and in many cases vice versa. So, ensure you make yourself comfortable with both and use something relevant for the context.
It is possible to add instruction to continue evaluating of other case statements after the match so that more than one part is executed. That is though considered anti-pattern and isn’t recommended.
The “switch-case” condition exists in Go (Golang) since its inception, whilst in Python it was added relatively recently (just in Python 3.10). The syntax is different between Go (Golang) and Python:
Python | Go / Golang |
---|---|
match network_operating_system: case “cisco-nxos”: parse_nxos(…) case “arista-eos”: parse_eos(…) case _: do_nothing(…) | switch network_operating_system { case “cisco-nxos”: parseNxos(…) case “arista-eos”: parseEos(…) default: doNsothing(…) } |
Loops
There are three distinctive types of looks, each being used in a specific scenario. The names of these types aren’t necessary existing in programming books, but I find them useful to describe their contexts and distinct syntax
Iterative Loops
The first one is “iterative loops“. They are used, when you need to go through multiple iterations during processing of something. For example, you have a list of network devices to poll configuration from and you want do it via connecting to every device, issuing some “show” commands and fetching the output. So the pseudo code will look like:
1
2
3 devices = ["leaf-01", "leaf-02"]
for element in devices:
do_something(...)
In this example you will be iterating over each element of the list and doing some job associated with it.
This type of loop exists both in Python and Go (Golang), but with slightly different syntax:
Python | Go / Golang |
---|---|
devices = [“leaf-01”, “leaf-02”] for element in devices: do_something(…) | devices := []string{“leaf-01”, “leaf-02”} for _, element := range devices: doSomething(…) |
Important difference between Python and Go is that in Go you use “range” operator, which always will return index of the element. So you must store it in “_“, if you don’t need it.
Conditional Loops
The second type of loops are “conditional loops“. These types of loops are executed whilst certain condition is met and stops execution, when condition isn’t met any anymore. Quite often they are used, when you are fetching some data over multiple iterations and you don’t know in advance how many iterations you may need to do. For example, when you fetch data from NetBox using REST API, you will get by default only 50 entries. If you want to get more, you need to do another poll request. And another, until you haven’t fetched all of them. The pseudo code is:
1
2
3
4 next, devices = get_devices(...)
while next:
next, extra_devices = get_devices(...)
devices.extend(extra_devices)
This code is executed as long as there is some value retrieved into “next“. Think about, next contains URL for next 50 entries, which shall be patched and once you made to the end of the inventory, next will be an empty string.
This type of the loop exists both in Go / Golang, but have different syntax:
Python | Go / Golang |
---|---|
next, devices = get_devices(…) while next: next, extra_devices = get_devices(…) devices.extend(extra_devices) | next, devices = getDevices(…) for next != “” { next, extra_devices = getDevices(…) devices = append(devices, extra_devices) } |
Go uses “for” instruction for all types of loops, whilst Python has different instructions for conditional and iterative loops.
Infinite Loops
Finally, the infinite loops are used in scenarios, where you create long running applications, which needs to constantly do something. For example, when you create a web service, which processes users requests, you will start infinite loop, which listens to user requests and once the request is received, then it performs some actions, possible returning some response back and then waiting for next request. Pseudo code:
1
2 while True:
do_something(...)
As you see the syntax is the same as with conditional loop. The only difference is that you’d hard code the condition to be true all the time. Let’s take a look on real syntax in Python and Go:
Python | Go / Golang |
---|---|
while True: do_something(…) | for { doSomething(…) } |
Short summary
We’ve just scratched the surface of conditionals and flow controls. There is a huge amount of details available for you to read further about both these structures in Python and Go / Golang. We encourage you to do so:
Examples
To put all these theoretical concept in motion so that you see how it works, let’s implement the following scenario:
- You have a list of lists of network devices and some their parameters, which shall be enough to imitate connection to network devices and to parse the configuration.
- You will loop through this list one by one and perform some actions.
- You will try to imitate the connection and data collection. If an IP address is missing in the initial input, you shall not progress with data collection and shall notify user about it.
- If you have collected data from network device, you shall attempt to parse if the operating system was defined or notify user if operating system is missing
- Print overall results of what was collected / parsed per device.
Let’s develop a software solution for this task.
Python
The following code is will service the purpose:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56 """From Python to Go: Python: 005 - Code Flow Control: Conditionals and Loops"""
# Variables
# Your initial inventory
inventory = [
["leaf-01", "cisco-nxos", "192.168.1.1"],
["leaf-02", "arista-eos", "192.168.1.2"],
["spine-01", "cisco-nxos", ""],
["spine-02", "arosta-eos", "192.168.1.12"],
]
# Functions
def get_data(ip_address: str) -> list:
"""Function that pretends to connect to network device and collect some output"""
# Ensure there is IP address to connect
if not ip_address:
return (False, "There is no IP address provided")
# Return some mock data
return (True, "Some raw data")
def parse_data(parser: str, data: str) -> list:
"""Function that pretends to parse the output from network device depeinding on its operating system type"""
match parser:
case "cisco-nxos":
return (True, f"parsed: {data}")
case "arista-eos":
return (True, f"parsed: {data}")
case _:
return (False, "There is no parser available")
# Execution
if __name__ == "__main__":
# Loop through all network devices
for device in inventory:
# Collect data for each network device
collected_data = get_data(device[2])
parsed_data = []
# Do parsing if data is collected
if collected_data[0]:
parsed_data = parse_data(device[1], collected_data[1])
else:
print(f"Collecting data from {device[0]} is not successful")
continue
# Print results
if parsed_data[0]:
print(f"Successfully collected and parsed data for {device[0]}")
else:
print(f"Successfully collected but NOT parsed data for {device[0]}")
This code solely relies on explanation we’ve done earlier. Although, we’ll add some more:
- The input is list of lists, where each nested list contains 3 parameters: hostname, IP address and type of the operating system the device runs. In future posts we’ll cover more advanced options, but it is good enough to start. It is also worth mentioning that such a structure (list of lists) is a common way to read data from databases, where each nested list represents a row in a database.
- The overall execution in Python is now put under conditional. This is a good practice to start your execution with “if __name__ == “__main__”” statement to ensure that his part of code is executed only if you deliberately launch this file and not via some side import.
- Looping through devices and performing some actions.
- The imitated actions (collecting data and parsing data) are implemented in sub-functions.
- Within each function, there are some validation using either if-else or switch-case statements are performed.
- The first value of return from each function signifies if the operation was successful or not.
Execution of this code:
1
2
3
4
5 $ python3.10 main.py
Successfully collected and parsed data for leaf-01
Successfully collected and parsed data for leaf-02
Collecting data from spine-01 is not successful
Successfully collected but NOT parsed data for spine-02
It is important to use Python3.10+ to run this code, as match-case hasn’t existed before.
Go (Golang)
Now it is time to implement the similar logic in Go / Golang:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65 /* From Python to Go: Python: 005 - Code Flow Control */
package main
import "fmt"
// Aux functions
func getData(ipAddress string) []string {
/* Function that pretends to connect to network device and collect some output */
// Ensure there is IP address to connect
if ipAddress == "" {
return []string{"NOT OK", "There is no IP address provided"}
}
// Return some mock data
return []string{"OK", "Some raw data"}
}
func parseData(parser, data string) []string {
/* Function that pretends to parse the output from network device depeinding on its operating system type */
switch parser {
case "cisco-nxos":
return []string{"OK", fmt.Sprintf("parsed: %v\n", data)}
case "arista-eos":
return []string{"OK", fmt.Sprintf("parsed: %v\n", data)}
default:
return []string{"NOT-OK", "There is no parser available"}
}
}
// Main
func main() {
// Define inventory
inventory := [][]string{
{"leaf-01", "cisco-nxos", "192.168.1.1"},
{"leaf-02", "arista-eos", "192.168.1.2"},
{"spine-01", "cisco-nxos", ""},
{"spine-02", "arosta-eos", "192.168.1.12"},
}
// Loop through all network devices
for _, device := range inventory {
// Collect data for each network device
collectedData := getData(device[2])
var parsedData []string
// Do parsing if data is collected
if collectedData[0] == "OK" {
parsedData = parseData(device[1], collectedData[1])
} else {
fmt.Printf("Collecing data from %v is not successful\n", device[0])
continue
}
// Print results
if parsedData[0] == "OK" {
fmt.Printf("Successfully collected and parsed data for %v\n", device[0])
} else {
fmt.Printf("Successfully collected but NOT parsed data for %v\n", device[0])
}
}
}
Let’s focus on the main differences between this code and Python, as the rest shall already be understandable to from previous blog posts, this blog post and using the comments in code to match to same blocks in Python:
- As we discussed in the previous blog post, in contrast to Python, Go strictly defines the data types of elements in list and all the elements must be homogeneous:
- In Python we’ve used list with two elements (first being boolean and second being string) to return results from auxiliary functions.
- In Go / Golang we are using the list of two elements, both are strings as we have defined list of list of strings using statement “[][]string{}“.
- This fact also impacted our conditional statements:
- In Python we simply write “if collected_data[0]” to check if boolean is True or if the value has non-default value (e.g., string is not empty, integer/float is not 0, etc).
- In Go / Gloang, as we are using strings here, we come up with “OK” / “NOT-OK” values to replace boolean True/False.
- Both in Python and Go / Golang we’ve used “continue” instruction, which can be used in all kinds of the loop. This instruction means to start new iterations of the loop immediately, regardless of all not-yet executed lines of code within this loop. In this scenario, we force next iteration to the next cycle if we were not able to collect data from the network device and, therefore, there is no sense to try to parse it.
And here is the execution:
1
2
3
4
5 $ go run .
Successfully collected and parsed data for leaf-01
Successfully collected and parsed data for leaf-02
Collecting data from spine-01 is not successful
Successfully collected but NOT parsed data for spine-02
Lessons in GitHub
You can find the final working versions of the files from this blog at out GitHub page.
Conclusion
First of all, we hope that by now you start getting some taste of Python and Go and are seeing similarities as well as differences. We haven’t yet started applying this concepts to network automation, but will do that right in a few blog posts when we cover a few more fundamentals. Secondly, we’d like you to encourage to do all the practical exercises, if you haven’t yet. Labs are essential for you to be able to pick up new programming language, whether it is Go/Golang, C, or Python. Take care and good bye.
Support us
P.S.
If you have further questions or you need help with your networks, I’m happy to assist you, just send me message. Also don’t forget to share the article on your social media, if you like it.
BR,
Anton Karneliuk