# Offline / air-gapped ScoutFS install

Two workflows for hosts that can't reach `https://rpm-1.srvno.de/`
directly:

- **Staging-host mirror** — an internet-connected host fetches the
  RPMs, creates a local repo, and syncs it to the target hosts.
- **Ad-hoc RPM copy** — `dnf install /path/to/*.rpm` directly,
  skipping the repo layer.  Simplest, but loses automatic upgrades.

Both require a valid license key (HTTP Basic auth) on the internet-
connected side.  Once the RPMs are on the air-gapped side they're
plain signed RPMs — no runtime license check.

## Two distribution tracks

The webroot publishes both `el<N>/` and `rhel<N>/` subtrees.  Pick
the track that matches your **target hosts** (not the staging
host's distro):

| If target hosts run… | Use webroot subtree | .repo file              |
|---------------------|---------------------|--------------------------|
| AlmaLinux 8/9, Rocky, CentOS Stream, Oracle Linux | `el<N>/` | `scoutfs-el<N>.repo` |
| Real RHEL 8/9       | `rhel<N>/`          | `scoutfs-rhel<N>.repo`  |

The two tracks contain different RPM payloads — RHEL track ships
**vanilla upstream scoutfs (no notify patches, no scoutfs-notifyd)**,
EL track ships scoutfs with our notify patches applied.  Don't mix
them in a single mirror.

The examples below use `el9` as the working example.  For RHEL,
substitute `rhel9` everywhere `el9` appears.

---

## 1. Discovering what to download

The `https://rpm-1.srvno.de/rpms.json` manifest is **public** (no key
required) and lists every published package.  New shape reflects
the flat one-repo-per-EL layout:

```jsonc
{
  "generated":  "2026-04-24T12:34:56Z",
  "public_url": "https://rpm-1.srvno.de",
  "repos": [
    {
      "repo_subdir":   "el9",        // "el<N>" or "rhel<N>"
      "track":         "el",         // "el" (Alma/Rocky/CentOS-Stream/OL)
                                     // or "rhel" (RHEL/UBI track)
      "el":            9,
      "repo_file_url": "https://rpm-1.srvno.de/scoutfs-el9.repo",
      "browse_url":    "https://rpm-1.srvno.de/el9/",
      "versions":      ["1.30", "1.29", "1.28"],
      "packages": [
        {
          "scoutfs_version": "1.30",
          "name":            "kmod-scoutfs-<sanitized-kernel>",
          "type":            "kmod",      // kmod|utils|dkms|meta|meta-dkms|meta-latest|plugin
          "version":         "1.30",
          "release":         "0.el9_7",
          "arch":            "x86_64",
          "kernel":          "5.14.0-503.40.1.el9_7",
          "filename":        "kmod-scoutfs-....rpm",
          "url":             "https://rpm-1.srvno.de/el9/Packages/kmod-scoutfs-....rpm",
          "size_bytes":      1234567,
          "summary":         "ScoutFS kernel module..."
        }
      ]
    }
  ]
}
```

Useful jq queries from a staging host:

```sh
# Every URL for scoutfs 1.30 on the EL track, EL9
# (use repo_subdir="rhel9" for the RHEL track):
curl -s https://rpm-1.srvno.de/rpms.json \
  | jq -r '.repos[] | select(.repo_subdir=="el9") | .packages[]
           | select(.scoutfs_version=="1.30") | .url'

# Every kmod built against a specific kernel (across both tracks):
curl -s https://rpm-1.srvno.de/rpms.json \
  | jq -r '.repos[].packages[] | select(.kernel=="5.14.0-503.40.1.el9_7") | .url'

# Total download size for ScoutFS 1.30 on EL9:
curl -s https://rpm-1.srvno.de/rpms.json \
  | jq '[.repos[] | select(.repo_subdir=="el9") | .packages[] | select(.scoutfs_version=="1.30") | .size_bytes] | add'

# Every metapackage (the ones consumers `dnf install`):
curl -s https://rpm-1.srvno.de/rpms.json \
  | jq -r '.repos[].packages[] | select(.type=="meta" or .type=="meta-latest") | .filename'
```

The landing page at `https://rpm-1.srvno.de/` renders the same data
as a filterable table (with EL and version dropdowns).  Pick
packages visually; right-click for URLs.  The JSON manifest itself
doesn't require sign-in; only the RPM URLs do.

---

## 2. Mirror workflow (recommended)

### 2a. On a staging host with internet access

```sh
KEY=YOUR-LICENSE-KEY
VER=1.30            # or omit for "all published versions"
EL=9
DEST=~/scoutfs-mirror/el${EL}

mkdir -p "${DEST}/Packages"
cd "${DEST}/Packages"

# 1. Download every RPM for (version, EL) — or drop the version filter
#    to pull every published version into a single mirror.
curl -s https://rpm-1.srvno.de/rpms.json \
  | jq -r --arg v "${VER}" --argjson e ${EL} '
        .repos[] | select(.el==$e) | .packages[]
        | select($v == "" or .scoutfs_version == $v) | .url' \
  | while read -r url; do
      echo ">>> $(basename "${url}")"
      curl -fSL -u "${KEY}:scoutfs-client" -O "${url}"
    done

# 2. Pull the GPG signing key (public).
curl -fsSL https://rpm-1.srvno.de/RPM-GPG-KEY-scoutfs \
     -o "${DEST}/RPM-GPG-KEY-scoutfs"

# 3. Build repo metadata.  Single createrepo_c over everything you
#    downloaded — dnf will see all versions and let the user pick via
#    `dnf install scoutfs` or `dnf install scoutfs-1.29`.
cd "${DEST}"
createrepo_c --update .

# 4. (Optional) Grab the upstream .repo template for reference.
curl -fsSL "https://rpm-1.srvno.de/scoutfs-el${EL}.repo" \
     -o "${DEST}/scoutfs-el${EL}.repo.template"
```

### 2b. Transport to the air-gapped host

```sh
# Option A — tar + usb stick:
tar -C ~/scoutfs-mirror -czf scoutfs-mirror.tar.gz .
# copy scoutfs-mirror.tar.gz to the target, then:
sudo mkdir -p /srv/scoutfs-mirror
sudo tar -C /srv/scoutfs-mirror -xzf scoutfs-mirror.tar.gz

# Option B — rsync over ssh between jump hosts:
rsync -av ~/scoutfs-mirror/ target.internal:/srv/scoutfs-mirror/
```

### 2c. Install on the air-gapped host

**From the local filesystem directly:**

```ini
# /etc/yum.repos.d/scoutfs-local.repo
[scoutfs-local]
name=ScoutFS (local mirror)
baseurl=file:///srv/scoutfs-mirror/el9/
enabled=1
gpgcheck=1
gpgkey=file:///srv/scoutfs-mirror/el9/RPM-GPG-KEY-scoutfs
```

```sh
sudo rpm --import /srv/scoutfs-mirror/el9/RPM-GPG-KEY-scoutfs

# Install the latest-tracking alias (picks the newest version in your mirror):
sudo dnf --disablerepo='*' --enablerepo=scoutfs-local install scoutfs

# Or pin a specific version:
sudo dnf --disablerepo='*' --enablerepo=scoutfs-local install scoutfs-1.30

sudo modprobe scoutfs
```

**Or via an internal HTTP server** (nginx / apache / caddy pointing
at `/srv/scoutfs-mirror/`) — same `.repo` file, but set
`baseurl=http://mirror.internal/scoutfs-mirror/el9/`.

### 2d. Refreshing the mirror

Re-run step 2a on the staging host.  `createrepo_c --update` only
reprocesses changed RPMs; iterations are fast.

---

## 3. Ad-hoc RPM copy (simplest, one-off)

Appropriate for a single host or a one-time install where you don't
need automatic kernel-update tracking.

```sh
# On a staging host:
KEY=YOUR-LICENSE-KEY
TARGET_KVER=$(ssh target.internal uname -r)   # get from the target
VER=1.30

# Grab the metapackage, the matching kmod, the utils, and the DKMS
# fallback all at once (substitute repo_subdir="rhel9" for RHEL):
curl -s https://rpm-1.srvno.de/rpms.json \
  | jq -r --arg v "${VER}" --arg k "${TARGET_KVER}" '
      .repos[] | select(.repo_subdir=="el9") | .packages[]
      | select(.scoutfs_version == $v)
      | select(.type == "meta" or .type == "utils" or .type == "dkms"
               or (.type == "kmod" and .kernel == $k))
      | .url' \
  | while read -r url; do curl -fSL -u "${KEY}:scoutfs-client" -O "${url}"; done

# Copy the resulting *.rpm files plus RPM-GPG-KEY-scoutfs to the target.

# On the target:
sudo rpm --import RPM-GPG-KEY-scoutfs
sudo rpm --checksig *.rpm
sudo dnf install ./scoutfs-${VER}-*.rpm ./kmod-scoutfs-*.rpm ./scoutfs-utils-*.rpm
sudo modprobe scoutfs
```

**Caveat**: no auto-updates on kernel upgrades.  When the target host's
kernel changes, repeat the process or switch to the mirror workflow.

---

## 4. DKMS fallback for novel kernels

For target kernels that don't appear in the published matrix (brand-
new EL point releases, custom/backported kernels), each version also
publishes a noarch `dkms-scoutfs` package that rebuilds the kmod
locally against the running kernel.  The `scoutfs-<ver>` metapackage
actually declares its kmod requirement as
`(kmod-scoutfs = <ver> or dkms-scoutfs = <ver>)`, so on a host without
a matching pre-built kmod dnf will pick up the DKMS variant
automatically.

