Custom Change Management Systems

FlexDeploy has out of box integration with ServiceNow, BMC Remedyforce, Freshworks Freshservice, and Jira ITSM, and provides a framework to enable custom integrations with other change management systems. Such third-party Change Management System integrations can be enabled using a Java or Groovy implementation.

Navigate to the Change Management Systems page from the Administration -> Integrations -> Change Management Systems menu.  Here you will see the out of the box integrations.

To create a custom integration for another provider click the Create button.  The following tasks will be performed to complete the integration:

  • Provide a unique name and description.

  • Determine whether you will implement the integration in Java or Groovy.  Groovy has the advantage that the script is loaded dynamically and does not require a FlexDeploy server recycle when you make changes.

  • Define properties for the new change management system. Properties are configuration values used by FlexDeploy to connect to the new system and parameterize its usage. 

  • Define Ticket Fields as necessary, which are used to create Ticket or Incidents automatically from FlexDeploy.

  • Develop either a Java class or Groovy script to perform the integration.

The examples below demonstrate a custom integration for Zendesk using both Java and Groovy.

Properties

The Properties tab defines the metadata for parameters necessary to both connect to the underlying system and provide configurable options for any instances which will consume this integration.

URL, credentials, and other connection related parameters should be exposed as properties so that the configuration can be updated without changes to the Java or Groovy classes/scripts.

For customers providing your own integrations you may choose to hard-code some of the logic into the scripts to follow enterprise standards for your organization.  For Systems Integrators who are developing solutions which may be utilized by multiple customers it is important to make use of properties where appropriate to allow simple configuration based on end customer requirements.

The metadata for each property consists of the following attributes:

Attribute

Description

Attribute

Description

Active

Whether this property is in use for the integration.  Allows hiding the property without removing it.

Key Name

A unique name which is used by the API and other scripts, and therefore must following keyword limitations of Java and Groovy (e.g. no spaces or special characters other than underscore).

Display Name

A name for the property which will be displayed for data entry rather than the technical Key Name.

Description

A meaningful description of what the property is used for. 

Data Type

The data type for the property.  Supported types - String, Boolean, Integer, or Double

Sub-Type

An optional qualifier describing the type of data the property will contain.  This is used to validate the data entry.  Supported types - URL, JDBCURL, or Directory.  Simply leave blank if none of these qualifiers are applicable.

Required

Whether or not a value for the property is required for CMS instances using this provider.  Implementations should use default values in the Java/Groovy class or script for any optional properties.

Encrypted

Indicates whether the property is secure and should be encrypted when stored, and care should be taken not to print their values in the logs.

Values for these properties will be provided when instances of this CMS are created.

API Implementation

Custom integrations must implement the following API operations:

Method Name

Parameter(s)

Return Type

Description

Method Name

Parameter(s)

Return Type

Description

createTicket

Map<String,Serializable> pTicketFields

void

Creates a Change Request ticket using the pDescription and pComment

createIncident

Map<String,Serializable> pTicketFields

String

Creates an Incident ticket using the pDescription and pComment

findCMSObjectByType

String pCMSObjectNumber

ChangeManagementSystem.CMSObjectType pCMSObjectType

void

Find a "ticket" or "incident" using the object identifier and type. The object type will be TICKET or INCIDENT, and implementations must translate that into the corresponding object type within the provider (e.g. Problem, Request, etc.) 

isTicketApproved

CMSObject pTicket

String pEnvironmentCode

Boolean

Returns whether the ticket is approved in the CMS. 

isTicketRejected

CMSObject pTicket

String pEnvironmentCode

Boolean

Returns whether the ticket is rejected in the CMS.

checkConnection

N/A

void

This method should invoke any status or heath check URL of the change management system to ensure the system is up and running and a connection can be authenticated.  This method will be called when the Test Connection button is clicked on the CMS Instance.

isDoPolling

N/A

void

