Skip to content

Commit 79d89d7

Browse files
author
Sqoia Dev Agent
committed
feat: complete rename to slurmledger, balance enforcement, production guide
File Rename: - slurmcostmanager.html/js/css → slurmledger.html/js/css - Updated all references: manifest.json, Makefile, metainfo XML, CI workflows, test files, README, CODE_OF_CONDUCT Balance Enforcement: - balance_enforcer.py: standalone script for allocation limit enforcement - Uses SLURM native GrpTRESMins to cap accounts at budget - Dry-run mode (--check) and enforcement mode (--enforce) - Cron-ready with logging to /var/log/slurmledger/ - Balance Checker panel in Admin dashboard with live status - Jobs blocked with PENDING reason AssocGrpCPUMinutesLimit when over budget Production Setup Guide: - PRODUCTION_SETUP.md: 14-step walkthrough from bare metal to first invoice - Prerequisites, install, permissions, DB verification, configuration - Allocation setup, billing rules, financial integration, roles - Balance enforcer cron, backup cron, verification checklist - Troubleshooting section for common issues
1 parent 5379481 commit 79d89d7

14 files changed

Lines changed: 616 additions & 36 deletions

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,6 @@ jobs:
9898
files: |
9999
dist/**/*
100100
rpmbuild/RPMS/**/*.rpm
101-
slurmcostmanager_*.deb
101+
slurmledger_*.deb
102102
env:
103103
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

CODE_OF_CONDUCT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ representative at an online or offline event.
6060

6161
Instances of abusive, harassing, or otherwise unacceptable behavior may be
6262
reported to the community leaders responsible for enforcement at
63-
slurmcostmanager-maintainers@example.com. All complaints will be reviewed and investigated
63+
slurmledger-maintainers@example.com. All complaints will be reviewed and investigated
6464
promptly and fairly.
6565

6666
All community leaders are obligated to respect the privacy and security of the

Makefile

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
DIST=dist
22
COCKPIT_DEVEL_DIR=$(HOME)/.local/share/cockpit
33
COCKPIT_PROD_DIR=/usr/share/cockpit
4-
COCKPIT_DEVEL_TARGET=$(COCKPIT_DEVEL_DIR)/slurmcostmanager
5-
COCKPIT_PROD_TARGET=$(COCKPIT_PROD_DIR)/slurmcostmanager
4+
COCKPIT_DEVEL_TARGET=$(COCKPIT_DEVEL_DIR)/slurmledger
5+
COCKPIT_PROD_TARGET=$(COCKPIT_PROD_DIR)/slurmledger
66
INSTALL_DIR = /usr/share/cockpit/slurmledger
77
CONFIG_DIR = /etc/slurmledger
88
VERSION:=$(shell jq -r .version manifest.json)
@@ -12,14 +12,14 @@ RPMBUILD?=rpmbuild
1212

1313
all: build
1414

15-
org.cockpit_project.slurmcostmanager.metainfo.xml: org.cockpit_project.slurmcostmanager.metainfo.xml.in manifest.json
15+
org.cockpit_project.slurmledger.metainfo.xml: org.cockpit_project.slurmledger.metainfo.xml.in manifest.json
1616
>sed -e "s/@VERSION@/$(VERSION)/" -e "s/@DATE@/$(DATE)/" $< > $@
1717

