228 lines
8.1 KiB
Ruby
228 lines
8.1 KiB
Ruby
# Change Management setup for Zammad (ITIL option 1)
|
|
# Run: rails runner change_mgmt.rb
|
|
UserInfo.current_user_id = 1
|
|
out = []
|
|
|
|
def log(out, msg); out << msg; puts msg; end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. GROUP
|
|
# ---------------------------------------------------------------------------
|
|
grp = Group.find_by(name: 'Change Management')
|
|
if grp.nil?
|
|
grp = Group.create!(
|
|
name: 'Change Management',
|
|
note: 'ITIL Change Management workflow',
|
|
active: true
|
|
)
|
|
log(out, "GROUP created id=#{grp.id}")
|
|
else
|
|
log(out, "GROUP exists id=#{grp.id}")
|
|
end
|
|
|
|
# Grant admin (user 1) full access to the group
|
|
admin = User.find(1)
|
|
admin.group_names_access_map = (admin.group_names_access_map || {}).merge(
|
|
'Change Management' => ['full']
|
|
)
|
|
admin.save!
|
|
log(out, "GROUP access granted to admin")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. STATES (change state machine)
|
|
# ---------------------------------------------------------------------------
|
|
open_type = Ticket::StateType.find_by(name: 'open')
|
|
pending_type = Ticket::StateType.find_by(name: 'pending action')
|
|
closed_type = Ticket::StateType.find_by(name: 'closed')
|
|
|
|
change_states = [
|
|
{ name: 'CM: Requested', type: open_type, order: 100, note: 'Change request submitted, awaiting CAB' },
|
|
{ name: 'CM: In Review', type: open_type, order: 101, note: 'Under CAB assessment' },
|
|
{ name: 'CM: Approved', type: open_type, order: 102, note: 'Approved by CAB, ready to schedule' },
|
|
{ name: 'CM: Scheduled', type: pending_type, order: 103, note: 'Approved and scheduled for a window' },
|
|
{ name: 'CM: Implementing', type: open_type, order: 104, note: 'Change in progress' },
|
|
{ name: 'CM: Review', type: open_type, order: 105, note: 'Post-implementation review' },
|
|
{ name: 'CM: Closed', type: closed_type, order: 106, note: 'Change complete and closed' },
|
|
{ name: 'CM: Rejected', type: closed_type, order: 107, note: 'Change rejected by CAB' },
|
|
{ name: 'CM: Failed', type: closed_type, order: 108, note: 'Change failed / rolled back' },
|
|
]
|
|
|
|
state_ids = {}
|
|
change_states.each do |s|
|
|
st = Ticket::State.find_by(name: s[:name])
|
|
if st.nil?
|
|
st = Ticket::State.create!(
|
|
name: s[:name],
|
|
state_type_id: s[:type].id,
|
|
note: s[:note],
|
|
active: true,
|
|
created_by_id: 1,
|
|
updated_by_id: 1
|
|
)
|
|
log(out, "STATE created #{s[:name]} id=#{st.id}")
|
|
else
|
|
log(out, "STATE exists #{s[:name]} id=#{st.id}")
|
|
end
|
|
state_ids[s[:name]] = st.id
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. OBJECT FIELDS (custom ticket attributes for change data)
|
|
# ---------------------------------------------------------------------------
|
|
def add_attr(out, name, display, data_type, data_option)
|
|
existing = ObjectManager::Attribute.get(object: 'Ticket', name: name)
|
|
if existing
|
|
log(out, "ATTR exists #{name}")
|
|
return
|
|
end
|
|
ObjectManager::Attribute.add(
|
|
object: 'Ticket',
|
|
name: name,
|
|
display: display,
|
|
data_type: data_type,
|
|
data_option: data_option,
|
|
active: true,
|
|
screens: {
|
|
create_middle: { '-all-' => { null: true, shown: true } },
|
|
edit: { '-all-' => { null: true, shown: true } },
|
|
},
|
|
position: 800,
|
|
editable: true
|
|
)
|
|
log(out, "ATTR created #{name}")
|
|
end
|
|
|
|
add_attr(out, 'change_type', 'Change Type', 'select', {
|
|
default: 'normal',
|
|
options: { 'standard' => 'Standard (pre-approved)', 'normal' => 'Normal', 'emergency' => 'Emergency' },
|
|
nulloption: false, maxlength: 255, null: false
|
|
})
|
|
|
|
add_attr(out, 'change_risk', 'Risk Level', 'select', {
|
|
default: 'medium',
|
|
options: { 'low' => 'Low', 'medium' => 'Medium', 'high' => 'High' },
|
|
nulloption: false, maxlength: 255, null: false
|
|
})
|
|
|
|
add_attr(out, 'change_impact', 'Impact', 'select', {
|
|
default: 'medium',
|
|
options: { 'low' => 'Low', 'medium' => 'Medium', 'high' => 'High' },
|
|
nulloption: false, maxlength: 255, null: false
|
|
})
|
|
|
|
add_attr(out, 'change_rollback', 'Rollback Plan', 'textarea', {
|
|
type: 'textarea', maxlength: 5000, rows: 4, null: true
|
|
})
|
|
|
|
add_attr(out, 'change_start', 'Scheduled Start', 'datetime', {
|
|
future: true, past: false, diff: 0, null: true
|
|
})
|
|
|
|
add_attr(out, 'change_end', 'Scheduled End', 'datetime', {
|
|
future: true, past: false, diff: 0, null: true
|
|
})
|
|
|
|
add_attr(out, 'change_cab_decision', 'CAB Decision', 'select', {
|
|
default: 'pending',
|
|
options: { 'pending' => 'Pending', 'approved' => 'Approved', 'rejected' => 'Rejected' },
|
|
nulloption: false, maxlength: 255, null: false
|
|
})
|
|
|
|
# Apply the DB migration for the new columns
|
|
if ObjectManager::Attribute.pending_migration?
|
|
ObjectManager::Attribute.migration_execute
|
|
log(out, "MIGRATION executed")
|
|
else
|
|
log(out, "MIGRATION not needed")
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. OVERVIEWS (saved views for the Change Management group)
|
|
# ---------------------------------------------------------------------------
|
|
cm_group = Group.find_by(name: 'Change Management')
|
|
|
|
def add_overview(out, name, link, condition, order)
|
|
ov = Overview.find_by(link: link)
|
|
if ov
|
|
log(out, "OVERVIEW exists #{name}")
|
|
return
|
|
end
|
|
Overview.create!(
|
|
name: name,
|
|
link: link,
|
|
prio: order,
|
|
role_ids: Role.where(name: ['Admin', 'Agent']).pluck(:id),
|
|
condition: condition,
|
|
order: { by: 'created_at', direction: 'DESC' },
|
|
view: { d: %w[number title change_type change_risk state created_at], s: %w[number title change_type state created_at], m: %w[number title state] },
|
|
active: true,
|
|
created_by_id: 1,
|
|
updated_by_id: 1
|
|
)
|
|
log(out, "OVERVIEW created #{name}")
|
|
end
|
|
|
|
add_overview(out, 'CM: Pending CAB', 'cm_pending_cab', {
|
|
'ticket.group_id' => { operator: 'is', value: [cm_group.id.to_s] },
|
|
'ticket.change_cab_decision' => { operator: 'is', value: ['pending'] },
|
|
'ticket.state_id' => { operator: 'is not', value: Ticket::State.where(name: ['CM: Closed', 'CM: Rejected', 'CM: Failed']).pluck(:id).map(&:to_s) },
|
|
}, 1000)
|
|
|
|
add_overview(out, 'CM: Scheduled', 'cm_scheduled', {
|
|
'ticket.group_id' => { operator: 'is', value: [cm_group.id.to_s] },
|
|
'ticket.state_id' => { operator: 'is', value: Ticket::State.where(name: ['CM: Scheduled', 'CM: Approved']).pluck(:id).map(&:to_s) },
|
|
}, 1001)
|
|
|
|
add_overview(out, 'CM: All Open', 'cm_all_open', {
|
|
'ticket.group_id' => { operator: 'is', value: [cm_group.id.to_s] },
|
|
'ticket.state_id' => { operator: 'is not', value: Ticket::State.where(name: ['CM: Closed', 'CM: Rejected', 'CM: Failed']).pluck(:id).map(&:to_s) },
|
|
}, 1002)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. TRIGGERS (approval gate + notifications)
|
|
# ---------------------------------------------------------------------------
|
|
def add_trigger(out, name, condition, perform)
|
|
tr = Trigger.find_by(name: name)
|
|
if tr
|
|
log(out, "TRIGGER exists #{name}")
|
|
return
|
|
end
|
|
Trigger.create!(
|
|
name: name,
|
|
condition: condition,
|
|
perform: perform,
|
|
active: true,
|
|
created_by_id: 1,
|
|
updated_by_id: 1
|
|
)
|
|
log(out, "TRIGGER created #{name}")
|
|
end
|
|
|
|
approved_state = Ticket::State.find_by(name: 'CM: Approved')
|
|
add_trigger(out, 'CM: CAB approved to Approved', {
|
|
'ticket.group_id' => { operator: 'is', value: cm_group.id.to_s },
|
|
'ticket.change_cab_decision' => { operator: 'is', value: 'approved' },
|
|
}, {
|
|
'ticket.state_id' => { value: approved_state.id.to_s },
|
|
'notification.email' => {
|
|
recipient: 'ticket_owner',
|
|
subject: 'Change #{ticket.number} approved by CAB',
|
|
body: 'Change request "#{ticket.title}" has been approved. Proceed to schedule and implement.'
|
|
}
|
|
})
|
|
|
|
rejected_state = Ticket::State.find_by(name: 'CM: Rejected')
|
|
add_trigger(out, 'CM: CAB rejected to Rejected', {
|
|
'ticket.group_id' => { operator: 'is', value: cm_group.id.to_s },
|
|
'ticket.change_cab_decision' => { operator: 'is', value: 'rejected' },
|
|
}, {
|
|
'ticket.state_id' => { value: rejected_state.id.to_s },
|
|
'notification.email' => {
|
|
recipient: 'ticket_owner',
|
|
subject: 'Change #{ticket.number} rejected by CAB',
|
|
body: 'Change request "#{ticket.title}" was rejected. See ticket notes for details.'
|
|
}
|
|
})
|
|
|
|
File.write('/tmp/cm_result.txt', out.join("\n"))
|
|
puts "==DONE=="
|