mirror of
https://github.com/nestriness/nestri.git
synced 2026-11-03 22:22:39 +02:00
release: rooms placement fixes (#459)
Ships dev to prod: fleet GPU model matching (vendor-stripped names), box GPU naming per pinned card.
This commit is contained in:
@@ -166,6 +166,24 @@ describe('POST /box on a chosen GPU', () => {
|
||||
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 () => {
|
||||
const f = await fleet([[V710], [V710]]);
|
||||
const a = await person('fleet-model-a');
|
||||
|
||||
@@ -32,7 +32,7 @@ export namespace CheckoutRoom {
|
||||
|
||||
/** Refuse a model the fleet does not have. */
|
||||
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(
|
||||
'validation',
|
||||
ErrorCodes.Validation.INVALID_PARAMETER,
|
||||
@@ -54,9 +54,10 @@ export namespace CheckoutRoom {
|
||||
cards: readonly Placement.Card[]
|
||||
): { model: string; substituted: boolean } | null {
|
||||
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;
|
||||
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 wantedVram = wanted?.vramMib ?? 0;
|
||||
const rank = (c: Placement.Card) => [
|
||||
|
||||
@@ -32,6 +32,24 @@ export namespace Rooms {
|
||||
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 {
|
||||
return plan === 'paid';
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ export namespace Box {
|
||||
.orderBy(BoxTable.timeCreated)
|
||||
.then((rows) =>
|
||||
rows.map((r) =>
|
||||
serialize(r.box, Machine.gpuName(r.gpus), {
|
||||
serialize(r.box, Machine.pinnedName(r.gpus, r.box), {
|
||||
host: r.host,
|
||||
fleet: r.organisationId !== null
|
||||
})
|
||||
|
||||
@@ -338,7 +338,9 @@ export namespace Placement {
|
||||
export const onFleetModel =
|
||||
(model: string): Placer =>
|
||||
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) {
|
||||
throw new VisibleError(
|
||||
'rate_limit',
|
||||
|
||||
@@ -85,6 +85,20 @@ export namespace Machine {
|
||||
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
|
||||
.object({
|
||||
id: z.string().meta({
|
||||
|
||||
Reference in New Issue
Block a user