Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: CI

on:
push:
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Test (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version:
- "3.11"
- "3.12"
- "3.13"
- "3.14"

steps:
- name: Check out repository
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: pip

- name: Install project and test dependencies
run: python -m pip install --upgrade pip -e ".[dev]"

- name: Run tests
run: python -m pytest -q

package:
name: Build and smoke-test package
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.14"
cache: pip

- name: Install build tooling
run: python -m pip install --upgrade pip build

- name: Build source and wheel distributions
run: python -m build

- name: Install and smoke-test wheel
run: |
python -m pip install dist/*.whl
nirj-agent --help

- name: Upload distributions
uses: actions/upload-artifact@v4
with:
name: distributions
path: dist/
if-no-files-found: error
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
__pycache__/
*.py[cod]
*.egg-info/
.pytest_cache/
.venv/
.sandbox/
87 changes: 87 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,88 @@
# nirj-agent

Device management agent for Northern Ireland Raspberry Jam devices.

## Development usage

Create a virtual environment and install the package in editable mode:

```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e '.[dev]'
pytest -q
```

Use `--root` to run the CLI against an isolated filesystem tree instead of
reading or writing the host's `/etc`, `/var`, `/boot`, and `/usr` paths:

```bash
mkdir -p .sandbox/etc/nirj-agent
mkdir -p .sandbox/var/lib/nirj-agent
nirj-agent --root .sandbox status
nirj-agent --root .sandbox get-config
nirj-agent --root .sandbox manifest refresh
nirj-agent --root .sandbox plan
```

The sandbox layout mirrors the production filesystem. For example, its
configuration belongs at `.sandbox/etc/nirj-agent/config.yaml` and its state
at `.sandbox/var/lib/nirj-agent/state.yaml`.

`manifest refresh` downloads and validates the configured manifest before
atomically caching it. `plan` reads that cached manifest and reports package
changes without installing, removing, or changing any state.

## Production usage

Run the installer as root with the device's asset ID and type:

```bash
curl -fsSL \
https://raw.githubusercontent.com/NIRaspberryJam/nirj-agent/main/install.sh \
| sudo bash -s -- --asset-id PI5-001 --device-type pi5
```

Valid device types for this systemd-based installer are `pi5` and `lpt-lx`.
The installer clones the repository into `/opt/nirj-agent/source`, creates a
virtual environment at `/opt/nirj-agent/venv`, creates the initial
configuration, and enables and starts `nirj-agent.service`. Existing
configuration is preserved when the installer is run again.

At every service start, `scripts/run-agent.sh` fast-forwards the checkout,
reinstalls the package, and runs `nirj-agent up`. The agent then refreshes and
applies the configured manifest before remaining active.

Inspect the service with:

```bash
sudo systemctl status nirj-agent.service
sudo journalctl -u nirj-agent.service -f
```

Production commands use `/opt/nirj-agent/venv` and omit `--root`:

```bash
sudo /opt/nirj-agent/venv/bin/nirj-agent get-config
/opt/nirj-agent/venv/bin/nirj-agent status
sudo /opt/nirj-agent/venv/bin/nirj-agent manifest refresh
sudo /opt/nirj-agent/venv/bin/nirj-agent plan
sudo /opt/nirj-agent/venv/bin/nirj-agent apply
sudo /opt/nirj-agent/venv/bin/nirj-agent up
```

`apply` requires root and uses the previously cached manifest. It runs
`apt-get update` only when packages need to be installed, installs missing
packages, removes only obsolete packages previously managed by the agent, and
persists state after every package operation succeeds. It never runs
`autoremove`. The command deliberately rejects `--root` because that option
cannot sandbox apt operations.

`up` is the long-running production command. It requires root, refreshes the
configured manifest, applies it, and then remains running until it receives a
shutdown signal. It deliberately rejects `--root` for the same reason as
`apply`.

The production configuration is read from `/etc/nirj-agent/config.yaml` and
state is read from `/var/lib/nirj-agent/state.yaml`. A systemd unit will be
added with the production installer.
26 changes: 26 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"

[project]
name = "nirj-agent"
version = "0.1.0"
description = "Device management agent for Northern Ireland Raspberry Jam"
requires-python = ">=3.11"
dependencies = [
"PyYAML>=6.0,<7",
]

[project.optional-dependencies]
dev = [
"pytest>=8,<9",
]

[project.scripts]
nirj-agent = "nirj_agent.cli:main"

[tool.setuptools.packages.find]
where = ["src"]

[tool.pytest.ini_options]
testpaths = ["tests"]
18 changes: 18 additions & 0 deletions scripts/run-agent.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -Eeuo pipefail

readonly INSTALL_DIR="${NIRJ_AGENT_INSTALL_DIR:-/opt/nirj-agent}"
readonly REPO_DIR="${INSTALL_DIR}/source"
readonly VENV_DIR="${INSTALL_DIR}/venv"
readonly BRANCH="${NIRJ_AGENT_BRANCH:-main}"

export GIT_TERMINAL_PROMPT=0
export PIP_DISABLE_PIP_VERSION_CHECK=1

git -C "${REPO_DIR}" pull --ff-only origin "${BRANCH}"

"${VENV_DIR}/bin/python" -m pip install \
--upgrade \
"${REPO_DIR}"

exec "${VENV_DIR}/bin/nirj-agent" up
1 change: 1 addition & 0 deletions src/nirj_agent/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.1.0"
3 changes: 3 additions & 0 deletions src/nirj_agent/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .main import main

__all__ = ["main"]
Loading
Loading