Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Python interpreter #45

Merged
merged 17 commits into from
Nov 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,646 changes: 1,526 additions & 120 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ edition = "2021"

[dependencies]
chrono = "0.4.38"
clap = { version = "4.5.18", features = ["unstable-styles"] }
clap = { version = "4.5.21", features = ["unstable-styles"] }
colored = "2.1.0"
libsysinspect = { path = "./libsysinspect" }
log = "0.4.22"
sysinfo = { version = "0.31.4", features = ["linux-tmpfs"] }
sysinfo = { version = "0.32.0", features = ["linux-tmpfs"] }
openssl = { version = "0.10", features = ["vendored"] }

[workspace]
resolver = "2"
Expand Down
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ devel-musl:
mv target/x86_64-unknown-linux-musl/debug/run target/x86_64-unknown-linux-musl/debug/sys/

musl:
cargo build --release
cargo build --release --workspace --target x86_64-unknown-linux-musl
cargo build -p proc -p net -p run --release --target x86_64-unknown-linux-musl
rm -rf target/x86_64-unknown-linux-musl/release/sys
mkdir -p target/x86_64-unknown-linux-musl/release/sys
Expand All @@ -34,8 +34,13 @@ devel:
mv target/debug/run target/debug/sys/

build:
cargo build --release
cargo build --release --workspace
cargo build -p proc -p net -p run --release
rm -rf target/release/sys/
mkdir -p target/release/sys/
mv target/release/proc target/release/sys/
mv target/release/net target/release/sys/
mv target/release/run target/release/sys/

# Move plugin binaries
rm -rf target/release/sys
Expand Down
12 changes: 12 additions & 0 deletions examples/models/inherited/evt.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,15 @@ constraints:
interfaces:
- fact: if-up.virbr1.port
equals: static(entities.addresses.claims.interfaces.virtual.port)

actions:
python-module-tests:
descr: Check if python module works at all
module: file.conf
bind:
- addresses

state:
interfaces:
opts:
args:
3 changes: 3 additions & 0 deletions libsysinspect/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ prettytable-rs = "0.10.0"
rand = "0.8.5"
regex = "1.10.6"
rsa = { version = "0.9.6", features = ["pkcs5", "sha1", "sha2"] }
rustpython = { version = "0.4.0", features = ["freeze-stdlib"] }
rustpython-pylib = { version = "0.4.0", features = ["freeze-stdlib"] }
rustpython-vm = { version = "0.4.0", features = ["freeze-stdlib"] }
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.128"
serde_yaml = "0.9.34"
Expand Down
41 changes: 35 additions & 6 deletions libsysinspect/src/intp/actproc/modfinder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::{
constraints::{Constraint, ConstraintKind},
functions,
},
pylang,
util::dataconv,
SysinspectError,
};
Expand Down Expand Up @@ -38,7 +39,7 @@ pub struct ModCall {
constraints: Vec<Constraint>,

// Module params
args: HashMap<String, String>,
args: HashMap<String, String>, // XXX: Should be String/Value, not String/String!
opts: Vec<String>,
}

Expand Down Expand Up @@ -201,12 +202,40 @@ impl ModCall {
cret
}

/// Run the module
pub fn run(&self) -> Result<Option<ActionResponse>, SysinspectError> {
// Event reactor:
// - Configurable
// - Chain plugins/functions
// - Event reactions
// - Should probably store all the result in a common structure
if self.module.extension().unwrap_or_default().to_str().unwrap_or_default().eq("py") {
self.run_python_module()
} else {
self.run_native_module()
}
}

/// Runs python script module
fn run_python_module(&self) -> Result<Option<ActionResponse>, SysinspectError> {
log::debug!("Calling Python module: {}", self.module.as_os_str().to_str().unwrap_or_default());

let opts = self.opts.iter().map(|v| json!(v)).collect::<Vec<serde_json::Value>>();
let args = self.args.iter().map(|(k, v)| (k.to_string(), json!(v))).collect::<HashMap<String, serde_json::Value>>();

match pylang::pvm::PyVm::new(None, None).as_ptr().call(&self.module, Some(opts), Some(args)) {
Ok(out) => match serde_json::from_str::<ActionModResponse>(&out) {
Ok(r) => Ok(Some(ActionResponse::new(
self.eid.to_owned(),
self.aid.to_owned(),
self.state.to_owned(),
r.clone(),
self.eval_constraints(&r),
))),
Err(e) => Err(SysinspectError::ModuleError(format!("JSON error: {e}"))),
},
Err(err) => Err(err),
}
}

