zPodFactory Core Engine
Complete the following steps to set up your development environment:
-
Install Docker and Docker Compose
-
Install uv:
curl -LsSf https://astral.sh/uv/install.sh | shuv will manage the Python toolchain for you — no separate pyenv step is required. Each subproject pins its own
requires-python, anduv syncwill download the matching interpreter automatically. -
Create each subproject virtualenv. In
/zpodapi,/zpodengine,/zpodcli, and/zpodsdk, run:uv sync
Each subproject is released independently and keeps its own
uv.lock. -
Configure Environment Variables. (See
/zpodapi/src/zpodapi/lib/settings.pyfile for all available settings) In the root directory, run:cp .env.default .env vim .env
-
For Visual Studios Code, do the following:
a. Configure the zpodcore.code-workspace. In
/run:cp zpodcore.code-workspace.default zpodcore.code-workspace
Make sure that the port variable in launch.configurations.connect.port matches the port stored in the
ZPODAPI_DEBUG_HOSTPORTenvironment variable. -
Build the Docker containers. In the root directory, run:
docker compose build
-
Start the environment. In the root directory, run:
just zpodcore-start
-
Verify that zpodapi is working by opening a browser and going to
http://localhost:[8000 or ZPODAPI_HOSTPORT]andhttp://localhost:[8000 or ZPODAPI_HOSTPORT]/docs -
Create Deployments
just zpodengine-deploy-all
The monorepo is split into independently-released subprojects, each with its own
pyproject.toml and uv.lock:
| Subproject | Role | Python | Released |
|---|---|---|---|
zpodsdk/ |
Generated API client | >=3.14 |
PyPI, from the tag |
zpodcli/ |
zcli Typer CLI (requires zpodsdk~=<version>) |
>=3.14 |
PyPI, from the tag |
zpodapi/ |
FastAPI service | 3.14 (image python:3.14-slim-trixie) |
Docker, built from the checkout |
zpodengine/ |
Prefect flows | 3.14 (image prefecthq/prefect:3.x-python3.14) |
Docker, built from the checkout |
zpodsdk_builder/ |
Regenerates zpodsdk from the OpenAPI document |
3.14 |
Docker (internal tool) |
zpodcommon/ |
Shared source tree — no pyproject.toml, consumed via PYTHONPATH |
n/a | — |
There is no uv workspace: each subproject locks on its own. The root pyproject.toml
only holds ruff config.
One version for four packages. A tag vX.Y.Z moves the zPod API, the zPod Engine, zcli
and zpodsdk together (nine version markers: four __init__.py, five pyproject.toml),
and the tag publishes zpodsdk then zpodcli to PyPI. The API decides the digit: minor
when the API changes shape (an endpoint, a schema, a permission), patch otherwise.
zpodsdk always carries the version of the API it was generated from. zcli X.Y.* is
built for API X.Y.* and requires zpodsdk~=X.Y.Z, so a zcli-only fix can ship on its
own later without an SDK upload; lockstep is the default.
| You run | Use | Why |
|---|---|---|
| zPod API 0.8.x | zcli 0.8.x, zpodsdk 0.8.x | same endpoints and schemas; patch digits may differ |
| zPod API 0.7.2 | zcli 0.7.2, zpodsdk 0.7.2 | the API rejects any other major.minor |
| several factories on different minors | one zcli per minor (uv tool install zpodcli==X.Y.Z into separate tool names, or a venv each) |
zcli sends its version with every request |
The API enforces this: every request carries the CLI's version in a version header, and
major.minor must equal the API's own or the request is refused with 417 and the message
names the version to install (zpodcli version 0.7.2 is required. Current zpodcli version is 0.8.0). Patch digits are not compared, which is what lets a zcli-only patch ship.
Cutting a release is one command, just zpod-release X.Y.Z, which runs
python3 tools/release.py X.Y.Z --push: the [Unreleased] section of CHANGELOG.md
becomes [X.Y.Z], the nine markers and the zpodsdk~= requirement move, the lockfiles
are refreshed, the four test suites run, both wheels build, then one commit, the tag,
the push. GitHub publishes the changelog section as the release note and both packages to
PyPI. tools/README.md has the details, what stops a release, and the PyPI setup.
just zpod-release-check runs the between-releases rules locally; CI runs them on every
push.
0.8.0 is the first release after two years. The upgrade is automatic where it can be; these steps are not:
- Back up the database. The one Alembic migration drops the
zpod_featurestable (features now live as JSON on the zPod), and the Postgres image moved from bullseye to trixie, so the entrypoint reindexes every database on first start. Both run on their own; neither is reversible. - Host toolchain: uv replaces pyenv and Poetry.
just zclineeds uv; a localuv syncofzpodapiorzpodengineneedsgccandlibpq-devforpsycopg2. - Prefect 2 → 3: stop the stack, remove the Prefect volume, start, then
just zpodengine-deploy-all. Flow-run history is lost; the deployments are re-registered. - zbox → zcore: after
git pull,just zcore-transition(needsjqandcurl). It stops the stack, rebuilds the images with--no-cache, starts it in the background, waits for zpodapi and the Prefect server, runsjust zpodengine-deploy-all, and only then resyncs the library, enableszcore-13.7, waits for it to beACTIVEand rewrites every profile whose first component iszbox-*; on a stack that was not rebuilt first the last part breaks.SKIP_REBUILD=1 just zcore-transitionreruns it on a stack already rebuilt. Existing zPods are untouched. - Broadcom downloads: set the
zpodfactory_broadcom_download_tokensetting; library entries still using the removedcustomerconnectoravipulseengines end inFAILED_UNSUPPORTED_ENGINEuntil their URL is changed. - Optional: set
compute.vdson endpoints whose NSX transport zone spans several VDSs; enableff_nsx_clean_orphan_portsonly after reading one destroy log with it off; enableff_endpoint_ova_stagingper site. - No
.envchange:.env.defaultis identical to 0.7.2; new behaviour is driven by settings seeded at API start.
just zpod-update X.Y.Z fetches the tag, rebuilds and restarts the stack, waits for the
API and redeploys the flows; it prints which versions it crosses and points here.
PyPI users: uv tool install zpodcli==X.Y.Z (uv provisions Python 3.14).
Besides the four networks a deploy creates (the management /26 on the native VLAN and the
routed /26s on VLANs 64, 128 and 192), a zPod owner can add VLAN networks on the zPod's
zcore: zcli zpod network add <zpod> --name LOCALNET111 --vlan 111 --gateway 10.42.111.1/24.
The VLAN id is the user's choice outside the ids zboxapi reserves on zcore (10, 20, 30 and
64, 128, 192), the subnet too as long as it overlaps nothing of the zPod or the endpoint's pool. By default the network is routing: LOCAL, reachable inside the zPod only;
--masquerade (or zcli zpod network masquerade on) gives it egress through source
translation on zcore, which needs zboxapi 0.2.0 on the zcore. A superadmin can make it
GLOBAL, advertised to the NSX fabric through a T1 static route, with
zcli zpod network routing <zpod> <name> global [--dry-run]; that runs the audited
zpod_network_routing flow in zpodengine, whose checks and evidence are in the Prefect run.
The command follows the run and prints its verdict: the failing check when a guardrail refuses
the subnet (it already exists or overlaps on the fabric), and the network goes back to LOCAL.
zcli zpod network list <zpod> shows every network with its live status from zcore, zcore's own
VLANs 10, 20 and 30 included (kind: zcore, read-only).
Appliance compatibility: everything except masquerade works on the deployed zcore 13.5
appliances (zboxapi 0.1.1 already has /vlan), the routing flow included. Masquerade needs
zboxapi 0.2.0 (zcore 13.7); on an older zcore the API answers 501 up front, add --masquerade
creates nothing, and the row never claims a translation zcore did not set up.
A zPod owner manages the storage zcore serves, in three levels that mirror zboxapi:
- Disks are virtual disks of the zcore VM:
zcli zpod disk add <zpod> --size 500adds a thin 500 GiB disk through a zpodengine flow (which the command follows),disk grow <zpod> /dev/sdc --size 1000grows one (the filer disk/dev/sdbincluded),disk removedeletes a blank or foreign disk with its data. A disk is matched to its device in zcore by serial, its vSphere UUID (disk.EnableUUID), so device names that shift after a reboot change nothing. Each change first checks that the zcore VM's datastore keepszpodfactory_datastore_min_free_pctfree (default 15); a disk is at mostzpodfactory_zpod_disk_max_gb(default 2048 GiB, superadmin may exceed it). - Storages are filesystems zcore mounts under
/FILER:zcli zpod storage create <zpod> --disk /dev/sdc [--lvm]formats and mounts a blank disk asSTORAGE02,storage growuses the space a disk grow made, online,storage deleteunmounts it and keeps the data (storage adoptmounts it again),storage folder add|set|removemanages its folders.STORAGE01, built by zcore on the filer disk, only grows. - NFS exports share a folder:
zcli zpod nfs add <zpod> STORAGE02 DATASTORE01exports it to*(any host that reaches zcore) unless--clientsnarrows it;nfs client add|remove,nfs set,nfs remove, andnfs statusshows who has each export mounted. NFS-01, the nested ESXi datastore, and zcore's own/etc/exportslines are never changed.
zcli zpod storage add <zpod> --size 500, storage grow <name> --size N and storage remove <name> do the disk step and the storage step in one command; zcli zpod storage list shows
each storage with its folders and their exports. Sizes are GiB everywhere. Data is destroyed
only by storage folder remove --force, disk remove and storage remove, each confirmed.
Appliance compatibility: all of it needs zboxapi 0.2.1 (zcore 13.7), read from zboxapi on every call; an older zcore answers 501 and nothing is changed.
just zpod-runtests runs the four suites with a summary; or, per subproject,
uv run pytest in zpodapi, zpodcli, zpodengine (which also picks up
zpodcommon/tests/) and zpodsdk. Everything is light by design: zpodapi exercises the
FastAPI app with TestClient against in-memory SQLite (users CRUD, a smoke pass over every
collection route, FQDN validation); zpodcli checks --help and --version; zpodengine
is import smoke for the libraries and flows plus the NSX orphan-port classification;
zpodcommon covers enums and MgmtIp; zpodsdk instantiates the client and a model.
Nothing hits Postgres, Prefect, vCenter or NSX. CI runs the same four commands on every push.
The monorepo moved from Poetry to uv in a single branch. A few things worth knowing if you are resuming a branch older than the migration or troubleshooting an unexpected pin:
- Local
uv syncforzpodapi/zpodenginerequires bothgccandlibpq-devon the host because those projects pinpsycopg2(source-only). psycopg2'ssetup.pyshells out toccand includes libpq headers. On Debian/Ubuntu:sudo apt install gcc libpq-dev. If you only want the lockfile without building, runuv lockinstead ofuv sync— resolution alone doesn't compile anything. Full installs still happen inside the Docker builder stages, which install bothlibpq-devandgccbeforeuv sync. zpodsdk_builderrunsopenapi-python-client0.29 (the earlierclick<8.2workaround for 0.20 is gone).just zpodsdk-updateregenerateszpodsdk/src/zpodsdk/from the running API;zpodsdk/README.mdandzpodsdk/pyproject.tomlare hand-written.- Dev path dep on
zpodsdkis inzpodcli/pyproject.tomlunder[tool.uv.sources], not in[project.dependencies], which carries thezpodsdk~=<version>requirement the wheel ships. Do not re-add apath=entry to the main dependency list — it would be embedded in the published wheel. - Build backend is
hatchlingfor the two publishable projects (zpodsdk,zpodcli).zpodapi,zpodengine, andzpodsdk_builderare all[tool.uv] package = false— they have no build target because their source is either bind-mounted in dev orCOPY'd into the Docker image in prod. - Dockerfiles no longer set
POETRY_VENV/POETRY_VERSION. They now pulluvfromghcr.io/astral-sh/uv:0.12and write to/opt/venv(kept at that path viaUV_PROJECT_ENVIRONMENT=/opt/venvfor operator familiarity). uv.lockreplacespoetry.lock. Commit it. It is cross-platform and deterministic. If you see apoetry.lockreappear in a PR, it is stale.