summaryrefslogtreecommitdiff
path: root/net-mgmt
diff options
context:
space:
mode:
Diffstat (limited to 'net-mgmt')
-rw-r--r--net-mgmt/Makefile2
-rw-r--r--net-mgmt/peering-manager/Makefile93
-rw-r--r--net-mgmt/peering-manager/distinfo3
-rwxr-xr-xnet-mgmt/peering-manager/files/850.peeringmanager-housekeeping.in32
-rw-r--r--net-mgmt/peering-manager/files/gunicorn.conf.py.in245
-rw-r--r--net-mgmt/peering-manager/files/patch-peering__manager_configuration.example.py11
-rwxr-xr-xnet-mgmt/peering-manager/files/peering_manager_rq.in50
-rw-r--r--net-mgmt/peering-manager/files/pkg-message.in9
-rw-r--r--net-mgmt/peering-manager/pkg-descr24
-rw-r--r--net-mgmt/py-pyixapi/Makefile23
-rw-r--r--net-mgmt/py-pyixapi/distinfo3
-rw-r--r--net-mgmt/py-pyixapi/pkg-descr3
12 files changed, 498 insertions, 0 deletions
diff --git a/net-mgmt/Makefile b/net-mgmt/Makefile
index a7e943abb5f7..5aebf555281d 100644
--- a/net-mgmt/Makefile
+++ b/net-mgmt/Makefile
@@ -296,6 +296,7 @@
SUBDIR += pandorafms_agent
SUBDIR += pandorafms_console
SUBDIR += pandorafms_server
+ SUBDIR += peering-manager
SUBDIR += pftabled
SUBDIR += php-fpm_exporter
SUBDIR += php81-snmp
@@ -343,6 +344,7 @@
SUBDIR += py-pyIOSXR
SUBDIR += py-pyang
SUBDIR += py-pyeapi
+ SUBDIR += py-pyixapi
SUBDIR += py-pynetbox
SUBDIR += py-pynxos
SUBDIR += py-pypowerwall
diff --git a/net-mgmt/peering-manager/Makefile b/net-mgmt/peering-manager/Makefile
new file mode 100644
index 000000000000..527e3d99d9dd
--- /dev/null
+++ b/net-mgmt/peering-manager/Makefile
@@ -0,0 +1,93 @@
+PORTNAME= peering-manager
+DISTVERSIONPREFIX= v
+DISTVERSION= 1.9.7
+CATEGORIES= net-mgmt python
+
+MAINTAINER= bofh@FreeBSD.org
+COMMENT= BGP sessions management tool
+WWW= https://peering-manager.net/
+
+LICENSE= APACHE20
+LICENSE_FILE= ${WRKSRC}/LICENSE
+
+RUN_DEPENDS= \
+ ${PYTHON_PKGNAMEPREFIX}Jinja2>=3.1:devel/py-Jinja2@${PY_FLAVOR} \
+ bgpq4>0:net-mgmt/bgpq4 \
+ ${PYTHON_PKGNAMEPREFIX}django51>=5.1<5.2:www/py-django51@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}dj51-djangorestframework>=3.15:www/py-dj51-djangorestframework@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}dj51-django-debug-toolbar>=5.0:www/py-dj51-django-debug-toolbar@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}dj51-django-filter>=25.1:www/py-dj51-django-filter@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}dj51-django-netfields>=1.3:www/py-dj51-django-netfields@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}dj51-django-prometheus>=2.3:www/py-dj51-django-prometheus@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}dj51-django-redis>=5.4:www/py-dj51-django-redis@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}dj51-django-rq>=2.10:devel/py-dj51-django-rq@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}dj51-django-tables2>=2.7:www/py-dj51-django-tables2@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}dj51-django-taggit>=6.1:www/py-dj51-django-taggit@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}dj51-drf-spectacular>=0.28:www/py-dj51-drf-spectacular@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}dj51-drf-spectacular-sidecar>=2025:www/py-dj51-drf-spectacular-sidecar@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}dj51-social-auth-app-django>=5.4:www/py-dj51-social-auth-app-django@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}dulwich>=0.22:devel/py-dulwich@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}gunicorn>=23.0.0:www/py-gunicorn@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}markdown>=3.7:textproc/py-markdown@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}napalm>=5.0:net-mgmt/py-napalm@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}packaging>=23.2:devel/py-packaging@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}psycopg>=3.1:databases/py-psycopg@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}psycopg-pool>=3.1:databases/py-psycopg-pool@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}pyixapi>=0.2:net-mgmt/py-pyixapi@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}pynetbox>=7.3:net-mgmt/py-pynetbox@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}pyyaml>=6.0:devel/py-pyyaml@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}requests>=2.32:www/py-requests@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}social-auth-core>=4.5.4:security/py-social-auth-core@${PY_FLAVOR}
+
+USES= cpe pgsql:13+ python:3.10-3.12
+CPE_VENDOR= ${PORTNAME}
+CPE_PRODUCT= ${PORTNAME:S/-/_/}
+USE_GITHUB= yes
+USE_RC_SUBR= peering_manager_rq
+
+NO_ARCH= yes
+NO_BUILD= yes
+SUB_FILES= gunicorn.conf.py 850.peeringmanager-housekeeping
+SUB_LIST= WWWDIR=${WWWDIR} PORTNAME=${PORTNAME} WSGI_APP=peering_manager.wsgi PYTHON_CMD=${PYTHON_CMD} PYTHON_VER=${PYTHON_VER}
+
+PORTDOCS= *
+
+OPTIONS_DEFINE= DOCS
+OPTIONS_DEFAULT=REDIS
+OPTIONS_RADIO= KVBACKENDS
+OPTIONS_RADIO_KVBACKENDS= REDIS VALKEY
+
+KVBACKENDS_DESC=Key Value Storage Backends
+REDIS_DESC= Redis Key Value Backend support
+VALKEY_DESC= Valkey Key Value Backend support
+
+REDIS_RUN_DEPENDS= redis>=8.2.1:databases/redis
+VALKEY_RUN_DEPENDS= valkey>=1.0:databases/valkey
+
+FIND_EXPR= "! -name *.orig ! -name .gitattributes ! -name .gitignore ! -name .gitattributes ! -name .isort.cfg ! -name .pre-commit-config.yaml ! -name .readthedocs.yaml ! -name CHANGELOG.md ! -name LICENSE ! -name README.md ! -name mkdocs.yaml ! -name poetry.lock ! -name pyproject.toml ! -name requirements.txt ! -name configuration.example.py ! -path */.github ! -path */.github/* ! -path */docs ! -path */docs/* -prune"
+
+do-install:
+ ${MKDIR} ${STAGEDIR}${WWWDIR}
+ ${MKDIR} ${STAGEDIR}${PREFIX}/etc/periodic/daily
+ ${ECHO} "@owner www" >> ${TMPPLIST}
+ ${ECHO} "@group www" >> ${TMPPLIST}
+ (cd ${WRKSRC} && ${COPYTREE_SHARE} . ${STAGEDIR}${WWWDIR} ${FIND_EXPR})
+ ${FIND} -s ${STAGEDIR}${PREFIX}/www/${PORTNAME} -not -type d | ${SORT} | \
+ ${SED} -e 's|^${STAGEDIR}${PREFIX}/||' >> ${TMPPLIST}
+ ${FIND} -s ${STAGEDIR}${PREFIX}/www/${PORTNAME} -type d -empty | ${SORT} -r | \
+ ${SED} -e 's|^${STAGEDIR}${PREFIX}/|@dir |' >> ${TMPPLIST}
+ ${INSTALL_DATA} ${WRKDIR}/gunicorn.conf.py ${STAGEDIR}${WWWDIR}/gunicorn.conf.py.sample
+ ${INSTALL_DATA} ${WRKSRC}/peering_manager/configuration.example.py ${STAGEDIR}${WWWDIR}/peering_manager/configuration.py.sample
+ ${INSTALL_DATA} ${WRKDIR}/850.peeringmanager-housekeeping ${STAGEDIR}${PREFIX}/etc/periodic/daily/850.peeringmanager-housekeeping
+ ${ECHO} "@sample ${WWWDIR}/gunicorn.conf.py.sample" >> ${TMPPLIST}
+ ${ECHO} "@sample ${WWWDIR}/peering_manager/configuration.py.sample" >> ${TMPPLIST}
+ ${ECHO} "etc/periodic/daily/850.peeringmanager-housekeeping" >> ${TMPPLIST}
+
+do-install-DOCS-on:
+ @${MKDIR} ${STAGEDIR}${DOCSDIR}
+ cd ${WRKSRC}/docs && ${COPYTREE_SHARE} . ${STAGEDIR}${DOCSDIR}
+.for f in CHANGELOG.md README.md
+ ${INSTALL_DATA} ${WRKSRC}/${f} ${STAGEDIR}${DOCSDIR}
+.endfor
+
+.include <bsd.port.mk>
diff --git a/net-mgmt/peering-manager/distinfo b/net-mgmt/peering-manager/distinfo
new file mode 100644
index 000000000000..95bbe9b48822
--- /dev/null
+++ b/net-mgmt/peering-manager/distinfo
@@ -0,0 +1,3 @@
+TIMESTAMP = 1756827986
+SHA256 (peering-manager-peering-manager-v1.9.7_GH0.tar.gz) = fa272abe40fec06d3f0c541d771d560f9a93f8940dea96b8538785a9cef32afd
+SIZE (peering-manager-peering-manager-v1.9.7_GH0.tar.gz) = 8349343
diff --git a/net-mgmt/peering-manager/files/850.peeringmanager-housekeeping.in b/net-mgmt/peering-manager/files/850.peeringmanager-housekeeping.in
new file mode 100755
index 000000000000..675f0f2aef63
--- /dev/null
+++ b/net-mgmt/peering-manager/files/850.peeringmanager-housekeeping.in
@@ -0,0 +1,32 @@
+#!/bin/sh
+# This shell script invokes Peering Manager's housekeeping management command,
+# which intended to be run nightly.
+#
+# If you want to enable this script, copy it to %%PREFIX%%/etc/periodic/daily
+# and place the following into /etc/periodic.conf:
+#
+# daily_peeringmanager_housekeeping_enable="YES"
+#
+# If Peering Manager has been installed into a nonstandard location, update the
+# paths below.
+command="%%PYTHON_CMD%%"
+peeringmanager_root="%%WWWDIR%%"
+
+# If there is a global system configuration file, suck it in.
+#
+if [ -r /etc/defaults/periodic.conf ]; then
+ . /etc/defaults/periodic.conf
+ source_periodic_confs
+fi
+
+rc=0
+
+case "$daily_peeringmanager_housekeeping_enable" in
+ [Yy][Ee][Ss])
+ echo ""
+ echo "Running Peering Manager housekeeping:"
+ $command "$peeringmanager_root/manage.py" housekeeping
+ rc=$?
+esac
+
+exit $rc
diff --git a/net-mgmt/peering-manager/files/gunicorn.conf.py.in b/net-mgmt/peering-manager/files/gunicorn.conf.py.in
new file mode 100644
index 000000000000..0477e5bf9f64
--- /dev/null
+++ b/net-mgmt/peering-manager/files/gunicorn.conf.py.in
@@ -0,0 +1,245 @@
+# Sample Gunicorn configuration file.
+import multiprocessing
+#
+# Server socket
+#
+# bind - The socket to bind.
+#
+# A string of the form: 'HOST', 'HOST:PORT', 'unix:PATH'.
+# An IP is a valid HOST.
+#
+# backlog - The number of pending connections. This refers
+# to the number of clients that can be waiting to be
+# served. Exceeding this number results in the client
+# getting an error when attempting to connect. It should
+# only affect servers under significant load.
+#
+# Must be a positive integer. Generally set in the 64-2048
+# range.
+#
+
+bind = ['127.0.0.1:8001','[::1]:8001']
+backlog = 2048
+
+#
+# Worker processes
+#
+# workers - The number of worker processes that this server
+# should keep alive for handling requests.
+#
+# A positive integer generally in the 2-4 x $(NUM_CORES)
+# range. You'll want to vary this a bit to find the best
+# for your particular application's work load.
+#
+# worker_class - The type of workers to use. The default
+# sync class should handle most 'normal' types of work
+# loads. You'll want to read
+# http://docs.gunicorn.org/en/latest/design.html#choosing-a-worker-type
+# for information on when you might want to choose one
+# of the other worker classes.
+#
+# A string referring to a Python path to a subclass of
+# gunicorn.workers.base.Worker. The default provided values
+# can be seen at
+# http://docs.gunicorn.org/en/latest/settings.html#worker-class
+#
+# worker_connections - For the eventlet and gevent worker classes
+# this limits the maximum number of simultaneous clients that
+# a single process can handle.
+#
+# A positive integer generally set to around 1000.
+#
+# timeout - If a worker does not notify the master process in this
+# number of seconds it is killed and a new worker is spawned
+# to replace it.
+#
+# Generally set to thirty seconds. Only set this noticeably
+# higher if you're sure of the repercussions for sync workers.
+# For the non sync workers it just means that the worker
+# process is still communicating and is not tied to the length
+# of time required to handle a single request.
+#
+# keepalive - The number of seconds to wait for the next request
+# on a Keep-Alive HTTP connection.
+#
+# A positive integer. Generally set in the 1-5 seconds range.
+#
+
+#workers = 5
+workers = multiprocessing.cpu_count() * 2 + 1
+worker_class = 'sync'
+worker_connections = 1000
+timeout = 300
+keepalive = 2
+threads = 3
+max_requests = 5000
+max_requests_jitter = 500
+
+#
+# spew - Install a trace function that spews every line of Python
+# that is executed when running the server. This is the
+# nuclear option.
+#
+# True or False
+#
+
+spew = False
+
+#
+# Server mechanics
+#
+# daemon - Detach the main Gunicorn process from the controlling
+# terminal with a standard fork/fork sequence.
+#
+# True or False
+#
+# raw_env - Pass environment variables to the execution environment.
+#
+# pidfile - The path to a pid file to write
+#
+# A path string or None to not write a pid file.
+#
+# user - Switch worker processes to run as this user.
+#
+# A valid user id (as an integer) or the name of a user that
+# can be retrieved with a call to pwd.getpwnam(value) or None
+# to not change the worker process user.
+#
+# group - Switch worker process to run as this group.
+#
+# A valid group id (as an integer) or the name of a user that
+# can be retrieved with a call to pwd.getgrnam(value) or None
+# to change the worker processes group.
+#
+# umask - A mask for file permissions written by Gunicorn. Note that
+# this affects unix socket permissions.
+#
+# A valid value for the os.umask(mode) call or a string
+# compatible with int(value, 0) (0 means Python guesses
+# the base, so values like "0", "0xFF", "0022" are valid
+# for decimal, hex, and octal representations)
+#
+# tmp_upload_dir - A directory to store temporary request data when
+# requests are read. This will most likely be disappearing soon.
+#
+# A path to a directory where the process owner can write. Or
+# None to signal that Python should choose one on its own.
+#
+
+daemon = False
+umask = 0
+user = None
+tmp_upload_dir = None
+pythonpath = '%%WWWDIR%%'
+chdir = '%%WWWDIR%%'
+wsgi_app = '%%WSGI_APP%%'
+
+#
+# Logging
+#
+# logfile - The path to a log file to write to.
+#
+# A path string. "-" means log to stdout.
+#
+# loglevel - The granularity of log output
+#
+# A string of "debug", "info", "warning", "error", "critical"
+#
+
+syslog = True
+syslog_prefix = '%%PORTNAME%%'
+syslog_addr = 'unix:///var/run/log#dgram'
+disable_redirect_access_to_syslog = True
+errorlog = '-'
+loglevel = 'info'
+accesslog = '-'
+access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
+
+#
+# Process naming
+#
+# proc_name - A base to use with setproctitle to change the way
+# that Gunicorn processes are reported in the system process
+# table. This affects things like 'ps' and 'top'. If you're
+# going to be running more than one instance of Gunicorn you'll
+# probably want to set a name to tell them apart. This requires
+# that you install the setproctitle module.
+#
+# A string or None to choose a default of something like 'gunicorn'.
+#
+
+proc_name = '%%PORTNAME%%'
+
+#
+# Server hooks
+#
+# post_fork - Called just after a worker has been forked.
+#
+# A callable that takes a server and worker instance
+# as arguments.
+#
+# pre_fork - Called just prior to forking the worker subprocess.
+#
+# A callable that accepts the same arguments as post_fork
+#
+# pre_exec - Called just prior to forking off a secondary
+# master process during things like config reloading.
+#
+# A callable that takes a server instance as the sole argument.
+#
+
+def post_fork(server, worker):
+ server.log.info("Worker spawned (pid: %s)", worker.pid)
+
+def pre_fork(server, worker):
+ pass
+
+def pre_exec(server):
+ server.log.info("Forked child, re-executing.")
+
+def when_ready(server):
+ server.log.info("Server is ready. Spawning workers")
+
+def worker_int(worker):
+ worker.log.info("worker received INT or QUIT signal")
+
+ ## get traceback info
+ import threading, sys, traceback
+ id2name = {th.ident: th.name for th in threading.enumerate()}
+ code = []
+ for threadId, stack in sys._current_frames().items():
+ code.append("\n# Thread: %s(%d)" % (id2name.get(threadId,""),
+ threadId))
+ for filename, lineno, name, line in traceback.extract_stack(stack):
+ code.append('File: "%s", line %d, in %s' % (filename,
+ lineno, name))
+ if line:
+ code.append(" %s" % (line.strip()))
+ worker.log.debug("\n".join(code))
+
+def worker_abort(worker):
+ worker.log.info("worker received SIGABRT signal")
+
+def ssl_context(conf, default_ssl_context_factory):
+ import ssl
+
+ # The default SSLContext returned by the factory function is initialized
+ # with the TLS parameters from config, including TLS certificates and other
+ # parameters.
+ context = default_ssl_context_factory()
+
+ # The SSLContext can be further customized, for example by enforcing
+ # minimum TLS version.
+ context.minimum_version = ssl.TLSVersion.TLSv1_3
+
+ # Server can also return different server certificate depending which
+ # hostname the client uses. Requires Python 3.7 or later.
+ def sni_callback(socket, server_hostname, context):
+ if server_hostname == "foo.127.0.0.1.nip.io":
+ new_context = default_ssl_context_factory()
+ new_context.load_cert_chain(certfile="foo.pem", keyfile="foo-key.pem")
+ socket.context = new_context
+
+ context.sni_callback = sni_callback
+
+ return context
diff --git a/net-mgmt/peering-manager/files/patch-peering__manager_configuration.example.py b/net-mgmt/peering-manager/files/patch-peering__manager_configuration.example.py
new file mode 100644
index 000000000000..1865973e0e30
--- /dev/null
+++ b/net-mgmt/peering-manager/files/patch-peering__manager_configuration.example.py
@@ -0,0 +1,11 @@
+--- peering_manager/configuration.example.py.orig 2025-09-05 10:59:41 UTC
++++ peering_manager/configuration.example.py
+@@ -9,7 +9,7 @@ ALLOWED_HOSTS = ["*"]
+ # A random one can be generated with Python in the Peering Manager venv with
+ # from django.core.management.utils import get_random_secret_key
+ # get_random_secret_key()
+-SECRET_KEY = "ef7npku*djrj_r4jt4cojo8^j@2($$@05e(eq_mn!ywx*jg0vy"
++#SECRET_KEY = "<GENERATE A KEY>"
+
+ # Base URL path if accessing Peering Manager within a directory.
+ BASE_PATH = ""
diff --git a/net-mgmt/peering-manager/files/peering_manager_rq.in b/net-mgmt/peering-manager/files/peering_manager_rq.in
new file mode 100755
index 000000000000..6b12856dfa9b
--- /dev/null
+++ b/net-mgmt/peering-manager/files/peering_manager_rq.in
@@ -0,0 +1,50 @@
+#!/bin/sh
+
+# This sample rc script starts the RQ worker background service which is
+# required for Webhooks and various automation tasks.
+
+#
+# PROVIDE: peering_manager_rq
+# REQUIRE: DAEMON
+# KEYWORD: shutdown
+#
+# Add the following line to /etc/rc.conf.local or /etc/rc.conf
+# to enable peering_manager-rq:
+#
+# peering_manager_rq_enable (bool): Set to NO by default.
+# Set it to YES to enable peering_manager_rq.
+#
+# peering_manager_rq_user (str): User to run worker as.
+# Defaults to www.
+
+. /etc/rc.subr
+
+name=peering_manager_rq
+rcvar=peering_manager_rq_enable
+
+load_rc_config $name
+
+: ${peering_manager_rq_enable:=NO}
+: ${peering_manager_rq_user:=www}
+: ${peering_manager_rq_workers:=1}
+
+start_cmd="peering_manager_rq_start"
+start_precmd="peering_manager_rq_precmd"
+command="%%PYTHON_CMD%%"
+command_args="%%WWWDIR%%/manage.py rqworker"
+_pidprefix="/var/run/%%PORTNAME%%"
+
+peering_manager_rq_precmd()
+{
+ install -d -o ${peering_manager_rq_user} ${_pidprefix}
+}
+
+peering_manager_rq_start()
+{
+ echo "Starting peering_manager_rq."
+ for i in `jot - 1 $peering_manager_rq_workers`; do
+ /usr/sbin/daemon -cf -p ${_pidprefix}/${name}-${i}.pid -u ${peering_manager_rq_user} ${command} ${command_args} --name peering-manager@${i}
+ done
+}
+
+run_rc_command "$1"
diff --git a/net-mgmt/peering-manager/files/pkg-message.in b/net-mgmt/peering-manager/files/pkg-message.in
new file mode 100644
index 000000000000..13913edb961b
--- /dev/null
+++ b/net-mgmt/peering-manager/files/pkg-message.in
@@ -0,0 +1,9 @@
+[
+{ type: install
+ message: <<EOD
+For installation instructions please refer to the related wiki page:
+
+- https://wiki.freebsd.org/Ports/net-mgmt/peering-manager
+EOD
+}
+]
diff --git a/net-mgmt/peering-manager/pkg-descr b/net-mgmt/peering-manager/pkg-descr
new file mode 100644
index 000000000000..ae00708cbbd6
--- /dev/null
+++ b/net-mgmt/peering-manager/pkg-descr
@@ -0,0 +1,24 @@
+Peering Manager was originally and still is developed by its lead
+ maintainer, Guillaume Mazoyer in 2017 as part of an effort to automate
+ BGP peering provisionning.
+
+Since then, many organisations around the world have used Peering
+ Manager as their central network source of truth to empower both
+ network operators and automation.
+
+Key Features
+Peering Manager was built specifically to serve the needs of network
+ engineers and operators operating BGP networks. Below is a very brief
+ overview of the core features it provides.
+
+- Autonomous system management
+- BGP groups
+- Internet Exchange Points
+- BGP sessions with with differences between classic ones and IXP ones
+- BGP communities and routing policies
+- Devices and configuration rendering leveraging Jinja2
+- Configuration installation for NAPALM supported platforms
+- Detailed, automatic change logging
+- Global search engine
+- Event-driven webhooks
+- Interoperability with other tools such as PeeringDB, IX-API, and more
diff --git a/net-mgmt/py-pyixapi/Makefile b/net-mgmt/py-pyixapi/Makefile
new file mode 100644
index 000000000000..b1c57adfe523
--- /dev/null
+++ b/net-mgmt/py-pyixapi/Makefile
@@ -0,0 +1,23 @@
+PORTNAME= pyixapi
+DISTVERSION= 0.2.6
+CATEGORIES= net-mgmt python
+MASTER_SITES= PYPI
+PKGNAMEPREFIX= ${PYTHON_PKGNAMEPREFIX}
+
+MAINTAINER= bofh@FreeBSD.org
+COMMENT= Python API client library for IX-API
+WWW= https://ix-api.net/
+
+LICENSE= APACHE20
+LICENSE_FILE= ${WRKSRC}/LICENSE
+
+BUILD_DEPENDS= ${PYTHON_PKGNAMEPREFIX}poetry-core>0:devel/py-poetry-core@${PY_FLAVOR}
+RUN_DEPENDS= ${PYTHON_PKGNAMEPREFIX}pyjwt>=2.4.0:www/py-pyjwt@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}requests>=2.20.0:www/py-requests@${PY_FLAVOR}
+
+USES= python
+USE_PYTHON= autoplist pep517
+
+NO_ARCH= yes
+
+.include <bsd.port.mk>
diff --git a/net-mgmt/py-pyixapi/distinfo b/net-mgmt/py-pyixapi/distinfo
new file mode 100644
index 000000000000..108271427920
--- /dev/null
+++ b/net-mgmt/py-pyixapi/distinfo
@@ -0,0 +1,3 @@
+TIMESTAMP = 1756893360
+SHA256 (pyixapi-0.2.6.tar.gz) = 864ef41255f62613db3161127b4c7c7bb36c776fb675cb3cdea3f7deee8a9732
+SIZE (pyixapi-0.2.6.tar.gz) = 14614
diff --git a/net-mgmt/py-pyixapi/pkg-descr b/net-mgmt/py-pyixapi/pkg-descr
new file mode 100644
index 000000000000..41fcb40e2774
--- /dev/null
+++ b/net-mgmt/py-pyixapi/pkg-descr
@@ -0,0 +1,3 @@
+Python API client library for IX-API.
+
+Currently Supported API versions are version 1 and version 2.