Intro
This is a mini-series describing how to set up a simple webservice.
I’ll try to cover as much as possible from the whole service lifecycle, from developing the app itself to actually running and maintaining it.
This contains at least the following steps (not necessarily in that order):
- Develop the app
- Write tests
- Containerize the app
- Set up CI/CD
- Run the app
This list will probably change as I make progress and shall be updated accordingly.
In this first part, I will briefly cover what I want my app to do, and explain the code.
App Overview
Basically I want to provide an API that returns some useful information about IP networks, like the Broadcast Address, the Netmask, how many usable Addresses the network contains, etc. - ideally for both IPv4 and IPv6 networks.
I will write this app in python, which already contains the useful ipaddress module providing all the backend functionality needed for now - no need to reinvent the wheel here.
To provide an API via HTTP we can also use existing frameworks like the great FastAPI. This obviously does not ship with python by default, so it has to be installed separately, the easiest option is to use pythons package manager pip.
Setup
To keep our python installation clean, we would typically use virtual environments (.venv in this case) and install the project-related modules into those:
python3 -m venv .venv # create the virtual environment named '.venv'
source .venv/bin/activate # activate the virtual environment
# your shell prompt should begin with '(.venv)' now!
pip install --upgrade pip # upgrade pip
pip install fastapi # install the fastapi module
The whole development is strongly influenced by FastAPIs “First Steps” Tutorial found HERE.
Code
Using ipaddress and fastapi as building blocks, our app consists of just a few lines of python code:
"""
An API providing basic information about IP networks.
"""
import ipaddress
import fastapi
app = fastapi.FastAPI()
@app.get("/")
def get_net_info(q: str) -> dict: # pylint: disable=invalid-name
"""
Returns basic information about an IP network.
Supports IPv6 as well as legacy-IP.
IPv4+ is NOT supported ;)
Args:
q (str): The string representation of the queried IP network
Returns:
dict: A dict containing basic information about the queried network.
"""
net = ipaddress.ip_network(q)
data = {
"cidr": f"{net}",
"net_addr": f"{net.network_address}",
"broadcast_addr": f"{net.broadcast_address}",
"prefix_len": f"{net.prefixlen}",
"netmask": f"{net.netmask}",
"addr_range": f"{net.network_address} - {net.broadcast_address}",
"usable_addr": f"{net.num_addresses - 2}",
}
if net.version == 6:
data["available_subnets"] = sum(1 for e in net.subnets(new_prefix=64))
return data
Step by Step Explanation
First, we import the required modules.
app = fastapi.FastAPI() then instantiates a FastAPI app, which we use in decorators to easily create API endpoints, and which can later be passed to a WSGI (or ASGI) webserver like gunicorn, uvicorn or similar.
Next, we define our API endpoint. For now we need only a single endpoint, and we will simply use the root path "/" (so the API will be reachable at api.example.com/). This is done with the @app.get("/") decorator.
The query (The network about which the requester wants to know about) will be passed as a query parameter in the form of q=192.168.55.0/24 and will be processed by the function get_net_info, which takes as argument the query parameter q and returns a dict object.
In the end, a requester will send a HTTP GET request like api.example.com/?q=192.168.55.0/24
The comment
# pylint: disable=invalid-namehas no function for the app, it just suppresses a warning in pylint, because the parameter nameqis shorter than the recommended minimum variable name length.
The next block contains the actual “logic” of the app, it uses the ipaddress module to build the dictionary we want to return to the requester (FastAPI will take care of serializing the dict into JSON for us).
if net.version == 6 checks if the passed network is an IPv6 network and adds additional info accordingly.
Calling the function now returns a dict:
print(get_net_info("192.168.55.0/24"))
prints the following to stdout:
{'addr_range': '192.168.55.0 - 192.168.55.255',
'broadcast_addr': '192.168.55.255',
'cidr': '192.168.55.0/24',
'net_addr': '192.168.55.0',
'netmask': '255.255.255.0',
'prefix_len': '24',
'usable_addr': '254'}
Running the app
To run this app, we need to install a webserver (uvicorn), then we can use it to start serving our app:
pip install uvicorn
uvicorn main:app --reload
This tells uvicorn to look for the object app in the file main.py, and also to reload whenever a change to the file main.py is detected (very useful during development, in production this flag will not be used).
You should now see some console output similar to this:
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [14721] using statreload
INFO: Started server process [14723]
INFO: Waiting for application startup.
INFO: Application startup complete.
You can test it using curl (and, if you want, jq to pretty-print the JSON):
curl 127.0.0.1:8000/?q=192.168.55.0/24 | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 195 100 195 0 0 65000 0 --:--:-- --:--:-- --:--:-- 65000
{
"cidr": "192.168.55.0/24",
"net_addr": "192.168.55.0",
"broadcast_addr": "192.168.55.255",
"prefix_len": "24",
"netmask": "255.255.255.0",
"addr_range": "192.168.55.0 - 192.168.55.255",
"usable_addr": "254"
}
Alternatively, open your browser and navigate to http://127.0.0.1:8000/?q=192.168.55.0/24, you should see the same JSON output there.
API docs
One of the great features of FastAPI is that it automagically creates a documentation for your API.
Simply navigate to the /docs and/or /redoc endpoints and be amazed!
Keeping track of Modules
For running the app and for furhter development, it is probably a good idea to keep track of the modules our app depends on.
Luckily, we can use pip for this as well:
pip freeze > requirements.txt
This creates a text file containing all the modules currently installed (run this from within the virtual environment!).
Now if we or someone else on another machine wants to run the app or continue development, we can simply create a new virtual environment and use
pip install -r requirements.txt
to install all the necessary modules.
Wrapping up
In this article, we developed a very basic web app which serves an HTTP API, providing information about IP networks.
Our project structure currently looks like this:
tree
.
├── .venv
├── main.py
└── requirements.txt
In the upcoming articles, we will explore how we can go about running this app somewhere else than our local machine, how we can test our code, how to make sure that our “live” version of the app is always up to date with the latest code, and probably a lot more.
See you around!