|
| 1 | +Script Activities |
| 2 | +================= |
| 3 | + |
| 4 | +Yamcs can run arbitrary scripts or programs as a background activity. |
| 5 | + |
| 6 | +Scripts are predefined and stored under :file:`etc/scripts`. |
| 7 | + |
| 8 | +Script files may be directly executable, or be associated to another program based on its file extension. |
| 9 | + |
| 10 | + |
| 11 | +Configuration |
| 12 | +------------- |
| 13 | + |
| 14 | +Script activity options are configured in the instance configuration file :file:`etc/yamcs.{instance}.yaml`. |
| 15 | + |
| 16 | +.. code-block:: yaml |
| 17 | + :caption: :file:`etc/yamcs.{instance}.yaml` |
| 18 | +
|
| 19 | + activities: |
| 20 | + scriptExecution: |
| 21 | + searchPath: etc/scripts |
| 22 | + impersonateCaller: false |
| 23 | + fileAssociations: |
| 24 | + py: python3 -u |
| 25 | +
|
| 26 | +
|
| 27 | +Configuration Options |
| 28 | +--------------------- |
| 29 | + |
| 30 | +searchPath (string or string[]) |
| 31 | + Directory where to locate scripts or executables. |
| 32 | + |
| 33 | + Default: :file:`etc/scripts` |
| 34 | + |
| 35 | +fileAssociations (map) |
| 36 | + Extend or override the default file associations. Each entry maps a file extension (case-insensitive) to a program that should be used to execute this file. |
| 37 | + |
| 38 | + The default file associations are: |
| 39 | + |
| 40 | + .. code-block:: yaml |
| 41 | +
|
| 42 | + fileAssociations: |
| 43 | + java: java |
| 44 | + js: node |
| 45 | + mjs: node |
| 46 | + pl: perl |
| 47 | + py: python -u |
| 48 | + rb: ruby |
| 49 | +
|
| 50 | + Any file that does not have an association, is executed directly. |
| 51 | + |
| 52 | + .. note:: |
| 53 | + Some Linux distributions no longer have a ``python`` command. In this case you can adapt the file association to ``python3 -u``. Alternatively, on Debian-based distributions you may want to install the ``python-is-python3`` package: |
| 54 | + |
| 55 | + .. code-block:: shell |
| 56 | +
|
| 57 | + sudo apt install python-is-python3 |
| 58 | + |
| 59 | + This package creates a symlink so that ``python`` invokes ``python3``. |
| 60 | + |
| 61 | + .. note:: |
| 62 | + The ``-u`` flag added to the ``python`` command forces Python to run in **unbuffered mode**. This affects how Python handles output streams, allowing Yamcs to capture output in realtime instead of needing to wait until the buffer is full. |
| 63 | + |
| 64 | +impersonateCaller (boolean) |
| 65 | + Scripts receive a transient API key via an environment variable. By default this API key uses the built-in ``System`` user, which provides unrestricted access. |
| 66 | + |
| 67 | + When this property is enabled, the script receives instead an API key of the user that started the activity. |
| 68 | + |
| 69 | + Default: ``false`` |
| 70 | + |
| 71 | + |
| 72 | +Activity Options |
| 73 | +---------------- |
| 74 | + |
| 75 | +script (string) |
| 76 | + **Required.** Script path relative to the :file:`etc/scripts` directory. |
| 77 | + |
| 78 | +args (string, or list of strings) |
| 79 | + Command line arguments. |
| 80 | + |
| 81 | +processor (string) |
| 82 | + If provided this information is passed to the called script as a ``YAMCS_PROCESSOR`` environment variable. |
| 83 | + |
| 84 | + |
| 85 | +Execution |
| 86 | +--------- |
| 87 | + |
| 88 | +The script's ``stdout`` and ``stderr`` output streams are captured in the Yamcs activity log. The activity tracks the lifecycle of the subprocess. The exit code of this process determines whether the activity is considered successful or not. A non-zero exit code indicates failure. |
| 89 | + |
| 90 | + |
| 91 | +Environment Variables |
| 92 | +--------------------- |
| 93 | + |
| 94 | +Scripts are executed with the following environment variables: |
| 95 | + |
| 96 | +``YAMCS`` |
| 97 | + Always set to ``1``. |
| 98 | + |
| 99 | +``YAMCS_INSTANCE`` |
| 100 | + Set to the applicable Yamcs instance. |
| 101 | + |
| 102 | +``YAMCS_PROCESSOR`` |
| 103 | + Set to the applicable Yamcs processor. |
| 104 | + |
| 105 | +``YAMCS_URL`` |
| 106 | + URL that the script can use to reach Yamcs. |
| 107 | + |
| 108 | + For example: ``http://localhost:8090`` |
| 109 | + |
| 110 | +If Yamcs requires authentication, another environment variable is set: |
| 111 | + |
| 112 | +``YAMCS_API_KEY`` |
| 113 | + A randomly generated API key that the script may use to authenticate against Yamcs. Keys are invalidated when the script terminates. |
| 114 | + |
| 115 | + Clients can use an API key by setting the ``x-api-key`` HTTP header on each request. Yamcs will know how to link this back to the appropriate user, which (depending on configuration) could be the script caller, or a generic ``System`` user. |
| 116 | + |
| 117 | + |
| 118 | +Python Scripts |
| 119 | +-------------- |
| 120 | + |
| 121 | +Python scripts may use the `Python Yamcs Client <https://docs.yamcs.org/python-yamcs-client/>`_ to access the Yamcs API. This includes a static utility function ``YamcsClient.from_environment()`` that reads aforementioned environment variables, and authenticates when needed. |
| 122 | + |
| 123 | +.. code-block:: python |
| 124 | +
|
| 125 | + from yamcs.client import YamcsClient |
| 126 | + import sys |
| 127 | +
|
| 128 | + # Create a client instance |
| 129 | + client = YamcsClient.from_environment() |
| 130 | +
|
| 131 | + # Print all command-line arguments |
| 132 | + print(sys.argv) |
| 133 | +
|
| 134 | + # ... |
| 135 | +
|
| 136 | +The Python Yamcs Client is something that would need to be installed separate from Yamcs. |
| 137 | + |
| 138 | + |
| 139 | +Shell Scripts |
| 140 | +------------- |
| 141 | + |
| 142 | +Below is an example of a shell script that generates a Yamcs event. Remember to make the file executable. |
| 143 | + |
| 144 | +.. code-block:: shell |
| 145 | +
|
| 146 | + #!/bin/sh |
| 147 | + |
| 148 | + echo "Creating event" |
| 149 | + |
| 150 | + MSG="$(whoami) says hi" |
| 151 | + JSON_STRING=$(printf '{"message": "%s"}' "$MSG") |
| 152 | + |
| 153 | + curl -XPOST $YAMCS_URL/api/archive/$YAMCS_INSTANCE/events \ |
| 154 | + --silent -d "$JSON_STRING" --fail-with-body |
| 155 | +
|
| 156 | +For authenticated servers, you can specify an additional HTTP header on the curl command: |
| 157 | + |
| 158 | +.. code-block:: shell |
| 159 | +
|
| 160 | + -H "x-api-key: $YAMCS_API_KEY" |
| 161 | +
|
| 162 | +
|
| 163 | +Yamcs UI |
| 164 | +-------- |
| 165 | + |
| 166 | +In the Yamcs UI, choose :menuselection:`Procedures --> Run a script`. A list of the available scripts is displayed, and you can choose to run one **immediately**, providing any script arguments. |
| 167 | + |
| 168 | +To execute a script **at a later time**, choose the option :guilabel:`Run later...`. You will be asked to enter the desired execution time. This will create an activity *item* in the :doc:`../timeline/index`. |
| 169 | + |
| 170 | + |
| 171 | +Python Yamcs Client |
| 172 | +------------------- |
| 173 | + |
| 174 | +Run a script activity immediately (does not use Yamcs Timeline): |
| 175 | + |
| 176 | +.. code-block:: python |
| 177 | +
|
| 178 | + from yamcs.client import YamcsClient |
| 179 | +
|
| 180 | + client = YamcsClient("localhost:8090") |
| 181 | + processor = client.get_processor("simulator", "realtime") |
| 182 | +
|
| 183 | + # Simulate LOS for 5 seconds |
| 184 | + # (the run_script call does not block) |
| 185 | + processor.run_script("simulate_los.py", "--duration 5") |
| 186 | +
|
| 187 | +
|
| 188 | +Run a script activity one minute from now: |
| 189 | + |
| 190 | +.. code-block:: python |
| 191 | +
|
| 192 | + from datetime import datetime, timedelta, timezone |
| 193 | +
|
| 194 | + from yamcs.client import ScriptActivity, YamcsClient, TimelineActivity |
| 195 | +
|
| 196 | + client = YamcsClient("http://localhost:8090") |
| 197 | + timeline = client.get_timeline_client("simulator") |
| 198 | +
|
| 199 | + now = datetime.now(tz=timezone.utc) |
| 200 | +
|
| 201 | + item = TimelineActivity( |
| 202 | + name="Simulate LOS", |
| 203 | + start=now + timedelta(minutes=1), |
| 204 | + duration=timedelta(seconds=5), # Planned duration |
| 205 | + activity=ScriptActivity( |
| 206 | + script="simulate_los.py", |
| 207 | + args="--duration 5", |
| 208 | + processor="realtime", |
| 209 | + ), |
| 210 | + ) |
| 211 | + timeline.save_item(item) |
0 commit comments