Returns whether FlexDeploy should automatically poll the CMS to identify whether the ticket is approved or rejected.  If polling is enabled FlexDeploy will lookup the ticket every 1 minute using findCMSObjectByType and then check the status using the isTicketApproved and isTicketRejected methods.

If polling is not enabled, the CMS or another external system is responsible for approving/rejecting the associated task using the FlexDeploy REST API.

getTicketURL

CMSObject pCMSObject

String

returns the REST API url of the change ticket

If your usage of the CMS will not include the automated creation of TICKET or INCIDENT objects the API can be simplified.  Although createTicket and createIncident are required to be implemented, if you are not using these auto-creation features you can choose to simply throw an exception from one or both methods as appropriate.

Example

throw new UnsupportedOperationException("createIncident is not supported for Zendesk CMS integration");

Add custom task notes

With out-of-the-box CMS providers including BMCHelixRemedyforce, ServiceNow, JiraITSM, and Freshworks, you have the ability to add notes to a FlexDeploy External Approval upon approval or rejection. Custom Change Management Systems have the same functionality. Can do so by overriding the following getAdditionalTicketInfo in your Groovy API. It takes CMSObject and environment code as parameters and must return a Map<String, String>.

Simple Example

def getAdditionalTicketInfo(CMSObject pTicket, String pEnvironmentCode) { // Create notes to add to External Approval def changeNumber = pTicket.getNumber() def map = {'notes': 'Approval note for ' changeNumber + ' added by FD admin'} return map }

Java Implementation

Here are high level steps for Java implementation. You can use any IDE to prepare this implementation. 

  • Create java class that extends flexagon.fd.model.integration.cms.api.ChangeManagementSystem. Example shown below has the methods implemented which uses properties map to retrieve the configuration values to connect to the change management system.

  • All properties defined are available in Map returned by getProperties method.

  • We are using Zendesk as a use case

package mycompany.extension.flexdeploy.zendesk; import flexagon.fd.model.integration.cms.api.CMSObject; import flexagon.fd.model.integration.cms.api.ChangeManagementSystem; import flexagon.fd.model.integration.util.ApiException; import java.io.Serializable; import java.io.StringReader; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.json.Json; import javax.json.JsonObject; import javax.json.JsonObjectBuilder; import javax.json.JsonReader; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.Entity; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.glassfish.jersey.client.ClientConfig; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.filter.LoggingFilter; public class ZendeskServiceIntegration extends ChangeManagementSystem { private static final String CLZ_NAM = ZendeskServiceIntegration.class.getName(); private Logger mLogger; private Client mRestClient; private String mAccessToken; public ZendeskServiceIntegration() { super(); this.mLogger = Logger.getLogger(CLZ_NAM); } private void getOAuthAccessToken(String pScope) throws ApiException { String methodName = "getOAuthAccessToken"; mLogger.logp(Level.INFO, mLogger.getName(), methodName, "ENTRY"); JsonObjectBuilder zenOAuthRequestJson = Json.createObjectBuilder(); zenOAuthRequestJson.add("grant_type", "password"); zenOAuthRequestJson.add("client_id", getClientId()); zenOAuthRequestJson.add("client_secret", getClientSecret()); zenOAuthRequestJson.add("username", getUserName()); zenOAuthRequestJson.add("password", getPassword()); zenOAuthRequestJson.add("scope", pScope); String payLoad = zenOAuthRequestJson.build().toString(); Response clientResponse = getRestClient().target(getZendeskDomain()).path(getOAuthURI()).request(MediaType.APPLICATION_JSON_TYPE).accept(MediaType.APPLICATION_JSON_TYPE).post(Entity.json(payLoad)); checkResponse(clientResponse); JsonObject jsonResponseObject = readJsonObject(clientResponse.readEntity(String.class)); mLogger.logp(Level.INFO, mLogger.getName(), methodName, "OAuth Response " + jsonResponseObject); mAccessToken = jsonResponseObject.getString("access_token"); mLogger.logp(Level.INFO, mLogger.getName(), methodName, "EXIT"); } private String getAccessToken(String pScope) throws ApiException { getOAuthAccessToken(pScope); return "Bearer " + mAccessToken; } private String getOAuthURI() { return getPropertyAsString("ZD_OAUTH_URI"); } private String getClientId() { return getPropertyAsString("ZD_CLIENT_ID"); } private String getClientSecret() { return getPropertyAsString("ZD_CLIENT_SECRET"); } @Override public CMSObject createTicket(Map<String, Serializable> pTicketFields) throws ApiException { String methodName = "createRequest"; mLogger.logp(Level.INFO, mLogger.getName(), methodName, "ENTRY"); mLogger.logp(Level.INFO, mLogger.getName(), methodName, " Creating new Request using " + pTicketFields); CMSObject request = postTicket("request", "ZD_REQUEST_CREATE_PATTERN", "requests:write read", pTicketFields); mLogger.logp(Level.INFO, mLogger.getName(), methodName, "EXIT"); return request; } private CMSObject postTicket(String pObjectType, String pURLKey, String pScope, Map<String, Serializable> pTicketFields) throws ApiException { String methodName = "postTicket"; mLogger.logp(Level.INFO, mLogger.getName(), methodName, "ENTRY"); JsonObjectBuilder zenInputJson = Json.createObjectBuilder(); Serializable requestor = pTicketFields.remove("requester_id"); JsonObjectBuilder reqParamJsonBuider = null; if (requestor != null) { reqParamJsonBuider = Json.createObjectBuilder(); reqParamJsonBuider.add("name", requestor.toString()); } Serializable description = pTicketFields.remove("description"); JsonObjectBuilder desParamJsonBuider = null; if (description != null) { desParamJsonBuider = Json.createObjectBuilder(); desParamJsonBuider.add("body", description.toString()); } JsonObjectBuilder inputJsonObject = buildJsonRequest(pTicketFields); if (requestor != null) { inputJsonObject.add("requester", reqParamJsonBuider.build()); } if (description != null) { inputJsonObject.add("comment", desParamJsonBuider.build()); } zenInputJson.add(pObjectType, inputJsonObject); WebTarget postRequest = getWebResource(getPropertyAsString(pURLKey)); String payLoad = zenInputJson.build().toString(); mLogger.logp(Level.INFO, mLogger.getName(), methodName, String.format(" Creating new %s %s", pObjectType, payLoad)); Response clientResponse = postRequest.request(MediaType.APPLICATION_JSON_TYPE).accept(MediaType.APPLICATION_JSON_TYPE).header("Authorization", getAccessToken(pScope)).post(Entity.json(payLoad)); checkResponse(clientResponse); JsonObject jsonResponseObject = readJsonObject(clientResponse.readEntity(String.class)); JsonObject requestResponseJson = jsonResponseObject.getJsonObject(pObjectType); CMSObject request = createTicketFromJson(requestResponseJson, ChangeManagementSystem.CMSObjectType.TICKET); mLogger.logp(Level.INFO, mLogger.getName(), methodName, String.format(" Successfully created ticket %s", request.getNumber())); mLogger.logp(Level.INFO, mLogger.getName(), methodName, "EXIT"); return request; } private CMSObject createTicketFromJson(JsonObject jsonObject, CMSObjectType pType) { String methodName = "createTicketFromJson"; mLogger.logp(Level.INFO, mLogger.getName(), methodName, "ENTRY"); CMSObject ticket = CMSObjectImpl.getInstance(String.valueOf(jsonObject.getInt("id")), pType, jsonObject, jsonObject.getString("description")); mLogger.logp(Level.INFO, mLogger.getName(), methodName, "EXIT"); return ticket; } private JsonObject readJsonObject(String source) { String methodName = "readJsonObject"; mLogger.logp(Level.INFO, mLogger.getName(), methodName, "ENTRY"); JsonReader jsonReader = Json.createReader(new StringReader(source)); JsonObject object = null; try { object = jsonReader.readObject(); mLogger.logp(Level.INFO, mLogger.getName(), methodName, String.format(" Json object created for response = %s", source)); } catch (Exception e) { mLogger.logp(Level.INFO, mLogger.getName(), methodName, String.format(" Exception while creating response json object, error %s, response = %s", e.getMessage(), source)); } finally { if (jsonReader != null) { jsonReader.close(); } } mLogger.logp(Level.INFO, mLogger.getName(), methodName, "EXIT"); return object; } @Override public CMSObject createIncident(Map<String, Serializable> pIncidentFields) { String methodName = "createIncident"; mLogger.logp(Level.INFO, mLogger.getName(), methodName, "ENTRY"); CMSObject request = null; try { request = postTicket("ticket", "ZD_TICKET_CREATE_PATTERN", "tickets:write read", pIncidentFields); } catch (ApiException e) { mLogger.logp(Level.INFO, mLogger.getName(), methodName, String.format(" Exception while creating incident, error %s", e.getMessage())); } mLogger.logp(Level.INFO, mLogger.getName(), methodName, "EXIT"); return request; } @Override public CMSObject findCMSObjectByType(String pRequestNumber, ChangeManagementSystem.CMSObjectType pRequestType) throws ApiException { String methodName = "findCMSObjectByType"; mLogger.logp(Level.INFO, mLogger.getName(), methodName, "ENTRY"); CMSObject changeRequest = getChangeRequest(pRequestNumber); if (changeRequest != null && changeRequest.getTypeName().equalsIgnoreCase("QUESTION") && pRequestType.equals(ChangeManagementSystem.CMSObjectType.TICKET)) { mLogger.logp(Level.INFO, mLogger.getName(), methodName, pRequestNumber + " is of type QUESTION, setting it as REQUEST for FD"); } mLogger.logp(Level.INFO, mLogger.getName(), methodName, "EXIT"); return getChangeRequest(pRequestNumber); } private CMSObject getChangeRequest(String pRequestNumber) throws ApiException { return getTicket("request", "ZD_REQUEST_GET_PATTERN", pRequestNumber, "{ZENDESK_REQUEST}"); } private CMSObject getTicket(String pObjectType, String pURLKey, String pRequestNumber, String pSearchPattern) throws ApiException { String methodName = "getTicket"; mLogger.logp(Level.INFO, mLogger.getName(), methodName, "ENTRY"); mLogger.logp(Level.INFO, mLogger.getName(), methodName, String.format(" Find Ticket [%s] ", pRequestNumber)); //Zendesk Get Request URL Pattern (/api/v2/requests/{ZENDESK_REQUEST}.json) String urlString = getPropertyAsString(pURLKey); urlString = urlString.replace(pSearchPattern, pRequestNumber); Response clientResponse = getWebResource(urlString).request(MediaType.APPLICATION_JSON_TYPE).header("Authorization", getAccessToken("tickets:write read")).get(Response.class); checkResponse(clientResponse); JsonObject jsonResponseObject = readJsonObject(clientResponse.readEntity(String.class)); JsonObject ticketResponseJson = jsonResponseObject.getJsonObject(pObjectType); CMSObject request = createTicketFromJson(ticketResponseJson, ChangeManagementSystem.CMSObjectType.TICKET); mLogger.logp(Level.INFO, mLogger.getName(), methodName, String.format(" Successfully found ticket %s", request.getNumber())); mLogger.logp(Level.INFO, mLogger.getName(), methodName, "EXIT"); return request; } @Override public Boolean isTicketApproved(CMSObject requestNumber, String environmentCode) { String methodName = "isTicketApproved"; mLogger.logp(Level.INFO, mLogger.getName(), methodName, "ENTRY"); CMSObject findRequest = requestNumber; Boolean approved = false; try { if (findRequest != null) { String status = findRequest.getJson().getString("status"); mLogger.logp(Level.INFO, mLogger.getName(), methodName, String.format("#[%s] status [%s]", requestNumber, status)); if ("solved".equalsIgnoreCase(status)) { approved = true; } } } catch (Exception e) { // TODO: Add catch code } mLogger.logp(Level.INFO, mLogger.getName(), methodName, "EXIT"); return approved; } @Override public Boolean isTicketRejected(CMSObject requestNumber, String environmentCode) { String methodName = "isTicketRejected"; mLogger.logp(Level.INFO, mLogger.getName(), methodName, "ENTRY"); CMSObject findRequest = requestNumber; Boolean rejected = false; try { if (findRequest != null) { String status = findRequest.getJson().getString("status"); mLogger.logp(Level.INFO, mLogger.getName(), methodName, String.format("#[%s] status [%s]", requestNumber, status)); if ((!"New".equalsIgnoreCase(status)) && ("on-hold".equalsIgnoreCase(status) || "Closed".equalsIgnoreCase(status))) { rejected = true; } } } catch (Exception e) { // TODO: Add catch code } mLogger.logp(Level.INFO, mLogger.getName(), methodName, "EXIT"); return rejected; } @Override public Boolean isDoPolling() { return false; } @Override public void checkConnection() throws ApiException { String methodName = "checkConnection"; mLogger.logp(Level.INFO, mLogger.getName(), methodName, "ENTRY"); mLogger.logp(Level.INFO, mLogger.getName(), methodName, " getting user details to check connection"); // ClientResponse clientResponse = getWebResource("/api/v2/users/me.json").header("Authorization", "Basic " + getAuthString()).get(ClientResponse.class); Response clientResponse = getWebResource("/api/v2/users/me.json").request(MediaType.APPLICATION_JSON_TYPE).header("Authorization", getAccessToken("organizations:write read")).get(Response.class); checkResponse(clientResponse); String responseString = clientResponse.readEntity(String.class); mLogger.logp(Level.INFO, mLogger.getName(), methodName, "responseString=" + responseString + ", Validated that JSON data was received from test connction URL invocation."); mLogger.logp(Level.INFO, mLogger.getName(), methodName, "EXIT"); } private StringBuilder getZendeskURLBuilder() { StringBuilder urlBuilder = new StringBuilder(getPropertyAsString("ZD_DOMAIN_NAME")); return urlBuilder; } private void checkResponse(Response clientResponse) throws ApiException { String methodName = "checkResponse"; mLogger.logp(Level.INFO, mLogger.getName(), methodName, "EXIT"); mLogger.logp(Level.INFO, mLogger.getName(), methodName, String.format(" Response %s", clientResponse)); int statusCode = clientResponse.getStatusInfo().getStatusCode(); if (statusCode == 401) { throw new ApiException("Invalid credentials.", ""); } if (statusCode == 500) { //record not found or other scenarious don't want to fail. return; } if (!(clientResponse.getStatusInfo().getFamily() == Response.Status.Family.SUCCESSFUL)) { throw new ApiException(clientResponse.toString(), clientResponse.getStatusInfo().getReasonPhrase()); } mLogger.logp(Level.INFO, mLogger.getName(), methodName, "EXIT"); } private String getZendeskDomain() { return getZendeskURLBuilder().toString(); } private String getUserName() { String userName = getPropertyAsString("ZD_USER_NAME"); return userName; } private String getPassword() { String pwd = getPropertyAsString("ZD_PASSWORD"); return pwd; } private String getPropertyAsString(String pKey) { return (String) getProperties().get(pKey); } private Client getRestClient() throws ApiException { if (mRestClient == null) { mRestClient = ClientBuilder.newClient(new ClientConfig().register(LoggingFilter.class)); mRestClient.property(ClientProperties.CONNECT_TIMEOUT, 10000); mRestClient.property(ClientProperties.READ_TIMEOUT, 20000); } return mRestClient; } public WebTarget getWebResource(String resource) throws ApiException { if (resource != null && !resource.isEmpty() && !resource.startsWith("/")) { resource += "/" + resource; } WebTarget webResource = null; try { webResource = getRestClient().target(getZendeskDomain()).path(resource); } catch (Exception e) { throw new ApiException(e.getMessage(), e.getMessage()); } return webResource; } protected JsonObjectBuilder buildJsonRequest(Map<String, Serializable> pFields) { JsonObjectBuilder jsonBuider = Json.createObjectBuilder(); for (String fieldKey: pFields.keySet()) { Serializable fieldValue = pFields.get(fieldKey); if (fieldValue != null) { if (fieldValue instanceof String) { jsonBuider.add(fieldKey, (String) fieldValue); } else if (fieldValue instanceof Boolean) { jsonBuider.add(fieldKey, (Boolean) fieldValue); } else if (fieldValue instanceof Integer) { jsonBuider.add(fieldKey, (Integer) fieldValue); } else if (fieldValue instanceof Float) { jsonBuider.add(fieldKey, (Float) fieldValue); } else if (fieldValue instanceof Double) { jsonBuider.add(fieldKey, (Double) fieldValue); } else if (fieldValue instanceof Long) { jsonBuider.add(fieldKey, (Long) fieldValue); } else { // convert to String if type unknown jsonBuider.add(fieldKey, fieldValue.toString()); } } } return jsonBuider; } public String getTicketURL(CMSObject pCMSObject) { String methodName = "getTicketURL()"; log.logFinestEntering(methodName, pCMSObject); StringBuilder tickerUrlBuilder = new StringBuilder(); if(pCMSObject != null) { tickerUrlBuilder.append(getZendeskDomain()); String urlString = getPropertyAsString("ZD_REQUEST_GET_PATTERN"); String changeRequestURI = urlString.replace("{ZENDESK_REQUEST}"), pCMSObject.getNumber()); tickerUrlBuilder.append(changeRequestURI); } String url = tickerUrlBuilder.toString(); log.logFinestExiting(methodName, url); return url; } }
  • In order to compile your java class, you will need FlexDeployAPI.jar on classpath.

  • Implement all the methods described in the table in the API Implementation section.

  • For any failure connecting to the system or if any issues with the data, then you can throw exception. For example throw new ApiException("Invalid credentials.", "");

  • Once you are ready with unit testing, you can prepare Jar file for your credential store java class and other utility classes. This jar file can be placed on server classpath now.

    • For Tomcat, put this jar file in apache-tomcat-flexdeploy/lib folder.

    • For WebLogic, put this jar file in Domain lib folder.

    • If you are using any third-party libraries from your Java implementation, then those jar files will also need to be added to same lib folder. Keep in mind that this can cause issues with server functioning, so be prepared to remove your additional library files.

  • To pickup changes to your API, the FlexDeploy server must be restarted.

Groovy Implementation

Here are high level steps for Groovy implementation. You can use any IDE to prepare this implementation. 

As groovy is able to access FlexDeploy variables and Java classes, you can take advantage of Java libraries from Groovy script. For example, if there is Java library used to access the change management system, you can places those in lib folder and use those classes from Groovy script. This allows you to keep dynamic part of implementation in Groovy and use Java library.

  • Create a groovy class. Example shown below has the methods implemented.

  • We are using Zendesk as a use case

  • All properties defined are available as groovy binding variables. For example, properties can be accessed directly like BMC_DOMAIN_NAME, BMC_SALESFORCE_HOST_NAME or BMC_USER_NAME etc

  • Implement all the methods described in the table in the API Implementation section

  • For any failure connecting to the system or if any issues with the data, then you can throw exception. For example throw new ApiException("Invalid credentials.", "");

Groovy Utilities

There are some utility variables provided by FlexDeploy that can be used in your custom Groovy code.

  • log is a FlexDeploy logger variable which should be used to log any information from the groovy class.

  • fdrestutils is a utility object available to use FlexDeploy API to invoke any REST API. See the Java docs for more details on the functions available.

 

The following macros are not currently supported in the footer:
  • style