summaryrefslogtreecommitdiffstats
path: root/crates/core/plugin_sm
diff options
context:
space:
mode:
authorLukasz Woznicki <75632179+makr11st@users.noreply.github.com>2021-11-24 20:54:56 +0000
committerGitHub <noreply@github.com>2021-11-24 20:54:56 +0000
commita4ffeccf60090e4456755bc53a6e3b8c8038e855 (patch)
tree9583f187114913a92866571920dd3bb205bd50a3 /crates/core/plugin_sm
parent8217e80670e76dbf9168780f5e0545355a39f8f3 (diff)
Restructure directories of the workspace (#559)
* Restructure directories of the workspace * Rename c8y_translator_lib to c8y_translator * Update comment on how to get dummy plugin path Signed-off-by: Lukasz Woznicki <lukasz.woznicki@softwareag.com>
Diffstat (limited to 'crates/core/plugin_sm')
-rw-r--r--crates/core/plugin_sm/Cargo.toml24
-rw-r--r--crates/core/plugin_sm/src/lib.rs4
-rw-r--r--crates/core/plugin_sm/src/log_file.rs25
-rw-r--r--crates/core/plugin_sm/src/logged_command.rs231
-rw-r--r--crates/core/plugin_sm/src/plugin.rs468
-rw-r--r--crates/core/plugin_sm/src/plugin_manager.rs273
-rw-r--r--crates/core/plugin_sm/tests/fixtures/plugin/plugin_get_command_list.03
-rw-r--r--crates/core/plugin_sm/tests/plugin.rs329
-rw-r--r--crates/core/plugin_sm/tests/plugin_manager.rs203
9 files changed, 1560 insertions, 0 deletions
diff --git a/crates/core/plugin_sm/Cargo.toml b/crates/core/plugin_sm/Cargo.toml
new file mode 100644
index 00000000..1e67dbf3
--- /dev/null
+++ b/crates/core/plugin_sm/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "plugin_sm"
+version = "0.4.3"
+authors = ["thin-edge.io team <info@thin-edge.io>"]
+edition = "2018"
+
+[dependencies]
+async-trait = "0.1"
+csv = "1.1"
+download = { path = "../../common/download" }
+json_sm = { path = "../json_sm" }
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+tedge_utils = { path = "../../common/tedge_utils" }
+thiserror = "1.0"
+tokio = { version = "1.8", features = ["process", "rt"] }
+tracing = { version = "0.1", features = ["attributes", "log"] }
+url = "2.2"
+
+[dev-dependencies]
+anyhow = "1.0"
+assert_matches = "1.5"
+structopt = "0.3"
+tempfile = "3.2"
diff --git a/crates/core/plugin_sm/src/lib.rs b/crates/core/plugin_sm/src/lib.rs
new file mode 100644
index 00000000..79800999
--- /dev/null
+++ b/crates/core/plugin_sm/src/lib.rs
@@ -0,0 +1,4 @@
+pub mod log_file;
+pub mod logged_command;
+pub mod plugin;
+pub mod plugin_manager;
diff --git a/crates/core/plugin_sm/src/log_file.rs b/crates/core/plugin_sm/src/log_file.rs
new file mode 100644
index 00000000..86d50a7c
--- /dev/null
+++ b/crates/core/plugin_sm/src/log_file.rs
@@ -0,0 +1,25 @@
+use std::path::PathBuf;
+use tokio::fs::File;
+use tokio::io::BufWriter;
+
+pub struct LogFile {
+ path: PathBuf,
+ buffer: BufWriter<File>,
+}
+
+impl LogFile {
+ pub async fn try_new(path: PathBuf) -> Result<LogFile, std::io::Error> {
+ let file = File::create(path.clone()).await?;
+ let buffer = BufWriter::new(file);
+
+ Ok(LogFile { path, buffer })
+ }
+
+ pub fn path(&self) -> &str {
+ &self.path.to_str().unwrap_or("/var/log/tedge/agent")
+ }
+
+ pub fn buffer(&mut self) -> &mut BufWriter<File> {
+ &mut self.buffer
+ }
+}
diff --git a/crates/core/plugin_sm/src/logged_command.rs b/crates/core/plugin_sm/src/logged_command.rs
new file mode 100644
index 00000000..11739ce0
--- /dev/null
+++ b/crates/core/plugin_sm/src/logged_command.rs
@@ -0,0 +1,231 @@
+use std::ffi::OsStr;
+use std::process::{Output, Stdio};
+use tokio::fs::File;
+use tokio::io::{AsyncWriteExt, BufWriter};
+use tokio::process::{Child, Command};
+
+pub struct LoggingChild {
+ command_line: String,
+ pub inner_child: Child,
+}
+
+impl LoggingChild {
+ pub async fn wait_with_output(
+ self,
+ logger: &mut BufWriter<File>,
+ ) -> Result<Output, std::io::Error> {
+ let outcome = self.inner_child.wait_with_output().await;
+ if let Err(err) = LoggedCommand::log_outcome(&self.command_line, &outcome, logger).await {
+ tracing::log::error!("Fail to log the command execution: {}", err);
+ }
+
+ outcome
+ }
+}
+
+/// A command which execution is logged.
+///
+/// This struct wraps the main command with a nice representation of that command.
+/// This `command_line` field is only required because the
+/// [`Command::get_program()`](https://doc.rust-lang.org/std/process/struct.Command.html#method.get_program)
+/// and
+/// [`Command::get_args()`](https://doc.rust-lang.org/std/process/struct.Command.html#method.get_args)
+/// are nightly-only experimental APIs.
+pub struct LoggedCommand {
+ command_line: String,
+ command: Command,
+}
+
+impl std::fmt::Display for LoggedCommand {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.command_line.fmt(f)
+ }
+}
+
+impl LoggedCommand {
+ pub fn new(program: impl AsRef<OsStr>) -> LoggedCommand {
+ let command_line = match program.as_ref().to_str() {
+ None => format!("{:?}", program.as_ref()),
+ Some(cmd) => cmd.to_string(),
+ };
+
+ let mut command = Command::new(program);
+ command
+ .current_dir("/tmp")
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped());
+
+ LoggedCommand {
+ command_line,
+ command,
+ }
+ }
+
+ pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut LoggedCommand {
+ // The arguments are displayed as debug, to be properly quoted and distinguished from each other.
+ self.command_line.push_str(&format!(" {:?}", arg.as_ref()));
+ self.command.arg(arg);
+ self
+ }
+
+ /// Execute the command and log its exit status, stdout and stderr
+ ///
+ /// If the command has been executed the outcome is returned (successful or not).
+ /// If the command fails to execute (say not found or not executable) an `std::io::Error` is returned.
+ ///
+ /// If the function fails to log the execution of the command,
+ /// this is logged with `log::error!` without changing the return value.
+ pub async fn execute(mut self, logger: &mut BufWriter<File>) -> Result<Output, std::io::Error> {
+ let outcome = self.command.output().await;
+
+ if let Err(err) = LoggedCommand::log_outcome(&self.command_line, &outcome, logger).await {
+ tracing::log::error!("Fail to log the command execution: {}", err);
+ }
+
+ outcome
+ }
+
+ pub fn spawn(&mut self) -> Result<LoggingChild, std::io::Error> {
+ let child = self.command.spawn()?;
+ Ok(LoggingChild {
+ command_line: self.command_line.clone(),
+ inner_child: child,
+ })
+ }
+
+ async fn log_outcome(
+ command_line: &str,
+ result: &Result<Output, std::io::Error>,
+ logger: &mut BufWriter<File>,
+ ) -> Result<(), std::io::Error> {
+ logger
+ .write_all(format!("----- $ {}\n", command_line).as_bytes())
+ .await?;
+
+ match result.as_ref() {
+ Ok(output) => {
+ match &output.status.code() {
+ None => logger.write_all(b"exit status: unknown\n\n").await?,
+ Some(code) => {
+ logger
+ .write_all(format!("exit status: {}\n\n", code).as_bytes())
+ .await?
+ }
+ };
+ logger.write_all(b"stdout <<EOF\n").await?;
+ logger.write_all(&output.stdout).await?;
+ logger.write_all(b"EOF\n\n").await?;
+ logger.write_all(b"stderr <<EOF\n").await?;
+ logger.write_all(&output.stderr).await?;
+ logger.write_all(b"EOF\n").await?;
+ }
+ Err(err) => {
+ logger
+ .write_all(format!("error: {}\n", &err).as_bytes())
+ .await?;
+ }
+ }
+
+ logger.flush().await?;
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use tempfile::*;
+ use tokio::fs::File;
+
+ #[tokio::test]
+ async fn on_execute_are_logged_command_line_exit_status_stdout_and_stderr(
+ ) -> Result<(), anyhow::Error> {
+ // Prepare a log file
+ let tmp_dir = TempDir::new()?;
+ let log_file_path = tmp_dir.path().join("operation.log");
+ let log_file = File::create(log_file_path.clone()).await?;
+ let mut logger = BufWriter::new(log_file);
+
+ // Prepare a command
+ let mut command = LoggedCommand::new("echo");
+ command.arg("Hello").arg("World!");
+
+ // Execute the command with logging
+ let _ = command.execute(&mut logger).await;
+
+ let log_content = String::from_utf8(std::fs::read(&log_file_path)?)?;
+ assert_eq!(
+ log_content,
+ r#"----- $ echo "Hello" "World!"
+exit status: 0
+
+stdout <<EOF
+Hello World!
+EOF
+
+stderr <<EOF
+EOF
+"#
+ );
+ Ok(())
+ }
+
+ #[tokio::test]
+ async fn on_execute_with_error_stderr_is_logged() -> Result<(), anyhow::Error> {
+ // Prepare a log file
+ let tmp_dir = TempDir::new()?;
+ let log_file_path = tmp_dir.path().join("operation.log");
+ let log_file = File::create(log_file_path.clone()).await?;
+ let mut logger = BufWriter::new(log_file);
+
+ // Prepare a command that triggers some content on stderr
+ let mut command = LoggedCommand::new("ls");
+ command.arg("dummy-file");
+
+ // Execute the command with logging
+ let _ = command.execute(&mut logger).await;
+
+ // On expect the errors to be logged
+ let log_content = String::from_utf8(std::fs::read(&log_file_path)?)?;
+ assert_eq!(
+ log_content,
+ r#"----- $ ls "dummy-file"
+exit status: 2
+
+stdout <<EOF
+EOF
+
+stderr <<EOF
+ls: cannot access 'dummy-file': No such file or directory
+EOF
+"#
+ );
+ Ok(())
+ }
+
+ #[tokio::test]
+ async fn on_execution_error_are_logged_command_line_and_error() -> Result<(), anyhow::Error> {
+ // Prepare a log file
+ let tmp_dir = TempDir::new()?;
+ let log_file_path = tmp_dir.path().join("operation.log");
+ let log_file = File::create(log_file_path.clone()).await?;
+ let mut logger = BufWriter::new(log_file);
+
+ // Prepare a command that cannot be executed
+ let command = LoggedCommand::new("dummy-command");
+
+ // Execute the command with logging
+ let _ = command.execute(&mut logger).await;
+
+ // The fact that the command cannot be executed must be logged
+ let log_content = String::from_utf8(std::fs::read(&log_file_path)?)?;
+ assert_eq!(
+ log_content,
+ r#"----- $ dummy-command
+error: No such file or directory (os error 2)
+"#
+ );
+ Ok(())
+ }
+}
diff --git a/crates/core/plugin_sm/src/plugin.rs b/crates/core/plugin_sm/src/plugin.rs
new file mode 100644
index 00000000..977ed88b
--- /dev/null
+++ b/crates/core/plugin_sm/src/plugin.rs
@@ -0,0 +1,468 @@
+use crate::logged_command::LoggedCommand;
+use async_trait::async_trait;
+use csv::ReaderBuilder;
+use download::Downloader;
+use json_sm::*;
+use std::{path::PathBuf, process::Output};
+use tokio::io::BufWriter;
+use tokio::{fs::File, io::AsyncWriteExt};
+use tracing::error;
+
+#[async_trait]
+pub trait Plugin {
+ async fn prepare(&self, logger: &mut BufWriter<File>) -> Result<(), SoftwareError>;
+
+ async fn install(
+ &self,
+ module: &SoftwareModule,
+ logger: &mut BufWriter<File>,
+ ) -> Result<(), SoftwareError>;
+
+ async fn remove(
+ &self,
+ module: &SoftwareModule,
+ logger: &mut BufWriter<File>,
+ ) -> Result<(), SoftwareError>;
+
+ async fn update_list(
+ &self,
+ modules: &Vec<SoftwareModuleUpdate>,
+ logger: &mut BufWriter<File>,
+ ) -> Result<(), SoftwareError>;
+
+ async fn finalize(&self, logger: &mut BufWriter<File>) -> Result<(), SoftwareError>;
+
+ async fn list(
+ &self,
+ logger: &mut BufWriter<File>,
+ ) -> Result<Vec<SoftwareModule>, SoftwareError>;
+
+ async fn version(
+ &self,
+ module: &SoftwareModule,
+ logger: &mut BufWriter<File>,
+ ) -> Result<Option<String>, SoftwareError>;
+
+ async fn apply(
+ &self,
+ update: &SoftwareModuleUpdate,
+ logger: &mut BufWriter<File>,
+ ) -> Result<(), SoftwareError> {
+ match update.clone() {
+ SoftwareModuleUpdate::Install { mut module } => {
+ let module_url = module.url.clone();
+ match module_url {
+ Some(url) => self.install_from_url(&mut module, &url, logger).await?,
+ None => self.install(&module, logger).await?,
+ }
+
+ Ok(())
+ }
+ SoftwareModuleUpdate::Remove { module } => self.remove(&module, logger).await,
+ }
+ }
+
+ async fn apply_all(
+ &self,
+ mut updates: Vec<SoftwareModuleUpdate>,
+ logger: &mut BufWriter<File>,
+ ) -> Vec<SoftwareError> {
+ let mut failed_updates = Vec::new();
+
+ // Prepare the updates
+ if let Err(prepare_error) = self.prepare(logger).await {
+ failed_updates.push(prepare_error);
+ return failed_updates;
+ }
+
+ // Download all modules for which a download URL is provided
+ let mut downloaders = Vec::new();
+ for update in updates.iter_mut() {
+ let module = match update {
+ SoftwareModuleUpdate::Remove { module } => module,
+ SoftwareModuleUpdate::Install { module } => module,
+ };
+ let module_url = module.url.clone();
+ if let Some(url) = module_url {
+ match Self::download_from_url(module, &url, logger).await {
+ Err(prepare_error) => {
+ failed_updates.push(prepare_error);
+ break;
+ }
+ Ok(downloader) => downloaders.push(downloader),
+ }
+ }
+ }
+
+ // Execute the updates
+ if failed_updates.is_empty() {
+ let outcome = self.update_list(&updates, logger).await;
+ if let Err(SoftwareError::UpdateListNotSupported(_)) = outcome {
+ for update in updates.iter() {
+ if let Err(error) = self.apply(update, logger).await {
+ failed_updates.push(error);
+ };
+ }
+ } else if let Err(update_list_error) = outcome {
+ failed_updates.push(update_list_error);
+ }
+ }
+
+ // Finalize the updates
+ if let Err(finalize_error) = self.finalize(logger).await {
+ failed_updates.push(finalize_error);
+ }
+
+ // Cleanup all the downloaded modules
+ for downloader in downloaders {
+ if let Err(cleanup_error) = Self::cleanup_downloaded_artefacts(downloader, logger).await
+ {
+ failed_updates.push(cleanup_error);
+ }
+ }
+
+ failed_updates
+ }
+
+ async fn install_from_url(
+ &self,
+ module: &mut SoftwareModule,
+ url: &DownloadInfo,
+ logger: &mut BufWriter<File>,
+ ) -> Result<(), SoftwareError> {
+ let downloader = Self::download_from_url(module, url, logger).await?;
+ let result = self.install(module, logger).await;
+ Self::cleanup_downloaded_artefacts(downloader, logger).await?;
+
+ result
+ }
+
+ async fn download_from_url(
+ module: &mut SoftwareModule,
+ url: &DownloadInfo,
+ logger: &mut BufWriter<File>,
+ ) -> Result<Downloader, SoftwareError> {
+ let downloader = Downloader::new(&module.name, &module.version, "/tmp");
+
+ logger
+ .write_all(
+ format!(
+ "----- $ Downloading: {} to {} \n",
+ &url.url(),
+ &downloader.filename().to_string_lossy().to_string()
+ )
+ .as_bytes(),
+ )
+ .await?;
+
+ if let Err(err) =
+ downloader
+ .download(url)
+ .await
+ .map_err(|err| SoftwareError::DownloadError {
+ reason: err.to_string(),
+ url: url.url().to_string(),
+ })
+ {
+ error!("Download error: {}", &err);
+ logger
+ .write_all(format!("error: {}\n", &err).as_bytes())
+ .await?;
+ return Err(err);
+ }
+
+ module.file_path = Some(downloader.filename().to_owned());
+
+ Ok(downloader)
+ }
+
+ async fn cleanup_downloaded_artefacts(
+ downloader: Downloader,
+ logger: &mut BufWriter<File>,
+ ) -> Result<(), SoftwareError> {
+ if let Err(err) = downloader
+ .cleanup()
+ .await
+ .map_err(|err| SoftwareError::IoError {
+ reason: err.to_string(),
+ })
+ {
+ logger
+ .write_all(format!("warn: {}\n", &err).as_bytes())
+ .await?;
+ }
+ Ok(())
+ }
+}
+
+#[derive(Debug)]
+pub struct ExternalPluginCommand {
+ pub name: SoftwareType,
+ pub path: PathBuf,
+ pub sudo: Option<PathBuf>,
+}
+
+impl ExternalPluginCommand {
+ pub fn new(name: impl Into<SoftwareType>, path: impl Into<PathBuf>) -> ExternalPluginCommand {
+ ExternalPluginCommand {
+ name: name.into(),
+ path: path.into(),
+ sudo: Some("sudo".into()),
+ }
+ }
+
+ pub fn command(
+ &self,
+ action: &str,
+ maybe_module: Option<&SoftwareModule>,
+ ) -> Result<LoggedCommand, SoftwareError> {
+ let mut command = if let Some(sudo) = &self.sudo {
+ let mut command = LoggedCommand::new(sudo);
+ command.arg(&self.path);
+ command
+ } else {
+ LoggedCommand::new(&self.path)
+ };
+ command.arg(action);
+
+ if let Some(module) = maybe_module {
+ self.check_module_type(module)?;
+ command.arg(&module.name);
+ if let Some(ref version) = module.version {
+ command.arg("--module-version");
+ command.arg(version);
+ }
+
+ if let Some(ref path) = module.file_path {
+ command.arg("--file");
+ command.arg(path);
+ }
+ }
+
+ Ok(command)
+ }
+
+ pub async fn execute(
+ &self,
+ command: LoggedCommand,
+ logger: &mut BufWriter<File>,
+ ) -> Result<Output, SoftwareError> {
+ let output = command
+ .execute(logger)
+ .await
+ .map_err(|err| self.plugin_error(err))?;
+ Ok(output)
+ }
+
+ pub fn content(&self, bytes: Vec<u8>) -> Result<String, SoftwareError> {
+ String::from_utf8(bytes).map_err(|err| self.plugin_error(err))
+ }
+
+ pub fn plugin_error(&self, err: impl std::fmt::Display) -> SoftwareError {
+ SoftwareError::Plugin {
+ software_type: self.name.clone(),
+ reason: format!("{}", err),
+ }
+ }
+
+ /// This test validates if an incoming module can be handled by it, by matching the module type with the plugin type
+ pub fn check_module_type(&self, module: &SoftwareModule) -> Result<(), SoftwareError> {
+ match &module.module_type {
+ Some(name) if name == &self.name.clone() => Ok(()),
+ Some(name) if name == DEFAULT => Ok(()),
+ Some(name) => Err(SoftwareError::WrongModuleType {
+ actual: self.name.clone(),
+ expected: name.clone(),
+ }),
+ None => Ok(()), // A software module without a type can be handled by any plugin that's configured as default plugin
+ }
+ }
+}
+
+const PREPARE: &str = "prepare";
+const INSTALL: &str = "install";
+const REMOVE: &str = "remove";
+const UPDATE_LIST: &str = "update-list";
+const FINALIZE: &str = "finalize";
+pub const LIST: &str = "list";
+const VERSION: &str = "version";
+
+#[async_trait]
+impl Plugin for ExternalPluginCommand {
+ async fn prepare(&self, logger: &mut BufWriter<File>) -> Result<(), SoftwareError> {
+ let command = self.command(PREPARE, None)?;
+ let output = self.execute(command, logger).await?;
+
+ if output.status.success() {
+ Ok(())
+ } else {
+ Err(SoftwareError::Prepare {
+ software_type: self.name.clone(),
+ reason: self.content(output.stderr)?,
+ })
+ }
+ }
+
+ async fn install(
+ &self,
+ module: &SoftwareModule,
+ logger: &mut BufWriter<File>,
+ ) -> Result<(), SoftwareError> {
+ let command = self.command(INSTALL, Some(module))?;
+ let output = self.execute(command, logger).await?;
+
+ if output.status.success() {
+ Ok(())
+ } else {
+ Err(SoftwareError::Install {
+ module: module.clone(),
+ reason: self.content(output.stderr)?,
+ })
+ }
+ }
+
+ async fn remove(
+ &self,
+ module: &SoftwareModule,
+ logger: &mut BufWriter<File>,
+ ) -> Result<(), SoftwareError> {
+ let command = self.command(REMOVE, Some(module))?;
+ let output = self.execute(command, logger).await?;
+
+ if output.status.success() {
+ Ok(())
+ } else {
+ Err(SoftwareError::Remove {
+ module: module.clone(),
+ reason: self.content(output.stderr)?,
+ })
+ }
+ }
+
+ async fn update_list(
+ &self,
+ updates: &Vec<SoftwareModuleUpdate>,
+ logger: &mut BufWriter<File>,
+ ) -> Result<(), SoftwareError> {
+ let mut command = self.command(UPDATE_LIST, None)?;
+
+ let mut child = command.spawn()?;
+ let child_stdin =
+ child
+ .inner_child
+ .stdin
+ .as_mut()
+ .ok_or_else(|| SoftwareError::IoError {
+ reason: "Plugin stdin unavailable".into(),
+ })?;
+
+ for update in updates {
+ let action = match update {
+ SoftwareModuleUpdate::Install { module } => {
+ format!(
+ "install\t{}\t{}\t{}\n",
+ module.name,
+ module.version.clone().map_or("".into(), |v| v),
+ module.file_path.clone().map_or("".into(), |v| v
+ .to_str()
+ .map_or("".into(), |u| u.to_string()))
+ )
+ }
+
+ SoftwareModuleUpdate::Remove { module } => {
+ format!(
+ "remove\t{}\t{}\t\n",
+ module.name,
+ module.version.clone().map_or("".into(), |v| v),
+ )
+ }
+ };
+
+ child_stdin.write_all(action.as_bytes()).await?
+ }
+
+ let output = child.wait_with_output(logger).await?;
+ match output.status.code() {
+ Some(0) => Ok(()),
+ Some(1) => Err(SoftwareError::UpdateListNotSupported(self.name.clone())),
+ Some(_) => Err(SoftwareError::UpdateList {
+ software_type: self.name.clone(),
+ reason: self.content(output.stderr)?,
+ }),
+ None => Err(SoftwareError::UpdateList {
+ software_type: self.name.clone(),
+ reason: "Interrupted".into(),
+ }),
+ }
+ }
+
+ async fn finalize(&self, logger: &mut BufWriter<File>) -> Result<(), SoftwareError> {
+ let command = self.command(FINALIZE, None)?;
+ let output = self.execute(command, logger).await?;
+
+ if output.status.success() {
+ Ok(())
+ } else {
+ Err(SoftwareError::Finalize {
+ software_type: self.name.clone(),
+ reason: self.content(output.stderr)?,
+ })
+ }
+ }
+
+ async fn list(
+ &self,
+ logger: &mut BufWriter<File>,
+ ) -> Result<Vec<SoftwareModule>, SoftwareError> {
+ let command = self.command(LIST, None)?;
+ let output = self.execute(command, logger).await?;
+ if output.status.success() {
+ let mut software_list = Vec::new();
+ let mut rdr = ReaderBuilder::new()
+ .has_headers(false)
+ .delimiter(b'\t')
+ .from_reader(output.stdout.as_slice());
+
+ for module in rdr.deserialize() {
+ let (name, version): (String, Option<String>) = module?;
+ software_list.push(SoftwareModule {
+ name,
+ version,
+ module_type: Some(self.name.clone()),
+ file_path: None,
+ url: None,
+ });
+ }
+
+ Ok(software_list)
+ } else {
+ Err(SoftwareError::Plugin {
+ software_type: self.name.clone(),
+ reason: self.content(output.stderr)?,
+ })
+ }
+ }
+
+ async fn version(
+ &self,
+ module: &SoftwareModule,
+ logger: &mut BufWriter<File>,
+ ) -> Result<Option<String>, SoftwareError> {
+ let command = self.command(VERSION, Some(module))?;
+ let output = self.execute(command, logger).await?;
+
+ if output.status.success() {
+ let version = String::from(self.content(output.stdout)?.trim());
+ if version.is_empty() {
+ Ok(None)
+ } else {
+ Ok(Some(version))
+ }
+ } else {
+ Err(SoftwareError::Plugin {
+ software_type: self.name.clone(),
+ reason: self.content(output.stderr)?,
+ })
+ }
+ }
+}
diff --git a/crates/core/plugin_sm/src/plugin_manager.rs b/crates/core/plugin_sm/src/plugin_manager.rs
new file mode 100644
index 00000000..44e7b1bc
--- /dev/null
+++ b/crates/core/plugin_sm/src/plugin_manager.rs
@@ -0,0 +1,273 @@
+use crate::plugin::{Plugin, LIST};
+use crate::{log_file::LogFile, plugin::ExternalPluginCommand};
+use json_sm::{
+ SoftwareError, SoftwareListRequest, SoftwareListResponse, SoftwareType, SoftwareUpdateRequest,
+ SoftwareUpdateResponse, DEFAULT,
+};
+use std::{
+ collections::HashMap,
+ fs,
+ io::{self, ErrorKind},
+ path::PathBuf,
+ process::{Command, Stdio},
+};
+use tedge_utils::paths::pathbuf_to_string;
+use tracing::{error, info, warn};
+
+/// The main responsibility of a `Plugins` implementation is to retrieve the appropriate plugin for a given software module.
+pub trait Plugins {
+ type Plugin;
+
+ /// Return the plugin to be used by default when installing a software module, if any.
+ fn default(&self) -> Option<&Self::Plugin>;
+
+ /// Return the plugin declared with the given name, if any.
+ fn by_software_type(&self, software_type: &str) -> Option<&Self::Plugin>;
+
+ /// Return the plugin associated with the file extension of the module name, if any.
+ fn by_file_extension(&self, module_name: &str) -> Option<&Self::Plugin>;
+
+ fn plugin(&self, software_type: &str) -> Result<&Self::Plugin, SoftwareError> {
+ let module_plugin = self.by_software_type(software_type).ok_or_else(|| {
+ SoftwareError::UnknownSoftwareType {
+ software_type: software_type.into(),
+ }
+ })?;
+
+ Ok(module_plugin)
+ }
+
+ fn update_default(&mut self, new_default: &Option<SoftwareType>) -> Result<(), SoftwareError>;
+}
+
+#[derive(Debug)]
+pub struct ExternalPlugins {
+ plugin_dir: PathBuf,
+ plugin_map: HashMap<SoftwareType, ExternalPluginCommand>,
+ default_plugin_type: Option<SoftwareType>,
+ sudo: Option<PathBuf>,
+}
+
+impl Plugins for ExternalPlugins {
+ type Plugin = ExternalPluginCommand;
+
+ fn default(&self) -> Option<&Self::Plugin> {
+ if let Some(default_plugin_type) = &self.default_plugin_type {
+ self.by_software_type(default_plugin_type.as_str())
+ } else if self.plugin_map.len() == 1 {
+ Some(self.plugin_map.iter().next().unwrap().1) //Unwrap is safe here as one entry is guaranteed
+ } else {
+ None
+ }
+ }
+
+ fn update_default(&mut self, new_default: &Option<SoftwareType>) -> Result<(), SoftwareError> {
+ self.default_plugin_type = new_default.to_owned();
+ Ok(())
+ }
+
+ fn by_software_type(&self, software_type: &str) -> Option<&Self::Plugin> {
+ if software_type.eq(DEFAULT) {
+ self.default()
+ } else {
+ self.plugin_map.get(software_type)