18-
build: org.cockpit_project.slurmcostmanager.metainfo.xml
18+
build: org.cockpit_project.slurmledger.metainfo.xml
1919
>mkdir -p $(DIST)
2020
>cp -r src/* $(DIST)/
2121
>cp manifest.json $(DIST)/
22-
>cp org.cockpit_project.slurmcostmanager.metainfo.xml $(DIST)/
22+
>cp org.cockpit_project.slurmledger.metainfo.xml $(DIST)/
2323

2424
install: build
2525
>mkdir -p $(DESTDIR)$(INSTALL_DIR)
@@ -30,7 +30,7 @@ install: build
3030
>chmod 640 $(DESTDIR)$(CONFIG_DIR)/*.json
3131

3232
clean:
33-
>rm -rf $(DIST) rpmbuild debbuild slurmcostmanager-*.tar.gz slurmcostmanager_*.deb org.cockpit_project.slurmcostmanager.metainfo.xml
33+
>rm -rf $(DIST) rpmbuild debbuild slurmledger-*.tar.gz slurmledger_*.deb org.cockpit_project.slurmledger.metainfo.xml
3434

3535
devel-install: build
3636
>mkdir -p $(COCKPIT_DEVEL_DIR)
@@ -51,22 +51,22 @@ watch:
5151
>done
5252

5353
package: build
54-
>tar -czf slurmcostmanager-$(VERSION).tar.gz -C $(DIST) . --transform 's,^,slurmcostmanager-$(VERSION)/,'
54+
>tar -czf slurmledger-$(VERSION).tar.gz -C $(DIST) . --transform 's,^,slurmledger-$(VERSION)/,'
5555

5656
rpm: package
5757
>rm -rf rpmbuild
5858
>mkdir -p rpmbuild/BUILD rpmbuild/RPMS rpmbuild/SOURCES rpmbuild/SPECS rpmbuild/SRPMS
59-
>cp slurmcostmanager-$(VERSION).tar.gz rpmbuild/SOURCES/
60-
>printf 'Summary: SlurmLedger\nName: slurmcostmanager\nVersion: $(VERSION)\nRelease: 1\nLicense: MIT\nSource0: %%{name}-%%{version}.tar.gz\nBuildArch: noarch\nRequires: cockpit\nRequires: python3\nRequires: python3-pymysql\nRequires: python3-reportlab\n%%description\nSlurmLedger Cockpit plugin\n%%prep\n%%setup -q\n%%build\n%%install\nmkdir -p %%{buildroot}/usr/share/cockpit/slurmcostmanager\ncp -a * %%{buildroot}/usr/share/cockpit/slurmcostmanager/\nmkdir -p %%{buildroot}/etc/slurmledger\ncp rates.json %%{buildroot}/etc/slurmledger/rates.json\ncp institution.json %%{buildroot}/etc/slurmledger/institution.json\n%%files\n/usr/share/cockpit/slurmcostmanager\n%%config(noreplace) /etc/slurmledger/rates.json\n%%config(noreplace) /etc/slurmledger/institution.json\n' > rpmbuild/SPECS/slurmcostmanager.spec
61-
>$(RPMBUILD) --define "_topdir $(PWD)/rpmbuild" -bb rpmbuild/SPECS/slurmcostmanager.spec
59+
>cp slurmledger-$(VERSION).tar.gz rpmbuild/SOURCES/
60+
>printf 'Summary: SlurmLedger\nName: slurmledger\nVersion: $(VERSION)\nRelease: 1\nLicense: MIT\nSource0: %%{name}-%%{version}.tar.gz\nBuildArch: noarch\nRequires: cockpit\nRequires: python3\nRequires: python3-pymysql\nRequires: python3-reportlab\n%%description\nSlurmLedger Cockpit plugin\n%%prep\n%%setup -q\n%%build\n%%install\nmkdir -p %%{buildroot}/usr/share/cockpit/slurmledger\ncp -a * %%{buildroot}/usr/share/cockpit/slurmledger/\nmkdir -p %%{buildroot}/etc/slurmledger\ncp rates.json %%{buildroot}/etc/slurmledger/rates.json\ncp institution.json %%{buildroot}/etc/slurmledger/institution.json\n%%files\n/usr/share/cockpit/slurmledger\n%%config(noreplace) /etc/slurmledger/rates.json\n%%config(noreplace) /etc/slurmledger/institution.json\n' > rpmbuild/SPECS/slurmledger.spec
61+
>$(RPMBUILD) --define "_topdir $(PWD)/rpmbuild" -bb rpmbuild/SPECS/slurmledger.spec
6262

6363
deb: package
6464
>rm -rf debbuild
65-
>mkdir -p debbuild/usr/share/cockpit/slurmcostmanager
66-
>cp -a $(DIST)/* debbuild/usr/share/cockpit/slurmcostmanager/
65+
>mkdir -p debbuild/usr/share/cockpit/slurmledger
66+
>cp -a $(DIST)/* debbuild/usr/share/cockpit/slurmledger/
6767
>mkdir -p debbuild/DEBIAN
68-
>echo "Package: slurmcostmanager\nVersion: $(VERSION)\nSection: admin\nPriority: optional\nArchitecture: all\nMaintainer: Unknown\nDepends: cockpit\nDescription: SlurmLedger Cockpit plugin" > debbuild/DEBIAN/control
69-
>dpkg-deb --build debbuild slurmcostmanager_$(VERSION)_all.deb
68+
>echo "Package: slurmledger\nVersion: $(VERSION)\nSection: admin\nPriority: optional\nArchitecture: all\nMaintainer: Unknown\nDepends: cockpit\nDescription: SlurmLedger Cockpit plugin" > debbuild/DEBIAN/control
69+
>dpkg-deb --build debbuild slurmledger_$(VERSION)_all.deb
7070
>rm -rf debbuild
7171

7272
check:

PRODUCTION_SETUP.md

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
# SlurmLedger Production Setup Guide
2+
3+
## Prerequisites
4+
5+
- SLURM cluster with SlurmDBD configured (MySQL/MariaDB backend)
6+
- Cockpit installed on the head/admin node
7+
- Root access (for initial setup only)
8+
- Python 3.9+
9+
10+
## Step 1: Install Dependencies
11+
12+
### RHEL/Rocky Linux 9
13+
```bash
14+
sudo dnf install cockpit python3 python3-pymysql
15+
sudo pip3 install reportlab
16+
sudo systemctl enable --now cockpit.socket
17+
```
18+
19+
### Ubuntu 22.04+
20+
```bash
21+
sudo apt install cockpit python3 python3-pymysql python3-reportlab
22+
sudo systemctl enable --now cockpit.socket
23+
```
24+
25+
## Step 2: Install SlurmLedger
26+
27+
### From Release Package
28+
```bash
29+
# Download latest release
30+
curl -LO https://github.com/NessieCanCode/SlurmLedger/releases/latest/download/slurmledger-1.0.0-1.noarch.rpm
31+
sudo dnf install slurmledger-1.0.0-1.noarch.rpm
32+
```
33+
34+
### From Source
35+
```bash
36+
git clone https://github.com/NessieCanCode/SlurmLedger.git
37+
cd SlurmLedger
38+
pip3 install -r requirements.txt
39+
make build
40+
sudo make install
41+
```
42+
43+
## Step 3: Configure File Permissions
44+
45+
```bash
46+
# Create config directory
47+
sudo mkdir -p /etc/slurmledger
48+
sudo mkdir -p /etc/slurmledger/invoices
49+
sudo mkdir -p /var/log/slurmledger
50+
51+
# Copy default configs (only if they don't exist)
52+
sudo cp -n /usr/share/cockpit/slurmledger/rates.json /etc/slurmledger/
53+
sudo cp -n /usr/share/cockpit/slurmledger/institution.json /etc/slurmledger/
54+
55+
# Set permissions
56+
sudo chown -R root:cockpit-ws /etc/slurmledger
57+
sudo chmod 750 /etc/slurmledger
58+
sudo chmod 640 /etc/slurmledger/*.json
59+
sudo chmod 750 /etc/slurmledger/invoices
60+
61+
# Log directory
62+
sudo chown root:root /var/log/slurmledger
63+
sudo chmod 750 /var/log/slurmledger
64+
```
65+
66+
## Step 4: Verify SlurmDB Access
67+
68+
SlurmLedger reads database credentials from `slurmdbd.conf`. Verify the Cockpit user can read it:
69+
70+
```bash
71+
# Check slurmdbd.conf is readable
72+
sudo cat /etc/slurm/slurmdbd.conf | grep StoragePass
73+
# Should show the database password
74+
75+
# Test database connectivity
76+
python3 /usr/share/cockpit/slurmledger/slurmdb.py \
77+
--start $(date -d '1 month ago' +%Y-%m-%d) \
78+
--end $(date +%Y-%m-%d) \
79+
--output - | python3 -m json.tool | head -20
80+
```
81+
82+
If this fails, check:
83+
- `slurmdbd.conf` has correct `StorageHost`, `StorageUser`, `StoragePass`
84+
- MySQL/MariaDB is running and accessible
85+
- The storage user has SELECT access to the `slurm_acct_db` database
86+
87+
## Step 5: Access the Plugin
88+
89+
1. Open a browser to `https://your-server:9090`
90+
2. Log in with an admin account
91+
3. Click "SlurmLedger" in the left navigation
92+
4. The **Setup Wizard** will guide you through initial configuration:
93+
- **Step 1**: Enter institution details (name, address, contacts)
94+
- **Step 2**: Set billing rates (CPU rate per core-hour, GPU rate)
95+
- **Step 3**: Test database connection
96+
97+
## Step 6: Configure Billing Rates
98+
99+
Navigate to **Administration → Rate Configuration**:
100+
101+
1. Set the default CPU rate ($/core-hour). Common range: $0.005 – $0.05
102+
2. Set the default GPU rate ($/GPU-hour). Common range: $0.05 – $0.50
103+
3. Add per-account overrides if needed (e.g., discounted rates for funded groups)
104+
4. Click **Save**
105+
106+
### Historical Rates
107+
If rates changed in the past, add entries in the **Historical Rates** section so retroactive billing is accurate.
108+
109+
## Step 7: Set Up Allocations (Optional)
110+
111+
Navigate to **Administration → Allocations**:
112+
113+
1. For each account, choose:
114+
- **Pre-paid**: Account has a fixed SU budget. Jobs are blocked when exhausted.
115+
- **Post-paid**: Account is billed after the fact. No enforcement.
116+
2. Set budget (in Service Units / core-hours)
117+
3. Set allocation period (annual, quarterly, monthly)
118+
4. Configure alert thresholds (default: 80%, 90%, 100%)
119+
120+
## Step 8: Configure Billing Rules
121+
122+
Navigate to **Administration → Billing Rules**:
123+
124+
Default rules (adjust as needed):
125+
-**No charge for failed jobs** (except OOM and timeout)
126+
-**No charge for jobs under 1 minute**
127+
- ❌ Debug partition 50% discount (enable if applicable)
128+
- ❌ Visualization partition free (enable if applicable)
129+
130+
## Step 9: Configure Institution Profile
131+
132+
Navigate to **Administration → Institution Profile**:
133+
134+
1. Fill in all required fields (name, address, contacts)
135+
2. Upload institution logo (for invoices)
136+
3. Enter bank/payment details (for invoice footer)
137+
4. Set payment terms (e.g., "Net 30")
138+
139+
## Step 10: Set Up Balance Enforcement (Optional)
140+
141+
For pre-paid allocations, install the cron job:
142+
143+
```bash
144+
# Create cron job for hourly balance checks
145+
sudo tee /etc/cron.d/slurmledger-enforcer << 'EOF'
146+
# SlurmLedger Balance Enforcer — check allocations hourly
147+
0 * * * * root /usr/bin/python3 /usr/share/cockpit/slurmledger/balance_enforcer.py --enforce --log /var/log/slurmledger/enforcer.log
148+
EOF
149+
sudo chmod 644 /etc/cron.d/slurmledger-enforcer
150+
151+
# Test it first (dry run):
152+
sudo python3 /usr/share/cockpit/slurmledger/balance_enforcer.py --check
153+
```
154+
155+
The enforcer uses SLURM's native `GrpTRESMins` limit to cap accounts at their allocation. Jobs submitted after the limit is reached will be held in PENDING state with reason `AssocGrpCPUMinutesLimit`.
156+
157+
## Step 11: Configure Financial Integration (Optional)
158+
159+
Navigate to **Administration → Financial Integration**:
160+
161+
1. Select your ERP system (Oracle Financials, Workday, Banner, Kuali, or Generic Webhook)
162+
2. Enter webhook URL and API key
163+
3. Map SLURM accounts to Chart of Accounts codes
164+
4. Click **Test Connection**
165+
166+
## Step 12: Set Up Roles
167+
168+
Navigate to **Administration → Institution Profile** (roles section in institution.json):
169+
170+
```json
171+
{
172+
"roles": {
173+
"admins": ["root", "hpc-admin"],
174+
"finance": ["billing-dept"],
175+
"pis": []
176+
}
177+
}
178+
```
179+
180+
- **Admins**: Full access to all features
181+
- **Finance**: Read-only access to invoices, can mark invoices as paid
182+
- **PIs**: Auto-detected from SLURM account coordinators
183+
- **Members**: Anyone else — sees only their own usage
184+
185+
## Step 13: Generate Your First Invoice
186+
187+
1. Navigate to **Detailed Transactions**
188+
2. Select a month and account
189+
3. Click **Export Invoice**
190+
4. Review the PDF — verify rates, line items, and totals
191+
5. Navigate to **Invoices** to see the invoice in the ledger
192+
6. Click **Mark as Sent** when you send it to the PI
193+
194+
## Step 14: Set Up Backups
195+
196+
```bash
197+
# Daily backup of all SlurmLedger config and data
198+
sudo tee /etc/cron.daily/slurmledger-backup << 'EOF'
199+
#!/bin/bash
200+
BACKUP_DIR=/backup/slurmledger/$(date +%Y%m%d)
201+
mkdir -p "$BACKUP_DIR"
202+
cp -a /etc/slurmledger/ "$BACKUP_DIR/"
203+
find /backup/slurmledger/ -maxdepth 1 -mtime +90 -exec rm -rf {} \;
204+
EOF
205+
sudo chmod 755 /etc/cron.daily/slurmledger-backup
206+
```
207+
208+
## Verification Checklist
209+
210+
- [ ] Cockpit accessible at https://server:9090
211+
- [ ] SlurmLedger appears in Cockpit navigation
212+
- [ ] Setup wizard completes successfully
213+
- [ ] Billing data loads for current month
214+
- [ ] Rates are configured (not demo values)
215+
- [ ] Institution profile is complete
216+
- [ ] Test invoice generates with correct branding
217+
- [ ] Invoice numbers are sequential
218+
- [ ] Balance enforcer runs without errors (dry run)
219+
- [ ] Backup cron is active
220+
- [ ] File permissions are correct on /etc/slurmledger/
221+
222+
## Troubleshooting
223+
224+
### "Failed to load data" on first visit
225+
- Check `slurmdbd.conf` is readable by the Cockpit user
226+
- Verify MySQL is running: `systemctl status mariadb`
227+
- Test manually: `python3 slurmdb.py --start 2026-01-01 --end 2026-12-31 --output -`
228+
229+
### Invoice PDF has no logo or bank details
230+
- Complete the Institution Profile in Administration
231+
- Upload a logo (PNG/JPG, under 256KB)
232+
- Fill in the bank/payment information
233+
234+
### Balance enforcer says "No allocations configured"
235+
- Set up allocations in Administration → Allocations
236+
- Only "prepaid" allocations are enforced
237+
238+
### Permission denied on config save
239+
- Check `/etc/slurmledger/` ownership: `ls -la /etc/slurmledger/`
240+
- Should be `root:cockpit-ws 750` for directory, `640` for files

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,23 @@ Create custom rules via the admin UI — no config file editing required.
184184
└─────────────┘
185185
```
186186

187+
## Balance Enforcement
188+
189+
For pre-paid allocations, `balance_enforcer.py` enforces budget limits via SLURM's native `GrpTRESMins` mechanism. Install the cron job to run it hourly:
190+
191+
```
192+
# /etc/cron.d/slurmledger-enforcer
193+
0 * * * * root /usr/bin/python3 /usr/share/cockpit/slurmledger/balance_enforcer.py --enforce --log /var/log/slurmledger/enforcer.log
194+
```
195+
196+
Run a dry-run check manually at any time:
197+
198+
```bash
199+
python3 /usr/share/cockpit/slurmledger/balance_enforcer.py --check
200+
```
201+
202+
The **Check Balances** button in the Admin Dashboard runs the same check interactively and displays results in the UI.
203+
187204
## Testing
188205

189206
```bash

manifest.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"version": "1.0.0",
3-
"name": "slurmcostmanager",
3+
"name": "slurmledger",
44
"summary": "Web UI for analyzing and managing Slurm accounting costs",
55
"description": "Cockpit plugin that lets HPC admins view consolidated Slurm charges, retrieve invoice PDFs, and drill into cost breakdowns (core-hours, GPU-hours) for historical analysis and auditing.",
66
"license": "MIT",
@@ -9,8 +9,8 @@
99
"cockpit": "142"
1010
},
1111
"menu": {
12-
"slurmcostmanager": {
13-
"path": "slurmcostmanager.html",
12+
"slurmledger": {
13+
"path": "slurmledger.html",
1414
"label": "SlurmLedger"
1515
}
1616
}

org.cockpit_project.slurmcostmanager.metainfo.xml

Lines changed: 0 additions & 10 deletions
This file was deleted.

org.cockpit_project.slurmcostmanager.metainfo.xml.in renamed to org.cockpit_project.slurmledger.metainfo.xml.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<component type="addon">
3-
<id>org.cockpit_project.slurmcostmanager</id>
3+
<id>org.cockpit_project.slurmledger</id>
44
<name>SlurmLedger</name>
55
<summary>Cockpit plugin for viewing and managing Slurm billing and cost recovery</summary>
66
<url type="homepage">https://github.com/NessieCanCode/SlurmCostManager</url>

0 commit comments

Comments
 (0)