---
name: owncloud
description: "Manage ACG's ownCloud server (cloud.acghosting.com / 172.16.3.22, ownCloud 10.16 Community) via occ over SSH: read-only GPS backup inventory (who has an account, per-user storage + quota + last login, which GuruRMM endpoints run the desktop client) PLUS gated admin writes - user lifecycle, quota, groups, apps/config, maintenance mode, files scan/transfer, trashbin/versions cleanup. Reads run freely; writes require --confirm. Triggers: owncloud, cloud.acghosting, who backs up to owncloud, owncloud usage, owncloud audit, create/disable owncloud user, set owncloud quota, owncloud group, owncloud maintenance mode."
---

# ownCloud Skill

Client for ACG's live **ownCloud 10.16.0 Community** server
(`cloud.acghosting.com`, VM `172.16.3.22`, Rocky Linux 9.6), driven entirely
through the `occ` admin CLI over SSH.

Two halves:
- **Read (GPS audit, ungated):** which customers back up here and how much data
  they hold - and, with `--rmm`, which enrolled machines actually run the
  ownCloud desktop client.
- **Manage (gated):** user/quota/group lifecycle, apps/config, maintenance mode,
  filesystem scans, ownership transfer, trashbin/versions cleanup. Every
  state-changing command **refuses without `--confirm`** (prints the WOULD line,
  exits 3), mirroring the `b2` / `seafile` fleet convention.

This skill is one of the backup-verification siblings alongside `seafile`,
`b2` (Backblaze) and `datto-workplace`.

## Running - read (GPS audit)

```bash
OWNCLOUD=".claude/skills/owncloud/scripts/owncloud.py"
py "$OWNCLOUD" status                 # SSH ok + occ version / edition
py "$OWNCLOUD" users [--json]         # all users + enabled/quota/used/last login
py "$OWNCLOUD" usage [--json]         # per-user rollup, used GB desc (the audit view)
py "$OWNCLOUD" audit [--client NAME] [--rmm] [--json]
```

Add `--json` to `users` / `usage` / `audit` for machine-readable output.
`--host` overrides the server IP (default `172.16.3.22`).

## Running - manage (writes gated behind `--confirm`)

```bash
# users
py "$OWNCLOUD" user-add jdoe --display-name "J Doe" --email j@x.com -g Clients --gen-password --confirm
py "$OWNCLOUD" user-disable jdoe --confirm         # user-enable to re-enable
py "$OWNCLOUD" user-reset-password jdoe --password-env OC_NEW_PW --confirm
py "$OWNCLOUD" user-modify jdoe --email new@x.com --confirm
py "$OWNCLOUD" user-delete jdoe --confirm          # DELETES the user AND all their data

# quota   (quota <uid> reads it; value like '5 GB', 'none' = unlimited)
py "$OWNCLOUD" quota jdoe
py "$OWNCLOUD" quota-set jdoe "50 GB" --confirm     # quota-reset -> back to default

# groups  (groups / group-members / user-groups are read-only)
py "$OWNCLOUD" groups
py "$OWNCLOUD" group-add Clients --confirm
py "$OWNCLOUD" group-add-member Clients -m jdoe -m asmith --confirm
py "$OWNCLOUD" group-remove-member Clients -m jdoe --confirm
py "$OWNCLOUD" group-delete Clients --confirm

# shares (read-only - occ cannot create shares; see Notes)
py "$OWNCLOUD" shares [--user jdoe]

# server / apps / config
py "$OWNCLOUD" maintenance status|on|off [--confirm]   # on/off need --confirm
py "$OWNCLOUD" apps
py "$OWNCLOUD" app-enable|app-disable <app> --confirm
py "$OWNCLOUD" config-get [name]                        # no name -> dump all system config
py "$OWNCLOUD" config-set <name> --value V [--type string|integer|double|boolean|json] --confirm

# files admin
py "$OWNCLOUD" files-scan (--user jdoe | --all) [--path sub/dir] --confirm
py "$OWNCLOUD" transfer-ownership <src> <dst> [--path folder] --confirm
py "$OWNCLOUD" trashbin-cleanup --user jdoe --confirm
py "$OWNCLOUD" versions-cleanup --user jdoe --confirm
# fleet-wide purge needs the extra ack flag on top of --confirm:
py "$OWNCLOUD" versions-cleanup --all-users --force-all-users --confirm
```

### Passwords + the credential rule

`user-add` / `user-reset-password` take the password one of three ways:
`--gen-password` (generates a strong one, prints it **once**), `--password-env VAR`
(reads it from that env var - preferred, keeps it out of shell history), or a
literal `--password` (discouraged: lands in history + `ps`). On the server the
value is passed via occ's own `OC_PASS` + `--password-from-env`.

**Any password created or reset here MUST be stored in the SOPS vault** (house
rule) - the command prints a `[CRITICAL]` reminder with the exact `vault.sh`
line (suggested path `infrastructure/owncloud-users/<uid>.sops.yaml`).

