allow mdns cifs shares (#933)

* allow mdns cifs shares

* regex and messaging for cifs

* Update install-wizard.component.ts

* refactor

* always include leading slash when saving path

* add avahi-utils

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
This commit is contained in:
Aiden McClelland
2021-12-14 17:09:00 -07:00
parent 8869361eec
commit 60a48d7af2
16 changed files with 104 additions and 43 deletions

View File

@@ -1,4 +1,4 @@
use std::path::PathBuf; use std::path::{Path, PathBuf};
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
use futures::TryStreamExt; use futures::TryStreamExt;
@@ -47,7 +47,7 @@ pub async fn add(
let guard = TmpMountGuard::mount(&cifs).await?; let guard = TmpMountGuard::mount(&cifs).await?;
let embassy_os = recovery_info(&guard).await?; let embassy_os = recovery_info(&guard).await?;
guard.unmount().await?; guard.unmount().await?;
let path_string = cifs.path.display().to_string(); let path_string = Path::new("/").join(&cifs.path).display().to_string();
let id: u32 = sqlx::query!( let id: u32 = sqlx::query!(
"INSERT INTO cifs_shares (hostname, path, username, password) VALUES (?, ?, ?, ?) RETURNING id AS \"id: u32\"", "INSERT INTO cifs_shares (hostname, path, username, password) VALUES (?, ?, ?, ?) RETURNING id AS \"id: u32\"",
cifs.hostname, cifs.hostname,
@@ -95,7 +95,7 @@ pub async fn update(
let guard = TmpMountGuard::mount(&cifs).await?; let guard = TmpMountGuard::mount(&cifs).await?;
let embassy_os = recovery_info(&guard).await?; let embassy_os = recovery_info(&guard).await?;
guard.unmount().await?; guard.unmount().await?;
let path_string = cifs.path.display().to_string(); let path_string = Path::new("/").join(&cifs.path).display().to_string();
if sqlx::query!( if sqlx::query!(
"UPDATE cifs_shares SET hostname = ?, path = ?, username = ?, password = ? WHERE id = ?", "UPDATE cifs_shares SET hostname = ?, path = ?, username = ?, password = ? WHERE id = ?",
cifs.hostname, cifs.hostname,

View File

@@ -3,6 +3,7 @@ use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use async_trait::async_trait; use async_trait::async_trait;
use color_eyre::eyre::eyre;
use digest::generic_array::GenericArray; use digest::generic_array::GenericArray;
use digest::Digest; use digest::Digest;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -12,6 +13,7 @@ use tracing::instrument;
use super::FileSystem; use super::FileSystem;
use crate::disk::mount::guard::TmpMountGuard; use crate::disk::mount::guard::TmpMountGuard;
use crate::net::mdns::resolve_mdns;
use crate::util::Invoke; use crate::util::Invoke;
use crate::Error; use crate::Error;
@@ -24,16 +26,21 @@ pub async fn mount_cifs(
mountpoint: impl AsRef<Path>, mountpoint: impl AsRef<Path>,
) -> Result<(), Error> { ) -> Result<(), Error> {
tokio::fs::create_dir_all(mountpoint.as_ref()).await?; tokio::fs::create_dir_all(mountpoint.as_ref()).await?;
let ip: IpAddr = String::from_utf8( let ip: IpAddr = if hostname.ends_with(".local") {
Command::new("nmblookup") resolve_mdns(hostname).await?
.arg(hostname) } else {
.invoke(crate::ErrorKind::Network) String::from_utf8(
.await?, Command::new("nmblookup")
)? .arg(hostname)
.split(" ") .invoke(crate::ErrorKind::Network)
.next() .await?,
.unwrap() )?
.parse()?; .split(" ")
.next()
.unwrap()
.trim()
.parse()?
};
let absolute_path = Path::new("/").join(path.as_ref()); let absolute_path = Path::new("/").join(path.as_ref());
Command::new("mount") Command::new("mount")
.arg("-t") .arg("-t")

View File

@@ -1,16 +1,40 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::net::IpAddr;
use avahi_sys::{ use avahi_sys::{
self, avahi_client_errno, avahi_entry_group_add_service, avahi_entry_group_commit, self, avahi_client_errno, avahi_entry_group_add_service, avahi_entry_group_commit,
avahi_entry_group_free, avahi_entry_group_reset, avahi_free, avahi_strerror, AvahiClient, avahi_entry_group_free, avahi_entry_group_reset, avahi_free, avahi_strerror, AvahiClient,
AvahiEntryGroup, AvahiEntryGroup,
}; };
use color_eyre::eyre::eyre;
use libc::c_void; use libc::c_void;
use tokio::process::Command;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use torut::onion::TorSecretKeyV3; use torut::onion::TorSecretKeyV3;
use super::interface::InterfaceId; use super::interface::InterfaceId;
use crate::s9pk::manifest::PackageId; use crate::s9pk::manifest::PackageId;
use crate::util::Invoke;
use crate::Error;
pub async fn resolve_mdns(hostname: &str) -> Result<IpAddr, Error> {
Ok(String::from_utf8(
Command::new("avahi-resolve-host-name")
.arg(hostname)
.invoke(crate::ErrorKind::Network)
.await?,
)?
.split_once("\t")
.ok_or_else(|| {
Error::new(
eyre!("Failed to resolve hostname: {}", hostname),
crate::ErrorKind::Network,
)
})?
.1
.trim()
.parse()?)
}
pub struct MdnsController(Mutex<MdnsControllerInner>); pub struct MdnsController(Mutex<MdnsControllerInner>);
impl MdnsController { impl MdnsController {

View File

@@ -15,6 +15,7 @@ apt-get install -y \
nginx \ nginx \
libavahi-client3 \ libavahi-client3 \
avahi-daemon \ avahi-daemon \
avahi-utils \
iotop \ iotop \
bmon \ bmon \
exfat-utils \ exfat-utils \

View File

@@ -16,11 +16,12 @@
[(ngModel)]="cifs.hostname" [(ngModel)]="cifs.hostname"
name="hostname" name="hostname"
#hostname="ngModel" #hostname="ngModel"
placeholder="e.g. My Computer, Bob's Laptop" placeholder="e.g. 'My Computer' OR 'my-computer.local'"
pattern="^[a-zA-Z0-9._-]+( [a-zA-Z0-9]+)*$"
></ion-input> ></ion-input>
</ion-item> </ion-item>
<p [hidden]="hostname.valid || hostname.pristine"> <p [hidden]="hostname.valid || hostname.pristine">
<ion-text color="danger">Hostname is required</ion-text> <ion-text color="danger">Hostname is required. e.g. 'My Computer' OR 'my-computer.local'</ion-text>
</p> </p>
<p>Path *</p> <p>Path *</p>

View File

@@ -77,8 +77,3 @@ export class CifsModal {
alert.present() alert.present()
} }
} }
interface MappedCifs {
hasValidBackup: boolean
cifs: CifsBackupTarget
}

View File

@@ -24,7 +24,7 @@
</h2> </h2>
<p class="ion-padding-bottom"> <p class="ion-padding-bottom">
Using a shared folder is the recommended way to recover from backup, since it works with all Embassy hardware configurations. Using a shared folder is the recommended way to recover from backup, since it works with all Embassy hardware configurations.
To restore from a shared folder, please follow the <a href="https://docs.start9.com/user-manual/general/backups.html" target="blank" noreferrer>instructions</a>. To restore from a shared folder, please follow the <a href="https://docs.start9.com/user-manual/general/backups.html#shared-network-folder" target="_blank" noreferrer>instructions</a>.
</p> </p>
<!-- connect --> <!-- connect -->
@@ -42,7 +42,7 @@
</h2> </h2>
<p class="ion-padding-bottom"> <p class="ion-padding-bottom">
Warning! Plugging in more than one physical drive to Embassy can lead to power failure and data corruption. Warning! Plugging in more than one physical drive to Embassy can lead to power failure and data corruption.
To restore from a physical drive, please follow the <a href="https://docs.start9.com/user-manual/general/backups.html" target="blank" noreferrer>instructions</a>. To restore from a physical drive, please follow the <a href="https://docs.start9.com/user-manual/general/backups.html#physical-drive" target="_blank" noreferrer>instructions</a>.
</p> </p>
<ng-container *ngFor="let mapped of mappedDrives"> <ng-container *ngFor="let mapped of mappedDrives">

View File

@@ -19,7 +19,7 @@
<ion-item> <ion-item>
<ion-label> <ion-label>
<h2> <h2>
Shared folders are the recommended way to create Embassy backups. Shared folders are the recommended way to create Embassy backups. View the <a href="https://docs.start9.com/user-manual/general/backups.html#shared-network-folder" target="_blank" noreferrer>Instructions</a>
</h2> </h2>
</ion-label> </ion-label>
</ion-item> </ion-item>
@@ -59,7 +59,7 @@
</h2> </h2>
<br /> <br />
<h2> <h2>
To backup to a physical drive, please follow the <a href="https://docs.start9.com/user-manual/general/backups.html" target="blank" noreferrer>instructions</a>. To backup to a physical drive, please follow the <a href="https://docs.start9.com/user-manual/general/backups.html#physical-drive" target="_blank" noreferrer>instructions</a>.
</h2> </h2>
</ion-label> </ion-label>
</ion-item> </ion-item>

View File

@@ -238,8 +238,10 @@ const CifsSpec: ConfigSpec = {
hostname: { hostname: {
type: 'string', type: 'string',
name: 'Hostname', name: 'Hostname',
description: 'The local URL of the shared folder.', description: 'The hostname of your target device on the Local Area Network.',
placeholder: `e.g. My Computer, Bob's Laptop`, placeholder: `e.g. 'My Computer' OR 'my-computer.local'`,
pattern: '^[a-zA-Z0-9._-]+( [a-zA-Z0-9]+)*$',
'pattern-description': `Must be a valid hostname. e.g. 'My Computer' OR 'my-computer.local'`,
nullable: false, nullable: false,
masked: false, masked: false,
copyable: false, copyable: false,
@@ -247,7 +249,7 @@ const CifsSpec: ConfigSpec = {
path: { path: {
type: 'string', type: 'string',
name: 'Path', name: 'Path',
description: 'The path to the shared folder on the target device.', description: 'The directory path to the shared folder on your target device.',
placeholder: 'e.g. /Desktop/my-folder', placeholder: 'e.g. /Desktop/my-folder',
nullable: false, nullable: false,
masked: false, masked: false,

View File

@@ -277,7 +277,8 @@ export class WizardBaker {
whileLoading: { }, whileLoading: { },
afterLoading: { text: 'Cancel' }, afterLoading: { text: 'Cancel' },
}, },
next: 'Stop Service' }, next: 'Stop Service',
},
}, },
{ {
slide: { slide: {

View File

@@ -6,14 +6,16 @@
> >
<span *ngIf= "!installProgress"> <span *ngIf= "!installProgress">
{{ disconnected ? 'Unknown' : rendering.display }} {{ disconnected ? 'Unknown' : rendering.display }}
<span *ngIf="rendering.showDots">...</span> <span *ngIf="rendering.showDots" class="loading-dots"></span>
</span> </span>
<span *ngIf="installProgress"> <span *ngIf="installProgress">
<span *ngIf="installProgress < 99"> <span *ngIf="installProgress < 99">
Installing... {{ installProgress }}% Installing
<span class="loading-dots"></span>{{ installProgress }}%
</span> </span>
<span *ngIf="installProgress >= 99"> <span *ngIf="installProgress >= 99">
Finalizing install. This could take a minute... Finalizing install. This could take a minute
<span class="loading-dots"></span>
</span> </span>
</span> </span>

View File

@@ -109,13 +109,16 @@
<ion-text *ngIf="(pkg.manifest.version | compareEmver : localPkg.manifest.version) === 1" color="warning">Update Available</ion-text> <ion-text *ngIf="(pkg.manifest.version | compareEmver : localPkg.manifest.version) === 1" color="warning">Update Available</ion-text>
</p> </p>
<p *ngIf="[PackageState.Installing, PackageState.Updating] | includes : localPkg.state"> <p *ngIf="[PackageState.Installing, PackageState.Updating] | includes : localPkg.state">
<ion-text color="primary"> <ion-text color="primary" *ngIf="(localPkg['install-progress'] | installState) as progress">
<span>{{ (localPkg['install-progress'] | installState).display }}</span> Installing
<span class="loading-dots"></span>{{ progress.totalProgress < 99 ? progress.totalProgress + '%' : 'finalizing' }}
</ion-text> </ion-text>
</p> </p>
<p *ngIf="localPkg.state === PackageState.Removing"> <p *ngIf="localPkg.state === PackageState.Removing">
<ion-text color="warning">{{ localPkg.state | titlecase }}</ion-text> <ion-text color="danger">
<ion-spinner name="dots" color="warning"></ion-spinner> Removing
<span class="loading-dots"></span>
</ion-text>
</p> </p>
</ng-container> </ng-container>
<ng-template #none> <ng-template #none>

View File

@@ -32,12 +32,17 @@
</p> </p>
<!-- installing, updating --> <!-- installing, updating -->
<p *ngIf="[PackageState.Installing, PackageState.Updating] | includes : localPkg.state"> <p *ngIf="[PackageState.Installing, PackageState.Updating] | includes : localPkg.state">
<ion-text color="primary">{{ (localPkg['install-progress'] | installState).display }}</ion-text> <ion-text color="primary" *ngIf="(localPkg['install-progress'] | installState) as progress">
Installing
<span class="loading-dots"></span>{{ progress.totalProgress < 99 ? progress.totalProgress + '%' : 'finalizing' }}
</ion-text>
</p> </p>
<!-- removing --> <!-- removing -->
<p *ngIf="localPkg.state === PackageState.Removing"> <p *ngIf="localPkg.state === PackageState.Removing">
<ion-text color="warning">{{ localPkg.state | titlecase }}</ion-text> <ion-text color="danger">
<ion-spinner name="dots" color="warning"></ion-spinner> Removing
<span class="loading-dots"></span>
</ion-text>
</p> </p>
</ng-template> </ng-template>
</div> </div>

View File

@@ -119,9 +119,6 @@ const sshSpec = {
name: 'SSH Key', name: 'SSH Key',
description: 'Enter the SSH public key of you would like to authorize for root access to your Embassy.', description: 'Enter the SSH public key of you would like to authorize for root access to your Embassy.',
nullable: false, nullable: false,
// @TODO regex for SSH Key
// pattern: '',
'pattern-description': 'Must be a valid SSH key',
masked: false, masked: false,
copyable: false, copyable: false,
} }

View File

@@ -40,7 +40,6 @@ export function packageLoadingProgress (
validateProgress: Math.floor((100 * validated) / size), validateProgress: Math.floor((100 * validated) / size),
unpackProgress: Math.floor((100 * unpacked) / size), unpackProgress: Math.floor((100 * unpacked) / size),
isComplete: downloadComplete && validationComplete && unpackComplete, isComplete: downloadComplete && validationComplete && unpackComplete,
display: totalProgress > 98 ? 'Finalizing...' : `Installing... ${totalProgress}%`,
} }
} }
@@ -50,5 +49,4 @@ export interface ProgressData {
validateProgress: number validateProgress: number
unpackProgress: number unpackProgress: number
isComplete: boolean isComplete: boolean
display: string
} }

View File

@@ -311,3 +311,28 @@ ion-loading {
h2 { h2 {
line-height: unset; line-height: unset;
} }
.loading-dots:after {
content: "...";
overflow: hidden;
display: inline-block;
vertical-align: bottom;
animation: ellipsis-dot 1s infinite .3s;
animation-fill-mode: forwards;
width: 1em;
}
@keyframes ellipsis-dot {
25% {
content: "";
}
50% {
content: ".";
}
75% {
content: "..";
}
100% {
content: "...";
}
}