audb_core/tools/
session.rs

1/// DeviceSession abstraction for managing SSH connections to Aurora devices
2///
3/// This module provides a high-level interface for connecting to and executing
4/// commands on Aurora OS devices, eliminating code duplication across features.
5
6use crate::tools::{
7    errors::DeviceError,
8    ssh::SshClient,
9    types::Device,
10};
11use anyhow::{Context, Result};
12use russh::client::Handle;
13use std::path::Path;
14
15/// Manages an active SSH session to an Aurora device
16///
17/// This struct encapsulates the device connection and provides convenient
18/// methods for executing commands, both as regular user and as root via devel-su.
19///
20/// # Example
21/// ```no_run
22/// use audb::tools::session::DeviceSession;
23/// use audb::tools::types::Device;
24///
25/// # async fn example(device: Device) -> anyhow::Result<()> {
26/// let mut session = DeviceSession::connect(&device).await?;
27/// let output = session.exec("uname -a").await?;
28/// println!("System info: {:?}", output);
29/// # Ok(())
30/// # }
31/// ```
32pub struct DeviceSession {
33    device: Device,
34    session: Handle<SshClient>,
35}
36
37impl DeviceSession {
38    /// Connect to a device and return an active session
39    ///
40    /// # Arguments
41    /// * `device` - The device configuration to connect to
42    ///
43    /// # Returns
44    /// A new `DeviceSession` or an error if connection fails
45    ///
46    /// # Errors
47    /// Returns `DeviceError::ConnectionFailed` if the SSH connection cannot be established
48    pub fn connect(device: &Device) -> Result<Self, DeviceError> {
49        let session = SshClient::connect(&device.host, device.port, &device.auth_path())
50            .map_err(|e| DeviceError::ConnectionFailed(format!("{}", e)))?;
51
52        Ok(Self {
53            device: device.clone(),
54            session,
55        })
56    }
57
58    /// Execute command as regular user
59    ///
60    /// # Arguments
61    /// * `command` - The shell command to execute
62    ///
63    /// # Returns
64    /// A vector of output lines (stdout) from the command
65    ///
66    /// # Errors
67    /// Returns an error if command execution fails or returns non-zero exit code
68    pub fn exec(&mut self, command: &str) -> Result<Vec<String>> {
69        SshClient::exec(&mut self.session, command)
70            .with_context(|| format!("Failed to execute: {}", command))
71    }
72
73    /// Execute command as root via devel-su
74    ///
75    /// This method uses the device's configured root password to execute commands
76    /// with root privileges using Aurora OS's devel-su mechanism.
77    ///
78    /// # Arguments
79    /// * `command` - The shell command to execute as root
80    ///
81    /// # Returns
82    /// A vector of output lines (stdout) from the command
83    ///
84    /// # Errors
85    /// * Returns `DeviceError::RootPasswordNotConfigured` if no root password is set
86    /// * Returns an error if command execution fails
87    ///
88    /// # Security
89    /// The password and command are properly escaped to prevent shell injection.
90    pub fn exec_as_root(&mut self, command: &str) -> Result<Vec<String>, DeviceError> {
91        if self.device.root_password.is_empty() {
92            return Err(DeviceError::RootPasswordNotConfigured(
93                self.device.display_name(),
94            ));
95        }
96
97        SshClient::exec_as_devel_su(&mut self.session, command, &self.device.root_password)
98            .map_err(DeviceError::SshError)
99            .with_context(|| format!("Failed to execute as root: {}", command))
100            .map_err(DeviceError::SshError)
101    }
102
103    /// Upload file to device via SFTP
104    ///
105    /// # Arguments
106    /// * `local_path` - Path to the local file to upload
107    /// * `remote_path` - Destination path on the remote device
108    ///
109    /// # Errors
110    /// Returns an error if file upload fails
111    pub fn upload_file(&mut self, local_path: &Path, remote_path: &Path) -> Result<()> {
112        SshClient::upload(&mut self.session, local_path, remote_path)
113            .with_context(|| {
114                format!(
115                    "Failed to upload {} to {}",
116                    local_path.display(),
117                    remote_path.display()
118                )
119            })
120    }
121
122    /// Read remote file contents as base64 string
123    ///
124    /// This method is useful for reading files that may be owned by root,
125    /// as it uses the root password if configured.
126    ///
127    /// # Arguments
128    /// * `remote_path` - Path to the file on the remote device
129    ///
130    /// # Returns
131    /// Base64-encoded contents of the file
132    ///
133    /// # Errors
134    /// * Returns `DeviceError::RootPasswordNotConfigured` if no root password is set
135    /// * Returns an error if file reading fails
136    pub fn read_file_base64(&mut self, remote_path: &Path) -> Result<String, DeviceError> {
137        if self.device.root_password.is_empty() {
138            return Err(DeviceError::RootPasswordNotConfigured(
139                self.device.display_name(),
140            ));
141        }
142
143        SshClient::read_file_base64(&mut self.session, remote_path, &self.device.root_password)
144            .map_err(DeviceError::SshError)
145    }
146
147    /// Get a reference to the underlying device configuration
148    pub fn device(&self) -> &Device {
149        &self.device
150    }
151
152    /// Get the device's display name
153    pub fn device_name(&self) -> String {
154        self.device.display_name()
155    }
156}