new file: .gitea/workflows/publish.yml
Publish Python package to PyPI / publish (push) Failing after 4s

new file:   .github/workflows/python-publish.yml
new file:   .gitignore
new file:   LICENSE
modified:   README.md
modified:   certbot_dns_netbox/dns_netbox.py
modified:   pyproject.toml
This commit is contained in:
Sven Luthi committed 2026-10-27 10:18:57 +01:00
1 parent 91562ae538
commit 4416d3a645
7 files changed
+186 -40

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
name: Publish Python package to PyPI
on:
push:
tags:
- "v*"
jobs:
publish:
runs-on: self-hosted
steps:
- name: Check out repository
uses: https://github.com/actions/checkout@v4
- name: Verify tag matches package version
run: |
TAG="${GITHUB_REF_NAME}"
VERSION="$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")"
if [ "$TAG" != "v$VERSION" ]; then
echo "::error::Tag ${TAG} does not match package version v${VERSION}"
exit 1
fi
- name: Install build tools
run: python3 -m pip install --upgrade build twine
- name: Build package
run: python3 -m build
- name: Check package
run: python3 -m twine check dist/*
- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
TWINE_NON_INTERACTIVE: "1"
run: python3 -m twine upload dist/*
+42
View File
@@ -0,0 +1,42 @@
# This workflow will upload a Python Package to PyPI when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Create and Upload Python Package
on:
release:
types: [published]
permissions:
contents: read
jobs:
build-and-publish:
name: Build and Publish
environment: pypi
permissions:
id-token: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
#- uses: actions/setup-python@v5
# with:
# python-version: "3.x"
- name: Install build tools
run: |
python -m pip install --upgrade pip
python -m pip install --upgrade build
- name: Build release
run: |
python -m build --sdist --wheel
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
+6
View File
@@ -0,0 +1,6 @@
__pycache__/
*.py[cod]
build/
dist/
*.egg-info/
.venv/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Sven Luthi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+27 -14
View File
@@ -1,9 +1,11 @@
# certbot-dns-netbox # Certbot DNS Netbox
Certbot DNS-01 authenticator plugin that updates the challenge TXT record in Certbot DNS-01 authenticator plugin that updates the challenge TXT records in
[NetBox DNS](https://github.com/sys4/netbox-plugin-dns) (the `netbox-dns` [NetBox DNS](https://github.com/sys4/netbox-plugin-dns) (the `netbox-dns`
NetBox plugin) via the bundled `nbapi.py` API helper. NetBox plugin) via the bundled `nbapi.py` API helper.
***DISCLAIMER: THIS PROJECT IS VIBE CODED USING AN LLM.***
## How it works ## How it works
For every domain in the certificate request certbot asks this plugin to set an For every domain in the certificate request certbot asks this plugin to set an
@@ -13,16 +15,27 @@ and the plugin **never creates or deletes records**:
1. looks up the existing TXT records for the FQDN (records can be filtered by 1. looks up the existing TXT records for the FQDN (records can be filtered by
`fqdn`, `zone` and `view` by name), `fqdn`, `zone` and `view` by name),
2. writes the challenge token into the existing record. If the record does 2. writes each challenge token into one of the pre-provisioned records,
not exist yet, certbot aborts with an error: create the record once claiming one record per token,
beforehand (in the NetBox UI or your tooling) and rerun, 3. leaves the records in place after validation so they can be updated again
3. leaves the record in place after validation so it can be updated again on on the next renewal.
the next renewal.
Because a single record holds a single token, a certificate request must not ### Provisioning
combine a domain and its wildcard (`-d example.com -d '*.example.com'`): both
map to the same `_acme-challenge.example.com` record, and the second token The challenge TXT records must be created beforehand in netbox-dns, in the
would overwrite the first. Issue those as separate certificates. view configured via `search_view` (`certbot_dns_netbox/dns_netbox.py`,
default: `global`). Each challenge token needs its own record:
| Certificate request | TXT records needed |
| ------------------------------------------------------ | ------------------ |
| `-d example.com` or `-d '*.example.com'` | 1 |
| `-d example.com -d '*.example.com'` (apex + wildcard) | 2 |
All records for a challenge share the same name (e.g. name
`_acme-challenge.unifi` in zone `ewcs.ch`); netbox-dns allows multiple TXT
records with different values for the same name. If a certificate request
needs more records than exist, certbot aborts with an error. Renewals simply
rewrite the values, so the records never need to be touched again.
Connection settings are taken from the environment, exactly as `nbapi.py` Connection settings are taken from the environment, exactly as `nbapi.py`
expects them: expects them:
@@ -94,6 +107,6 @@ certbot certonly --dry-run \
-d sub.example.com # end-to-end against the staging ACME API -d sub.example.com # end-to-end against the staging ACME API
``` ```
Watch the `_acme-challenge` TXT record in the `global` view: its value changes Watch the `_acme-challenge` TXT records in the `global` view: their values
to the challenge token during the run and remains in place afterwards. The change to the challenge tokens during the run and remain in place afterwards.
next renewal updates the same record again. The next renewal updates the same records again.
+42 -24
View File
@@ -1,10 +1,11 @@
"""NetBox DNS (netbox-plugin-dns) DNS-01 authenticator plugin for Certbot. """NetBox DNS (netbox-plugin-dns) DNS-01 authenticator plugin for Certbot.
The TXT record required for the ACME dns-01 challenge is managed The TXT records for the ACME dns-01 challenge are managed exclusively
exclusively through the netbox-dns record API endpoint, via the bundled through the netbox-dns record API endpoint, via the bundled nbapi.py
nbapi.py helper. The zone endpoint is never queried and the plugin never helper. The zone endpoint is never queried and the plugin never creates
creates or deletes records: an existing TXT record (created beforehand in or deletes records: the challenge TXT records (created beforehand in
NetBox) is updated with the challenge token and left in place afterwards. NetBox, one per challenge token) are updated with the challenge tokens
and left in place afterwards.
Connection settings are taken from the environment, exactly as nbapi.py Connection settings are taken from the environment, exactly as nbapi.py
expects them: expects them:
@@ -59,8 +60,8 @@ class Authenticator(dns_common.DNSAuthenticator):
def more_info(self) -> str: def more_info(self) -> str:
return ( return (
"This plugin updates an existing DNS TXT record in netbox-dns " "This plugin updates pre-provisioned DNS TXT records in "
"with the token of the dns-01 challenge." "netbox-dns with the tokens of the dns-01 challenges."
) )
def _setup_credentials(self) -> None: def _setup_credentials(self) -> None:
@@ -106,8 +107,8 @@ class _NetboxDnsClient:
Only the record API endpoint is used, so the API token needs no Only the record API endpoint is used, so the API token needs no
permissions on zones. Records are never created or deleted: the permissions on zones. Records are never created or deleted: the
challenge TXT record must already exist in NetBox and is updated in challenge TXT records must be provisioned beforehand in NetBox, one
place. per challenge token, and are updated in place.
""" """
def __init__(self, ttl: Optional[int] = None, insecure: bool = False): def __init__(self, ttl: Optional[int] = None, insecure: bool = False):
@@ -118,6 +119,10 @@ class _NetboxDnsClient:
self._nbapi = nbapi self._nbapi = nbapi
self._ttl = ttl self._ttl = ttl
# record ids claimed by this certbot run; each challenge token gets
# its own pre-provisioned record so multiple tokens for the same
# name (e.g. apex and wildcard) do not overwrite each other.
self._claimed: set = set()
if insecure: if insecure:
nbapi.disable_ssl_verify() nbapi.disable_ssl_verify()
@@ -141,30 +146,43 @@ class _NetboxDnsClient:
) from e ) from e
def update_txt_record(self, record_fqdn: str, validation: str) -> None: def update_txt_record(self, record_fqdn: str, validation: str) -> None:
"""Write the challenge token into the existing TXT record.""" """Write the challenge token into one of the pre-provisioned records.
Each challenge token claims its own record so that several tokens
for the same name (e.g. apex and wildcard on one certificate) stay
live simultaneously.
"""
fqdn = self._fqdn(record_fqdn) fqdn = self._fqdn(record_fqdn)
records = self._get_txt_records(fqdn) records = self._get_txt_records(fqdn)
if any(record["value"] == validation for record in records): for record in records:
logger.debug( if record["value"] == validation and not record.get("managed"):
"TXT record for %s already has the expected value.", record_fqdn self._claimed.add(record["id"])
) logger.debug(
return "TXT record for %s already has the expected value.",
record_fqdn,
)
return
existing = [record for record in records if not record.get("managed")] existing = sorted(
if not existing: (record for record in records if not record.get("managed")),
key=lambda record: record["id"],
)
free = [record for record in existing if record["id"] not in self._claimed]
if not free:
raise errors.PluginError( raise errors.PluginError(
f"No updatable TXT record found for {record_fqdn} in view " f"Not enough TXT records for {record_fqdn} in view "
f"{search_view!r}. This plugin only updates existing records: " f"{search_view!r}: {len(existing)} record(s) exist, but all of "
"create the record once (type TXT) and rerun certbot." "them are already in use by this certificate request. Add "
"another TXT record with the same name and rerun certbot."
) )
patch = {"id": existing[0]["id"], "value": validation} target = free[0]
patch = {"id": target["id"], "value": validation}
if self._ttl: if self._ttl:
patch["ttl"] = self._ttl patch["ttl"] = self._ttl
self._nbapi.modify_dns_record(patch) self._nbapi.modify_dns_record(patch)
self._claimed.add(target["id"])
logger.info( logger.info(
"Updated TXT record %s (id %s).", "Updated TXT record %s (id %s).", target["fqdn"], target["id"]
existing[0]["fqdn"],
existing[0]["id"],
) )
+9 -2
View File
@@ -1,5 +1,5 @@
[build-system] [build-system]
requires = ["setuptools>=61"] requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
@@ -8,7 +8,9 @@ version = "1.0.0"
description = "NetBox DNS (netbox-plugin-dns) DNS-01 authenticator plugin for Certbot" description = "NetBox DNS (netbox-plugin-dns) DNS-01 authenticator plugin for Certbot"
readme = "README.md" readme = "README.md"
requires-python = ">=3.9" requires-python = ">=3.9"
license = { text = "Apache-2.0" } license = "MIT"
license-files = ["LICENSE"]
authors = [{ name = "Sven Luthi", email = "dev@sven.luethi.co" }]
keywords = ["certbot", "acme", "letsencrypt", "netbox", "dns", "dns-01"] keywords = ["certbot", "acme", "letsencrypt", "netbox", "dns", "dns-01"]
classifiers = [ classifiers = [
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
@@ -16,6 +18,7 @@ classifiers = [
"Intended Audience :: System Administrators", "Intended Audience :: System Administrators",
"Operating System :: POSIX :: Linux", "Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP",
"Topic :: Security", "Topic :: Security",
] ]
@@ -25,6 +28,10 @@ dependencies = [
"urllib3>=1.26", "urllib3>=1.26",
] ]
[project.urls]
Homepage = "https://git.luethi.co/sven/certbot-dns-netbox"
Repository = "https://git.luethi.co/sven/certbot-dns-netbox.git"
[project.entry-points."certbot.plugins"] [project.entry-points."certbot.plugins"]
dns-netbox = "certbot_dns_netbox.dns_netbox:Authenticator" dns-netbox = "certbot_dns_netbox.dns_netbox:Authenticator"