Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/generated-schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ enum ArrayDiskStatus {
}

enum ArrayDiskType {
BOOT
DATA
PARITY
FLASH
Expand Down
140 changes: 140 additions & 0 deletions api/src/__test__/core/modules/array/get-array-data.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { getArrayData } from '@app/core/modules/array/get-array-data.js';
import { getParityCheckStatus, ParityCheckStatus } from '@app/core/modules/array/parity-check-status.js';
import { store } from '@app/store/index.js';
import { FileLoadStatus } from '@app/store/types.js';
import {
ArrayDisk,
ArrayDiskStatus,
ArrayDiskType,
ArrayState,
} from '@app/unraid-api/graph/resolvers/array/array.model.js';

vi.mock('@app/core/modules/array/parity-check-status.js', () => ({
ParityCheckStatus: {
NEVER_RUN: 'never_run',
},
getParityCheckStatus: vi.fn(),
}));

const buildDisk = (overrides: Partial<ArrayDisk> = {}): ArrayDisk => ({
id: 'disk-id',
idx: 1,
type: ArrayDiskType.DATA,
device: 'sda',
comment: null,
fsFree: 0,
fsSize: 0,
fsUsed: 0,
...overrides,
});

const buildGetState = (disks: ArrayDisk[]) => () =>
({
emhttp: {
status: FileLoadStatus.LOADED,
var: {
mdState: ArrayState.STOPPED,
maxArraysz: 28,
},
disks,
},
}) as unknown as ReturnType<typeof store.getState>;

describe('getArrayData', () => {
beforeEach(() => {
vi.mocked(getParityCheckStatus).mockReturnValue({
status: ParityCheckStatus.NEVER_RUN,
speed: '0',
date: undefined,
duration: 0,
progress: 0,
});
});

it('prefers Boot slots from disks.ini over flash disks', () => {
const result = getArrayData(
buildGetState([
buildDisk({
id: 'flash-disk',
idx: 32,
device: 'sdb',
type: ArrayDiskType.FLASH,
}),
buildDisk({
id: 'boot-disk',
idx: 54,
device: 'nvme0n1',
type: ArrayDiskType.BOOT,
status: ArrayDiskStatus.DISK_OK,
}),
])
);

expect(result.boot?.id).toBe('boot-disk');
});

it('matches webgui and picks the first present Boot disk with a device', () => {
const result = getArrayData(
buildGetState([
buildDisk({
id: 'boot-disk-missing',
idx: 54,
device: '',
type: ArrayDiskType.BOOT,
status: ArrayDiskStatus.DISK_NP,
}),
buildDisk({
id: 'boot-disk-online',
idx: 55,
device: 'nvme0n1',
type: ArrayDiskType.BOOT,
status: ArrayDiskStatus.DISK_OK,
}),
])
);

expect(result.boot?.id).toBe('boot-disk-online');
});

it('falls back to a mounted Boot disk when no present Boot disk has a device', () => {
const result = getArrayData(
buildGetState([
buildDisk({
id: 'boot-disk-mounted',
idx: 54,
device: '',
type: ArrayDiskType.BOOT,
status: ArrayDiskStatus.DISK_NP,
fsStatus: 'Mounted',
}),
buildDisk({
id: 'boot-disk-unmounted',
idx: 55,
device: '',
type: ArrayDiskType.BOOT,
status: ArrayDiskStatus.DISK_NP_DSBL,
fsStatus: 'Unmounted',
}),
])
);

expect(result.boot?.id).toBe('boot-disk-mounted');
});

it('falls back to flash when disks.ini has no Boot slots', () => {
const result = getArrayData(
buildGetState([
buildDisk({
id: 'flash-disk',
idx: 32,
device: 'sdb',
type: ArrayDiskType.FLASH,
}),
])
);

expect(result.boot?.id).toBe('flash-disk');
});
});
45 changes: 44 additions & 1 deletion api/src/__test__/store/state-parsers/slots.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { join } from 'path';

import { expect, test } from 'vitest';

import type { SlotsIni } from '@app/store/state-parsers/slots.js';
import type { IniSlot, SlotsIni } from '@app/store/state-parsers/slots.js';
import { store } from '@app/store/index.js';
import { ArrayDiskType } from '@app/unraid-api/graph/resolvers/array/array.model.js';

test('Returns parsed state file', async () => {
const { parse } = await import('@app/store/state-parsers/slots.js');
Expand Down Expand Up @@ -194,3 +195,45 @@ test('Returns parsed state file', async () => {
]
`);
}, 15000);

test('preserves Boot slots from disks.ini for internal boot selection', async () => {
const { parse } = await import('@app/store/state-parsers/slots.js');

const slot: IniSlot = {
color: 'green-on',
comment: '',
critical: '',
device: '',
exportable: '',
format: '-',
fsColor: 'green-on',
fsFree: '0',
fsSize: '0',
fsStatus: 'Mounted',
fsType: 'vfat',
fsUsed: '0',
id: '',
idx: '54',
luksState: '0',
name: 'boot',
numErrors: '0',
numReads: '0',
numWrites: '0',
rotational: '0',
size: '0',
sizeSb: '0',
slots: '1',
spundown: '0',
status: 'DISK_NP',
temp: '*',
transport: '',
type: 'Boot',
warning: '',
};

const [parsed] = parse({ boot: slot } as unknown as SlotsIni);

expect(parsed?.id).toBe('slot-54-boot');
expect(parsed?.type).toBe(ArrayDiskType.BOOT);
expect(parsed?.fsStatus).toBe('Mounted');
});
39 changes: 34 additions & 5 deletions api/src/core/modules/array/get-array-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,37 @@ import { store } from '@app/store/index.js';
import { FileLoadStatus } from '@app/store/types.js';
import {
ArrayCapacity,
ArrayDisk,
ArrayDiskStatus,
ArrayDiskType,
ArrayState,
UnraidArray,
} from '@app/unraid-api/graph/resolvers/array/array.model.js';

const selectBootDisk = (bootDisks: ArrayDisk[]): ArrayDisk | undefined => {
if (bootDisks.length === 0) {
return undefined;
}

for (const disk of bootDisks) {
if (disk.status === ArrayDiskStatus.DISK_NP_DSBL || disk.status === ArrayDiskStatus.DISK_NP) {
continue;
}
if (!disk.device) {
continue;
}
return disk;
}

for (const disk of bootDisks) {
if (disk.fsStatus === 'Mounted') {
return disk;
}
}

return bootDisks[0];
};

export const getArrayData = (getState = store.getState): UnraidArray => {
// Var state isn't loaded
const state = getState();
Expand All @@ -25,13 +51,16 @@ export const getArrayData = (getState = store.getState): UnraidArray => {
const { emhttp } = state;

// All known disks
const allDisks = emhttp.disks.filter((disk) => disk.device);
const allDisks = emhttp.disks;
const disksWithDevice = allDisks.filter((disk) => disk.device);
const bootDisks = allDisks.filter((disk) => disk.type === ArrayDiskType.BOOT);

// Array boot/parities/disks/caches
const boot = allDisks.find((disk) => disk.type === ArrayDiskType.FLASH);
const parities = allDisks.filter((disk) => disk.type === ArrayDiskType.PARITY);
const disks = allDisks.filter((disk) => disk.type === ArrayDiskType.DATA);
const caches = allDisks.filter((disk) => disk.type === ArrayDiskType.CACHE);
const boot =
selectBootDisk(bootDisks) ?? disksWithDevice.find((disk) => disk.type === ArrayDiskType.FLASH);
const parities = disksWithDevice.filter((disk) => disk.type === ArrayDiskType.PARITY);
const disks = disksWithDevice.filter((disk) => disk.type === ArrayDiskType.DATA);
const caches = disksWithDevice.filter((disk) => disk.type === ArrayDiskType.CACHE);
// Disk sizes
const disksTotalKBytes = sum(disks.map((disk) => disk.fsSize));
const disksFreeKBytes = sum(disks.map((disk) => disk.fsFree));
Expand Down
29 changes: 24 additions & 5 deletions api/src/store/state-parsers/slots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import {
ArrayDiskType,
} from '@app/unraid-api/graph/resolvers/array/array.model.js';

type SlotStatus = 'DISK_OK';
type SlotFsStatus = 'Mounted';
type SlotStatus = keyof typeof ArrayDiskStatus;
type SlotFsStatus = string;
type SlotFsType = 'vfat' | 'ntfs';
type SlotType = 'Flash' | 'Cache' | 'Array' | 'Parity';
type SlotType = 'Flash' | 'Cache' | 'Array' | 'Parity' | 'Boot';
type SlotColor = 'green-on';

export type IniSlot = {
Expand Down Expand Up @@ -47,12 +47,24 @@ export type IniSlot = {

export type SlotsIni = IniSlot[];

const resolveSlotId = (slot: IniSlot): string => {
if (slot.id) {
return slot.id;
}

if (slot.device) {
return slot.device;
}

return `slot-${slot.idx}-${slot.name}`;
};

export const parse: StateFileToIniParserMap['disks'] = (disksIni) =>
Object.values(disksIni)
.filter((slot) => slot.id)
.filter((slot) => Boolean(slot.id || slot.device || slot.type === 'Boot'))
.map((slot) => {
const result: ArrayDisk = {
id: slot.id,
id: resolveSlotId(slot),
device: slot.device,
comment: slot.comment ?? null,
exportable: toBoolean(slot.exportable),
Expand Down Expand Up @@ -85,6 +97,13 @@ export const parse: StateFileToIniParserMap['disks'] = (disksIni) =>
transport: slot.transport ?? null,
isSpinning: slot.spundown ? slot.spundown === '0' : null,
};
Object.defineProperties(result, {
fsStatus: {
value: slot.fsStatus ?? null,
enumerable: false,
writable: true,
},
});
// @TODO Zod Parse This
return result;
});
1 change: 1 addition & 0 deletions api/src/unraid-api/cli/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ export enum ArrayDiskStatus {
}

export enum ArrayDiskType {
BOOT = 'BOOT',
CACHE = 'CACHE',
DATA = 'DATA',
FLASH = 'FLASH',
Expand Down
5 changes: 4 additions & 1 deletion api/src/unraid-api/graph/resolvers/array/array.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export class ArrayDisk extends Node {
exportable?: boolean;

@Field(() => ArrayDiskType, {
description: 'Type of Disk - used to differentiate Cache / Flash / Array / Parity',
description: 'Type of Disk - used to differentiate Boot / Cache / Flash / Array / Parity',
})
type!: ArrayDiskType;

Expand All @@ -129,6 +129,8 @@ export class ArrayDisk extends Node {

@Field(() => Boolean, { nullable: true, description: 'Whether the disk is currently spinning' })
isSpinning?: boolean | null;

fsStatus?: string | null;
Comment thread
pujitm marked this conversation as resolved.
}

@ObjectType({
Expand Down Expand Up @@ -222,6 +224,7 @@ registerEnumType(ArrayDiskStatus, {
export enum ArrayDiskType {
DATA = 'DATA',
PARITY = 'PARITY',
BOOT = 'BOOT',
FLASH = 'FLASH',
CACHE = 'CACHE',
}
Expand Down
1 change: 1 addition & 0 deletions web/src/composables/gql/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ export enum ArrayDiskStatus {
}

export enum ArrayDiskType {
BOOT = 'BOOT',
CACHE = 'CACHE',
DATA = 'DATA',
FLASH = 'FLASH',
Expand Down
Loading