python-dotenv package to retrieve your environmental variables
When writing code, you will run into situations where you will need to call back to usernames, passwords, or variables that may have data unique to the local system. For this reason, there will be specific variable values that you will not want to hardcode into your Python script. There are multiple ways of dealing with this situation. For example, you could store the data as environmental variables in your Bash session, but that may not be a secure and practical method for your situation. This is what is great about using a .env
file to store environmental variables to be used in your code.
The .env file (pronounced “dot-e-n-v”) is a hidden file that stores environmental variables for your code in a text format. This file stays local to your environment and is not meant to be shared or pushed to a version control system (VCS) remote repository like GitHub or Gitlab. Suppose you are using Git for your project. In that case, the .env
file should be tracked in the .gitignore
folder so that the .env
file remains untracked in your local repository and is not accidentally pushed to your remote repository, where it may be shared with other developers.
The .env
is not a full-proof, highly secure way of protecting sensitive data; even though it is a hidden file, the file is pretty easy to find, and the data in the .env
is cleartext. You will want to explore more secure methods such as Ansible Vault, HashiCorp Vault, or something similar that stores data in an encrypted manner. However, using the .env
file is an excellent choice, depending on your needs.
Another benefit of using the .env
file is portability. Suppose your script uses specific variables that will be unique to your situation. In that case, hardcoding the variables into the script means that if you share the script with another person or move it to another environment, you will need to go back into the script and update all the values for the variables. This is not ideal and, to be honest, is amateurish.
Part of writing good code is separating the config values from the script. See The Twelve-Factor App - III. Config.
In this tutorial, we will write a simple RESTCONF script to get the IP address information about the Gigabit Ethernet interfaces from a CSR 1000V IOS XE router. We will create a .env
file to store the credentials for the CSR. Feel free to follow along, make the script, and test it. There are links at the end of this tutorial where you can download the script and .env
file. If you don’t have access to a local device, feel free to use the DevNet CSR in the DevNet Sandbox.
For this exercise, we will use this simple RESTCONF script that retrieves all the primary IP information for all the Gigabit Ethernet interfaces on a CSR 1000V using IOS XE in my local lab. Please take a moment to look at the code, and you’ll notice that I have hardcoded the unique config values for the CSR into the script.
app.py
from requests.packages.urllib3.exceptions import InsecureRequestWarning
import requests
import json
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
username = "student"
password = "1234QWer"
device_addr = "172.17.82.23" # IP ADDRESS OR FQDN
HEADERS = {"Accept": "application/yang-data+json"}
AUTH = (username, password)
URL = (f"https://{device_addr}/restconf/data/Cisco-IOS-XE-native:native/interface/GigabitEthernet")
response = requests.get( url=URL, auth=AUTH, headers=HEADERS, verify=False)
interfaces = json.loads(response.text)
for interface in interfaces["Cisco-IOS-XE-native:GigabitEthernet"]:
print("GigabitEthernet "+ interface["name"])
print(interface["ip"]["address"])
print("*" * 80)
You should have noticed that the variables username
, password
, and device_addr
are hardcoded into this script. If you were to copy this script or clone it from a repository and try to run it on a different environment for another device, the script would likely fail if its credentials and IP address were different. If you wanted to use this script elsewhere, you would need to reconfigure those three variables.
Let’s start by creating the .env file. So right now, I am starting in the root directory of my project. It needs to be in the same directory as your Python script. Use whichever text editor you like to create your file. I am using Vim here. Name the file .env
. The ‘.’ at the beginning of the filename will make it a hidden file.
barweiss@ubuntu-srv-01:~/tutorial_labs$ vi .env
In the .env
file, I am going to add my environmental variables. The hash ("#") can be used for commenting.
# .env file
# CSR USERNAME
username="student"
# CSR PASSWORD
password="1234QWer"
# CSR IP ADDRESS
device_ip="172.17.82.23"
Now that the .env
file is configured, I will save my configuration. Notice that my .env
file is in the same directory as my script, app.py
. You’ll also see that this directory is using Git, and because I don’t want to accidentally upload my .env
file to a remote repository where others can use it, I’ll list the .env
in the .gitignore
file.
.
├── app.py
├── .env
├── .git
├── .gitignore
└── .python-version
Now let’s move on to the next step and configure our script to use the environmental variables from the .env
file.
As you may have seen in other IT disciplines, there are situations where there are several methods to achieve the same goal. Of course, some ways are more efficient than others, but sometimes it just boils down to personal preference. The same thing is true when coding in Python. You can write your script to retrieve the values from the .env
file in a few different ways. I prefer using python-dotenv, which will be the method I will show you in this tutorial. The python-dotenv library has a few other interesting features worth checking out in the documentation, but today, we will only use the load_dotenv
object.
First, the package must be downloaded from PyPI to our environment. I’ll use pip install
to do this.
barweiss@ubuntu-srv-01:~/tutorial_labs$ pip install python-dotenv
Now we are ready to update our script so that it will use the load_dotenv
object to load our .env as environmental variables.
At the beginning of the script, we will add a new import statement so that we can use load_dotenv
from the python-dotenv package. We only need the load_dotenv
, so we will add the following line:
from dotenv import load_dotenv
We also need one more package to retrieve the environmental variables once loaded into the session environment. This will be the os
package, and because it is built into the standard Python library, it does not need to be installed. Along with the previous import
statement, we will add the following line:
import os
Now we will go two lines down from our last import statement and add the load_dotenv
object to the script:
load_dotenv()
The load_dotenv
object only loads the environmental variables into the session’s environment, but we will use the os.getenv
to retrieve the environmental variables. We will replace the username, password, and IP address variables in the script with the names of the environmental variables used in the .env
file:
username = os.getenv('username')
password = os.getenv('password')
device_addr = os.getenv('device_ip') # MAY USE IP ADDRESS OR FQDN
This is our final draft of the code that now uses the environmental variables from the .env
file. Notice that the code no longer has any actual credentials or config information in it. All that data is being retrieved from the environmental variables that were loaded from the .env
file using the load_dotenv
object from the python-dotenv package.
from requests.packages.urllib3.exceptions import InsecureRequestWarning
import requests
import json
from dotenv import load_dotenv
import os
load_dotenv()
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
username = os.getenv('username')
password = os.getenv('password')
device_addr = os.getenv('device_ip') # MAY USE IP ADDRESS OR FQDN
HEADERS = {"Accept": "application/yang-data+json"}
AUTH = (username, password)
URL = (f"https://{device_addr}/restconf/data/Cisco-IOS-XE-native:native/interface/GigabitEthernet")
response = requests.get( url=URL, auth=AUTH, headers=HEADERS, verify=False)
interfaces = json.loads(response.text)
for interface in interfaces["Cisco-IOS-XE-native:GigabitEthernet"]:
print("GigabitEthernet "+ interface["name"])
print(interface["ip"]["address"])
print("*" * 80)
I rerun the script, and it works like a charm! That’s pretty much it. Now you know one way to use environmental variables and a .env
file with your scripts.
If you want to try out this script on a real CSR 1000V and don’t have access to one, head to the DevNet Sandbox, where you can use the always-on CSR device or reserve one. Remember to change your .env
file to the proper values for your lab. Enjoy! 🎉