mirror of
https://github.com/nestriness/nestri.git
synced 2026-11-03 14:12:39 +02:00
fix(rooms): a fleet room is found by the name clients show, and a box is named by its pinned card
POST /box matched the fleet's model by exact string, but GET /location lists
the same models to clients and nesktop shows them without the vendor
("Radeon RX 9060 XT"), so every fleet room it asked for was refused as none
free. Models now match with or without the vendor, in placement and in the
checkout's GPU too.
A listed box's gpu was its host's first card, so a second room on a
multi-GPU host was named after the first. It is the card the box is pinned
to.
This commit is contained in:
@@ -166,6 +166,24 @@ describe('POST /box on a chosen GPU', () => {
|
|||||||
expect(box.gpuPciId).toBeNull();
|
expect(box.gpuPciId).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a fleet room is found by the name a client shows, without the vendor', async () => {
|
||||||
|
await fleet([[V710]]);
|
||||||
|
const p = await person('fleet-short-name', 'paid');
|
||||||
|
const res = await post(p.headers, { on: 'fleet', model: 'Radeon Pro V710' });
|
||||||
|
expect(res.status).toBe(201);
|
||||||
|
expect((await json(res)).data.gpuPciId).toBe(V710.pciId);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a box is named by the card it is pinned to, not its host\'s first card', async () => {
|
||||||
|
const p = await person('pinned-name');
|
||||||
|
const host = await Fixtures.machine(p.owner, 'two-cards');
|
||||||
|
await Machine.setGpus({ id: host, gpus: [BIG, V710] });
|
||||||
|
const res = await post(p.headers, { on: 'own', machineId: host, pciId: V710.pciId });
|
||||||
|
expect(res.status).toBe(201);
|
||||||
|
const listed = (await json(await app.request('/box', { headers: p.headers }))).data;
|
||||||
|
expect(listed.find((b: { machineId: string }) => b.machineId === host).gpu).toBe(V710.model);
|
||||||
|
});
|
||||||
|
|
||||||
test('a fleet room is placed by model on the least-loaded machine with one free', async () => {
|
test('a fleet room is placed by model on the least-loaded machine with one free', async () => {
|
||||||
const f = await fleet([[V710], [V710]]);
|
const f = await fleet([[V710], [V710]]);
|
||||||
const a = await person('fleet-model-a');
|
const a = await person('fleet-model-a');
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export namespace CheckoutRoom {
|
|||||||
|
|
||||||
/** Refuse a model the fleet does not have. */
|
/** Refuse a model the fleet does not have. */
|
||||||
export async function assertKnown(model: string): Promise<void> {
|
export async function assertKnown(model: string): Promise<void> {
|
||||||
if (!(await knownModels()).includes(model)) {
|
if (!(await knownModels()).some((m) => Rooms.sameModel(m, model))) {
|
||||||
throw new VisibleError(
|
throw new VisibleError(
|
||||||
'validation',
|
'validation',
|
||||||
ErrorCodes.Validation.INVALID_PARAMETER,
|
ErrorCodes.Validation.INVALID_PARAMETER,
|
||||||
@@ -54,9 +54,10 @@ export namespace CheckoutRoom {
|
|||||||
cards: readonly Placement.Card[]
|
cards: readonly Placement.Card[]
|
||||||
): { model: string; substituted: boolean } | null {
|
): { model: string; substituted: boolean } | null {
|
||||||
const free = cards.filter((c) => c.free);
|
const free = cards.filter((c) => c.free);
|
||||||
if (free.some((c) => c.model === requested)) return { model: requested, substituted: false };
|
const exact = free.find((c) => Rooms.sameModel(c.model, requested));
|
||||||
|
if (exact) return { model: exact.model, substituted: false };
|
||||||
if (free.length === 0) return null;
|
if (free.length === 0) return null;
|
||||||
const wanted = cards.find((c) => c.model === requested);
|
const wanted = cards.find((c) => Rooms.sameModel(c.model, requested));
|
||||||
const wantedVendor = wanted ? vendor(wanted.pciId) : '';
|
const wantedVendor = wanted ? vendor(wanted.pciId) : '';
|
||||||
const wantedVram = wanted?.vramMib ?? 0;
|
const wantedVram = wanted?.vramMib ?? 0;
|
||||||
const rank = (c: Placement.Card) => [
|
const rank = (c: Placement.Card) => [
|
||||||
|
|||||||
@@ -32,6 +32,24 @@ export namespace Rooms {
|
|||||||
return entryModels().some((m) => name.includes(m.toLowerCase()));
|
return entryModels().some((m) => name.includes(m.toLowerCase()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A card's name without the vendor: clients show `RTX 3060` and
|
||||||
|
* `Radeon RX 9060 XT`, hosts report `NVIDIA GeForce RTX 3060` and
|
||||||
|
* `AMD Radeon RX 9060 XT`.
|
||||||
|
*/
|
||||||
|
export function bareModel(model: string): string {
|
||||||
|
let m = model.trim();
|
||||||
|
for (const prefix of ['NVIDIA ', 'GeForce ', 'AMD ', 'Intel(R) ', 'Intel ']) {
|
||||||
|
if (m.toLowerCase().startsWith(prefix.toLowerCase())) m = m.slice(prefix.length);
|
||||||
|
}
|
||||||
|
return m.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether two names are one card, with or without the vendor. */
|
||||||
|
export function sameModel(a: string, b: string): boolean {
|
||||||
|
return bareModel(a) === bareModel(b);
|
||||||
|
}
|
||||||
|
|
||||||
export function isPro(plan: string | null | undefined): boolean {
|
export function isPro(plan: string | null | undefined): boolean {
|
||||||
return plan === 'paid';
|
return plan === 'paid';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ export namespace Box {
|
|||||||
.orderBy(BoxTable.timeCreated)
|
.orderBy(BoxTable.timeCreated)
|
||||||
.then((rows) =>
|
.then((rows) =>
|
||||||
rows.map((r) =>
|
rows.map((r) =>
|
||||||
serialize(r.box, Machine.gpuName(r.gpus), {
|
serialize(r.box, Machine.pinnedName(r.gpus, r.box), {
|
||||||
host: r.host,
|
host: r.host,
|
||||||
fleet: r.organisationId !== null
|
fleet: r.organisationId !== null
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -338,7 +338,9 @@ export namespace Placement {
|
|||||||
export const onFleetModel =
|
export const onFleetModel =
|
||||||
(model: string): Placer =>
|
(model: string): Placer =>
|
||||||
async () => {
|
async () => {
|
||||||
const free = (await lockedFreeCards(fleetOrganisation())).filter((c) => c.model === model);
|
const free = (await lockedFreeCards(fleetOrganisation())).filter((c) =>
|
||||||
|
Rooms.sameModel(c.model, model)
|
||||||
|
);
|
||||||
if (free.length === 0) {
|
if (free.length === 0) {
|
||||||
throw new VisibleError(
|
throw new VisibleError(
|
||||||
'rate_limit',
|
'rate_limit',
|
||||||
|
|||||||
@@ -85,6 +85,20 @@ export namespace Machine {
|
|||||||
return gpus?.[0]?.model ?? null;
|
return gpus?.[0]?.model ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The card a box is pinned to, by its slot, else its PCI id; the host's
|
||||||
|
* first card for a box that names none.
|
||||||
|
*/
|
||||||
|
export function pinnedName(
|
||||||
|
gpus: readonly { model: string; pciId: string; slot?: string }[] | null | undefined,
|
||||||
|
pin: { gpuPciId?: string | null; gpuSlot?: string | null }
|
||||||
|
): string | null {
|
||||||
|
const card =
|
||||||
|
(pin.gpuSlot ? gpus?.find((g) => g.slot === pin.gpuSlot) : undefined) ??
|
||||||
|
(pin.gpuPciId ? gpus?.find((g) => g.pciId === pin.gpuPciId) : undefined);
|
||||||
|
return card?.model ?? gpuName(gpus);
|
||||||
|
}
|
||||||
|
|
||||||
export const Info = z
|
export const Info = z
|
||||||
.object({
|
.object({
|
||||||
id: z.string().meta({
|
id: z.string().meta({
|
||||||
|
|||||||
Reference in New Issue
Block a user