Metadata-Version: 2.4
Name: dnsleaf
Version: 1.3.0
Summary: Lightweight workspace-driven Cloudflare DDNS for dynamic and static IP targets.
Author: eserie-fox
License-Expression: MIT
Project-URL: Repository, https://github.com/eserie-fox/dnsleaf
Project-URL: Issues, https://github.com/eserie-fox/dnsleaf/issues
Project-URL: Changelog, https://github.com/eserie-fox/dnsleaf/blob/main/CHANGELOG.md
Keywords: ddns,cloudflare,proxmox,systemd,ipv6
Classifier: Environment :: Console
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: Name Service (DNS)
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx<1.0,>=0.27
Requires-Dist: pydantic<3.0,>=2.8
Requires-Dist: PyYAML<7.0,>=6.0
Requires-Dist: typer<1.0,>=0.12
Provides-Extra: dev
Requires-Dist: build<2.0,>=1.2; extra == "dev"
Requires-Dist: mypy<2.0,>=1.11; extra == "dev"
Requires-Dist: pytest<9.0,>=8.3; extra == "dev"
Requires-Dist: pytest-cov<6.0,>=5.0; extra == "dev"
Requires-Dist: ruff<1.0,>=0.6; extra == "dev"
Requires-Dist: types-PyYAML<7.0,>=6.0; extra == "dev"
Requires-Dist: twine<7.0,>=6.0; extra == "dev"
Dynamic: license-file

# dnsleaf

