Feature/install wizard disk info (#1923)

* update disk res for install wizard (#1914)

* change types to match setup api

Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
This commit is contained in:
Aiden McClelland
2022-11-09 12:27:43 -07:00
parent 67b54ac1eb
commit 4e47960440
16 changed files with 139 additions and 127 deletions

View File

@@ -8,7 +8,6 @@ use helpers::NonDetachingJoinHandle;
use patch_db::{DbHandle, LockReceipt, LockType}; use patch_db::{DbHandle, LockReceipt, LockType};
use tokio::process::Command; use tokio::process::Command;
use crate::config::util::MergeWith;
use crate::context::rpc::RpcContextConfig; use crate::context::rpc::RpcContextConfig;
use crate::db::model::ServerStatus; use crate::db::model::ServerStatus;
use crate::install::PKG_DOCKER_DIR; use crate::install::PKG_DOCKER_DIR;

View File

@@ -12,9 +12,11 @@ use crate::disk::mount::filesystem::bind::Bind;
use crate::disk::mount::filesystem::block_dev::BlockDev; use crate::disk::mount::filesystem::block_dev::BlockDev;
use crate::disk::mount::filesystem::ReadWrite; use crate::disk::mount::filesystem::ReadWrite;
use crate::disk::mount::guard::{MountGuard, TmpMountGuard}; use crate::disk::mount::guard::{MountGuard, TmpMountGuard};
use crate::disk::util::DiskInfo;
use crate::disk::OsPartitionInfo; use crate::disk::OsPartitionInfo;
use crate::util::serde::IoFormat; use crate::util::serde::IoFormat;
use crate::util::{display_none, Invoke}; use crate::util::{display_none, Invoke};
use crate::Error;
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
@@ -24,28 +26,19 @@ pub struct PostInstallConfig {
wifi_interface: Option<String>, wifi_interface: Option<String>,
} }
#[command(subcommands(status, execute, reboot))] #[command(subcommands(disk, execute, reboot))]
pub fn install() -> Result<(), Error> { pub fn install() -> Result<(), Error> {
Ok(()) Ok(())
} }
#[derive(Debug, Deserialize, Serialize)] #[command(subcommands(list))]
#[serde(rename_all = "kebab-case")] pub fn disk() -> Result<(), Error> {
pub struct InstallTarget { Ok(())
logicalname: PathBuf,
embassy_data: bool,
} }
#[command(display(display_none))] #[command(display(display_none))]
pub async fn status() -> Result<Vec<InstallTarget>, Error> { pub async fn list() -> Result<Vec<DiskInfo>, Error> {
let disks = crate::disk::util::list(&Default::default()).await?; crate::disk::util::list(&Default::default()).await
Ok(disks
.into_iter()
.map(|d| InstallTarget {
logicalname: d.logicalname,
embassy_data: d.guid.is_some() || d.partitions.into_iter().any(|p| p.guid.is_some()),
})
.collect())
} }
pub async fn find_wifi_iface() -> Result<Option<String>, Error> { pub async fn find_wifi_iface() -> Result<Option<String>, Error> {

View File

@@ -5,6 +5,7 @@ import { IonicModule } from '@ionic/angular'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { HomePage } from './home.page' import { HomePage } from './home.page'
import { SwiperModule } from 'swiper/angular' import { SwiperModule } from 'swiper/angular'
import { UnitConversionPipesModule } from '@start9labs/shared'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -20,6 +21,7 @@ const routes: Routes = [
IonicModule, IonicModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
SwiperModule, SwiperModule,
UnitConversionPipesModule,
], ],
declarations: [HomePage], declarations: [HomePage],
}) })

View File

