diff --git a/lessons/lesson-13/stage4/notebook.ipynb b/lessons/lesson-13/stage4/notebook.ipynb
index 1cd26cc9..8127508d 100644
--- a/lessons/lesson-13/stage4/notebook.ipynb
+++ b/lessons/lesson-13/stage4/notebook.ipynb
@@ -163,7 +163,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "NAPALM also comes with a few [built-in templates] you can use to perform some common tasks without building your own configuration.\n",
+ "NAPALM also comes with a few [built-in templates](https://napalm.readthedocs.io/en/latest/support/#available-configuration-templates) you can use to perform some common tasks without building your own configuration.\n",
"\n",
"> You can see that even the small number of templates included in the project include examples that will only work on some vendor devices. This is the challenge with trying to unify existing configuration paradigms.\n",
"\n",
diff --git a/lessons/lesson-22/stage1/configs/vqfx1.txt b/lessons/lesson-22/stage1/configs/vqfx1.txt
deleted file mode 100644
index f4bf02ab..00000000
--- a/lessons/lesson-22/stage1/configs/vqfx1.txt
+++ /dev/null
@@ -1,171 +0,0 @@
-
- 15.1X53-D60.4
-
- vqfx1
-
- $1$mlo32jo6$BOMVhmtORai2Kr24wRCCv1
-
- ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key
-
-
-
-
- antidote
- super-user
-
- $1$iH4TNedH$3RKJbtDRO.N4Ua8B6LL/v/
-
-
-
- set-transitions
- 0
-
-
-
-
- allow
-
-
-
-
-
-
-
-
- 8080
-
-
-
-
-
-
- *
-
- any
-
-
-
-
- messages
-
- any
-
-
-
- authorization
-
-
-
-
- interactive-commands
-
- interactive-commands
-
-
-
-
-
-
- juniper
-
- juniper
- commercial
-
-
-
- chef
-
- juniper
- commercial
-
-
-
-
-
-
- em0
-
- 0
-
-
-
- 10.0.0.15/24
-
-
-
-
-
-
- em3
-
- 0
-
-
-
- 10.31.0.11/24
-
-
-
-
-
-
- em4
-
- 0
-
-
-
- 10.12.0.11/24
-
-
-
-
-
-
-
-
- default
-
-
-
-
-
-
- 64001
-
-
-
-
-
- PEERS
- external
-
- 10.31.0.13
- 64003
-
-
- 10.12.0.12
- 64002
-
-
-
-
-
- default
-
-
-
- 5
- 2
-
- all
-
-
-
-
-
- default
- 1
-
-
-
\ No newline at end of file
diff --git a/lessons/lesson-22/stage1/configs/vqfx2.txt b/lessons/lesson-22/stage1/configs/vqfx2.txt
deleted file mode 100644
index 2370adb5..00000000
--- a/lessons/lesson-22/stage1/configs/vqfx2.txt
+++ /dev/null
@@ -1,171 +0,0 @@
-
- 15.1X53-D60.4
-
- vqfx2
-
- $1$mlo32jo6$BOMVhmtORai2Kr24wRCCv1
-
- ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key
-
-
-
-
- antidote
- super-user
-
- $1$iH4TNedH$3RKJbtDRO.N4Ua8B6LL/v/
-
-
-
- set-transitions
- 0
-
-
-
-
- allow
-
-
-
-
-
-
-
-
- 8080
-
-
-
-
-
-
- *
-
- any
-
-
-
-
- messages
-
- any
-
-
-
- authorization
-
-
-
-
- interactive-commands
-
- interactive-commands
-
-
-
-
-
-
- juniper
-
- juniper
- commercial
-
-
-
- chef
-
- juniper
- commercial
-
-
-
-
-
-
- em0
-
- 0
-
-
-
- 10.0.0.15/24
-
-
-
-
-
-
- em3
-
- 0
-
-
-
- 10.12.0.12/24
-
-
-
-
-
-
- em4
-
- 0
-
-
-
- 10.23.0.12/24
-
-
-
-
-
-
-
-
- default
-
-
-
-
-
-
- 64002
-
-
-
-
-
- PEERS
- external
-
- 10.12.0.11
- 64001
-
-
- 10.23.0.13
- 64003
-
-
-
-
-
- default
-
-
-
- 5
- 2
-
- all
-
-
-
-
-
- default
- 1
-
-
-
\ No newline at end of file
diff --git a/lessons/lesson-22/stage1/configs/vqfx3.txt b/lessons/lesson-22/stage1/configs/vqfx3.txt
deleted file mode 100644
index bddc87c0..00000000
--- a/lessons/lesson-22/stage1/configs/vqfx3.txt
+++ /dev/null
@@ -1,171 +0,0 @@
-
- 15.1X53-D60.4
-
- vqfx3
-
- $1$mlo32jo6$BOMVhmtORai2Kr24wRCCv1
-
- ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key
-
-
-
-
- antidote
- super-user
-
- $1$iH4TNedH$3RKJbtDRO.N4Ua8B6LL/v/
-
-
-
- set-transitions
- 0
-
-
-
-
- allow
-
-
-
-
-
-
-
-
- 8080
-
-
-
-
-
-
- *
-
- any
-
-
-
-
- messages
-
- any
-
-
-
- authorization
-
-
-
-
- interactive-commands
-
- interactive-commands
-
-
-
-
-
-
- juniper
-
- juniper
- commercial
-
-
-
- chef
-
- juniper
- commercial
-
-
-
-
-
-
- em0
-
- 0
-
-
-
- 10.0.0.15/24
-
-
-
-
-
-
- em3
-
- 0
-
-
-
- 10.23.0.13/24
-
-
-
-
-
-
- em4
-
- 0
-
-
-
- 10.31.0.13/24
-
-
-
-
-
-
-
-
- default
-
-
-
-
-
-
- 64003
-
-
-
-
-
- PEERS
- external
-
- 10.23.0.12
- 64002
-
-
- 10.31.0.11
- 64001
-
-
-
-
-
- default
-
-
-
- 5
- 2
-
- all
-
-
-
-
-
- default
- 1
-
-
-
\ No newline at end of file
diff --git a/lessons/lesson-22/stage1/guide.md b/lessons/lesson-22/stage1/guide.md
index 2ce9200d..21ccc9a9 100644
--- a/lessons/lesson-22/stage1/guide.md
+++ b/lessons/lesson-22/stage1/guide.md
@@ -1,35 +1,149 @@
-# Working with Network APIs
-## Part 1 - Your First API Call
+# Introduction to Python
+## Part 1 - The Python Interpreter and Basic Data Types
-
- This course comes with a video, and it's highly recommended that you watch this first. Click the "Lesson Video" button above to watch!
-
+Python is a powerful programming language - combining the simplicity and ease-of-use of a scripting language, with many of the more powerful features
+needed to build modern fully-fledged applications. In the world of network automation, it has emerged as the de-facto choice for those that want to be able to perform automated tasks as quickly and simply as possible.
-You've undoubtedly heard a lot about APIs in recent years. But what are they all about? And why should you as, a network engineer, care?
+In this lesson, we'll cover the basics of Python, and ensure you have a basic understanding of how to work with it in your journey to Network Reliability Engineering.
-In much the same way that the CLI was built for humans to consume, an API is intended to be consumed by software. In the modern data center alone, there are a virtually unlimited number of interactions taking place between the various IT systems, such as sharing information, updating databases, performing configurations, and more. These are all happening without **direct** human intervention, even though a human at some point, probably long ago, told the software how to do that.
+You may hear Python referred to as an "interpreted language". Generally, when people use this term, what they mean is that you don't have to "compile" a Python program. By simply executing your code, the Python "interpreter" takes care of performing the necessary steps to translate Python code into lower-level languages that can eventually be understood by the CPU.
-In this lesson we'll start actually working with REST APIs, so we can better understand **what** they are, and then we'll move into some practical examples that will show you how you can use REST APIs in your day-to-day.
+This is in contrast to a language like C, where you must first compile your entire program before you can execute it. Because of this, not only can we build Python applications as source code files (ending in `.py`) that we store on the filesystem, like we would with any language, we can also execute Python instructions line-by-line using the interpreter, just like we do with the Bash shell when we execute commands like `echo` or `ls`.
-For this exercise, we'll be working with the [Junos REST API](https://www.juniper.net/documentation/en_US/junos/topics/concept/rest-api-overview.html), but it should be noted that nearly every network operating system these days has their own REST API. The kind of information you have to put into them, as well as what you get out of them, won't all be the same, but they'll follow the same general ideas we'll explore here.
-
-If you've ever worked on a Linux system, or maybe an Apple computer, you might have run into a command called `curl`. This is a simple tool for fetching the contents of a web page. A simple example would be to request the front page of `google.com`:
+To "enter" the Python interpreter, merely type:
```
-curl https://google.com
+python
```
-What we've just done is effectively the same as what your browser would do if you were to navigate to Google there - it requests a resource from the remote server, and the server responds with the HTML you see in your terminal. Of course, the intention is that the browser would then render this HTML into something we can look at. `curl` performs no such function, so we just get the raw HTML.
+The prompt has now changed to `>>>` indicating that you're now in the Python interpreter shell. Any commands you type here must be Python-compatible (any of the Bash commands you may have learned in Linux environments, or other lessons, won't work here).
-What if, instead, we were to query a resource somewhere that wasn't even intended to be rendered visually in a browser? Remember, machines don't care what things "look" like, they just need the information in as efficient a manner as possible. If we add a few parameters, and change the URL, we can query a resource from one of our vQFX switches that fits this description:
+One of the most basic tasks we'll need to be able to do is work with variables in Python. We'll start off by running a command, and then explaining what we just did:
-```
-curl \
- -u "antidote:antidotepassword" \
- http://vqfx1:8080/rpc/get-interface-information \
- --header "Accept: application/json"
+```python
+new_var = "Hello, World!"
```
+We just created a variable. These are placeholders for data that we want to work with in our Python programs. Variables can hold many different kinds of values in Python. We'll briefly explore many of these in this lab. The type we saw in the snippet above is called a **string**". The value we entered can be found inside the double-quotes, and as you can see, this is simply a sequence of characters.
+
+A really handy way to tell the type of a variable is to pass it in to the `type()` function:
+
+```python
+type(new_var)
+```
+
+
+We can use the builtin `print()` function to print the contents of any variable back out to our terminal:
+
+```python
+print(new_var)
+```
+
+
+Finally, you can add two strings together and form a new string. This is called **concatenation**. There are a variety of ways to do this in Python,
+but one of the simplest is shown below simply by using the `+` operator between the two strings:
+
+```python
+print(new_var + " This is our new Python program!")
+```
+
+
+Another type in Python is the `integer`. These can be numbers like 10, 42, or -1:
+
+```python
+a = 10
+b = 42
+c = -1
+type(a)
+type(b)
+type(c)
+```
+
+
+We can also use the `+` sign here, but instead of concatenating the two variables, they're mathematically summed, as one might expect:
+
+```python
+print(a + b + c)
+```
+
+
+One of the simplest, but often the most commonly used types is the `boolean` type. These are one of two possible values: `True`, and `False`.
+
+```python
+bool_variable = True
+type(bool_variable)
+```
+
+
+Note there are no double-quotes here - this is not a string! Both `True` and `False` are special keywords in Python that represent these boolean values. It's also important to note that boolean variables can also be negated, using the `not` keyword:
+
+```python
+print(not bool_variable)
+```
+
+
+There are a few other basic types, but that will suffice for now. We should now touch on a few more of the advanced types within Python.
+
+### Advanced Types
+
+Sometimes, the data you want to work with doesn't come in a simple string, or integer. Sometimes it needs more structure to organize the data in the way that makes the most sense.
+
+As with the previous section, we'll only touch on a subset of what's available in Python, but for the purposes of arming you properly for network automation with Python, it's crucial we cover at least two of these advanced data structures. The `list`, and the `dictionary`.
+
+If you have any experience with programming whatsoever, you undoubtedly have encountered the term `array`. This is the most common term for being able to store a sequence of values or variables. In Python, while other similar constructs exist, the commonly used tool for this is called the `list`.
+
+Lists are created using bracket notation, like so:
+
+```python
+my_list = [1, 2, 3]
+```
+
+
+Another built-in function we haven't tried yet, `len()` is used to calculate the length of any data type, including lists, that has a length.
+
+```python
+len(my_list)
+```
+
+
+Lists in Python are remarkably flexible, especially compared to arrays in other more strongly typed languages. For instance, we can add and remove items from a list very easily:
+
+```python
+my_list.append(4)
+print(my_list)
+my_list.remove(4)
+print(my_list)
+```
+
+
+In additon, you don't even have to store items of the same type in a list. The below is totally acceptable!
+
+```python
+crazy_list = [123, "hello!", True, -1]
+```
+
+
+Items within a list can be directly referred to via their index. Lists are zero-indexed, which means that the first item is at
+index 0, the second at index 1, and so on.
+
+```python
+print(my_list[0])
+print(my_list[2])
+```
+
+
+However, sometimes you need to store bits of data in a sequence, but don't necessarily know or care about the order. This can make it difficult to look up that data later by index, because you won't know what it is. In that case, you may want to consider another advanced data type, Dictionaries. As with Lists, Python uses the term "dictionary" where other languages might use "map", or "hash", but these all mean roughly the same thing. A Dictionary is a way to store key/value pairs. You can think of these almost like little mini-variables within a single data type. For instance, in our first example, we created a string variable. We can create a dictionary to hold this value, and refer to that value using another string:
+
+```python
+my_dict = {
+ "my_str": "Hello, World!"
+}
+print(my_dict)
+```
+
+
+While it's fun and easy to use the Python interpreter to get familiar with Python commands, the real power is in being able to build repeatable Python code and store it on disk as a program or script. So, for the remainder of this lesson, we'll set aside the interactive interpreter shell, and instead, work through examples embedded in Jupyter notebooks.
+See you in the next lab!
\ No newline at end of file
diff --git a/lessons/lesson-22/stage2/notebook.ipynb b/lessons/lesson-22/stage2/notebook.ipynb
new file mode 100644
index 00000000..8127508d
--- /dev/null
+++ b/lessons/lesson-22/stage2/notebook.ipynb
@@ -0,0 +1,195 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Multi-Vendor Network Automation with NAPALM\n",
+ "\n",
+ "## Part 4 - Make Configuration Changes with NAPALM\n",
+ "\n",
+ "> This lesson uses Jupyter notebooks to provide a guided experience with in-line, executable and editable code snippets. Read here for more details on how to interact with these."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Everything we've done thus far is retrieve information from our network device. While this is a very powerful tool to have, we'll eventually need to make changes on our device. Does NAPALM support this?\n",
+ "\n",
+ "The good news is - yes, it totally does. However, this part isn't always as abstracted as the \"getter\" functions we've seen thus far. By retrieving data, we can focus only on very particular use cases, and NAPALM will do the hard work of translating the retrieved data from vendor-specific APIs into a common format. The problem with trying to do this with configuration data is that vendors have wildly different configuration syntaxes, and it's not very useful to only focus on configuring a small subset of a device's capabilities.\n",
+ "\n",
+ "> Hopefully this will eventually improve, with the advent of [OpenConfig](http://www.openconfig.net/), models which can be used with the [napalm-yang](https://napalm-yang.readthedocs.io/en/latest/root/supported_models/index.html) project to provide vendor-agnostic configurations. Unfortunately only a handful of vendors currently support OpenConfig.\n",
+ "\n",
+ "So, what we end up having to do is construct a vendor-specific configuration outside of NAPALM, such as with a templating language like Jinja, and then pass this in to one of the configuration functions in NAPALM. Let's do that now.\n",
+ "\n",
+ "Let's assume you have a variable titled `vqfx1_config` that contains a partial configuration for setting the description of an interface. Ideally you would have built this from a template using Jinja and something like YAML, but we'll just create this explicitly for now so we can learn how to apply it using NAPALM:\n",
+ "\n",
+ "> Try changing the description yourself in the XML structure below, by editing the text between the `` and `` tags."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "vqfx1_config = \"\"\"\n",
+ "\n",
+ " \n",
+ " \n",
+ " em0\n",
+ " \n",
+ " 0\n",
+ " This is em0, and it connects to something.\n",
+ " \n",
+ " \n",
+ " \n",
+ "\n",
+ "\"\"\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Again, we need to recreate the NAPALM driver and device objects so we can use them in this notebook:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import napalm\n",
+ "driver = napalm.get_network_driver(\"junos\")\n",
+ "device = driver(hostname=\"vqfx1\", username=\"antidote\", password=\"antidotepassword\")\n",
+ "device.open()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Next, we can use a function we haven't seen yet, called `load_merge_candidate`, and pass our configuration string as the `config` parameter.\n",
+ "\n",
+ "> If we had the config stored in a file, we could use the `filename` parameter instead.\n",
+ "\n",
+ "The `merge` portion of that function name is the \"strategy\" used for applying that config. In short, this will attempt to merge the existing configuration with the changes you're proposing, and only change what needs to change to incorporate the new configuration."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "device.load_merge_candidate(config=vqfx1_config)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "As is common on modern network operating systems, the changes we've proposed have gone into a candidate configuration store, which means they've been sent to the device, but the device hasn't started using the new changes. We have to `commit` them in order to do that. As is custom in these situations, we can use the `compare_config` to see the exact diff of what will happen to the configuration if we were to commit it now:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {
+ "scrolled": false
+ },
+ "outputs": [],
+ "source": [
+ "print(device.compare_config())\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If we didn't like the diff we saw above, we could call `device.discard_config()` to get rid of the changes we proposed as candidate. However, this looks good to us, so we can apply the changes with a `commit_config`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "device.commit_config()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "> The function `load_replace_candidate` is similar, but instead of attempting to merge the configuration, it overwrites the entire configuration with what you pass in. **USE WITH CAUTION** as you will need to make sure your configuration is exactly what it needs to be.\n",
+ "\n",
+ "Now that our change is applied, we can use the `get_interfaces` function to see the description applied to the operational state of the device:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [],
+ "source": [
+ "device.get_interfaces()['em0.0']['description']"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Often, we make mistakes and need to roll back a change. Let's say we made a typo in the description and just want to undo what we just committed. No worries - the `rollback` function does this for us."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "device.rollback()\n",
+ "device.get_interfaces()['em0.0']['description']"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "NAPALM also comes with a few [built-in templates](https://napalm.readthedocs.io/en/latest/support/#available-configuration-templates) you can use to perform some common tasks without building your own configuration.\n",
+ "\n",
+ "> You can see that even the small number of templates included in the project include examples that will only work on some vendor devices. This is the challenge with trying to unify existing configuration paradigms.\n",
+ "\n",
+ "That's it for now! In future versions of the lesson (or perhaps in other lessons) we'll dive deeper into some of the more specific tools within NAPALM for targeted workflows. For now, check out the [NAPALM documentation](https://napalm.readthedocs.io) for more information.\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.6.6"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/lessons/lesson-22/stage3/notebook.ipynb b/lessons/lesson-22/stage3/notebook.ipynb
new file mode 100644
index 00000000..8127508d
--- /dev/null
+++ b/lessons/lesson-22/stage3/notebook.ipynb
@@ -0,0 +1,195 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Multi-Vendor Network Automation with NAPALM\n",
+ "\n",
+ "## Part 4 - Make Configuration Changes with NAPALM\n",
+ "\n",
+ "> This lesson uses Jupyter notebooks to provide a guided experience with in-line, executable and editable code snippets. Read here for more details on how to interact with these."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Everything we've done thus far is retrieve information from our network device. While this is a very powerful tool to have, we'll eventually need to make changes on our device. Does NAPALM support this?\n",
+ "\n",
+ "The good news is - yes, it totally does. However, this part isn't always as abstracted as the \"getter\" functions we've seen thus far. By retrieving data, we can focus only on very particular use cases, and NAPALM will do the hard work of translating the retrieved data from vendor-specific APIs into a common format. The problem with trying to do this with configuration data is that vendors have wildly different configuration syntaxes, and it's not very useful to only focus on configuring a small subset of a device's capabilities.\n",
+ "\n",
+ "> Hopefully this will eventually improve, with the advent of [OpenConfig](http://www.openconfig.net/), models which can be used with the [napalm-yang](https://napalm-yang.readthedocs.io/en/latest/root/supported_models/index.html) project to provide vendor-agnostic configurations. Unfortunately only a handful of vendors currently support OpenConfig.\n",
+ "\n",
+ "So, what we end up having to do is construct a vendor-specific configuration outside of NAPALM, such as with a templating language like Jinja, and then pass this in to one of the configuration functions in NAPALM. Let's do that now.\n",
+ "\n",
+ "Let's assume you have a variable titled `vqfx1_config` that contains a partial configuration for setting the description of an interface. Ideally you would have built this from a template using Jinja and something like YAML, but we'll just create this explicitly for now so we can learn how to apply it using NAPALM:\n",
+ "\n",
+ "> Try changing the description yourself in the XML structure below, by editing the text between the `` and `` tags."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "vqfx1_config = \"\"\"\n",
+ "\n",
+ " \n",
+ " \n",
+ " em0\n",
+ " \n",
+ " 0\n",
+ " This is em0, and it connects to something.\n",
+ " \n",
+ " \n",
+ " \n",
+ "\n",
+ "\"\"\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Again, we need to recreate the NAPALM driver and device objects so we can use them in this notebook:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import napalm\n",
+ "driver = napalm.get_network_driver(\"junos\")\n",
+ "device = driver(hostname=\"vqfx1\", username=\"antidote\", password=\"antidotepassword\")\n",
+ "device.open()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Next, we can use a function we haven't seen yet, called `load_merge_candidate`, and pass our configuration string as the `config` parameter.\n",
+ "\n",
+ "> If we had the config stored in a file, we could use the `filename` parameter instead.\n",
+ "\n",
+ "The `merge` portion of that function name is the \"strategy\" used for applying that config. In short, this will attempt to merge the existing configuration with the changes you're proposing, and only change what needs to change to incorporate the new configuration."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "device.load_merge_candidate(config=vqfx1_config)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "As is common on modern network operating systems, the changes we've proposed have gone into a candidate configuration store, which means they've been sent to the device, but the device hasn't started using the new changes. We have to `commit` them in order to do that. As is custom in these situations, we can use the `compare_config` to see the exact diff of what will happen to the configuration if we were to commit it now:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {
+ "scrolled": false
+ },
+ "outputs": [],
+ "source": [
+ "print(device.compare_config())\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If we didn't like the diff we saw above, we could call `device.discard_config()` to get rid of the changes we proposed as candidate. However, this looks good to us, so we can apply the changes with a `commit_config`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "device.commit_config()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "> The function `load_replace_candidate` is similar, but instead of attempting to merge the configuration, it overwrites the entire configuration with what you pass in. **USE WITH CAUTION** as you will need to make sure your configuration is exactly what it needs to be.\n",
+ "\n",
+ "Now that our change is applied, we can use the `get_interfaces` function to see the description applied to the operational state of the device:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [],
+ "source": [
+ "device.get_interfaces()['em0.0']['description']"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Often, we make mistakes and need to roll back a change. Let's say we made a typo in the description and just want to undo what we just committed. No worries - the `rollback` function does this for us."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "device.rollback()\n",
+ "device.get_interfaces()['em0.0']['description']"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "NAPALM also comes with a few [built-in templates](https://napalm.readthedocs.io/en/latest/support/#available-configuration-templates) you can use to perform some common tasks without building your own configuration.\n",
+ "\n",
+ "> You can see that even the small number of templates included in the project include examples that will only work on some vendor devices. This is the challenge with trying to unify existing configuration paradigms.\n",
+ "\n",
+ "That's it for now! In future versions of the lesson (or perhaps in other lessons) we'll dive deeper into some of the more specific tools within NAPALM for targeted workflows. For now, check out the [NAPALM documentation](https://napalm.readthedocs.io) for more information.\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.6.6"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/lessons/lesson-22/stage4/notebook.ipynb b/lessons/lesson-22/stage4/notebook.ipynb
new file mode 100644
index 00000000..8127508d
--- /dev/null
+++ b/lessons/lesson-22/stage4/notebook.ipynb
@@ -0,0 +1,195 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Multi-Vendor Network Automation with NAPALM\n",
+ "\n",
+ "## Part 4 - Make Configuration Changes with NAPALM\n",
+ "\n",
+ "> This lesson uses Jupyter notebooks to provide a guided experience with in-line, executable and editable code snippets. Read here for more details on how to interact with these."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Everything we've done thus far is retrieve information from our network device. While this is a very powerful tool to have, we'll eventually need to make changes on our device. Does NAPALM support this?\n",
+ "\n",
+ "The good news is - yes, it totally does. However, this part isn't always as abstracted as the \"getter\" functions we've seen thus far. By retrieving data, we can focus only on very particular use cases, and NAPALM will do the hard work of translating the retrieved data from vendor-specific APIs into a common format. The problem with trying to do this with configuration data is that vendors have wildly different configuration syntaxes, and it's not very useful to only focus on configuring a small subset of a device's capabilities.\n",
+ "\n",
+ "> Hopefully this will eventually improve, with the advent of [OpenConfig](http://www.openconfig.net/), models which can be used with the [napalm-yang](https://napalm-yang.readthedocs.io/en/latest/root/supported_models/index.html) project to provide vendor-agnostic configurations. Unfortunately only a handful of vendors currently support OpenConfig.\n",
+ "\n",
+ "So, what we end up having to do is construct a vendor-specific configuration outside of NAPALM, such as with a templating language like Jinja, and then pass this in to one of the configuration functions in NAPALM. Let's do that now.\n",
+ "\n",
+ "Let's assume you have a variable titled `vqfx1_config` that contains a partial configuration for setting the description of an interface. Ideally you would have built this from a template using Jinja and something like YAML, but we'll just create this explicitly for now so we can learn how to apply it using NAPALM:\n",
+ "\n",
+ "> Try changing the description yourself in the XML structure below, by editing the text between the `` and `` tags."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "vqfx1_config = \"\"\"\n",
+ "\n",
+ " \n",
+ " \n",
+ " em0\n",
+ " \n",
+ " 0\n",
+ " This is em0, and it connects to something.\n",
+ " \n",
+ " \n",
+ " \n",
+ "\n",
+ "\"\"\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Again, we need to recreate the NAPALM driver and device objects so we can use them in this notebook:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import napalm\n",
+ "driver = napalm.get_network_driver(\"junos\")\n",
+ "device = driver(hostname=\"vqfx1\", username=\"antidote\", password=\"antidotepassword\")\n",
+ "device.open()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Next, we can use a function we haven't seen yet, called `load_merge_candidate`, and pass our configuration string as the `config` parameter.\n",
+ "\n",
+ "> If we had the config stored in a file, we could use the `filename` parameter instead.\n",
+ "\n",
+ "The `merge` portion of that function name is the \"strategy\" used for applying that config. In short, this will attempt to merge the existing configuration with the changes you're proposing, and only change what needs to change to incorporate the new configuration."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "device.load_merge_candidate(config=vqfx1_config)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "As is common on modern network operating systems, the changes we've proposed have gone into a candidate configuration store, which means they've been sent to the device, but the device hasn't started using the new changes. We have to `commit` them in order to do that. As is custom in these situations, we can use the `compare_config` to see the exact diff of what will happen to the configuration if we were to commit it now:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {
+ "scrolled": false
+ },
+ "outputs": [],
+ "source": [
+ "print(device.compare_config())\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If we didn't like the diff we saw above, we could call `device.discard_config()` to get rid of the changes we proposed as candidate. However, this looks good to us, so we can apply the changes with a `commit_config`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "device.commit_config()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "> The function `load_replace_candidate` is similar, but instead of attempting to merge the configuration, it overwrites the entire configuration with what you pass in. **USE WITH CAUTION** as you will need to make sure your configuration is exactly what it needs to be.\n",
+ "\n",
+ "Now that our change is applied, we can use the `get_interfaces` function to see the description applied to the operational state of the device:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [],
+ "source": [
+ "device.get_interfaces()['em0.0']['description']"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Often, we make mistakes and need to roll back a change. Let's say we made a typo in the description and just want to undo what we just committed. No worries - the `rollback` function does this for us."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "device.rollback()\n",
+ "device.get_interfaces()['em0.0']['description']"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "NAPALM also comes with a few [built-in templates](https://napalm.readthedocs.io/en/latest/support/#available-configuration-templates) you can use to perform some common tasks without building your own configuration.\n",
+ "\n",
+ "> You can see that even the small number of templates included in the project include examples that will only work on some vendor devices. This is the challenge with trying to unify existing configuration paradigms.\n",
+ "\n",
+ "That's it for now! In future versions of the lesson (or perhaps in other lessons) we'll dive deeper into some of the more specific tools within NAPALM for targeted workflows. For now, check out the [NAPALM documentation](https://napalm.readthedocs.io) for more information.\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.6.6"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/lessons/lesson-22/stage5/notebook.ipynb b/lessons/lesson-22/stage5/notebook.ipynb
new file mode 100644
index 00000000..8127508d
--- /dev/null
+++ b/lessons/lesson-22/stage5/notebook.ipynb
@@ -0,0 +1,195 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Multi-Vendor Network Automation with NAPALM\n",
+ "\n",
+ "## Part 4 - Make Configuration Changes with NAPALM\n",
+ "\n",
+ "> This lesson uses Jupyter notebooks to provide a guided experience with in-line, executable and editable code snippets. Read here for more details on how to interact with these."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Everything we've done thus far is retrieve information from our network device. While this is a very powerful tool to have, we'll eventually need to make changes on our device. Does NAPALM support this?\n",
+ "\n",
+ "The good news is - yes, it totally does. However, this part isn't always as abstracted as the \"getter\" functions we've seen thus far. By retrieving data, we can focus only on very particular use cases, and NAPALM will do the hard work of translating the retrieved data from vendor-specific APIs into a common format. The problem with trying to do this with configuration data is that vendors have wildly different configuration syntaxes, and it's not very useful to only focus on configuring a small subset of a device's capabilities.\n",
+ "\n",
+ "> Hopefully this will eventually improve, with the advent of [OpenConfig](http://www.openconfig.net/), models which can be used with the [napalm-yang](https://napalm-yang.readthedocs.io/en/latest/root/supported_models/index.html) project to provide vendor-agnostic configurations. Unfortunately only a handful of vendors currently support OpenConfig.\n",
+ "\n",
+ "So, what we end up having to do is construct a vendor-specific configuration outside of NAPALM, such as with a templating language like Jinja, and then pass this in to one of the configuration functions in NAPALM. Let's do that now.\n",
+ "\n",
+ "Let's assume you have a variable titled `vqfx1_config` that contains a partial configuration for setting the description of an interface. Ideally you would have built this from a template using Jinja and something like YAML, but we'll just create this explicitly for now so we can learn how to apply it using NAPALM:\n",
+ "\n",
+ "> Try changing the description yourself in the XML structure below, by editing the text between the `` and `` tags."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "vqfx1_config = \"\"\"\n",
+ "\n",
+ " \n",
+ " \n",
+ " em0\n",
+ " \n",
+ " 0\n",
+ " This is em0, and it connects to something.\n",
+ " \n",
+ " \n",
+ " \n",
+ "\n",
+ "\"\"\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Again, we need to recreate the NAPALM driver and device objects so we can use them in this notebook:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import napalm\n",
+ "driver = napalm.get_network_driver(\"junos\")\n",
+ "device = driver(hostname=\"vqfx1\", username=\"antidote\", password=\"antidotepassword\")\n",
+ "device.open()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Next, we can use a function we haven't seen yet, called `load_merge_candidate`, and pass our configuration string as the `config` parameter.\n",
+ "\n",
+ "> If we had the config stored in a file, we could use the `filename` parameter instead.\n",
+ "\n",
+ "The `merge` portion of that function name is the \"strategy\" used for applying that config. In short, this will attempt to merge the existing configuration with the changes you're proposing, and only change what needs to change to incorporate the new configuration."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "device.load_merge_candidate(config=vqfx1_config)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "As is common on modern network operating systems, the changes we've proposed have gone into a candidate configuration store, which means they've been sent to the device, but the device hasn't started using the new changes. We have to `commit` them in order to do that. As is custom in these situations, we can use the `compare_config` to see the exact diff of what will happen to the configuration if we were to commit it now:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {
+ "scrolled": false
+ },
+ "outputs": [],
+ "source": [
+ "print(device.compare_config())\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If we didn't like the diff we saw above, we could call `device.discard_config()` to get rid of the changes we proposed as candidate. However, this looks good to us, so we can apply the changes with a `commit_config`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "device.commit_config()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "> The function `load_replace_candidate` is similar, but instead of attempting to merge the configuration, it overwrites the entire configuration with what you pass in. **USE WITH CAUTION** as you will need to make sure your configuration is exactly what it needs to be.\n",
+ "\n",
+ "Now that our change is applied, we can use the `get_interfaces` function to see the description applied to the operational state of the device:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [],
+ "source": [
+ "device.get_interfaces()['em0.0']['description']"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Often, we make mistakes and need to roll back a change. Let's say we made a typo in the description and just want to undo what we just committed. No worries - the `rollback` function does this for us."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "device.rollback()\n",
+ "device.get_interfaces()['em0.0']['description']"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "NAPALM also comes with a few [built-in templates](https://napalm.readthedocs.io/en/latest/support/#available-configuration-templates) you can use to perform some common tasks without building your own configuration.\n",
+ "\n",
+ "> You can see that even the small number of templates included in the project include examples that will only work on some vendor devices. This is the challenge with trying to unify existing configuration paradigms.\n",
+ "\n",
+ "That's it for now! In future versions of the lesson (or perhaps in other lessons) we'll dive deeper into some of the more specific tools within NAPALM for targeted workflows. For now, check out the [NAPALM documentation](https://napalm.readthedocs.io) for more information.\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.6.6"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/lessons/lesson-22/stage6/notebook.ipynb b/lessons/lesson-22/stage6/notebook.ipynb
new file mode 100644
index 00000000..8127508d
--- /dev/null
+++ b/lessons/lesson-22/stage6/notebook.ipynb
@@ -0,0 +1,195 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Multi-Vendor Network Automation with NAPALM\n",
+ "\n",
+ "## Part 4 - Make Configuration Changes with NAPALM\n",
+ "\n",
+ "> This lesson uses Jupyter notebooks to provide a guided experience with in-line, executable and editable code snippets. Read here for more details on how to interact with these."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Everything we've done thus far is retrieve information from our network device. While this is a very powerful tool to have, we'll eventually need to make changes on our device. Does NAPALM support this?\n",
+ "\n",
+ "The good news is - yes, it totally does. However, this part isn't always as abstracted as the \"getter\" functions we've seen thus far. By retrieving data, we can focus only on very particular use cases, and NAPALM will do the hard work of translating the retrieved data from vendor-specific APIs into a common format. The problem with trying to do this with configuration data is that vendors have wildly different configuration syntaxes, and it's not very useful to only focus on configuring a small subset of a device's capabilities.\n",
+ "\n",
+ "> Hopefully this will eventually improve, with the advent of [OpenConfig](http://www.openconfig.net/), models which can be used with the [napalm-yang](https://napalm-yang.readthedocs.io/en/latest/root/supported_models/index.html) project to provide vendor-agnostic configurations. Unfortunately only a handful of vendors currently support OpenConfig.\n",
+ "\n",
+ "So, what we end up having to do is construct a vendor-specific configuration outside of NAPALM, such as with a templating language like Jinja, and then pass this in to one of the configuration functions in NAPALM. Let's do that now.\n",
+ "\n",
+ "Let's assume you have a variable titled `vqfx1_config` that contains a partial configuration for setting the description of an interface. Ideally you would have built this from a template using Jinja and something like YAML, but we'll just create this explicitly for now so we can learn how to apply it using NAPALM:\n",
+ "\n",
+ "> Try changing the description yourself in the XML structure below, by editing the text between the `` and `` tags."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "vqfx1_config = \"\"\"\n",
+ "\n",
+ " \n",
+ " \n",
+ " em0\n",
+ " \n",
+ " 0\n",
+ " This is em0, and it connects to something.\n",
+ " \n",
+ " \n",
+ " \n",
+ "\n",
+ "\"\"\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Again, we need to recreate the NAPALM driver and device objects so we can use them in this notebook:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import napalm\n",
+ "driver = napalm.get_network_driver(\"junos\")\n",
+ "device = driver(hostname=\"vqfx1\", username=\"antidote\", password=\"antidotepassword\")\n",
+ "device.open()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Next, we can use a function we haven't seen yet, called `load_merge_candidate`, and pass our configuration string as the `config` parameter.\n",
+ "\n",
+ "> If we had the config stored in a file, we could use the `filename` parameter instead.\n",
+ "\n",
+ "The `merge` portion of that function name is the \"strategy\" used for applying that config. In short, this will attempt to merge the existing configuration with the changes you're proposing, and only change what needs to change to incorporate the new configuration."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "device.load_merge_candidate(config=vqfx1_config)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "As is common on modern network operating systems, the changes we've proposed have gone into a candidate configuration store, which means they've been sent to the device, but the device hasn't started using the new changes. We have to `commit` them in order to do that. As is custom in these situations, we can use the `compare_config` to see the exact diff of what will happen to the configuration if we were to commit it now:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {
+ "scrolled": false
+ },
+ "outputs": [],
+ "source": [
+ "print(device.compare_config())\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If we didn't like the diff we saw above, we could call `device.discard_config()` to get rid of the changes we proposed as candidate. However, this looks good to us, so we can apply the changes with a `commit_config`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "device.commit_config()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "> The function `load_replace_candidate` is similar, but instead of attempting to merge the configuration, it overwrites the entire configuration with what you pass in. **USE WITH CAUTION** as you will need to make sure your configuration is exactly what it needs to be.\n",
+ "\n",
+ "Now that our change is applied, we can use the `get_interfaces` function to see the description applied to the operational state of the device:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [],
+ "source": [
+ "device.get_interfaces()['em0.0']['description']"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Often, we make mistakes and need to roll back a change. Let's say we made a typo in the description and just want to undo what we just committed. No worries - the `rollback` function does this for us."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "device.rollback()\n",
+ "device.get_interfaces()['em0.0']['description']"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "NAPALM also comes with a few [built-in templates](https://napalm.readthedocs.io/en/latest/support/#available-configuration-templates) you can use to perform some common tasks without building your own configuration.\n",
+ "\n",
+ "> You can see that even the small number of templates included in the project include examples that will only work on some vendor devices. This is the challenge with trying to unify existing configuration paradigms.\n",
+ "\n",
+ "That's it for now! In future versions of the lesson (or perhaps in other lessons) we'll dive deeper into some of the more specific tools within NAPALM for targeted workflows. For now, check out the [NAPALM documentation](https://napalm.readthedocs.io) for more information.\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.6.6"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/lessons/lesson-22/syringe.yaml b/lessons/lesson-22/syringe.yaml
index 38e7c475..cc2270fa 100644
--- a/lessons/lesson-22/syringe.yaml
+++ b/lessons/lesson-22/syringe.yaml
@@ -2,15 +2,16 @@
lessonName: Introduction to Python
lessonId: 22
category: fundamentals
-lessondiagram: https://raw.githubusercontent.com/nre-learning/antidote/master/lessons/lesson-22/lessondiagram.png
-tier: ptr
+# lessondiagram: https://raw.githubusercontent.com/nre-learning/antidote/master/lessons/lesson-22/lessondiagram.png
+tier: prod
prereqs:
- 23 # Linux
-description: Python is THE langage to learn when it comes to network automation. In this lesson, you'll get a basic introduction to Python, an exploration of common Python libraries related to networking and network automation, and prepare to dive deeper in follow-up lessons on using Python for advanced automation.
+description: Python is THE programming/scripting langage to learn when it comes to network automation. In this lesson, you'll get a basic introduction to Python, an exploration of common Python libraries related to networking and network automation, and prepare to dive deeper in follow-up lessons on using Python for advanced automation.
slug: Python
tags:
- python
- programming
+- scripting
utilities:
- name: linux1
@@ -18,10 +19,24 @@ utilities:
stages:
- id: 1
- description: Running Python for the First Time
+ description: The Python Interpreter and Basic Data Types
- # - id: 2
- # description: Basic Python Data Types
+ - id: 2
+ description: Conditionals
+ jupyterLabGuide: true
- # - id: 3
- # description: Making Python Code More Re-usable
+ - id: 3
+ description: Loops
+ jupyterLabGuide: true
+
+ - id: 4
+ description: Functions
+ jupyterLabGuide: true
+
+ - id: 5
+ description: Classes
+ jupyterLabGuide: true
+
+ - id: 6
+ description: Packages and Modules
+ jupyterLabGuide: true