### Gating / exit codes

- write command without `--confirm` -> prints `Would: ...`, **exit 3**, no change.
- `trashbin-cleanup` / `versions-cleanup` with **no** `--user` and **no**
  `--all-users` -> **hard-fail exit 2** (occ treats "no user" as *every* user; a
  fleet-wide purge must be made explicit with `--all-users`).
- `--all-users` on those two also **hard-fails exit 2** unless
  `--force-all-users` is passed **in addition to** `--confirm` - a single stray
  `--confirm` must never be enough to irreversibly purge every user's history.
- invalid uid on `user-add` -> **exit 2**.

## Access channel

There is **no ownCloud admin web / OCS account** in the vault, so the channel is
**SSH + the `occ` CLI**, not the OCS HTTP API (the OCS endpoint is reachable at
`https://cloud.acghosting.com/status.php` but we lack an OCS admin cred). The
client SSHes to `root@172.16.3.22:22` with **paramiko** (password auth,
AutoAddPolicy) and runs occ **as the web user**:

```
sudo -u apache php /var/www/owncloud/occ <cmd>
```

Credentials load lazily from the SOPS vault (never hardcoded):
`infrastructure/owncloud-vm.sops.yaml` -> `credentials.username` (root) /
`credentials.password`.

## The occ / DB commands it settled on

- **status:** `occ status --output=json` (+ `occ -V` for the display version).
- **users:** `occ user:list --show-all-attributes --output=json` -> per user:
  `uid, displayName, email, quota, enabled, lastLogin, creationTime, home,
  backend`. `lastLogin`/`creationTime` are Unix epoch seconds (0 = never); the
  client renders them as UTC.
- **used bytes:** occ has **no `user:info` / per-user usage** on this version, so
  used storage is read from ownCloud's own **filecache** (the exact figure the
  UI shows). DB creds are read live from `config.php` (we already have root), then:
  `SELECT s.id, fc.size FROM oc_storages s JOIN oc_filecache fc ON
  fc.storage=s.numeric_id WHERE s.id LIKE 'home::%' AND fc.path='';`
  Each row is `home::<uid>` -> root size in bytes.

## What the data means

- **used_bytes** is the authoritative per-account storage used; an `enabled` user
  with non-zero usage is actively backing up here. `usage`/`audit` roll this up,
  sorted desc, with a total.
- **quota** is a string: `default`, `none` (unlimited), or a byte/size value.
- Usage is concentrated: as of the build (2026-07-05) a single account (`pavon`)
  holds ~14 TB and `sysadmin` ~2.9 TB, dwarfing the rest - ownCloud is used by a
  handful of clients, not the whole GPS base.

## The "both" cross-check (`--rmm`)

Server-side (occ) tells you who has an account; `--rmm` adds the endpoint half. It
logs in to GuruRMM (`infrastructure/gururmm-server.sops.yaml`) and, for each agent
(filter with `--client`), runs a **read-only** detection PowerShell reporting
installed products / running processes / config folders matching **`ownCloud`**.
An agent is reported only if the client is actually present. This dispatches a
command to live endpoints - **always scope with `--client`** rather than sweeping
all 350+ agents.

## Notes / gotchas / limitations

- **Dirty filecache (-1):** if a user's cached root size is `-1` (cache not yet
  computed), the client falls back to `du -sb <home>/files` for that one user.
  That du can take minutes on a multi-hundred-GB home (timeout is generous, 900s);
  `used_source` in `--json` reports `filecache` vs `du` per user. At build time
  one account (`anaise`) was dirty and resolved via du (~0.6 TB).
- **Orphan storages:** the filecache can contain `home::<uid>` rows for deleted
  users (e.g. a former `QWM-Sheila`); the skill keys off the live `occ user:list`,
  so orphans do not appear in `users`/`usage`.
- **No OCS/web API:** deeper per-file / sharing queries would need an OCS admin
  cred we do not have; occ + filecache cover the audit needs.
- **Shares are read-only here:** occ on this version exposes no share create/list
  (only `sharing:cleanup-remote-storages`), so `shares` reads straight from the
  `oc_share` DB table (joined to `oc_filecache` for the real path). Creating a
  share would need the OCS API / an admin web cred we do not have.
- **occ command surface was verified live** against this exact 10.16 box (`occ
  list` / `occ help <cmd>`) before wiring the writes - flags differ by version.
  Notable ones used: `user:modify <uid> displayname|email`, quota via
  `user:setting <uid> files quota --value=`, passwords via `--password-from-env`,
  `files:transfer-ownership -s`, `trashbin:cleanup`/`versions:cleanup` (no user
  arg = ALL users, hence the `--all-users` guard).
- Shared plumbing (repo-root resolve, vault read, GuruRMM client + endpoint
  detection) lives in `.claude/scripts/backup_common.py`, used by all the
  server-backup skills. Detection patterns must be precise per backend - this
  skill uses `ownCloud` (not a bare `cloud`, which would over-match).