Mirror or copy it alongside the regular RPMs:

```sh
# On the staging host, grab just the DKMS RPMs:
curl -s https://rpm-1.srvno.de/rpms.json \
  | jq -r '.repos[].packages[] | select(.type == "dkms") | .url' \
  | xargs -I{} curl -fSL -u "${KEY}:scoutfs-client" -O {}

# On the target (needs dkms from EPEL):
sudo dnf install epel-release
sudo dnf install ./dkms-scoutfs-*.noarch.rpm ./scoutfs-utils-*.rpm \
                 ./scoutfs-${VER}-*.rpm
# DKMS rebuilds on first install and on every future kernel update.
```

---

## 5. Signature verification (always)

All RPMs are GPG-signed.  Verify before installing — the signing key
is the same across every version and EL:

```sh
rpm --import RPM-GPG-KEY-scoutfs
rpm --checksig *.rpm
# expected: "*.rpm: digests signatures OK" for every file
```

If a signature fails, don't install — fetch again, and if the failure
reproduces, contact the vendor.

---

## 6. License-key handling in offline scenarios

The license key is only needed to **download** RPMs, not to install
them.  Once they're on the air-gapped host they are plain signed
RPMs with no runtime check.

Practical implications:

- The target host never sees or stores the key.
- Keep the key in an env var or `~/.netrc` on the staging host, not
  in scripts checked into version control:

  ```
  # ~/.netrc (mode 0600)
  machine rpm-1.srvno.de
    login YOUR-LICENSE-KEY
    password scoutfs-client
  ```

  Then:

  ```sh
  curl -fSL --netrc -O https://rpm-1.srvno.de/el9/Packages/....rpm
  ```

