diff --git a/frontend/scripts/capture-screenshots.mjs b/frontend/scripts/capture-screenshots.mjs
index d66c41f..2e0a4d5 100644
--- a/frontend/scripts/capture-screenshots.mjs
+++ b/frontend/scripts/capture-screenshots.mjs
@@ -91,10 +91,11 @@ for (const theme of ["light", "dark"]) {
await captureDashboards(page, dir)
await captureMedia(page, dir)
await captureRuns(page, dir)
+ await captureWorkers(page, dir)
await context.close()
console.log(
- ` wrote ${dir}/{website-hero,app-login,app-dashboard,app-flows,app-flow-panel,app-panel,app-runs,app-run,app-run-context}.png`,
+ ` wrote ${dir}/{website-hero,app-login,app-dashboard,app-flows,app-flow-panel,app-panel,app-runs,app-run,app-run-context,app-workers}.png`,
)
}
}
@@ -232,6 +233,14 @@ async function captureMedia(page, dir) {
* The flow editor, empty-handed if the instance has no flows yet: seeds one
* with a node so the canvas and the node panel are both worth looking at.
*/
+/** Every machine a node can run on, and the sizes it can ask for. */
+async function captureWorkers(page, dir) {
+ await page.goto(`${APP_URL}/workers`, { waitUntil: "networkidle" })
+ await page.getByText(/^Sizes$/).waitFor({ timeout: 15000 })
+ await page.waitForTimeout(500)
+ await page.screenshot({ path: `${dir}/app-workers.png`, fullPage: true })
+}
+
async function captureFlows(page, dir) {
await page.goto(`${APP_URL}/flows`, { waitUntil: "networkidle" })
diff --git a/frontend/src/client/schemas.gen.ts b/frontend/src/client/schemas.gen.ts
index 7ce7f7b..5e38b81 100644
--- a/frontend/src/client/schemas.gen.ts
+++ b/frontend/src/client/schemas.gen.ts
@@ -722,6 +722,154 @@ export const EventRowSchema = {
title: 'EventRow'
} as const;
+export const FlavorCreateSchema = {
+ properties: {
+ cpus: {
+ type: 'integer',
+ minimum: 1,
+ title: 'Cpus',
+ default: 1
+ },
+ gpus: {
+ type: 'integer',
+ minimum: 0,
+ title: 'Gpus',
+ default: 0
+ },
+ ram: {
+ type: 'integer',
+ minimum: 1,
+ title: 'Ram',
+ default: 2048
+ },
+ description: {
+ type: 'string',
+ maxLength: 255,
+ title: 'Description',
+ default: ''
+ },
+ name: {
+ type: 'string',
+ maxLength: 64,
+ title: 'Name'
+ }
+ },
+ type: 'object',
+ required: ['name'],
+ title: 'FlavorCreate'
+} as const;
+
+export const FlavorPublicSchema = {
+ properties: {
+ cpus: {
+ type: 'integer',
+ minimum: 1,
+ title: 'Cpus',
+ default: 1
+ },
+ gpus: {
+ type: 'integer',
+ minimum: 0,
+ title: 'Gpus',
+ default: 0
+ },
+ ram: {
+ type: 'integer',
+ minimum: 1,
+ title: 'Ram',
+ default: 2048
+ },
+ description: {
+ type: 'string',
+ maxLength: 255,
+ title: 'Description',
+ default: ''
+ },
+ name: {
+ type: 'string',
+ title: 'Name'
+ }
+ },
+ type: 'object',
+ required: ['name'],
+ title: 'FlavorPublic'
+} as const;
+
+export const FlavorUpdateSchema = {
+ properties: {
+ cpus: {
+ anyOf: [
+ {
+ type: 'integer',
+ minimum: 1
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Cpus'
+ },
+ gpus: {
+ anyOf: [
+ {
+ type: 'integer',
+ minimum: 0
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Gpus'
+ },
+ ram: {
+ anyOf: [
+ {
+ type: 'integer',
+ minimum: 1
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Ram'
+ },
+ description: {
+ anyOf: [
+ {
+ type: 'string',
+ maxLength: 255
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Description'
+ }
+ },
+ type: 'object',
+ title: 'FlavorUpdate',
+ description: 'Everything but the name: renaming would orphan the nodes that ask.'
+} as const;
+
+export const FlavorsPublicSchema = {
+ properties: {
+ data: {
+ items: {
+ '$ref': '#/components/schemas/FlavorPublic'
+ },
+ type: 'array',
+ title: 'Data'
+ },
+ count: {
+ type: 'integer',
+ title: 'Count'
+ }
+ },
+ type: 'object',
+ required: ['data', 'count'],
+ title: 'FlavorsPublic'
+} as const;
+
export const FlowDef_InputSchema = {
properties: {
name: {
@@ -2364,6 +2512,22 @@ export const RemoteUserBodySchema = {
title: 'RemoteUserBody'
} as const;
+export const ResourceLevelSchema = {
+ properties: {
+ total: {
+ type: 'integer',
+ title: 'Total'
+ },
+ free: {
+ type: 'integer',
+ title: 'Free'
+ }
+ },
+ type: 'object',
+ required: ['total', 'free'],
+ title: 'ResourceLevel'
+} as const;
+
export const ResourcesSchema = {
properties: {
cpus: {
@@ -2380,6 +2544,44 @@ export const ResourcesSchema = {
description: 'Whole devices held for the whole execution, named to the node through CUDA_VISIBLE_DEVICES. Nothing else is given them while it runs, which is what keeps two preallocating processes apart.',
default: 0
},
+ ram: {
+ anyOf: [
+ {
+ type: 'integer',
+ minimum: 1
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Ram',
+ description: "Megabytes held for the whole execution; accepts '512M' or '2G'. Counted against machines that said how much they have, and ignored by those that did not — which is a machine with nothing to say about memory, not one with none."
+ },
+ flavor: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Flavor',
+ description: 'A stored size by name, standing in for cpus, gpus and ram. Read again every time the node is built, so editing the flavor edits what the next run gets.'
+ },
+ duration_s: {
+ anyOf: [
+ {
+ type: 'integer',
+ minimum: 1
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Duration S',
+ description: "How long one execution is expected to take; accepts '30m' or '2h'. A statement about the node for whoever is planning around it, not a limit — the limit is `timeout`."
+ },
env: {
additionalProperties: {
type: 'string'
@@ -2407,6 +2609,42 @@ arrives. Both are a node saying how much of the machine it takes, which is
what this is.`
} as const;
+export const ResourcesSnapshotSchema = {
+ properties: {
+ cpus: {
+ '$ref': '#/components/schemas/ResourceLevel'
+ },
+ gpus: {
+ '$ref': '#/components/schemas/ResourceLevel'
+ },
+ waiting: {
+ items: {
+ '$ref': '#/components/schemas/WaitingNode'
+ },
+ type: 'array',
+ title: 'Waiting'
+ },
+ targets: {
+ items: {
+ '$ref': '#/components/schemas/TargetResources'
+ },
+ type: 'array',
+ title: 'Targets'
+ },
+ provisioners: {
+ items: {
+ additionalProperties: true,
+ type: 'object'
+ },
+ type: 'array',
+ title: 'Provisioners'
+ }
+ },
+ type: 'object',
+ required: ['cpus', 'gpus'],
+ title: 'ResourcesSnapshot'
+} as const;
+
export const RuleSchema = {
properties: {
events: {
@@ -2877,6 +3115,47 @@ export const SweepEntrySchema = {
title: 'SweepEntry'
} as const;
+export const TargetResourcesSchema = {
+ properties: {
+ target: {
+ type: 'string',
+ title: 'Target'
+ },
+ cpus: {
+ '$ref': '#/components/schemas/ResourceLevel'
+ },
+ gpus: {
+ '$ref': '#/components/schemas/ResourceLevel'
+ },
+ ram_mb: {
+ anyOf: [
+ {
+ '$ref': '#/components/schemas/ResourceLevel'
+ },
+ {
+ type: 'null'
+ }
+ ]
+ },
+ labels: {
+ items: {
+ type: 'string'
+ },
+ type: 'array',
+ title: 'Labels'
+ },
+ in_flight: {
+ type: 'integer',
+ title: 'In Flight',
+ default: 0
+ }
+ },
+ type: 'object',
+ required: ['target', 'cpus', 'gpus'],
+ title: 'TargetResources',
+ description: 'One machine: this engine, or a worker attached to it.'
+} as const;
+
export const TokenSchema = {
properties: {
access_token: {
@@ -3326,6 +3605,26 @@ export const ValidationResultSchema = {
title: 'ValidationResult'
} as const;
+export const WaitingNodeSchema = {
+ properties: {
+ node: {
+ type: 'string',
+ title: 'Node'
+ },
+ reason: {
+ type: 'string',
+ title: 'Reason'
+ },
+ seconds: {
+ type: 'number',
+ title: 'Seconds'
+ }
+ },
+ type: 'object',
+ required: ['node', 'reason', 'seconds'],
+ title: 'WaitingNode'
+} as const;
+
export const WidgetDefSchema = {
properties: {
id: {
@@ -3427,6 +3726,27 @@ export const WorkerInfoSchema = {
type: 'string',
title: 'Venv Digest',
default: ''
+ },
+ cpus: {
+ type: 'integer',
+ title: 'Cpus',
+ default: 1
+ },
+ gpus: {
+ type: 'integer',
+ title: 'Gpus',
+ default: 0
+ },
+ ram_mb: {
+ anyOf: [
+ {
+ type: 'integer'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Ram Mb'
}
},
type: 'object',
diff --git a/frontend/src/client/sdk.gen.ts b/frontend/src/client/sdk.gen.ts
index 68f02c8..0cb2aba 100644
--- a/frontend/src/client/sdk.gen.ts
+++ b/frontend/src/client/sdk.gen.ts
@@ -3,7 +3,7 @@
import type { CancelablePromise } from './core/CancelablePromise';
import { OpenAPI } from './core/OpenAPI';
import { request as __request } from './core/request';
-import type { AlertsReadAlertsConfigResponse, AlertsSaveAlertsConfigData, AlertsSaveAlertsConfigResponse, AlertsTestChannelData, AlertsTestChannelResponse, ArtifactsPutArtifactData, ArtifactsPutArtifactResponse, ArtifactsGetArtifactData, ArtifactsGetArtifactResponse, CloudReadStatusResponse, CloudEnrollData, CloudEnrollResponse, CloudAddRemoteUserData, CloudAddRemoteUserResponse, CloudDisconnectResponse, DashboardsReadDashboardsResponse, DashboardsReadDashboardData, DashboardsReadDashboardResponse, DashboardsCreateDashboardData, DashboardsCreateDashboardResponse, DashboardsSaveDashboardData, DashboardsSaveDashboardResponse, DashboardsDeleteDashboardData, DashboardsDeleteDashboardResponse, DashboardsGenerateResultsDashboardData, DashboardsGenerateResultsDashboardResponse, DashboardsPublishDashboardData, DashboardsPublishDashboardResponse, DashboardsDiscardDashboardDraftData, DashboardsDiscardDashboardDraftResponse, DashboardsRenameDashboardData, DashboardsRenameDashboardResponse, FlowsReadFlowsResponse, FlowsReadNodeTypesResponse, FlowsReadGraphResponse, FlowsReadLibraryResponse, FlowsDeleteSharedNodeData, FlowsDeleteSharedNodeResponse, FlowsReadFlowData, FlowsReadFlowResponse, FlowsSaveFlowData, FlowsSaveFlowResponse, FlowsDeleteFlowData, FlowsDeleteFlowResponse, FlowsPublishFlowData, FlowsPublishFlowResponse, FlowsDiscardDraftData, FlowsDiscardDraftResponse, FlowsRenameFlowData, FlowsRenameFlowResponse, FlowsReadNodeSourceData, FlowsReadNodeSourceResponse, FlowsSaveNodeSourceData, FlowsSaveNodeSourceResponse, FlowsShareNodeData, FlowsShareNodeResponse, FlowsUnshareNodeData, FlowsUnshareNodeResponse, FlowsStartFlowData, FlowsStartFlowResponse, FlowsStopFlowData, FlowsStopFlowResponse, FlowsPauseFlowData, FlowsPauseFlowResponse, FlowsResumeFlowData, FlowsResumeFlowResponse, FlowsStepFlowData, FlowsStepFlowResponse, FlowsValidateFlowData, FlowsValidateFlowResponse, FlowsRunFlowData, FlowsRunFlowResponse, FlowsTriggerNodeData, FlowsTriggerNodeResponse, FlowsCancelNodeData, FlowsCancelNodeResponse, FlowsAcknowledgeNodeErrorData, FlowsAcknowledgeNodeErrorResponse, FlowsReadFlowStateData, FlowsReadFlowStateResponse, FlowsReadMessageHistoryData, FlowsReadMessageHistoryResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, MessagesReadMessagesResponse, MessagesPublishMessageData, MessagesPublishMessageResponse, MessagesReadMessageHistoryData, MessagesReadMessageHistoryResponse, ModulesReadModulesResponse, ModulesApplyModulesData, ModulesApplyModulesResponse, ModulesRefreshModulesResponse, OauthRegisterClientData, OauthRegisterClientResponse, OauthAuthorizeValidateData, OauthAuthorizeValidateResponse, OauthAuthorizeData, OauthAuthorizeResponse, OauthTokenData, OauthTokenResponse, OauthReadClientsResponse, OauthRevokeClientData, OauthRevokeClientResponse, ObservabilityReadSummaryResponse, ObservabilityReadTimeseriesData, ObservabilityReadTimeseriesResponse, ObservabilityReadFlowRollupsData, ObservabilityReadFlowRollupsResponse, ObservabilityReadRunsData, ObservabilityReadRunsResponse, ObservabilityReadEventsData, ObservabilityReadEventsResponse, ObservabilityReadDeadLettersData, ObservabilityReadDeadLettersResponse, PanelsReadPanelsResponse, PanelsSavePanelsData, PanelsSavePanelsResponse, PanelsStartPairingResponse, PanelsPollPairingData, PanelsPollPairingResponse, PanelsPendingDeviceData, PanelsPendingDeviceResponse, PanelsApprovePairingData, PanelsApprovePairingResponse, PanelsUnpairPanelData, PanelsUnpairPanelResponse, PanelsReadPanelData, PanelsReadPanelResponse, PrivateCreateUserData, PrivateCreateUserResponse, RunsCreateRunData, RunsCreateRunResponse, RunsCreateSweepData, RunsCreateSweepResponse, RunsReadRunsData, RunsReadRunsResponse, RunsReadOverviewResponse, RunsReadRunData, RunsReadRunResponse, RunsCancelRunData, RunsCancelRunResponse, RunsReadMetricsData, RunsReadMetricsResponse, RunsCompareMetricData, RunsCompareMetricResponse, SecretsReadSecretsResponse, SecretsSaveSecretData, SecretsSaveSecretResponse, SecretsDeleteSecretData, SecretsDeleteSecretResponse, UsersReadUsersData, UsersReadUsersResponse, UsersCreateUserData, UsersCreateUserResponse, UsersReadUserMeResponse, UsersDeleteUserMeResponse, UsersUpdateUserMeData, UsersUpdateUserMeResponse, UsersUpdatePasswordMeData, UsersUpdatePasswordMeResponse, UsersRegisterUserData, UsersRegisterUserResponse, UsersReadUserByIdData, UsersReadUserByIdResponse, UsersUpdateUserData, UsersUpdateUserResponse, UsersDeleteUserData, UsersDeleteUserResponse, UtilsTestEmailData, UtilsTestEmailResponse, UtilsHealthCheckResponse, UtilsHealthResponse, WorkersReadWorkersResponse, WorkersReadResourcesResponse, WorkersIssueTokenData, WorkersIssueTokenResponse, WorkersReadRuntimeResponse } from './types.gen';
+import type { AlertsReadAlertsConfigResponse, AlertsSaveAlertsConfigData, AlertsSaveAlertsConfigResponse, AlertsTestChannelData, AlertsTestChannelResponse, ArtifactsPutArtifactData, ArtifactsPutArtifactResponse, ArtifactsGetArtifactData, ArtifactsGetArtifactResponse, CloudReadStatusResponse, CloudEnrollData, CloudEnrollResponse, CloudAddRemoteUserData, CloudAddRemoteUserResponse, CloudDisconnectResponse, DashboardsReadDashboardsResponse, DashboardsReadDashboardData, DashboardsReadDashboardResponse, DashboardsCreateDashboardData, DashboardsCreateDashboardResponse, DashboardsSaveDashboardData, DashboardsSaveDashboardResponse, DashboardsDeleteDashboardData, DashboardsDeleteDashboardResponse, DashboardsGenerateResultsDashboardData, DashboardsGenerateResultsDashboardResponse, DashboardsPublishDashboardData, DashboardsPublishDashboardResponse, DashboardsDiscardDashboardDraftData, DashboardsDiscardDashboardDraftResponse, DashboardsRenameDashboardData, DashboardsRenameDashboardResponse, FlavorsReadFlavorsResponse, FlavorsCreateFlavorData, FlavorsCreateFlavorResponse, FlavorsUpdateFlavorData, FlavorsUpdateFlavorResponse, FlavorsDeleteFlavorData, FlavorsDeleteFlavorResponse, FlowsReadFlowsResponse, FlowsReadNodeTypesResponse, FlowsReadGraphResponse, FlowsReadLibraryResponse, FlowsDeleteSharedNodeData, FlowsDeleteSharedNodeResponse, FlowsReadFlowData, FlowsReadFlowResponse, FlowsSaveFlowData, FlowsSaveFlowResponse, FlowsDeleteFlowData, FlowsDeleteFlowResponse, FlowsPublishFlowData, FlowsPublishFlowResponse, FlowsDiscardDraftData, FlowsDiscardDraftResponse, FlowsRenameFlowData, FlowsRenameFlowResponse, FlowsReadNodeSourceData, FlowsReadNodeSourceResponse, FlowsSaveNodeSourceData, FlowsSaveNodeSourceResponse, FlowsShareNodeData, FlowsShareNodeResponse, FlowsUnshareNodeData, FlowsUnshareNodeResponse, FlowsStartFlowData, FlowsStartFlowResponse, FlowsStopFlowData, FlowsStopFlowResponse, FlowsPauseFlowData, FlowsPauseFlowResponse, FlowsResumeFlowData, FlowsResumeFlowResponse, FlowsStepFlowData, FlowsStepFlowResponse, FlowsValidateFlowData, FlowsValidateFlowResponse, FlowsRunFlowData, FlowsRunFlowResponse, FlowsTriggerNodeData, FlowsTriggerNodeResponse, FlowsCancelNodeData, FlowsCancelNodeResponse, FlowsAcknowledgeNodeErrorData, FlowsAcknowledgeNodeErrorResponse, FlowsReadFlowStateData, FlowsReadFlowStateResponse, FlowsReadMessageHistoryData, FlowsReadMessageHistoryResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, MessagesReadMessagesResponse, MessagesPublishMessageData, MessagesPublishMessageResponse, MessagesReadMessageHistoryData, MessagesReadMessageHistoryResponse, ModulesReadModulesResponse, ModulesApplyModulesData, ModulesApplyModulesResponse, ModulesRefreshModulesResponse, OauthRegisterClientData, OauthRegisterClientResponse, OauthAuthorizeValidateData, OauthAuthorizeValidateResponse, OauthAuthorizeData, OauthAuthorizeResponse, OauthTokenData, OauthTokenResponse, OauthReadClientsResponse, OauthRevokeClientData, OauthRevokeClientResponse, ObservabilityReadSummaryResponse, ObservabilityReadTimeseriesData, ObservabilityReadTimeseriesResponse, ObservabilityReadFlowRollupsData, ObservabilityReadFlowRollupsResponse, ObservabilityReadRunsData, ObservabilityReadRunsResponse, ObservabilityReadEventsData, ObservabilityReadEventsResponse, ObservabilityReadDeadLettersData, ObservabilityReadDeadLettersResponse, PanelsReadPanelsResponse, PanelsSavePanelsData, PanelsSavePanelsResponse, PanelsStartPairingResponse, PanelsPollPairingData, PanelsPollPairingResponse, PanelsPendingDeviceData, PanelsPendingDeviceResponse, PanelsApprovePairingData, PanelsApprovePairingResponse, PanelsUnpairPanelData, PanelsUnpairPanelResponse, PanelsReadPanelData, PanelsReadPanelResponse, PrivateCreateUserData, PrivateCreateUserResponse, RunsCreateRunData, RunsCreateRunResponse, RunsCreateSweepData, RunsCreateSweepResponse, RunsReadRunsData, RunsReadRunsResponse, RunsReadOverviewResponse, RunsReadRunData, RunsReadRunResponse, RunsCancelRunData, RunsCancelRunResponse, RunsReadMetricsData, RunsReadMetricsResponse, RunsCompareMetricData, RunsCompareMetricResponse, SecretsReadSecretsResponse, SecretsSaveSecretData, SecretsSaveSecretResponse, SecretsDeleteSecretData, SecretsDeleteSecretResponse, UsersReadUsersData, UsersReadUsersResponse, UsersCreateUserData, UsersCreateUserResponse, UsersReadUserMeResponse, UsersDeleteUserMeResponse, UsersUpdateUserMeData, UsersUpdateUserMeResponse, UsersUpdatePasswordMeData, UsersUpdatePasswordMeResponse, UsersRegisterUserData, UsersRegisterUserResponse, UsersReadUserByIdData, UsersReadUserByIdResponse, UsersUpdateUserData, UsersUpdateUserResponse, UsersDeleteUserData, UsersDeleteUserResponse, UtilsTestEmailData, UtilsTestEmailResponse, UtilsHealthCheckResponse, UtilsHealthResponse, WorkersReadWorkersResponse, WorkersReadResourcesResponse, WorkersIssueTokenData, WorkersIssueTokenResponse, WorkersReadRuntimeResponse } from './types.gen';
export class AlertsService {
/**
@@ -415,6 +415,85 @@ export class DashboardsService {
}
}
+export class FlavorsService {
+ /**
+ * Read Flavors
+ * Every size a node can ask for.
+ * @returns FlavorsPublic Successful Response
+ * @throws ApiError
+ */
+ public static readFlavors(): CancelablePromise
+ {resources
+ ? "Held for the whole execution, and what the node's own libraries are told they may use. The duration is a planning fact, not a limit."
+ : "Nothing held: the node shares the pool with every other node that says nothing."}
+
+ This engine does not account for resources. +
+ ) + } + + return ( ++ {node.node} — {node.reason} ·{" "} + {node.seconds.toFixed(0)}s +
+ ))} ++ {String(provisioner.name)} can start{" "} + {(provisioner.profiles as string[]).join(", ")} + {outstanding.length > 0 + ? ` — asked for ${outstanding + .map((job) => `${job.profile} (job ${job.job})`) + .join(", ")}` + : ""} + {provisioner.last_error + ? ` — last failed: ${String(provisioner.last_error)}` + : ""} +
+ ) + })} +
+ Nothing attached. A machine runs{" "}
+ pip install fluksio-worker and dials
+ in; nodes go to it by label, or because it has room and this engine does
+ not.
+
+ Every machine this engine can run a node on, what is free of each, and + the sizes a node can ask for by name. +
+