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-name has no function for the app, it just suppresses a warning in pylint, because the parameter name q is 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!