Over the last several years I’ve made a couple of efforts to become better at Python. As it stands now – I’d consider myself comfortable with Python but more in terms of scripting rather than actual Python development. What does Python Development mean? To me – at this point – it’s means writing more than a single Python file that does one thing. So why have my previous efforts failed? I think there have been a few reasons I never progressed much past scripting…

  • I didn’t have a good understanding of Python tooling and the best way to build a project. This meant that I spent a large chunk of time messing around with Python versions, packages, and not actually writing any code.  I was missing the fundamentals in terms of how to work in Python.
  • I didn’t have a real goal in mind.  Trying to learn something through ‘Hello World’ type examples doesn’t work for me.  I need to see real world examples of something I’d actually use in order to understand how it works.  I think this is likely why some of the higher level concepts in Python didn’t fully take hold on the first few attempts.
  • I got stuck in the ‘make it work’ mindset which led me to the copying code snippets kind of mentality.  Again – not a great learning tool for me.
  • As a follow up to the previous point – I also got stuck on the other end of the spectrum.  The ‘Import this and just use it without knowing how it works’ idea didn’t sit with me causing me to try and comprehend all of the code in other packages and modules (Im a detail person.  I need the details).  In some cases this is beneficial but I’m now also of the opinion that some packages you use can just be used without reading all of the code.  Trying to understand code written by Python experts instead of just using it was a great way for me to spend hours spinning.  This boils down to the ‘walk before run’ concept in my opinion.
  • I completely ignored testing.  These is no excuse for this.

So this time I’m going to try something different.  I’m going to start from scratch on a new project and see if I can use what I perceive to be (hoping people call me out on things that aren’t) proper Python development.  The first few posts will cover what I perceive to be Python fundamentals. Things you need to know about setting up Python even before you start writing any serious code.

Enough talk – let’s start…

Virtual Environments

One of the first things that really clicked with me about Python development was the use of Python virtual environments.  If you’ve ever struggled with having multiple versions of Python on your system, and figuring out which one you were using, or which version had which package, you need to look at virtual environments. Virtual environments create a sort of isolated sandbox for you to run your code in.  They can contain a specific version of Python and specific Python packages installed through a tool like PIP.  There’s lots of options and possible configurations but let’s just look at some of the basics of using virtual environments.  To create a virtual environment, we need the virtualenv Python package installed.  If you don’t have it already, you can easily install it through PIP with pip install virtualenv.  Once installed, you can create a virtual environment rather simply by using the following syntax…

In this case – we created a couple of base folders to store our project in and then created a virtual environment called my_venv. If we look inside of our project directory we’ll now see that we have a folder called my_venv

If we dig in a little deeper we see a bin folder that contains all of the Python binaries we need. Also notice that there’s a file called activate in the bin directory.  If we source this file, our terminal will activate this virtual environment.  A common way to do this is from the base project directory as shown below…

Notice how the command line is now prefaced with (my_venv) to indicate that we are running in a given virtual environment. Now if we run a command like pip list we should see a limited number of packages…

Note that we don’t see the package virtualenv which we know exists on our base system (since we’re using it right now). We don’t see any of the base system packages because we’re running in a sandbox. In order to use certain packages in this sandbox (virtual environment) we’d need to install them in the virtual environment. Another less common option is to allow the virtual environment to inherit the packages from the base system. This would be done by passing the flag --system-site-packages when you create the virtual environment. We can demonstrate that here by creating a second virtual environment…

Note: To deactivate a virtual environment simply type deactivate into the terminal.

Notice that we have a ton of packages that the virtual environment perceives to be installed since it’s inheriting them from the base system. So what about the actual Pyhton version we’re using in the virtual environment? If not specified when created, the virtual environment will use the Python version used by the base system. That is – the one specific in your PATH or PYTHONPATH environmental variable. In my case, that’s Python 2.7 which we can see just by starting the Python interpreter from the CLI outside of the virtual environment…

And if we once again activate our virtual environment we’ll see that the versions align perfectly…

To use a different version, we need to tell the virtualenv command which version to use. To do that we can pass the --python flag. The trick to this is passing the path to exact Python executable you wish to use. So let’s dig around a little bit. Im on a Mac so most of my Python versions were installed through HomeBrew. So first let’s see which executable I’m using when I type python outside of my virtual environment…

Whoa. There’s a lot of versions there. The interesting thing is that these are mostly all symlinks pointing to other places. For instance…

