Skip to Content
Technical Articles

Mass-updating Solution Documentation via the Process Management API

Summary

This article describes an approach to update Elements in Solution Documentation via the SAP Solution Manager’s Process Management API and concludes with a basic prototype of a mass-update script in Python.

This is a continuation of the previous article Process Management API in SAP Solution Manager, which covers how to read from Solution Documentation via the API.

 

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

 

How does updating Solution Documentation Elements work?

In a nutshell, updating SolDoc Elements requires the following steps:

  1. Download branch content as json (API Call: BranchContentSet)
  2. Json decode branch content
  3. Modify branch content
  4. Json re-encode modified branch content
  5. Upload modified branch content back to Solution Manager (API Call: BranchContentImporterSet)

In order to make any changes to Solution Documentation Elements a token is required.
This token can be “fetched” on successful authentication and retrieved from the response header.

Simple Python example:

# example: authenticate + obtain token (http-get)
headers = {'X-CSRF-Token': 'Fetch', 'Accept': 'application/json', 'Content-Type': 'application/json'}
response = client.get('http://' + host + ':' + port + '/sap/opu/odata/sap/ProcessManagement?sap-client=' + sap_client, auth=(user, passwd), headers=headers)
token = response.headers['x-csrf-token']

The branch content can be retrieved via the BranchContentSet Call and then json-decoded:

# example: retrieve and json-decode branch content (http-get)
headers = {'Accept': 'application/json', 'Content-Type': 'application/json'}
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)
branch_content = response.json()

It will contain multiple sections, such as NODES (all the SolDoc Elements: Documents, Folders, Processes, … and their properties) and NODES-STRUCTURE (the hierarchic parent/child relationships linking the NODES).
In order to make changes to specific Nodes we simply need to check each node for whether it matches our selection criteria and (if so) modify its properties it accordingly.

# example: loop through nodes 
for section in branch_content['sections']:
    if section['section-id'] == 'NODES':
        nodes_list = json.loads(section['section-content'])  # json-de-code section-content, as it is a string with an extra layer of json-encoding
        for my_node in nodes_list:
            # --- check & modify node here ---
        section['section-content'] = json.dumps(nodes_list) # json-re-encode modified section-content

 

Simplified JSON representation of a NODE (A Test Case, in this example):

After that we will json-re-encode the modified branch content and upload it (as put-data) back to solution manager, this time supplying the token via the request header:

# example: json-re-encode branch content and upload to solman (http-put)
data = json.dumps(branch_content)
headers = {'X-CSRF-Token': token, 'Accept': 'application/json', 'Content-Type': 'application/json'}
url = 'http://'+ host + ':' + port + '/sap/opu/odata/sap/ProcessManagement/BranchContentImporterSet(BranchId=\'' + branch_id + '\',ChangeDocumentId=\'' + change_document_id + '\')/$value'
response = client.put(url, data=data, headers=headers)

 

Prototype for a Mass-Update:

In order to keep things simple and, this prototype covers only the most basic selection criteria (all nodes matching these criteria will be updated) and only one attribute to be updated:

Selection Criteria:

  • Object Type
  • Description
  • Status

Attributes to be updated:

  • Status

For this prototype, we want to select all Test Documents, which have a Description starting with “New” and Status “Copy Editing”. We want to update the status of all matching nodes to “Released”:

# selection criteria (which nodes to update?)
select_obj_type = 'TESTDOCUMENT' # object type i.e. DOCUMENT, TESTDOCUMENT, PROC, ...
select_attr_description = re.compile('New.*') # description - Regular Expression or exact match (case sensitive!)
select_attr_smd_state = '0COPY_EDITING' # status i.e. 0IN_PROGRESS, 0COPY_EDITING, 0RELEASED, ...

# new attribute value(s) to be set (for nodes matching the selection criteria)
update_attr_smd_state = '0RELEASED' # status (see above)

 

If everything works correctly, the output looks like this:
Basically we are printing 2 lines per matching node (before and after update) to the console.
In our Demo Solution, 3 matching Test Documents have been found and updated:

(My apologies for the sliced screenshot, but posting it as a single image would make it unreadable).

 

The changes can be verified in Solution Documentation, List View:

“New Testcase B2” has not been updated, because it had Status “In Progress” and thus did not match our selection criteria.

 

Full Source Code:

Before attempting to make changes to a productive solution manager, please consider:

  • This code will update all matching documents of a branch, including the library.
  • There is no undo, and it is rather easy to make unintended changes, so use at your own risk.
  • I strongly recommend to simulate first:
    If the flag SIMULATE_ONLY is set, the changes will not be uploaded to Solution Manager.
    Only turn SIMULATE_ONLY off, after the simulation looks good.
