diff --git a/Server-Side Components/Business Rules/Enforce CI maintenance window on Change schedule/README.md b/Server-Side Components/Business Rules/Enforce CI maintenance window on Change schedule/README.md new file mode 100644 index 0000000000..8ee0c64989 --- /dev/null +++ b/Server-Side Components/Business Rules/Enforce CI maintenance window on Change schedule/README.md @@ -0,0 +1,36 @@ +# Enforce CI maintenance window on Change schedule + +## What this solves +Change requests are sometimes scheduled outside the maintenance windows of the affected CIs, causing risky or blocked implementations. This rule validates the planned start and end times of a Change against the maintenance schedules of its related CIs and blocks the update if none of the CIs allow that window. + +## Where to use +- Table: `change_request` +- When: before insert and before update +- Order: early (for example 50) + +## How it works +- Looks up CIs related to the Change via `task_ci` +- For each CI with a defined `maintenance_schedule` (reference to `cmn_schedule`), uses `GlideSchedule.isInSchedule` to verify the planned start and end are inside the window +- If at least one CI’s maintenance schedule permits the window, the Change is allowed +- If no related CI permits the window, the rule aborts the action with a clear message +- Behaviour for CIs without a defined maintenance schedule is configurable + +## Configure +At the top of the script: +- `BLOCK_WHEN_NO_SCHEDULE`: if true, treat CIs without a maintenance schedule as non-compliant +- `REQUIRE_BOTH_BOUNDARIES`: if true, both planned start and planned end must be inside a permitted window +- `TIMEZONE`: optional IANA time zone string (for example `Europe/London`); leave blank to use the schedule or instance default + +## Notes +- If your process requires all CIs to permit the window, change `anyPass` logic to `allPass` +- This rule checks only maintenance windows defined on the CI record. If you store schedules at the Business Service level, adapt the CI lookup accordingly + +## References +- GlideSchedule API + https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideSchedule/concept/c_GlideScheduleAPI.html +- GlideDateTime API + https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideDateTime/concept/c_GlideDateTimeAPI.html +- Change Management fields + https://www.servicenow.com/docs/bundle/zurich-it-service-management/page/product/change-management/concept/change-management-overview.html +- Task CI relationship (`task_ci`) + https://www.servicenow.com/docs/bundle/zurich-servicenow-platform/page/product/configuration-management/reference/task-ci.html diff --git a/Server-Side Components/Business Rules/Enforce CI maintenance window on Change schedule/br_enforce_ci_maintenance_window.js b/Server-Side Components/Business Rules/Enforce CI maintenance window on Change schedule/br_enforce_ci_maintenance_window.js new file mode 100644 index 0000000000..1eeab52539 --- /dev/null +++ b/Server-Side Components/Business Rules/Enforce CI maintenance window on Change schedule/br_enforce_ci_maintenance_window.js @@ -0,0 +1,87 @@ +// Business Rule: Enforce CI maintenance window on Change schedule +// Table: change_request | When: before insert, before update + +(function executeRule(current, previous /*null*/) { + // ===== Configuration ===== + var BLOCK_WHEN_NO_SCHEDULE = false; // if true, a CI without maintenance_schedule causes failure + var REQUIRE_BOTH_BOUNDARIES = true; // if true, both planned start and end must be inside maintenance window + var TIMEZONE = 'Europe/London'; // optional; '' to use schedule/instance default + // ========================= + + try { + // Only run when dates are meaningful + if (!current.planned_start_date || !current.planned_end_date) return; + + // Build GDTs once + var psd = new GlideDateTime(current.planned_start_date.getDisplayValue()); + var ped = new GlideDateTime(current.planned_end_date.getDisplayValue()); + if (psd.after(ped)) { + gs.addErrorMessage('Planned start is after planned end. Please correct the schedule.'); + current.setAbortAction(true); + return; + } + + // Collect related CIs for this Change + var ciIds = []; + var tci = new GlideRecord('task_ci'); + tci.addQuery('task', current.getUniqueValue()); + tci.query(); + while (tci.next()) ciIds.push(String(tci.getValue('ci_item'))); + + if (ciIds.length === 0) { + // No CIs; nothing to validate + return; + } + + var anyPass = false; + var missingScheduleCount = 0; + var evaluated = 0; + + // Evaluate each CI's maintenance schedule + var ci = new GlideRecord('cmdb_ci'); + ci.addQuery('sys_id', 'IN', ciIds.join(',')); + ci.query(); + + while (ci.next()) { + evaluated++; + + var schedRef = ci.getValue('maintenance_schedule'); + if (!schedRef) { + missingScheduleCount++; + continue; + } + + var sched = new GlideSchedule(schedRef, TIMEZONE || ''); + var startOK = sched.isInSchedule(psd); + var endOK = sched.isInSchedule(ped); + + var pass = REQUIRE_BOTH_BOUNDARIES ? (startOK && endOK) : (startOK || endOK); + if (pass) { + anyPass = true; + break; // at least one CI permits this window + } + } + + // Handle missing schedules according to policy + if (!anyPass) { + var hasBlockingNoSchedule = BLOCK_WHEN_NO_SCHEDULE && missingScheduleCount > 0; + if (hasBlockingNoSchedule || evaluated > 0) { + gs.addErrorMessage(buildMessage()); + current.setAbortAction(true); + } + } + + function buildMessage() { + var parts = []; + parts.push('Planned window does not fall inside any related CI maintenance schedules.'); + if (REQUIRE_BOTH_BOUNDARIES) parts.push('Both start and end must be inside a permitted window.'); + if (missingScheduleCount > 0) { + parts.push((BLOCK_WHEN_NO_SCHEDULE ? 'Blocking' : 'Ignoring') + ' ' + missingScheduleCount + ' CI(s) with no maintenance schedule.'); + } + return parts.join(' '); + } + } catch (e) { + gs.error('Maintenance window validation failed: ' + e.message); + // Be safe: do not block due to a runtime error + } +})(current, previous);