Ok so we can see that these are really pointing to HomeBrew install locations. So if we wanted to – we could tell the virtualenv command to point directly to these exact destinations. Or, since the symlinks are there and work, we can just reference the version we want to use so long as it’s resolvable through the PATH variable. For instance…

So as you can see – it just needs some way to find the Python executable you wish to use. In both cases, we ended up with the same version of Python (3.6) in the virtual environment. Nice! So now let’s talk a little bit about package management.

Package Management with PIP

Installing packages for Python is drop dead easy. You can use pip and simply pip install whatever and it works awesome. The problem is – how do you keep track of all of this? Better yet – how do you keep track of what version of what package you are using? If you’ve poked around Python projects for any length of time, you’ve likely seen a file called requirements.txt. This file keeps track of the Python packages and their versions that you need to make the code you downloaded run. Pretty easy right? But tracking these manually can be a sincere pain and something often forgotten. Luckily, PIP (not sure if this should be capitalized or not all the time (I’ll just keep flipping between caps and no caps)) has a feature that can create this for you called freeze

In the above output we installed a package in our virtual environment called pyyaml. We then used the traditional pip list command to show that it was installed. However – we also got info about the default Pyhton packages which we dont really care about since, well, they’re default. When we use the pip freeze command we get only installed packages and in a slightly different format (notice the used of ==). To store it in a file we can simply > it into a file as shown below…

Ok – so now we have two packages in our requirements.txt file. This is great and all, but what do we do with this? Using PIP we can install packages based on this file like so…

Notice here that we first deactivated the virtual environment we were in, then we activated another, showed that only the base packages were there, and then we used the requirements.txt file to install all of the packages we needed through PIP. Pretty slick right?

So now we know how to handle Python versions and packages inside of virtual environments. This is powerful by itself, but there’s even more ways we can optimize this. We’ll build on this in our next post. Stay tuned!

I’ve been spending more time on the MX recently and I thought it would be worthwhile to document some of the basics around interface configuration.  If you’re like me, and come from more of a Cisco background, some of configuration options when working with the MX weren’t as intuitive.  In this post, I want to walk through the bare bone basic of configuring interfaces on a MX router.

Basic L3 interface

The most basic interface configuration possible is a simple routed interface. You’ll note that the interface address is configured under a unit. To understand what a unit is you need to understand some basic terminology that Juniper uses. Juniper describes a physical interface as an IFD (Interface Device). In our example above the IFD would be the physical interface ge-0/0/0. We can then layer one or more IFL (Interface Logical) on top of the IFD. In our example the IFL would be the unit configuration, in this case ge-0/0/0.0. Depending on the configuration of the IFD you may be able to provision additional units. These additional units (Logical interfaces (IFLs)) can be thought of as sub-interfaces in Cisco parlance and would be identified by VLANs just as sub-interfaces are. However, in our current configuration, the MX thinks this is a standard L3 interface so it will not allow us to configure additional units…

Basic L3 interface with VLAN tags

As we mentioned above, a default L3 interface will only allow you define a single unit, unit 0. If you wish to define more units you have to enable the IFD to do this by providing the vlan-tagging configuration. This will allow the interface to handle single dot1q tags. Once the IFD is configured you simply provide the vlan-id to use for each IFL under the unit configuration. I’ll point out here that it is not a requirement for the unit numbers to correlate to the vlan-id but it is good practice to match these up.

Basic L3 interface with QinQ VLAN tags

In order to support QinQ vlan tagging we need to change the configuration on the IFD to stacked-vlan-tagging. However, in doing so, we break any IFL configuration that used the vlan-id parameter. The fix for this is to instead use the flexible-vlan-tagging option at the IFD which will allow both configurations to coexist…

Basic bridged interfaces

In this example we are simply bridging two interfaces together. In this case, the MX will treat these interfaces as access ports and simply switch frames between them. The interface configuration is straight forward as we define each interface to have an encapsulation of ethernet-bridge. In addition, it is required that each interface has a unit 0 definition. Notice that in addition to the interface configuration we must also define a bridge-domain and specifically add each interface which we want to participate in the domain.

Basic bridged interfaces with VLAN tags

In this example the ge-0/0/1 interface is VLAN aware and the ge-0/0/0 interface is still acting like an access port. The bridge domain configuration ties these two ports together meaning that a device connected to ge-0/0/1 passing a VLAN tag of 15 will be able to talk to the device connected to the access port.

