How to develop a Linux plugin with an external script
In this tutorial it will be shown how to develop a basic plugin that collects the list of all the crontab
jobs on a Linux machine. Since it has not been found a one-fits-all one-liner to collect the data for all users, an external script will be employed.
1. External script
The following script will be saved in a file called crontab.sh
placed in the path launcher/ansible/roles/linus/files
:
#!/bin/bash
#
# Source: https://stackoverflow.com/questions/134906/how-do-i-list-all-cron-jobs-for-all-users
#
# System-wide crontab file and cron job directory. Change these for your system.
CRONTAB='/etc/crontab'
CRONDIR='/etc/cron.d'
# Single tab character. Annoyingly necessary.
tab=$(echo -en "\t")
# Given a stream of crontab lines, exclude non-cron job lines, replace
# whitespace characters with a single space, and remove any spaces from the
# beginning of each line.
function clean_cron_lines() {
while read line ; do
echo "${line}" |
egrep --invert-match '^($|\s*#|\s*[[:alnum:]_]+=)' |
sed --regexp-extended "s/\s+/ /g" |
sed --regexp-extended "s/^ //"
done;
}
# Given a stream of cleaned crontab lines, echo any that don't include the
# run-parts command, and for those that do, show each job file in the run-parts
# directory as if it were scheduled explicitly.
function lookup_run_parts() {
while read line ; do
match=$(echo "${line}" | egrep -o 'run-parts (-{1,2}\S+ )*\S+')
if [[ -z "${match}" ]] ; then
echo "${line}"
else
cron_fields=$(echo "${line}" | cut -f1-6 -d' ')
cron_job_dir=$(echo "${match}" | awk '{print $NF}')
if [[ -d "${cron_job_dir}" ]] ; then
for cron_job_file in "${cron_job_dir}"/* ; do # */ <not a comment>
[[ -f "${cron_job_file}" ]] && echo "${cron_fields} ${cron_job_file}"
done
fi
fi
done;
}
# Temporary file for crontab lines.
temp=$(mktemp) || exit 1
# Add all of the jobs from the system-wide crontab file.
cat "${CRONTAB}" | clean_cron_lines | lookup_run_parts >"${temp}"
# Add all of the jobs from the system-wide cron directory.
cat "${CRONDIR}"/* | clean_cron_lines >>"${temp}" # */ <not a comment>
# Add each user's crontab (if it exists). Insert the user's name between the
# five time fields and the command.
while read user ; do
crontab -l -u "${user}" 2>/dev/null |
clean_cron_lines |
sed --regexp-extended "s/^((\S+ +){5})(.+)$/\1${user} \3/" >>"${temp}"
done < <(cut --fields=1 --delimiter=: /etc/passwd)
# Output the collected crontab lines. Replace the single spaces between the
# fields with tab characters, sort the lines by hour and minute, insert the
# header line, and format the results as a table.
cat "${temp}" |
sed --regexp-extended "s/^(\S+) +(\S+) +(\S+) +(\S+) +(\S+) +(\S+) +(.*)$/\1\t\2\t\3\t\4\t\5\t\6\t\7/" |
sort --numeric-sort --field-separator="${tab}" --key=2,1 |
column -s"${tab}" -t
rm --force "${temp}"
2. Scaffolding
The plugin name will be linux_crontab
so the command to be executed to create the empty plugin structure is the following:
user@master-node:~/rusthunter$ cp -r app/src/plugins/sample app/src/plugins/linux/crontab
The plugin folder contains the mod.rs
file which provides the plugin logic:
user@master-node:~/rusthunter$ ls -al app/src/plugins/linux/crontab
mod.rs
3. Customization
The file app/src/plugins/linux/crontab/mod.rs
must be properly customized to collect the desired data on the target machine.
- Replace all the occurrencies of the struct name
SamplePlugin
withLinuxCrontab
. It can be easily done via the command:
user@master-node:~/rusthunter$ sed -i 's/SamplePlugin/LinuxCrontab/g' app/src/plugins/linux/crontab/mod.rs
-
Replace the plugin name
sample_plugin
withlinux_crontab
(Line 10). -
Replace the plugin description
Sample description
withCrontab jobs
(Line 14). -
Replace the plugin operating system
OS::Unknown
withOS::Linux
(Line 18). -
Replace the plugin line
let command = "sample command";
withlet command = format!("{}/{}", _binary_directory, "crontab.sh");
(Line 22). -
Replace
Ok(())
(Line 30) with the correct output processing function. Since the output of the command will be a simple list the needed function isself._split_list(output)
.
Finally, the code should be this:
use serde_json::Value;
use crate::config::Config;
use crate::plugins::{Plugin, OS};
pub struct LinuxCrontab {}
impl Plugin for LinuxCrontab {
fn name(&self) -> &str {
&"linux_crontab"
}
fn description(&self) -> &str {
&"Crontab jobs"
}
fn os(&self) -> OS {
OS::Linux
}
fn run(&self, _config: &Config, _binary_directory: &str) -> Result<Value, String> {
let command = format!("{}/{}", _binary_directory, "crontab.sh");
match self.execute_command(&command) {
Ok(output) => self.process(&output),
Err(e) => Err(e),
}
}
fn process(&self, output: &str) -> Result<Value, String> {
self._split_list(output)
}
}
impl LinuxCrontab {
pub fn new() -> Self {
LinuxCrontab {}
}
}
3. Unlocking
The plugin logic has been defined but the plugin must be linked to the main application logic, so:
-
Add the line
pub mod crontab;
to theapp/src/plugins/linux/mod.rs
file. -
Update the
execute
method in theapp/src/lib.rs
file:- Below the comment
// Import Linux plugins
add the lineroot::LinuxCrontab,
- Below the comment
// Instantiate Linux plugins
add the linelet linux_crontab = LinuxCrontab::new();
- Below the comment
// Execute Linux plugins
add the line&linux_crontab,
- Below the comment
Finally, the code should be like the following:
} else if #[cfg(target_os = "linux")] {
use crate::plugins::linux::{
// Import Linux plugins
users::LinuxUsers,
// ...
root::LinuxCrontab, // <- New module
// ...
tcp_listen::LinuxTCPListen,
};
// Instantiate Linux plugins
let linux_users = LinuxUsers::new();
// ...
let linux_crontab = LinuxCrontab::new(); // <- New module
// ...
let linux_tcp_listen = LinuxTCPListen::new();
let plugins: Vec<&dyn Plugin> = vec![
// Execute Linux plugins
&linux_users,
// ...
&linux_crontab, // <- New module
&linux_tcp_listen,
// ...
];
} else if #[cfg(target_os = "macos")] {
4. Configuration
In order to enable/disable the plugin execution, it is necessary to add to the config.ini
INI file the following block:
[linux_crontab]
enabled = true
5. Re-building
Execute the following command to rebuild the framework (requires Docker) to include the new plugin:
user@master-node:~/rusthunter$ sudo ./rusthunter.sh build
The Docker image peco602/rust-universal-compiler
will be downloaded (it can take some time) and used to rebuild the Rust code for all the platforms.