Metadata-Version: 2.4
Name: mirror.py-rsync-server
Version: 1.1.0
Summary: mirror.py event plug-in that generates rsyncd.conf and rsyncd.secrets from the package list.
Author: SPARCS KAIST
License-Expression: Apache-2.0
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mirror.py>=1.3.0
Dynamic: license-file

# mirror.py rsync-server plugin

An event plugin for [mirror.py](https://github.com/sparcs-kaist/mirror.py) that generates
`rsyncd.conf` and `rsyncd.secrets` from the live mirror package list plus a sidecar
`rsync.json` configuration file. The plugin regenerates both files automatically when the
mirror daemon starts (`MASTER.INIT.POST`) and whenever the package set changes via
`mirror config reload` (`MASTER.CONFIG_RELOAD.POST`).


## How it works

The plugin listens on two mirror.py events:

- `MASTER.INIT.POST` — fires once when the master daemon finishes starting up.
- `MASTER.CONFIG_RELOAD.POST` — fires after `mirror config reload` has loaded a new
  configuration. Until the mirror.py core ships this event, regeneration happens at
  daemon start only; the listener signature accepts `(*args, **kwargs)` so it is
  forward-compatible.

**File generation only.** The plugin writes `rsyncd.conf` and `rsyncd.secrets`; it does
not start, stop, or signal the rsyncd process. This is intentional: rsyncd re-reads
`rsyncd.conf` and the secrets file on every incoming client connection, so adding or
removing mirror modules takes effect immediately with no reload signal needed.

Writes are atomic (via a temporary file in the same directory followed by `os.replace`).
If the generated content is identical to what is already on disk, the file is not
rewritten. Regeneration runs under a module-level lock so concurrent event firings do not
race.

Packages with `settings.hidden: true` are completely excluded — they appear in neither
the public nor the private module list.


## Installation

Requires **mirror.py >= 1.3.0** (uses the 1.3.0 plugin API: API version gate and the
per-plugin config-file mechanism).

Install the plugin into the same Python environment as mirror.py:

```
pip install -e .
```

Enable the plugin in mirror.py's `config.json`. As of mirror.py 1.3.0 the `plugins`
block is enable-only:

```json
"plugins": { "rsync-server": { "enabled": true } }
```

Any `config` sub-block here is ignored — the plugin's own settings live in `rsync.json`
(see the Configuration section below).

### Scaffold rsync.json

The plugin supports `mirror plugin config create`:

```
mirror plugin config create rsync-server
```

This writes an example `rsync.json` (mode `0600`, since it holds plaintext passwords).

Caveat: during `mirror plugin config create` the mirror config path is not loaded, so the
file is written to the fixed fallback location **`/etc/mirror/rsync.json`** regardless of
any `--config` flag. `--config` only points the command at a valid `config.json` so the
plugin registry can load; it does not change where the scaffold is written. If your
mirror config lives elsewhere, move the generated file next to your `config.json`, or copy
[`rsync.json.example`](rsync.json.example) manually. (At daemon runtime the config path
*is* known, so generation and reads use `rsync.json` next to your `config.json`.)


## Configuration

`rsync.json` must live in the same directory as mirror.py's `config.json` (default path:
`/etc/mirror/rsync.json`).

See [`rsync.json.example`](rsync.json.example) for a complete, realistic example.

All keys are optional and fall back to the defaults listed below.

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `rsyncd_conf` | string (absolute path) | `/etc/rsyncd.conf` | Destination path for the generated `rsyncd.conf`. |
| `secrets_file` | string (absolute path) | `/etc/rsyncd.secrets` | Destination path for the generated secrets file. Also emitted as `secrets file` in the global section of `rsyncd.conf`. |
| `users` | object | `{}` | Map of `username` to `password`. Written to the secrets file in insertion order. |
| `global` | object | `{}` | Raw rsyncd parameter passthrough. Keys and values are emitted in insertion order as `key = value` lines in the global section. Values may be strings, integers, or booleans (booleans render as `true`/`false`). The key `secrets file` is managed by the plugin and is rejected if placed here. |
| `private_modules.enabled` | boolean | `true` | When `false`, no `[.Name]` private module sections are generated. |
| `private_modules.auth_users` | string | `*` | Value written to `auth users` in every private module section. |
| `private_modules.list` | boolean | `false` | Value written to `list` in every private module section. |
| `private_modules.lock_file` | string (absolute path) | `/var/run/rsyncd-private.lock` | Value written to `lock file` in every private module section. The private modules share this lock file, giving them a separate max-connections accounting pool independent of the public modules. |

