Skip to Content
Technical Articles

Process Management API in SAP Solution Manager

This article will describe the SAP Solution Manager’s Process Management API, give an example of how it can be used to export the structure of branch to CSV/Excel and concludes with an outlook regarding further potential use-cases.

What is the Process Management API?

The Process Management API offers all the necessary building blocks to read/write to almost any solution documentation elements (Test Steps Test cases are not supported though). Obviously the solution manager’s authorization/change control mechanisms still apply.

Resource Model + Content Model (List of API Calls):

https://wiki.scn.sap.com/wiki/display/SM/Process+Management+API

Description of Exchange Format:

https://wiki.scn.sap.com/wiki/display/SM/Exchange+format

Getting started – Basic example:

We will demonstrate how to obtain a list of solutions using the SolutionSet() API call. To do this, simply (after replacing the <placeholders> according to your solution manager) open the below URL in a browser. You will be asked for your username/password.

URL: http://<hostname>:50000/sap/opu/odata/sap/ProcessManagement/SolutionSet?sap-client=<client>&$format=json

Result:

The resulting JSON is in fact a full list of solutions. It tells us, that several solutions have been created in this solution manager. One of those solutions (the one expanded) goes by the Name “DELETE_ME”.

Using the “SolutionId”, (the solution’s  unique identifier) we could now dig deeper by using the BranchSet() API Call, to obtain a list of branches of the “DELETE_ME” Solution:

URL:
http://<hostname>:50000/sap/opu/odata/sap/ProcessManagement/BranchSet?$filter= SolutionId eq ‘051MZjd97jUdYfSEOG}k10’ &sap-client=<client>&$format=json

Result:

A List of the “DELETE_ME” solution’s branches, each with name, type and id. With the BranchID, we will be able to read the full content structure of a branch through the BranchContentSet() API Call.

Exporting a full branch structure

At this point all that is left to do, is to identify the ROOT node of the tree structure that comprises a branches content and then walk through it (the JSON, that is) along the parent/child relationships as follows:

  • Process (output) the current node.
  • Process (output) all children.

This obviously cannot be done by typing requests in a browser anymore, but rather requires a small program.
For this prototype I chose Python 3, as it natively supports JSON, offers a convenient way handle http requests and is widely available / has a large community.

In python:

def map_branch(branch_id, node_id, depth):
    #recursively walks through a branch and outputs all elements
    #1: output the current node
    sep = '\t' #separator
    node = get_node_by_id(branch_id, node_id)
    description = ''.join(get_node_attribute_values(node, 'DESCRIPTION') + get_node_attribute_values(node, '_DESCRIPTION'))
    name = get_node_name_by_id(branch_id, node_id)
    print('\t' * depth + node['obj_type'], sep, name)
    #2: do the same for all children
    for child_id in get_children_ids(branch_id, node_id):
        map_branch(branch_id, child_id, depth + 1)

To make this more convenient and reusable I included a set of functions, which encapsulate the API calls such as get_node_by_id() or get_children_ids().

Full source code is available below.

 

Content of the “DELETE_ME” solution’s the “Development” Branch:

Our “DELETE_ME” solution’s development branch contains Process “My Process” (containing a Functional Spec document, an Executable, 3 Test Cases and a Test Configuration) as well as a Process Step Reference “My Step” (containing another Func Spec document, two test case documents and another test case document inherited from the original process step).

We also will have to adjust the hostname, port, sap-Client, username, password, solution and branch variables in the demo code (below). The resulting output…

…can be copy/pasted to excel, to make it more readable:

The exported structure of the solution documentation is clearly recognizable.

We also notice, that there are some additional nodes, grouping sub-elements which are not displayed in the solution documentation. For example the process step references (REF_PROCSTEP) are not immediate sub elements of the process (PROC). Instead there is a node of type PROCSTEPGRP between the two, encapsulating all process step references. Same with nodes of type DOCUGRP, TESTGRP and EXECGRP encapsulating documents, test-resources and executables beneath process(-step reference) nodes.

This makes uploading/creating elements  particularly more difficult, as you will have to check whether or not there already is a fitting grouping node available (and if there is none, create one).

