Metadata-Version: 2.4
Name: lvi-script-sandbox-utils
Version: 0.6.1
Summary: Convenience utilities for LogicVein Script Sandbox Python scripts
License-Expression: Apache-2.0
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Dynamic: license-file

# LogicVein Script Sandbox Utils

Convenience helpers for Python scripts that LogicVein Playbooks run in the
Script Sandbox container. They wrap the sandbox's file-and-stdout contract:
a Playbook hands your script its inputs in `in.json`, and your script returns
results by printing JSON to stdout.

## Installation

From PyPI:

```bash
pip install lvi-script-sandbox-utils
```

From TestPyPI (pre-release / testing):

```bash
pip install \
  --index-url https://test.pypi.org/simple/ \
  --extra-index-url https://pypi.org/simple/ \
  lvi-script-sandbox-utils
```

The sandbox runs scripts in a container, so the package has to be available
inside it. If your sandbox mounts a packages directory (for example
`./packages:/packages`), install into it:

```bash
pip install --target ./packages lvi-script-sandbox-utils
```

The library has no third-party dependencies (standard library only).

### Upgrading from 0.1.x

0.1.x installed as `logicvein.lvi_script_sandbox_utils`. From 0.2.0 it is
the top-level package `lvi_script_sandbox_utils`, so change

```python
from logicvein import lvi_script_sandbox_utils as lvi
```

to `import lvi_script_sandbox_utils as lvi`. The `logicvein` package is
left to `lvi-api-client`: when both shipped a `logicvein/` directory,
`pip install --target` skipped whichever was installed second (or, with
`--upgrade`, deleted the first).

## Usage

```python
import lvi_script_sandbox_utils as lvi

device = lvi.get_device()
output = lvi.get_input("cli_output", default="")  # "" in the Playbook's first script node

lvi.flush_output(hostname=device.device_hostname, line_count=len(output.splitlines()))
```

### Functions

| Function | Purpose |
|----------|---------|
| `load_input(path=None, reload=False)` | The whole `in.json` as a dict, read once and cached. Treat it as read-only. |
| `get_input(input_type, default=...)` | Read an input from `in.json`. `"cli_output"` returns the previous node's result value; `"device"` returns the raw device dict. A missing input returns `default`, or raises `KeyError` if none was given; any other `input_type` raises `ValueError`. |
| `get_global(name, default=...)` | One Playbook global's value, e.g. `get_global("hostname")`. A missing global returns `default`, or raises `KeyError` naming the globals that are set. |
| `get_globals()` | Every Playbook global as `{name: value}`. |
| `get_metadata()` | Details of this run as an object: `playbook`, `execution_id`, `executor`, `job_id`, `node`, `started_at`, and so on. |
| `get_device(blank_none=True)` | The Playbook's device as an object with a basic set of properties (see below). |
| `get_full_device(blank_none=True)` | Like `get_device()`, with the additional properties included (`uuid`, `lastBackup`, `severity`, `memoSummary`, ...). |
| `set_output(key, value)` | Record a global for the next `flush_output()`. |
| `flush_output(**kwargs)` | Print the `set_output()` values plus `kwargs` (which win on a clash) to stdout as Playbook global variables (`output.globals`), then clear the `set_output()` values. Playbook globals are strings, so a list, dict, device or metadata object is printed as its JSON text (see below). |
| `flush_string_output(text)` | Print a string to stdout as base64-encoded raw output (`output.raw`). |

On the device objects:

- `hostname` and `network` are renamed to `device_hostname` and
  `device_network`, so they don't collide with other Playbook variables.
- `traits` lists the device's capabilities from `metaJson` (`ssh`, `snmp`,
  `ncm`, ...); `customFields` is dropped, since it repeats `custom1`..`custom5`.
- End-of-life/sale epochs (`endOfLife`, `endOfSale`, `softwareEndOfLife`,
  `softwareEndOfSale`) become `YYYY-MM-DD` dates, and `lastBackup`,
  `lastChange`, `lastDiscovery`, `lastTelemetry` become
  `YYYY-MM-DDTHH:MM:SSZ` timestamps, all in UTC. `get_metadata().started_at`
  is converted the same way.
- `None` values become `""`; pass `blank_none=False` to keep `None`.
- A property LogicVein doesn't send is simply absent from the object.

`vars(device)` gives the properties as a dict, so
`lvi.flush_output(**vars(device))` turns every property into a Playbook
global.

### Output globals are strings

LogicVein reads `output.globals` as strings. It converts numbers and booleans
itself, but rejects the whole output if any value is a list or an object
(`MismatchedInputException ... from Array value`). `flush_output()` therefore
prints such values as their JSON text: `device.traits` becomes
`'["icmp", "snmp", ...]'`, which a later script node can read back with
`json.loads(lvi.get_global("traits"))`. For a different format, convert the
value yourself first, e.g. `traits=", ".join(device.traits)`.

### Where the input comes from

`in.json` is read on first use, not at import, and only once; `load_input()`
returns it as a dict (pass `reload=True` to read it again). The module
attributes `device` and `device_full` are built the first time a script uses
them. By default the file is `in.json` in the working directory, where the
sandbox puts it; set `LVI_SANDBOX_INPUT` to read another file, e.g. a saved
`in.json` while developing a script outside a Playbook:

```bash
LVI_SANDBOX_INPUT=./samples/in.json python my_script.py
```

## Development and releases

See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and
[PUBLISHING.md](PUBLISHING.md) for how releases are published.
