new file: .gitea/workflows/publish.yml
Publish Python package to PyPI / publish (push) Failing after 4s
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:
1 parent
91562ae538
commit
4416d3a645
7 files changed
+186
-40
No files matched your search
@@ -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/*
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
.venv/
|
||||
@@ -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.
|
||||
@@ -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 plugin) via the bundled `nbapi.py` API helper.
|
||||
|
||||
***DISCLAIMER: THIS PROJECT IS VIBE CODED USING AN LLM.***
|
||||
|
||||
## How it works
|
||||
|
||||
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
|
||||
`fqdn`, `zone` and `view` by name),
|
||||
2. writes the challenge token into the existing record. If the record does
|
||||
not exist yet, certbot aborts with an error: create the record once
|
||||
beforehand (in the NetBox UI or your tooling) and rerun,
|
||||
3. leaves the record in place after validation so it can be updated again on
|
||||
the next renewal.
|
||||
2. writes each challenge token into one of the pre-provisioned records,
|
||||
claiming one record per token,
|
||||
3. leaves the records in place after validation so they can be updated again
|
||||
on the next renewal.
|
||||
|
||||
Because a single record holds a single token, a certificate request must not
|
||||
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
|
||||
would overwrite the first. Issue those as separate certificates.
|
||||
### Provisioning
|
||||
|
||||
The challenge TXT records must be created beforehand in netbox-dns, in the
|
||||
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`
|
||||
expects them:
|
||||
@@ -94,6 +107,6 @@ certbot certonly --dry-run \
|
||||
-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
|
||||
to the challenge token during the run and remains in place afterwards. The
|
||||
next renewal updates the same record again.
|
||||
Watch the `_acme-challenge` TXT records in the `global` view: their values
|
||||
change to the challenge tokens during the run and remain in place afterwards.
|
||||
The next renewal updates the same records again.
|
||||
@@ -1,10 +1,11 @@
|
||||
"""NetBox DNS (netbox-plugin-dns) DNS-01 authenticator plugin for Certbot.
|
||||
|
||||
The TXT record required for the ACME dns-01 challenge is managed
|
||||
exclusively through the netbox-dns record API endpoint, via the bundled
|
||||
nbapi.py helper. The zone endpoint is never queried and the plugin never
|
||||
creates or deletes records: an existing TXT record (created beforehand in
|
||||
NetBox) is updated with the challenge token and left in place afterwards.
|
||||
The TXT records for the ACME dns-01 challenge are managed exclusively
|
||||
through the netbox-dns record API endpoint, via the bundled nbapi.py
|
||||
helper. The zone endpoint is never queried and the plugin never creates
|
||||
or deletes records: the challenge TXT records (created beforehand in
|
||||
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
|
||||
expects them:
|
||||
@@ -59,8 +60,8 @@ class Authenticator(dns_common.DNSAuthenticator):
|
||||
|
||||
def more_info(self) -> str:
|
||||
return (
|
||||
"This plugin updates an existing DNS TXT record in netbox-dns "
|
||||
"with the token of the dns-01 challenge."
|
||||
"This plugin updates pre-provisioned DNS TXT records in "
|
||||
"netbox-dns with the tokens of the dns-01 challenges."
|
||||
)
|
||||
|
||||
def _setup_credentials(self) -> None:
|
||||
@@ -106,8 +107,8 @@ class _NetboxDnsClient:
|
||||
|
||||
Only the record API endpoint is used, so the API token needs no
|
||||
permissions on zones. Records are never created or deleted: the
|
||||
challenge TXT record must already exist in NetBox and is updated in
|
||||
place.
|
||||
challenge TXT records must be provisioned beforehand in NetBox, one
|
||||
per challenge token, and are updated in place.
|
||||
"""
|
||||
|
||||
def __init__(self, ttl: Optional[int] = None, insecure: bool = False):
|
||||
@@ -118,6 +119,10 @@ class _NetboxDnsClient:
|
||||
|
||||
self._nbapi = nbapi
|
||||
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:
|
||||
nbapi.disable_ssl_verify()
|
||||
|
||||
@@ -141,30 +146,43 @@ class _NetboxDnsClient:
|
||||
) from e
|
||||
|
||||
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)
|
||||
records = self._get_txt_records(fqdn)
|
||||
|
||||
if any(record["value"] == validation for record in records):
|
||||
logger.debug(
|
||||
"TXT record for %s already has the expected value.", record_fqdn
|
||||
)
|
||||
return
|
||||
for record in records:
|
||||
if record["value"] == validation and not record.get("managed"):
|
||||
self._claimed.add(record["id"])
|
||||
logger.debug(
|
||||
"TXT record for %s already has the expected value.",
|
||||
record_fqdn,
|
||||
)
|
||||
return
|
||||
|
||||
existing = [record for record in records if not record.get("managed")]
|
||||
if not existing:
|
||||
existing = sorted(
|
||||
(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(
|
||||
f"No updatable TXT record found for {record_fqdn} in view "
|
||||
f"{search_view!r}. This plugin only updates existing records: "
|
||||
"create the record once (type TXT) and rerun certbot."
|
||||
f"Not enough TXT records for {record_fqdn} in view "
|
||||
f"{search_view!r}: {len(existing)} record(s) exist, but all of "
|
||||
"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:
|
||||
patch["ttl"] = self._ttl
|
||||
self._nbapi.modify_dns_record(patch)
|
||||
self._claimed.add(target["id"])
|
||||
logger.info(
|
||||
"Updated TXT record %s (id %s).",
|
||||
existing[0]["fqdn"],
|
||||
existing[0]["id"],
|
||||
"Updated TXT record %s (id %s).", target["fqdn"], target["id"]
|
||||
)
|
||||
+9
-2
@@ -1,5 +1,5 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61"]
|
||||
requires = ["setuptools>=77"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
@@ -8,7 +8,9 @@ version = "1.0.0"
|
||||
description = "NetBox DNS (netbox-plugin-dns) DNS-01 authenticator plugin for Certbot"
|
||||
readme = "README.md"
|
||||
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"]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -16,6 +18,7 @@ classifiers = [
|
||||
"Intended Audience :: System Administrators",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
"Topic :: Security",
|
||||
]
|
||||
@@ -25,6 +28,10 @@ dependencies = [
|
||||
"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"]
|
||||
dns-netbox = "certbot_dns_netbox.dns_netbox:Authenticator"
|
||||
|
||||
|
||||
Reference in new issue
Block a user