We also see, that the document inherited from the step original is not listed (as it is a child of the step original).

Conclusion & Outlook:

As the API allows not only possible to read / download structure but also documents/content. It also offers write/update-access to virtually all Solution Documentation elements – suggesting it can be used the following use-cases:

  • Mass-downloading documents (not the structure, but content – aka documents themselves)
  • Mass-updating Solution Documentation elements (i.e. change status of all test documents under scenario X in branch Y to “Released”)
  • Mass-uploading documents (i.e. Mass-upload documents to specific processes-nodes based on a CSV containing process-name and file name of the document(s) to upload).
    Uploading a document via the Process Management API would involve three steps:

    • Create an (empty) Knowledge Warehouse Object (“KWOBJ”)
      • API Call: CreateDocument
    • Upload content (the actual document) to the KWOBJ
      • API Call: DocumentContentImporter
    • Create an Entry in a Process Management Branch, which points to the new KWOBJ
      • API Calls: BranchContentImporter, BranchContent (as a prerequisite)

 

Full Source Code:

import json
import requests
import pprint

host = 'SOLMAN.HOSTNAME'    #SolMan Hostname or IP
port = '50000'  #Port (most likely 5000)
sap_client = '800'  #Client

user = 'USERNAME' #Username
passwd = 'PASSWORD' #Password

solution_name = 'DELETE_ME' #Solution Name
branch_name = 'DEVELOPMENT' #Branch Name


def get_solution_by_name (solution_name):
    #returns a list of solutions by name
    try:
        response = requests.get('http://'+ host + ':' + port + '/sap/opu/odata/sap/ProcessManagement/SolutionSet?sap-client=' + sap_client, auth=(user, passwd), headers=headers)
        response.raise_for_status()
    except requests.exceptions.HTTPError as errh:
        print("Http Error:", errh)
    except requests.exceptions.ConnectionError as errc:
        print("Error Connecting:", errc)
    except requests.exceptions.Timeout as errt:
        print("Timeout Error:", errt)
    except requests.exceptions.RequestException as err:
        print("Undefined Request Exception:", err)
    return list(filter(lambda s: s['SolutionName'] == solution_name, response.json()['d']['results']))

def get_branch_by_name (solution_id, branch_name):
    #returns a list of branches by name and solution_id
    response = requests.get('http://'+ host + ':' + port + '/sap/opu/odata/sap/ProcessManagement/BranchSet?sap-client=' + sap_client, auth=(user, passwd), headers=headers)
    return list(filter(lambda b: b['BranchName'] == branch_name and b['SolutionId'] == solution_id, response.json()['d']['results']))
    
def get_node_attribute_values(my_node, attribute_name):
    #returns the value of an attribute of a node_id
    res = []
    for my_attribute in my_node['attributes']:
        if my_attribute['attr_type'] == attribute_name: res = my_attribute['values']
    return res

def get_node_by_type(branch_id, node_type):
    #returns a list of nodes in a branch, having the specified node type
    response = client.get('http://'+ host + ':' + port + '/sap/opu/odata/sap/ProcessManagement/BranchContentSet(BranchId=\'' + branch_id + '\',ScopeId=\'SAP_DEFAULT_SCOPE\',SiteId=\'\',SystemRole=\'D\')/$value?sap-client=' + sap_client ,auth=(user, passwd),headers=headers)
    nodes_list = json.loads(list(filter(lambda s: s['section-id'] == 'NODES', response.json()['sections']))[0]['section-content'])
    return list(filter(lambda n: n['obj_type'] == node_type, nodes_list))

def get_children_ids(branch_id, node_id):
    #returns the list of children of a given node in a given branch
    response = client.get('http://'+ host + ':' + port + '/sap/opu/odata/sap/ProcessManagement/BranchContentSet(BranchId=\'' + branch_id + '\',ScopeId=\'SAP_DEFAULT_SCOPE\',SiteId=\'\',SystemRole=\'D\')/$value?sap-client=' + sap_client ,auth=(user, passwd),headers=headers)
    nodes_structure_list = json.loads(list(filter(lambda s: s['section-id'] == 'NODES-STRUCTURE', response.json()['sections']))[0]['section-content'])
    nodes_structure_list_filtered = list(filter(lambda x: x['parent_occ'] == node_id, nodes_structure_list))
    if len(nodes_structure_list_filtered) == 0:
        return []
    else:
        return list(filter(lambda x: x['parent_occ'] == node_id, nodes_structure_list_filtered))[0]['children']

