tinipon 0.1.0

Systems and services inventory and monitoring as Linked Data (RDF)
Documentation
//! Utilities for building tinipon providers
//!
//! A tinipon provider is a stand-alone binary that
//! fulfills the following protocol:
//!
//! * It takes two command line arguments:
//!   * the name of the operation (currently `describe` or `run`)
//!   * an NTriples term (e.g. an IRI) to use as the subject for the
//!     RDF triples describing the result
//! * It outputs NTriples data describing its results to stdout
//!   * On `describe`, it self-describes itself
//!   * On `run`, it provides information it collected from the system
//! * It exits with a defined exit status
//!   * `0` on successful operation
//!   * `1` if the command line arguments were invalid
//!   * `2` if the operation itself failed
//!
//! For example:
//!
//! ```shell
//! nik@makadamia ~/T/t/tinipon-systemd (main) [1]> ./target/release/tinipon-systemd describe "<urn:uuid:42434cfb-c6ff-4748-ad59-a23b5f61a94d>"
//! <urn:uuid:42434cfb-c6ff-4748-ad59-a23b5f61a94d> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://tinipon.taganak.net/vocab#ProviderResult> .
//! <urn:uuid:42434cfb-c6ff-4748-ad59-a23b5f61a94d> <http://tinipon.taganak.net/vocab#provider> <http://tinipon.taganak.net/reg/providers/tinipon-systemd> .
//! <http://tinipon.taganak.net/reg/providers/tinipon-systemd> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://tinipon.taganak.net/vocab#Provider> .
//! <http://tinipon.taganak.net/reg/providers/tinipon-systemd> <http://tinipon.taganak.net/vocab#module> <http://tinipon.taganak.net/reg/modules/system#> .
//! nik@makadamia ~/T/t/tinipon-systemd (main)> ./target/release/tinipon-systemd run "<urn:uuid:42434cfb-c6ff-4748-ad59-a23b5f61a94d>"
//! <urn:uuid:42434cfb-c6ff-4748-ad59-a23b5f61a94d> <http://tinipon.taganak.net/vocab#fact> <urn:uuid:dd699f80-4544-4b74-bac1-dbc19f18ef1f> .
//! <urn:uuid:dd699f80-4544-4b74-bac1-dbc19f18ef1f> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://tinipon.taganak.net/reg/modules/system#System> .
//! <urn:uuid:dd699f80-4544-4b74-bac1-dbc19f18ef1f> <http://tinipon.taganak.net/reg/modules/system#hostname> "makadamia" .
//! ```
//!
//! If transformed into Turtle, this results in:
//!
//! ```turtle
//! @prefix tp: <http://tinipon.taganak.net/vocab#> .
//! @prefix tpm.system: <http://tinipon.taganak.net/reg/modules/system#> .
//! @prefix tpp: <http://tinipon.taganak.net/reg/providers/> .
//!
//! tpp:tinipon-systemd tp:module tpm.system: ;
//!        a tp:Provider .
//!
//! <urn:uuid:dd699f80-4544-4b74-bac1-dbc19f18ef1f> tpm.system:hostname "makadamia" ;
//!         a tpm.system:System .
//!
//! <urn:uuid:63d7ed73-081d-4437-a2a6-b89362abb6cc> tp:fact <urn:uuid:dd699f80-4544-4b74-bac1-dbc19f18ef1f> ;
//!        tp:provider tpp:tinipon-systemd ;
//!        a tp:ProviderResult .
//! ```

use std::{pin::pin, sync::Arc};

use futures::Stream;
use taganak_core::prelude::{Term, Triple};

use async_stream::stream;
use taganak_framework::prelude::StreamExt;
use taganak_orm::re::TripleError;

use tracing::error;

/// Error occuring during execution of a provider
#[derive(Debug, Clone, thiserror::Error)]
pub enum ExecError {
    #[error("Invalid arguments")]
    ArgError,
    #[error("Operation failed")]
    OpError,
}

impl ExecError {
    /// Convert the error into a defined exit code
    pub fn exit_code(&self) -> i32 {
        match self {
            Self::ArgError => 1,
            Self::OpError => 2,
        }
    }
}

/// Interface for a provider that can be used with the [tinipon_main] macro
pub trait Provider {
    /// Short name of the provider, fit for insertion as URL component
    ///
    /// Can be implemented with the [tinipon_defaults] macro
    fn name() -> &'static str;

    /// Version
    ///
    /// Can be implemented with the [tinipon_defaults] macro
    fn version() -> &'static str;

