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}