Skip to content

Commit

Permalink
Merge pull request #45 from tinythings/isbm-pylang
Browse files Browse the repository at this point in the history
Add Python interpreter
  • Loading branch information
isbm authored Nov 24, 2024
2 parents 91df9d2 + 033c84a commit 921786b
Show file tree
Hide file tree
Showing 14 changed files with 1,981 additions and 133 deletions.
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

0 comments on commit 921786b

Please sign in to comment.