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. |
||
|---|---|---|
| src/ispconfig | ||
| tests | ||
| tools | ||
| .gitignore | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
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
pip install -e . # for use
pip install -e .[dev] # with pytest / mypy / ruff
Python 3.10+.
Quick start
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.pydriven bytools/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
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
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
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
db = c.databases.get(42)
c.databases.user_update(client_id=5, primary_id=42, params={"database_password": "x"})
clients
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):
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:
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_callwill return a faultstring like "Method not found".
Footguns (captured here so nobody has to rediscover them)
sites_web_domain_update's second arg isclient_id, notprimary_id. For admin-owned sites passclient_id=0. Wrong order = permission denied or silent ownership change.SitesModule.web_domain_updatesignature enforces the correct order.sys_groupid=1is the admin group. On update calls, if you read a record withsys_groupid=1, passclient_id=0, not1, or ISPConfig re-owns the record to "admin's admin".enable_php/enable_letsencrypthandle this automatically.fastcgi_php_versionvsserver_php_id. Older installs use the string fieldfastcgi_php_version(e.g."PHP-8.2:/usr/bin/php-cgi8.2:/etc/php/8.2/cgi"), newer installs useserver_php_id(int FK toserver_php). Ourenable_phpwraps the new field; set it directly viaweb_domain_updateif 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.")raisesno_domain_found. Our wrapper strips a trailing dot for you so either call works. mail_user_getwith a filter dict returns inconsistent shapes. If the filter matches multiple rows you get an array; exactly-one match returns a bare map. Ourmail.user_get(filter_dict)always normalizes to a list.no_domain_found/no_client_foundfaults. These are typed asNotFoundError;client_get_by_username("nope")raises it. Older code may have caught rawFaultError— v0.2 reclassifies both.dns_a_addtype-column bug. On some ISPConfig versions (<= ~3.2.11)dns_a_addinserts thedns_rrrow without setting thetypecolumn, so BIND never emits the record.DnsModule.a_add(..., fix_type_bug=True)(default) issues a follow-updns_rr_updatewith{"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=1by default). Tune viaISPConfigClient(..., max_retries=3)for very long batch jobs. mail_forward_*, notmail_forwarding_*. The remote method is singular; we expose it asc.mail.forward_addand keepc.mail.forwarding_addas 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 intools/extract_inventory.pywith 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
pytest # unit tests only, no network (12 tests)
To run the live smoke test against a real panel:
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:
# 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
pip install -e .[dev]
ruff check src/
mypy src/ispconfig
pytest
License
MIT — see LICENSE.