# 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=="