This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| wiki:ai:bcm_servicenow [2026/08/21 19:34] – bgourley | wiki:ai:bcm_servicenow [2026/09/17 12:26] (current) – bgourley | ||
|---|---|---|---|
| Line 10: | Line 10: | ||
| **Auth model confirmed working:** ServiceNow OAuth2 '' | **Auth model confirmed working:** ServiceNow OAuth2 '' | ||
| - | ---- | ||
| ===== Part 1: ServiceNow-Side Prerequisites ===== | ===== Part 1: ServiceNow-Side Prerequisites ===== | ||
| Line 35: | Line 34: | ||
| -H ' | -H ' | ||
| -H ' | -H ' | ||
| - | -d ' | + | -d ' |
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | | ||
| </ | </ | ||
| Line 84: | Line 91: | ||
| -X POST " | -X POST " | ||
| -d "{ | -d "{ | ||
| - | \" | + | \" |
| - | \" | + | \" |
| - | \"category\": \"Infrastructure\", | + | \" |
| - | \"urgency\": \"2\", | + | \"priority\": |
| - | \"impact\": \"2\" | + | \" |
| + | \"company\": \"cdw\", | ||
| + | \"configuration_item\": \"CDW-PLACEHOLDER\" | ||
| }" >> / | }" >> / | ||
| </ | </ | ||
| Line 292: | Line 301: | ||
| ---- | ---- | ||
| + | |||
| + | ===== Part 10: Adding Deduplication Logic ===== | ||
| + | |||
| + | < | ||
| + | #!/bin/bash | ||
| + | # BCM -> ServiceNow incident creation (OAuth2 client_credentials) | ||
| + | # | ||
| + | # Config files (both under / | ||
| + | # | ||
| + | # (same across every client deployment) | ||
| + | # | ||
| + | # (the one value that differs per client deployment) | ||
| + | |||
| + | SHARED_CONFIG_FILE="/ | ||
| + | COMPANY_CONFIG_FILE="/ | ||
| + | LOG=/ | ||
| + | |||
| + | log() { | ||
| + | echo " | ||
| + | } | ||
| + | |||
| + | # --------------------------------------------------------------------------- | ||
| + | # Load and validate config | ||
| + | # --------------------------------------------------------------------------- | ||
| + | if [ ! -f " | ||
| + | log " | ||
| + | exit 1 | ||
| + | fi | ||
| + | if [ ! -f " | ||
| + | log " | ||
| + | exit 1 | ||
| + | fi | ||
| + | # shellcheck disable=SC1090 | ||
| + | source " | ||
| + | # shellcheck disable=SC1090 | ||
| + | source " | ||
| + | |||
| + | if [ -z " | ||
| + | log " | ||
| + | exit 1 | ||
| + | fi | ||
| + | |||
| + | if [ -z " | ||
| + | log " | ||
| + | exit 1 | ||
| + | fi | ||
| + | |||
| + | # --------------------------------------------------------------------------- | ||
| + | # Alert context from CMDaemon | ||
| + | # --------------------------------------------------------------------------- | ||
| + | NODE=" | ||
| + | CHECK=" | ||
| + | SEVERITY=" | ||
| + | VALUE=" | ||
| + | |||
| + | # --------------------------------------------------------------------------- | ||
| + | # Step 1: get access token | ||
| + | # --------------------------------------------------------------------------- | ||
| + | TOKEN_RESPONSE=$(curl -s -X POST " | ||
| + | -H ' | ||
| + | --data-urlencode ' | ||
| + | --data-urlencode " | ||
| + | --data-urlencode " | ||
| + | |||
| + | ACCESS_TOKEN=$(echo " | ||
| + | |||
| + | if [ -z " | ||
| + | log " | ||
| + | exit 1 | ||
| + | fi | ||
| + | |||
| + | # --------------------------------------------------------------------------- | ||
| + | # Step 2: resolve Company Name -> sys_id (core_company table) | ||
| + | # --------------------------------------------------------------------------- | ||
| + | COMPANY_SYSID="" | ||
| + | COMPANY_LOOKUP_FAILED=no | ||
| + | |||
| + | COMPANY_RESPONSE=$(curl -s -w " | ||
| + | -H " | ||
| + | -H " | ||
| + | -G " | ||
| + | --data-urlencode " | ||
| + | --data-urlencode " | ||
| + | --data-urlencode " | ||
| + | |||
| + | COMPANY_HTTP_STATUS=$(echo " | ||
| + | COMPANY_BODY=$(echo " | ||
| + | |||
| + | if [ " | ||
| + | COMPANY_SYSID=$(echo " | ||
| + | fi | ||
| + | |||
| + | if [ -z " | ||
| + | COMPANY_LOOKUP_FAILED=yes | ||
| + | log " | ||
| + | fi | ||
| + | |||
| + | # --------------------------------------------------------------------------- | ||
| + | # Step 3: build a dedup identity for logging purposes. We no longer write to | ||
| + | # correlation_id (blocked by an ACL on this instance) — instead we match on | ||
| + | # short_description containing the check name and node name, scoped by | ||
| + | # company. NOTE: short_description also contains ${VALUE}, which changes | ||
| + | # every time the check fires, so we deliberately match on CHECK and NODE as | ||
| + | # SUBSTRINGS rather than an exact title match — an exact match would treat | ||
| + | # every differing value as a " | ||
| + | # --------------------------------------------------------------------------- | ||
| + | DEDUP_LABEL=" | ||
| + | |||
| + | # --------------------------------------------------------------------------- | ||
| + | # Step 4: check for an existing OPEN incident matching this check+node | ||
| + | # (+ company, if resolved). Retries a few times before treating the check | ||
| + | # as failed. | ||
| + | # --------------------------------------------------------------------------- | ||
| + | DEDUP_CHECK_FAILED=no | ||
| + | EXISTING_SYSID="" | ||
| + | EXISTING_NUMBER="" | ||
| + | MATCH_COUNT=0 | ||
| + | |||
| + | DEDUP_QUERY=" | ||
| + | if [ -n " | ||
| + | DEDUP_QUERY=" | ||
| + | fi | ||
| + | DEDUP_QUERY=" | ||
| + | |||
| + | for attempt in 1 2 3; do | ||
| + | DEDUP_RESPONSE=$(curl -s -w " | ||
| + | -H " | ||
| + | -H " | ||
| + | -G " | ||
| + | --data-urlencode " | ||
| + | --data-urlencode " | ||
| + | --data-urlencode " | ||
| + | --data-urlencode " | ||
| + | |||
| + | DEDUP_HTTP_STATUS=$(echo " | ||
| + | DEDUP_BODY=$(echo " | ||
| + | |||
| + | if [ " | ||
| + | MATCH_COUNT=$(echo " | ||
| + | if [ " | ||
| + | TOP_MATCH_STATE=$(echo " | ||
| + | if [ " | ||
| + | # Most recent matching incident is already resolved — this is a | ||
| + | # fresh recurrence of the issue, not a duplicate. Let it create new. | ||
| + | log "Most recent matching incident is Resolved — treating this as a new occurrence, not a duplicate." | ||
| + | else | ||
| + | EXISTING_SYSID=$(echo " | ||
| + | EXISTING_NUMBER=$(echo " | ||
| + | fi | ||
| + | fi | ||
| + | DEDUP_CHECK_FAILED=no | ||
| + | break | ||
| + | else | ||
| + | DEDUP_CHECK_FAILED=yes | ||
| + | log " | ||
| + | sleep 2 | ||
| + | fi | ||
| + | done | ||
| + | |||
| + | if [ " | ||
| + | log " | ||
| + | fi | ||
| + | |||
| + | # --------------------------------------------------------------------------- | ||
| + | # Case A: a genuine duplicate was found -> bump it, don't create a new one. | ||
| + | # --------------------------------------------------------------------------- | ||
| + | if [ -n " | ||
| + | NOTE_TEXT=" | ||
| + | |||
| + | curl -s -o /dev/null \ | ||
| + | -H " | ||
| + | -H " | ||
| + | -X PATCH " | ||
| + | -d " | ||
| + | |||
| + | log " | ||
| + | exit 0 | ||
| + | fi | ||
| + | |||
| + | # --------------------------------------------------------------------------- | ||
| + | # Case B: no duplicate (or dedup check failed open) -> create a new incident. | ||
| + | # --------------------------------------------------------------------------- | ||
| + | SHORT_DESC=" | ||
| + | DESC=" | ||
| + | CONFIG_ITEM=" | ||
| + | |||
| + | if [ " | ||
| + | DESC=" | ||
| + | fi | ||
| + | if [ " | ||
| + | DESC=" | ||
| + | fi | ||
| + | |||
| + | # Build the JSON payload, only including " | ||
| + | if [ -n " | ||
| + | PAYLOAD=$(jq -n \ | ||
| + | --arg short_description " | ||
| + | --arg description " | ||
| + | --arg company " | ||
| + | --arg configuration_item " | ||
| + | ' | ||
| + | else | ||
| + | PAYLOAD=$(jq -n \ | ||
| + | --arg short_description " | ||
| + | --arg description " | ||
| + | --arg configuration_item " | ||
| + | ' | ||
| + | fi | ||
| + | |||
| + | curl -s -w " | ||
| + | -H " | ||
| + | -H " | ||
| + | -X POST " | ||
| + | -d " | ||
| + | |||
| + | log " | ||
| + | |||
| + | </ | ||
| ===== Known Time Sinks to Avoid Next Time ===== | ===== Known Time Sinks to Avoid Next Time ===== | ||