Summary
Add a first integration test for openstack-project-manager that exercises
the real project-creation path against a live Keystone, not mocks. Keystone
is deployed locally on a kind cluster via
c5c3/forge (its ControlPlane Quick Start), and
the test then creates one project using the admin account and verifies it
through the OpenStack SDK.
This is deliberately a scaffold: one happy-path test. The goal is to stand
up the harness (kind + Keystone + clouds.yaml + a runnable assertion + CI
wiring) so that further integration tests can be added incrementally later.
Today the suite is unit-only (test/unit/, stdlib unittest, all OpenStack
clients mocked), run via the openstack-project-manager-tox Zuul job. Nothing
ever talks to a real Keystone, so regressions in the actual
openstack.connect() / keystone.projects.update() apply path can slip
through.
Reuse, don't reinvent
The kind/kubectl/Helm installation and the CI harness shape are copied from
the proven netbox-manager E2E work — do not author a novel installer:
What we reuse from that PR (almost verbatim, adapted for this repo):
- Ansible pre-run playbook that installs pinned, checksum-verified
kind/kubectl (+ Helm) and ensures Docker is running, using the
ensure-pip / ensure-pipenv / ensure-docker roles.
- The IPv6
accept_ra=2 sysctl fix in the pre-run playbook. This is the
non-obvious, hard-won bit: the OSISM Zuul nodes are IPv6-only and learn
their default route via SLAAC/RA. When kind makes Docker create a
dual-stack network it enables net.ipv6.conf.all.forwarding, after which the
kernel (default accept_ra=1) stops honouring RAs and the node drops off the
network mid-run (RESULT_UNREACHABLE). Setting accept_ra=2 keeps RAs
honoured while forwarding is on. Carry this over.
trap-based teardown in run.sh that always deletes the kind cluster on
exit (success or failure), but only tears down a cluster this run actually
created — a pre-existing debug cluster of the same name is reused and left in
place. Plus a cluster diagnostics dump on failure before teardown.
Makefile targets integration / integration-up / integration-down
(analogous to netbox's e2e / e2e-up / e2e-down).
- Zuul job placed in
periodic-daily (heavy, ~15 min) so the PR
check gate stays fast — mirroring netbox's pre-e2e.yml / test-e2e.yml
split and timeout: 2400.
Keystone via forge (only Keystone is needed)
Follow the forge ControlPlane Quick Start
(https://c5c3.github.io/forge/quick-start-controlplane.html) — only the
Keystone service is deployed:
# 1. toolchain (forge's own pinned installer → ~/.local/bin)
git clone https://github.com/c5c3/forge.git # pin a ref!
cd forge
make install-test-deps # pins kind v0.32.0, kubectl v1.36.1, jq, ...
export PATH="${HOME}/.local/bin:${PATH}"
# 2. infra + ControlPlane operator stack on a kind cluster (cluster name: "forge")
KIND_HOST_PORT=8443 WITH_CONTROLPLANE=true make deploy-infra
Then apply a Keystone-only ControlPlane CR (namespace openstack) and
wait for it:
apiVersion: c5c3.io/v1alpha1
kind: ControlPlane
metadata:
name: controlplane
namespace: openstack
spec:
openStackRelease: "2025.2"
services:
keystone:
replicas: 1
publicEndpoint: https://keystone.127-0-0-1.nip.io:8443/v3
gateway:
parentRef:
name: openstack-gw
hostname: keystone.127-0-0-1.nip.io
path: /
kubectl apply -f controlplane.yaml
kubectl wait controlplane/controlplane -n openstack --for=condition=Ready --timeout=15m
forge also supports WITH_CONTROLPLANE_CR=true to auto-apply a bundled CR,
but that bundle may include more than Keystone. Since only Keystone is
needed, we apply our own minimal CR (above), exactly as the Quick Start does.
Keystone access (no port-forward — the shared Envoy Gateway exposes it):
OS_AUTH_URL = https://keystone.127-0-0-1.nip.io:8443/v3 (nip.io → 127.0.0.1)
OS_USERNAME = admin, OS_PROJECT_NAME = admin
OS_USER_DOMAIN_NAME = Default, OS_PROJECT_DOMAIN_NAME = Default
- Admin password from the operator-projected Secret:
kubectl get secret controlplane-keystone-admin-credentials \
-n openstack -o jsonpath='{.data.password}' | base64 -d
- The endpoint uses a self-signed cert (cert-manager) →
verify: false
(a.k.a. --insecure).
How the test reaches the project-manager (clouds.yaml)
openstack-project-manager authenticates exclusively via clouds.yaml named
clouds; the create/manage commands default to --cloud admin. The harness
generates a clouds.yaml from the forge admin credentials:
clouds:
admin:
auth:
auth_url: https://keystone.127-0-0-1.nip.io:8443/v3
username: admin
password: "<from controlplane-keystone-admin-credentials>"
project_name: admin
user_domain_name: Default
project_domain_name: Default
identity_api_version: 3
verify: false # self-signed cert behind the Envoy Gateway
Point the run at it via OS_CLIENT_CONFIG_FILE (or place it on the standard
search path).
The single test
Action — create one project as admin, using the real CLI path:
tox -e create -- --cloud admin --domain default --name opm-integration-test \
--quota-class basic --nocreate-user
(tox -e create runs python openstack_project_manager/create.py, see
tox.ini. --nocreate-user keeps the scaffold minimal — no per-user
application credential.)
Verify — assert via the OpenStack SDK in a standalone
test/integration/verify.py (mirroring netbox's verify.py), e.g.:
import openstack
cloud = openstack.connect(cloud="admin")
project = cloud.identity.find_project("opm-integration-test", domain_id=<default-domain-id>)
assert project is not None
# optional spot-checks: the quota-class property and the per-project group exist
The script exits non-zero on any failed assertion. run.sh then optionally
deletes the project to keep reruns idempotent.
⚠️ Must not be collected by the fast unit gate. tox -e test runs
python -m unittest discover ./test, which recurses. The verifier must be a
standalone script (not named test_*.py) — exactly the rationale
netbox-manager used for keeping verify.py out of pytest tests/unit.
Proposed deliverables (file-by-file)
Mirroring netbox-manager#263, adapted to this repo's layout (test/ singular,
no Helm needed, Keystone via forge instead of a chart):
| File |
Purpose |
test/integration/deploy_keystone.sh |
Clone forge @ pinned ref, make install-test-deps, KIND_HOST_PORT=8443 WITH_CONTROLPLANE=true make deploy-infra, apply the Keystone-only ControlPlane CR, wait Ready, emit clouds.yaml. Safe to run standalone (make integration-up). |
test/integration/run.sh |
Orchestrate: deploy → generate clouds.yaml → tox -e create -- … → verify.py. trap-based teardown of a self-created cluster on every exit path + diagnostics dump on failure. |
test/integration/verify.py |
Standalone openstacksdk assertions (project exists, etc.). Not a unittest/pytest test. |
test/integration/controlplane.yaml |
The Keystone-only ControlPlane CR. |
test/integration/README.md |
Prerequisites (Docker, forge), make usage, env overrides, what is asserted. |
Makefile (new) |
integration (full cycle + teardown), integration-up (deploy + leave running), integration-down (kind delete cluster --name forge / forge make teardown-infra). |
playbooks/pre-integration.yml |
Reuse netbox#263 pre-e2e.yml: ensure-pip/ensure-pipenv/ensure-docker roles, the IPv6 accept_ra=2 sysctl fix, ensure Docker running. (Tool binaries via forge's make install-test-deps, invoked from deploy_keystone.sh.) |
playbooks/test-integration.yml |
Install deps into the venv, run make integration. |
.zuul.yaml |
New job openstack-project-manager-integration (nodeset: ubuntu-noble, pre-run: playbooks/pre-integration.yml, run: playbooks/test-integration.yml, timeout: 2400); add to periodic-daily only (keep check fast). |
Open decisions (please confirm during implementation)
- Toolchain install source. Recommended: reuse netbox#263's node-prep
(Docker + IPv6 fix + pip) and let forge's own make install-test-deps
install the pinned kind/kubectl/jq (authoritative for forge, avoids
version drift; forge pins kind v0.32.0, the same version netbox#263 pins).
Alternative: port netbox#263's pinned get_url download tasks and align
versions with forge. Either way we are not writing a novel installer.
- forge pinning. Pin a specific forge git ref/tag in
deploy_keystone.sh
for reproducibility (forge is a fast-moving prototyping repo).
- Project-manager invocation flags. Confirm the minimal
create flags work
against a bare forge Keystone — create.py also creates a <domain>-admin
group, a per-project group, assigns default roles, and sets quota-class
properties on the project. Verify the default Keystone ships the expected
roles (admin/member/reader) and Default domain; adjust flags
(--nocreate-user, --quota-class basic) if any step is unsupported.
- CI placement. Recommend
periodic-daily only (heavy job). If a PR-gate
signal is wanted, it can additionally be added to check.
- Resources. forge's
kind-config.yaml targets a single node ~7 GB RAM /
2 vCPU; confirm the ubuntu-noble nodeset matches what netbox#263 uses.
Acceptance criteria
References
Summary
Add a first integration test for
openstack-project-managerthat exercisesthe real project-creation path against a live Keystone, not mocks. Keystone
is deployed locally on a kind cluster via
c5c3/forge (its ControlPlane Quick Start), and
the test then creates one project using the admin account and verifies it
through the OpenStack SDK.
This is deliberately a scaffold: one happy-path test. The goal is to stand
up the harness (kind + Keystone + clouds.yaml + a runnable assertion + CI
wiring) so that further integration tests can be added incrementally later.
Today the suite is unit-only (
test/unit/, stdlibunittest, all OpenStackclients mocked), run via the
openstack-project-manager-toxZuul job. Nothingever talks to a real Keystone, so regressions in the actual
openstack.connect()/keystone.projects.update()apply path can slipthrough.
Reuse, don't reinvent
The kind/kubectl/Helm installation and the CI harness shape are copied from
the proven netbox-manager E2E work — do not author a novel installer:
NetBox on kind (Helm) + apply
example/+ verify via API".What we reuse from that PR (almost verbatim, adapted for this repo):
kind/kubectl(+ Helm) and ensures Docker is running, using theensure-pip/ensure-pipenv/ensure-dockerroles.accept_ra=2sysctl fix in the pre-run playbook. This is thenon-obvious, hard-won bit: the OSISM Zuul nodes are IPv6-only and learn
their default route via SLAAC/RA. When
kindmakes Docker create adual-stack network it enables
net.ipv6.conf.all.forwarding, after which thekernel (default
accept_ra=1) stops honouring RAs and the node drops off thenetwork mid-run (
RESULT_UNREACHABLE). Settingaccept_ra=2keeps RAshonoured while forwarding is on. Carry this over.
trap-based teardown inrun.shthat always deletes the kind cluster onexit (success or failure), but only tears down a cluster this run actually
created — a pre-existing debug cluster of the same name is reused and left in
place. Plus a cluster diagnostics dump on failure before teardown.
Makefiletargetsintegration/integration-up/integration-down(analogous to netbox's
e2e/e2e-up/e2e-down).periodic-daily(heavy, ~15 min) so the PRcheckgate stays fast — mirroring netbox'spre-e2e.yml/test-e2e.ymlsplit and
timeout: 2400.Keystone via forge (only Keystone is needed)
Follow the forge ControlPlane Quick Start
(https://c5c3.github.io/forge/quick-start-controlplane.html) — only the
Keystone service is deployed:
Then apply a Keystone-only
ControlPlaneCR (namespaceopenstack) andwait for it:
kubectl apply -f controlplane.yaml kubectl wait controlplane/controlplane -n openstack --for=condition=Ready --timeout=15mKeystone access (no port-forward — the shared Envoy Gateway exposes it):
OS_AUTH_URL = https://keystone.127-0-0-1.nip.io:8443/v3(nip.io→ 127.0.0.1)OS_USERNAME = admin,OS_PROJECT_NAME = adminOS_USER_DOMAIN_NAME = Default,OS_PROJECT_DOMAIN_NAME = Defaultverify: false(a.k.a.
--insecure).How the test reaches the project-manager (clouds.yaml)
openstack-project-managerauthenticates exclusively via clouds.yaml namedclouds; the create/manage commands default to
--cloud admin. The harnessgenerates a clouds.yaml from the forge admin credentials:
Point the run at it via
OS_CLIENT_CONFIG_FILE(or place it on the standardsearch path).
The single test
Action — create one project as admin, using the real CLI path:
tox -e create -- --cloud admin --domain default --name opm-integration-test \ --quota-class basic --nocreate-user(
tox -e createrunspython openstack_project_manager/create.py, seetox.ini.--nocreate-userkeeps the scaffold minimal — no per-userapplication credential.)
Verify — assert via the OpenStack SDK in a standalone
test/integration/verify.py(mirroring netbox'sverify.py), e.g.:The script exits non-zero on any failed assertion.
run.shthen optionallydeletes the project to keep reruns idempotent.
Proposed deliverables (file-by-file)
Mirroring netbox-manager#263, adapted to this repo's layout (
test/singular,no Helm needed, Keystone via forge instead of a chart):
test/integration/deploy_keystone.shmake install-test-deps,KIND_HOST_PORT=8443 WITH_CONTROLPLANE=true make deploy-infra, apply the Keystone-only ControlPlane CR, wait Ready, emit clouds.yaml. Safe to run standalone (make integration-up).test/integration/run.shtox -e create -- …→verify.py.trap-based teardown of a self-created cluster on every exit path + diagnostics dump on failure.test/integration/verify.pyunittest/pytesttest.test/integration/controlplane.yamltest/integration/README.mdmakeusage, env overrides, what is asserted.Makefile(new)integration(full cycle + teardown),integration-up(deploy + leave running),integration-down(kind delete cluster --name forge/ forgemake teardown-infra).playbooks/pre-integration.ymlpre-e2e.yml:ensure-pip/ensure-pipenv/ensure-dockerroles, the IPv6accept_ra=2sysctl fix, ensure Docker running. (Tool binaries via forge'smake install-test-deps, invoked fromdeploy_keystone.sh.)playbooks/test-integration.ymlmake integration..zuul.yamlopenstack-project-manager-integration(nodeset: ubuntu-noble,pre-run: playbooks/pre-integration.yml,run: playbooks/test-integration.yml,timeout: 2400); add toperiodic-dailyonly (keepcheckfast).Open decisions (please confirm during implementation)
(Docker + IPv6 fix + pip) and let forge's own
make install-test-depsinstall the pinned
kind/kubectl/jq(authoritative for forge, avoidsversion drift; forge pins kind
v0.32.0, the same version netbox#263 pins).Alternative: port netbox#263's pinned
get_urldownload tasks and alignversions with forge. Either way we are not writing a novel installer.
deploy_keystone.shfor reproducibility (forge is a fast-moving prototyping repo).
createflags workagainst a bare forge Keystone —
create.pyalso creates a<domain>-admingroup, a per-project group, assigns default roles, and sets quota-class
properties on the project. Verify the default Keystone ships the expected
roles (
admin/member/reader) andDefaultdomain; adjust flags(
--nocreate-user,--quota-class basic) if any step is unsupported.periodic-dailyonly (heavy job). If a PR-gatesignal is wanted, it can additionally be added to
check.kind-config.yamltargets a single node ~7 GB RAM /2 vCPU; confirm the
ubuntu-noblenodeset matches what netbox#263 uses.Acceptance criteria
make integrationon a Docker-capable host: provisions kind + Keystonevia forge, creates one project as admin, asserts it via the SDK, and tears
the cluster down on every exit path (incl. failure).
make integration-up/make integration-downwork for local debugging.openstack-project-manager-integrationZuul job runs inperiodic-daily; thecheckgate is unchanged and stays fast.tox -e test.test/integration/README.mddocuments prerequisites and usage.References
hack/install-test-deps.sh,hack/deploy-infra.sh,hack/kind-config.yaml)