/// Runs native external module
fn run_native_module(&self) -> Result<Option<ActionResponse>, SysinspectError> {
log::debug!("Calling native module: {}", self.module.as_os_str().to_str().unwrap_or_default());
match Command::new(&self.module).stdin(Stdio::piped()).stdout(Stdio::piped()).spawn() {
Ok(mut p) => {
// Send options
Expand Down
27 changes: 24 additions & 3 deletions libsysinspect/src/intp/conf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,10 @@ impl Config {
Err(SysinspectError::ModelDSLError("Unable to parse configuration".to_string()))
}

/// Get module from the namespace
/// Get module (or Python module) from the namespace
pub fn get_module(&self, namespace: &str) -> Result<PathBuf, SysinspectError> {
// Fool-proof cleanup, likely a bad idea
let modpath = &self.modules.to_owned().unwrap_or(PathBuf::from(DEFAULT_MODULES_ROOT)).join(
let mut modpath = self.modules.to_owned().unwrap_or(PathBuf::from(DEFAULT_MODULES_ROOT)).join(
namespace
.trim_start_matches('.')
.trim_end_matches('.')
Expand All @@ -98,8 +98,29 @@ impl Config {
.join("/"),
);

let pymodpath = modpath
.parent()
.unwrap()
.join(format!("{}.py", modpath.file_name().unwrap().to_os_string().to_str().unwrap_or_default()));

// Collision
if pymodpath.exists() && modpath.exists() {
return Err(SysinspectError::ModuleError(format!(
"Module names must be unique, however both \"{}\" and \"{}\" do exist. Please rename one of these, update your model and continue.",
pymodpath.file_name().unwrap_or_default().to_str().unwrap_or_default(),
modpath.file_name().unwrap_or_default().to_str().unwrap_or_default()
)));
}

if !modpath.exists() {
return Err(SysinspectError::ModuleError(format!("Module \"{}\" was not found at {:?}", namespace, modpath)));
if !pymodpath.exists() {
return Err(SysinspectError::ModuleError(format!(
"No module \"{}\" was not found as \"{:?}\"",
namespace, modpath
)));
} else {
modpath = pymodpath;
}
}

Ok(modpath.to_owned())
Expand Down
1 change: 1 addition & 0 deletions libsysinspect/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mod logger;
pub mod mdescr;
pub mod modlib;
pub mod proto;
pub mod pylang;
pub mod reactor;
pub mod rsa;
pub mod tmpl;
Expand Down
4 changes: 4 additions & 0 deletions libsysinspect/src/pylang/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod pvm;
pub mod pylib;

pub static PY_MAIN_FUNC: &str = "main";
206 changes: 206 additions & 0 deletions libsysinspect/src/pylang/pvm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/*
Python virtual machine
*/

use crate::{pylang::PY_MAIN_FUNC, SysinspectError};
use colored::Colorize;
use rustpython_vm::{
compiler::Mode::Exec,
function::{FuncArgs, KwArgs},
AsObject, PyResult,
};
use rustpython_vm::{Interpreter, Settings};
use rustpython_vm::{PyObjectRef, VirtualMachine};
use serde_json::{json, Value};
use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
sync::Arc,
};

// use super::pylib::pysystem::syscore;

pub struct PyVm {
itp: Interpreter,
libpath: String,
modpath: String,
}

impl PyVm {
pub fn new(libpath: Option<String>, modpath: Option<String>) -> Self {
let mut cfg = Settings::default();
let libpath = libpath.unwrap_or("/usr/share/sysinspect/lib".to_string());
cfg.path_list.push(libpath.to_string());

let itp = rustpython::InterpreterConfig::new()
.init_stdlib()
.settings(cfg)
.init_hook(Box::new(|vm| {
//vm.add_native_module("syscore".to_owned(), Box::new(syscore::make_module));
}))
.interpreter();

Self { itp, libpath: libpath.to_string(), modpath: modpath.unwrap_or("/usr/share/sysinspect/modules/".to_string()) }
}

/// Load main script of a module by a regular namespace
fn load_by_ns(&self, ns: &str) -> Result<String, SysinspectError> {
// XXX: util::get_namespace() ? Because something similar exists for the binaries
let pbuff = PathBuf::from(&self.modpath)
.join(format!("{}.py", ns.replace(".", "/").trim_start_matches("/").trim_end_matches("/")));
self.load_by_path(&pbuff)
}

fn load_by_path(&self, pth: &PathBuf) -> Result<String, SysinspectError> {
if pth.exists() {
return Ok(fs::read_to_string(pth).unwrap_or_default());
}

Err(SysinspectError::ModuleError(format!("Module at {} was not found", pth.to_str().unwrap_or_default().yellow())))
}

fn load_pylib(&self, vm: &VirtualMachine) -> Result<(), SysinspectError> {
match vm.import("sys", 0) {
Ok(sysmod) => match sysmod.get_attr("path", vm) {
Ok(syspath) => {
if let Err(err) = vm.call_method(&syspath, "append", (&self.libpath,)) {
return Err(SysinspectError::ModuleError(format!("{:?}", err)));
}
}
Err(err) => {
return Err(SysinspectError::ModuleError(format!("{:?}", err)));
}
},
Err(err) => {
return Err(SysinspectError::ModuleError(format!("{:?}", err)));
}
};
Ok(())
}

#[allow(clippy::arc_with_non_send_sync)]
pub fn as_ptr(&self) -> Arc<&Self> {
Arc::new(self)
}

#[allow(clippy::wrong_self_convention)]
#[allow(clippy::only_used_in_recursion)]
fn from_json(&self, vm: &VirtualMachine, value: Value) -> PyResult<PyObjectRef> {
Ok(match value {
Value::Null => vm.ctx.none(),
Value::Bool(b) => vm.ctx.new_bool(b).into(),
Value::Number(num) => {
if let Some(i) = num.as_i64() {
vm.ctx.new_int(i).into()
} else if let Some(f) = num.as_f64() {
vm.ctx.new_float(f).into()
} else {
vm.ctx.none()
}
}
Value::String(s) => vm.ctx.new_str(s).into(),
Value::Array(arr) => vm
.ctx
.new_list(arr.into_iter().map(|item| self.from_json(vm, item).expect("Failed to convert JSON")).collect())
.into(),
Value::Object(obj) => {
let py_dict = vm.ctx.new_dict();
for (key, val) in obj {
let py_val = self.from_json(vm, val)?;
py_dict.set_item(key.as_str(), py_val, vm)?;
}
py_dict.into()
}
})
}

/// Call a light Python module
pub fn call<T: AsRef<Path>>(
self: Arc<&Self>, namespace: T, opts: Option<Vec<Value>>, args: Option<HashMap<String, Value>>,
) -> Result<String, SysinspectError> {
self.itp.enter(|vm| {
let lpth = Path::new(&self.libpath);
if !lpth.exists() || !lpth.is_dir() {
return Err(SysinspectError::ModuleError(format!("Directory {} does not exist", &self.libpath)));
}
self.load_pylib(vm)?;

// Get script source
let src = if namespace.as_ref().is_absolute() {
self.load_by_path(&namespace.as_ref().to_path_buf())?
} else {
self.load_by_ns(namespace.as_ref().to_str().unwrap_or_default())?
};

let code_obj = match vm.compile(&src, Exec, "<embedded>".to_owned()) {
Ok(src) => src,
Err(err) => {
return Err(SysinspectError::ModuleError(format!(
"Unable to compile source code for {}: {err}",
namespace.as_ref().to_str().unwrap_or_default()
)));
}
};

log::debug!("Prepared to launch dispatcher for python script");

let scope = vm.new_scope_with_builtins();
if let Err(err) = vm.run_code_obj(code_obj, scope.clone()) {
let mut buff = String::new();
_ = vm.write_exception(&mut buff, &err);
return Err(SysinspectError::ModuleError(format!(
"Error running Python function \"{}\": {}",
namespace.as_ref().to_str().unwrap_or_default(),
buff.trim()
)));
}

// opts/args
let py_opts = opts.into_iter().map(|val| self.from_json(vm, json!(val)).unwrap()).collect::<Vec<_>>();
let py_args = vm.ctx.new_dict();
for (key, val) in args.unwrap_or_default() {
let py_key = vm.ctx.new_str(key);
let py_val = self.from_json(vm, val).unwrap();
py_args.set_item(py_key.as_object(), py_val, vm).unwrap();
}

let kwargs: KwArgs = py_args
.into_iter()
.map(|(k, v)| (k.downcast::<rustpython_vm::builtins::PyStr>().unwrap().as_str().to_string(), v))
.collect();

let fref = match scope.globals.get_item(PY_MAIN_FUNC, vm) {
Ok(fref) => fref,
Err(err) => {
let mut buff = String::new();
_ = vm.write_exception(&mut buff, &err);
return Err(SysinspectError::ModuleError(format!(
"Error running Python function \"{}\": {}",
namespace.as_ref().to_str().unwrap_or_default(),
buff.trim()
)));
}
};

let r = match fref.call(FuncArgs::new(py_opts, kwargs), vm) {
Ok(r) => r,
Err(err) => {
let mut buff = String::new();
_ = vm.write_exception(&mut buff, &err);
return Err(SysinspectError::ModuleError(format!(
"Error running \"{}\" Python module:\n{}",
namespace.as_ref().to_str().unwrap_or_default(),
buff.trim()
)));
}
};

if let Ok(py_str) = r.downcast::<rustpython_vm::builtins::PyStr>() {
return Ok(py_str.as_str().to_string());
}

Err(SysinspectError::ModuleError("Python script does not returns a JSON string".to_string()))
})
}
}
1 change: 1 addition & 0 deletions libsysinspect/src/pylang/pylib/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod pysystem;
Loading
Loading