Skip to content

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

  • admin role to create or delete windows. Listing existing windows requires only auditor or 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/jobs targeting a machine tagged prod (via target_ids or target_tags) is accepted immediately if the current time in Europe/Paris falls within a matching window; otherwise the job is created but its background thread blocks in waiting_window until 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/jobs returns 409 immediately instead of queuing forever:
    {
      "detail": "Les fenêtres de maintenance des machines ciblées ne se chevauchent jamais — aucun créneau commun possible. Vérifiez la configuration des fenêtres (tags et/ou overrides par machine)."
    }
    

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