378 lines
14 KiB
C#
378 lines
14 KiB
C#
using Renci.SshNet;
|
|
|
|
namespace AiQ_GUI
|
|
{
|
|
internal class SSH
|
|
{
|
|
public const string SSHUsername = "mav";
|
|
public const string SSHPassword = "mavPA$$";
|
|
|
|
// Connects to camera over SSH and collects the Vaxtor packages, filesystem name, filesystem size, and tailscale status.
|
|
public static SSHData CollectSSHData(string IPAddress)
|
|
{
|
|
SSHData Data = new();
|
|
|
|
try
|
|
{
|
|
using SshClient client = new(IPAddress, SSHUsername, SSHPassword);
|
|
client.Connect();
|
|
|
|
try
|
|
{
|
|
Data.packages = GetVaxtorPackages(client);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MainForm.Instance.AddToActionsList($"Failed to get Vaxtor packages: {ex.Message}");
|
|
Data.packages = "Error";
|
|
}
|
|
|
|
try
|
|
{
|
|
(Data.FilesystemName, Data.FilesystemSize) = GetRootFilesystemInfo(client);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MainForm.Instance.AddToActionsList($"Failed to get filesystem info: {ex.Message}");
|
|
Data.FilesystemName = "Unknown";
|
|
Data.FilesystemSize = "Unknown";
|
|
}
|
|
|
|
try
|
|
{
|
|
Data.tailscale = IsTailscaleInstalled(client);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MainForm.Instance.AddToActionsList($"Failed to check Tailscale: {ex.Message}");
|
|
Data.tailscale = false;
|
|
}
|
|
|
|
client.Disconnect();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MainForm.Instance.AddToActionsList($"SSH connection failed: {ex.Message}. Check password or network.");
|
|
}
|
|
|
|
string LogMssg = string.Join(" | ", typeof(SSHData).GetProperties().Select(p => $"{p.Name}: {p.GetValue(Data)}"));
|
|
Logging.LogMessage(LogMssg); // Log all of Data
|
|
|
|
return Data;
|
|
}
|
|
|
|
// Gets a list of packages with Vaxtor in the name
|
|
public static string GetVaxtorPackages(SshClient client)
|
|
{
|
|
try
|
|
{
|
|
SshCommand cmd = client.RunCommand("dpkg -l | grep vaxtor");
|
|
if (!string.IsNullOrWhiteSpace(cmd.Error))
|
|
throw new Exception(cmd.Error);
|
|
|
|
string result = cmd.Result;
|
|
if (string.IsNullOrWhiteSpace(result))
|
|
return "No Vaxtor packages found.";
|
|
|
|
string[] lines = result.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
|
List<string> packages = [];
|
|
|
|
foreach (string line in lines)
|
|
{
|
|
string[] parts = line.Split([' '], StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
if (parts.Length >= 3)
|
|
packages.Add($"{parts[1]} - {parts[2]}"); // Package name - Version
|
|
}
|
|
|
|
return packages.Count > 0 ? string.Join("\n", packages) : "No Vaxtor packages found.";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MainForm.Instance.AddToActionsList($"Error getting Vaxtor packages: {ex.Message}");
|
|
return "Error";
|
|
}
|
|
}
|
|
|
|
// Returns true if Tailscale is installed on the camera
|
|
public static bool IsTailscaleInstalled(SshClient client)
|
|
{
|
|
try
|
|
{
|
|
SshCommand cmd = client.RunCommand("dpkg -l | grep '^ii' | grep tailscale");
|
|
if (!string.IsNullOrWhiteSpace(cmd.Error))
|
|
throw new Exception(cmd.Error);
|
|
|
|
string command = cmd.Result;
|
|
if (string.IsNullOrWhiteSpace(command))
|
|
return false;
|
|
|
|
string[] lines = command.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
foreach (string line in lines)
|
|
{
|
|
string[] parts = line.Split([' '], StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
if (parts.Length >= 3 && parts[1].Equals("tailscale", StringComparison.OrdinalIgnoreCase))
|
|
return true; // Return true if any line contains "tailscale" as the package name
|
|
}
|
|
|
|
return false;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MainForm.Instance.AddToActionsList($"Error checking Tailscale: {ex.Message}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Connects to camera over SSH and collects the Vaxtor packages, filesystem name, filesystem size, and tailscale status.
|
|
public static (string filesystem, string size) CollectFSData(string IPAddress)
|
|
{
|
|
try
|
|
{
|
|
using SshClient client = new SshClient(IPAddress, SSHUsername, SSHPassword);
|
|
client.Connect();
|
|
|
|
try
|
|
{
|
|
return GetRootFilesystemInfo(client);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MainForm.Instance.AddToActionsList($"Failed to get filesystem info: {ex.Message}");
|
|
}
|
|
|
|
client.Disconnect();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MainForm.Instance.AddToActionsList($"SSH connection failed: {ex.Message}. Check password or network.");
|
|
}
|
|
|
|
return (string.Empty, string.Empty);
|
|
}
|
|
|
|
// Gets the filesystem size and partition name
|
|
public static (string filesystem, string size) GetRootFilesystemInfo(SshClient client)
|
|
{
|
|
try
|
|
{
|
|
SshCommand cmd = client.RunCommand("df -h");
|
|
if (!string.IsNullOrWhiteSpace(cmd.Error))
|
|
throw new Exception(cmd.Error);
|
|
|
|
string result = cmd.Result;
|
|
if (string.IsNullOrWhiteSpace(result))
|
|
return ("Unknown", "Unknown");
|
|
|
|
IEnumerable<string> lines = result.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries).Skip(1);
|
|
|
|
foreach (string line in lines)
|
|
{
|
|
string[] parts = line.Split([' '], StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
// Long enough & Filesystem name isn't tmpfs varient or none & Mountpoint is root
|
|
if (parts.Length >= 6 && !parts[0].Contains("tmpfs") && parts[0] != "none" && parts[5] == "/")
|
|
return (parts[0], parts[1]); // Return name & size of partition
|
|
}
|
|
|
|
return ("Unknown", "Unknown");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MainForm.Instance.AddToActionsList($"Error getting filesystem info: {ex.Message}");
|
|
return ("Unknown", "Unknown");
|
|
}
|
|
}
|
|
|
|
// Sorts the SSH client for the filesystem call
|
|
public static (string filesystem, string size) GetRootFilesystemInfo(string IPAddress)
|
|
{
|
|
try
|
|
{
|
|
using SshClient client = new(IPAddress, SSHUsername, SSHPassword);
|
|
client.Connect();
|
|
(string FilesystemName, string FilesystemSize) = GetRootFilesystemInfo(client);
|
|
client.Disconnect();
|
|
return (FilesystemName, FilesystemSize);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MainForm.Instance.AddToActionsList($"Error getting root filesystem info: {ex.Message}");
|
|
return ("Unknown", "Unknown");
|
|
}
|
|
}
|
|
|
|
// Checks the filesystem size and expands it if necessary, displays on the label how big the SD card is.
|
|
public static async Task<SSHData> CheckFSSize(string IPAddress, Label LblFSSize, SSHData sshData)
|
|
{
|
|
const double MinGoodSize = 100.0; // 100GB
|
|
const double MaxGoodSize = 150.0; // 150GB
|
|
|
|
double currentSize = NormaliseFSSize(sshData.FilesystemSize);
|
|
LblFSSize.Text = $"Filesystem Size = {currentSize}GB";
|
|
|
|
if (currentSize >= MinGoodSize && currentSize <= MaxGoodSize)
|
|
{
|
|
LblFSSize.ForeColor = Color.LightGreen;
|
|
return sshData;
|
|
}
|
|
|
|
if (currentSize < MinGoodSize)
|
|
{
|
|
try
|
|
{
|
|
if (await ExpandFS(sshData.FilesystemName, IPAddress))
|
|
{
|
|
(sshData.FilesystemName, sshData.FilesystemSize) = GetRootFilesystemInfo(IPAddress);
|
|
|
|
double newSize = NormaliseFSSize(sshData.FilesystemSize);
|
|
LblFSSize.Text = $"Filesystem Size = {newSize}GB";
|
|
|
|
if (newSize >= MinGoodSize && newSize <= MaxGoodSize)
|
|
{
|
|
LblFSSize.ForeColor = Color.LightGreen;
|
|
return sshData;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MainForm.Instance.AddToActionsList($"Error expanding filesystem: {ex.Message}");
|
|
}
|
|
|
|
LblFSSize.ForeColor = Color.Red;
|
|
MainForm.Instance.AddToActionsList("Size is too small, failed to expand FS, please try manually.");
|
|
return sshData;
|
|
}
|
|
|
|
LblFSSize.ForeColor = Color.Red;
|
|
LblFSSize.Text += " Size is too big.";
|
|
return sshData;
|
|
}
|
|
|
|
// Makes sure the units given are accounted for when calcualting the size of the SD card.
|
|
public static double NormaliseFSSize(string rootSize)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrWhiteSpace(rootSize)) return 0;
|
|
|
|
// Extract value & unit
|
|
System.Text.RegularExpressions.Match match = RegexCache.FileSizeRegex().Match(rootSize.Trim());
|
|
if (!match.Success)
|
|
return 0;
|
|
|
|
if (!double.TryParse(match.Groups["value"].Value, out double value))
|
|
return 0;
|
|
|
|
string unit = match.Groups["unit"].Value.ToUpperInvariant();
|
|
|
|
switch (unit) // Normalize to gigabytes
|
|
{
|
|
case "T": value *= 1024; break;
|
|
case "G": break;
|
|
case "M": value /= 1024; break;
|
|
case "K": value /= 1024 * 1024; break;
|
|
case "": value /= 1024 * 1024 * 1024; break; // assume bytes
|
|
default: value = 0; break;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MainForm.Instance.AddToActionsList($"Error normalizing FS size: {ex.Message}");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Expands the filesystem to max
|
|
public async static Task<bool> ExpandFS(string device, string IPAddress)
|
|
{
|
|
try
|
|
{
|
|
using SshClient ssh = new SshClient(IPAddress, SSHUsername, SSHPassword);
|
|
ssh.Connect();
|
|
|
|
SshCommand checkDevice = ssh.RunCommand($"[ -b {device} ] && echo OK || echo NOT_FOUND");
|
|
if (!string.IsNullOrWhiteSpace(checkDevice.Error))
|
|
throw new Exception(checkDevice.Error);
|
|
|
|
if (checkDevice.Result.Trim() != "OK") // Device not found
|
|
{
|
|
MainForm.Instance.AddToActionsList($"Block device {device} not found.");
|
|
return false;
|
|
}
|
|
|
|
SshCommand umountCmd = ssh.RunCommand($"sudo umount {device}");
|
|
if (!string.IsNullOrWhiteSpace(umountCmd.Error) && !umountCmd.Error.Contains("not mounted"))
|
|
{
|
|
MainForm.Instance.AddToActionsList($"Unmount error: {umountCmd.Error}");
|
|
return false;
|
|
}
|
|
|
|
await Task.Delay(1000); // Wait for mount to settle
|
|
|
|
SshCommand fsckCmd = ssh.RunCommand($"sudo e2fsck -f -y -v -C 0 {device}");
|
|
if (!string.IsNullOrWhiteSpace(fsckCmd.Error))
|
|
{
|
|
MainForm.Instance.AddToActionsList($"e2fsck error: {fsckCmd.Error}");
|
|
return false;
|
|
}
|
|
|
|
SshCommand resizeFs = ssh.RunCommand($"sudo resize2fs {device}");
|
|
ssh.Disconnect();
|
|
|
|
if (!string.IsNullOrWhiteSpace(resizeFs.Error))
|
|
{
|
|
MainForm.Instance.AddToActionsList($"resize2fs error: {resizeFs.Error}");
|
|
return false;
|
|
}
|
|
|
|
if (resizeFs.ExitStatus == 0)
|
|
return true;
|
|
|
|
MainForm.Instance.AddToActionsList($"resize2fs failed with exit code {resizeFs.ExitStatus}");
|
|
return false;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MainForm.Instance.AddToActionsList($"Error expanding filesystem: {ex.Message}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public static void Sync(string IPAddress)
|
|
{
|
|
try
|
|
{
|
|
using SshClient ssh = new SshClient(IPAddress, SSHUsername, SSHPassword);
|
|
ssh.Connect();
|
|
|
|
SshCommand checkDevice = ssh.RunCommand("sync");
|
|
if (!string.IsNullOrWhiteSpace(checkDevice.Error))
|
|
throw new Exception(checkDevice.Error);
|
|
|
|
if (checkDevice.Result.Trim().Length > 1) // Device not found
|
|
{
|
|
MainForm.Instance.AddToActionsList($"Cannot sync files to disk. Replied: {checkDevice.Result}. DO NOT TURN OFF, GET SUPERVISOR");
|
|
return;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MainForm.Instance.AddToActionsList($"Cannot sync becuase: {ex.Message}. DO NOT TURN OFF, GET SUPERVISOR");
|
|
}
|
|
}
|
|
}
|
|
|
|
public class SSHData
|
|
{
|
|
public string packages { get; set; } = string.Empty;
|
|
public string FilesystemName { get; set; } = string.Empty;
|
|
public string FilesystemSize { get; set; } = string.Empty;
|
|
public bool tailscale { get; set; }
|
|
}
|
|
} |