IRB interfaces with VLAN tags

Here we provide a VLAN interface (Known as a SVI in Cisco land) by utilizing an IRB (Integrated routing and bridging) interface. The IRB interface is assigned to the VLAN by mapping it into the bridge domain as a routing-interface. Traffic that comes into interface ge0/0/1 with a VLAN tag of 15 will be able to reach the IRB interface of 10.20.20.254.

In the next post, we’ll dig further into this by discussing the specifics of bridge domains, learning domains, and VLAN mapping. Stay tuned!

In the last 4 posts we’ve examined the fundamentals of Kubernetes networking…

Kubernetes networking 101 – Pods

Kubernetes networking 101 – Services

Kubernetes networking 101 – (Basic) External access into the cluster

Kubernetes Networking 101 – Ingress resources

My goal with these posts has been to focus on the primitives and to show how a Kubernetes cluster handles networking internally as well as how it interacts with the upstream or external network.  Now that we’ve seen that, I want to dig into a networking plugin for Kubernetes – Calico.  Calico is interesting to me as a network engineer because of wide variety of functionality that it offers.  To start with though, we’re going to focus on a basic installation.  To do that, I’ve updated my Ansible playbook for deploying Kubernetes to incorporate Calico.  The playbook can be found here.  If you’ve been following along up until this point, you have a couple of options.

  • Rebuild the cluster – I emphasized when we started all this that the cluster should be designed exclusively for testing.  Starting from scratch is always the best in my opinion if you’re looking to make sure you don’t have any lingering configuration.  To do that you can follow the steps here up until it asks you to deploy the KubeDNS pods.  You need to deploy Calico before deploying any pods to the cluster!
  • Download and rerun the playbook – This should work as well but I’d encourage you to delete all existing pods before doing this (even the ones in the Kube-System namespace!).  There are configuration changes that occur both on the master and the minion nodes so you’ll want to make sure that once the playbook is run that all the services have been restarted.  The playbook should do that for you but if you’re having issues check there first.

Regardless of which path you choose, I’m going to assume from this point on that you have a fresh Kubernetes cluster which was deployed using my Ansible role.  Using my Ansible role is not a requirement but it does some things for you which I’ll explain along the way so no worries if you aren’t using it.  The goal of this post is to talk about Calico, the lab being used is just a detail if you want to follow along.

So now that we have our lab sorted out – let’s talk about deploying Calico.  One of the nicest things about Calico is that it can be deployed through Kubernetes.  Awesome!  The recommended way to deploy it is to use the Calico manifest which they define over on their site under the Standard Hosted Installation directions.  If you’re using my Ansible role, a slightly edited version of this manifest can be found on your master in /var/lib/kubernetes/pod_defs.  Let’s take a look at what it defines…

That’s a lot so let’s walk through what the manifest defines. The first thing the manifest defines is a config-map that Calico uses to define high level parameters about the Calico installation. Calico relies on a ETCD key value store for some of it’s functions so this is where we define the location of that. In this case, I’m using the same one that I’m using for Kubernetes. Again – this is a lab – they don’t recommend you doing that in non-lab environments. So in my case, I point the etcd_endpoints parameter to the host Ubuntu-1 on port 2379. Since we’re using cert based auth for ETCD I also need to tell Calico where the certs are for that. To do that you just need to un-comment lines 46-48 in the config-map. Do not change these values assuming you need to point that at a real file location on the host!

The second item the manifest defines is a Kubernetes secret which we populate with the ETCD TLS information if we’re using it.  We are so we need to populate these fields (lines 46-48) with base 64 encoded versions of each of these items.  Again – this is something that Ansible will do for you if you use my role. If not, you need to manually insert the values (I removed them from the file just to save space). We haven’t talked about secrets specifically but they are a means to share secret information with objects inside the Kubernetes cluster.

The third item the manifest defines is a daemon-set. Dameon-sets are a means to deploy a specific workload to every Kubernetes node or minion. So say I had a logging system that I wanted on each system. Deploying it as a daemon-set allows Kubernetes to manage that for me. If I join a new node to the cluster, Kubernetes will start the logging system on that node as well. So in this case, the daemon-set is for Calico and consists of two containers. The node container is the brains of the operation and what does most of the heavy lifting. This is also where we changed the CALICO_IPV4POOL_CIDR parameter from the default to 10.100.0.0/16. This is not required but I wanted to keep the pod IP addresses in that subnet for my lab. The install-cni container takes care of creating the correct CNI definitions on each host so that Kubernetes can consume Calico through a CNI plugin. Once it completes this task it goes to sleep and never wakes back up. We’ll talk more about the CNI definitions below.

