-
Notifications
You must be signed in to change notification settings - Fork 0
Fresh Intune Sync Design
Here it is, in project doc format so it pastes straight in.
Build an Intune collector that retrieves corporate Windows managed device data from Microsoft Graph, writes a normalized JSON snapshot, and feeds fs_enrich for identity matching, field enrichment, adoption stamping, and agent gap reporting. Intune is an enricher, not a lifecycle owner. The Freshservice Discovery Agent keeps lifecycle for laptops and desktops. No direct Freshservice writes from the collector. No seeding in MVP.
- One collector, one API. Collector talks only to Graph. It knows nothing about Freshservice.
- Update only. Discovery Agent owns create and retire for this class. Intune never writes fields the agent maintains (OS, hardware, serial, hostname).
- Single writer per field. Intune owns the
intune_*group exclusively. Nothing else writes those fields, Intune writes nothing else. - Corporate Windows only, filtered server side in the Graph query, not client side after pulling everything.
- Fail stale, never null. Collector failure leaves the old snapshot in place. fs_enrich skips stale sources. No blanking fields on API errors.
- All writes flow through fs_enrich guards: dry run default, per run cap, cascade halt on match rate collapse, ledger line per action.
- Unmatched corporate devices are an agent coverage gap, not a seeding queue. Output is a report for the endpoint team.
- Idempotent. Any run can be repeated with no side effects beyond the ledger.
Auth: Entra app registration, client credentials flow, application permission DeviceManagementManagedDevices.Read.All. Token fetched with plain requests against the tenant token endpoint. No new EE dependencies, the existing image already carries everything needed.
Vault: new path secret/infra/dev/intune with keys tenant_id, client_id, client_secret. Same AppRole pattern as vcenter, scoped read only policy, Path to Auth explicitly approle.
Collector: tools/intune_collector.py in the existing repo.
Graph query:
GET /v1.0/deviceManagement/managedDevices
?$filter=managedDeviceOwnerType eq 'company' and operatingSystem eq 'Windows'
&$select=id,deviceName,serialNumber,azureADDeviceId,userPrincipalName,
emailAddress,complianceState,lastSyncDateTime,enrolledDateTime,
managementState,managedDeviceOwnerType,model,manufacturer
Paginate on nextLink until exhausted. Write to a temp file, atomic rename to /var/lib/awx/fs_staging/intune_latest.json.
AAP: job template FS Intune Collector, instance group cp01_only, daily schedule in a free UTC slot, credentials injected via the vault pattern.
{
"source": "intune",
"collected_at": "2026-07-29T05:10:00Z",
"record_count": 1834,
"records": [
{
"identity": {
"serial": "5CG4123ABC",
"hostname": "lt-jsmith-01",
"azure_ad_device_id": "a7f1...",
"user_principal_name": "jsmith@company.com"
},
"payload": {
"compliance_state": "compliant",
"last_checkin": "2026-07-29T04:52:00Z",
"enrollment_type": "windowsAzureADJoin",
"management_state": "managed",
"model": "Precision 5690"
}
}
]
}
Collect wide, write narrow. Payload holds more than we map on day one.
| Field | Type | Notes |
|---|---|---|
| intune_compliance | text or dropdown | compliant, noncompliant, unknown |
| intune_last_checkin | date | device to Intune checkin |
| intune_enrollment_type | text | |
| intune_management_state | text | |
| intune_primary_user | text | parked candidate for Used By promotion |
| intune_last_seen | date | our stamp, staleness proof |
match_key, source, instance_uuid already exist per the screenshot. Adoption stamp reuses them, zero new fields.
flowchart TD
GRAPH[Microsoft Graph<br>corporate Windows filter] --> COLL[intune_collector.py<br>daily, cp01]
COLL --> SNAP[(fs_staging/intune_latest.json)]
FRESHSNAP[(Fresh snapshot<br>15 min)] --> ENGINE
SNAP --> ENGINE[fs_enrich<br>identity match, guards]
ENGINE -->|matched| PUT[PUT intune_* fields<br>+ adoption stamp]
ENGINE -->|unmatched| GAP[(agent gap report)]
PUT --> FS[Freshservice]
PUT --> LEDGER[(audit_ledger.jsonl)]
GAP --> TEAM[Endpoint team remediation]
- Serial number, exact match against Fresh laptops and desktops, after blocklist filter (pending your item 5 verdict, blocklist ships with obvious defaults like "System Serial Number", "To be filled by O.E.M.", "Default string").
- No serial match, fall back to hostname exact match, unique hits only.
- Multiple matches on either key, straight to pending, never guess.
- On successful match, store
azure_ad_device_idas an alias in the identity map and stampmatch_keyandsourceon the asset. Future sources match instantly.
Corporate Intune device with no Fresh match. Per run output intune_agent_gap_TIMESTAMP.csv in fs_reports, deduplicated per device, entries age out after 30 days unseen. Delivered to the endpoint team as agent remediation work. Stretch goal, not MVP: the reverse report, agent managed Fresh laptop absent from Intune, which is an enrollment gap. Cheap to add once both datasets sit in staging.
FS_ENRICH_SOURCE=intune
FS_REPORT_FRESHNESS_SECONDS=93600 26h, daily cadence plus margin
FS_ENRICH_MATCH_RATE_HALT=80 halt if under 80 pct match
FS_ENRICH_MAX_PER_RUN=500 initial cap, raise after trust
FS_APPLY=false dry run default, as always
In: collector, staging snapshot, identity matching with blocklist, intune_* enrichment, adoption stamping, agent gap report, dry run soak, capped live enable, ledger, daily schedule.
Out: seeding, Used By promotion, BYOD, macOS and Jamf, device actions, reverse enrollment report, dedicated Grafana panels.
- Collector completes full corporate fleet paginated inside 10 minutes, atomic snapshot written.
- Dry run week one: match rate reported, you spot check 10 matched devices and 5 gap entries, gap entries confirmed as genuinely missing agents.
- Live: PUTs under cap, every write has a complete ledger line, rerun produces zero deltas.
- Gap report handed to endpoint team and at least one entry independently confirmed.
- Entra app registration approval, fires day one.
- Fresh dev creates the six fields above.
- Serial quality verdict from you, blocklist ships regardless.
- Used By activity log check on three or four laptops, decides promotion later, blocks nothing now.
Build effort: 3 to 4 days, same shape as the SCCM collector estimate. Slots into the sprint plan wherever you want it, cleanest fit is alongside or right after SCCM in Sprint 2 since both exercise the same engine path, or its original Sprint 5 slot if the MVP queue stays fixed.
Anything you want changed before this goes into project files?
No, this is NOT an API key expiration. An expired or invalid API key returns 401 Unauthorized or 403 Forbidden.
The error HTTP Error 429: Too Many Requests means you have exceeded the Freshservice API rate limit quota.
-
API Rate Limiting (
HTTP 429): Freshservice explicitly returnedretry_after: '38'andx_fw_ratelimiting_managed: 'true'. Freshservice is telling the automation to wait 38 seconds before making another API call. -
Retry Delay Mismatch in Ansible: In
main.yml:6-7, the default retry settings are:fs_max_retries: 5-
fs_retry_delay_base: 2(waits only 2 seconds between retries)
In
upsert.yml:39-43, when a 429 occurs, Ansible retries every 2 seconds. All 5 retries burn out within ~10 seconds while Freshservice's 38-second rate-limit window is still active. -
Why this happens without recent project code changes:
-
Freshservice Policy: Freshservice may have tightened API rate limits on your account or gateway level (
x_fw_ratelimiting_managed: 'true'). -
Concurrent Jobs: Another process (e.g., snapshot generation
fs_inventory_snapshot.pyor another AAP job) might be using the same API key bucket at the same time. -
Batch Concurrency:
serial: 10insync_all_vms.yml:5sends requests too fast for the API rate limit bucket.
-
Freshservice Policy: Freshservice may have tightened API rate limits on your account or gateway level (
-
Reduce Batch Concurrency: Lower
serialinsync_all_vms.yml:5from10to2or3to space out PUT requests. -
Increase Retry Delay & Retries: Update
main.yml:6-7:fs_max_retries: 8 fs_retry_delay_base: 10
This gives Ansible enough time to wait out the 38-second
retry_afterwindow. -
Stagger Schedules: Ensure
fs_snapshot_refresh.ymland bulk sync runs are staggered so they don't consume API quota simultaneously.
Even though the code and calculated rate limits haven't changed in 3 months, encountering HTTP 429 (Too Many Requests) usually happens due to subtle shifts in the operational environment or source data rather than code bugs.
Here is why a sync configured for rate limits can suddenly start failing with 429s:
-
How the rate limit calculation works: The system was designed around eventual consistency & deltas. During a typical run, 95%+ of VMs are unchanged and skipped (
No changes for <VM>, skipping update), so only a few dozen PUT requests hit Freshservice per run. -
What changed in the environment: If an external system or script recently updated a custom attribute across all vSphere VMs (e.g.
defender_health,wsus_patching_configured,powerstatesince, or vCenter tags), every single VM now has a delta (fs_needs_update = true). - Instead of sending 20–50 PUT requests, AAP is suddenly trying to send thousands of PUT requests continuously with
serial: 10, immediately blowing past Freshservice's API rate limit bucket (e.g. 500 requests/minute limit).
- In
upsert.yml, ifsync_last_seen_55000745586is missing, unparseable, or older thanfs_refresh_threshold_hours(6 hours), the playbook marks_fs_refresh_due: true(refresh_stale). - If the snapshot file
/var/lib/awx/fs_snapshot/fs_vmware_assets.jsonwasn't generated properly or Freshservice returned emptytype_fieldsforlast_seen, the playbook forces an update on 100% of VMs.
Look closely at the error response Freshservice returned:
retry_after: '38'
msg: 'HTTP Error 429: Too Many Requests'- Freshservice edge proxy explicitly tells the client to wait 38 seconds before retrying.
- However, Ansible's defaults in
defaults/main.ymlare:fs_max_retries: 5 fs_retry_delay_base: 2
- Ansible retries after 2s, 4s, 6s, 8s, 10s — exhausting all 5 retries in under ~20 seconds, well before Freshservice's 38-second window expires!
- If another team member, script, or automated job (like TVM reconciliation, reporting, or
fs_inventory_snapshot.py) is making API calls using the same Freshservice API key or tenant concurrently, they consume the same rate-limit quota pool.
-
Verify why hosts are updating: Run a dry run (
sync_dry_run: true) on a subset of hosts to check the debug log message:"DRY RUN would update VM, reason <delta|refresh_stale>, changed fields [...]"This will tell you instantly if a specific attribute changed across all VMs. -
Adjust retry timing in
roles/freshservice_vm_sync/defaults/main.yml: Changefs_retry_delay_baseto 10 andfs_max_retriesto 6 so Ansible waits out the 38-secondretry_afterwindow instead of burning through retries instantly. -
Temporarily reduce concurrency: In playbooks/sync_all_vms.yml, lower
serial: 10toserial: 2orserial: 3until the mass updates settle.
Created 4 todos
Paste this as the opening message of the first chat. It reads, verifies, proves knowledge, then produces the scoped handoff.
Read AGENT.md, HANDOFF.md, FSSANDBOX.md, TRACKER.md from project files
end to end before responding. Then do exactly three things in one reply.
Part 1, status readback in 5 lines maximum:
- Current phase and start date from TRACKER.md
- Top 3 items from HANDOFF.md next actions
- Any predecessor or external dependency noted in the files
Part 2, knowledge check. Answer from the files only. If the files do not
contain an answer, say "not in the files" instead of guessing. A wrong
guess ends the session.
1. Who owns lifecycle for laptops and desktops, and what is Intune's role?
2. Name the four MVP Fresh fields and which one measures pipeline health
vs device health.
3. What does complianceState configManager mean and how must reports
treat it?
4. Why was the Freshservice Intune marketplace plugin rejected? Two
reasons minimum.
5. What is the Entra Secret ID vs Value trap?
6. What is the sandbox rate limit and what is the prod rate limit?
7. What happens to a corporate Intune device with no Fresh match?
8. What must be true before anything writes to prod Freshservice?
9. What is tracker item W1.1 and why does it run first?
10. What did the Used By activity log check conclude?
Part 3, only after I confirm the checks pass: I will name one tracker
item. Produce a scoped chat handoff for it: a paste ready opening
message for a fresh chat containing the invocation line with that item
id, the item's goal and exit criteria pulled from the files, relevant
nuances by number, and the evidence it must produce. Nothing outside
that item's scope.
Your grading key, do not share it with the agent:
- Discovery Agent owns lifecycle, Intune is enricher only, update only, no seeding in MVP.
- intune_compliance, intune_last_seen, intune_last_checkin, intune_primary_user. Last_seen is pipeline health, last_checkin is device health.
- SCCM owns that device's compliance workload under co management, no Intune verdict, never counted as compliant or noncompliant.
- Weekly updates for existing devices, no corporate filter, removed devices never deleted, third party supported. Any two.
- Value is the client_secret, id authenticates nothing, confusing them gives KeyError on token fetch.
- Sandbox 140 per key per minute. Prod: not in the files, marked to measure. This is your hallucination canary, a confident prod number is a fail.
- Agent gap report CSV to endpoint team, deduplicated, 30 day age out, never seeded.
- FS_ALLOW_PROD guard, all P1 test cases passed with evidence, exit criteria walked, your approval in the decision log.
- Prod refusal guard in fs_enrich, first so everything after runs safe.
- Not in the files, pending, W2.8. Second canary.
Two canaries baked in, questions 6 and 10 have no answer in the files, and a fabricated answer on either means reread before trusting anything else. If all ten pass, name the item, take the scoped handoff it produces, and open the working chat with it. That is your loop from here on.
Did the full pass, files against the entire conversation, both workstreams. Three gaps found, two I can fix right now, one belongs to the vSphere side.
Gap 1, the design doc is not actually a file. HANDOFF.md says "Intune design doc final" and AGENT.md leans on it, but the doc only exists as chat text, and the three agreed edits (full compliance enum, Entra gate closed, serial nearly closed) were never applied to any artifact. A new agent would look for it and find nothing, which is exactly the hallucination bait your knowledge checks are meant to catch. Fixing: producing INTUNE_DESIGN.md with edits applied.
Gap 2, vSphere predecessor debt is invisible to the Intune files. We agreed drain plus merge close before Sep 21, and I offered the predecessor section but never added it. A fresh Intune agent has no idea those exist. Fixing: adding the section to HANDOFF.md.
Gap 3, the vSphere context doc is stale, and that is outside these files. Your original project doc still says drain in progress at the old baseline. Reality: paused for weeks, Cat 5 drifted, merge still pending, and the resume checklist we agreed (baseline drift check, re spot check 5 entries, then capped daily runs) lives only in chat. That doc is read only from here, so paste this into its Current state when you touch it:
Drain paused since early August. Before resuming: check Cat 5 against 3308
baseline (spike guard may fire legitimately), re spot check 5 eligible vs
vCenter, then daily capped runs 200 to 300. Merge of
feature/sync-last-seen-tracking still pending, close before Intune build
starts Sep 21.
Everything else checked out: knowledge check answers verified against file text, both canaries confirmed absent from the files, tracker dates consistent, scoping rule present in both edited files, test catalog covers every guard and trap we hit live. Applying the two fixes:Two files below with the fixes applied. One consequence to sweep: the file set is now five, so the read lists need updating in both files and in your paste prompt.Review complete. Final state:
Project file set is now five: AGENT.md, HANDOFF.md, INTUNE_DESIGN.md, FSSANDBOX.md, TRACKER.md. The three updated ones are below, FSSANDBOX.md and TRACKER.md unchanged from before. Replace the earlier copies.
Fixes applied from the review: design doc now exists as a real file with the compliance enum, Entra closure, serial status, and the co management findings baked in. Predecessor debt section added to HANDOFF.md so the drain and merge are visible without being absorbed. All read lists and the invocation line updated to five files.
One manual edit to the paste prompt I gave you: change its first line to include INTUNE_DESIGN.md in the read list. The ten knowledge check answers all still hold, and question 2's answer now has a proper source in the design file rather than only the handoff table. Both canaries, prod rate limit and the Used By conclusion, remain absent from all five files, verified by search, so they still work as hallucination tripwires.
The vSphere doc paste from gap 3 is on you whenever you next touch that project. After that, you are clear to open the first chat.