def get_node_by_id(branch_id, node_id):
    #returns the specified node (by node_id and branch_id)
    response = client.get('http://'+ host + ':' + port + '/sap/opu/odata/sap/ProcessManagement/BranchContentSet(BranchId=\'' + branch_id + '\',ScopeId=\'SAP_DEFAULT_SCOPE\',SiteId=\'\',SystemRole=\'D\')/$value?sap-client=' + sap_client ,auth=(user, passwd),headers=headers)
    nodes_list = json.loads(list(filter(lambda s: s['section-id'] == 'NODES', response.json()['sections']))[0]['section-content'])
    return list(filter(lambda x: x['occ_id'] == node_id, nodes_list))[0]

def get_node_name_by_id(branch_id, node_id):
    #returns a node's name (based on node_id and branch_id)
    response = client.get('http://'+ host + ':' + port + '/sap/opu/odata/sap/ProcessManagement/BranchContentSet(BranchId=\'' + branch_id + '\',ScopeId=\'SAP_DEFAULT_SCOPE\',SiteId=\'\',SystemRole=\'D\')/$value?sap-client=' + sap_client ,auth=(user, passwd),headers=headers)
    names_list = json.loads(list(filter(lambda s: s['section-id'] == 'ELEMENT-NAMES', response.json()['sections']))[0]['section-content'])
    names_list_filtered = list(filter(lambda n: n['occ_id'] == node_id, names_list['D']))
    if len(names_list_filtered) == 0: return ''
    else: return names_list_filtered[0]['name']

def map_branch(branch_id, node_id, depth):
    #recursively walks through a branch and outputs all elements
    #1: output the current node
    sep = '\t'
    node = get_node_by_id(branch_id, node_id)
    description = ''.join(get_node_attribute_values(node, 'DESCRIPTION') + get_node_attribute_values(node, '_DESCRIPTION'))
    name = get_node_name_by_id(branch_id, node_id)
    print('\t' * depth + node['obj_type'], sep, name)
    #2: do the same for all children
    for child_id in get_children_ids(branch_id, node_id):
        map_branch(branch_id, child_id, depth + 1)

client = requests.session()
headers = {'X-CSRF-Token': 'Fetch', 'Accept': 'application/json', 'Content-Type': 'application/json'}

#get solution id (by name), branch id (by name) and root node (by type)
solution_id = get_solution_by_name(solution_name)[0]['SolutionId']
branch_id = get_branch_by_name(solution_id, branch_name)[0]['BranchId']
root_node = get_node_by_type(branch_id, 'ROOT')

#map branch starting at root node
map_branch(branch_id, root_node[0]['occ_id'], 0)
3 Comments
You must be Logged on to comment or reply to a post.
  • Great blog Thomas and a very good overview of the Process Management API.

    Are there any plans to make these APIs available on the API hub (https://api.sap.com/)?

    I think this would be very helpful as some APIs are not self explanatory. E.g. I have experimented with the CreateDocument API.

    This one requires the X-CSRF-Token to be set in the header. Actually, I have tried this by first calling the API with a get and then with a post but the post fails with “CSRF token validation failed” and an “http 403 Forbidden”.

    Thanks, Klaus

    • Hi Klaus,

      thanks for your feedback!

      Didn’t know about the API hub so far. I’ll pass on your request to the owners of the API.

      Took me some experimenting to figure out the token mechanics as well.

      You can obtain the token on successful authentication by putting “x-csrf-token = fetch” in your request header. You only need to send the token with requests which make changes to Solution Documentation.

      Have a look at my latest article for an example.

       

      BR,

       

      Thomas

       

  • Hi Thomas Stoffels ,

    Do you have any plans for webhooks? The API is great for one-time exchange of information but lacks real-time sync (e.g. updating a folder name in Solution Documentation). At the moment I guess the only option is to reload the whole hierarchy and compare the delta at endpoint?

    Thanks!