The fourth and final piece of the manifest defines the Calico policy controller. We wont be talking about that piece of Calico in this post so just hold tight on that one for now.

So let’s deploy this file to the cluster…

Alright – now let’s run our net-test pod again so we have a testing point…

Once running let’s check and see what the pod received for networking.

First we notice that the eth0 interface is actually a VETH pair. We see that it’s peer is interface index 5 which on the host is an interface called cali182f84bfeba@if4. So the container’s network namespace is connected back to the host using a VETH pair. This is very similar to how most container networking solutions work with one minor change. The host side VETH pair is not connected to a bridge. It just lives by itself in the default or root namespace. We’ll talk more about the implications of this later on in this post. Next we notice that the pod received an IP address of 10.100.163.129. This doesn’t seem unusual since that was our pod CIDR we had defined in previous labs, but if you look at the kube-controller-manager service definition. You’l notice that we no longer configure that option…

Notice that the --cluster-cidr parameter is missing entirely and that the --allocate-node-cidrs parameter has been changed to false. This means that Kubernetes is no longer allocating pod CIDR networks to the nodes. So how are the pods getting IP addresses now? The answer to that lies in the kubelet configuration…

Our --network-plugin change from kubenet to cni. This means that we’re using native CNI in order to provision container networking. When doing so, Kubernetes acts as follows…

The CNI plugin is selected by passing Kubelet the –network-plugin=cni command-line option. Kubelet reads a file from –cni-conf-dir (default /etc/cni/net.d) and uses the CNI configuration from that file to set up each pod’s network. The CNI configuration file must match the CNI specification, and any required CNI plugins referenced by the configuration must be present in –cni-bin-dir (default /opt/cni/bin).
If there are multiple CNI configuration files in the directory, the first one in lexicographic order of file name is used.
In addition to the CNI plugin specified by the configuration file, Kubernetes requires the standard CNI lo plugin, at minimum version 0.2.0

Since we didnt specify --cni-conf-dir or –-cni-bin-dir the kubelet will look in the default path for each.  So let’s checkout what’s in the --cni-conf-dir (/etc/cni/net.d) now…

There’s quite a bit here and all of these files were written by Calico. Specifically by the install-cni container. We can verify that by checking it’s logs…

As we can see from the log of the container on each host, the CNI container created the binaries if they didnt exist (these may have already existed if you were using the previous lab build). It then created the CNI policy and the associated kubeconfig file for CNI to use. It also created the /etc/cni/net.d/calico-tls directory and placed the certs required to talk to etcd in that directory. It got this information from the Kubernetes secret /calico-secrets which is really the information from the secret calico-etcd-secrets that we created in the Calico manifest. The secret just happens to be mounted into the container as calico-secrets. The CNI definition also specifies that a plugin of calico should be use which we’ll find does exist in the /opt/cni/bin directory. it also specifies an IPAM plugin of calico-ipam meaning that calico is also taking care of our IP address assignment. One other interesting thing to point out is that the CNI definition lists the information required to talk to the Kubernetes API. To do this, it’s using the default pod token.  If you’re curious how the pods get the token to talk to the API server check out this piece of documentation that talks about default service accounts and credentials in Kubernetes.  Lastly – the install-CNI container created a kubeconfig file which specifies some further Kubernetes connectivity parameters.

So running the Calico manifest did quite a lot for us.  Each node node has the Calico CNI plugins and the means to talk to the Kubernetes API.  So now we know that Calico is driving the IP address allocation for the hosts, what about the actual networking side of things?  Let’s take a closer look at the routing for net-test container…

Well this is strange. The default route is pointing at 169.254.1.1. Let’s look on the host this container is running on and see what interfaces exist…

Nothing matching that IP address here. So what’s going on? How can a container route at an IP that doesnt exist? Let’s walk through what’s happening. Some of you reading this might have noticed that 169.254.1.1 is an IPv4 link local address.  The container has a default route pointing at a link local address meaning that the container expects this IP address to be reachable on it’s directly connected interface, in this case, the containers eth0 address. The container will attempt to ARP for that IP address when it wants to route out through the default route. Since our container hasnt talked to anything yet, we have the opportunity to attempt to capture it’s ARP request on the host. Let’s setup a TCPDUMP on the host ubuntu-3 and then use kubectl exec on the master to try talking to the outside world…

