Technical Articles
Automatically Add Members to Spaces in SAP Data Warehouse Cloud Using @sap/dwc-cli
This blog post is part of a series of blogs I published about @sap/dwc-cli. Find all blog posts related to this topic in my overview blog post here.
The @sap/dwc-cli Node.js module is available since wave 2021.20. In another blog “New Command-Line Interface for SAP Data Warehouse Cloud – code your way to the cloud!” I describe how to install and use the command-line interface (CLI). With this blog I’m introducing you to one of the most asked use cases for the CLI: automated mass-member assignment to a space in SAP Data Warehouse Cloud. Wow! 🙂
Introduction
This blog is part of a blog post series about the Command-Line Interface (CLI) for SAP Data Warehouse Cloud. Find the full overview of all available blogs in the overview blog post here.
Project Setup
$ node -v
v12.21.0
$ mkdir cli-add-users
$ cd cli-add-users
$ npm init --yes
{
"name": "dwc-cli-add-users",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
$ touch index.js
cli-add-users
|- package.json
|- index.js
Tipp: If you want to push your code to a git repository, you also want to create a .gitignore file in the root folder with the following content:
// .gitignore
node_modules
Installing Dependencies
$ npm install @sap/dwc-cli fs-extra puppeteer path
$ npm install -g @sap/dwc-cli
Creating the Executable Skeleton
// index.js
(async () => {
console.log('I was called!');
})();
$ node index.js
I was called!
Defining the Environment
- retrieve the definition of an existing space and store it locally
- read the `*.json` file containing the additional members to add to the space
- update the locally stored space definition file with the additional members
- push the updated space definition back to our SAP Data Warehouse Cloud tenant
- where the `*.json` file containing the new members is located
- where the space definition shall be stored locally
- the URL of the SAP Data Warehouse Cloud tenant
- the passcode URL of the SAP Data Warehouse Cloud tenant
- which space to update
- a business username and password used for reading and updating the space in the SAP Data Warehouse Cloud tenant
const MEMBERS_FILE = 'members.json';
const SPACE_DEFINITION_FILE = 'space.json';
const DWC_URL = 'https://<prefix>.<region>.hcs.cloud.sap/'; // eg https://mytenant.eu10.hcs.cloud.sap/
const DWC_PASSCODE_URL = 'https://<prefix>.authentication.<region>.hana.ondemand.com/passcode'; // eg https://mytenant.authentication.eu10.hana.ondemand.com/passcode
const SPACE = '<space ID>';
const USERNAME = '<username>'; // eg firstname.lastname@company.com
const PASSWORD = '<password>';
(async () => {
console.log('I was called!');
})();
Retrieving the Passcode
const puppeteer = require("puppeteer");
const MEMBERS_FILE = 'members.json';
[...]
const PASSWORD = '<password>';
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(DWC_PASSCODE_URL);
await page.waitForSelector('#logOnForm', {visible: true, timeout: 5000});
if (await page.$('#logOnForm') !== null) {
await page.type('#j_username', USERNAME);
await page.type('#j_password', PASSWORD);
await page.click('#logOnFormSubmit');
}
await page.waitForSelector('div.island > h1 + h2', {visible: true, timeout: 5000});
const passcode = await page.$eval('h2', el => el.textContent);
console.log('passcode', passcode);
await browser.close();
})();
$ node index.js
passcode qjm4Zlrza2
[...]
const PASSWORD = '<password>';
let page;
const getPasscode = async () => {
await page.waitForSelector('div.island > h1 + h2', {visible: true, timeout: 5000});
await page.reload();
return await page.$eval('h2', el => el.textContent);
}
(async () => {
const browser = await puppeteer.launch({ headless: false });
page = await browser.newPage();
await page.goto(DWC_PASSCODE_URL);
await page.waitForSelector('#logOnForm', {visible: true, timeout: 5000});
if (await page.$('#logOnForm') !== null) {
await page.type('#j_username', USERNAME);
await page.type('#j_password', PASSWORD);
await page.click('#logOnFormSubmit');
}
const passcode = await getPasscode();
console.log('passcode', passcode);
await browser.close();
})();
Reading the Space Definition
const puppeteer = require("puppeteer");
const path = require('path');
const exec = require("child_process").exec;
[...]
return await page.$eval('h2', el => el.textContent);
}
const execCommand = async (command) => new Promise(async (res, rej) => {
const passcode = await getPasscode();
const cmd = `node ${path.join(process.cwd(), 'node_modules', '@sap', 'dwc-cli', 'index.js')} ${command} -H ${DWC_URL} -p ${passcode}`;
exec(cmd, (error, stdout, stderr) => {
if (error) {
rej({ error, stdout, stderr });
}
res({ error, stdout, stderr });
});
});
(async () => {
const browser = await puppeteer.launch({ headless: false });
[...]
Please note: With version 2022.2.0 the call to the main.js file changed. Previously, it looked like this (the dist segment got removed and main.js was renamed to index.js with 2022.2.0): const cmd = `node ${path.join(process.cwd(), ‘node_modules’, ‘@sap’, ‘dwc-cli’, ‘dist’, ‘main.js’)} ${command} -H ${DWC_URL} -p ${passcode}`;
[...]
await page.click('#logOnFormSubmit');
}
await execCommand(`spaces read -S ${SPACE} -o ${SPACE_DEFINITION_FILE}`)
await browser.close();
})();
{
"<Space ID>": {
"spaceDefinition": {
"version": "1.0.4",
[...]
"members": [],
[...]
}
}
}
Adding New Members
- create a file members.json containing the list of members we want to add to the space,
- read and parse the file containing the list of members using fs-extra.readFile() and JSON.parse(),
- merge the list with the list of already assigned members as defined in the space definition file we retrieved from the tenant,
- store the updated members list in the local file using fs-extra.writeFile() and
- push the updated space definition back to the tenant.
[
{
"name": "<User ID 1>",
"type": "user"
},
{
"name": "<User ID 2>",
"type": "user"
},
{
"name": "<User ID 3>",
"type": "user"
},
[...]
]
[...]
const exec = require("child_process").exec;
const fs = require('fs-extra');
[...]
await execCommand(`spaces read -S ${SPACE} -o ${SPACE_DEFINITION_FILE}`);
const spaceDefinition = JSON.parse(await fs.readFile(SPACE_DEFINITION_FILE, 'utf-8'));
const additionalMembers = JSON.parse(await fs.readFile(MEMBERS_FILE, 'utf-8'));
await browser.close();
})();
[...]
const spaceDefinition = JSON.parse(await fs.readFile(SPACE_DEFINITION_FILE, 'utf-8'));
const additionalMembers = JSON.parse(await fs.readFile(MEMBERS_FILE, 'utf-8'));
spaceDefinition[SPACE].spaceDefinition.members = spaceDefinition[SPACE].spaceDefinition.members.concat(additionalMembers);
await fs.writeFile(SPACE_DEFINITION_FILE, JSON.stringify(spaceDefinition, null, 2), 'utf-8');
await browser.close();
})();
[...]
await fs.writeFile(SPACE_DEFINITION_FILE, JSON.stringify(spaceDefinition, null, 2), 'utf-8');
await execCommand(`spaces create -f ${SPACE_DEFINITION_FILE}`);
await browser.close();
})();
Conclusion
const puppeteer = require("puppeteer");
const path = require('path');
const exec = require("child_process").exec;
const fs = require('fs-extra');
const MEMBERS_FILE = 'members.json';
const SPACE_DEFINITION_FILE = 'space.json';
const DWC_URL = 'https://<prefix>.<region>.hcs.cloud.sap/'; // eg https://mytenant.eu10.hcs.cloud.sap/
const DWC_PASSCODE_URL = 'https://<prefix>.authentication.<region>.hana.ondemand.com/passcode'; // eg https://mytenant.authentication.eu10.hana.ondemand.com/passcode
const SPACE = '<space ID>';
const USERNAME = '<username>'; // eg firstname.lastname@company.com
const PASSWORD = '<password>';
let page;
const getPasscode = async () => {
await page.waitForSelector('div.island > h1 + h2', {visible: true, timeout: 5000});
await page.reload();
return await page.$eval('h2', el => el.textContent);
}
const execCommand = async (command) => new Promise(async (res, rej) => {
const passcode = await getPasscode();
const cmd = `node ${path.join(process.cwd(), 'node_modules', '@sap', 'dwc-cli', 'index.js')} ${command} -H ${DWC_URL} -p ${passcode} -V`;
exec(cmd, (error, stdout, stderr) => {
if (error) {
rej({ error, stdout, stderr });
}
res({ error, stdout, stderr });
});
});
(async () => {
const browser = await puppeteer.launch({ headless: true });
page = await browser.newPage();
await page.goto(DWC_PASSCODE_URL);
await page.waitForSelector('#logOnForm', {visible: true, timeout: 5000});
if (await page.$('#logOnForm') !== null) {
await page.type('#j_username', USERNAME);
await page.type('#j_password', PASSWORD);
await page.click('#logOnFormSubmit');
}
await execCommand(`spaces read -S ${SPACE} -o ${SPACE_DEFINITION_FILE}`);
const spaceDefinition = JSON.parse(await fs.readFile(SPACE_DEFINITION_FILE, 'utf-8'));
const additionalMembers = JSON.parse(await fs.readFile(MEMBERS_FILE, 'utf-8'));
spaceDefinition[SPACE].spaceDefinition.members = spaceDefinition[SPACE].spaceDefinition.members.concat(additionalMembers);
await fs.writeFile(SPACE_DEFINITION_FILE, JSON.stringify(spaceDefinition, null, 2), 'utf-8');
await execCommand(`spaces create -f ${SPACE_DEFINITION_FILE}`);
await browser.close();
})();
Please note: With version 2022.2.0 the call to the main.js file changed. Previously, it looked like this (the dist segment got removed and main.js was renamed to index.js with 2022.2.0): const cmd = `node ${path.join(process.cwd(), ‘node_modules’, ‘@sap’, ‘dwc-cli’, ‘dist’, ‘main.js’)} ${command} -H ${DWC_URL} -p ${passcode}`;
Further Reading
Blog: New Command-Line Interface for SAP Data Warehouse Cloud – code your way to the cloud!
Command-Line Interface for SAP Data Warehouse Cloud on npmjs.com
Command-Line Interface for SAP Data Warehouse Cloud on SAP Help
Hi Jascha Kanngiesser,
Thank you for this npm module and I can see great potential with this . Would it be possible in the near future to able to create a table using json ( csn ) ? Ideal scenario would be combining this with the existing cap library would be really handy
Thank you
Afshin
Hi Afshin Irani thank you for your feedback and that you see great potential in this tool! Deploying entities within spaces like tables and views is on our bucket list for sure. We didn't yet consider the CAP library aspect but I noted it down. Stay tuned for updates!
Hi Jascha
Thanks for sharing, I'm exploring this at the moment.
I'm having a little trouble making it work with a third party IDP for login to DWC (Azure AD).
It stops at the microsoft sign in (email)
Do you have any idea how to change the coding for this?
Br
Bjørn
Hi Bjørn,
I'm happy to dive into this problem deeper through e-mail if you like. Feel free to send more details to jascha.kanngiesser@sap.com!
Thanks,
Jascha
First of all this is great article, specially when you have to add 100s of users in space, however I believe it is quite dangerous to use this approach for day to day user maintenance when new user come on board since it requires to modify space every time user is added along with existing contents. Space is largest object within DWC and I would be uncomfortable changing it on a daily basis. It would be nice if SAP come up with APPEND approach vs Overwrite.
Hi,
From my experience, many things can go wrong in the code above; here is an easy way tutorial:
Provide a token password when asked.
Then, modify and save the space.json file with additional users.
Next, execute the below code;
Kind regards,
Sebastian