network/gaiia/dhcp_opt82_workflow.json
2026-07-19 14:42:02 -05:00

227 lines
13 KiB
JSON

{
"name": "DHCP Option 82 Auth",
"description": "WEBHOOK: Always upserts IP. If Option 82 present AND MAC linked to an account, syncs all inventoried MACs to Preseem with plan details.",
"trigger": {
"type": "WEBHOOK",
"inputJsonSchema": {
"type": "object",
"required": ["bound", "mac", "ip", "server", "remoteId"],
"properties": {
"bound": { "type": "string" },
"mac": { "type": "string" },
"ip": { "type": "string" },
"server": { "type": "string" },
"remoteId": { "type": "string" }
}
},
"nextSteps": [{ "slug": "check-inventory" }]
},
"steps": [
{
"name": "Find inventory item by MAC",
"slug": "check-inventory",
"type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST",
"nodeSlug": "gaiia-internal-api-gateway-graphql-request",
"allowSkipExecution": false,
"outputMapper": "({ output, state }) => {\n const items = output.response.findInventoryItemsFromDeviceIdentifiers;\n // Accept items assigned to Accounts OR NetworkSites\n const assigned = items.filter(\n (item) => {\n const type = item.inventoryItem?.assignation?.assignee?.__typename;\n return type === 'Account' || type === 'NetworkSite';\n }\n );\n if (assigned.length === 0) return null;\n const inv = assigned[0].inventoryItem;\n const assignee = inv.assignation.assignee;\n return {\n assigneeId: assignee.id,\n assigneeType: assignee.__typename,\n assigneeName: assignee.name || ''\n };\n}",
"inputs": [
{
"key": "query",
"type": "LITERAL",
"value": "string:query FindItem($input: FindInventoryItemsFromDeviceIdentifiersQueryInput!) {\n findInventoryItemsFromDeviceIdentifiers(input: $input) {\n identifier\n inventoryItem {\n id\n model { name }\n assignation {\n assignee {\n __typename\n ... on Account { id name }\n }\n }\n }\n }\n}"
},
{
"key": "variables",
"type": "FUNCTION",
"value": "({ state }) => {\n const rawMac = state.input.mac;\n const cleanedMac = rawMac.startsWith('1:') ? rawMac.slice(2) : rawMac;\n const formattedMac = cleanedMac\n .split(':')\n .map((segment) => segment.padStart(2, '0'))\n .join(':')\n .toUpperCase();\n return { input: { identifiers: [formattedMac] } };\n}"
}
],
"nextSteps": [{ "slug": "is-connect" }]
},
{
"name": "Connect or disconnect?",
"slug": "is-connect",
"type": "CONDITION",
"nodeSlug": "condition",
"condition": "({ secrets, state, utils, variables }) => {\n return state.input.bound === '1';\n}",
"elseSteps": [{ "slug": "expire-ip" }],
"thenSteps": [{ "slug": "has-account-connect" }]
},
{
"name": "Account found? (connect)",
"slug": "has-account-connect",
"type": "CONDITION",
"nodeSlug": "condition",
"condition": "({ secrets, state, utils, variables }) => {\n return state.steps['check-inventory'].output != null;\n}",
"elseSteps": [{ "slug": "upsert-ip-no-account" }],
"thenSteps": [{ "slug": "upsert-ip-with-account" }]
},
{
"name": "Upsert IP (no account)",
"slug": "upsert-ip-no-account",
"type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST",
"nodeSlug": "gaiia-internal-api-gateway-graphql-request",
"allowSkipExecution": false,
"inputs": [
{
"key": "query",
"type": "LITERAL",
"value": "string:mutation UpsertIp($input: UpsertMacAddressIpAddressAssignationsInput!) {\n upsertMacAddressIpAddressAssignations(input: $input) {\n assignations { macAddress ipAddress }\n }\n}"
},
{
"key": "variables",
"type": "FUNCTION",
"value": "({ state }) => {\n const rawMac = state.input.mac;\n const cleanedMac = rawMac.startsWith('1:') ? rawMac.slice(2) : rawMac;\n const formattedMac = cleanedMac\n .split(':')\n .map((segment) => segment.padStart(2, '0'))\n .join(':')\n .toUpperCase();\n return {\n input: {\n assignations: { macAddress: formattedMac, ipAddress: state.input.ip, expired: false }\n }\n };\n}"
}
],
"nextSteps": [{ "slug": "end" }]
},
{
"name": "Upsert IP (with account)",
"slug": "upsert-ip-with-account",
"type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST",
"nodeSlug": "gaiia-internal-api-gateway-graphql-request",
"allowSkipExecution": false,
"inputs": [
{
"key": "query",
"type": "LITERAL",
"value": "string:mutation UpsertIp($input: UpsertMacAddressIpAddressAssignationsInput!) {\n upsertMacAddressIpAddressAssignations(input: $input) {\n assignations { macAddress ipAddress }\n }\n}"
},
{
"key": "variables",
"type": "FUNCTION",
"value": "({ state }) => {\n const rawMac = state.input.mac;\n const cleanedMac = rawMac.startsWith('1:') ? rawMac.slice(2) : rawMac;\n const formattedMac = cleanedMac\n .split(':')\n .map((segment) => segment.padStart(2, '0'))\n .join(':')\n .toUpperCase();\n return {\n input: {\n assignations: { macAddress: formattedMac, ipAddress: state.input.ip, expired: false }\n }\n };\n}"
}
],
"nextSteps": [{ "slug": "should-sync-preseem" }]
},
{
"name": "Has Option 82 and account?",
"slug": "should-sync-preseem",
"type": "CONDITION",
"nodeSlug": "condition",
"condition": "({ secrets, state, utils, variables }) => {\n const inv = state.steps['check-inventory'].output;\n if (inv == null) return false;\n if (!state.input.remoteId || state.input.remoteId === '') return false;\n return inv.assigneeType === 'Account';\n}",
"elseSteps": [{ "slug": "should-sync-site" }],
"thenSteps": [{ "slug": "get-account-macs-and-plan" }]
},
{
"name": "Get all inventoried MACs and plan",
"slug": "get-account-macs-and-plan",
"type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST",
"nodeSlug": "gaiia-public-api-gateway-graphql-request",
"allowSkipExecution": false,
"outputMapper": "({ output, state }) => {\n const account = output.response.account;\n if (!account) {\n workflow.fail('Account not found', 'ACCOUNT_NOT_FOUND');\n }\n const macs = [];\n for (const assign of (account.assignedInventoryItems?.nodes || [])) {\n const inv = assign.inventoryItem || {};\n for (const f of (inv.fields?.nodes || [])) {\n const fieldName = (f.modelField?.name || '').toLowerCase();\n const data = (f.data || '').trim();\n if (fieldName.includes('mac') && data && data !== '00:00:00:00:00:00') {\n const cleaned = data.startsWith('1:') ? data.slice(2) : data;\n const parts = cleaned.split(':');\n if (parts.length === 6) {\n macs.push(parts.map((p) => p.padStart(2, '0').toUpperCase()).join(':'));\n }\n }\n }\n for (const a of (inv.ipAddressAssignments?.nodes || [])) {\n if (a.macAddress) macs.push(a.macAddress.toUpperCase());\n }\n }\n const plan = (account.billingSubscriptions?.nodes || [])\n .map((s) => s.productVersion?.product?.slug)\n .find(Boolean) || '';\n return { macs: [...new Set(macs)], plan, gaiiaId: account.readableId };\n}",
"inputs": [
{
"key": "query",
"type": "FUNCTION",
"value": "({ state }) => {\n const accountId = state.steps['check-inventory'].output.accountId;\n return `query { account(id: \"${accountId}\") { readableId assignedInventoryItems { nodes { inventoryItem { fields { nodes { data modelField { name } } } ipAddressAssignments { nodes { macAddress } } } } } billingSubscriptions { nodes { productVersion { product { slug } } } } } }`;\n}"
},
{
"key": "variables",
"type": "FUNCTION",
"value": "({ state }) => ({})"
}
],
"nextSteps": [{ "slug": "preseem-update" }]
},
{
"name": "Update Preseem",
"slug": "preseem-update",
"type": "PRESEEM_REST_REQUEST",
"nodeSlug": "preseem-rest-request",
"allowSkipExecution": false,
"inputs": [
{
"key": "method",
"type": "LITERAL",
"value": "string:PUT"
},
{
"key": "path",
"type": "FUNCTION",
"value": "({ state }) => {\n const { gaiiaId } = state.steps['get-account-macs-and-plan'].output;\n return `/services/${gaiiaId}`;\n}"
},
{
"key": "body",
"type": "FUNCTION",
"value": "({ state }) => {\n const { gaiiaId, macs, plan } = state.steps['get-account-macs-and-plan'].output;\n const rawMac = state.input.mac;\n const cleanedMac = rawMac.startsWith('1:') ? rawMac.slice(2) : rawMac;\n const connectingMac = cleanedMac\n .split(':')\n .map((segment) => segment.padStart(2, '0'))\n .join(':')\n .toLowerCase();\n const framedIp = state.input.ip;\n const inventoriedMacs = macs.map((m) => m.toLowerCase());\n const attachments = inventoriedMacs\n .filter((m) => state.input.bound === '0' || m !== connectingMac)\n .map((m) => ({ cpe_mac: m }));\n if (state.input.bound === '1') {\n attachments.push({ cpe_mac: connectingMac, network_prefixes: [framedIp] });\n }\n return {\n id: String(gaiiaId),\n attachments,\n account: String(gaiiaId),\n package: plan\n };\n}"
}
],
"nextSteps": [{ "slug": "end" }]
},
{
"name": "Expire IP",
"slug": "expire-ip",
"type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST",
"nodeSlug": "gaiia-internal-api-gateway-graphql-request",
"allowSkipExecution": false,
"inputs": [
{
"key": "query",
"type": "LITERAL",
"value": "string:mutation ExpireIp($input: UpsertMacAddressIpAddressAssignationsInput!) {\n upsertMacAddressIpAddressAssignations(input: $input) {\n assignations { macAddress ipAddress }\n }\n}"
},
{
"key": "variables",
"type": "FUNCTION",
"value": "({ state }) => {\n const rawMac = state.input.mac;\n const cleanedMac = rawMac.startsWith('1:') ? rawMac.slice(2) : rawMac;\n const formattedMac = cleanedMac\n .split(':')\n .map((segment) => segment.padStart(2, '0'))\n .join(':')\n .toUpperCase();\n return {\n input: {\n assignations: { macAddress: formattedMac, ipAddress: state.input.ip, expired: true }\n }\n };\n}"
}
],
"nextSteps": [{ "slug": "should-sync-preseem-disconnect" }]
},
{
"name": "Network site with Option 82?",
"slug": "should-sync-site",
"type": "CONDITION",
"nodeSlug": "condition",
"condition": "({ secrets, state, utils, variables }) => {\n const inv = state.steps['check-inventory'].output;\n if (inv == null) return false;\n if (!state.input.remoteId || state.input.remoteId === '') return false;\n return inv.assigneeType === 'NetworkSite';\n}",
"elseSteps": [{ "slug": "end" }],
"thenSteps": [{ "slug": "preseem-site-update" }]
},
{
"name": "Update Preseem (site)",
"slug": "preseem-site-update",
"type": "PRESEEM_REST_REQUEST",
"nodeSlug": "preseem-rest-request",
"allowSkipExecution": false,
"inputs": [
{
"key": "method",
"type": "LITERAL",
"value": "string:PUT"
},
{
"key": "path",
"type": "FUNCTION",
"value": "({ state }) => {\n const inv = state.steps['check-inventory'].output;\n return `/access_points/${inv.assigneeId}`;\n}"
},
{
"key": "body",
"type": "FUNCTION",
"value": "({ state }) => {\n const inv = state.steps['check-inventory'].output;\n const rawMac = state.input.mac;\n const cleanedMac = rawMac.startsWith('1:') ? rawMac.slice(2) : rawMac;\n const mac = cleanedMac\n .split(':')\n .map((segment) => segment.padStart(2, '0'))\n .join(':')\n .toLowerCase();\n return {\n id: inv.assigneeId,\n name: inv.assigneeName || inv.assigneeId,\n tower: inv.assigneeName || '',\n ip_address: state.input.ip\n };\n}"
}
],
"nextSteps": [{ "slug": "end" }]
},
{
"name": "Has Option 82 and account? (disconnect)",
"slug": "should-sync-preseem-disconnect",
"type": "CONDITION",
"nodeSlug": "condition",
"condition": "({ secrets, state, utils, variables }) => {\n const inv = state.steps['check-inventory'].output;\n if (inv == null) return false;\n if (!state.input.remoteId || state.input.remoteId === '') return false;\n return inv.assigneeType === 'Account';\n}",
"elseSteps": [{ "slug": "should-sync-site-disconnect" }],
"thenSteps": [{ "slug": "get-account-macs-and-plan" }]
},
{
"name": "Network site? (disconnect)",
"slug": "should-sync-site-disconnect",
"type": "CONDITION",
"nodeSlug": "condition",
"condition": "({ secrets, state, utils, variables }) => {\n const inv = state.steps['check-inventory'].output;\n if (inv == null) return false;\n if (!state.input.remoteId || state.input.remoteId === '') return false;\n return inv.assigneeType === 'NetworkSite';\n}",
"elseSteps": [{ "slug": "end" }],
"thenSteps": [{ "slug": "preseem-site-update" }]
}
]
}