import json
import requests
import re
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

change_document_id = '' # only needed if branch is under change control

SIMULATION_ONLY = False # simulate first (no import) then change this flag to False to perform actual import

# selection criteria (which nodes to update?)
select_obj_type = 'TESTDOCUMENT' # object type i.e. DOCUMENT, TESTDOCUMENT, PROC, ...
select_attr_description = re.compile('New.*') # description - Regular Expression or exact match (case sensitive!)
select_attr_smd_state = '0COPY_EDITING' # status i.e. 0IN_PROGRESS, 0COPY_EDITING, 0RELEASED, ...

# new attribute value(s) to be set (for nodes matching the selection criteria)
update_attr_smd_state = '0RELEASED' # status (see above)



def get_solution_by_name (solution_name):
    # returns a list of solutions by name
    response = requests.get('http://'+ host + ':' + port + '/sap/opu/odata/sap/ProcessManagement/SolutionSet?sap-client=' + sap_client, auth=(user, passwd), headers=headers)
    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


#Check connection and retrieve token
client = requests.session()
headers = {'X-CSRF-Token': 'Fetch', 'Accept': 'application/json', 'Content-Type': 'application/json'}
try:
    response = client.get('http://' + host + ':' + port + '/sap/opu/odata/sap/ProcessManagement?sap-client=' + sap_client, auth=(user, passwd), headers=headers)
    response.raise_for_status()
except requests.exceptions.RequestException as e:
    print('Connection unsuccessful:\n', e)
    exit(1)

token = response.headers['x-csrf-token']
print('Connection successful. Received token:', token)

# get solution id (by name), branch id (by name)
solution_id = get_solution_by_name(solution_name)[0]['SolutionId']
branch_id = get_branch_by_name(solution_id, branch_name)[0]['BranchId']

print('Fetching branch content...', end='')
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)
branch_content = response.json()
print('done.')

match_counter = 0

# get the section-content of the NODES section...
for section in branch_content['sections']:
    if section['section-id'] == 'NODES':
        nodes_list = json.loads(section['section-content'])  # json-de-code section-content, as it is a string with an extra layer of json-encoding
        # ... and check for each node whether it matches the selection criteria
        for my_node in nodes_list:
            if my_node['obj_type'] == select_obj_type: # object tye matches selection criteria?
                if get_node_attribute_values(my_node, '_SMD_STATE')[0] == select_attr_smd_state: # status matches  selection criteria?
                    if re.search(select_attr_description, get_node_attribute_values(my_node, '_DESCRIPTION')[0]): # description matches selection criteria?
                        #pprint.pprint(my_node)
                        # ...if there is a match: update the node
                        match_counter-=-1 #;-)
                        print('Match ' + str(match_counter) + ' (before): \t' + str(my_node))
                        # each node's "attributes" is a list of dictionaries (each one having an attr_type and a value). To target the dictionary we want (having attr__type = _SMD_STATE) we need its index in the list.
                        attr_smd_state_index = next((index for (index, attribute) in enumerate(my_node['attributes']) if
                                                     attribute["attr_type"] == "_SMD_STATE"), None)
                        my_node['attributes'][attr_smd_state_index]['values'] = [update_attr_smd_state]
                        print('Match ' + str(match_counter) + ' (after ): \t' + str(my_node))

        print('Total matching nodes found: \t' + str (match_counter))
        section['section-content'] = json.dumps(nodes_list) # json-re-encode modified section-content

if SIMULATION_ONLY:
    print('Simulation only --> skipping Import.')
    exit(0)

# if this is not a simulation run: import changes to solution manager
url = 'http://'+ host + ':' + port + '/sap/opu/odata/sap/ProcessManagement/BranchContentImporterSet(BranchId=\'' + branch_id + '\',ChangeDocumentId=\'' + change_document_id + '\')/$value'
data = json.dumps(branch_content)
headers = {'X-CSRF-Token': token, 'Accept': 'application/json', 'Content-Type': 'application/json'}

print('Importing modified branch content (expecting http 204)...: ', end='')
response = client.put(url, data=data, headers=headers)
print(response)


1 Comment
You must be Logged on to comment or reply to a post.
  • Hi Thomas,

    Thank you very much for providing the process description and the source code for changing test documents. Very helpful!

    Some thoughts/ideas for future examples:

    1. SAP offers a public Solution Manager system 7.2. Would be super if the examples are based on this public system so that the examples can be reproduced on customer side.
    2. An example how to upload “real” documents like Word- or Excel would be helpful. From the API definition it is not very clear how this works.

    BR, Klaus