Unknown top-level keys and unknown `private_modules` keys are rejected with a `ValueError`
at load time (typo protection). Any validation failure leaves previously generated files
untouched.

**SECURITY NOTE:** `rsync.json` contains plaintext passwords. Restrict its permissions:

```
chmod 600 /etc/mirror/rsync.json
```

The plugin logs a warning if `rsync.json` is group- or other-readable. The generated
`rsyncd.secrets` file is always written with mode `0600`, as required by rsyncd's strict
modes.


## Generated output

Given a package with id `archlinux` (display name `ArchLinux`), `src =
rsync://rsync.archlinux.org/ftp_tier1` and `dst = /mirror/ftp/ArchLinux`, and the global
settings from `rsync.json.example`, the plugin produces:

```
# WARNING: DO NOT EDIT!! Generated by the mirror.py rsync-server plugin.
uid = rsync
gid = nogroup
use chroot = no
max connections = 20
motd file = /mirror/etc/motd
log file = /var/log/geoul/rsyncd/all.log
transfer logging = yes
log format = %o %a - %u [%t] "%P/%f" - %l
pid file = /var/run/rsyncd.pid
exclude = .~tmp~

secrets file = /mirror/etc/rsyncd.secrets

[archlinux]
    path = /mirror/ftp/ArchLinux
    comment = from rsync://rsync.archlinux.org/ftp_tier1

[.archlinux]
    path = /mirror/ftp/ArchLinux
    comment = private module for ArchLinux without connection limits for authorized mirrors
    auth users = *
    list = false
    lock file = /var/run/rsyncd-private.lock
```

Each package produces a public/private module pair:

- `[pkgid]` is the public module. The module name is the package id, not the display
  name, so it is a valid rsyncd module token (no spaces or special characters). It is
  visible in `rsync host::` listings and carries a `comment = from <upstream src>` line.
  The comment line is omitted when a package has no upstream `src` (for example, locally
  synced packages).
- `[.pkgid]` is the authenticated, unlisted private twin. The leading dot hides it from
  `rsync host::` listings. Access is gated by `auth users` and the secrets file. This
  module has its own lock file, so its connection accounting is independent of the public
  module's `max connections` limit.

Modules are sorted by package id. The static header contains no timestamp so that
identical content is detected and the file is not rewritten unnecessarily.

The secrets file (`rsyncd.secrets`) contains one `username:password` line per entry in
`users`, written in insertion order, with no header line.


## Running rsyncd

Point rsyncd at the generated configuration file:

```
rsync --daemon --config=/etc/rsyncd.conf
```


## Operational notes

**Changing users or rsync settings.** Edit `rsync.json`, then run:

```
mirror config reload
```

No daemon restart is needed for module or authentication changes. rsyncd picks up the
new `rsyncd.conf` and `rsyncd.secrets` on the next incoming client connection.

**Parameters that require an rsyncd restart.** Daemon-socket parameters such as `port`,
`address`, and `pid file` are read by rsyncd only at startup. Changing them in `rsync.json`
and running `mirror config reload` will update the generated file, but rsyncd itself must
be restarted for these to take effect. Module-level parameters (path, auth, comment, etc.)
apply per connection and do not require a restart.

**Private module connection accounting.** The private modules share the lock file
specified by `private_modules.lock_file`. This gives them a separate max-connections pool
rather than unlimited connections — the pool size is controlled by rsyncd's global
`max connections` parameter applied against that lock file independently from the public
modules' lock file.


## Development

Set up a virtual environment and install the plugin alongside mirror.py:

```
uv venv
uv pip install -e /path/to/mirror.py -e . pytest
```

Run the test suite:

```
uv run pytest
```


## License

Apache-2.0