@@ -31,50 +31,61 @@
<ion-item <ion-item
*ngFor="let disk of disks" *ngFor="let disk of disks"
button button
detail="true"
(click)="next(disk)" (click)="next(disk)"
> >
<ion-icon <ion-icon
color="dark"
slot="start" slot="start"
name="save-outline" name="save-outline"
size="large"
color="dark"
></ion-icon> ></ion-icon>
<ion-label>{{ disk.logicalname }}</ion-label> <ion-label class="ion-text-wrap">
<h1>
{{ disk.vendor || 'Unknown Vendor' }} - {{ disk.model ||
'Unknown Model' }}
</h1>
<h2>
{{ disk.logicalname }} - {{ disk.capacity | convertBytes
}}
</h2>
</ion-label>
</ion-item> </ion-item>
</ng-template> </ng-template>
<ng-template swiperSlide> <ng-template swiperSlide>
<ion-item <ion-item
*ngIf="selectedDisk?.['embassy-data']" *ngIf="!!selectedDisk?.guid"
button button
(click)="tryInstall(false)" (click)="tryInstall(false)"
> >
<ion-icon <ion-icon
color="dark" color="dark"
slot="start" slot="start"
size="large"
name="medkit-outline" name="medkit-outline"
></ion-icon> ></ion-icon>
<ion-label> <ion-label>
<h2> <h1>
<ion-text color="success">Re-Install embassyOS</ion-text> <ion-text color="success">Re-Install embassyOS</ion-text>
</h2> </h1>
<h4>Will preserve existing embassyOS data</h4> <h2>Will preserve existing embassyOS data</h2>
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item button lines="none" (click)="tryInstall(true)"> <ion-item button lines="none" (click)="tryInstall(true)">
<ion-icon <ion-icon
color="dark" color="dark"
slot="start" slot="start"
size="large"
name="download-outline" name="download-outline"
></ion-icon> ></ion-icon>
<ion-label> <ion-label>
<h2> <h1>
<ion-text <ion-text
[color]="selectedDisk?.['embassy-data'] ? 'danger' : 'success'" [color]="!!selectedDisk?.guid ? 'danger' : 'success'"
>{{ selectedDisk?.['embassy-data'] ? 'Factory Reset' : >{{ !!selectedDisk?.guid ? 'Factory Reset' : 'Install
'Install embassyOS' }}</ion-text embassyOS' }}</ion-text
> >
</h2> </h1>
<h4>Will delete existing data on disk</h4> <h2>Will delete existing data on disk</h2>
</ion-label> </ion-label>
</ion-item> </ion-item>
</ng-template> </ng-template>

View File

@@ -1,7 +1,8 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { AlertController, IonicSlides, LoadingController } from '@ionic/angular' import { AlertController, IonicSlides, LoadingController } from '@ionic/angular'
import { ApiService, Disk } from 'src/app/services/api/api.service' import { ApiService } from 'src/app/services/api/api.service'
import SwiperCore, { Swiper } from 'swiper' import SwiperCore, { Swiper } from 'swiper'
import { DiskInfo } from '@start9labs/shared'
SwiperCore.use([IonicSlides]) SwiperCore.use([IonicSlides])
@@ -12,8 +13,8 @@ SwiperCore.use([IonicSlides])
}) })
export class HomePage { export class HomePage {
swiper?: Swiper swiper?: Swiper
disks: Disk[] = [] disks: DiskInfo[] = []
selectedDisk?: Disk selectedDisk?: DiskInfo
error = '' error = ''
constructor( constructor(
@@ -36,7 +37,7 @@ export class HomePage {
this.swiper = swiper this.swiper = swiper
} }
next(disk: Disk) { next(disk: DiskInfo) {
this.selectedDisk = disk this.selectedDisk = disk
this.swiper?.slideNext(500) this.swiper?.slideNext(500)
} }
@@ -48,13 +49,15 @@ export class HomePage {
async tryInstall(overwrite: boolean) { async tryInstall(overwrite: boolean) {
if (!this.selectedDisk) return if (!this.selectedDisk) return
const { logicalname, 'embassy-data': embassyData } = this.selectedDisk const { logicalname, guid } = this.selectedDisk
if (embassyData && !overwrite) { const hasEmbassyData = !!guid
if (hasEmbassyData && !overwrite) {
return this.install(logicalname, overwrite) return this.install(logicalname, overwrite)
} }
await this.presentAlertDanger(logicalname, embassyData) await this.presentAlertDanger(logicalname, hasEmbassyData)
} }
private async install(logicalname: string, overwrite: boolean) { private async install(logicalname: string, overwrite: boolean) {
@@ -73,10 +76,13 @@ export class HomePage {
} }
} }
private async presentAlertDanger(logicalname: string, embassyData: boolean) { private async presentAlertDanger(
const message = embassyData logicalname: string,
? 'This action will COMPLETELY erase your existing Embassy data' hasEmbassyData: boolean,
: `This action will COMPLETELY erase the disk <b>${logicalname}</b> and install embassyOS!` ) {
const message = hasEmbassyData
? 'This action COMPLETELY erases your existing Embassy data'
: `This action COMPLETELY erases the disk ${logicalname} and installs embassyOS`
const alert = await this.alertCtrl.create({ const alert = await this.alertCtrl.create({
header: 'Warning', header: 'Warning',

View File

@@ -1,14 +1,12 @@
import { DiskInfo } from '@start9labs/shared'
export abstract class ApiService { export abstract class ApiService {
abstract getDisks(): Promise<GetDisksRes> // install.status abstract getDisks(): Promise<GetDisksRes> // install.disk.list
abstract install(params: InstallReq): Promise<void> // install.execute abstract install(params: InstallReq): Promise<void> // install.execute
abstract reboot(): Promise<void> // install.reboot abstract reboot(): Promise<void> // install.reboot
} }
export type GetDisksRes = Disk[] export type GetDisksRes = DiskInfo[]
export type Disk = {
logicalname: string
'embassy-data': boolean
}
export type InstallReq = { export type InstallReq = {
logicalname: string logicalname: string

View File

@@ -13,7 +13,7 @@ export class LiveApiService implements ApiService {
async getDisks(): Promise<GetDisksRes> { async getDisks(): Promise<GetDisksRes> {
return this.rpcRequest({ return this.rpcRequest({
method: 'install.status', method: 'install.disk.list',
params: {}, params: {},
}) })
} }

View File

@@ -8,12 +8,48 @@ export class MockApiService implements ApiService {
await pauseFor(500) await pauseFor(500)
return [ return [
{ {
logicalname: 'abcdefgh', logicalname: 'abcd',
'embassy-data': false, vendor: 'Samsung',
model: 'T5',
partitions: [
{
logicalname: 'pabcd',
label: null,
capacity: 73264762332,
used: null,
'embassy-os': {
version: '0.2.17',
full: true,
'password-hash':
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
'wrapped-key': null,
},
},
],
capacity: 123456789123,
guid: 'uuid-uuid-uuid-uuid',
}, },
{ {
logicalname: '12345678', logicalname: 'dcba',
'embassy-data': true, vendor: 'Crucial',
model: 'MX500',
partitions: [
{
logicalname: 'pbcba',
label: null,
capacity: 73264762332,
used: null,
'embassy-os': {
version: '0.3.1',
full: true,
'password-hash':
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
'wrapped-key': null,
},
},
],
capacity: 123456789123,
guid: null,
}, },
] ]
} }

View File

@@ -4,11 +4,8 @@ import {
LoadingController, LoadingController,
ModalController, ModalController,
} from '@ionic/angular' } from '@ionic/angular'
import { import { ApiService, CifsBackupTarget } from 'src/app/services/api/api.service'
ApiService, import { EmbassyOSDiskInfo } from '@start9labs/shared'
CifsBackupTarget,
EmbassyOSRecoveryInfo,
} from 'src/app/services/api/api.service'
import { PasswordPage } from '../password/password.page' import { PasswordPage } from '../password/password.page'
@Component({ @Component({
@@ -61,7 +58,7 @@ export class CifsModal {
} }
private async presentModalPassword( private async presentModalPassword(
embassyOS: EmbassyOSRecoveryInfo, embassyOS: EmbassyOSDiskInfo,
): Promise<void> { ): Promise<void> {
const target: CifsBackupTarget = { const target: CifsBackupTarget = {
...this.cifs, ...this.cifs,

View File

@@ -8,10 +8,9 @@ import {
import { import {
ApiService, ApiService,
BackupRecoverySource, BackupRecoverySource,
DiskInfo,
DiskRecoverySource, DiskRecoverySource,
} from 'src/app/services/api/api.service' } from 'src/app/services/api/api.service'
import { ErrorToastService } from '@start9labs/shared' import { DiskInfo, ErrorToastService } from '@start9labs/shared'
import { StateService } from 'src/app/services/state.service' import { StateService } from 'src/app/services/state.service'
import { PasswordPage } from '../../modals/password/password.page' import { PasswordPage } from '../../modals/password/password.page'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'

View File

@@ -47,7 +47,7 @@
<h2 class="target-label">Physical Drive</h2> <h2 class="target-label">Physical Drive</h2>
<div class="ion-text-left ion-padding-bottom"> <div class="ion-text-left ion-padding-bottom">
<p> <p>
Restore your Emabssy from a physcial drive that is plugged Restore your Embassy from a physical drive that is plugged
directly into your Embassy. directly into your Embassy.
</p> </p>
<br /> <br />

View File

@@ -1,7 +1,7 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { AlertController, NavController } from '@ionic/angular' import { AlertController, NavController } from '@ionic/angular'
import { ApiService, DiskInfo } from 'src/app/services/api/api.service' import { ApiService } from 'src/app/services/api/api.service'
import { ErrorToastService } from '@start9labs/shared' import { DiskInfo, ErrorToastService } from '@start9labs/shared'
import { StateService } from 'src/app/services/state.service' import { StateService } from 'src/app/services/state.service'
@Component({ @Component({

View File

@@ -1,4 +1,5 @@
import * as jose from 'node-jose' import * as jose from 'node-jose'
import { DiskListResponse, EmbassyOSDiskInfo } from '@start9labs/shared'
export abstract class ApiService { export abstract class ApiService {
pubkey?: jose.JWK.Key pubkey?: jose.JWK.Key
@@ -6,7 +7,7 @@ export abstract class ApiService {
abstract getPubKey(): Promise<void> // setup.get-pubkey abstract getPubKey(): Promise<void> // setup.get-pubkey
abstract getDrives(): Promise<DiskListResponse> // setup.disk.list abstract getDrives(): Promise<DiskListResponse> // setup.disk.list
abstract getRecoveryStatus(): Promise<RecoveryStatusRes> // setup.recovery.status abstract getRecoveryStatus(): Promise<RecoveryStatusRes> // setup.recovery.status
abstract verifyCifs(cifs: CifsRecoverySource): Promise<EmbassyOSRecoveryInfo> // setup.cifs.verify abstract verifyCifs(cifs: CifsRecoverySource): Promise<EmbassyOSDiskInfo> // setup.cifs.verify
abstract importDrive(importInfo: ImportDriveReq): Promise<SetupEmbassyRes> // setup.attach abstract importDrive(importInfo: ImportDriveReq): Promise<SetupEmbassyRes> // setup.attach
abstract setupEmbassy(setupInfo: SetupEmbassyReq): Promise<SetupEmbassyRes> // setup.execute abstract setupEmbassy(setupInfo: SetupEmbassyReq): Promise<SetupEmbassyRes> // setup.execute
abstract setupComplete(): Promise<SetupEmbassyRes> // setup.complete abstract setupComplete(): Promise<SetupEmbassyRes> // setup.complete
@@ -48,15 +49,6 @@ export type SetupEmbassyRes = {
'root-ca': string 'root-ca': string
} }
export type EmbassyOSRecoveryInfo = {
version: string
full: boolean
'password-hash': string | null
'wrapped-key': string | null
}
export type DiskListResponse = DiskInfo[]
export type DiskBackupTarget = { export type DiskBackupTarget = {
vendor: string | null vendor: string | null
model: string | null model: string | null
@@ -64,7 +56,7 @@ export type DiskBackupTarget = {
label: string | null label: string | null
capacity: number capacity: number
used: number | null used: number | null
'embassy-os': EmbassyOSRecoveryInfo | null 'embassy-os': EmbassyOSDiskInfo | null
} }
export type CifsBackupTarget = { export type CifsBackupTarget = {
@@ -72,7 +64,7 @@ export type CifsBackupTarget = {
path: string path: string
username: string username: string
mountable: boolean mountable: boolean
'embassy-os': EmbassyOSRecoveryInfo | null 'embassy-os': EmbassyOSDiskInfo | null
} }
export type DiskRecoverySource = { export type DiskRecoverySource = {
@@ -99,25 +91,8 @@ export type CifsRecoverySource = {
password: Encrypted | null password: Encrypted | null
} }
export type DiskInfo = {
logicalname: string
vendor: string | null
model: string | null
partitions: PartitionInfo[]
capacity: number
guid: string | null // cant back up if guid exists, but needed if migrating
}
export type RecoveryStatusRes = { export type RecoveryStatusRes = {
'bytes-transferred': number 'bytes-transferred': number
'total-bytes': number 'total-bytes': number
complete: boolean complete: boolean
} }
export type PartitionInfo = {
logicalname: string
label: string | null
capacity: number
used: number | null
'embassy-os': EmbassyOSRecoveryInfo | null
}

View File

@@ -1,5 +1,7 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { import {
DiskListResponse,
EmbassyOSDiskInfo,
encodeBase64, encodeBase64,
HttpService, HttpService,
isRpcError, isRpcError,
@@ -9,10 +11,7 @@ import {
import { import {
ApiService, ApiService,
CifsRecoverySource, CifsRecoverySource,
DiskListResponse,
DiskMigrateSource,
DiskRecoverySource, DiskRecoverySource,
EmbassyOSRecoveryInfo,
GetStatusRes, GetStatusRes,
ImportDriveReq, ImportDriveReq,
RecoveryStatusRes, RecoveryStatusRes,
@@ -68,7 +67,7 @@ export class LiveApiService extends ApiService {
async verifyCifs(source: CifsRecoverySource) { async verifyCifs(source: CifsRecoverySource) {
source.path = source.path.replace('/\\/g', '/') source.path = source.path.replace('/\\/g', '/')
return this.rpcRequest<EmbassyOSRecoveryInfo>({ return this.rpcRequest<EmbassyOSDiskInfo>({
method: 'setup.cifs.verify', method: 'setup.cifs.verify',
params: source, params: source,
}) })

View File

@@ -14,3 +14,29 @@ export interface Log {
timestamp: string timestamp: string
message: string message: string
} }
export type DiskListResponse = DiskInfo[]
export interface DiskInfo {
logicalname: string
vendor: string | null
model: string | null
partitions: PartitionInfo[]
capacity: number
guid: string | null
}
export interface PartitionInfo {
logicalname: string
label: string | null
capacity: number
used: number | null
'embassy-os': EmbassyOSDiskInfo | null
}
export type EmbassyOSDiskInfo = {
version: string
full: boolean
'password-hash': string | null
'wrapped-key': string | null
}

View File

@@ -7,7 +7,7 @@ import {
DependencyError, DependencyError,
Manifest, Manifest,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { LogsRes, ServerLogsReq } from '@start9labs/shared' import { EmbassyOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared'
export module RR { export module RR {
// DB // DB
@@ -332,13 +332,6 @@ export type PlatformType =
export type BackupTarget = DiskBackupTarget | CifsBackupTarget export type BackupTarget = DiskBackupTarget | CifsBackupTarget
export interface EmbassyOSRecoveryInfo {
version: string
full: boolean
'password-hash': string | null
'wrapped-key': string | null
}
export interface DiskBackupTarget { export interface DiskBackupTarget {
type: 'disk' type: 'disk'
vendor: string | null vendor: string | null
@@ -347,7 +340,7 @@ export interface DiskBackupTarget {
label: string | null label: string | null
capacity: number capacity: number
used: number | null used: number | null
'embassy-os': EmbassyOSRecoveryInfo | null 'embassy-os': EmbassyOSDiskInfo | null
} }
export interface CifsBackupTarget { export interface CifsBackupTarget {
@@ -356,7 +349,7 @@ export interface CifsBackupTarget {
path: string path: string
username: string username: string
mountable: boolean mountable: boolean
'embassy-os': EmbassyOSRecoveryInfo | null 'embassy-os': EmbassyOSDiskInfo | null
} }
export type RecoverySource = DiskRecoverySource | CifsRecoverySource export type RecoverySource = DiskRecoverySource | CifsRecoverySource
@@ -374,28 +367,6 @@ export interface CifsRecoverySource {
password: string password: string
} }
export interface DiskInfo {
logicalname: string
vendor: string | null
model: string | null
partitions: PartitionInfo[]
capacity: number
guid: string | null
}
export interface PartitionInfo {
logicalname: string
label: string | null
capacity: number
used: number | null
'embassy-os': EmbassyOsDiskInfo | null
}
export interface EmbassyOsDiskInfo {
version: string
full: boolean
}
export interface BackupInfo { export interface BackupInfo {
version: string version: string
timestamp: string timestamp: string