Enrollment
Reference for abaca/enrollment.
The enrollment subsystem registers an S3 bucket as a usable backup target by running a sequence of automated checks and then initializing a Kopia repository inside the bucket. Use enrollment whenever you add a new BackupTarget to a Domain: Abacá runs preflight conformance checks, generates a cryptographically-random repository password stored exclusively in Barbican, and calls kopia repository create (or connects to an existing repository when adopting). The result is a BackupTarget in active status, ready to accept backup jobs.
Enrollment is modelled as a standard Abacá target_enroll job and passes through the shared job state machine. In production the abaca-api casts the job to abaca-conductor via oslo.messaging; the conductor drives the state machine and a worker VM executes the Kopia operations.
The parameters below apply to execute_enroll_job (the internal orchestrator) and to the fields that must be populated on a BackupTarget before enrollment can proceed. When you trigger enrollment through the API or CLI, these fields are drawn from the target record and the associated Barbican secrets.
| Parameter | Type | Required | Description |
|---|---|---|---|
job | Job ORM object | Yes | The target_enroll job row created by the API. Enrollment mutates job.state, job.progress, job.started_at, job.ended_at, and error fields. |
target | TargetBucket ORM object | Yes | The bucket being enrolled. Must have kopia_key_trust_id, s3_access_key_ref, and s3_secret_key_ref populated before the job runs; enrollment mutates target.status, target.preflight_detail, target.preflight_at, target.capabilities, and target.kopia_key_ref. |
executor | Kopia executor instance | Yes | The executor used to run Kopia commands. In production this is an HttpWorkerExecutor (Kopia runs inside a worker VM, never inside the conductor process). In the R1 development/reference environment an EphemeralContainerExecutor may be used. Unit tests supply a FakeKopia. |
now | callable → datetime | No | Clock injection for tests. Defaults to datetime.datetime.now(datetime.UTC) when None. |
target.kopia_key_trust_id | string (Keystone trust ID) | Yes | A Keystone trust that allows Abacá's service user to read Barbican secrets on behalf of the tenant. Must be set before enrollment; if absent, the job fails immediately with EnrollmentInputsMissing. |
target.s3_access_key_ref | string (Barbican href) | Yes | Barbican href for the S3 access key ID. Must be set before enrollment. |
target.s3_secret_key_ref | string (Barbican href) | Yes | Barbican href for the S3 secret access key. Must be set before enrollment. |
target.kopia_key_ref | string (Barbican href) | No | If already set (from a previous partial attempt), bootstrap reuses the stored repository password rather than generating a new one, avoiding orphaned secrets in Barbican. |
Dispatch configuration ([enrollment] config group)
| Option | Type | Default | Description |
|---|---|---|---|
dispatch | string (rpc | inline) | rpc | Controls how a freshly-created enrollment job is routed. rpc is the only valid production mode: the API casts run_job to the conductor over oslo.messaging and returns immediately. inline drives the state machine on the API thread and is refused at startup unless [api] noauth=True; it exists solely as a unit-test harness. |
Kopia executor configuration ([kopia] config group)
| Option | Type | Notes |
|---|---|---|
executor | string | Must be http in production so that S3 credentials and the repository password never enter the conductor process. ephemeral_container is a development convenience that logs a startup warning; it MUST NOT be used in production deployments. |
On success, execute_enroll_job returns the terminal JobState value available and produces the following side effects:
job.state→available,job.progress→100,job.started_atandjob.ended_atpopulated.target.status→active(BucketStatus.ACTIVE).target.preflight_detail— a dict recording the outcome of every preflight check, suitable for operator inspection.target.preflight_at— UTC timestamp of the preflight run.target.capabilities— merged capability dict from the preflight result (e.g.object_lock_enabled,versioning_enabled,existing_repo_adoptable).target.kopia_key_ref— Barbican href for the repository password (written beforekopia repository createis called, so the ref is durable even if the job dies mid-flight).
Repository adoption path: when preflight detects an existing Kopia repository in the bucket (capability existing_repo_adoptable: true), bootstrap calls kopia repository connect instead of kopia repository create. The resulting capabilities update records repository_adopted: true; the kopia_encryption and kopia_block_hash capability fields are set to null because Abacá did not create the repository and cannot assert its crypto profile.
On failure, execute_enroll_job returns error and mutates target.status to one of:
target.status | Condition |
|---|---|
preflight_failed | A preflight check failed, enrollment inputs were missing, or the Barbican trust could not be redeemed. The tenant must correct the underlying issue and re-enroll. |
enroll_failed | Preflight passed but repository creation (or a subsequent step) failed. This is an operator-visible condition. |
The job.error_category field is set to tenant_action_required or operator_action_required depending on which exception terminated the state machine (see the Throws section).
The following exceptions are raised internally by the enrollment job. They are caught by the state machine and translated into job error states; you will not see Python tracebacks in the API response, but the error_code, error_title, and error_category fields on the Job resource reflect them.
| Exception | error_code | HTTP equivalent | error_category | When it occurs |
|---|---|---|---|---|
EnrollmentInputsMissing | abaca.enrollment.inputs_missing | 400 | tenant_action_required | target.kopia_key_trust_id, target.s3_access_key_ref, or target.s3_secret_key_ref is None or empty when enrollment starts. PATCH the missing fields onto the target and re-enroll. |
EnrollmentPreflightFailed | abaca.enrollment.preflight_failed | 422 | tenant_action_required (default) or operator_action_required (if the failing preflight check is tagged as such, e.g. an S3 backend outage) | A preflight conformance check returned FAIL. The error_category is copied from the specific failing check, so a lifecycle-rule violation is tenant_action_required while a backend connectivity failure is operator_action_required. |
RepositoryPasswordRequired | abaca.enrollment.repository_password_required | 400 | tenant_action_required | The bucket already contains a Kopia repository (adoption path) but no repository password was supplied or previously stored in Barbican. Because Kopia repositories are client-side encrypted the password cannot be recovered by Abacá; the tenant must supply it. |
EnrollmentRepositoryFailed | abaca.enrollment.repository_failed | 500 | operator_action_required | kopia repository create (or repository connect during adoption) failed after preflight passed. Check the conductor and worker logs for the underlying Kopia error message. |
RepositoryBootstrapError | — (wrapped into EnrollmentRepositoryFailed) | 500 | operator_action_required | Internal bootstrap step failed — for example, Barbican refused to store the generated password, or Kopia returned a non-zero exit code. The chained __cause__ contains the scrubbed Kopia error; secret material is never included. |
Startup guard — DispatchConfigurationError: this exception (a SystemExit subclass) is raised by validate_dispatch_config at process startup, not during a job run. It fires when [enrollment] dispatch=inline and [api] noauth=False are set simultaneously. The process exits non-zero without a stack trace. Correct this by setting dispatch=rpc in your abaca.conf.
Trigger enrollment via the OpenStack CLI
After registering a bucket as a BackupTarget, trigger enrollment by requesting the enroll action. Enrollment runs asynchronously; poll the returned job ID for completion.
# Enroll the target — returns a job ID immediately
openstack share protection target enroll <target-id>
# Poll the job until state is 'available' or 'error'
openstack share protection job show <job-id>
Expected output when enrollment succeeds:
+------------------+--------------------------------------+
| Field | Value |
+------------------+--------------------------------------+
| id | 7f3a1c2e-... |
| type | target_enroll |
| state | available |
| progress | 100 |
| error_category | None |
| started_at | 2025-03-14T09:00:00Z |
| ended_at | 2025-03-14T09:01:43Z |
+------------------+--------------------------------------+
Expected output when preflight fails:
+------------------+--------------------------------------+
| Field | Value |
+------------------+--------------------------------------+
| id | 7f3a1c2e-... |
| type | target_enroll |
| state | error |
| error_code | abaca.enrollment.preflight_failed |
| error_category | tenant_action_required |
| error_detail | preflight failed: lifecycle_rules -> |
| | bucket has lifecycle rules that |
| | would delete live backup data |
+------------------+--------------------------------------+
Check the target's preflight detail after a failed enrollment
The preflight_detail field on the target records every check result, not just the first failure. Inspect it to understand which checks passed and which failed.
openstack share protection target show <target-id> -f json | jq '.preflight_detail'
Example output:
{
"checks": [
{"name": "reachability", "result": "pass", "detail": null},
{"name": "versioning", "result": "pass", "detail": null},
{"name": "object_lock", "result": "pass", "detail": null},
{"name": "lifecycle_rules", "result": "fail",
"detail": "bucket has lifecycle rules that would delete live backup data",
"category": "tenant_action_required"}
]
}
Re-enroll after correcting a preflight failure
Fix the underlying issue (for example, remove conflicting S3 lifecycle rules) and then re-enroll. Abacá runs the full preflight again from the start.
# After fixing the bucket, re-enroll
openstack share protection target enroll <target-id>
# Confirm the target reaches 'active' status
openstack share protection target show <target-id> -f value -c status
# active
Repository adoption (disaster recovery)
If you are rebuilding the control plane from database loss and pointing Abacá at a bucket that already contains a Kopia repository, enrollment detects the existing repository (via the existing_repo_adoptable capability) and calls kopia repository connect instead of kopia repository create. You must have previously stored the repository password in Barbican under the target's kopia_key_ref; if that Barbican secret is lost, enrollment fails with RepositoryPasswordRequired and there is no recovery path (Kopia repositories are client-side encrypted).
# Adoption is automatic when preflight detects an existing Kopia repo.
# Trigger enrollment the same way:
openstack share protection target enroll <target-id>
# Verify the adopted capability on success:
openstack share protection target show <target-id> -f json | jq '.capabilities.repository_adopted'
# true
Inspect enrollment dispatch configuration (operator)
Verify that your deployment uses rpc dispatch. The inline mode is a unit-test harness and will be refused at startup if [api] noauth=False.
# /etc/abaca/abaca.conf
[enrollment]
dispatch = rpc
[kopia]
# Must be 'http' in production — credentials stay inside worker VMs.
executor = http
If you see the following in the API pod logs at startup, the configuration is unsafe and the process has exited:
REFUSE TO START: [enrollment] dispatch=inline is a unit-test harness, not a
deployment mode. This process has [api] noauth=False (real auth) — inline
dispatch would bypass the conductor / worker separation that the R1 threat
model depends on. Set [enrollment] dispatch=rpc or run with --noauth
(dev/test only).
State machine mapping for enrollment jobs
Enrollment reuses the shared JobStateMachine. The provisioning_network and provisioning_source stages are no-ops (enrollment involves no Manila share and no network attachment). All meaningful work happens in the later stages:
| State | What happens | Progress |
|---|---|---|
queued | Job created by API, cast to conductor | — |
provisioning_network | No-op | 5 % |
provisioning_source | No-op | 10 % |
connecting_repository | Preflight: redeem trust, fetch S3 credentials from Barbican, run conformance checks, persist preflight_detail and capabilities | 25 % |
transferring | kopia repository create (or connect for adoption); repository password generated and stored in Barbican before Kopia is invoked | 60 % |
finalizing | Capability dict and kopia_key_ref are durable in the database | 90 % |
releasing | No-op | 95 % |
available / error | Terminal | 100 % / — |
Repository password is written before Kopia runs. The kopia_key_ref Barbican href is persisted to the database (via on_password_stored) before kopia repository create is called. This ordering is intentional: if the job dies between creating the repository and returning, a retry can re-read the same password from Barbican and call kopia repository connect rather than generating a new password and failing because the bucket already has a repository. If the order were reversed, a mid-flight crash would leave an encrypted repository whose password exists nowhere durable.
Secret material never travels through the conductor process in production. The [kopia] executor = http setting means S3 credentials and the repository password are fetched inside the worker VM, not in the conductor. Setting executor = ephemeral_container causes a startup warning and MUST NOT be used in production; it would cause credentials to enter the conductor process, violating the threat model.
dispatch=inline is refused in production. validate_dispatch_config is called explicitly from every process entry point (abaca-api and abaca-conductor). It raises DispatchConfigurationError (a SystemExit subclass) if dispatch=inline and [api] noauth=False are both set. There is no import-time check — the validation is intentionally visible in the main functions of each entry point.
Error categories guide who must act. Every enrollment error carries an error_category of either tenant_action_required or operator_action_required. Preflight failures inherit their category from the specific failing check: a missing IAM permission or a lifecycle rule that would delete backup data is tenant_action_required; an S3 backend outage is operator_action_required. Post-preflight failures (Kopia repository create) are operator_action_required.
target.status transitions on failure are distinct. A target that fails during preflight (or because inputs are missing) moves to preflight_failed. A target that passes preflight but fails during or after repository creation moves to enroll_failed. This distinction prevents an operator from misreading a Kopia failure as a bucket-conformance issue when inspecting the Dashboard or the openstack share protection target show output.
Adopted repositories do not carry a FIPS profile assertion. When enrollment connects to an existing Kopia repository (adoption path), capabilities.kopia_encryption and capabilities.kopia_block_hash are set to null. Abacá cannot assert that a repository it did not create uses FIPS-approved algorithms. If FIPS compliance is a requirement, create a fresh bucket and enroll it from scratch.
executed_by field records which path ran. After a successful enrollment job, job.executed_by is set to conductor:<hostname> for the production RPC path. The value inline:<hostname> indicates the unit-test harness ran the job on the API thread. The worker: prefix is reserved for a future release and is not set by any production code path today.