Server-Side Scripting#
Note
This page has been migrated from the old documentation, and has not yet been fully revised. There might be inconsistencies or errors when using with current LinkAhead versions.
Introduction#
Server-Side Scripting is a LinkAhead feature which allows to install executables in the LinkAhead Server and trigger the execution via the Server-Side Scripting API.
Small computation task, like some visualization, might be easily implemented in Python or some other language, but cumbersome to integrate into the server. Furthermore, the LinkAhead server should stay a general tool without burden from specific projects. Also, one might want to trigger some standardized processing task from the web interface for convenience. For these situations, server side scripting can be used.
The basic idea is that a script or program (script in the following) can be called to run on the server (or elsewhere in future) to do some calculations. This triggering of the script is done over the API so it can be done with any client. Input arguments can be passed to the script and STDOUT and STDERR are returned.
Each script is executed in a temporary home directory, which is automatically cleaned up. Scripts can store files in a special folder and provide a link that allows to download these afterward.
This section contains hands-on, step-by-step instructions on:
How to trigger scripts
How to implement a server-side script:
How to configure the permissions:
Please also consider to look at the more technical specification of the API.
Recipes#
Install a Minimal Script#
This is our minimal example script, minimal.bash:
#!/bin/bash
printf "$0"
printf " %s" "$@"
printf "\n"
Move it to your scripting directory. By default, this in the
custom/caosdb-server/scripting/bin/directory under your profile directory.Add executable permissions at least for the user account of the server (user in the docker container, see
user_groupin theprofile.yml):chmod +x custom/caosdb-server/scripting/bin/minimal.bash
Note
The scripting directories can be configured via theSERVER_SIDE_SCRIPTING_BIN_DIRS option of the
server. See Server Configuration for further information.
In principle, the script can be invoked via the scripting API now after restarting the server. You can trigger the script via a browser based HTML form or by using the LinkAhead python library. See below for more on that.
If you face any issues try the Deep Dive section which explains how to test and debug the installation and how exactly scripts are being called and parameter and files are being passed to the script by the server.
How to Trigger Scripts#
Server side scripts are triggered by sending a POST to the /scripting resource using a
multipart/form-data or application/x-www-form-urlencoded content type. There are the following
arguments that can be provided via the requests fields.
call: the name of the script to be called-pN: positional arguments (e.g.-p0,-p1etc.)-ONAME: named arguments (e.g.-Otest,-Onumberetc.)
The arguments will be passed on to the script. See the Writing Server-Side Scripts section or take the Deep Dive for more information.
Simple, Static Form#
The scripting API was designed to interact with HTML forms.
A simple HTML form like this
<h1>Trigger Analysis</h1>
<form action="/scripting" method="post">
<input type="hidden" name="call" value="minimal.bash"/>
<input type="hidden" name="-p0" value="analyze"/>
<label>Some parameter<input type="text" name="-p1"/></label>
<label>Algorithm<input type="text" name="-Oalgorithm" value="fast"/></label>
<input type="submit" value="Submit">
</form>
will trigger a call to the scripting API when submitted. The server will execute
minimal.bash --auth-token=[...] --algorithm=fast analyze [...] with the respective parameters the
user specified in the form.
To install a static web form like this one, just store this html file to
custom/caosdb-server/caosdb-webui/src/ext/html/minimal.html.
Then (re-)start the server. You can browse to
https://localhost:10443/webinterface/html/minimal.html where the webform will be served.
Note
Usually you need to sign in first, otherwise you might not have the permissions to execute the
script. In this case, browse to https://localhost:10443 first.
Note
While static forms are a straightforward way to trigger scripts via the browser they produce rather
ugly responses. See the section on the form_elements module for other means to create web forms
and present the script’s output in a way that is more pleasing to the eye.
Using the form_elements module.#
The linkahead-webui contains the form_elementsmodule. It provides functionality to dynamically
generate forms which interact with the scripting API and present the script’s results to the user.
A configuration for a simple form triggering the minimal.bash script might look like this:
const config = {
script: "minimal.bash",
fields: [
{ type: "integer", name: "number", label: "A Number", required: true },
{ type: "date", name: "date", label: "A Date", required: false },
{ type: "text", name: "comment", label: "A Comment", required: false },
],
};
The data from this kind of forms will be converted into a json file form.json and uploaded to the
scripting API. The script will be called as
minimal.bash --auth-token=[...] ./upload_files/form.json
This sure is an opinionated implementation as it only calls scripts in a very particular way and
uses a json file instead of passing the parameters directly to the script. However, forms generated
by the form_elements module may contain deeply nested fields which is not conveniently mapping to
the flat structure of optional and positional parameters.
Note
This section is obviouly just a teaser. Find more information about forms for triggering server-side scripts in the webui documentation.
Note
The form_elements module of the webui works particularly good for python scripts using the
convenience methods of the caosadvancedtools package. See next section.
Using a Python Script#
The linkahead-pylib library comes with convenient functionality for triggering server-side scripts. This can be as easy as this:
from linkahead.utils.server_side_scripting import run_server_side_script
with open("test_file.txt", "w") as f:
f.write("this is a test")
response = run_server_side_script("my_script.py",
"pos0",
"pos1",
option1="val1",
option2="val2",
files={"-Ofile": "test_file.txt"})
assert response.stderr is None
assert response.code == 0
assert response.call == ('my_script.py '
'--option1=val1 --option2=val2 --file=.upload_files/test_file.txt '
'pos0 pos1')
You can try this out using for example the diagnostics.py script (it is part of
the LinkAhead server repository
and is also available on https://demo.indiscale.com). The script returns information about the
server in JSON format. You can do for example the following:
import json
from linkahead.utils.server_side_scripting import run_server_side_script
response = run_server_side_script('administration/diagnostics.py')
print("JSON content:")
print(json.loads(response.stdout))
print("stderr:")
print(response.stderr)
See linkahead.utils.server_side_scripting
module for more information.
Writing Server-Side Scripts#
Up until now this document has been about triggering server-side scripts, mainly. This section is about writing convenient scripts which are to be deployed and executed as server-side scripts.
Minimal Requirements#
Be executable. The server will spawn a process and execute the script as the server user. So the script needs to be executable for the server user.
Be located in the correct directory. The server needs to find the script in one of it’s configure
SERVER_SIDE_SCRIPTING_BIN_DIRS.
How to Find Uploaded Files#
The uploaded files are being stored in the ./upload_files directory, relative to the working
directory of the server-side script.
The script may search in that directory for a particular file name pattern. However, if the HTTP
request to the scripting API (using multipart/form-data type) specified the name of the file field
the way described above (as optional or positional parameters) the files location is passed to the
script via the respective parameter. This is the recommended approach.
How to Create a Downloadable File#
The runtime environment of the script has a special SHARED_DIR variable pointing to a directory
where the script may dump files. If the script wants to output links to the files for downloading it
can construct a relative link like this:
/${CONTEXT_ROOT_OF_THE_SERVER}/Shared/${SHARED_DIR}/${FILE_NAME}
Note
The Caosadvancedtools package provides a convenience function for this under
caosadvancedtools.serverside.helper.get_shared_filename. See below for more information.
How to Invoke Transactions#
To insert, retrieve, update or delete entities from within a script, use a suitable client, e.g. the python client.
Note
The LinkAhead python client library is pre-installed in every LinkAhead instance and can be used by server-side scripts right away.
How to Send an Email#
If LinkAhead is configured appropriately, a sendmail client can be accessed to send emails by a
script.
Note
The caosadvancedtools package provides a convenience function for this under
caosadvancedtools.serverside.helper.send_mail. See below for more information.
Using the LinkAhead Python Library#
The LinkAhead python client library is pre-installed such that you can use it for writing scripts right away.
In order to configure the connection to the server you need to place a .pylinkahead.ini file at
custom/caosdb-server/scripting/home/.pylinkahead.ini with the following content (or similar):
# -- custom/caosdb-server/scripting/home/.pylinkahead.ini
[Connection]
url = https://localhost:10443
cacert = /opt/caosdb/cert/caosdb.cert.pem
#ssl_insecure = True
[Misc]
#optional:
#sendmail=/usr/sbin/sendmail
Use the hostname in the URL for which the certificate is created and the correct port. If the
certificate is signed by a well-known CA you may leave cacert blank since the certificate can then
be verified without the file.
Note
The script is executed inside Docker. Thus, the URL should be publicly valid (i.e. the external
hostname and port) or at least valid inside the container (which is typically the one given above
with localhost). If you cannot make it work because of ssl errors try to add ssl_insecure = True
to the
Now you can use the Auth Token which is being passed to the script to configure the authentication and use the library:
import linkahead as la
from caosadvancedtools.serverside import helper
def main():
parser = helper.get_argument_parser()
args = parser.parse_args()
auth_token = args.auth_token
la.configure_connection(auth_token=auth_token)
la.execute_query("FIND Experiment")
...
Note
We use Caosadvancedtools for parsing the parameters passed to the script. See the next section for more information.
Using the Convenience Methods of Caosadvancedtools#
The python package caosadvancedtools comes with the module caosadvancedtools.serverside which
provides functionality for sending mails, making files available for download, rendering the output
as HTML, and much more.
Note
See the documentation of caosadvancedtools for more information.
Auth Token#
Caosadvancedtools come with a special argument parser which parses the Auth Token right away:
import linkahead as la
from caosadvancedtools.serverside import helper
def main():
parser = helper.get_argument_parser()
args = parser.parse_args()
auth_token = args.auth_token
la.configure_connection(auth_token=auth_token)
la.execute_query("FIND Experiment")
...
If for some reason you need information about the user who called the script (such as name, roles,
or realm), you will find that the linkahead.get_connection().get_username() method will not work
because the connection is configured with an auth token. However, you can still access the user
information using the Info resource and the UserInfo (see
pylib API documentation ) therein:
import linkahead as la
inf = la.Info()
username = inf.user_info.name
Using the Logger for Output#
Caosadvancedtools comes with a convenient function for configuring the native python logging for html output. This helps to produce pretty output for users calling the script via the webinterace.
...
import logging
def main():
# setup logging and reporting
configure_server_side_logging("my-logger")
...
logger = logging.getLogger("my-logger")
logger.info("The script terminated successfully.")
This renders as
<div class="alert alert-info alert-dismissible" role="alert">The script terminated successfully.</div>
Send an Email#
You can use Caosadvancedtools to send an email via the sendmail daemon. You need to configure
LinkAhead appropriately, and add sendmail=/usr/sbin/sendmail to the [Misc] section of the
pylinkahead.ini of the server-side scripting home directory.
Now you can do
def notify_user(user_email, subject, message):
helper.send_mail(from_addr="admin@example.com", to=user_email, subject=subject, body=message)
Uploaded File#
Caosadvancedtools integrates particularly well with the form_elements module of the webui.
To read the json file provided by the form_elements trigger request you can use the argument
parser as well:
...
import json
def main():
parser = helper.get_argument_parser()
args = parser.parse_args()
# Read the input from the form (form.json)
with open(args.filename) as form_json:
form_data = json.load(form_json)
...
Create Files for Download#
Caosadvancedtools has a helper method named get_shared_filename. It generates a pair of paths. The
first path is intended for generating the download links for the user. The second is intended for
opening and write the file:
...
def write_output_file(result):
display_path, internal_path = helper.get_shared_filename("output.json")
with open(internal_path, 'w') as f:
json.dump(result, f)
# use the logger to inform the user where the can download the file
logger.info(f"Download the <a href=/Shared/{display_path}>output.json</a>.")
...
Changing Permissions#
Up until now, our examples only worked for users with the administration role. If anyone else tried to trigger a script they most likely got a permission error. This is the case when a user lacks the appropriate permissions to call a script.
Permissions to Call a Script#
You can allow users to run a particular script by granting the role permission
SCRIPTING:EXECUTE:?PATH? where the path is the path of the script in the server’s scripting
directory (SERVER_SIDE_SCRIPTING_BIN_DIRS).
Note
The path can also specify a glob pattern using *. E.g.SCRIPTING:EXECUTE:* is the permission to
execute any script. SCRIPTING:EXECUTE:my_scripts/* is the permission to execute any script under
./my_scripts/ in one of the servers scripting directories.
Note
Find more information about permissions in general and how to grant them in the server docs on permissions.
Permissions of the Auth Token passed to the Script#
By default, the Auth Token passed to the script as the --auth-token optional parameter
authenticates the script against the server as the user who called the script in the first place.
However, there are use cases, where we want the script to have 1) more permissions than the caller or use a different user. One example are scripts which are to be called by unprivileged users or the anonymous user which need to update entities. This is a pattern to allow unprivileged users to exactly one operation (like adding a new entry to a list) but nothing else.
In this case, you can configure the authtoken.yaml (server option: AUTHTOKEN_CONFIG) to issue a
more powerful Auth Token for the script. See the server’s documentation for the AUTHTOKEN_CONFIG
for more information.
Example: Putting it All Together#
The sections show how to create a web form using form_element which triggers a server-side script.
The script will be executable for any user having the team role, and will act with
administration permissions.
Create the HTML Form#
It has one field which is a file upload with the name “csvfile” and one text field with the name “myparameter”.
The javascript snippet to create such a form using the webUI’s form_elements module looks like
this:
// -- trigger_sss_form.js
const form_config = {
script: "handle_csv.py",
fields: [{
type: "text",
name: "myparameter",
label: "My Parameter",
required: true,
help: "Define a parameter",
}, {
type: "file",
name: "csvfile",
label: "CSV Table",
required: true,
accept: "*.csv",
help: "Upload a CSV Table",
}, ],
};
// create the form
const form = form_elements.make_form(form_config);
// now inject it into the document
document.body.append(form);
This snippet needs to be installed under
custom/caosdb-server/caosdb-webui/src/ext/js/trigger_sss_form.js
Create a Server-Side Script#
This script accepts a form.json file containing myparameter and csvfile fields as generated by
the above form. It uses the Auth Token to communicate with the server, it uses the logger configured
by Caosadvancedtools to print the output nicely renderen as HTML, and it returns a download link to
the user for downloading a generated file.
# -- handle_csv.py
import os
import logging
import linkahead as la
from caosadvancedtools.serverside import helper
from caosadvancedtools.serverside.logging import configure_server_side_logging
LOGGER_NAME = "my-logger"
logger = logging.getLogger(LOGGER_NAME)
def do_something(csv_file_path, my_parameter):
"""Do something with the uploaded csv file and the parameter, then return a dict."""
la.execute_query("FIND Something")
return {"data": "example"}
def main():
# setup logging and reporting
configure_server_side_logging(LOGGER_NAME)
parser = helper.get_argument_parser()
args = parser.parse_args()
# configure the connection using the auth_token.
la.configure_connection(auth_token=args.auth_token)
# Read the input from the form (form.json)
with open(args.filename) as form_json:
form_data = json.load(form_json)
# files are uploaded to this directory
upload_dir = os.path.dirname((args.filename))
# Read content of the uploaded form data
csv_file_path = os.path.join(upload_dir, form_data["csvfile"])
my_parameter = form_data["myparameter"]
result = do_something(csv_file_path, my_parameter)
# create a downloadable file and write the results into that file
display_path, internal_path = helper.get_shared_filename("output.json")
with open(internal_path, 'w') as f:
json.dump(result, f)
# use the logger to inform the user where the can download the file
logger.info(f"Download the <a href=/Shared/{display_path}>output.json</a>.")
if __name__ == "__main__":
main()
This file needs to be installed under custom/caosdb-server/scripting/bin/handle_csv.py as an
executable file.
Configure the Server#
You can add a configuration file at custom/caosdb-server/conf/ext/server.conf.d/sss.conf
Be sure to have the server configured with SERVER_SIDE_SCRIPTING_BIN_DIRS=./scripting/bin`. This
is the default, so probably you are just fine.
Add a configuration file at custom/caosdb-server/conf/ext/authtoken.yaml for the Auth Tokens
passed to the script:
# -- custom/caosdb-server/conf/ext/authtoken.yaml
- purpose: SCRIPTING:EXECUTE:handle_csv.py
roles:
- administration
Note
Find more examples for the configuration in the authtoken.example.yaml
Put the file into custom/caosdb-server/conf/authtoken.yaml and configure the server with
# -- custom/caosdb-server/conf/ext/server.conf.d/sss.conf
AUTHTOKEN_CONFIG=`./conf/ext/authtoken.yaml`
Deep Dive into the Scripting API#
This section shows how parameters or files are being passed to the script and how you can test an installation.
Testing an Installation#
Given that you installed the minimal bash script from the previous section, you can start the server and test it with curl:
curl -F call=minimal.bash 'https://localhost:10443/scripting' -H 'Cookie: SessionToken=[...]'
Note
The scripting API expects an HTTP POST request with content type application/x-www-form-urlencoded
or multipart/form-data. It has one mandatory parameter, which is call, and the value must be the
script’s path below the scripting directory.
Note
You need a valid session token. Easiest way to get one: Log in with the browser, open the developer tools and copy-paste it from “Storage” -> “Cookies” -> “SessionToken” (firefox).
Note
If curl fails with “curl: (60) SSL certificate problem: self-signed certificate” or a similar error
message, you must give curl the path to the public certificate or a signing certificate authority
using the --cacert or --capath options. If you want to ignore the issue because you only do this
for testing purposes you may use the --insecure|-k option.
The server’s response should look like this:
<Response>
...
<script code="0">
<call>minimal.bash</call>
<stdout>/opt/caosdb/git/caosdb-server/scripting/bin/minimal.bash --auth-token=["S","PAM","admin",...]</stdout>
<stderr />
</script>
</Response>
You can see the exit code <script code="0"> was 0 (no errors). The standard output and error
output are there, and you can see that the server called the script with one additional
--auth-token=["S", "PAM", "admin",...] option. This AuthToken can be used by the script for
further communication with the server.
Note
The session token should never be leaked by the script. This script does it only for didactic reasons.
Note
You need to restart LinkAhead every time you install a new script or make changes to an existing one. If you need to add or change a script in a running system you must copy your script into the docker container to the correct directory manually.
Call a Script With Parameters#
You can add parameter to the scripting call by adding key-value pairs to your POST request. There are two types of parameters that will be processed:
Keys matching
-p[1-9][0-9]*, e.g.-p0,-p1,… These are positional parameters which are going to be passed to the script.Keys starting with
-O. These are options which are going to be parsed to the script.
Using our minimal script and calling it via curl:
curl -F call=minimal.bash -F -p0=Positional0 -F -p1=Positional1 -F -Ooption1= -F -Ooption2=some-value 'https://localhost:10443/scripting' -H 'Cookie: SessionToken=[...]'
This server’s response should look like this:
<Response>
...
<script code="0">
<call>minimal.bash --option1= --option2=some-value Positional0 Positional1</call>
<stdout>/opt/caosdb/git/caosdb-server/scripting/bin/minimal.bash --auth-token=["S","PAM","admin",...] --option1= --option2=some-value Positional0 Positional1</stdout>
<stderr />
</script>
</Response>
You can see that the parameters have been passed to the script and the effective call was
minimal.bash --option1= --option2=some-value Positional0 Positional1.
Note
The session token should never be leaked by the script. This script does it only for didactic
reasons. The <call>... element prints the complete call but omits the session token.
Pass Files to the Script#
You can upload files and pass the files to the script via the scripting API using the POST request
with the multipart/form-data content type (this is the standard way of uploading files via HTTP).
Let’s assume you have a file data.json like this:
{ "my_key": "my value" }
You can upload the file and call the script via
curl -F call=minimal.bash -F -Omy-file=@data.json 'https://localhost:10443/scripting' -H 'Cookie: SessionToken=[...]'
The server will store the file under .upload_files/data.json relative to the scripts working
directory.
...
<script code="0">
<call>minimal.bash --my-file=.upload_files/data.json </call>
...
</script>
...
You can see that the server passes the file’s location to the script.
Note
We used a key starting with -O here, resulting in an optional parameter. However, you can also use
a positional parameter by using the -pN keys for the file.
We can now add another minimal script print_file.bash which prints the content of the uploaded
file and install it as shown above.
#!/bin/bash
cat $2
Note
Note: Since the first parameter is always the server-generated --auth-token the $2 refers to
the first parameter specified by the user.
This script expects to be called with one positional parameter. So we call it via
curl -F call=print_file.bash -F -p0=@data.json 'https://localhost:10443/scripting' -H 'Cookie: SessionToken=[...]'
The server’s response will look like this:
...
<script code="0">
<call>print_file.bash .upload_files/data.json</call>
<stdout>{"my-key":"my-value"}</stdout>
<stderr />
</script>
...
You can see that the script can indeed access the file and print the content.
Using the Auth Token From Within the Script#
Up until now, we ignored the --auth-token parameter. It can be used by the script to make calls to
the server. By default, the authentication token passed to script is just a session token for the
current session of the user account who is calling the script: If you call the script as “user1” the
script can execute any operation on the server as the same “user1” user.
Using curl inside a minimal script user_info.bash we can see this happening:
#!/bin/bash
# extract the session token from the --auth-token option.
SESSION_TOKEN="${1#--auth_token=}"
curl -o /dev/null -s -w "\nNo SessionToken: %{http_code}" --insecure 'https://localhost:10443/'
curl -o /dev/null -s -w "\nWith SessionToken: %{http_code}" --insecure -H "Cookie: SessionToken=${SESSION_TOKEN}" 'https://localhost:10443'
If we call this script via the scripting API without any further parameters
curl -k -F call=user_info.bash 'https://localhost:10443/scripting' -H 'Cookie: SessionToken=[...]'
the server’s response shows a “401” (Unauthorized) response for the first request and a “200” (Ok) response for the second request:
...
<script code="0">
<call>user_info.bash</call>
<stdout>No SessionToken: 401
th SessionToken: 200</stdout>
<stderr />
</script>
...
A Pattern: The Server as a Client of Itself.#
So what did we do in the last step?
We installed a script user_info.bash. If we call that script via the scripting API, the server
executes that script on our behalf. The script sends two request back to the server and the
scripting API response reaches us after both curl requests of the script terminated.
| USER: calls `user_info.bash`
| | SERVER: receives the scripting API request
| | SERVER: executes `user_info.bash`
| | | SCRIPT: curl sends request 1
| | | | SERVER receives the request
| | | * SERVER responds with 401
| | | SCRIPT: curl terminates 1st time
| | | SCRIPT: curl sends request 2
| | | | SERVER receives the request
| | | * SERVER responds with 200
| | | SCRIPT: curl terminates 2nd time
| | * SCRIPT: terminates
| * SERVER: sends response `<script code="0"> ...`
* USER: receives the servers response.
So in the lines in the middle, the server is talking to itself. Actually, this is a very common pattern. This pattern can be used to encapsulate a complex series of transactions into one single command with parameters.