====== BCM → ServiceNow Alert Integration: Deployment Guide ====== This guide documents a working, tested process for routing NVIDIA Base Command Manager (BCM) monitoring alerts — both health checks and metric thresholds — into ServiceNow as incidents. It's based on a live build-out and includes the gotchas that cost the most time, so future setups can skip them. ===== Overview ===== BCM raises alerts through CMDaemon's monitoring framework: **measurables** (metrics or health checks) get evaluated by **triggers** (conditions), which fire **actions** (scripts) when the condition is met. This guide wires a trigger's action to a script that authenticates to ServiceNow via OAuth2 client credentials and creates an incident via the Table API. **Auth model confirmed working:** ServiceNow OAuth2 ''client_credentials'' grant against ''/oauth_token.do'', then a Bearer token against ''/api/now/table/incident''. ===== Part 1: ServiceNow-Side Prerequisites ===== Confirm with your ServiceNow admin before starting: - **OAuth Application Registry** exists with grant type ''Client Credentials'', and you have the ''client_id'' / ''client_secret''. - The system property ''glide.oauth.inbound.client.credential.grant_type.enabled'' is set to ''true'' (off by default). - The **OAuth Application User** tied to that registry has write access to the ''incident'' table (or whichever table you're targeting). A missing grant here produces a 403 on incident creation even though the token request succeeds. **Test both halves independently before touching BCM:** # 1. Get a token curl -s -X POST 'https://YOUR_INSTANCE.service-now.com/oauth_token.do' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=client_credentials' \ --data-urlencode 'client_id=YOUR_CLIENT_ID' \ --data-urlencode 'client_secret=YOUR_CLIENT_SECRET' | jq # 2. Use the token to create a test incident curl -s -w '\nHTTP_STATUS:%{http_code}\n' -X POST \ 'https://YOUR_INSTANCE.service-now.com/api/now/table/incident' \ -H 'Authorization: Bearer PASTE_ACCESS_TOKEN_HERE' \ -H 'Content-Type: application/json' \ -d '{ \"short_description\": \"Test incident from BCM integration testing\", \"description\": \"Manual curl test, ignore/close this\", \"assignment_group\": \"SO-NOC-CLDAI\", \"priority\": \"3\", \"severity\": \"3\", \"company\": \"cdw\", \"configuration_item\": \"CDW-PLACEHOLDER\" }' | jq Expect ''HTTP_STATUS:201'' on the second call. **Gotcha:** don't accidentally POST the incident payload to the token URL (''/oauth_token.do'') — it silently returns a 401 that looks like an auth failure but is actually a wrong-endpoint error. Always double check the URL, not just the credentials, when debugging a 401. ---- ===== Part 2: The Action Script ===== ==== 2.1 Script location and content ==== sudo mkdir -p /cm/local/apps/cmd/scripts/actions sudo nano /cm/local/apps/cmd/scripts/actions/servicenow-incident.sh #!/bin/bash # BCM -> ServiceNow incident creation (OAuth2 client_credentials) SN_INSTANCE="https://YOUR_INSTANCE.service-now.com" SN_CLIENT_ID="your_client_id" SN_CLIENT_SECRET="your_client_secret" # Confirmed real CMDaemon env vars (see "Environment Variables" section below) NODE="${CMD_ENTITY_NAME:-unknown-node}" CHECK="${CMD_MEASURABLE_NAME:-unknown-check}" SEVERITY="${CMD_SEVERITY:-unknown}" VALUE="${CMD_VALUE:-N/A}" # Step 1: get access token TOKEN_RESPONSE=$(curl -s -X POST "${SN_INSTANCE}/oauth_token.do" \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=client_credentials' \ --data-urlencode "client_id=${SN_CLIENT_ID}" \ --data-urlencode "client_secret=${SN_CLIENT_SECRET}") ACCESS_TOKEN=$(echo "${TOKEN_RESPONSE}" | jq -r '.access_token') if [ -z "${ACCESS_TOKEN}" ] || [ "${ACCESS_TOKEN}" = "null" ]; then echo "$(date) - Failed to get access token: ${TOKEN_RESPONSE}" >> /var/log/cmd-servicenow-status.log exit 1 fi # Step 2: create the incident curl -s -w "\nHTTP_STATUS:%{http_code}\n" \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -X POST "${SN_INSTANCE}/api/now/table/incident" \ -d "{ \"short_description\": \"Test incident from BCM integration testing\", \"description\": \"Manual curl test, ignore/close this\", \"assignment_group\": \"SO-NOC-CLDAI\", \"priority\": \"3\", \"severity\": \"3\", \"company\": \"cdw\", \"configuration_item\": \"CDW-PLACEHOLDER\" }" >> /var/log/cmd-servicenow-status.log 2>&1 ==== 2.2 Permissions and ownership ==== CMDaemon runs as **root** (not a dedicated ''cmdaemon'' user — that user doesn't exist by default). Match BCM's own convention for scripts under ''/cm/local/apps/cmd/scripts/'': sudo chown root:root /cm/local/apps/cmd/scripts/actions/servicenow-incident.sh sudo chmod 700 /cm/local/apps/cmd/scripts/actions/servicenow-incident.sh ''700'' keeps the embedded ''client_secret'' from being group/world-readable. For anything beyond a validation exercise, split credentials into a separate ''chmod 600'' file sourced by the script, so the secret isn't mixed into logic you might paste elsewhere for debugging. ==== 2.3 Standalone test (before touching BCM at all) ==== sudo /cm/local/apps/cmd/scripts/actions/servicenow-incident.sh sudo cat /var/log/cmd-servicenow-status.log Confirm ''HTTP_STATUS:201'' and a real incident in ServiceNow (values will show as "unknown-node"/"unknown-check" here since the ''CMD_*'' vars are only populated when CMDaemon itself invokes the script). ---- ===== Part 3: Registering the Action in BCM ===== % monitoring action % add ServiceNowIncident % set script /cm/local/apps/cmd/scripts/actions/servicenow-incident.sh % set runon "Active head node" % set timeout 30 % commit **Gotchas:** * The parameter is ''script'', not ''command''. * There's no ''runlocally'' parameter — it's ''runon'', and it takes one of exactly three quoted strings: ''Node'', ''"Active head node"'', or ''"Monitoring node"''. Bare ''ACTIVE'' will fail. * Set an explicit ''timeout'' so a hung ServiceNow call doesn't block other monitoring actions. Verify: % use ServiceNowIncident % show ---- ===== Part 4: Finding the Right Measurable ===== BCM's data model: every metric or health check is a **measurable**, each tied to a **producer**. The ''monitoring setup; list'' view truncates the measurables column — don't rely on it. Instead, use the top-level ''measurable'' command: % measurable % list This gives the full table with ''Type'' (Metric / HealthCheck / Enum), ''Name'', ''Parameter'' (e.g. which GPU, which disk), ''Class'', and ''Producer''. Use this to find your exact target, e.g.: ^ Type ^ Name ^ Parameter ^ Producer ^ | HealthCheck | ManagedServicesOk | — | CMDaemonState | | Metric | CPUUsage | — | ProcStat | | Metric | gpu_utilization | gpu0 | GPUSampler | **Gotcha — fractions, not percentages:** Metrics like ''CPUUsage'' and ''gpu_utilization'' are stored as a **0–1 fraction**, not 0–100. Check ''Minimum''/''Maximum'' via ''measurable; use ; show'' before writing any threshold — ''80'' in an expression means 8000%, not 80%. Use ''.8'', not ''80''. **Gotcha — device scope:** ''latestmetricdata'' and ''measurable'' show data for whatever device you're currently ''use'''d into under ''device'' mode. If a node doesn't appear in ''device; list'', BCM has no visibility into it at all — its hardware (including any GPU) is invisible to monitoring until it's provisioned/added as a managed device. Don't assume a machine you SSH into by one hostname is the same device BCM refers to by another name — confirm by comparing IPs (''show'' on the device vs. ''ip a'' on the box). ---- ===== Part 5: Creating a Trigger ===== Triggers are a separate top-level mode from both ''measurable'' and ''action'' — they reference a measurable via an ''Expression'' string and fire actions on state transitions. ==== 5.1 Inspect an existing trigger first ==== % trigger % list % use "Failing health checks" % show This reveals the real field names, most importantly: * ''Expression'' — pattern format ''(entity, measurable, parameter) ''. Wildcards are ''*''. * ''Enter actions'' / ''During actions'' / ''Leave actions'' — fire respectively on entering the condition, while it persists, and on leaving it. Use **Enter actions** for one-shot alerting (don't fire repeatedly while still in the failed/high state). * ''State flapping period'' / ''State flapping count'' — built-in noise suppression; a real sustained breach still fires normally, this just protects against a value rapidly oscillating across the threshold. * ''Mark entity as failed'' — set ''yes'' for real failure conditions (health checks), ''no'' for a metric threshold that isn't itself a device failure (e.g. high CPU). ==== 5.2 Health check trigger ==== % trigger % add ServiceNowManagedServicesFail % set expression "(*, ManagedServicesOk, *) == FAIL" % set enteractions ServiceNowIncident % commit **Gotcha — field order:** The expression format is ''(entity, measurable, parameter)'', not ''(measurable, parameter, entity)''. Putting the measurable name in the first slot produces a silent-ish warning: ''No known entity matches the specified regex''. ==== 5.3 Metric threshold trigger ==== % trigger % add ServiceNowCPUUsageHigh % set expression "(*, CPUUsage, *) > .8" % set enteractions ServiceNowIncident % set markentityasfailed no % commit ---- ===== Part 6: The Real CMDaemon Environment Variables ===== The following names are **confirmed correct** (captured via ''env > /tmp/debug.txt'' inside the action script during a live trigger firing). Guessed names like ''CMD_NODE'' / ''CMD_HEALTHCHECK'' **do not exist** — using them silently falls through to blank/default values with no error. ^ Variable ^ Example value ^ Use for ^ | CMD_ENTITY_NAME | bcm | Node/device hostname | | CMD_MEASURABLE_NAME | CPUUsage | Which metric/health check fired | | CMD_MEASURABLE_PARAMETER | (e.g. gpu0) | Sub-parameter, if any | | CMD_SEVERITY | 10 | Numeric severity | | CMD_VALUE | 71.8% | Formatted/display value | | CMD_RAW_VALUE | 0.717588 | Raw numeric value | | CMD_TRIGGER_NAME | ServiceNowCPUUsageHigh | Which trigger fired | | CMD_TRIGGER_EXPRESSION | (*, CPUUsage, *) > .8 | The expression that matched | | CMD_ENTITY_TYPE | HeadNode | Device role type | | CMD_ACTION_NAME | ServiceNowIncident | Which action is running | To re-capture this list on a different BCM version (field names may drift), temporarily add ''env > /tmp/cmd-env-dump.txt'' as the first line of the action script, trigger a real alert, then ''cat''/''grep'' the dump — don't rely on documentation or guessing. ---- ===== Part 7: Editing Device Services (autostart/monitored) ===== To stop BCM from auto-restarting a service you're trying to fail intentionally for testing: % device % use % services % list **Gotcha:** ''use '' frequently fails with ''Unable to find'', even for a service visibly in the list — this submode expects ''add'', which is idempotent (re-adds/re-enters edit mode on an existing entry without duplicating it): % add % set autostart no % commit Stop the service at the systemd level using its **real unit name**, which may differ from BCM's internal key (e.g. BCM calls it ''ntpd'', systemd calls it ''ntpsec''): sudo systemctl stop **Always revert after testing:** % add % set autostart yes % commit sudo systemctl start **Caution:** stopping certain services (auth, DNS, firewall, NFS) even briefly can disrupt your own session or other systems depending on it. Prefer inert services (e.g. ''ntpd''/time sync) for test purposes, and check what depends on a service before stopping it. ---- ===== Part 8: Useful cmsh Commands for Faster Iteration ===== ^ Command ^ Purpose ^ | measurable; list | Full list of all metrics/health checks with exact names | | latestmetricdata (inside device; use ) | Current recorded value for every metric on that device | | latestmetricdata | Filter to one metric | | latesthealthdata | Current health check states for a device | | samplenow | Force an immediate sample rather than waiting for the interval (check samplenow ? for exact usage) | | set ? or set ? | Show valid parameter names/values in current mode — use this liberally, cmsh's field names frequently differ from what you'd guess (e.g. runon not runlocally, values must be exact strings like "Active head node") | ---- ===== Part 9: End-to-End Test Checklist ===== - Confirm ServiceNow OAuth prerequisites (Part 1) with a manual curl test. - Deploy and standalone-test the script (Part 2.3) — confirms ServiceNow half works independent of BCM. - Register the action in ''cmsh'' (Part 3). - Identify the exact measurable via ''measurable; list'' (Part 4) — confirm fraction vs. percentage before setting any threshold. - Create the trigger with ''Enter actions'' pointed at the action (Part 5). - Trigger a real condition (service stop, load generation) and confirm: * ''latesthealthdata'' / ''latestmetricdata'' shows the expected state on the correct device * ''/var/log/cmd-servicenow-status.log'' shows a fresh ''HTTP_STATUS:201'' * The incident in ServiceNow shows **real** node/check values, not "unknown" (confirms Part 6 env vars are wired correctly) - Revert any test-only changes (service autostart, temporary thresholds, debug lines in the script). - Restore the trigger to a realistic production threshold if it was temporarily lowered for faster testing. ---- ===== Part 10: Adding Deduplication Logic ===== #!/bin/bash # BCM -> ServiceNow incident creation (OAuth2 client_credentials) # # Config files (both under /cm/local/apps/cmd/scripts/actions/, chmod 600, root-owned): # servicenow-shared.env - SN_INSTANCE / SN_CLIENT_ID / SN_CLIENT_SECRET # (same across every client deployment) # servicenow-company.env - SN_COMPANY_NAME # (the one value that differs per client deployment) SHARED_CONFIG_FILE="/cm/local/apps/cmd/scripts/actions/servicenow-shared.env" COMPANY_CONFIG_FILE="/cm/local/apps/cmd/scripts/actions/servicenow-company.env" LOG=/var/log/cmd-servicenow-status.log log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG" } # --------------------------------------------------------------------------- # Load and validate config # --------------------------------------------------------------------------- if [ ! -f "$SHARED_CONFIG_FILE" ]; then log "FATAL: shared config file not found at $SHARED_CONFIG_FILE" exit 1 fi if [ ! -f "$COMPANY_CONFIG_FILE" ]; then log "FATAL: company config file not found at $COMPANY_CONFIG_FILE" exit 1 fi # shellcheck disable=SC1090 source "$SHARED_CONFIG_FILE" # shellcheck disable=SC1090 source "$COMPANY_CONFIG_FILE" if [ -z "$SN_INSTANCE" ] || [ -z "$SN_CLIENT_ID" ] || [ -z "$SN_CLIENT_SECRET" ]; then log "FATAL: SN_INSTANCE / SN_CLIENT_ID / SN_CLIENT_SECRET missing from $SHARED_CONFIG_FILE" exit 1 fi if [ -z "$SN_COMPANY_NAME" ]; then log "FATAL: SN_COMPANY_NAME is required but not set in $COMPANY_CONFIG_FILE — refusing to create an unattributed incident" exit 1 fi # --------------------------------------------------------------------------- # Alert context from CMDaemon # --------------------------------------------------------------------------- NODE="${CMD_ENTITY_NAME:-unknown-node}" CHECK="${CMD_MEASURABLE_NAME:-unknown-check}" SEVERITY="${CMD_SEVERITY:-unknown}" VALUE="${CMD_VALUE:-N/A}" # --------------------------------------------------------------------------- # Step 1: get access token # --------------------------------------------------------------------------- TOKEN_RESPONSE=$(curl -s -X POST "${SN_INSTANCE}/oauth_token.do" \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=client_credentials' \ --data-urlencode "client_id=${SN_CLIENT_ID}" \ --data-urlencode "client_secret=${SN_CLIENT_SECRET}") ACCESS_TOKEN=$(echo "${TOKEN_RESPONSE}" | jq -r '.access_token') if [ -z "${ACCESS_TOKEN}" ] || [ "${ACCESS_TOKEN}" = "null" ]; then log "Failed to get access token: ${TOKEN_RESPONSE}" exit 1 fi # --------------------------------------------------------------------------- # Step 2: resolve Company Name -> sys_id (core_company table) # --------------------------------------------------------------------------- COMPANY_SYSID="" COMPANY_LOOKUP_FAILED=no COMPANY_RESPONSE=$(curl -s -w "\n%{http_code}" \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -G "${SN_INSTANCE}/api/now/table/core_company" \ --data-urlencode "sysparm_query=name=${SN_COMPANY_NAME}" \ --data-urlencode "sysparm_fields=sys_id,name" \ --data-urlencode "sysparm_limit=1") COMPANY_HTTP_STATUS=$(echo "$COMPANY_RESPONSE" | tail -n1) COMPANY_BODY=$(echo "$COMPANY_RESPONSE" | sed '$d') if [ "$COMPANY_HTTP_STATUS" = "200" ]; then COMPANY_SYSID=$(echo "$COMPANY_BODY" | jq -r '.result[0].sys_id // empty') fi if [ -z "$COMPANY_SYSID" ]; then COMPANY_LOOKUP_FAILED=yes log "WARNING: could not resolve company '${SN_COMPANY_NAME}' to a sys_id (HTTP ${COMPANY_HTTP_STATUS}). Incident will be created without a company link and tagged for review." 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 "new" issue and defeat deduplication entirely. # --------------------------------------------------------------------------- DEDUP_LABEL="check=${CHECK} node=${NODE} company=${SN_COMPANY_NAME}" # --------------------------------------------------------------------------- # 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="short_descriptionLIKE[BCM-ALERT]^short_descriptionLIKECheck: ${CHECK} |^short_descriptionLIKENode: ${NODE} |" if [ -n "$COMPANY_SYSID" ]; then DEDUP_QUERY="${DEDUP_QUERY}^company=${COMPANY_SYSID}" fi DEDUP_QUERY="${DEDUP_QUERY}^ORDERBYDESCsys_updated_on" for attempt in 1 2 3; do DEDUP_RESPONSE=$(curl -s -w "\n%{http_code}" \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -G "${SN_INSTANCE}/api/now/table/incident" \ --data-urlencode "sysparm_query=${DEDUP_QUERY}" \ --data-urlencode "sysparm_fields=sys_id,number,sys_updated_on,state" \ --data-urlencode "sysparm_display_value=true" \ --data-urlencode "sysparm_limit=1") DEDUP_HTTP_STATUS=$(echo "$DEDUP_RESPONSE" | tail -n1) DEDUP_BODY=$(echo "$DEDUP_RESPONSE" | sed '$d') if [ "$DEDUP_HTTP_STATUS" = "200" ]; then MATCH_COUNT=$(echo "$DEDUP_BODY" | jq '.result | length') if [ "$MATCH_COUNT" -gt 0 ]; then TOP_MATCH_STATE=$(echo "$DEDUP_BODY" | jq -r '.result[0].state') if [ "$TOP_MATCH_STATE" = "Resolved" ]; then # 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 "$DEDUP_BODY" | jq -r '.result[0].sys_id') EXISTING_NUMBER=$(echo "$DEDUP_BODY" | jq -r '.result[0].number') fi fi DEDUP_CHECK_FAILED=no break else DEDUP_CHECK_FAILED=yes log "WARNING: dedup check attempt ${attempt}/3 failed (HTTP ${DEDUP_HTTP_STATUS}), retrying..." sleep 2 fi done if [ "$DEDUP_CHECK_FAILED" = "yes" ]; then log "WARNING: dedup check failed after 3 attempts for ${DEDUP_LABEL}. Failing open — will create a new incident tagged as unverified for duplicates." fi # --------------------------------------------------------------------------- # Case A: a genuine duplicate was found -> bump it, don't create a new one. # --------------------------------------------------------------------------- if [ -n "$EXISTING_SYSID" ]; then NOTE_TEXT="Recurred again at $(date '+%Y-%m-%d %H:%M:%S %Z'). Node: ${NODE}, Check: ${CHECK}, Severity: ${SEVERITY}, Value: ${VALUE}." curl -s -o /dev/null \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -X PATCH "${SN_INSTANCE}/api/now/table/incident/${EXISTING_SYSID}" \ -d "{\"work_notes\": \"${NOTE_TEXT}\"}" >> "$LOG" 2>&1 log "Duplicate suppressed for ${DEDUP_LABEL} — bumped existing incident ${EXISTING_NUMBER} with a work note instead of creating a new one." exit 0 fi # --------------------------------------------------------------------------- # Case B: no duplicate (or dedup check failed open) -> create a new incident. # --------------------------------------------------------------------------- SHORT_DESC="[BCM-ALERT] Check: ${CHECK} | Node: ${NODE} | Value: ${VALUE}" DESC="Health check/metric ${CHECK} on ${NODE}, THIS IS A TEST INCIDENT" CONFIG_ITEM="${SN_COMPANY_NAME} - ${NODE}" if [ "$DEDUP_CHECK_FAILED" = "yes" ]; then DESC="${DESC} [DEDUP CHECK UNVERIFIED — ServiceNow query failed after retries; this may be a duplicate, please review.]" fi if [ "$COMPANY_LOOKUP_FAILED" = "yes" ]; then DESC="${DESC} [COMPANY LOOKUP FAILED — no company record matched '${SN_COMPANY_NAME}'; incident created without a company link.]" fi # Build the JSON payload, only including "company" if we actually resolved one. if [ -n "$COMPANY_SYSID" ]; then PAYLOAD=$(jq -n \ --arg short_description "$SHORT_DESC" \ --arg description "$DESC" \ --arg company "$COMPANY_SYSID" \ --arg configuration_item "$CONFIG_ITEM" \ '{short_description: $short_description, description: $description, assignment_group: "SO-NOC-CLDAI", priority: "3", severity: "3", company: $company, configuration_item: $configuration_item}') else PAYLOAD=$(jq -n \ --arg short_description "$SHORT_DESC" \ --arg description "$DESC" \ --arg configuration_item "$CONFIG_ITEM" \ '{short_description: $short_description, description: $description, assignment_group: "SO-NOC-CLDAI", priority: "3", severity: "3", configuration_item: $configuration_item}') fi curl -s -w "\nHTTP_STATUS:%{http_code}\n" \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -X POST "${SN_INSTANCE}/api/now/table/incident" \ -d "$PAYLOAD" >> "$LOG" 2>&1 log "Created new incident for ${DEDUP_LABEL}" ===== Known Time Sinks to Avoid Next Time ===== * Don't guess ''cmsh'' parameter/value names — ''set ?'' and tab-completion resolve this in seconds versus multiple failed attempts. * Don't assume a metric is 0–100 — check ''Minimum''/''Maximum'' on the measurable first. * Don't assume the machine you're SSHed into is the device BCM refers to by the same name — confirm via ''device; list'' and IP comparison before spending time debugging "missing" metrics. * Don't guess CMDaemon's passed environment variable names — dump ''env'' once inside a real trigger firing and read the real names directly. * ''use'' vs ''add'' vs ''use "quoted role-tagged key"'' varies by submode in cmsh — when ''use '' fails with "Unable to find," try ''add '' before assuming the object doesn't exist.