[![CI](https://github.com/eserie-fox/dnsleaf/actions/workflows/ci.yml/badge.svg)](https://github.com/eserie-fox/dnsleaf/actions/workflows/ci.yml)

Workspace-driven Cloudflare DDNS for Linux, primarily Proxmox VE hosts. Requires Python 3.11+.
Each entry publishes IPv4, IPv6, or both from a PVE LXC guest, a PVE VM guest agent,
the local host, or explicit static addresses. A oneshot sync and a system-level systemd timer
handle periodic updates. Cloudflare is the only supported provider.

**1.3.0 is pending release.** The public PyPI installation commands below apply after publication.
For this pending version, install the reviewed wheel with the same `uv tool install` command.

## Install the Python tool

Use [uv](https://docs.astral.sh/uv/guides/tools/) for a persistent, isolated installation.
For PVE system services, run these commands in a root shell with uv available:

```bash
uv tool install --python 3.11 dnsleaf
DNSLEAF="$(uv tool dir --bin)/dnsleaf"
"$DNSLEAF" --version
"$DNSLEAF" --help
```

For pre-release review, replace `dnsleaf` in the install command with the absolute path to
`dnsleaf-1.3.0-py3-none-any.whl`. Installing the package creates a CLI; it does not install a timer
or synchronize DNS. Keep this tool environment and its Python interpreter installed for the timer.
The module entrypoint is `python -m dnsleaf` when that Python environment contains the package.

Unprivileged users can initialize and validate writable workspaces, and sync local/static entries
when credentials and state are accessible. PVE guest discovery requires host privileges.
`apply` and `uninstall` require root. From a non-root shell, use explicit sudo with the **absolute**
installed entry, for example `sudo /absolute/path/to/dnsleaf apply --workspace /etc/dnsleaf/example-zone`.
The tool never elevates itself.

## Create and configure a workspace

In the same root shell:

```bash
"$DNSLEAF" init /etc/dnsleaf/example-zone
chmod 700 /etc/dnsleaf/example-zone
install -m 600 /dev/null /etc/dnsleaf/example-zone/secrets/cloudflare_api_token.txt
```

Put your Cloudflare API token in that file using your editor. Restrict it to the intended zone
and grant the DNS permissions needed for record reads and edits; zone lookup by name also needs
zone read access. Never put the token in YAML, commands, Git, or shared reports.

Edit `/etc/dnsleaf/example-zone/workspace.yaml`: set `zone_name`, optionally `zone_id`, and keep
`api_token_file` pointing to the token file. Its relative path is based on the workspace directory.
The generated YAML contains the packaged defaults; partial overrides are also supported.
See [configuration](docs/configuration.md) for schema 4 and all settings.

Add entries with the CLI, or edit `entries.yaml` (schema 2):

```bash
"$DNSLEAF" entry add lxc --workspace /etc/dnsleaf/example-zone \
  --id 101 --fqdn web.example.com --name web --family both
"$DNSLEAF" entry add vm --workspace /etc/dnsleaf/example-zone \
  --id 201 --fqdn vm.example.com --name vm --family ipv6
"$DNSLEAF" entry add local --workspace /etc/dnsleaf/example-zone \
  --fqdn host.example.com --name host --family both
"$DNSLEAF" entry add static --workspace /etc/dnsleaf/example-zone \
  --fqdn edge.example.com --name edge --family both \
  --ipv4 192.0.2.10 --ipv6 2001:db8::10
```

These names, guest IDs, and static documentation addresses are placeholders: use your own values.
LXC discovery uses `pct exec`; VM discovery supports Linux and Windows through a working standard QEMU guest agent; local discovery uses
`ip`. The default selection policy is unchanged. Windows Guests with multiple plausible IPv6
addresses can explicitly opt into `--selection-policy windows-dhcpv6` on `entry add vm`,
`entry update`, or `discover vm`. This policy also authorizes a fixed read-only PowerShell metadata
probe through `qm guest exec`; a working network-interface query alone is insufficient.
See [discovery](docs/discovery.md#windows-dhcpv6-opt-in) for prerequisites and refusal behavior,
and [entry management](docs/entry_management.md).

## Find an existing workspace

Existing dnsleaf deployments keep their directories and YAML; 1.3.0 requires no relocation or
reinitialization. Select with `--workspace` / `-w` (including explicit `.`), then a non-empty
`DNSLEAF_WORKSPACE`, or let dnsleaf search automatically. Automatic bases are the current directory,
its ancestors nearest to farthest including `/`, then home if not already visited. Each base checks
its immediate child directories in lexicographic order **before the base itself**. The first directory
with readable regular `workspace.yaml` and `entries.yaml` files wins; there is no sibling ambiguity error.

Search is one level only. From a home containing `ddns-config/` and
`dnsleaf-backups/backup-2026-09-17/`, the nested backup is not inspected. A complete backup placed
directly among searched siblings is a normal candidate; use `--workspace` to choose a particular one.

Partial candidates warn on stderr and search continues. A complete candidate with invalid YAML,
schema, or relevant runtime references fails without fallback. Explicit paths and environment paths
are authoritative. See [configuration](docs/configuration.md#workspace-discovery) for diagnostics.

## Validate, plan, and synchronize

```bash
"$DNSLEAF" validate --workspace /etc/dnsleaf/example-zone
"$DNSLEAF" plan --workspace /etc/dnsleaf/example-zone
"$DNSLEAF" sync-once --workspace /etc/dnsleaf/example-zone --apply
```

`validate` checks local configuration and token readability without API calls. `plan` and
`sync-once` without `--apply` read Cloudflare state and discover addresses, but never change DNS.
`sync-once --apply` performs one immediate sync. These commands may write workspace logs.

IPv4 and IPv6 are handled independently. Missing or ambiguous addresses are skipped without
publishing guesses or deleting existing records. `default_ttl: auto` uses Cloudflare automatic TTL;
`default_proxied: null` preserves existing proxy settings. Entry overrides may set `true` or `false`.
Enabled entries must own distinct FQDN + record type targets (case and trailing dots are ignored).
Each sync queries a dynamic source once, sharing its raw snapshot across node/service names and
families. Address-discovery commands have a package-default 30-second timeout.
Prune is off by default; explicit `--prune-managed` only removes stale records tracked by this
workspace. Unmanaged zone records are preserved. See [DNS sync](docs/dns_sync.md).

## Install the timer

```bash
"$DNSLEAF" apply --workspace /etc/dnsleaf/example-zone --no-run-sync
"$DNSLEAF" status --workspace /etc/dnsleaf/example-zone
systemctl status dnsleaf-example-zone.timer
journalctl -u dnsleaf-example-zone.service
```

`apply` validates and renders the workspace, installs system units, then enables and restarts the
timer. `--no-run-sync` skips the command's immediate sync; the enabled timer will still run on its
schedule. `render` only writes local generated artifacts. The service uses the absolute Python
from the environment that ran `apply`, followed by `-m dnsleaf`, so its entry does not depend on
an interactive shell's PATH or a development checkout. See [systemd](docs/systemd.md).

Workspace files are separated by purpose:

| Location | Purpose |
| --- | --- |
| `workspace.yaml`, `entries.yaml` | User configuration |
| `secrets/` | Local token file |
| `rendered/` | Effective JSON and generated unit files |
| `runtime/logs/dnsleaf.log` | Symlink to the current daily log |
| `state/` | Last installation and explicitly managed DNS records |

## Upgrade and uninstall

Existing default-policy YAML and state require no migration for 1.3.0. Existing systemd units
need no reinstall when upgrading in the same Python environment. If the installing interpreter or
workspace path changes, refresh units using the updated entry:

```bash
uv tool upgrade dnsleaf
DNSLEAF="$(uv tool dir --bin)/dnsleaf"
"$DNSLEAF" apply --workspace /etc/dnsleaf/example-zone --no-run-sync
```

Remove the local service installation before uninstalling the Python tool:

```bash
"$DNSLEAF" uninstall --workspace /etc/dnsleaf/example-zone
uv tool uninstall dnsleaf
```

Repeat local service removal for every installed workspace before removing the tool environment.
Normal `uninstall` retains configuration, credentials, and state. To explicitly delete a workspace,
use `uninstall --purge` while the tool is still installed. Neither form deletes remote DNS records.
A stop/disable failure aborts cleanup and returns failure. Historical pre-dnsleaf private deployments had separate migration requirements; existing dnsleaf
workspace schema 4 / entries schema 2 deployments need no reinitialization for 1.3.0.

## Development and release

```bash
make sync
make check
make build
```

See [development](docs/development.md), [architecture](docs/architecture.md),
[release preparation](docs/release.md), [1.3.0 notes](docs/release-notes/1.3.0.md), and
[CHANGELOG](CHANGELOG.md). Author: eserie-fox. License: [MIT](LICENSE).
