Repository URLs, version strings, and example creds normalized for the public git.sulkta.com endpoint. No code-behavior change. Audit-applied by the public-flip rolling-audit pass — see kayos/openclaw-workspace memory/2026-05-27 logs for the campaign context.
306 lines
11 KiB
Markdown
306 lines
11 KiB
Markdown
# ispconfig-py
|
|
|
|
Python SDK for the ISPConfig remote SOAP API. Wraps the panel's SOAP surface
|
|
so you stop writing throw-away PHP scripts every time you need to touch a
|
|
site, zone, or mailbox.
|
|
|
|
v0.2 covers the full remote API — every method exposed by ISPConfig's
|
|
`remote/index.php`, 312 of them as of ISPConfig 3.2.11. The hand-audited
|
|
helpers (stable names, param-order fixes, convenience wrappers) sit on top
|
|
of auto-generated wrappers that mirror the PHP surface 1:1.
|
|
|
|
## Why no zeep?
|
|
|
|
ISPConfig's `/remote/index.php` uses PHP's `SoapServer` in non-WSDL mode and
|
|
refuses to generate a WSDL (`?wsdl` returns a fault). zeep requires WSDL. So
|
|
we hand-roll SOAP envelopes with the stdlib (`urllib` + `xml.etree`). Zero
|
|
runtime dependencies — fewer moving parts, nothing to pin.
|
|
|
|
## Install
|
|
|
|
```bash
|
|
pip install -e . # for use
|
|
pip install -e .[dev] # with pytest / mypy / ruff
|
|
```
|
|
|
|
Python 3.10+.
|
|
|
|
## Quick start
|
|
|
|
```python
|
|
from ispconfig import ISPConfigClient
|
|
|
|
with ISPConfigClient(
|
|
"https://panel.example.com:8080/remote/index.php",
|
|
username="admin",
|
|
password="...",
|
|
) as c:
|
|
site = c.sites.web_domain_get(156)
|
|
print(site["domain"], site["php"])
|
|
|
|
# Flip on Let's Encrypt in one call.
|
|
c.sites.enable_letsencrypt(156)
|
|
```
|
|
|
|
The `with` block handles login on enter and logout on exit. Session IDs are
|
|
managed internally — callers don't touch them.
|
|
|
|
Set `verify_ssl=False` for dev boxes with self-signed certs. Default is `True`.
|
|
|
|
## Changelog
|
|
|
|
### 0.2.0 — 2026-04-22
|
|
|
|
Full-coverage extension. Every remote method ISPConfig exposes is now wrapped
|
|
in one of the submodules below. Hand-audited helpers from v0.1 (footgun
|
|
fixes, convenience wrappers) are preserved beneath a delimiter and survive
|
|
regeneration. New submodules: `admin`, `aps`, `backups`, `cron`, `domains`,
|
|
`ftp`, `misc`, `monitor`, `openvz`, `server`, `shell`, `webdav`.
|
|
|
|
- Code generator at `tools/gen_methods.py` driven by `tools/method_inventory.json`.
|
|
- One-command refresh for future ISPConfig upgrades (see below).
|
|
- `ISPConfigClient.raw_call(method, *args)` escape hatch for methods not yet
|
|
in the inventory.
|
|
- `ISPConfigClient.list_functions()` introspects the panel's exposed method list.
|
|
- Live smoke test extended from 4 to 21 read-only calls covering every module.
|
|
|
|
### 0.1.0 — 2026-04-21
|
|
|
|
Initial release. Hand-audited coverage of sites / dns / mail / databases / clients.
|
|
|
|
## Modules
|
|
|
|
### Hand-audited (stable API, verified in prod)
|
|
|
|
Names normalized for ergonomics; footgun fixes and retries baked in. These
|
|
are the wrappers you should prefer.
|
|
|
|
#### `sites`
|
|
|
|
```python
|
|
c.sites.web_domain_get(156)
|
|
c.sites.web_domain_update(client_id=0, primary_id=156, params={"php": "fast-cgi"})
|
|
c.sites.enable_php(156, mode="php-fpm", server_php_id=2, pm="ondemand")
|
|
c.sites.enable_letsencrypt(156)
|
|
```
|
|
|
|
#### `dns`
|
|
|
|
```python
|
|
zone_id = c.dns.zone_get_id("example.com") # trailing dot is stripped either way
|
|
zone = c.dns.zone_get(zone_id)
|
|
records = c.dns.rr_get_all_by_zone(zone_id)
|
|
rr_id = c.dns.a_add(0, {"zone": zone_id, "name": "www", "data": "1.2.3.4", "ttl": 3600, "active": "Y"})
|
|
```
|
|
|
|
#### `mail`
|
|
|
|
```python
|
|
md = c.mail.domain_get_by_domain("example.com")
|
|
users = c.mail.user_get({"email": "%@example.com"}) # always a list
|
|
new_id = c.mail.create_mailbox(client_id=5, domain="example.com",
|
|
local_part="info", password="x", quota_mb=2048)
|
|
```
|
|
|
|
#### `databases`
|
|
|
|
```python
|
|
db = c.databases.get(42)
|
|
c.databases.user_update(client_id=5, primary_id=42, params={"database_password": "x"})
|
|
```
|
|
|
|
#### `clients`
|
|
|
|
```python
|
|
cli = c.clients.get_by_username("alice")
|
|
groupid = c.clients.get_groupid(cli["client_id"])
|
|
ids = c.clients.get_all()
|
|
```
|
|
|
|
### Auto-generated (full surface, v0.2)
|
|
|
|
Wrappers mirror the PHP method names 1:1. Param shapes come from PHPDoc where
|
|
available and default to `Any` otherwise. Verified to wire up against
|
|
ISPConfig 3.2.11 but not exercised in every code path — file issues if you
|
|
hit one.
|
|
|
|
| Module | Class | Methods |
|
|
|--------------|----------------------|---------|
|
|
| `admin` | `AdminModule` | 10 |
|
|
| `aps` | `ApsModule` | 10 |
|
|
| `backups` | `BackupsModule` | 2 |
|
|
| `clients` | `ClientsModule` | 19 |
|
|
| `cron` | `CronModule` | 4 |
|
|
| `databases` | `DatabasesModule` | 9 |
|
|
| `dns` | `DnsModule` | 87 |
|
|
| `domains` | `DomainsModule` | 5 |
|
|
| `ftp` | `FtpModule` | 5 |
|
|
| `mail` | `MailModule` | 82 |
|
|
| `misc` | `MiscModule` | 4 |
|
|
| `monitor` | `MonitorModule` | 1 |
|
|
| `openvz` | `OpenvzModule` | 22 |
|
|
| `server` | `ServerModule` | 12 |
|
|
| `shell` | `ShellModule` | 4 |
|
|
| `sites` | `SitesModule` | 29 |
|
|
| `webdav` | `WebdavModule` | 4 |
|
|
|
|
Every auto-generated method carries a docstring with the original PHP
|
|
filename + line number and an `AUTO-GENERATED — param shapes may need
|
|
verification` warning. Methods that exist in both the auto and hand-audited
|
|
blocks are skipped in the auto block — the hand version wins.
|
|
|
|
### Escape hatch
|
|
|
|
For methods not yet in the inventory (e.g. on a newer ISPConfig version than
|
|
we've regenerated against):
|
|
|
|
```python
|
|
result = c.raw_call("some_new_method", arg1, arg2)
|
|
```
|
|
|
|
`raw_call` routes through the same session-management + fault-mapping
|
|
pipeline as typed methods, so auth/retry still works. To see what the panel
|
|
exposes:
|
|
|
|
```python
|
|
funcs = c.list_functions()
|
|
```
|
|
|
|
### Not covered
|
|
|
|
- `__construct` — PHP constructor, not a real API method.
|
|
- Anything gated by ISPConfig plugins we don't have installed — probing via
|
|
`raw_call` will return a faultstring like "Method not found".
|
|
|
|
## Footguns (captured here so nobody has to rediscover them)
|
|
|
|
- **`sites_web_domain_update`'s second arg is `client_id`, not `primary_id`.**
|
|
For admin-owned sites pass `client_id=0`. Wrong order = permission denied
|
|
or silent ownership change. `SitesModule.web_domain_update` signature
|
|
enforces the correct order.
|
|
- **`sys_groupid=1` is the admin group.** On update calls, if you read a
|
|
record with `sys_groupid=1`, pass `client_id=0`, not `1`, or ISPConfig
|
|
re-owns the record to "admin's admin". `enable_php` / `enable_letsencrypt`
|
|
handle this automatically.
|
|
- **`fastcgi_php_version` vs `server_php_id`.** Older installs use the string
|
|
field `fastcgi_php_version` (e.g. `"PHP-8.2:/usr/bin/php-cgi8.2:/etc/php/8.2/cgi"`),
|
|
newer installs use `server_php_id` (int FK to `server_php`). Our
|
|
`enable_php` wraps the new field; set it directly via `web_domain_update`
|
|
if you need the old one.
|
|
- **BIND trailing dot on zone origins — BUT in reverse.** Contrary to a lot
|
|
of older documentation and PHP snippets floating around, ISPConfig 3.2.11+
|
|
wants the origin **without** a trailing dot: `dns_zone_get_id("example.com")`
|
|
works, `dns_zone_get_id("example.com.")` raises `no_domain_found`. Our
|
|
wrapper strips a trailing dot for you so either call works.
|
|
- **`mail_user_get` with a filter dict returns inconsistent shapes.** If the
|
|
filter matches multiple rows you get an array; exactly-one match returns a
|
|
bare map. Our `mail.user_get(filter_dict)` always normalizes to a list.
|
|
- **`no_domain_found` / `no_client_found` faults.** These are typed as
|
|
`NotFoundError`; `client_get_by_username("nope")` raises it. Older code
|
|
may have caught raw `FaultError` — v0.2 reclassifies both.
|
|
- **`dns_a_add` type-column bug.** On some ISPConfig versions (<= ~3.2.11)
|
|
`dns_a_add` inserts the `dns_rr` row without setting the `type` column,
|
|
so BIND never emits the record. `DnsModule.a_add(..., fix_type_bug=True)`
|
|
(default) issues a follow-up `dns_rr_update` with `{"type": "A"}` — no-op
|
|
on fixed versions, required on the broken ones.
|
|
- **Session timeouts.** ISPConfig sessions expire mid-long-operation without
|
|
warning. The client detects the "session not valid" fault, re-authenticates,
|
|
and retries once (`max_retries=1` by default). Tune via
|
|
`ISPConfigClient(..., max_retries=3)` for very long batch jobs.
|
|
- **`mail_forward_*`, not `mail_forwarding_*`.** The remote method is
|
|
singular; we expose it as `c.mail.forward_add` and keep
|
|
`c.mail.forwarding_add` as an alias for PHP-snippet refugees.
|
|
- **Filter dicts on `mail_user_get`.** Pass an int to get one row; pass a
|
|
dict like `{"email": "%@example.com"}` to get a list. The SOAP method is
|
|
overloaded and untyped on the wire.
|
|
- **PHP method signatures with `array()` defaults.** ISPConfig's extract
|
|
regex used to stop at the first `)` in `$params = array()`, missing three
|
|
methods on 3.2.x (`sites_aps_available_packages_list`,
|
|
`sites_aps_instance_delete`, `openvz_vm_add_from_template`). Fixed in
|
|
`tools/extract_inventory.py` with balanced-paren matching — worth
|
|
double-checking on future panel upgrades.
|
|
- **Known admin-only methods.** Reseller logins fault with "permission denied"
|
|
on a non-admin user. These are skipped in the smoke tests:
|
|
`monitor_jobqueue_count`, `sites_cron_get`, `sites_ftp_user_get`,
|
|
`openvz_*`, `quota_get_by_user`, `client_templates_get_all`. Use an admin
|
|
login if you need them.
|
|
|
|
## Errors
|
|
|
|
Typed hierarchy — never `zeep.exceptions.Fault` (we don't use zeep), never a
|
|
raw SOAP fault leaked to callers:
|
|
|
|
```
|
|
ISPConfigError
|
|
├── AuthError # login failed OR session expired
|
|
├── PermissionError # caller lacks rights on the record
|
|
├── NotFoundError # primary_id didn't match anything
|
|
└── FaultError # everything else (.faultcode, .faultstring)
|
|
```
|
|
|
|
## Tests
|
|
|
|
```bash
|
|
pytest # unit tests only, no network (12 tests)
|
|
```
|
|
|
|
To run the live smoke test against a real panel:
|
|
|
|
```bash
|
|
export ISPCONFIG_TEST_URL="https://panel.example.com:8080/remote/index.php"
|
|
export ISPCONFIG_TEST_USER="admin"
|
|
export ISPCONFIG_TEST_PASS="..."
|
|
export ISPCONFIG_TEST_VERIFY_SSL=0 # for self-signed certs
|
|
pytest tests/test_smoke.py
|
|
```
|
|
|
|
21 read-only calls covering every auto-generated module + the originals. No
|
|
`_add` / `_update` / `_delete` calls — safe against production. Methods that
|
|
require admin privileges skip gracefully with a documented reason.
|
|
|
|
## Regenerating for newer ISPConfig versions
|
|
|
|
When ISPConfig ships a new version, resync against its PHP sources:
|
|
|
|
```bash
|
|
# 1. Pull fresh PHP sources from the panel host (sudo required):
|
|
mkdir -p /tmp/ispconfig-php-src
|
|
ssh PANEL_HOST "sudo tar -cz -C /usr/local/ispconfig/interface/lib/classes/remote.d ." \
|
|
| tar -xz -C /tmp/ispconfig-php-src/
|
|
ssh PANEL_HOST "sudo cat /usr/local/ispconfig/interface/lib/classes/remoting.inc.php" \
|
|
> /tmp/ispconfig-php-src/remoting.inc.php
|
|
|
|
# 2. Re-extract the method inventory:
|
|
python3 tools/extract_inventory.py /tmp/ispconfig-php-src tools/method_inventory.json
|
|
|
|
# 3. Regenerate wrappers:
|
|
python3 tools/gen_methods.py
|
|
|
|
# 4. Clean up formatting + sanity check:
|
|
ruff format src/ tools/
|
|
ruff check src/ tools/
|
|
mypy src/ispconfig
|
|
pytest
|
|
|
|
# 5. Review and commit:
|
|
git diff --stat
|
|
git diff tools/method_inventory.json # new methods jump out here
|
|
```
|
|
|
|
Hand-edits below the `HAND-EDIT ONLY BELOW` marker in each module survive
|
|
regeneration. Method-name collisions between auto and hand are resolved in
|
|
favor of the hand version; the generator emits a `# skipped ...` comment
|
|
in the auto block for traceability.
|
|
|
|
## Development
|
|
|
|
```bash
|
|
pip install -e .[dev]
|
|
ruff check src/
|
|
mypy src/ispconfig
|
|
pytest
|
|
```
|
|
|
|
## License
|
|
|
|
MIT — see `LICENSE`.
|