In the top output you can see we have the container send a single ping to 4.2.2.2. This will surely follow the container’s default route and cause it to ARP for it’s gateway at 169.254.1.1. In the bottom output you see the capture on the host Ubuntu-3. Notice we did the capture on the interface cali182f84bfeba which is the host side of the VETH pair connecting the container back to the root or default network namespace on the host. In the output of the TCPDUMP we see the container with a source of 10.100.163.129 send an ARP request. The reply comes from 2e:7e:32:de:8c:a3 which, if we reference the above output, will see is the MAC address of the host side VETH pair cali182f84bfeba. So you might be wondering how on earth the host is replying to an ARP request for which it doesn’t have an IP interface on. The answer is proxy-arp. If we check the host side VETH interface we’ll see that proxy-arp is enabled…

By enabling proxy-arp on this interface Calico is instructing the host to reply to the ARP request on behalf of someone else that is, through proxy. The rules for proxy-ARP are simple. A host which has proxy-ARP enabled will reply to ARP requests with it’s own MAC address when…

  • The host receives an ARP request on an interface which has proxy-ARP enabled.
  • The host knows how to reach the destination
  • The interface the host would use to reach the destination is not the same one that it received the ARP request on

So in this case, the container is sending an ARP request for 169.254.1.1.  Despite this being a link-local address, the host would attempt to route this following it’s default route out the hosts physical interface.  This means we’ve met all three requirements so the host will reply to the ARP request with it’s MAC address.

Note: If you’re curious about these requirements go ahead and try them out yourself.  For requirement 1 you can disable proxy-arp on the interface with echo 0 > /proc/sys/net/ipv4/conf/<interface name goes here>/proxy_arp.  For requirement 2 simply remove the hosts default route (make sure you have a 10’s route or some other means to reach the host before you do that!) like so sudo ip route del 0.0.0.0/0.  For the third requirement point the route 169.254.0.0/16 at the VETH interface itself like this sudo ip route add 169.254.0.0/16 dev <Calico VETH interface name>.  If you do any of these, the container will no longer be able to access the outside world.  Part of me wonders if this makes it a bit fragile but I also assume that most hosts will have a default route.  

The ARP process for the container would look like this…

In this case, the proxy ARP requirements are met since the host has a default route it can follow for the destination of 169.254.1.1 so it replies to the container with it’s own MAC address.  At this point, the container believes it has a valid ARP entry for it’s default gateway and will start initiating normal traffic toward the host.  It’s a pretty clever configuration but one that takes some time to understand.

I had mentioned above that the host side of the container VETH pair just lived in the hosts default or root namespace. In other container implementations, this interface would be attached to a common bridge so that all connected containers could talk to one another directly. In that scenario, the bridge would commonly be allocated an IP address giving the host an IP address on the same subnet as the containers.  This would allow the host to talk (do things like ARP) to the container directly. Having the bridge also allows the containers themselves to talk directly to one another through the bridge. This describes a layer 2 scenario where the host and all containers attached to the bridge can ARP for each others IP addresses directly. Since we don’t have the bridge, we need to tell the host how to route to each container. If we look at the hosts routing table we’ll see that we have a /32 route for the IP of the our net-test container…

The route points the IP address at the host side VETH pair. We also notice some other unusual routes in the hosts routing table…

These routes are inserted by Calico and represent the subnets allocated by Calico to all of the other hosts in our Kubernetes cluster. We can see that Calico is allocating a /26 network to each host…

10.100.243.0/26 – Ubuntu-2
10.100.163.128/26 – Ubuntu-3
10.100.5.192/26 – Ubuntu-4
10.100.138.192/26 – Ubuntu-5

Notice that these destinations are reachable through the tunl0 interface which is Calico’s IPIP overlay transport tunnel. This means that we don’t need to tell the upstream or physical network how to get to each POD CIDR range since it’s being done in the overlay. This also means that we can no longer reach the pod IP address directly. This conforms more closely with what the Kubernetes documentation describes when it says that the pod networks are not routable externally.  In our previous examples they were reachable since we were manually routing the subnets to each host.

We’ve just barely scratched the surface of Calico in this post but it should be enough to get you and running. In the next post we’ll talk about how Calico shares routing and reachability information between the hosts.

« Older entries