Compare commits

...

10 Commits

Author SHA1 Message Date
TheJoKlLa
3a16d1031b Added Changelog for INTERFACER Release 2023-03-17 14:20:01 +00:00
TheJoKlLa
7aa808a3fe Update file README.md 2023-03-15 11:19:27 +00:00
Nadja Reitzenstein
bed59f4aaa Fix that part 2023-02-23 14:21:49 +01:00
Nadja Reitzenstein
d196050fe0 prodable machines 2023-02-14 17:03:40 +01:00
Nadja Reitzenstein
198845f176 Fix deadlock (whoops) 2023-01-18 17:38:32 +01:00
Nadja Reitzenstein
386ac5645d Do not do raw processing on restores 2023-01-18 16:59:36 +01:00
Nadja Reitzenstein
98ed9efec9 Improvements for process actor and raw_write 2023-01-18 16:55:43 +01:00
Nadja Reitzenstein
1602879e1e Example actor implementation 2023-01-18 16:30:24 +01:00
Nadja Reitzenstein
5fdfae295a Add raw handling to process actor 2023-01-18 16:26:09 +01:00
Nadja Reitzenstein
b8092e9090 Implement first draft of raw write serverside 2023-01-18 15:58:32 +01:00
14 changed files with 213 additions and 28 deletions

View File

@ -1,5 +1,23 @@
# Revision history for Difluoroborane
## 0.4.3 -- 2023-03-17
Final INTERFACER Rlease
### Features
* improved API
* authenticate users with SmartCard
* access Machines via SmartCard
* log data of machines in Audit-Log
### Updates
* authenticate users via FabReader and the FabFire-Adapter
* pass aditional data form users via server to actors
* provide new interface to handle lockers
## 0.4.2 -- 2022-10-07
Beta Release of INTERFACER Project.
## 0.4.1 -- 2022-04-24
* Initial full implementation of the FabAccess 0.3 API, "Spigots of Berlin".

17
Cargo.lock generated
View File

