Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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);
Loading