use async_trait::async_trait;
use libloading::Library;
use serde::Deserialize;
use std::any::Any;
use std::collections::HashMap;
use std::fmt;
use std::fmt::Debug;
use crate::PluginManager;
use genja_core::inventory::{
ConnectionKey, Hosts, Inventory, ResolvedConnectionParams, TransformFunction,
};
use genja_core::settings::RunnerConfig;
use genja_core::task::{TaskConnectionResolver, TaskDefinition, TaskProcessor, TaskResults, Tasks};
use genja_core::{InventoryLoadError, Settings};
use std::sync::Arc;
pub type PathString = String;
pub type GroupOrName = String;
pub type PluginName = String;
pub type PluginResult = Result<(Library, Vec<Box<dyn Plugin>>), Box<dyn std::error::Error>>;
pub type PluginCreate = unsafe fn() -> Vec<Box<dyn Plugin>>;
pub type PluginCreatePlugins = unsafe fn() -> Vec<Plugins>;
pub type PluginResultPlugins = Result<(Library, Vec<Plugins>), Box<dyn std::error::Error>>;
#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum PluginEntry {
Individual(PathString),
Group(HashMap<String, PathString>),
}
pub struct PluginInfo {
pub plugin: Box<dyn Plugin>,
pub group: Option<String>,
}
pub trait Plugin: Send + Sync + Any {
fn name(&self) -> String;
fn group(&self) -> String {
String::from("BasePlugin")
}
}
pub trait PluginInventory: Plugin {
fn load(
&self,
settings: &Settings,
plugins: &PluginManager,
) -> Result<Inventory, InventoryLoadError>;
fn group(&self) -> String {
String::from("InventoryPlugin")
}
}
#[async_trait]
pub trait AsyncPluginInventory: Plugin {
async fn load_async(
&self,
settings: &Settings,
plugins: &PluginManager,
) -> Result<Inventory, InventoryLoadError>;
fn group(&self) -> String {
String::from("InventoryPlugin")
}
}
impl Debug for dyn Plugin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {{ name: {} }}", Plugin::group(self), self.name())
}
}
impl Debug for dyn PluginInventory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {{ name: {} }}",
PluginInventory::group(self),
self.name()
)
}
}
impl Debug for dyn AsyncPluginInventory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {{ name: {} }}",
AsyncPluginInventory::group(self),
self.name()
)
}
}
impl Debug for dyn PluginConnection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {{ name: {} }}",
PluginConnection::group(self),
self.name()
)
}
}
#[async_trait]
pub trait PluginRunner: Plugin {
async fn run_task(
&self,
task: &TaskDefinition,
hosts: &Hosts,
connection_resolver: Option<Arc<dyn TaskConnectionResolver>>,
runner_config: &RunnerConfig,
max_depth: usize,
) -> Result<TaskResults, genja_core::GenjaError>;
async fn run_tasks(
&self,
tasks: &Tasks,
hosts: &Hosts,
connection_resolver: Option<Arc<dyn TaskConnectionResolver>>,
runner_config: &RunnerConfig,
max_depth: usize,
) -> Result<Vec<TaskResults>, genja_core::GenjaError> {
let mut results = Vec::with_capacity(tasks.len());
for task in tasks.iter() {
results.push(
self.run_task(
task,
hosts,
connection_resolver.clone(),
runner_config,
max_depth,
)
.await?,
);
}
Ok(results)
}
fn group(&self) -> String {
String::from("RunnerPlugin")
}
}
impl Debug for dyn PluginRunner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {{ name: {} }}",
PluginRunner::group(self),
self.name()
)
}
}
pub trait PluginTransformFunction: Plugin {
fn transform_function(&self) -> TransformFunction;
fn group(&self) -> String {
String::from("TransformFunctionPlugin")
}
}
impl Debug for dyn PluginTransformFunction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {{ name: {} }}",
PluginTransformFunction::group(self),
self.name()
)
}
}
pub trait PluginProcessor: Plugin {
fn processor(&self) -> Arc<dyn TaskProcessor>;
fn group(&self) -> String {
String::from("ProcessorPlugin")
}
}
impl Debug for dyn PluginProcessor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {{ name: {} }}",
PluginProcessor::group(self),
self.name()
)
}
}
#[async_trait]
pub trait PluginConnection: Plugin {
fn create(&self, key: &ConnectionKey) -> Box<dyn PluginConnection>;
async fn open(&mut self, params: &ResolvedConnectionParams) -> Result<(), String>;
async fn execute_command(&mut self, _command: &str) -> Result<String, String> {
Err("connection plugin does not implement execute_command".to_string())
}
fn close(&mut self) -> ConnectionKey;
fn is_alive(&self) -> bool;
fn group(&self) -> String {
String::from("ConnectionPlugin")
}
}
#[derive(Debug)]
pub enum Plugins {
Connection(Box<dyn PluginConnection>),
Inventory(Box<dyn PluginInventory>),
AsyncInventory(Box<dyn AsyncPluginInventory>),
Processor(Box<dyn PluginProcessor>),
Runner(Box<dyn PluginRunner>),
TransformFunction(Box<dyn PluginTransformFunction>),
}
impl Plugins {
pub fn name(&self) -> String {
match self {
Plugins::Connection(connection) => connection.name(),
Plugins::Inventory(inventory) => inventory.name(),
Plugins::AsyncInventory(inventory) => inventory.name(),
Plugins::Processor(processor) => processor.name(),
Plugins::Runner(runner) => runner.name(),
Plugins::TransformFunction(transform) => transform.name(),
}
}
pub fn group_name(&self) -> String {
match self {
Plugins::Connection(_) => String::from("Connection"),
Plugins::Inventory(_) => String::from("Inventory"),
Plugins::AsyncInventory(_) => String::from("Inventory"),
Plugins::Processor(_) => String::from("Processor"),
Plugins::Runner(_) => String::from("Runner"),
Plugins::TransformFunction(_) => String::from("TransformFunction"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use genja_core::inventory::{
ConnectionKey, Host, Hosts, ResolvedConnectionParams, TransformFunction,
};
use genja_core::task::{
HostTaskResult, Task, TaskError, TaskExecutionMode, TaskInfo, TaskRuntimeContext,
TaskSuccess,
};
use serde_json::{Value, json};
use std::future::Future;
use tokio::runtime::Builder;
#[derive(Debug)]
struct DummyPlugin {
name: &'static str,
}
impl DummyPlugin {
fn new(name: &'static str) -> Self {
Self { name }
}
}
impl Plugin for DummyPlugin {
fn name(&self) -> String {
self.name.to_string()
}
}
#[derive(Debug)]
struct DummyInventory {
name: &'static str,
}
impl DummyInventory {
fn new(name: &'static str) -> Self {
Self { name }
}
}
impl Plugin for DummyInventory {
fn name(&self) -> String {
self.name.to_string()
}
}
impl PluginInventory for DummyInventory {
fn load(
&self,
_settings: &Settings,
_plugins: &PluginManager,
) -> Result<Inventory, InventoryLoadError> {
Ok(Inventory::builder().build())
}
}
#[derive(Debug)]
struct DummyAsyncInventory {
name: &'static str,
}
impl DummyAsyncInventory {
fn new(name: &'static str) -> Self {
Self { name }
}
}
impl Plugin for DummyAsyncInventory {
fn name(&self) -> String {
self.name.to_string()
}
}
#[async_trait]
impl AsyncPluginInventory for DummyAsyncInventory {
async fn load_async(
&self,
_settings: &Settings,
_plugins: &PluginManager,
) -> Result<Inventory, InventoryLoadError> {
Ok(Inventory::builder().build())
}
}
#[derive(Debug)]
struct DummyRunner {
name: &'static str,
}
impl DummyRunner {
fn new(name: &'static str) -> Self {
Self { name }
}
}
impl Plugin for DummyRunner {
fn name(&self) -> String {
self.name.to_string()
}
}
#[async_trait]
impl PluginRunner for DummyRunner {
async fn run_task(
&self,
task: &TaskDefinition,
_hosts: &Hosts,
_connection_resolver: Option<Arc<dyn TaskConnectionResolver>>,
_runner_config: &RunnerConfig,
_max_depth: usize,
) -> Result<TaskResults, genja_core::GenjaError> {
Ok(TaskResults::new(task.name()))
}
}
struct DummyTask {
name: &'static str,
}
impl TaskInfo for DummyTask {
fn name(&self) -> &str {
self.name
}
fn connection_plugin_name(&self) -> Option<&str> {
None
}
fn options(&self) -> Option<&Value> {
None
}
}
#[async_trait]
impl Task for DummyTask {
async fn start_async(
&self,
_host: &Host,
_context: &TaskRuntimeContext,
) -> Result<HostTaskResult, TaskError> {
Ok(HostTaskResult::passed(TaskSuccess::new()))
}
fn execution_mode(&self) -> TaskExecutionMode {
TaskExecutionMode::Async
}
}
fn run_async<F: Future>(future: F) -> F::Output {
Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime should build")
.block_on(future)
}
#[derive(Debug)]
struct DummyTransform {
name: &'static str,
}
impl DummyTransform {
fn new(name: &'static str) -> Self {
Self { name }
}
}
impl Plugin for DummyTransform {
fn name(&self) -> String {
self.name.to_string()
}
}
impl PluginTransformFunction for DummyTransform {
fn transform_function(&self) -> TransformFunction {
TransformFunction::new(|host, _| host.clone())
}
}
#[derive(Debug)]
struct DummyConnection {
name: &'static str,
key: ConnectionKey,
alive: bool,
}
impl DummyConnection {
fn new(name: &'static str) -> Self {
Self {
name,
key: ConnectionKey::new("host1", "dummy"),
alive: false,
}
}
}
impl Plugin for DummyConnection {
fn name(&self) -> String {
self.name.to_string()
}
}
#[async_trait]
impl PluginConnection for DummyConnection {
fn create(&self, key: &ConnectionKey) -> Box<dyn PluginConnection> {
Box::new(Self {
name: self.name,
key: key.clone(),
alive: false,
})
}
async fn open(&mut self, _params: &ResolvedConnectionParams) -> Result<(), String> {
self.alive = true;
Ok(())
}
fn close(&mut self) -> ConnectionKey {
self.alive = false;
self.key.clone()
}
fn is_alive(&self) -> bool {
self.alive
}
}
#[test]
fn plugin_entry_deserializes_individual_and_group() {
let individual: PluginEntry = serde_json::from_value(json!("path/to/lib.so")).unwrap();
match individual {
PluginEntry::Individual(path) => assert_eq!(path, "path/to/lib.so"),
PluginEntry::Group(_) => panic!("expected individual plugin entry"),
}
let grouped: PluginEntry = serde_json::from_value(json!({
"ssh": "path/to/libssh.so",
"telnet": "path/to/libtelnet.so"
}))
.unwrap();
match grouped {
PluginEntry::Group(map) => {
assert_eq!(map.get("ssh"), Some(&"path/to/libssh.so".to_string()));
assert_eq!(map.get("telnet"), Some(&"path/to/libtelnet.so".to_string()));
}
PluginEntry::Individual(_) => panic!("expected grouped plugin entry"),
}
}
#[test]
fn plugins_name_and_group_name_match_variants() {
let connection = Plugins::Connection(Box::new(DummyConnection::new("conn")));
let inventory = Plugins::Inventory(Box::new(DummyInventory::new("inv")));
let async_inventory = Plugins::AsyncInventory(Box::new(DummyAsyncInventory::new("ainv")));
let runner = Plugins::Runner(Box::new(DummyRunner::new("run")));
let transform = Plugins::TransformFunction(Box::new(DummyTransform::new("tf")));
assert_eq!(connection.name(), "conn");
assert_eq!(connection.group_name(), "Connection");
assert_eq!(inventory.name(), "inv");
assert_eq!(inventory.group_name(), "Inventory");
assert_eq!(async_inventory.name(), "ainv");
assert_eq!(async_inventory.group_name(), "Inventory");
assert_eq!(runner.name(), "run");
assert_eq!(runner.group_name(), "Runner");
assert_eq!(transform.name(), "tf");
assert_eq!(transform.group_name(), "TransformFunction");
}
#[test]
fn debug_impls_include_group_and_name() {
let base = DummyPlugin::new("base");
let inventory = DummyInventory::new("inv");
let runner = DummyRunner::new("run");
let transform = DummyTransform::new("tf");
let connection = DummyConnection::new("conn");
let base_dbg = format!("{:?}", &base as &dyn Plugin);
let inventory_dbg = format!("{:?}", &inventory as &dyn PluginInventory);
let runner_dbg = format!("{:?}", &runner as &dyn PluginRunner);
let transform_dbg = format!("{:?}", &transform as &dyn PluginTransformFunction);
let connection_dbg = format!("{:?}", &connection as &dyn PluginConnection);
assert_eq!(base_dbg, "BasePlugin { name: base }");
assert_eq!(inventory_dbg, "InventoryPlugin { name: inv }");
assert_eq!(runner_dbg, "RunnerPlugin { name: run }");
assert_eq!(transform_dbg, "TransformFunctionPlugin { name: tf }");
assert_eq!(connection_dbg, "ConnectionPlugin { name: conn }");
}
#[test]
fn runner_default_run_tasks_delegates_to_run_task_in_task_order() {
let runner = DummyRunner::new("run");
let mut tasks = Tasks::new();
tasks.add_task(DummyTask { name: "first" });
tasks.add_task(DummyTask { name: "second" });
let results =
run_async(runner.run_tasks(&tasks, &Hosts::new(), None, &RunnerConfig::default(), 0))
.expect("default run_tasks should execute");
assert_eq!(results.len(), 2);
assert_eq!(results[0].task_name(), "first");
assert_eq!(results[1].task_name(), "second");
}
#[test]
fn plugin_info_holds_group_and_plugin() {
let info = PluginInfo {
plugin: Box::new(DummyPlugin::new("example")),
group: Some("network".to_string()),
};
assert_eq!(info.plugin.name(), "example");
assert_eq!(info.group.as_deref(), Some("network"));
}
#[test]
fn plugin_entry_rejects_invalid_shapes() {
let bad_individual: Result<PluginEntry, _> = serde_json::from_value(serde_json::json!(123));
assert!(bad_individual.is_err());
let bad_group: Result<PluginEntry, _> = serde_json::from_value(serde_json::json!({
"ssh": 123,
"telnet": false
}));
assert!(bad_group.is_err());
}
}