In this topic, I want to show the installation of DevPi (https://github.com/devpi/devpi), which is a local, simple and lightweight repository for Python modules, just like the Registry, there are other more complete repositories, such as the Jfrog Community Edition, which I want to show you how to use in a next post. But because they are simple and light, they now work better.

What is the need for DevPi now? Soon I will finalize some more configurations between GitLab and Jenkins and create a simple pipeline, and from this pipeline I want to show how to use pythons for any task and these pythons, I intend to use modules that I create as needed. Obviously I’m not going to upload these modules to PiPy.org, so I need to somehow have them locally, so that I can install them as pip normally.

I will show two types of installation, one in a VM and the other creating a Docker image.

Already with an environment where Python3 is installed, install the devpi modules. Installation may take a while.

pip install devpi-server devpi-web devpi-client

Now, let’s start preparing the folders, configuration files, and service.

I will use a shell file to start devpi-server and a systemctl service to call this shell, this way, changes are easier, without needing the daemon-reload every time.

I will use a /work/devpi-data folder as the root of the service. In this folder, it is also necessary to run a command to init the DevPi settings and data folder (/work/devpi-data/data):

sudo adduser usr_devpi
sudo usermod usr_devpi -s /sbin/nologin
sudo mkdir -p /work/devpi-data/
devpi-init --serverdir /work/devpi-data/data

touch /var/log/devpi.log
sudo chown -R usr_devpi:usr_devpi /var/log/devpi.log

Create the shell script:

vi /work/devpi-data/start-devpi.sh

#!/bin/bash
devpi-server -c /work/devpi-data/config.yml --logger-cfg /work/devpi-data/logger.yaml
chmod u+x /work/devpi-data/start-devpi.sh

Create the service file:

vi /etc/systemd/system/devpi.service

[Unit]
Description=Devpi Server
Requires=network-online.target
After=network-online.target

[Service]
Restart=on-success
# ExecStart:
# - shall point to existing devpi-server executable
# - shall not use the deprecated `--start`. We want the devpi-server to start in foreground
ExecStart=/work/devpi-data/start-devpi.sh
# set User according to user which is able to run the devpi-server
User=usr_devpi

[Install]
WantedBy=multi-user.target

Now let’s create the configuration file. Note that the host has the listener as 0.0.0.0 and port 4040.

vi /work/devpi-data/config.yml
devpi-server:
    host: 0.0.0.0
    port: 4040
    role: standalone 
    serverdir: /work/devpi-data/data
    restrict-modify: root

And now the file that will create the logs, both in the /var/log folder and in stdout.

vi /work/devpi-data/logger.yaml

version: 1
formatters:
    simple:
        format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
        datefmt: '%Y-%m-%d %H:%M:%S'

handlers:
    console:
        class: logging.StreamHandler
        level: DEBUG
        formatter: simple
        stream: ext://sys.stdout
    file:
        class: logging.FileHandler
        level: INFO
        formatter: simple
        filename: '/var/log/devpi.log'
        mode: a

loggers:
    development:
        level: DEBUG
        handlers: [console]
        propagate: no

    staging:
        level: INFO
        handlers: [console, file]
        propagate: no

    production:
        level: WARNING
        handlers: [file]
        propagate: no

root:
    level: DEBUG
    handlers: [console,file]

With everything ready, let’s start the service:

sudo chown -R usr_devpi:usr_devpi /work/devpi-data/

sudo systemctl start devpi

systemctl status devpi

 devpi.service - Devpi Server
     Loaded: loaded (/etc/systemd/system/devpi.service; enabled; vendor preset: enabled)
     Active: active (running) since Tue 2024-04-23 23:13:57 UTC; 2s ago
   Main PID: 2939 (start-devpi.sh)
      Tasks: 4 (limit: 2191)
     Memory: 559.2M
        CPU: 4.224s
     CGroup: /system.slice/devpi.service
             ├─2939 /bin/bash /work/devpi-data/start-devpi.sh
             └─2940 /usr/bin/python3 /usr/local/bin/devpi-server -c /work/devpi-data/config.yml --logger-cfg /work/devpi-data/logger.yaml

Apr 23 23:13:58 srv-vmware-01 start-devpi.sh[2940]: 2024-04-23 23:13:58 - root - INFO - NOCTX Loading node info from /work/devpi-data/data/.nodeinfo
Apr 23 23:13:58 srv-vmware-01 start-devpi.sh[2940]: 2024-04-23 23:13:58 - root - INFO - NOCTX wrote nodeinfo to: /work/devpi-data/data/.nodeinfo
Apr 23 23:13:58 srv-vmware-01 start-devpi.sh[2940]: 2024-04-23 23:13:58 - root - DEBUG - [Rtx0] closing transaction at 0
Apr 23 23:13:58 srv-vmware-01 start-devpi.sh[2940]: 2024-04-23 23:13:58 - root - INFO - NOCTX running with role 'standalone'
Apr 23 23:13:58 srv-vmware-01 start-devpi.sh[2940]: 2024-04-23 23:13:58 - root - DEBUG - NOCTX creating application in process 2940
Apr 23 23:13:58 srv-vmware-01 start-devpi.sh[2940]: 2024-04-23 23:13:58 - root - WARNING - NOCTX No secret file provided, creating a new random secret. Login tokens issued before are invalid. Use --secretfile option to provide a persistent secret. You can create a proper secret with the devpi-gen-secret command.

If the service is running well, you will be able to open the DevPi web page: http://127.0.0.1:4040/
Or as in my case, http://172.21.5.160:4040/ once I’m done, I must create a DNS for this entry.

Here we will create a simple image, based on Ubuntu 22, with the basic DevPi installations

We have a Dockerfile and a shell for the startup… the configuration files will be attached to the container when we create it.

Dockerfile

FROM ubuntu:22.04

ENV WORKDIR /work/devpi-data/
RUN mkdir -p ${WORKDIR}
WORKDIR ${WORKDIR}

# install dependencies
RUN apt update \
    && apt install -y python3 python3-pip
    
RUN pip3 install devpi-server devpi-web devpi-client 

RUN devpi-init --serverdir /work/devpi-data/data

# Auto-Start service
COPY startup.sh ${WORKDIR}
RUN chmod +x startup.sh
WORKDIR ${WORKDIR}
ENTRYPOINT ./startup.sh

startup.sh

#!/bin/bash
devpi-server -c /work/devpi-data/config.yml --logger-cfg /work/devpi-data/logger.yaml

Having the above files ready, let’s create the image:

docker image build --rm --no-cache -t img_devpi:latest .

After the image is created, validate that it is already available.

docker image ls
REPOSITORY                                          TAG                    IMAGE ID       CREATED         SIZE
img_devpi                                           latest                 34a6a7f64237   6 minutes ago   515MB

Take advantage, if you have the Registry environment ready and upload the image to the repository.

But, with the image ready, let’s create the configuration files.

config.yml

devpi-server:
    host: 0.0.0.0
    port: 4040
    role: standalone 
    serverdir: /work/devpi-data/data
    restrict-modify: root

logger.yaml

version: 1
formatters:
    simple:
        format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
        datefmt: '%Y-%m-%d %H:%M:%S'

handlers:
    console:
        class: logging.StreamHandler
        level: DEBUG
        formatter: simple
        stream: ext://sys.stdout
    file:
        class: logging.FileHandler
        level: INFO
        formatter: simple
        filename: '/var/log/devpi.log'
        mode: a

loggers:
    development:
        level: DEBUG
        handlers: [console]
        propagate: no

    staging:
        level: INFO
        handlers: [console, file]
        propagate: no

    production:
        level: WARNING
        handlers: [file]
        propagate: no

root:
    level: DEBUG
    handlers: [console,file]

Create the container:

docker run -tid -p 4040:4040 \
   --network local-bridge \
   -v /work/DevPi/config.yml:/work/devpi-data/config.yml \
   -v /work/DevPi/logger.yaml:/work/devpi-data/logger.yaml \
   --name srv-devpi-01 img_devpi:latest

From now on, the configuration is the same

Internal structure.

For this service, I will not create integration with LDAP, it will be simple authentication.
In the next steps, from within the host/vm/container where DevPi is installed, we will create the basic authentication user and the Index structure for the modules that will be hosted on DevPi.
For the commands from now on, I will use the IP I created, remember to change it.

Connect to the server and log in with root.

devpi use http://172.21.5.160:4040

devpi login root --password ''
logged in 'root', credentials valid for 10.00 hours

Change the root password

devpi user -m root password=1234qwer

/root changing password: ********
user modified: root

Let’s create a basic authentication user and create an index using it as root.

devpi user -c devpi_devopsdb

new password for user devpi_devopsdb:
repeat new password for user devpi_devopsdb:
user created: devpi_devopsdb

devpi index -c devpi_devopsdb/stable bases=root/pypi volatile=True

http://172.21.5.160:4040/devpi_devopsdb/stable?no_projects=:
  type=stage
  bases=root/pypi
  volatile=True
  acl_upload=devpi_devopsdb
  acl_toxresult_upload=:ANONYMOUS:
  mirror_whitelist=
  mirror_whitelist_inheritance=intersection

Logout of user root:

devpi logoff
login information deleted

Example module.

I’m going to leave here a very simple structure of a python module, so we can test the push/upload and use of this module on another host. This can be created in any other environment, it does not need to and should not be created within the DevPi Server.

The python module is simple, two functions and nothing more:

.
├── ip_number
│   ├── __init__.py
│   └── ipnumber.py
└── setup.py

__init__.py

__version__ = '1.0.0'
from .ipnumber import fn_IP_Int
from .ipnumber import fn_Int_IP

ipnumber.py

"""
Module to convert IPs into Numbers and Numbers into IPs.
"""

def fn_IP_Int(ip):
    h = list(map(int, ip.split(".")))
    return (h[0] << 24) + (h[1] << 16) + (h[2] << 8) + (h[3] << 0)

def fn_Int_IP(ip):
    return ".".join(map(str, [((ip >> 24) & 0xff), ((ip >> 16) & 0xff), ((ip >> 8) & 0xff), ((ip >> 0) & 0xff)]))

setup.py

from setuptools import setup, find_packages

VERSION = '1.0.0' 
DESCRIPTION = 'Convert IP to Int'
LONG_DESCRIPTION = 'Module to convert IPs into Numbers and Numbers into IPs.'

# Setting up
setup(
       # the name must match the folder name 'ip_number'
        name="ip_number", 
        version=VERSION,
        author="You",
        author_email="your.email@gmail.com",
        description=DESCRIPTION,
        long_description=LONG_DESCRIPTION,
        packages=find_packages(),
        install_requires=[],
)

After the entire structure is ready, in the same folder where setup.py is located, we need to run setup so that the WHL and Tar.GZ files, necessary for uploading to DevPi, are created.

There are also other ways to create this wheels structure, I prefer to use setup.py, it’s simpler.

Let’s prepare the environment for the setup, first it is necessary to install the modules that will be used in the setup:

pip install setuptools wheel devpi-client

So now, in the same folder as setup.py:

python setup.py sdist bdist_wheel

running sdist
running egg_info
creating ip_number.egg-info
writing ip_number.egg-info/PKG-INFO
[...]
adding 'ip_number-1.0.0.dist-info/top_level.txt'
adding 'ip_number-1.0.0.dist-info/RECORD'
removing build/bdist.macosx-13.5-arm64/wheel

Okay, now you should notice that the folder structure has become much larger.

.
├── build
│   ├── bdist.macosx-13.5-arm64
│   └── lib
│       └── ip_number
│           ├── __init__.py
│           └── ipnumber.py
├── dist
│   ├── ip_number-1.0.0-py3-none-any.whl
│   └── ip_number-1.0.0.tar.gz
├── ip_number
│   ├── __init__.py
│   └── ipnumber.py
├── ip_number.egg-info
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   └── top_level.txt
└── setup.py

Module push.

Now, with the entire structure ready, it’s time to upload it.
The host you are uploading to must have the devpi-client module installed.

pip install devpi-client

Stay in the same setup.py folder. We need to log in to DevPi with the same user that we created the index in the first configurations and then select the same index.

$ devpi use http://172.21.5.160:4040

Warning: insecure http host, trusted-host will be set for pip
using server: http://172.21.5.160:4040/ (not logged in)
no current index: type 'devpi use -l' to discover indices
/work/DevPi/.config/pip/pip.conf: no config file exists
/work/DevPi/.pydistutils.cfg: no config file exists
/work/DevPi/.buildout/default.cfg: no config file exists
always-set-cfg: no

###

$ devpi login devpi_devopsdb --password '1234qwer'

logged in 'devpi_devopsdb', credentials valid for 10.00 hours

###
$ devpi use stable

Warning: insecure http host, trusted-host will be set for pip
current devpi index: http://172.21.5.160:4040/devpi_devopsdb/stable (logged in as devpi_devopsdb)
supported features: server-keyvalue-parsing
/work/DevPi/.config/pip/pip.conf: no config file exists
/work/DevPi/.pydistutils.cfg: no config file exists
/work/DevPi/.buildout/default.cfg: no config file exists
always-set-cfg: no

So, let’s upload the dist folder that was created in setup.

$ devpi upload --sdist --wheel --from-dir dist

file_upload of ip_number-1.0.0-py3-none-any.whl to http://172.21.5.160:4040/devpi_devopsdb/stable/
file_upload of ip_number-1.0.0.tar.gz to http://172.21.5.160:4040/devpi_devopsdb/stable/

You can check in the GUI, the package should already be there.

Using the module.

Now, to test, use a different host, one that has Python and nothing else from DevPi.

You can now configure Pip to directly retrieve any request from your DevPi, such as creating pip.conf with index-url and extra-index-url, having the DevPi address directly in the url, having a pip alternative .conf, etc, etc, etc.

Now, for demonstration purposes, I will make the call directly in pip, later on, I will create an image with this pip.conf.

Validate that the module is not actually installed:

python

>>> import ip_number
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'ip_number'

Install using pip and providing the DevPi address.

$ pip install -i http://172.21.5.160:4040/devpi_devopsdb/stable/ ip_number --trusted-host 172.21.5.160

Looking in indexes: http://172.21.5.160:4040/devpi_devopsdb/stable/
Collecting ip_number
  Downloading http://172.21.5.160:4040/devpi_devopsdb/stable/+f/adc/ec1038aab9861/ip_number-1.0.0-py3-none-any.whl
Installing collected packages: ip-number
Successfully installed ip-number-1.0.0

Test again

import ip_number

var_ip = ip_number.fn_IP_Int('10.124.44.140')
print(var_ip)

var_number = ip_number.fn_Int_IP(var_ip)
print(var_number)

Done… quickly and easily, you have a repository / artifactory of python modules locally.

DNS:

nslookup devpi.devops-db.internal
Server:         172.21.5.72
Address:        172.21.5.72#53

Name:   devpi.devops-db.internal
Address: 172.21.5.160

pip.conf.

Another way to install the module is to have a pip configuration file, it can be in the default paths, but I prefer to have a normal conf file and indicate its use at the time of execution or set the PIP_CONFIG_FILE environment variable.
For example. Create the pip-devops.conf file in any path, in the example /work/conf-files/pip-devops.conf

[global]
index-url = http://devpi.devops-db.internal:4040/devpi_devopsdb/stable
trusted-host = devpi.devops-db.internal

And then the pip install call looks like this:

PIP_CONFIG_FILE=/work/conf-files/pip-devops.conf pip install devopsdb

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.