-
Notifications
You must be signed in to change notification settings - Fork 117
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support ticket servicenow integration (#4081)
Add support ticket integration with ServiceNow API.
- Loading branch information
Showing
8 changed files
with
508 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'rest_client' | ||
|
||
# HTTP client to create a ServiceNow incident using the API | ||
# Credentials are not compulsory to support authentication through Apache proxy settings | ||
# Configuration parameters: | ||
# - `server`: URL for the ServiceNow server (required) | ||
# - `user`: ServiceNow API username | ||
# - `pass`: ServiceNow API password | ||
# - `pass_env`: Environment variable to use for the ServiceNow API password | ||
# - `auth_token`: ServiceNow API key | ||
# - `auth_token_env`: Environment variable to use for the ServiceNow API key | ||
# - `auth_header`: ServiceNow API key HTTP header. Defaults to x-sn-apikey. | ||
# - `timeout`: Connection and read timeout in seconds. Defaults to 30. | ||
# - `verify_ssl`: Whether or not the client should validate SSL certificates. Defaults to true. | ||
# - `proxy`: Proxy server URL. Defaults to no proxy. | ||
# | ||
class ServiceNowClient | ||
|
||
UA = 'Open OnDemand ruby ServiceNow Client' | ||
attr_reader :server, :auth_header, :client, :timeout, :verify_ssl | ||
|
||
def initialize(config) | ||
# FROM CONFIGURATION | ||
@user = config[:user] | ||
@pass = config[:pass] | ||
@auth_token = config[:auth_token] | ||
@auth_header = config[:auth_header] || 'x-sn-apikey' | ||
@timeout = config[:timeout] || 30 | ||
@verify_ssl = config[:verify_ssl] || false | ||
@server = config[:server] if config[:server] | ||
|
||
raise ArgumentError, 'server is a required parameter for ServiceNow client' unless @server | ||
|
||
# Allow to pass secrets securely through environment variables | ||
auth_token_env = config[:auth_token_env] | ||
@auth_token = ENV[auth_token_env] if auth_token_env | ||
pass_env = config[:pass_env] | ||
@pass = ENV[pass_env] if pass_env | ||
|
||
headers = { 'User-Agent' => UA, | ||
'Cookie' => '' } | ||
headers[@auth_header] = @auth_token if @auth_token | ||
|
||
options = { | ||
headers: headers, | ||
timeout: @timeout, | ||
verify_ssl: @verify_ssl, | ||
} | ||
options[:user] = @user if @user | ||
options[:password] = @pass if @pass | ||
options[:proxy] = config[:proxy] if config[:proxy] | ||
|
||
@client = RestClient::Resource.new(@server, options) | ||
end | ||
|
||
def create(payload, attachments) | ||
incident = @client['/api/now/table/incident'].post(payload.to_json, content_type: :json) | ||
response_hash = JSON.parse(incident.body)['result'].symbolize_keys | ||
incident_number = response_hash[:number] | ||
incident_id = response_hash[:sys_id] | ||
|
||
raise StandardError, "Unable to create ticket. Server response: #{incident}" unless incident_id | ||
|
||
begin | ||
attachments.to_a.each do |request_file| | ||
add_attachment(incident_id, request_file) | ||
end | ||
attachments_success = true | ||
rescue StandardError => e | ||
Rails.logger.info "Could not add attachments to incident: #{incident_number}. Error=#{e}" | ||
attachments_success = false | ||
end | ||
|
||
create_response(incident_number, attachments.to_a.size, attachments_success) | ||
end | ||
|
||
def add_attachment(incident_id, request_file) | ||
params = { | ||
table_name: 'incident', | ||
table_sys_id: incident_id, | ||
file_name: request_file.original_filename, | ||
} | ||
file = File.new(request_file.tempfile, 'rb') | ||
@client['/api/now/attachment/file'].post(file, params: params, content_type: request_file.content_type) | ||
end | ||
|
||
private | ||
|
||
def create_response(incident_number, attachments, attachments_success) | ||
OpenStruct.new({ | ||
number: incident_number, | ||
attachments: attachments, | ||
attachments_success: attachments_success | ||
}) | ||
end | ||
|
||
end |
113 changes: 113 additions & 0 deletions
113
apps/dashboard/app/apps/support_ticket_service_now_service.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
# frozen_string_literal: true | ||
|
||
# Service class responsible to create a support ticket and delivery it via ServiceNow API | ||
# | ||
# It implements the support ticket interface as defined in the SupportTicketController | ||
class SupportTicketServiceNowService | ||
|
||
attr_reader :support_ticket_config | ||
|
||
# Constructor | ||
# | ||
# @param [Hash] support_ticket_config Support ticket configuration | ||
def initialize(support_ticket_config) | ||
@support_ticket_config = support_ticket_config | ||
end | ||
|
||
# Creates a support ticket model with default data. | ||
# Will load an interactive session if a session_id provided in the request parameters. | ||
# | ||
# @param [Hash] request_params Request data sent to the controller | ||
# | ||
# @return [SupportTicket] support_ticket model | ||
def default_support_ticket(request_params) | ||
support_ticket = SupportTicket.from_config(support_ticket_config) | ||
support_ticket.username = CurrentUser.name | ||
support_ticket.session_id = request_params[:session_id] | ||
set_session(support_ticket) | ||
end | ||
|
||
# Uses SupportTicket model to create and validate the request data. | ||
# The model needs to be validated before returning | ||
# | ||
# @param [Hash] request_data Request data posted to the controller | ||
# | ||
# @return [SupportTicket] support_ticket model | ||
def validate_support_ticket(request_data = {}) | ||
support_ticket = SupportTicket.from_config(support_ticket_config) | ||
support_ticket.attributes = request_data | ||
set_session(support_ticket) | ||
support_ticket.tap(&:validate) | ||
end | ||
|
||
# Creates a support ticket in the ServiceNow system configured | ||
# | ||
# @param [SupportTicket] support_ticket support ticket created in validate_support_ticket | ||
# | ||
# @return [String] success message | ||
def deliver_support_ticket(support_ticket) | ||
service_config = support_ticket_config.fetch(:servicenow_api, {}) | ||
session = get_session(support_ticket) | ||
description = create_description_text(service_config, support_ticket, session) | ||
payload = { | ||
caller_id: support_ticket.email, | ||
watch_list: support_ticket.cc, | ||
short_description: support_ticket.subject, | ||
description: description, | ||
} | ||
|
||
mapping_fields = service_config.fetch(:map, {}).to_h | ||
mapping_fields.each do |snow_field, form_field| | ||
# Map field names from the form into field names from ServiceNow | ||
# arrays are supported for form_field names and the values will be joined with commas. | ||
value = Array.wrap(form_field).map { |name| support_ticket.send(name).to_s }.reject(&:blank?).join(',') | ||
payload[snow_field] = value | ||
end | ||
|
||
custom_payload = service_config.fetch(:payload, {}) | ||
custom_payload.each do |key, value| | ||
# Use the values from the custom payload if available. | ||
# Default to the values from the form when nil provided. | ||
payload[key] = value.nil? ? support_ticket.send(key) : value | ||
end | ||
|
||
snow_client = ServiceNowClient.new(service_config) | ||
result = snow_client.create(payload, support_ticket.attachments) | ||
Rails.logger.info "Support Ticket created in ServiceNow: #{result.number} - Attachments[#{result.attachments}] success=#{result.attachments_success}" | ||
message_key = result.attachments_success ? 'creation_success' : 'attachments_failure' | ||
service_config.fetch(:success_message, I18n.t("dashboard.support_ticket.servicenow.#{message_key}", number: result.number)) | ||
end | ||
|
||
private | ||
|
||
def create_description_text(service_config, support_ticket_request, session) | ||
ticket_template_context = { | ||
session: session, | ||
support_ticket: support_ticket_request, | ||
} | ||
|
||
template = service_config.fetch(:template, 'servicenow_content.text.erb') | ||
ticket_content_template = ERB.new(File.read(Rails.root.join('app/views/support_ticket/servicenow').join(template))) | ||
ticket_content_template.result_with_hash({ context: ticket_template_context, helpers: TemplateHelpers.new }) | ||
end | ||
|
||
def set_session(support_ticket) | ||
session = get_session(support_ticket) | ||
if session | ||
created_at = session.created_at ? Time.at(session.created_at).localtime.strftime("%Y-%m-%d %H:%M:%S %Z") : "N/A" | ||
support_ticket.session_description = "#{session.title}(#{session.job_id}) - #{session.status} - #{created_at}" | ||
end | ||
|
||
support_ticket | ||
end | ||
|
||
def get_session(support_ticket) | ||
if !support_ticket.session_id.blank? && BatchConnect::Session.exist?(support_ticket.session_id) | ||
BatchConnect::Session.find(support_ticket.session_id) | ||
end | ||
end | ||
|
||
class TemplateHelpers | ||
include SupportTicketHelper | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
34 changes: 34 additions & 0 deletions
34
apps/dashboard/app/views/support_ticket/servicenow/servicenow_content.text.erb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
Ticket submitted from OnDemand dashboard application | ||
Username: <%= context[:support_ticket].username %> | ||
Email: <%= context[:support_ticket].email %> | ||
CC: <%= context[:support_ticket].cc %> | ||
|
||
<% if context[:session] %> | ||
User selected session: <%= context[:session].id %> | ||
Title: <%= context[:session].title %> | ||
Scheduler job id: <%= context[:session].job_id %> | ||
Status: <%= context[:session].status.to_sym %> | ||
<% end %> | ||
|
||
Description: | ||
<%= context[:support_ticket].description %> | ||
|
||
------------------------------------- | ||
Session Information: | ||
<% if context[:session] %> | ||
<%= JSON.pretty_generate( | ||
{ | ||
id: context[:session].id, | ||
clusterId: context[:session].cluster_id, | ||
jobId: context[:session].job_id, | ||
createdAt: Time.at(context[:session].created_at).iso8601, | ||
token: context[:session].token, | ||
title: context[:session].title, | ||
user_context: context[:session].user_context, | ||
info: helpers.filter_session_parameters(context[:session].info), | ||
deletedInDays: context[:session].days_till_old, | ||
}) | ||
%> | ||
<% else %> | ||
No session was selected. | ||
<% end %> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'test_helper' | ||
|
||
class ServiceNowClientTest < ActiveSupport::TestCase | ||
test 'should throw exception when server is not provided' do | ||
config = { | ||
server: nil | ||
} | ||
|
||
assert_raises(ArgumentError) { ServiceNowClient.new(config) } | ||
end | ||
|
||
test 'should allow missing credentials' do | ||
config = { | ||
server: 'http://server.com', | ||
} | ||
|
||
assert_not_nil(ServiceNowClient.new(config)) | ||
end | ||
|
||
test 'should set the expected default values' do | ||
config = { | ||
server: 'http://server.com' | ||
} | ||
|
||
target = ServiceNowClient.new(config) | ||
assert_equal('x-sn-apikey', target.auth_header) | ||
assert_equal(30, target.timeout) | ||
assert_equal(false, target.verify_ssl) | ||
end | ||
|
||
test 'should set RestClient options when provided' do | ||
config = { | ||
server: 'http://server.com', | ||
user: 'payload_username', | ||
pass: 'payload_password', | ||
auth_token: 'payload_token', | ||
timeout: 90, | ||
verify_ssl: true, | ||
proxy: 'proxy.com:8888' | ||
} | ||
|
||
target = ServiceNowClient.new(config) | ||
assert_equal('payload_username', target.client.options[:user]) | ||
assert_equal('payload_password', target.client.options[:password]) | ||
assert_equal('payload_token', target.client.options[:headers]['x-sn-apikey']) | ||
assert_equal(90, target.client.options[:timeout]) | ||
assert_equal(true, target.client.options[:verify_ssl]) | ||
assert_equal('proxy.com:8888', target.client.options[:proxy]) | ||
end | ||
|
||
test 'should set password and token from environment if configured' do | ||
config = { | ||
server: 'http://server.com', | ||
pass_env: 'SNOW_PASSWORD', | ||
auth_token_env: 'SNOW_TOKEN' | ||
} | ||
|
||
with_modified_env(SNOW_PASSWORD: 'password_from_env', SNOW_TOKEN: 'token_from_env') do | ||
target = ServiceNowClient.new(config) | ||
assert_equal('password_from_env', target.client.options[:password]) | ||
assert_equal('token_from_env', target.client.options[:headers]['x-sn-apikey']) | ||
end | ||
|
||
end | ||
end |
Oops, something went wrong.