diff --git a/incubating/service-now/CHANGELOG.md b/incubating/service-now/CHANGELOG.md index a554a48cf..1fd0bdf06 100644 --- a/incubating/service-now/CHANGELOG.md +++ b/incubating/service-now/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v1.4.0 (Mar 28, 2025) +### Added +* ability to create a Standard Change passing the template name + in the `STD_CR_TEMPLATE` variable. + ## [1.2.5] - 2025-01-02 ### Fixed diff --git a/incubating/service-now/README.md b/incubating/service-now/README.md index 94cb50922..2c0f79645 100644 --- a/incubating/service-now/README.md +++ b/incubating/service-now/README.md @@ -22,12 +22,13 @@ Example `codefresh.yml` build is below with required Arguments in place. | CR_ACTION | createCR | string | no | createCR, closeCR, updateCR | the operation to execute | | CR_CONFLICT_POLICY | ignore | string | no | ignore, wait, reject | What do when a schedule conflict arises | | CR_DATA | N/A | JSON string | no | JSON block | the JSON block to pass when opening, updating or closing a CR | +| STD_CR_TEMPLATE | | string | no | | the name of a Standard template | | CR_SYSID | N/A | string | no | uuid | the sysid of the CR record as returned by the createCR action. USed to update or close a CR | | CR_CLOSE_CODE | successful | string | no | sucessful or any value accepted by the close_code field | | CR_CLOSE_NOTES | N/A | string | no | Any string accepted for the close_notes field | -### codefresh.yml +### codefresh.yaml Codefresh build step to execute AWS CDK commands @@ -62,7 +63,7 @@ steps: echo END_DATE=\"$END_DATE\" >> ${{CF_VOLUME_PATH}}/env_vars_to_export createCR: - type: service-now + type: service-now:1.4.0 title: Create Service Now Change Request stage: deploy arguments: @@ -73,7 +74,7 @@ steps: TOKEN: ${{CF_TOKEN}} CR_CONFLICT_POLICY: reject CR_DATA: >- - {"short_description": "Globex deployment to QA", + {"short_description": "Globex deployment to QA", "description": "Change for build ${{CF_BUILD_ID}}.\nThis change was created by the Codefresh plugin", "justification": "I do not need a justification\nMy app is awesome", "cmdb_ci":"tomcat", @@ -91,7 +92,7 @@ steps: modifyCR: stage: deploy title: "Modify the implementation plan" - type: service-now + type: service-now:1.4.0 fail_fast: false arguments: CR_ACTION: updateCR @@ -120,7 +121,7 @@ steps: modifyTestPlan: stage: test title: "Modify the test plan" - type: service-now + type: service-now:1.4.0 fail_fast: false arguments: CR_ACTION: updateCR @@ -131,7 +132,7 @@ steps: CR_DATA: '{"test_plan":"The testing suit has passed."}' closeCR: - type: service-now + type: service-now:1.4.0 title: Close Service Now Change Request stage: post arguments: diff --git a/incubating/service-now/lib/snow.py b/incubating/service-now/lib/snow.py index 5709172e1..2bd279ced 100644 --- a/incubating/service-now/lib/snow.py +++ b/incubating/service-now/lib/snow.py @@ -3,10 +3,27 @@ import json import requests import logging +import urllib.parse API_NAMESPACE=409723 env_file_path = "/meta/env_vars_to_export" +def exportVariable(name, value): + if os.path.exists(env_file_path): + file=open(env_file_path, "a") + else: + file=open("/tmp/env_vars_to_export", "a") + file.write(f"{name}={value}\n") + file.close() + +def exportJson(name, json): + if os.path.exists(env_file_path): + json_file = open("/codefresh/volume/%s" %(name), "a") + else: + json_file = open("/tmp/%s" % (name), "a") + json_file.write(json) + json_file.close() + def getBaseUrl(instance): baseUrl = "%s/api" %(instance); logging.debug("baseUrl: " + baseUrl) @@ -41,16 +58,9 @@ def processCreateChangeRequestResponse(response): logging.info(f" Change Request sys_id: {CR_SYSID}") logging.debug( " Change Request full answer:\n" + FULL_JSON) - if os.path.exists(env_file_path): - env_file = open(env_file_path, "a") - env_file.write(f"CR_NUMBER={CR_NUMBER}\n") - env_file.write(f"CR_SYSID={CR_SYSID}\n") - env_file.write("CR_FULL_JSON=/codefresh/volume/servicenow-cr.json\n") - env_file.close() - - json_file=open("/codefresh/volume/servicenow-cr.json", "w") - json_file.write(FULL_JSON) - json_file.close() + exportVariable("CR_NUMBER", CR_NUMBER) + exportVariable("CR_SYSID", CR_SYSID) + exportVariable("CR_CREATE_JSON", FULL_JSON) # # Call SNow REST API to create a new Change Request @@ -80,6 +90,79 @@ def createChangeRequest(user, password, baseUrl, data): auth=(user, password)) processCreateChangeRequestResponse(response=resp) +def processSearchStandardTemplateResponse(name, response): + logging.info("Processing answer from Standard Template search") + logging.debug("Template search returned code %s" % (response.status_code)) + if (response.status_code != 200 and response.status_code != 201): + logging.critical("Standard Change Template for '%s' errored out with code %s", name, response.status_code) + logging.critical("%s" + response.text) + sys.exit(response.status_code) + data=response.json() + logging.debug("Full JSON answer: %s", data) + + if len(data["result"]) ==0 : + logging.critical("Standard Change Template '%s' was not found", name) + sys.exit(1) + + logging.info("Standard template search successful") + STD_SYSID=data["result"][0]["sys_id"] + return STD_SYSID + +def processCreateStandardChangeRequestResponse(response): + logging.info("Processing answer from standard CR creation REST call") + logging.debug("Change Request returned code %s" % (response.status_code)) + if (response.status_code != 200 and response.status_code != 201): + logging.critical("Change Request creation failed with code %s", response.status_code) + logging.critical("%s", response.text) + sys.exit(response.status_code) + + logging.info("Change Request creation successful") + data=response.json() + FULL_JSON=json.dumps(data, indent=2) + CR_NUMBER=data["result"]["number"]["value"] + CR_SYSID=data["result"]["sys_id"]["value"] + exportVariable("CR_NUMBER", CR_NUMBER) + exportVariable("CR_SYSID", CR_SYSID) + exportVariable("CR_CREATE_JSON", FULL_JSON) + return CR_NUMBER + +# Call SNow REST API to create a new Standard Change Request +# Fields required are pasted in the data +def createStandardChangeRequest(user, password, baseUrl, data, standardName): + logging.info("Creating a new Standard Change Request using '%s' template", standardName) + encodedName=urllib.parse.quote_plus(standardName) + + url="%s/now/table/std_change_record_producer?sysparm_query=sys_name=%s" % (baseUrl, encodedName) + + logging.debug("Standard Change URL %s:",url) + resp=requests.get(url, + headers = {"content-type":"application/json"}, + auth=(user, password)) + sysid=processSearchStandardTemplateResponse(name=standardName, response=resp) + logging.info("Template found: %s", sysid) + + if (bool(data)): + crBody=json.loads(data) + logging.debug("Data: %s", data) + else: + crBody= {} + logging.debug(" Data: None") + crBody["cf_build_id"] = os.getenv('CF_BUILD_ID') + + + url="%s/sn_chg_rest/change/standard/%s" % (baseUrl, sysid) + + logging.debug("URL %s:",url) + logging.debug("User: %s", user) + logging.debug("Body: %s", crBody) + + resp=requests.post(url, + json = crBody, + headers = {"content-type":"application/json"}, + auth=(user, password)) + return processCreateStandardChangeRequestResponse(response=resp) + + def processModifyChangeRequestResponse(response, action): logging.debug("Processing answer from CR %s REST call" %(action)) @@ -97,24 +180,17 @@ def processModifyChangeRequestResponse(response, action): FULL_JSON=json.dumps(data, indent=2) if (action == "close" ): - jsonVar="CR_CLOSE_FULL_JSON" - jsonFileName="/codefresh/volume/servicenow-cr-close.json" + exportVariable("CR_CLOSE_FULL_JSON", "/codefresh/volume/servicenow-cr-close.json") + exportJson("servicenow-cr-close.json", FULL_JSON) elif (action == "update" ): - jsonVar="CR_UPDATE_FULL_JSON" - jsonFileName="/codefresh/volume/servicenow-cr-update.json" + exportVariable("CR_UPDATE_FULL_JSON", "/codefresh/volume/servicenow-cr-update.json") + exportJson("servicenow-cr-update.json", FULL_JSON) else: print("ERROR: action unknown. Should not be here. Error should have been caught earlier") - if os.path.exists(env_file_path): - env_file = open(env_file_path, "a") - env_file.write(f"{jsonVar}=/codefresh/volume/servicenow-cr-close.json\n") - env_file.write(f"CR_NUMBER={CR_NUMBER}\n") - env_file.write(f"CR_SYSID={CR_SYSID}\n") - env_file.close() + exportVariable("CR_NUMBER", CR_NUMBER) + exportVariable("CR_SYSID", CR_SYSID) - json_file=open("/codefresh/volume/servicenow-cr-close.json", "w") - json_file.write(FULL_JSON) - json_file.close() # Call SNow REST API to close a CR # Fields required are pasted in the data @@ -196,6 +272,14 @@ def checkToken(token): logging.error("FATAL: TOKEN is not defined.") sys.exit(1) +def checkUser(username): + logging.debug("Entering checkUser: ") + logging.debug(" CR_USER: %s" % (username)) + + if ( username == None ): + logging.error("FATAL: CR_USER is not defined.") + sys.exit(1) + def checkConflictPolicy(policy): logging.debug("Entering checkConflictPolicy: ") logging.debug(" CR_CONFLICT_POLICY: %s" % (policy)) @@ -214,6 +298,7 @@ def main(): PASSWORD = os.getenv('SN_PASSWORD') INSTANCE = os.getenv('SN_INSTANCE') DATA = os.getenv('CR_DATA') + STD_NAME = os.getenv('STD_CR_TEMPLATE') DEBUG = True if os.getenv('DEBUG', "false").lower() == "true" else False TOKEN = os.getenv('TOKEN') POLICY = os.getenv('CR_CONFLICT_POLICY') @@ -230,17 +315,26 @@ def main(): logging.debug(f" DATA: {DATA}") logging.debug(" SYSID: %s" % (os.getenv('CR_SYSID'))) + checkUser(USER) if ACTION == "createcr": # Used only later in the callback but eant to check for error early checkToken(TOKEN) checkConflictPolicy(POLICY) - createChangeRequest(user=USER, - password=PASSWORD, - baseUrl=getBaseUrl(instance=INSTANCE), - data=DATA - ) + if STD_NAME: + cr_number=createStandardChangeRequest(user=USER, + standardName=STD_NAME, + password=PASSWORD, + baseUrl=getBaseUrl(instance=INSTANCE), + data=DATA + ) + else: + createChangeRequest(user=USER, + password=PASSWORD, + baseUrl=getBaseUrl(instance=INSTANCE), + data=DATA + ) elif ACTION == "callback": callback(user=USER, password=PASSWORD, diff --git a/incubating/service-now/step.yaml b/incubating/service-now/step.yaml index 70ba28567..1bfcab355 100644 --- a/incubating/service-now/step.yaml +++ b/incubating/service-now/step.yaml @@ -2,7 +2,7 @@ kind: step-type version: '1.0' metadata: name: service-now - version: 1.2.5 + version: 1.4.0 isPublic: true description: Integration with ServiceNow Change Management sources: @@ -86,7 +86,7 @@ spec: }, "SN_IMAGE_VERSION": { "type": "string", - "default": "1.2.5", + "default": "1.4.0", "description": "Version of the ServiceNow image to use, Docker image tag." }, "SN_INSTANCE": { @@ -114,6 +114,10 @@ spec: "type": "string", "description": "The body to create the CR. Need to include all the fields required for your Change Management implementation." }, + "STD_CR_TEMPLATE": { + "type": "string", + "description": "name of a Standard Change template. Using this parameter will open a Standard Change (pre-approved) instead of a normal one." + }, "CR_CONFLICT_POLICY": { "type": "string", "description": "Policy to exectute in case of schedule conflict. Accepted values are ignore (no check is done), wait (pipeline will wait until the conflict is resolved) or reject ServiceNow flow returns a deny answer", @@ -209,15 +213,18 @@ spec: codefresh create annotation workflow ${{CF_BUILD_ID}} CR_NUMBER=${{CR_NUMBER}} codefresh create annotation workflow ${{CF_BUILD_ID}} CR_SYSID=${{CR_SYSID}} cf_export annotation_CF_OUTPUT_URL="[[.Arguments.SN_INSTANCE]]/nav_to.do?uri=%2Fchange_request.do%3Fsys_id%3D$CR_SYSID" + + [[ if eq .Arguments.STD_CR_TEMPLATE "" ]] callback: name: invoke scripted REST API to have ServiceNow callback Codefresh when CR is approved/rejected title: ServiceNow callback setup image: '[[.Arguments.SN_IMAGE]]:[[.Arguments.SN_IMAGE_VERSION]]' environment: - [[ range $key, $val := .Arguments ]] + [[ range $key, $val := .Arguments ]] - '[[ $key ]]=[[ $val ]]' - [[- end ]] + [[- end ]] - ACTION=callback + [[ end ]] [[ end ]] delimiters: left: '[['