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

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)


3 Comments