Maintenance windows for installs¶
Maintenance windows restrict when POST /install/jobs is allowed to actually push packages to a machine — defined per inventory tag or as a per-machine override. A machine with no applicable window runs installs immediately, exactly as before this feature existed; adding a window is what turns the restriction on.
1. Prerequisites¶
adminrole to create or delete windows. Listing existing windows requires onlyauditoror above.- Know the IANA timezone name for the window (e.g.
Europe/Paris,America/New_York) — windows are stored as local wall-clock time plus a named timezone, not a fixed UTC offset, so DST is handled automatically. - Decide whether you're targeting a tag (every machine carrying it) or a specific machine (an override that replaces that machine's tag-derived windows entirely, it never merges with them).
Default is open
A machine with no tag window and no client override runs install jobs immediately, at any time — unchanged behavior for fleets that never configure this feature.
2. Check existing windows¶
curl -s http://repod.example.com:8000/api/v1/inventory/maintenance-windows \
-H "Authorization: Bearer repod_xxxxxxxxxxxxxxxx" | jq .
Filter by principal:
curl -s "http://repod.example.com:8000/api/v1/inventory/maintenance-windows?principal_type=tag&principal_id=prod" \
-H "Authorization: Bearer repod_xxxxxxxxxxxxxxxx" | jq .
3. Create a window¶
curl -s -X POST http://repod.example.com:8000/api/v1/inventory/maintenance-windows \
-H "Authorization: Bearer repod_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"principal_type": "tag",
"principal_id": "prod",
"days": ["mon", "tue", "wed", "thu", "fri"],
"start_time": "02:00",
"end_time": "04:00",
"timezone": "Europe/Paris"
}' | jq .
| Field | Values | Meaning |
|---|---|---|
principal_type |
tag | client |
Whether this window applies to every machine carrying a tag, or overrides one specific machine |
principal_id |
string | The tag name (e.g. prod), or the client's id, depending on principal_type |
days |
list of mon|tue|wed|thu|fri|sat|sun |
Which days of the week the window is open |
start_time |
"HH:MM" |
Local start time |
end_time |
"HH:MM" |
Local end time. If earlier than start_time, the window crosses midnight. |
timezone |
IANA name | e.g. Europe/Paris, UTC |
Response (201):
{
"window": {
"id": "...",
"principal_type": "tag",
"principal_id": "prod",
"days": ["mon", "tue", "wed", "thu", "fri"],
"start_time": "02:00",
"end_time": "04:00",
"timezone": "Europe/Paris",
"created_by": "admin",
"created_at": "..."
}
}
Worked example — only allow installs on prod machines Mon-Fri 02:00-04:00 Europe/Paris:
The POST above does exactly this. From this point on:
POST /install/jobstargeting a machine taggedprod(viatarget_idsortarget_tags) is accepted immediately if the current time inEurope/Parisfalls within a matching window; otherwise the job is created but its background thread blocks inwaiting_windowuntil the window opens (or the job is cancelled).- A machine's effective window, when multiple targeted machines are involved, is the intersection of all targeted machines' windows — machines with no window at all don't constrain the intersection.
- If the intersection can never be satisfied — for example two targeted machines have windows on entirely disjoint days —
POST /install/jobsreturns409immediately instead of queuing forever:
Client override replaces tag windows, it never merges with them
A principal_type: "client" window for one specific machine fully replaces that machine's tag-derived windows — the tag windows are not combined with it. This mirrors the same override-replaces-not-merges convention used by machine_access (see Restrict distribution & machine access). To give one prod machine a different maintenance schedule than the rest of the fleet, create a client-type window referencing its id; you do not need to remove the tag-level window first.
4. Cross-midnight windows¶
An end_time earlier than start_time is interpreted as crossing midnight:
curl -s -X POST http://repod.example.com:8000/api/v1/inventory/maintenance-windows \
-H "Authorization: Bearer repod_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"principal_type": "tag",
"principal_id": "batch-nodes",
"days": ["sat", "sun"],
"start_time": "23:00",
"end_time": "01:00",
"timezone": "UTC"
}' | jq .
This window is open from 23:00 Saturday through 01:00 Sunday, and again from 23:00 Sunday through 01:00 Monday.
5. Delete a window¶
curl -s -X DELETE http://repod.example.com:8000/api/v1/inventory/maintenance-windows/<window_id> \
-H "Authorization: Bearer repod_xxxxxxxxxxxxxxxx"
Returns 204 on success, 404 if the window doesn't exist. Deleting the last window for a tag/machine reopens it to installs at any time.
6. Verify it worked¶
Outside the window — creating a job targeting only prod-tagged machines should either wait or, if no common window exists at all across the targets, fail fast:
curl -s -X POST http://repod.example.com:8000/api/v1/install/jobs \
-H "Authorization: Bearer repod_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"package_name": "nginx",
"package_version": "1.24.0-1",
"target_tags": ["prod"]
}' | jq .
# Poll job status — expect step="waiting_window" outside 02:00-04:00 Europe/Paris
curl -s http://repod.example.com:8000/api/v1/install/jobs/<job_id> \
-H "Authorization: Bearer repod_xxxxxxxxxxxxxxxx" | jq '.step'
Inside the window — the same request should proceed straight to the dry-run/confirm flow without ever reporting waiting_window.
Disjoint windows across targets — targeting two machines whose windows never overlap should fail immediately with 409, not hang:
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://repod.example.com:8000/api/v1/install/jobs \
-H "Authorization: Bearer repod_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"package_name": "nginx",
"target_ids": ["<machine-A-id-with-mon-window>", "<machine-B-id-with-tue-only-window>"]
}'
# → 409