Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
TSTOFFELS
Explorer
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=...

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)
7 Comments
S0007726612
Explorer
0 Kudos
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
TSTOFFELS
Explorer
0 Kudos
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

 
sabatale
Contributor
0 Kudos

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!

0 Kudos
Hi Thomas Stoffels,

Thanks for the great exemplified use of the proccessmanagement api, it is very usefull.

I tried this on the demo site , but alas ends with https://service.sap.com/sap/support/notes/1797736 in this case meaning Authorization missing for Branch 'PRODUCTION', tried with the other branches aswell with Agatha Bauer ,

any plans for allowing read acces for the demo site ?

https://solman.almdemo.com/sap/opu/odata/sap/ProcessManagement/BranchContentSet(BranchId='2W{J}lVb7jMi{yMlWfLWf0',ScopeId='Show All',SiteId='',SystemRole='D')/$value?sap-client=001)

Thanks ,Thue
AndreasManthey
Advisor
Advisor
0 Kudos
Hi Thomas,

Did you ever write that post about "Mass-uploading documents" you mentioned in "Conclusion & outlook"? Couldn't find it via the search function...

I'm particularly interested in how to upload a document to one parent and a reference to that document to another parent. That's because I need to translate from our world of multi-referenced objects (where all parents reference the same original object) to the SolMan world, where there's one original object and possibly multiple links to it.

The API docu wasn't very helpful with this and inspecting the traffic from the web UI ("insert assigment")  wasn't either, because there are no direct API calls.

Thanks, Andreas.
Markus7
Participant
0 Kudos
Hello Thomas

Great blog - it already helped me a lot!

Since I am not very firm with Python, could you please tell me how I can get your example ("Full Source Code" above) to work? If I need to use https, do I need to replace "http://" by "https://"?

When calling the script I get:

Error Connecting: HTTPSConnectionPool(host='host', port=12345): Max retries exceeded with url: /sap/opu/odata/sap/ProcessManagement/SolutionSet?sap-client=001 (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:992)')))
Traceback (most recent call last):
File "C:\...\Test.py", line 89, in <module>
solution_id = get_solution_by_name(solution_name)[0]['SolutionId']
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\...\Test.py", line 29, in get_solution_by_name
return list(filter(lambda s: s['SolutionName'] == solution_name, response.json()['d']['results']))
^^^^^^^^
UnboundLocalError: cannot access local variable 'response' where it is not associated with a value

Any help would be highly appreciated!

Best regards
Markus
Markus7
Participant
0 Kudos
... got it working ...