Hawk is a small layer to deterministically apply configuration. It consists of a small templating layer.
Hawk supports loading JSON data from files and HTTP(S) sources into the template context via a configuration file. This allows dynamic data injection without custom helper plugins.
The datasource configuration file defaults to /etc/gs-hawk.json and can be overridden with --config <path>.
The configuration is a JSON array of source objects:
[
{
"source": "file:///run/node-id",
"as_variable": "node_id"
},
{
"source": "file:///run/gridscale/hc.json",
"as": "hybridcore",
"sub_key": "hybridcore"
},
{
"source": "file:///run/gridscale/hc.json",
"as": "node",
"sub_key": "hybridcore.nodes[${node_id}]"
},
{
"source": "https://api.example.com/v1/data",
"as": "api_data",
"cache_key": "api_cache",
"sub_key": "cluster"
}
]| Property | Type | Default | Description |
|---|---|---|---|
source |
string | - | Required. URL of the data source. Supports file://, http://, https://, inline:. Variable substitution (${var}) is supported. Content must be valid YAML (JSON-compatible). Files may use .json extension. |
as_variable |
string | - | If present, the raw content (or sub_key value) is stored as a simple variable, not merged into template context. |
as_log_context |
string | - | If present, the value is added to gshawk.vars.log_context for logging. Processed after all variables are defined. |
as |
string | . |
Target key in template context. Defaults to root merge. |
sub_key |
string | . |
Extract a nested value using dot notation and array indexing (e.g., hybridcore.nodes[${node_id}]). Ignored when json: false. |
json |
boolean | true |
If true (default), parse content as YAML (JSON-compatible). If false, treat content as raw string (sub_key has no effect). |
include_keys |
array | - | Only include these top-level keys in the merged data. |
exclude_keys |
array | - | Exclude these top-level keys from the merged data. |
- All sources with
as_variableare processed first, making variables available for substitution in subsequent sources. - Variables are added to template context and global args.
- Remaining sources (without
as_variableoras_log_context) are processed and deep-merged into the template context. - Sources with
as_log_contextare processed and added to the log context.
HTTP(S) sources are cached locally. The cache directory is determined as follows:
| User | macOS | Linux |
|---|---|---|
| root (uid=0) | /Library/Caches/gs-hawk |
/var/cache/gs-hawk |
| regular user | ~/.cache/gs-hawk |
~/.cache/gs-hawk |
The default can be overridden with --cache-dir <path>.
- On successful HTTP fetch, the response is cached with a timestamp.
- If HTTP fetch fails after 3 retry attempts, the cached value is used as fallback.
- If no cache is available, Hawk fails to start.
- Cache files not referenced by the current configuration are purged on each run.
- Cache timestamps are exported as Prometheus metrics to
/var/lib/prometheus/node-exporter/hawk-cache.prom(if the directory exists).- Metric format:
hawk_cache_timestamp{cache_key="<key>"} <unix_timestamp>
- Metric format:
[
{
"source": "file:///run/node-id",
"as_variable": "node_id"
},
{
"source": "file:///run/gridscale/hc.json",
"as": "hybridcore",
"sub_key": "hybridcore"
},
{
"source": "file:///run/gridscale/hc.json",
"as": "node",
"sub_key": "hybridcore.nodes[${node_id}]"
},
{
"source": "file:///run/gridscale/boot.json",
"as": "boot"
}
]This configuration makes the following available in templates:
node_id- The node ID stringhybridcore- The full hybridcore objectnode- The node-specific object fromhybridcore.nodes[<node_id>]boot- The boot.json contents
[
{
"source": "file:///run/node-id",
"as_variable": "node_id"
},
{
"source": "inline:\"noble-generic\"",
"as": "default_variant"
},
{
"source": "file:///run/node-id",
"as_log_context": "node"
},
{
"source": "inline:{\"env\": \"production\", \"region\": \"eu-central\"}",
"as_log_context": "."
}
]This configuration:
- Sets
default_variantto a static string value viainline: - Adds
nodeto the log context with the node ID value - Merges
envandregioninto the log context from inline JSON
[
{
"source": "file:///etc/hostname",
"as_variable": "hostname",
"json": false
},
{
"source": "file:///run/secrets/api_key",
"as": "api_key",
"json": false
}
]When json: false, the file content is treated as a raw string:
- Content is stripped of leading/trailing whitespace
sub_key,include_keys, andexclude_keyshave no effect- Useful for reading plain text files like hostnames, tokens, or keys
| Option | Default | Description |
|---|---|---|
--config |
/etc/gs-hawk.json |
Path to datasource configuration JSON |
--cache-dir |
See below | Directory for HTTP source cache |
--metrics-dir |
/var/lib/prometheus/node-exporter |
Directory for Prometheus metrics export |
--show-datasources |
false |
Output merged datasource JSON and exit |
--show-log-context |
false |
Output log context JSON and exit |
--source |
/usr/share/gs-hawk |
Root dir of Hawk feathers |
--target |
/ |
Root dir of Hawk output |
--dry-run |
false |
Do not write output files |
--no-diff |
false |
Do not show diff |
--skip-systemd |
false |
Skip systemd executions and hooks |
--log |
/dev/null |
Log template usage as JSON |
--expression |
'' |
Evaluate template string and output result |
--target |
/ |
Root dir of Hawk output |
--dry-run |
false |
Do not write output files |
--no-diff |
false |
Do not show diff |
--skip-systemd |
false |
Skip systemd executions and hooks |
--log |
/dev/null |
Log template usage as JSON |
--expression |
'' |
Evaluate template string and output result |
Files placed in the /usr/share/gs-hawk/<feather> directory will be applied to the target root.
Hawk will smartly detect any systemd unit which was altered during the application of the changes.
Any unit (.service, .path, .timer, target, hook) will be handled as configured in the X-Hawk section of the unit. See below for details.
Files in the Hawk folder will be copied over as-is, with 2 exceptions:
- The file in question has a
#!- the file will be marked executable in the target system - The filename ends with
.jinja2- The file will get templated and the suffix removed.
Hawk enables Jinja2 based templating. Inserted into the template is nothing by default. This requires a plugin.
Included filters:
hawk.utils.{ipaddr,ipv4,ipv6}hawk.utils.regex_replacehawk.utils.b64{de,en}code
Helper Methods:
hawk.interfaces()- returns an array of network interfaceshawk.hardware.cpuset_reserve_physical_cores(desired, prefer_E_cores=True)- If desired is positive, the selected cores are returned as a cpu set string (e.g.
0-1,8-9,0,4, etc.) - (reserve X cores) - If desired is negative, the inverse of the selected cores are returned as a cpu set string - (reserve all except X cores)
- if
prefer_E_coresistrueand the CPU is on bigLITTLE topology, E-cores are preferredly allocated. Otherwise cores are allocated by id - NOTE: This is not an actual allocation! This is a tool to be able to divide a system up into 2 partitions, by calling this helper in 2 places - once, with a positive count, once with a negative count. Subsequent calls will return the same cpu sets!
- If desired is positive, the selected cores are returned as a cpu set string (e.g.
hawk.hardware.memory_reserve_{bytes,mib,gib}(amount)- Returns a float. Cast as needed.
- If
amountis positive, it is checked that the amount is available on the system and then returned - If
amountis negative, the total system memory reduced by the amount specified is returned - NOTE: This is not an actual allocation! This is a tool to be able to divide a system up into 2 partitions, by calling this helper in 2 places - once, with a positive count, once with a negative count. Subsequent calls will return the same amount of memory!
hawk.hardware.memory_total_{bytes,mib,gib}()- Returns a float. Cast as needed.
Because we need some finesse in dealing with some services, we have a section in Systemd units that is ignored by systemd itself, and just parsed in Hawk.
The [X-Hawk] section can be used in any systemd unit deployed via Hawk. For any option not specified the following defaults will be used:
[X-Hawk]
RestartOnConfigChange=yes
RestartOnUnitChange=yes
ReloadOnConfigChange=no
EnableUnit=yes
StopOnUnitDisable=no
StartUnit=yes
ExecOnUnitChange=""
ExecOnApply=""
ConfigFile=""
# systemd-analyze condition queries
Condidion...=Query
# If EnableUnit==yes:
[Install]
WantedBy=multi-user.target
| Option | Default | Description |
|---|---|---|
RestartOnConfigChange |
yes |
If yes the affetcted service will restart if one of the paths specified via ConfigFile have changed |
RestartOnUnitChange |
yes |
If yes Hawk will restart the affected unit if the file content changed. Otherwise it is not restarted |
ReloadOnConfigChange |
no |
If yes the affetcted service will reload if one of the paths specified via ConfigFile have changed |
EnableUnit |
yes |
If yes, systemctl enable is called on the unit. It is only started, if StartUnit is also yes, otherwise it is enabled, but not started. If no, disables the Unit. The unit will not be stopped unless StopUnitOnDisable is yes. |
StopUnitOnDisable |
no |
If yes a unit with EnableUnit=no will be stopped when deployed. Otherwise the state does not change |
StartUnit |
yes |
If yes the unit will get started upon rollout |
ExecOnUnitChange |
- | This command will be executed on the node if the file content changed and the deployment conditions were met. Can be specified multiple times. Executed via bash -c |
ExecOnApply |
- | This command will be executed on the node after Hawk ran and the deployment conditions were met. Can be specified multiple times. Executed via bash -c |
ConfigFile |
- | Path to a file. When specified, and Re(start|load)OnConfigChange=yes a change in the file will trigger a restart/reload of the unit. Can be specified multiple times. |
Condition...|Assert... |
- | If specified in X-Hawk section, will be checked at apply time. Units with non-matching conditions will not get copied or handled. If specified in Unit section, the units will get copied and hooks get executed. Actual evaluation of the rules happens in systemd at runtime. See systemd documentation for all possible options. |
TemplateCondition |
yes |
If no or False the unit template will be dropped. Use this for deciding whether to copy a unit over from a templated value. Can be specified multiple times. |
hawk-crypt is a commandline tool to encrypt secrets for use in Hawk.
By default hawk-crypt will make use of the 1Password CLI to retrieve the encryption keys. This can be disabled by passing --one-password=false
The tool requires CAP_IPC_LOCK or root permissions on Linux. Alternatively run with INSECURE_NO_LOCK defined
For gridscale internal use, the packaging is handled automatically via the CI/CD pipeline defined in .gitlab-ci.yml. External parties who wish to build and run Hawk independently can use one of the following methods:
Build a Debian package using the provided Makefile:
# Requires: Ruby fpm gem (gem install fpm)
make debThis creates a .deb package in the dist/ directory with all dependencies configured. Install with:
sudo dpkg -i dist/gs-hawk_*.debThe package includes:
- Python source files in
/usr/lib/gs-hawk/ - Binaries (
hawk,hawk-crypt) in/usr/bin/ - Systemd helper units in
/usr/lib/systemd/system/ - Directory for feathers in
/usr/share/gs-hawk/
Run Hawk directly from a Python virtual environment:
# Create and activate virtual environment
python3 -m venv .venv
source .venv/bin/activate
# Install dependencies
pip install -r requirements.txt
# Run Hawk
./bin/hawk --help
./bin/hawk-crypt --helpPackage Hawk as a self-contained PEX binary:
# Install pex
pip install pex
# Build PEX binary
pex -r requirements.txt -c hawk -o hawk.pex bin/hawk
# Run
./hawk.pex --helpPEX bundles all dependencies into a single executable file, making it easy to distribute and run on different systems.