Transforms
Transform plugins normalize or enrich inventory values when Genja accesses hosts, groups, or defaults. They are useful when the source inventory is valid but not yet in the exact shape tasks and plugins should consume.
When To Use A Transform
Transforms are a good fit for:
- normalizing hostnames, usernames, or platform names
- filling derived fields into host
data - merging default values into a cleaner runtime shape
- mapping source-system field names into the naming Genja tasks expect
- enriching inventory values from a lightweight lookup before task execution
Use an inventory plugin instead when you need to:
- fetch inventory from a new source
- load hosts, groups, or defaults from an API, database, or service
- decide which inventory records exist in the first place
Hook Model
A transform may implement one, multiple, or all of these hooks:
transform_hosttransform_grouptransform_defaults
Missing hooks pass the original value through unchanged.
Genja passes the same optional transform_function_options object to every
implemented hook, so use nested keys when different hooks need different
settings.
inventory:
plugin: FileInventoryPlugin
options:
hosts_file: ./hosts.yaml
transform_function: normalize_inventory
transform_function_options:
hostname_suffix: ".lab"
defaults:
platform: linux
Rust Transform Plugins
Rust transforms implement Transform and are exposed through a
PluginTransformFunction wrapper. The plugin name() must match the
transform_function value from settings, so transform_function:
normalize_inventory selects the normalize_inventory plugin below.
use genja_plugin_manager::plugin_types::{Plugin, PluginTransformFunction, Plugins};
use genja_plugin_manager::PluginManager;
use genja::genja_core::inventory::{
BaseBuilderHost, Defaults, Host, Transform, TransformFunctionOptions, TransformFunction,
};
struct NormalizeInventory;
impl Transform for NormalizeInventory {
fn transform_host(
&self,
host: &Host,
options: Option<&TransformFunctionOptions>,
) -> Host {
let suffix = options
.and_then(|options| options.get("hostname_suffix"))
.and_then(|value| value.as_str())
.unwrap_or("");
match host.hostname() {
Some(hostname) => host
.to_builder()
.hostname(format!("{hostname}{suffix}"))
.build(),
None => host.clone(),
}
}
fn transform_defaults(
&self,
defaults: &Defaults,
_options: Option<&TransformFunctionOptions>,
) -> Defaults {
defaults.to_builder().platform("linux").build()
}
}
struct NormalizeInventoryPlugin;
impl Plugin for NormalizeInventoryPlugin {
fn name(&self) -> String {
"normalize_inventory".to_string()
}
}
impl PluginTransformFunction for NormalizeInventoryPlugin {
fn transform_function(&self) -> TransformFunction {
TransformFunction::new_full(NormalizeInventory)
}
}
let mut plugins = PluginManager::new();
plugins.register_plugin(Plugins::TransformFunction(Box::new(
NormalizeInventoryPlugin,
)));
Python Transform Plugins
Python transforms extend TransformFunctionPluginBase.
import genja as genja_lib
from genja.transform import TransformFunctionPluginBase
class NormalizeInventory(TransformFunctionPluginBase):
name = "normalize_inventory"
def transform_host(
self,
host: dict[str, object],
options: dict[str, object] | None,
) -> dict[str, object]:
suffix = (options or {}).get("hostname_suffix", "")
hostname = host.get("hostname")
if hostname is None:
return host
return {
**host,
"hostname": f"{hostname}{suffix}",
}
def transform_defaults(
self,
defaults: dict[str, object],
options: dict[str, object] | None,
) -> dict[str, object]:
default_options = (options or {}).get("defaults", {})
return {**defaults, **default_options}
plugins = genja_lib.PluginManager()
plugins.register_plugin(NormalizeInventory())
Async Variant
Python transform hooks may be synchronous or asynchronous. Use async def when
the transform needs to read from an async API before returning the updated
inventory value.
from genja.transform import TransformFunctionPluginBase
class AsyncNormalizeInventory(TransformFunctionPluginBase):
name = "async_normalize_inventory"
async def transform_host(
self,
host: dict[str, object],
options: dict[str, object] | None,
) -> dict[str, object]:
suffix = (options or {}).get("hostname_suffix", "")
hostname = host.get("hostname")
if hostname is None:
return host
return {
**host,
"hostname": f"{hostname}{suffix}",
}
Common Patterns
Typical transform patterns include:
- append or rewrite hostnames for lab vs production environments
- set fallback platform values in
defaults - copy nested source metadata into flatter task-friendly keys
- normalize group names or host tags before filtering
Keep transforms narrow. They work best when they reshape already-loaded inventory, not when they become a second inventory source.