- If your license expires, already-downloaded mirrors keep working
  indefinitely.  You only need a valid key to *fetch new* RPMs.

---

## 7. Troubleshooting

**`HTTP 401 Unauthorized` on download**

Check the `X-ScoutFS-Auth-Reason` header in a verbose curl:

```sh
curl -v -u "${KEY}:scoutfs-client" \
     https://rpm-1.srvno.de/el9/repodata/repomd.xml 2>&1 \
  | grep -i 'X-ScoutFS-Auth-Reason'
```

Expected values:

- `unknown license key` — typo or revoked
- `license expired on YYYY-MM-DD` — renew with the vendor

**`Package kmod-scoutfs-*.rpm is not signed`** on install

Did you `rpm --import` the GPG key?  If `gpgcheck=1` in your .repo
and you didn't import, dnf refuses.  For debugging only:

```sh
sudo dnf install --nogpgcheck ./kmod-scoutfs-*.rpm
```

**`No match for argument: scoutfs`** with a local repo

`createrepo_c` didn't run or dnf cache is stale:

```sh
cd /srv/scoutfs-mirror/el9/
sudo createrepo_c --update .
sudo dnf clean expire-cache
```

**Kernel mismatch** — modprobe complains about `vermagic`.  The RPM
was built for a different kernel.  Find one whose `kernel` field in
`rpms.json` matches your target's `uname -r` exactly, or install the
DKMS package (`dkms-scoutfs-<ver>`) and let it rebuild against the
running kernel.