    /// List of tinipon modules provided by this provider
    ///
    /// Tinipon modules are categories of information that are
    /// normally filled by one provider. For example, the `system` module
    /// can be supported by a `systemd` provider, or by anything else
    /// on non-systemd systems.
    fn modules() -> &'static [&'static str] {
        &[]
    }

    /// RDF subject to use for the provider itself
    ///
    /// For registered providers, this defaults to the correct namespace
    /// term under the Tinipon namespace. Unregistered modules must override
    /// this method to build their own term/IRI.
    fn subject() -> Term {
        format!(
            "<http://tinipon.taganak.net/reg/providers/{}>",
            Self::name()
        )
        .try_into()
        .expect("default impl should be ok")
    }

    /// Stream of triples describing the provider itself
    ///
    /// The self-decription must be linked to `recv_subject` with
    /// the `provider` predicate.
    async fn describe(
        recv_subject: Arc<Term>,
    ) -> impl Stream<Item = Result<Arc<Triple>, TripleError>> {
        let subject = Arc::new(Self::subject());
        let a: Arc<Term> = Arc::new(
            "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>"
                .try_into()
                .expect("static IRI"),
        );
        let by: Arc<Term> = Arc::new(
            "<http://tinipon.taganak.net/vocab#provider>"
                .try_into()
                .expect("static IRI"),
        );
        let module_pred: Arc<Term> = Arc::new(
            "<http://tinipon.taganak.net/vocab#module>"
                .try_into()
                .expect("static IRI"),
        );
        let provider: Arc<Term> = Arc::new(
            "<http://tinipon.taganak.net/vocab#Provider>"
                .try_into()
                .expect("static IRI"),
        );
        let result: Arc<Term> = Arc::new(
            "<http://tinipon.taganak.net/vocab#ProviderResult>"
                .try_into()
                .expect("static IRI"),
        );
        stream! {
            yield Triple::new(recv_subject.clone(), a.clone(), result.clone());
            yield Triple::new(recv_subject.clone(), by.clone(), subject.clone());
            yield Triple::new(subject.clone(), a.clone(), provider.clone());

            for module in Self::modules() {
                yield Triple::new(subject.clone(), module_pred.clone(), Arc::new(format!("<http://tinipon.taganak.net/reg/modules/{}#>", module).try_into().expect("should be valid")));
            }
        }
    }

    /// Stream of triples describing the results of the provider run
    ///
    /// All RDF resources yielded by this stream must be linked to `recv_subject` with
    /// the `fact` predicate.
    async fn run(recv_subject: Arc<Term>) -> impl Stream<Item = Result<Arc<Triple>, TripleError>>;

    /// Execute this provider from the standard command line arguments
    async fn exec() -> Result<(), ExecError> {
        let mut args = std::env::args();

        if args.len() != 3 {
            return Err(ExecError::ArgError);
        }

        let recv_subject = args.next_back().expect("we just checked");
        let op = args.next_back().expect("we jsut checked");

        let recv_subject: Arc<Term> = match recv_subject.try_into() {
            Ok(term) => Arc::new(term),
            Err(e) => {
                return Err(ExecError::ArgError);
            }
        };

        match op.as_str() {
            "describe" => {
                let mut describe = pin!(Self::describe(recv_subject.clone()).await);
                while let Some(triple) = describe.next().await {
                    if let Ok(triple) = triple {
                        println!("{}", *triple);
                    } else {
                        error!("Failed to describe: {:?}", triple);
                        return Err(ExecError::OpError);
                    }
                }
            }
            "run" => {
                let mut run = pin!(Self::run(recv_subject.clone()).await);
                while let Some(triple) = run.next().await {
                    if let Ok(triple) = triple {
                        println!("{}", *triple);
                    } else {
                        error!("Failed to run: {:?}", triple);
                        return Err(ExecError::OpError);
                    }
                }
            }
            other => {
                error!("Invalid operation: {other}");
                return Err(ExecError::ArgError);
            }
        }

        Ok(())
    }
}

/// Generate a main function using the tokio runtime around a
/// type implementing [Provider]
#[macro_export]
macro_rules! tinipon_main {
    ($Provider:ty) => {
        fn main() {
            let res = tinipon::prelude::tokio::runtime::Builder::new_multi_thread()
                .enable_all()
                .build()
                .unwrap()
                .block_on(<$Provider>::exec());

            if let Err(e) = res {
                std::process::exit(e.exit_code());
            }
        }
    };
}

/// Implement some [Provider] methods with sane defaults
///
/// * [Provider::name] from the `CARGO_BIN_NAME` environment variable
/// * [Provider::version] from the `CARGO_PKG_VERSION` environment variable
#[macro_export]
macro_rules! tinipon_defaults {
    () => {
        fn name() -> &'static str {
            env!("CARGO_BIN_NAME")
        }

        fn version() -> &'static str {
            env!("CARGO_PKG_VERSION")
        }
    };
}