diff --git a/apps/dashboard/app/apps/servicenow_client.rb b/apps/dashboard/app/apps/servicenow_client.rb new file mode 100644 index 0000000000..71d78ab273 --- /dev/null +++ b/apps/dashboard/app/apps/servicenow_client.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'rest_client' + +# HTTP client to create a ServiceNow incident using the API +# Configuration parameters: +# - `server`: URL for the ServiceNow server (required) +# - `user`: ServiceNow API username +# - `pass`: ServiceNow API password +# - `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 option for ServiceNow client' unless @server + + if !@auth_token && !@user && !@pass + raise ArgumentError, 'auth_token or user and pass are required options for the ServiceNow client' + end + + headers = { 'User-Agent' => UA, + 'Cookie' => '' } + headers[@auth_header] = @auth_token if @auth_token + + options = { + headers: headers, + timeout: @timeout, + verify_ssl: @verify_ssl, + } + + if @user && @pass + options[:user] = @user + options[:pass] = @pass + end + 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] + + attachments&.each do |request_file| + add_attachment(incident_id, request_file) + end + + return incident_number if incident_number + + raise StandardError, "Unable to create ticket. Server response: #{incident}" + 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') + resp = @client['/api/now/attachment/file'].post(file, params: params, content_type: request_file.content_type) + response_hash = JSON.parse(resp.body)['result'].symbolize_keys + Rails.logger.info response_hash + + end + + def add_attachment_upload(incident_id, request_file) + form_data = { + table_name: 'incident', + table_sys_id: incident_id, + uploadFile: File.new(request_file.tempfile), + } + resp = @client['/api/now/attachment/upload'].post(form_data, content_type: :multipart) + response_hash = JSON.parse(resp.body)['result'].symbolize_keys + Rails.logger.info response_hash + + end + +end diff --git a/apps/dashboard/app/apps/support_ticket_servicenow_service.rb b/apps/dashboard/app/apps/support_ticket_servicenow_service.rb new file mode 100644 index 0000000000..1d7d8b6398 --- /dev/null +++ b/apps/dashboard/app/apps/support_ticket_servicenow_service.rb @@ -0,0 +1,103 @@ +# 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] + support_ticket.queue = request_params[:queue] + 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.username, + short_description: support_ticket.subject, + description: description, + } + 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. + payload[key] = value.nil? ? support_ticket.send(key) : value + end + + snow_client = ServicenowClient.new(service_config) + number = snow_client.create(payload, support_ticket.attachments) + Rails.logger.info "Support Ticket created in ServiceNow: #{number}" + service_config.fetch(:success_message, I18n.t('dashboard.support_ticket.servicenow.creation_success', number: 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 diff --git a/apps/dashboard/app/models/user_configuration.rb b/apps/dashboard/app/models/user_configuration.rb index ac9f0ea684..0fcecf663e 100644 --- a/apps/dashboard/app/models/user_configuration.rb +++ b/apps/dashboard/app/models/user_configuration.rb @@ -140,6 +140,7 @@ def support_ticket_service # Supported delivery mechanism return SupportTicketEmailService.new(support_ticket) if support_ticket[:email] return SupportTicketRtService.new(support_ticket) if support_ticket[:rt_api] + return SupportTicketServicenowService.new(support_ticket) if support_ticket[:servicenow_api] raise StandardError, I18n.t('dashboard.user_configuration.support_ticket_error') end diff --git a/apps/dashboard/app/views/support_ticket/servicenow/servicenow_content.text.erb b/apps/dashboard/app/views/support_ticket/servicenow/servicenow_content.text.erb new file mode 100644 index 0000000000..a76a64bc9d --- /dev/null +++ b/apps/dashboard/app/views/support_ticket/servicenow/servicenow_content.text.erb @@ -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 %> \ No newline at end of file diff --git a/apps/dashboard/config/locales/en.yml b/apps/dashboard/config/locales/en.yml index 5e37ebd24c..9beafdbf6f 100644 --- a/apps/dashboard/config/locales/en.yml +++ b/apps/dashboard/config/locales/en.yml @@ -306,6 +306,8 @@ en: rt: creation_success: "Support ticket created in RequestTracker system. TicketId: %{ticket_id}" + servicenow: + creation_success: "Support ticket created in ServiceNow system. Number: %{number}" custom_pages: invalid: "Invalid page code: %{page}. This page has not been configured"