Metadata-Version: 2.5
Name: rostainer-archive
Version: 0.0.1
Summary: rostainer plugin: rotate ROS logs and bags into archives, and fetch them from robots
License-Expression: MIT
Keywords: docker,logs,ros,ros2,rostainer
Requires-Python: >=3.7
Requires-Dist: rostainer
Requires-Dist: rostainer-deploy
Description-Content-Type: text/markdown

# rostainer-archive

A [rostainer](https://git.whoi.edu/bmillerklugman/rostainer) plugin that
archives a workspace's ROS logs and bags, and fetches the archives from
deployed robots.

- **On every container start** (dev containers and packages alike), the
  contents of the log directories move into an archive, with a snapshot of
  the workspace's SQLite databases, and are tarred to
  `<archive_dir>/tar/<time>.tar.gz`. Anything still open by a process (a
  node's log, a bag being recorded) is left for the next rotation, so
  nothing is archived half-written.
- **`rostainer archive rotate`** rotates now, in the workspace's running
  dev container(s), while ROS keeps running. On a robot, the installed
  package's own **`./rostainer archive rotate`** does the same.
- **`rostainer archive fetch [TARGET_OR_GROUP ...]`** downloads the tarballs
  from the robots `rostainer deploy` deploys to, over the same ssh
  connections. There's no second list of hosts.

## Install

```bash
pipx inject rostainer rostainer-archive     # or: pip install rostainer-archive
```

It needs the deploy plugin (`rostainer-deploy`), which pip installs with it.
In the workspace's `.rostainer.toml`:

```toml
# Refuse to start without it, rather than run without log rotation.
requires_plugins = ["archive"]

[plugins.archive]
dirs = [".ros/log", "bags"]   # archived; in the container, relative to the user's home
database_dir = "database"     # SQLite *.db snapshotted (not moved) into each archive; "" for none
archive_dir = "archive"       # where archives go
keep_unpacked = true          # keep each archive's directory next to its tarball
download_dir = "~/rostainer-logs"   # where 'fetch' saves, in a directory per target
```

`rostainer init --with archive` adds the table, with each setting described.

Mount the directories from the host with `volume_mappings`, so the logs and
archives outlive the container, and `fetch` can reach them:

```toml
volume_mappings = [
    "data/logs:/home/$USER/.ros/log",
    "data/bags:/home/$USER/bags",
    "data/archive:/home/$USER/archive",
]
```

rostainer's own ROS log pruning starts before the plugin's rotation, so it
could delete logs before they're archived. Turn it off:

```toml
ros_log_max_size_mb = 0
ros_log_max_age_days = 0
```

## Rotation

The plugin's `rotate.sh` runs as a post-build hook: on every start, before
the workspace's own post-build script. Its files are at
`/usr/local/lib/rostainer/plugins/archive/` in the container. A failed
rotation is a warning, and the start goes on.

Archives go to `<archive_dir>/<time>/{log,bags,database}/` and
`<archive_dir>/tar/<time>.tar.gz`. The time is the newest archived file's
modification time, in the container's timezone (UTC by default). Only the
top-level entries of each directory are considered, so a bag directory or
a launch's run directory is archived whole or not at all. Hidden entries
and symlinks (ROS 1's `latest`) stay.

```bash
rostainer archive rotate               # every running dev container of the workspace
rostainer archive rotate my_container  # any container with the plugin's files
./rostainer archive rotate             # on a robot, in the installed package's directory
```

Both run the plugin's `rotate` command inside the container, with the
settings that container (or package) was started or made with. Neither
needs a TTY, so either can run from cron or a systemd timer.

## Fetching

```bash
rostainer archive fetch                     # the newest archive of every enabled robot
rostainer archive fetch divers --all        # every archive, from a group
rostainer archive fetch robot1 --since 2026-10-23T00:00:00Z
rostainer archive fetch robot1 --rotate --delete
```

Robots are picked as `rostainer deploy` picks them: by name, by group from
`[plugins.deploy.groups]`, or `all` (the default). Several are reached at
once, and one that fails doesn't stop the others.

On each robot, `fetch` finds the package's container (`./rostainer
container`), and where `archive_dir` is mounted from on the robot
(`docker inspect`). It then downloads the tarballs into
`<download_dir>/<target>/`, or `--output-dir DIR`.

- `--since TIME` takes every tarball modified at or after TIME, an ISO 8601
  time with a timezone.
- `--rotate` rotates first, in the package's running container
  (`./rostainer archive rotate`, with the package's own settings).
- `--delete` deletes each tarball on the robot once it's here and verified,
  after checking the robot's copy again. The unpacked directories stay.

Each download is checked against the robot's SHA-256 before it takes its
name, so a file is never left half-written. A tarball that's already here
with the same hash is skipped. One that's here but different is never
overwritten.

`fetch` uses the settings of this checkout: keep `archive_dir` as it was
when the robot's package was made.

## Tests

```bash
python -m unittest discover tests
```
