openstack-janitor
A CLI that audits an OpenStack cloud for orphaned and wasteful resources.
Status: early development. Seven detectors and a clean command are
working — see Detectors and Cleaning; more
detectors and safety rails are coming — see Roadmap.
Install
Requires Python 3.9+. On older interpreters, pip automatically selects a compatible older version of openstacksdk.
From PyPI:
pipx install openstack-janitor # recommended for CLI use
# or
pip install openstack-janitor
Standalone Linux binary — no Python needed at all. Built against glibc 2.28, so it runs on RHEL 8-era hosts whose system Python is too old for the package:
curl -LO https://github.com/mabunemeh/openstack-janitor/releases/latest/download/janitor-linux-x86_64
chmod +x janitor-linux-x86_64
./janitor-linux-x86_64 audit --cloud my-cloud
From source:
git clone https://github.com/mabunemeh/openstack-janitor
cd openstack-janitor
pip install -e .
Old distro pip (e.g. Ubuntu 22.04's pip 22.0): source installs can fail with
No module named 'packaging.licenses'— the distro-patched pip leaks the system's oldpackaginginto the build environment. Installing from PyPI is unaffected. For source installs, use a fresh venv with an upgraded pip:python3 -m venv .venv && .venv/bin/pip install -U pip.
Usage
janitor detectors
janitor audit
janitor audit -c my-cloud
janitor audit -d unattached-volumes -d orphaned-ports
janitor audit -f json > findings.json
janitor audit -f html > report.html
Short options: -c / --cloud, -d / --detector, -f / --format, -h / --help.
janitor detectors lists every registered detector (name and description)
without connecting to a cloud. Use the names it prints with
audit --detector.
--format table (the default) prints a rich table; json and html write
machine-readable / shareable reports to stdout.
Example output when orphaned volumes are found:
$ janitor audit --cloud my-cloud
openstack-janitor findings
┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Type ┃ ID ┃ Name ┃ Project ┃ Reason ┃
┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ volume │ a1b2c3d4… │ old-db │ proj-1 │ volume is unattached │
│ │ │ │ │ (status=available) │
└───────────────┴───────────┴─────────┴─────────┴──────────────────────────────┘
$ echo $?
1
janitor audit exits 0 when nothing is found, 1 when findings were
reported (so it's safe to wire into a cron job or CI check), 2 if an
unknown --detector name is given, and 3 if connecting to the cloud
fails.
Cleaning
janitor clean -d unattached-volumes # dry run: preview only, deletes nothing
janitor clean -d unattached-volumes --yes # delete what that detector flags
janitor clean -d unattached-volumes -e vol-0001 --yes # keep specific resource IDs
janitor clean is dry run by default — it re-runs the detectors and shows
what it would delete, but touches nothing. Pass --yes to actually delete.
Deletes are real and irreversible: once a volume, snapshot, floating IP,
port, security group, instance, or image is gone, it is gone.
--detector is required. clean refuses to act on every detector at once,
so a single command can never delete across all seven resource types.
Read this before using --yes:
- Some detectors have no age threshold.
orphaned-portsandunused-security-groupsflag by state alone, so a port or group created seconds ago — mid-provisioning, mid-CI-run — is a finding and will be deleted. Until tag/age rails land, keep--detectornarrow. - The preview does not bind the execution. Dry run and
--yesare two independent detection passes. A resource created in between is deleted without ever having appeared in the table you reviewed. - Cleaning can create new findings. Deleting a shutoff instance leaves its
volumes unattached; deleting snapshots orphans the images built from them.
The next run will flag those. Re-read each dry run rather than looping
--yesblindly. --excludetakes resource IDs, not names. An--excludevalue that matches no finding aborts the run rather than being ignored, so a typo cannot silently delete what it was meant to protect.- Deletes are asynchronous. A successful call means the delete was
accepted; verify with
janitor auditafterwards.
Tag/age safety rails (e.g. a janitor:keep marker) are on the
roadmap but not implemented yet.
janitor clean exits 0 on a successful dry run or execute, 1 if any
resource was not deleted during --yes — either the deletion failed or the
detector does not support cleaning (other resources are still processed;
failures are isolated per resource) — 2 for a missing or unknown
--detector or an --exclude ID that matched nothing, and 3 if connecting
to the cloud or scanning it fails.
Detectors
| Name | Flags |
|---|---|
unattached-volumes |
Volumes in available status with no attachments. |
unassociated-floating-ips |
Floating IPs not associated with any port. |
orphaned-ports |
Ports with no device owner and no device id. Infrastructure ports (DHCP, routers, load balancer VIPs) always carry one of these, so they are never flagged; a pre-created port awaiting attachment will be. |
old-snapshots |
Volume snapshots older than a threshold (default 90 days). |
shutoff-instances |
Instances in SHUTOFF status whose last update is older than a threshold (default 30 days). There is no "shutoff since" field in the Compute API, so the age is a conservative lower bound — the detector may under-report but never over-reports. |
unused-security-groups |
Security groups not attached to any port and not referenced as a remote_group_id by any rule. The per-project default group is always skipped. |
orphan-snapshot-images |
Glance images whose block_device_mapping references a Cinder volume snapshot that no longer exists. Includes hidden images. |
Detection is always read-only — janitor audit never modifies anything. The
same detectors also know how to delete what they flag, but only
janitor clean --yes ever does so.
Resources without a parseable timestamp are never flagged by the age-based
detectors. Note that orphaned-ports and unused-security-groups have no age
threshold at all, so they can flag a resource created seconds ago — see the
warning under Cleaning. Thresholds become configurable once
janitor.toml support lands (see Roadmap).
Authentication
openstack-janitor uses openstacksdk
for authentication, so anything openstacksdk understands works here too:
- A named cloud from
clouds.yamlvia--cloud my-cloud(or theOS_CLOUDenvironment variable). - The standard
OS_*environment variables (OS_AUTH_URL,OS_USERNAME,OS_PASSWORD,OS_PROJECT_NAME, etc.) if no cloud is specified.
See the openstacksdk configuration documentation for the full resolution order and file locations.
Roadmap
janitor.tomlfor per-cloud configuration (which detectors run, age thresholds, exclusions).- Safety rails: a
janitor:keeptag (or similar) so resources can be marked "do not touch" beforecleanever deletes anything, beyond today's--excludeflag and dry-run review.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file openstack_janitor-0.3.0.tar.gz.
File metadata
- Download URL: openstack_janitor-0.3.0.tar.gz
- Upload date:
- Size: 32.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
def2165a72d809e225f49535c5fe145a73cc6aecc6c446840e3e63037fe847b6
|
|
| MD5 |
01680f814c084c423e6ab166abe47003
|
|
| BLAKE2b-256 |
91be1be4c904e6695b4b68809440bcc4f8416de1210c239cf523634f869fdbea
|
Provenance
The following attestation bundles were made for openstack_janitor-0.3.0.tar.gz:
Publisher:
release.yml on mabunemeh/openstack-janitor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
openstack_janitor-0.3.0.tar.gz -
Subject digest:
def2165a72d809e225f49535c5fe145a73cc6aecc6c446840e3e63037fe847b6 - Sigstore transparency entry: 2245808050
- Sigstore integration time:
-
Permalink:
mabunemeh/openstack-janitor@44de0bd73dc71a328dd2e4369a5cdc9ac47fd332 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/mabunemeh
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@44de0bd73dc71a328dd2e4369a5cdc9ac47fd332 -
Trigger Event:
push
-
Statement type:
File details
Details for the file openstack_janitor-0.3.0-py3-none-any.whl.
File metadata
- Download URL: openstack_janitor-0.3.0-py3-none-any.whl
- Upload date:
- Size: 28.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d2ef4f36d1f9d79bd1a205e5dedadfd999ffba6cb736b94b180e934c66d96c43
|
|
| MD5 |
5fcd4ee998a2c6a1ad4a60ce8d85883d
|
|
| BLAKE2b-256 |
bbdaee172ae17c709b3587f5600e00f52352c5a3eb3dcd5f6b7fdd5914786eaf
|
Provenance
The following attestation bundles were made for openstack_janitor-0.3.0-py3-none-any.whl:
Publisher:
release.yml on mabunemeh/openstack-janitor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
openstack_janitor-0.3.0-py3-none-any.whl -
Subject digest:
d2ef4f36d1f9d79bd1a205e5dedadfd999ffba6cb736b94b180e934c66d96c43 - Sigstore transparency entry: 2245808704
- Sigstore integration time:
-
Permalink:
mabunemeh/openstack-janitor@44de0bd73dc71a328dd2e4369a5cdc9ac47fd332 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/mabunemeh
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@44de0bd73dc71a328dd2e4369a5cdc9ac47fd332 -
Trigger Event:
push
-
Statement type: