Skip to content
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
4 changes: 2 additions & 2 deletions ci/methods.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM rust:bullseye
FROM rust:bookworm
LABEL ci=ncf/ci/methods.Dockerfile

# Accept all OSes
Expand All @@ -9,6 +9,6 @@ wget https://repository.rudder.io/tools/rudder-setup
sed -i "s/set -e/set -xe/" rudder-setup
sed -i "s/rudder agent inventory//" rudder-setup
sed -i "s/rudder agent health/rudder agent health || true/" rudder-setup
sh ./rudder-setup setup-agent latest
sh ./rudder-setup setup-agent 9.0-nightly
curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin
EOF
2 changes: 1 addition & 1 deletion policies/lib/tests/quick/ordering.pl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# ordering from cfengine documentation
# convert to a hash { promise_type => order_id }
my $orderings = {
agent => make_hash(qw/meta vars defaults classes users files packages guest_environments methods processes services commands storage databases augeas template reports/),
agent => make_hash(qw/meta vars defaults classes users files packages guest_environments methods processes services commands storage databases augeas template reports ruddercommands template/),
edit_line => make_hash(qw/meta vars defaults classes delete_lines field_edits insert_lines replace_patterns reports/),
server => make_hash(qw/vars classes access roles/),
monitor => make_hash(qw/vars classes measurements reports/),
Expand Down
36 changes: 26 additions & 10 deletions policies/module-types/commands/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
mod cli;
use crate::cli::Cli;
use anyhow::{Context, Result, bail};
use rudder_module_type::rudder_info;
use rudder_module_type::{
CheckApplyResult, ModuleType0, ModuleTypeMetadata, Outcome, PolicyMode, ValidateResult,
cfengine::called_from_agent, parameters::Parameters, run_module,
};
use rustix::{fs::Mode, process::umask};
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::{Value, json};
use std::io::ErrorKind;
use std::{
collections::HashMap,
fs::{self, File},
Expand All @@ -11,15 +20,6 @@ use std::{
str::from_utf8,
time::{Duration, Instant},
};

use anyhow::{Context, Result, bail};
use rudder_module_type::{
CheckApplyResult, ModuleType0, ModuleTypeMetadata, Outcome, PolicyMode, ValidateResult,
cfengine::called_from_agent, parameters::Parameters, run_module,
};
use rustix::{fs::Mode, process::umask};
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::{Value, json};
use wait_timeout::ChildExt;

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
Expand Down Expand Up @@ -237,7 +237,23 @@ impl Commands {

let start_time = Instant::now();

let mut child = command.spawn()?;
let mut child = match command.spawn() {
Ok(child) => child,
Err(e) => match e.kind() {
ErrorKind::NotFound => {
bail!(
"Command '{}' not found. Please check if it's installed and in PATH",
p.command
);
}
ErrorKind::PermissionDenied => {
bail!("Permission denied. Could not execute '{}'", p.command);
}
_ => {
bail!("Failed to spawn process: {}", e);
}
},
};
if let Some(stdin) = &p.stdin {
let mut child_stdin = child.stdin.take().expect("Failed to get child stdin");
if p.stdin_add_newline {
Expand Down
18 changes: 0 additions & 18 deletions policies/rudder-module-type/README.adoc

This file was deleted.

63 changes: 63 additions & 0 deletions policies/rudder-module-type/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Rudder Module - rudder

This repository contains a Rudder module runner.

## Running the Module

To run a compiled module the same way the agent does, you must use the `--cfengine` parameter:

```bash
/opt/rudder/bin/rudder-module-commands --cfengine
````

## Protocol Overview

The communication protocol is a **mix of raw text and JSON messages**.

1. The first message must always be a raw text line indicating the agent version:

```
cf-agent 3.24.3 v1
```

2. The following messages must be JSON objects. They start with a `validate_promise` operation, followed by `evaluate_promise` operations (see [CFEngine custom promises protocol documentation](https://docs.cfengine.com/docs/3.24/reference-promise-types-custom.html#protocol)).

### Example Session

```bash
~commands% /opt/rudder/bin/rudder-module-commands --cfengine
cf-agent 3.24.3 v1

Commands 0.0.1 v1 json_based action_policy

{"filename":"/tmp/oui.fda","line_number":42,"operation":"validate_promise","log_level":"info","promise_type":"ruddercommand","promiser":"fda_test","attributes":{"node_id":"root","agent_frequency_minutes":"5","rudder_module_protocol":0,"data":{"args":"","chdir":"","command":"/bin/true /tmp/.tmpcMCLwA/target.txt","compliant_codes":"","env_vars":"","gid":"1000","in_shell":false,"output_to_file":"","repaired_codes":"0","run_in_audit_mode":false,"shell_path":"/bin/sh","show_content":true,"stdin":"","stdin_add_newline":true,"strip_output":false,"timeout":"30","uid":"1000","umask":"0022"}}}

{"operation":"validate_promise","promiser":"fda_test","attributes":{"temporary_dir":"/var/rudder/tmp/","backup_dir":"/var/rudder/modified-files/","state_dir":"/var/rudder/cfengine-community/state/","node_id":"root","agent_frequency_minutes":5,"rudder_module_protocol":0,"report_id":null,"data":{"args":"","chdir":"","command":"/bin/true /tmp/.tmpcMCLwA/target.txt","compliant_codes":"","env_vars":"","gid":"1000","in_shell":false,"output_to_file":"","repaired_codes":"0","run_in_audit_mode":false,"shell_path":"/bin/sh","show_content":true,"stdin":"","stdin_add_newline":true,"strip_output":false,"timeout":"30","uid":"1000","umask":"0022"},"action_policy":"fix"},"result":"valid"}
```

## Notes

* Everything under the `"data"` subkey must be adapted depending on the module you want to execute.
* The `"promise_type"` value also changes depending on the module.

When sending JSON messages (`validate_promise`, `evaluate_promise`), the following top-level keys are required:

| Key | Description |
|--------------|-----------------------------------------------------------------------------|
| `filename` | Path to the file containing the promise. |
| `line_number`| Line number in the policy file. |
| `operation` | The requested operation (`validate_promise`, `evaluate_promise`, etc.). |
| `log_level` | Log verbosity (e.g. `info`). |
| `promise_type` | Type of promise (must match the module, e.g. `ruddercommand`). |
| `promiser` | Identifier of the promise being validated/evaluated. |
| `attributes` | Object containing metadata and module-specific parameters (see below). |

Inside `attributes`, some subkeys are also mandatory:

| Subkey | Description |
|-------------------|-------------------------------------------------------------------------|
| `node_id` | Node uuid. |
| `agent_frequency_minutes` | Agent run frequency in minutes. |
| `rudder_module_protocol` | Protocol version (typically `0`). |
| `data` | Object containing module-specific data (e.g. command, args, env, etc.). |

6 changes: 6 additions & 0 deletions policies/rudder-module-type/src/cfengine/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,12 @@ pub(crate) struct TerminateRequest {
operation: TerminateOperation,
}

#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
pub(crate) enum Request {
Validate(ValidateRequest),
Evaluate(EvaluateRequest),
Terminate(TerminateRequest),
}
////////////////////////////////////

// {"operation": "validate_promise", "promiser": "/opt/cfengine/masterfiles", "attributes": {"repo": "https://github.com/cfengine/masterfiles"}, "result": "valid"}
Expand Down
92 changes: 63 additions & 29 deletions policies/rudder-module-type/src/cfengine/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use std::{
use anyhow::{Error, bail};
use serde::Serialize;

use crate::cfengine::protocol::Request;
use crate::{
ModuleType0, ProtocolResult, Runner0,
cfengine::{
Expand Down Expand Up @@ -104,6 +105,26 @@ impl CfengineRunner {
Self::write_line(output, &json)
}

fn try_parse_request(line: &str) -> Result<Request, Vec<String>> {
let mut errors = Vec::new();

match serde_json::from_str::<ValidateRequest>(line) {
Ok(req) => return Ok(Request::Validate(req)),
Err(e) => errors.push(format!("ValidateRequest: {}", e)),
}

match serde_json::from_str::<EvaluateRequest>(line) {
Ok(req) => return Ok(Request::Evaluate(req)),
Err(e) => errors.push(format!("EvaluateRequest: {}", e)),
}

match serde_json::from_str::<TerminateRequest>(line) {
Ok(req) => return Ok(Request::Terminate(req)),
Err(e) => errors.push(format!("TerminateRequest: {}", e)),
}

Err(errors)
}
fn run_type<T: ModuleType0, R: BufRead, W: Write, L: Write>(
&self,
mut promise: T,
Expand Down Expand Up @@ -145,35 +166,48 @@ impl CfengineRunner {
}

// Handle requests
if let Ok(req) = serde_json::from_str::<ValidateRequest>(&line) {
set_max_level(req.log_level);
// Check parameters
// FIXME add parameters spec check with info from the module type
let result: ValidateOutcome = promise.validate(&req.attributes).into();
Self::write_json(
&mut output,
&mut logger,
ValidateResponse::new(&req, result),
)?
} else if let Ok(req) = serde_json::from_str::<EvaluateRequest>(&line) {
set_max_level(req.log_level);
let result: EvaluateOutcome = promise
.check_apply(req.attributes.action_policy.into(), &req.attributes)
.into();
Self::write_json(
&mut output,
&mut logger,
EvaluateResponse::new(&req, result, vec![]),
)?
} else if let Ok(_req) = serde_json::from_str::<TerminateRequest>(&line) {
let result: ProtocolOutcome = promise.terminate().into();
Self::write_json(&mut output, &mut logger, TerminateResponse::new(result))?;
// Stop the runner
return Ok(());
} else {
// Stop the program? Not sure if there is something better and safe to do.
bail!("Could not parse request: {}", line);
};
match Self::try_parse_request(&line) {
Ok(Request::Validate(req)) => {
set_max_level(req.log_level);
// Check parameters
// FIXME add parameters spec check with info from the module type
let result: ValidateOutcome = promise.validate(&req.attributes).into();
Self::write_json(
&mut output,
&mut logger,
ValidateResponse::new(&req, result),
)?
}
Ok(Request::Evaluate(req)) => {
set_max_level(req.log_level);
let result: EvaluateOutcome = promise
.check_apply(req.attributes.action_policy.into(), &req.attributes)
.into();
Self::write_json(
&mut output,
&mut logger,
EvaluateResponse::new(&req, result, vec![]),
)?
}
Ok(Request::Terminate(_req)) => {
let result: ProtocolOutcome = promise.terminate().into();
Self::write_json(&mut output, &mut logger, TerminateResponse::new(result))?;
// Stop the runner
return Ok(());
}
Err(errors) => {
let error_msg = format!(
"Failed to parse JSON as any known request type:\n{}\nOriginal input: {}",
errors
.iter()
.map(|e| format!(" - {}", e))
.collect::<Vec<_>>()
.join("\n"),
line
);
bail!(error_msg);
}
}
}
}
}
1 change: 1 addition & 0 deletions policies/rudderc/src/backends/unix/prelude.cf
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ bundle common g
{
vars:
# TODO tmpdir?
"agent_run_interval" string => "5";
"rudder_var" string => "/var/rudder";
"rudder_base" string => "/opt/rudder";
"rudder_verify_certs_option" string => "";
Expand Down
Loading