@ -425,6 +425,12 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "base64"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
[[package]]
name = "bitfield"
version = "0.13.2"
@ -999,6 +1005,7 @@ dependencies = [
"async-process",
"async-trait",
"backtrace",
"base64 0.21.0",
"capnp",
"capnp-rpc",
"chrono",
@ -1467,7 +1474,7 @@ version = "7.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea9fe3952d32674a14e0975009a3547af9ea364995b5ec1add2e23c2ae523ab"
dependencies = [
"base64",
"base64 0.13.0",
"byteorder",
"crossbeam-channel",
"flate2",
@ -2541,7 +2548,7 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
dependencies = [
"base64",
"base64 0.13.0",
"blake2b_simd",
"constant_time_eq",
"crossbeam-utils",
@ -2583,7 +2590,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ee86d63972a7c661d1536fefe8c3c8407321c3df668891286de28abcd087360"
dependencies = [
"base64",
"base64 0.13.0",
]
[[package]]
@ -2592,7 +2599,7 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55"
dependencies = [
"base64",
"base64 0.13.0",
]
[[package]]
@ -3135,7 +3142,7 @@ dependencies = [
"async-stream",
"async-trait",
"axum",
"base64",
"base64 0.13.0",
"bytes",
"futures-core",
"futures-util",

View File

@ -43,6 +43,7 @@ backtrace = "0.3.65"
miette = { version = "4.7.1", features = ["fancy"] }
thiserror = "1.0.31"
toml = "0.5.8"
base64 = "0.21"
# Well-known paths/dirs for e.g. cache
dirs = "4.0.0"

View File

@ -25,3 +25,6 @@ See [INSTALL.md](INSTALL.md)
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md). Thanks!
# Funding
This project was funded by the European Regional Development Fund (ERDF) in the context of the INTERFACER Project [https://www.interfacerproject.eu/](https://www.interfacerproject.eu/)

@ -1 +1 @@
Subproject commit 19f20f5154f0eced6288ff56cac840025ee51da1
Subproject commit cde4677575f8e133ac764663e131c80fc891d545

View File

@ -31,11 +31,18 @@ mod process;
mod shelly;
pub trait Actor {
/// The state is being restored after a restart or recreation of the actor
fn restore(&mut self, state: ArchivedValue<State>) -> BoxFuture<'static, ()> {
self.apply(state)
}
/// The state is a changed state that is applied
fn apply(&mut self, state: ArchivedValue<State>) -> BoxFuture<'static, ()>;
}
pub struct ActorDriver<S: 'static> {
signal: S,
first: bool,
actor: Box<dyn Actor + Send + Sync>,
future: Option<BoxFuture<'static, ()>>,
@ -45,6 +52,7 @@ impl<S: Signal<Item = ArchivedValue<State>>> ActorDriver<S> {
pub fn new(signal: S, actor: Box<dyn Actor + Send + Sync>) -> Self {
Self {
signal,
first: true,
actor,
future: None,
}
@ -60,6 +68,7 @@ where
fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
// Work until there is no more work to do.
loop {
tracing::trace!("polling actor driver");
// Poll the `apply` future. And ensure it's completed before the next one is started
match self
.future
@ -81,9 +90,15 @@ where
Poll::Pending => return Poll::Pending,
Poll::Ready(None) => return Poll::Ready(()),
Poll::Ready(Some(state)) => {
tracing::trace!(?state, "actor driver received state update");
// This future MUST be polled before we exit from the Actor::poll because if we
// do not do that it will not register the dependency and thus NOT BE POLLED.
let f = self.actor.apply(state);
let f = if self.first {
self.first = false;
self.actor.restore(state)
} else {
self.actor.apply(state)
};
self.future.replace(f);
}
}

View File

@ -1,6 +1,7 @@
use futures_util::future::BoxFuture;
use std::collections::HashMap;
use std::process::{Command, Stdio};
use base64::Engine;
use crate::actors::Actor;
use crate::db::ArchivedValue;
@ -30,7 +31,7 @@ impl Process {
}
impl Actor for Process {
fn apply(&mut self, state: ArchivedValue<State>) -> BoxFuture<'static, ()> {
fn restore(&mut self, state: ArchivedValue<State>) -> BoxFuture<'static, ()> {
tracing::debug!(name=%self.name, cmd=%self.cmd, ?state,
"Process actor updating state");
let mut command = Command::new(&self.cmd);
@ -60,6 +61,67 @@ impl Actor for Process {
}
}
let name = self.name.clone();
Box::pin(async move {
match command.output() {
Ok(retv) if retv.status.success() => {
tracing::trace!("Actor was restored");
let outstr = String::from_utf8_lossy(&retv.stdout);
for line in outstr.lines() {
tracing::debug!(%name, %line, "actor stdout");
}
}
Ok(retv) => {
tracing::warn!(%name, ?state, code=?retv.status,
"Actor failed to restore: nonzero exitcode"
);
if !retv.stderr.is_empty() {
let errstr = String::from_utf8_lossy(&retv.stderr);
for line in errstr.lines() {
tracing::warn!(%name, %line, "actor stderr");
}
}
}
Err(error) => tracing::warn!(%name, ?error, "process actor failed to run cmd"),
}
})
}
fn apply(&mut self, state: ArchivedValue<State>) -> BoxFuture<'static, ()> {
tracing::debug!(name=%self.name, cmd=%self.cmd, ?state,
"Process actor updating state");
let mut command = Command::new(&self.cmd);
command
.stdin(Stdio::null())
.args(self.args.iter())
.arg(&self.name);
if state.as_ref().raw.is_empty() {
match &state.as_ref().inner.state {
ArchivedStatus::Free => {
command.arg("free");
}
ArchivedStatus::InUse(by) => {
command.arg("inuse").arg(by.id.as_str());
}
ArchivedStatus::ToCheck(by) => {
command.arg("tocheck").arg(by.id.as_str());
}
ArchivedStatus::Blocked(by) => {
command.arg("blocked").arg(by.id.as_str());
}
ArchivedStatus::Disabled => {
command.arg("disabled");
}
ArchivedStatus::Reserved(by) => {
command.arg("reserved").arg(by.id.as_str());
}
}
} else {
let b64 = base64::prelude::BASE64_STANDARD_NO_PAD.encode(&state.as_ref().raw);
command.arg("raw").arg(b64);
}
let name = self.name.clone();
Box::pin(async move {
match command.output() {

View File

@ -6,10 +6,12 @@ use api::general_capnp::optional;
use api::machine_capnp::machine::{
self, admin, admin::Server as AdminServer, check, check::Server as CheckServer,
in_use as inuse, in_use::Server as InUseServer, info, info::Server as InfoServer, manage,
manage::Server as ManageServer, use_, use_::Server as UseServer, MachineState,
manage::Server as ManageServer, use_, use_::Server as UseServer, MachineState, prodable::Server as ProdableServer,
};
use capnp::capability::Promise;
use capnp::Error;
use capnp_rpc::pry;
use api::machine_capnp::machine::prodable::{ProdWithDataParams, ProdWithDataResults};
#[derive(Clone)]
pub struct Machine {
@ -52,6 +54,9 @@ impl Machine {
}
{
builder.set_use(capnp_rpc::new_client(self.clone()));
if self.resource.get_description().prodable {
builder.set_prodable(capnp_rpc::new_client(self.clone()));
}
}
if self.session.has_manage(&self.resource) {
@ -173,12 +178,20 @@ impl InUseServer for Machine {
fn send_raw_data(
&mut self,
_: inuse::SendRawDataParams,
mut params: inuse::SendRawDataParams,
_: inuse::SendRawDataResults,
) -> Promise<(), ::capnp::Error> {
Promise::err(::capnp::Error::unimplemented(
"method not implemented".to_string(),
))
let data: Vec<u8> = pry!(pry!(params.get()).get_data()).to_vec();
self.resource.send_raw(data);
Promise::ok(())
}
}
impl ProdableServer for Machine {
fn prod_with_data(&mut self, mut params: ProdWithDataParams, _: ProdWithDataResults) -> Promise<(), Error> {
let data: Vec<u8> = pry!(pry!(params.get()).get_data()).to_vec();
self.resource.send_raw(data);
Promise::ok(())
}
}

View File

@ -59,6 +59,16 @@ pub struct MachineDescription {
/// The permission required
#[serde(flatten)]
pub privs: PrivilegesBuf,
#[serde(default = "default_prodable", skip_serializing_if = "bool_is_false", deserialize_with = "deser_bool")]
pub prodable: bool,
}
fn default_prodable() -> bool {
false
}
fn bool_is_false(b: &bool) -> bool {
!b
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -116,6 +126,14 @@ pub struct ModuleConfig {
pub params: HashMap<String, String>,
}
fn deser_bool<'de, D>(d: D) -> Result<bool, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(bool::deserialize(d).unwrap_or(false))
}
pub(crate) fn deser_option<'de, D, T>(d: D) -> std::result::Result<Option<T>, D::Error>
where
D: serde::Deserializer<'de>,

View File

@ -9,7 +9,7 @@ use crate::config::MachineDescription;
use crate::db::ArchivedValue;
use crate::resources::modules::fabaccess::{ArchivedStatus, MachineState, Status};
use crate::resources::state::db::StateDB;
use crate::resources::state::State;
use crate::resources::state::{ArchivedState, State};
use crate::session::SessionHandle;
use crate::users::UserRef;
use rkyv::option::ArchivedOption;
@ -161,7 +161,7 @@ impl Resource {
fn set_state(&self, state: MachineState) {
let mut serializer = AllocSerializer::<1024>::default();
serializer.serialize_value(&state);
let _ = serializer.serialize_value(&State::new(state));
let archived = ArchivedValue::new(serializer.into_serializer().into_inner());
self.inner.set_state(archived)
}
@ -177,6 +177,26 @@ impl Resource {
self.set_state(new);
}
pub fn send_raw(&self, data: Vec<u8>) {
let mut serializer = AllocSerializer::<1024>::default();
let archived = {
let old_state_ref = self.inner.get_state_ref();
let old_state: &ArchivedState = old_state_ref.as_ref();
let m_state: MachineState =
match Deserialize::<MachineState, _>::deserialize(&old_state.inner, &mut serializer) {
Ok(s) => s,
Err(error) => {
tracing::error!(?error, "failed to deserialize stored state!");
return;
}
};
let _ = serializer.serialize_value(&State::with_raw(m_state, data));
ArchivedValue::new(serializer.into_serializer().into_inner())
};
self.inner.set_state(archived);
}
pub async fn try_update(&self, session: SessionHandle, new: Status) {
let old = self.get_state();
let old: &Archived<State> = old.as_ref();

View File

@ -88,9 +88,7 @@ impl MachineState {
Deserialize::deserialize(state, &mut Infallible).unwrap()
}
pub fn to_state(&self) -> State {
State {
inner: self.clone(),
}
State::new(self.clone())
}
pub fn free(previous: Option<UserRef>) -> Self {

View File

@ -1,5 +1,4 @@
use std::fmt::{Debug, Display, Formatter};
use std::{fmt, hash::Hasher};
use std::fmt;
use std::ops::Deref;
@ -20,6 +19,16 @@ pub mod value;
#[archive_attr(derive(Debug))]
pub struct State {
pub inner: MachineState,
pub raw: Vec<u8>,
}
impl State {
pub fn new(inner: MachineState) -> Self {
Self::with_raw(inner, Vec::new())
}
pub fn with_raw(inner: MachineState, raw: Vec<u8>) -> Self {
Self { inner, raw }
}
}
impl fmt::Debug for State {
@ -34,8 +43,8 @@ impl fmt::Debug for State {
}
impl fmt::Display for ArchivedState {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(&self.inner, f)
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.inner, f)
}
}
@ -62,7 +71,7 @@ struct StateVisitor;
impl<'de> serde::de::Visitor<'de> for StateVisitor {
type Value = State;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "a map from OIDs to value objects")
}
@ -75,7 +84,7 @@ impl<'de> serde::de::Visitor<'de> for StateVisitor {
));
}
let val: MachineState = map.next_value()?;
Ok(State { inner: val })
Ok(State { inner: val, raw: Vec::new() })
}
}

View File

@ -100,6 +100,14 @@ def on_reserve(args, actor_name, user_id):
print("process called with unexpected combo id %s and state 'Reserved'" % actor_name)
exit(-1)
def on_raw(args, actor_name, data):
"""
Function called when a raw_write call was issued for the connected machine
data is the data passed with that call, base64-encoded
"""
if args.verbose > 2:
print("on_raw called! data=" + data)
def main(args):
"""
@ -121,6 +129,9 @@ def main(args):
else:
args.verbose = 0
if args.restore and args.verbose > 0:
print("running in restore mode")
# You could also check the actor name here and call different functions
# depending on that variable instead of passing it to the state change
# methods.
@ -138,6 +149,8 @@ def main(args):
on_disabled(args, args.name)
elif new_state == "reserved":
on_reserve(args, args.name, args.userid)
elif new_state == "raw":
on_raw(args, args.name, args.data)
else:
print("Process actor called with unknown state %s" % new_state)
@ -150,6 +163,7 @@ if __name__ == "__main__":
parser.add_argument("-q", "--quiet", help="be less verbose", action="store_true")
parser.add_argument("-v", "--verbose", help="be more verbose", action="count")
parser.add_argument("-r", "--restore", help="run in restore mode", action="store_true")
parser.add_argument("name",
help="name of this actor as configured in bffh.dhall"
@ -175,5 +189,8 @@ if __name__ == "__main__":
parser_reserved = subparsers.add_parser("reserved")
parser_reserved.add_argument("userid", help="The user that reserved the machine")
parser_raw = subparsers.add_parser("raw")
parser_raw.add_argument("data", help="Raw data set for this actor")
args = parser.parse_args()
main(args)

View File

@ -57,7 +57,7 @@
--
-- If you want either parents or permissions to be empty its best to completely skip it:
testrole = {
permissions = [ "lab.some.admin" ]
permissions = [ "lab.some.admin", "bffh.users.admin", "bffh.users.manage" ]
},
somerole = {
parents = ["testparent"],
@ -135,7 +135,8 @@
manage = "lab.test.admin",
name = "Another",
read = "lab.test.read",
write = "lab.test.write"
write = "lab.test.write",
prodable = True
},
Yetmore = {
description = "Yet more test machines",
@ -213,7 +214,7 @@
-- actor can only be connected to one machine.
actor_connections = [
{ machine = "Testmachine", actor = "Shelly1234" },
{ machine = "Another", actor = "Bash" },
{ machine = "Another", actor = "DoorControl1" },
{ machine = "Yetmore", actor = "Bash2" },
{ machine = "Yetmore", actor = "FailBash"}
],
@ -229,6 +230,9 @@
-- Linking up machines to initiators. Similar to actors a machine can have several initiators assigned but an
-- initiator can only be assigned to one machine.
-- The below is once again how you have to define *no* initiators.
init_connections = [] : List { machine : Text, initiator : Text }
init_connections = [] : List { machine : Text, initiator : Text },
--init_connections = [{ machine = "Testmachine", initiator = "Initiator" }]
spacename = Some "foospace",
instanceurl = Some "https://example.com"
}